diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 53dcf84..a249707 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -7,6 +7,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" "git.huangwc.com/pig/pig-farm-controller/internal/app/service" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "github.com/labstack/echo/v4" ) @@ -52,7 +53,7 @@ func (c *Controller) CreatePlan(ctx echo.Context) error { if err != nil { c.logger.Errorf("%s: 服务层创建计划失败: %v", actionType, err) // 根据服务层返回的错误类型,转换为相应的HTTP状态码 - if errors.Is(err, service.ErrPlanNotFound) { + if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划数据校验失败或关联计划不存在", req) } return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "服务层创建计划失败", req) @@ -86,7 +87,7 @@ func (c *Controller) GetPlan(ctx echo.Context) error { resp, err := c.planService.GetPlanByID(uint(id)) if err != nil { c.logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id) - if errors.Is(err, service.ErrPlanNotFound) { + if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) } return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: "+err.Error(), actionType, "服务层获取计划详情失败", id) @@ -157,9 +158,9 @@ func (c *Controller) UpdatePlan(ctx echo.Context) error { resp, err := c.planService.UpdatePlan(uint(id), &req) if err != nil { c.logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id) - if errors.Is(err, service.ErrPlanNotFound) { + if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) - } else if errors.Is(err, service.ErrPlanCannotBeModified) { + } else if errors.Is(err, plan.ErrPlanCannotBeModified) { // 修改为 plan.ErrPlanCannotBeModified return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许修改", id) } return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "服务层更新计划失败", req) @@ -193,9 +194,9 @@ func (c *Controller) DeletePlan(ctx echo.Context) error { err = c.planService.DeletePlan(uint(id)) if err != nil { c.logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id) - if errors.Is(err, service.ErrPlanNotFound) { + if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) - } else if errors.Is(err, service.ErrPlanCannotBeDeleted) { + } else if errors.Is(err, plan.ErrPlanCannotBeDeleted) { // 修改为 plan.ErrPlanCannotBeDeleted return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许删除", id) } return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划失败: "+err.Error(), actionType, "服务层删除计划失败", id) @@ -229,11 +230,11 @@ func (c *Controller) StartPlan(ctx echo.Context) error { err = c.planService.StartPlan(uint(id)) if err != nil { c.logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id) - if errors.Is(err, service.ErrPlanNotFound) { + if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) - } else if errors.Is(err, service.ErrPlanCannotBeStarted) { + } else if errors.Is(err, plan.ErrPlanCannotBeStarted) { // 修改为 plan.ErrPlanCannotBeStarted return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许手动启动", id) - } else if errors.Is(err, service.ErrPlanAlreadyEnabled) { + } else if errors.Is(err, plan.ErrPlanAlreadyEnabled) { // 修改为 plan.ErrPlanAlreadyEnabled return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "计划已处于启动状态", id) } return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "启动计划失败: "+err.Error(), actionType, "服务层启动计划失败", id) @@ -267,11 +268,11 @@ func (c *Controller) StopPlan(ctx echo.Context) error { err = c.planService.StopPlan(uint(id)) if err != nil { c.logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id) - if errors.Is(err, service.ErrPlanNotFound) { + if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) - } else if errors.Is(err, service.ErrPlanCannotBeStopped) { + } else if errors.Is(err, plan.ErrPlanCannotBeStopped) { // 修改为 plan.ErrPlanCannotBeStopped return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许停止", id) - } else if errors.Is(err, service.ErrPlanNotEnabled) { + } else if errors.Is(err, plan.ErrPlanNotEnabled) { // 修改为 plan.ErrPlanNotEnabled return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "计划未启用", id) } return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划失败: "+err.Error(), actionType, "服务层停止计划失败", id) diff --git a/internal/app/dto/plan_converter.go b/internal/app/dto/plan_converter.go index 7763ecf..adc018c 100644 --- a/internal/app/dto/plan_converter.go +++ b/internal/app/dto/plan_converter.go @@ -53,7 +53,7 @@ func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) { return response, nil } -// NewPlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证 +// NewPlanFromCreateRequest 将CreatePlanRequest转换为Plan模型 func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { if req == nil { return nil, nil @@ -75,7 +75,7 @@ func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { for i, childPlanID := range subPlanSlice { plan.SubPlans[i] = models.SubPlan{ ChildPlanID: childPlanID, - ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认 + ExecutionOrder: i, // 默认执行顺序 } } } @@ -93,19 +93,10 @@ func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { } } - // 1. 首先,执行重复性验证 - if err := plan.ValidateExecutionOrder(); err != nil { - // 如果检测到重复,立即返回错误 - return nil, err - } - - // 2. 然后,调用方法来修复顺序断层 - plan.ReorderSteps() - return plan, nil } -// NewPlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证 +// NewPlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型 func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { if req == nil { return nil, nil @@ -127,7 +118,7 @@ func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { for i, childPlanID := range subPlanSlice { plan.SubPlans[i] = models.SubPlan{ ChildPlanID: childPlanID, - ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认 + ExecutionOrder: i, // 默认执行顺序 } } } @@ -145,15 +136,6 @@ func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { } } - // 1. 首先,执行重复性验证 - if err := plan.ValidateExecutionOrder(); err != nil { - // 如果检测到重复,立即返回错误 - return nil, err - } - - // 2. 然后,调用方法来修复顺序断层 - plan.ReorderSteps() - return plan, nil } diff --git a/internal/app/service/plan_service.go b/internal/app/service/plan_service.go index de3c123..5678fe0 100644 --- a/internal/app/service/plan_service.go +++ b/internal/app/service/plan_service.go @@ -6,26 +6,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" - "gorm.io/gorm" -) - -var ( - // ErrPlanNotFound 表示未找到计划 - ErrPlanNotFound = errors.New("计划不存在") - // ErrPlanCannotBeModified 表示计划不允许修改 - ErrPlanCannotBeModified = errors.New("系统计划不允许修改") - // ErrPlanCannotBeDeleted 表示计划不允许删除 - ErrPlanCannotBeDeleted = errors.New("系统计划不允许删除") - // ErrPlanCannotBeStarted 表示计划不允许手动启动 - ErrPlanCannotBeStarted = errors.New("系统计划不允许手动启动") - // ErrPlanAlreadyEnabled 表示计划已处于启动状态 - ErrPlanAlreadyEnabled = errors.New("计划已处于启动状态,无需重复操作") - // ErrPlanNotEnabled 表示计划未处于启动状态 - ErrPlanNotEnabled = errors.New("计划当前不是启用状态") - // ErrPlanCannotBeStopped 表示计划不允许停止 - ErrPlanCannotBeStopped = errors.New("系统计划不允许停止") ) // PlanService 定义了计划相关的应用服务接口 @@ -48,84 +29,62 @@ type PlanService interface { // planService 是 PlanService 接口的实现 type planService struct { - logger *logs.Logger - planRepo repository.PlanRepository - analysisPlanTaskManager plan.AnalysisPlanTaskManager + logger *logs.Logger + domainPlanService plan.Service // 替换为领域层的服务接口 } // NewPlanService 创建一个新的 PlanService 实例 func NewPlanService( logger *logs.Logger, - planRepo repository.PlanRepository, - analysisPlanTaskManager plan.AnalysisPlanTaskManager, + domainPlanService plan.Service, // 接收领域层服务 ) PlanService { return &planService{ - logger: logger, - planRepo: planRepo, - analysisPlanTaskManager: analysisPlanTaskManager, + logger: logger, + domainPlanService: domainPlanService, // 注入领域层服务 } } // CreatePlan 创建一个新的计划 func (s *planService) CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error) { - const actionType = "服务层:创建计划" + const actionType = "应用服务层:创建计划" - // 使用已有的转换函数,它已经包含了验证和重排逻辑 + // 使用 DTO 转换函数将请求转换为领域实体 planToCreate, err := dto.NewPlanFromCreateRequest(req) if err != nil { s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) return nil, err } - // --- 业务规则处理 --- - // 1. 设置计划类型:用户创建的计划永远是自定义计划 - planToCreate.PlanType = models.PlanTypeCustom - - // 2. 自动判断 ContentType - if len(req.SubPlanIDs) > 0 { - planToCreate.ContentType = models.PlanContentTypeSubPlans - } else { - // 如果 SubPlanIDs 未提供,则默认为 Tasks 类型(即使 Tasks 字段也未提供) - planToCreate.ContentType = models.PlanContentTypeTasks - } - - // 调用仓库方法创建计划 - if err := s.planRepo.CreatePlan(planToCreate); err != nil { - s.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err) - return nil, err - } - - // 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列 - if err := s.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil { - // 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功 - s.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err) - } - - // 使用已有的转换函数将创建后的模型转换为响应对象 - resp, err := dto.NewPlanToResponse(planToCreate) + // 调用领域服务创建计划 + createdPlan, err := s.domainPlanService.CreatePlan(planToCreate) if err != nil { - s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, planToCreate) + s.logger.Errorf("%s: 领域服务创建计划失败: %v", actionType, err) + return nil, err // 直接返回领域层错误 + } + + // 将领域实体转换为响应 DTO + resp, err := dto.NewPlanToResponse(createdPlan) + if err != nil { + s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, createdPlan) return nil, errors.New("计划创建成功,但响应生成失败") } - s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID) + s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, createdPlan.ID) return resp, nil } // GetPlanByID 根据ID获取计划详情 func (s *planService) GetPlanByID(id uint) (*dto.PlanResponse, error) { - const actionType = "服务层:获取计划详情" + const actionType = "应用服务层:获取计划详情" - plan, err := s.planRepo.GetPlanByID(id) + // 调用领域服务获取计划 + plan, err := s.domainPlanService.GetPlanByID(id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) - return nil, ErrPlanNotFound - } - s.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id) - return nil, err + s.logger.Errorf("%s: 领域服务获取计划详情失败: %v, ID: %d", actionType, err, id) + return nil, err // 直接返回领域层错误 } + // 将领域实体转换为响应 DTO resp, err := dto.NewPlanToResponse(plan) if err != nil { s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan) @@ -138,15 +97,19 @@ func (s *planService) GetPlanByID(id uint) (*dto.PlanResponse, error) { // ListPlans 获取计划列表,支持过滤和分页 func (s *planService) ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error) { - const actionType = "服务层:获取计划列表" + const actionType = "应用服务层:获取计划列表" + // 将 DTO 查询参数转换为领域层可接受的选项 opts := repository.ListPlansOptions{PlanType: query.PlanType} - plans, total, err := s.planRepo.ListPlans(opts, query.Page, query.PageSize) + + // 调用领域服务获取计划列表 + plans, total, err := s.domainPlanService.ListPlans(opts, query.Page, query.PageSize) if err != nil { - s.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) - return nil, err + s.logger.Errorf("%s: 领域服务获取计划列表失败: %v", actionType, err) + return nil, err // 直接返回领域层错误 } + // 将领域实体列表转换为响应 DTO 列表 planResponses := make([]dto.PlanResponse, 0, len(plans)) for _, p := range plans { resp, err := dto.NewPlanToResponse(&p) @@ -168,23 +131,9 @@ func (s *planService) ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansRespon // UpdatePlan 更新计划 func (s *planService) UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error) { - const actionType = "服务层:更新计划" - - existingPlan, err := s.planRepo.GetBasicPlanByID(id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) - return nil, ErrPlanNotFound - } - s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) - return nil, err - } - - if existingPlan.PlanType == models.PlanTypeSystem { - s.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, id) - return nil, ErrPlanCannotBeModified - } + const actionType = "应用服务层:更新计划" + // 使用 DTO 转换函数将请求转换为领域实体 planToUpdate, err := dto.NewPlanFromUpdateRequest(req) if err != nil { s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) @@ -192,31 +141,14 @@ func (s *planService) UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.Plan } planToUpdate.ID = id // 确保ID被设置 - if len(req.SubPlanIDs) > 0 { - planToUpdate.ContentType = models.PlanContentTypeSubPlans - } else { - planToUpdate.ContentType = models.PlanContentTypeTasks - } - - // 只要是更新任务,就重置执行计数器 - planToUpdate.ExecuteCount = 0 - s.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID) - - if err := s.planRepo.UpdatePlanMetadataAndStructure(planToUpdate); err != nil { - s.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate) - return nil, err - } - - if err := s.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil { - s.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err) - } - - updatedPlan, err := s.planRepo.GetPlanByID(id) + // 调用领域服务更新计划 + updatedPlan, err := s.domainPlanService.UpdatePlan(planToUpdate) if err != nil { - s.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, id) - return nil, errors.New("获取更新后计划详情时发生内部错误") + s.logger.Errorf("%s: 领域服务更新计划失败: %v, ID: %d", actionType, err, id) + return nil, err // 直接返回领域层错误 } + // 将领域实体转换为响应 DTO resp, err := dto.NewPlanToResponse(updatedPlan) if err != nil { s.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan) @@ -229,33 +161,13 @@ func (s *planService) UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.Plan // DeletePlan 删除计划(软删除) func (s *planService) DeletePlan(id uint) error { - const actionType = "服务层:删除计划" + const actionType = "应用服务层:删除计划" - plan, err := s.planRepo.GetBasicPlanByID(id) + // 调用领域服务删除计划 + err := s.domainPlanService.DeletePlan(id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) - return ErrPlanNotFound - } - s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) - return err - } - - if plan.PlanType == models.PlanTypeSystem { - s.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id) - return ErrPlanCannotBeDeleted - } - - if plan.Status == models.PlanStatusEnabled { - if err := s.planRepo.StopPlanTransactionally(id); err != nil { - s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id) - return err - } - } - - if err := s.planRepo.DeletePlan(id); err != nil { - s.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id) - return err + s.logger.Errorf("%s: 领域服务删除计划失败: %v, ID: %d", actionType, err, id) + return err // 直接返回领域层错误 } s.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id) @@ -264,46 +176,13 @@ func (s *planService) DeletePlan(id uint) error { // StartPlan 启动计划 func (s *planService) StartPlan(id uint) error { - const actionType = "服务层:启动计划" + const actionType = "应用服务层:启动计划" - plan, err := s.planRepo.GetBasicPlanByID(id) + // 调用领域服务启动计划 + err := s.domainPlanService.StartPlan(id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) - return ErrPlanNotFound - } - s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) - return err - } - - if plan.PlanType == models.PlanTypeSystem { - s.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id) - return ErrPlanCannotBeStarted - } - if plan.Status == models.PlanStatusEnabled { - s.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id) - return ErrPlanAlreadyEnabled - } - - if plan.Status != models.PlanStatusEnabled { - if plan.ExecuteCount > 0 { - if err := s.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil { - s.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID) - return err - } - s.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID) - } - - if err := s.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil { - s.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID) - return err - } - s.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID) - } - - if err := s.analysisPlanTaskManager.CreateOrUpdateTrigger(plan.ID); err != nil { - s.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID) - return err + s.logger.Errorf("%s: 领域服务启动计划失败: %v, ID: %d", actionType, err, id) + return err // 直接返回领域层错误 } s.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id) @@ -312,31 +191,13 @@ func (s *planService) StartPlan(id uint) error { // StopPlan 停止计划 func (s *planService) StopPlan(id uint) error { - const actionType = "服务层:停止计划" + const actionType = "应用服务层:停止计划" - plan, err := s.planRepo.GetBasicPlanByID(id) + // 调用领域服务停止计划 + err := s.domainPlanService.StopPlan(id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) - return ErrPlanNotFound - } - s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) - return err - } - - if plan.PlanType == models.PlanTypeSystem { - s.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id) - return ErrPlanCannotBeStopped - } - - if plan.Status != models.PlanStatusEnabled { - s.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status) - return ErrPlanNotEnabled - } - - if err := s.planRepo.StopPlanTransactionally(id); err != nil { - s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id) - return err + s.logger.Errorf("%s: 领域服务停止计划失败: %v, ID: %d", actionType, err, id) + return err // 直接返回领域层错误 } s.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index 4907e70..2489d18 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -170,7 +170,7 @@ func initDomainServices(cfg *config.Config, infra *Infrastructure, logger *logs. ) // 计划管理器 - planService := plan.NewPlanService(planExecutionManager, analysisPlanTaskManager, logger) + planService := plan.NewPlanService(planExecutionManager, analysisPlanTaskManager, infra.repos.planRepo, logger) return &DomainServices{ pigPenTransferManager: pigPenTransferManager, @@ -223,7 +223,7 @@ func initAppServices(infra *Infrastructure, domainServices *DomainServices, logg domainServices.generalDeviceService, ) auditService := audit.NewService(infra.repos.userActionLogRepo, logger) - planService := service.NewPlanService(logger, infra.repos.planRepo, domainServices.analysisPlanTaskManager) + planService := service.NewPlanService(logger, domainServices.planService) userService := service.NewUserService(infra.repos.userRepo, infra.tokenService, infra.notifyService, logger) return &AppServices{ diff --git a/internal/domain/plan/plan_service.go b/internal/domain/plan/plan_service.go index 5dc108f..eeae571 100644 --- a/internal/domain/plan/plan_service.go +++ b/internal/domain/plan/plan_service.go @@ -1,7 +1,29 @@ package plan import ( + "errors" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" + "gorm.io/gorm" +) + +var ( + // ErrPlanNotFound 表示未找到计划 + ErrPlanNotFound = errors.New("计划不存在") + // ErrPlanCannotBeModified 表示计划不允许修改 + ErrPlanCannotBeModified = errors.New("系统计划不允许修改") + // ErrPlanCannotBeDeleted 表示计划不允许删除 + ErrPlanCannotBeDeleted = errors.New("系统计划不允许删除") + // ErrPlanCannotBeStarted 表示计划不允许手动启动 + ErrPlanCannotBeStarted = errors.New("系统计划不允许手动启动") + // ErrPlanAlreadyEnabled 表示计划已处于启动状态 + ErrPlanAlreadyEnabled = errors.New("计划已处于启动状态,无需重复操作") + // ErrPlanNotEnabled 表示计划未处于启动状态 + ErrPlanNotEnabled = errors.New("计划当前不是启用状态") + // ErrPlanCannotBeStopped 表示计划不允许停止 + ErrPlanCannotBeStopped = errors.New("系统计划不允许停止") ) // Service 定义了计划领域服务的接口。 @@ -12,26 +34,46 @@ type Service interface { Stop() // RefreshPlanTriggers 刷新计划触发器,同步数据库中的计划状态和待执行队列中的触发器任务。 RefreshPlanTriggers() error - // TODO: 在这里添加其他与计划相关的领域服务方法 + + // CreatePlan 创建一个新的计划 + CreatePlan(plan *models.Plan) (*models.Plan, error) + // GetPlanByID 根据ID获取计划详情 + GetPlanByID(id uint) (*models.Plan, error) + // ListPlans 获取计划列表,支持过滤和分页 + ListPlans(opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) + // UpdatePlan 更新计划 + UpdatePlan(plan *models.Plan) (*models.Plan, error) + // DeletePlan 删除计划(软删除) + DeletePlan(id uint) error + // StartPlan 启动计划 + StartPlan(id uint) error + // StopPlan 停止计划 + StopPlan(id uint) error } // planServiceImpl 是 Service 接口的具体实现。 type planServiceImpl struct { executionManager ExecutionManager taskManager AnalysisPlanTaskManager - logger *logs.Logger + planRepo repository.PlanRepository // 新增 + // deviceRepo repository.DeviceRepository // 如果需要,新增 + logger *logs.Logger } // NewPlanService 创建一个新的 Service 实例。 func NewPlanService( executionManager ExecutionManager, taskManager AnalysisPlanTaskManager, + planRepo repository.PlanRepository, // 新增 + // deviceRepo repository.DeviceRepository, // 如果需要,新增 logger *logs.Logger, ) Service { return &planServiceImpl{ executionManager: executionManager, taskManager: taskManager, - logger: logger, + planRepo: planRepo, // 注入 + // deviceRepo: deviceRepo, // 注入 + logger: logger, } } @@ -52,3 +94,257 @@ func (s *planServiceImpl) RefreshPlanTriggers() error { s.logger.Infof("PlanService 正在刷新计划触发器...") return s.taskManager.Refresh() } + +// CreatePlan 创建一个新的计划 +func (s *planServiceImpl) CreatePlan(planToCreate *models.Plan) (*models.Plan, error) { + const actionType = "领域层:创建计划" + + // 1. 业务规则处理 + // 用户创建的计划永远是自定义计划 + planToCreate.PlanType = models.PlanTypeCustom + + // 自动判断 ContentType + if len(planToCreate.SubPlans) > 0 { + planToCreate.ContentType = models.PlanContentTypeSubPlans + } else { + planToCreate.ContentType = models.PlanContentTypeTasks + } + + // 2. 验证和重排顺序 (领域逻辑) + if err := planToCreate.ValidateExecutionOrder(); err != nil { + s.logger.Errorf("%s: 计划 (ID: %d) 的执行顺序无效: %v", actionType, planToCreate.ID, err) + return nil, err + } + planToCreate.ReorderSteps() + + // 3. 调用仓库方法创建计划 + if err := s.planRepo.CreatePlan(planToCreate); err != nil { + s.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err) + return nil, err + } + + // 4. 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列 + if err := s.taskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil { + // 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功 + s.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err) + } + + s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID) + return planToCreate, nil +} + +// GetPlanByID 根据ID获取计划详情 +func (s *planServiceImpl) GetPlanByID(id uint) (*models.Plan, error) { + const actionType = "领域层:获取计划详情" + + plan, err := s.planRepo.GetPlanByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) + return nil, ErrPlanNotFound + } + s.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id) + return nil, err + } + + s.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id) + return plan, nil +} + +// ListPlans 获取计划列表,支持过滤和分页 +func (s *planServiceImpl) ListPlans(opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) { + const actionType = "领域层:获取计划列表" + + plans, total, err := s.planRepo.ListPlans(opts, page, pageSize) + if err != nil { + s.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) + return nil, 0, err + } + + s.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(plans)) + return plans, total, nil +} + +// UpdatePlan 更新计划 +func (s *planServiceImpl) UpdatePlan(planToUpdate *models.Plan) (*models.Plan, error) { + const actionType = "领域层:更新计划" + + existingPlan, err := s.planRepo.GetBasicPlanByID(planToUpdate.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, planToUpdate.ID) + return nil, ErrPlanNotFound + } + s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, planToUpdate.ID) + return nil, err + } + + // 系统计划不允许修改 + if existingPlan.PlanType == models.PlanTypeSystem { + s.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, planToUpdate.ID) + return nil, ErrPlanCannotBeModified + } + + // 自动判断 ContentType + if len(planToUpdate.SubPlans) > 0 { + planToUpdate.ContentType = models.PlanContentTypeSubPlans + } else { + planToUpdate.ContentType = models.PlanContentTypeTasks + } + + // 验证和重排顺序 (领域逻辑) + if err := planToUpdate.ValidateExecutionOrder(); err != nil { + s.logger.Errorf("%s: 计划 (ID: %d) 的执行顺序无效: %v", actionType, planToUpdate.ID, err) + return nil, err + } + planToUpdate.ReorderSteps() + + // 只要是更新任务,就重置执行计数器 + planToUpdate.ExecuteCount = 0 + s.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID) + + if err := s.planRepo.UpdatePlanMetadataAndStructure(planToUpdate); err != nil { + s.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate) + return nil, err + } + + if err := s.taskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil { + s.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err) + } + + updatedPlan, err := s.planRepo.GetPlanByID(planToUpdate.ID) + if err != nil { + s.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, planToUpdate.ID) + return nil, errors.New("获取更新后计划详情时发生内部错误") + } + + s.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID) + return updatedPlan, nil +} + +// DeletePlan 删除计划(软删除) +func (s *planServiceImpl) DeletePlan(id uint) error { + const actionType = "领域层:删除计划" + + plan, err := s.planRepo.GetBasicPlanByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) + return ErrPlanNotFound + } + s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) + return err + } + + // 系统计划不允许删除 + if plan.PlanType == models.PlanTypeSystem { + s.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id) + return ErrPlanCannotBeDeleted + } + + // 如果计划处于启用状态,先停止它 + if plan.Status == models.PlanStatusEnabled { + if err := s.planRepo.StopPlanTransactionally(id); err != nil { + s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id) + return err + } + } + + if err := s.planRepo.DeletePlan(id); err != nil { + s.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id) + return err + } + + s.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id) + return nil +} + +// StartPlan 启动计划 +func (s *planServiceImpl) StartPlan(id uint) error { + const actionType = "领域层:启动计划" + + plan, err := s.planRepo.GetBasicPlanByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) + return ErrPlanNotFound + } + s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) + return err + } + + // 系统计划不允许手动启动 + if plan.PlanType == models.PlanTypeSystem { + s.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id) + return ErrPlanCannotBeStarted + } + // 计划已处于启动状态,无需重复操作 + if plan.Status == models.PlanStatusEnabled { + s.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id) + return ErrPlanAlreadyEnabled + } + + // 如果计划未处于启用状态 + if plan.Status != models.PlanStatusEnabled { + // 如果执行计数器大于0,重置为0 + if plan.ExecuteCount > 0 { + if err := s.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil { + s.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID) + return err + } + s.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID) + } + + // 更新计划状态为启用 + if err := s.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil { + s.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID) + return err + } + s.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID) + } + + // 创建或更新触发器 + if err := s.taskManager.CreateOrUpdateTrigger(plan.ID); err != nil { + s.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID) + return err + } + + s.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id) + return nil +} + +// StopPlan 停止计划 +func (s *planServiceImpl) StopPlan(id uint) error { + const actionType = "领域层:停止计划" + + plan, err := s.planRepo.GetBasicPlanByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) + return ErrPlanNotFound + } + s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) + return err + } + + // 系统计划不允许停止 + if plan.PlanType == models.PlanTypeSystem { + s.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id) + return ErrPlanCannotBeStopped + } + + // 计划当前不是启用状态 + if plan.Status != models.PlanStatusEnabled { + s.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status) + return ErrPlanNotEnabled + } + + // 停止计划事务性操作 + if err := s.planRepo.StopPlanTransactionally(id); err != nil { + s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id) + return err + } + + s.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id) + return nil +}