任务2.4

This commit is contained in:
2025-10-31 16:28:26 +08:00
parent b44e1a0e7c
commit 942ffa29a1
7 changed files with 516 additions and 273 deletions

View File

@@ -62,16 +62,16 @@ type API struct {
func NewAPI(cfg config.ServerConfig, func NewAPI(cfg config.ServerConfig,
logger *logs.Logger, logger *logs.Logger,
userRepo repository.UserRepository, userRepo repository.UserRepository,
planRepository repository.PlanRepository,
pigFarmService service.PigFarmService, pigFarmService service.PigFarmService,
pigBatchService service.PigBatchService, pigBatchService service.PigBatchService,
monitorService service.MonitorService, monitorService service.MonitorService,
deviceService service.DeviceService, deviceService service.DeviceService,
planService service.PlanService,
tokenService token.Service, tokenService token.Service,
auditService audit.Service, auditService audit.Service,
notifyService domain_notify.Service, notifyService domain_notify.Service,
listenHandler webhook.ListenHandler, listenHandler webhook.ListenHandler,
analysisTaskManager *scheduler.AnalysisPlanTaskManager) *API { ) *API {
// 使用 echo.New() 创建一个 Echo 引擎实例 // 使用 echo.New() 创建一个 Echo 引擎实例
e := echo.New() e := echo.New()
@@ -96,7 +96,7 @@ func NewAPI(cfg config.ServerConfig,
// 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员 // 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员
deviceController: device.NewController(deviceService, logger), deviceController: device.NewController(deviceService, logger),
// 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员 // 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员
planController: plan.NewController(logger, planRepository, analysisTaskManager), planController: plan.NewController(logger, planService),
// 在 NewAPI 中初始化猪场管理控制器 // 在 NewAPI 中初始化猪场管理控制器
pigFarmController: management.NewPigFarmController(logger, pigFarmService), pigFarmController: management.NewPigFarmController(logger, pigFarmService),
// 在 NewAPI 中初始化猪群控制器 // 在 NewAPI 中初始化猪群控制器

View File

@@ -6,29 +6,24 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller" "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/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/scheduler" "git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "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"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"gorm.io/gorm"
) )
// --- 控制器定义 --- // --- 控制器定义 ---
// Controller 定义了计划相关的控制器 // Controller 定义了计划相关的控制器
type Controller struct { type Controller struct {
logger *logs.Logger logger *logs.Logger
planRepo repository.PlanRepository planService service.PlanService
analysisPlanTaskManager *scheduler.AnalysisPlanTaskManager
} }
// NewController 创建一个新的 Controller 实例 // NewController 创建一个新的 Controller 实例
func NewController(logger *logs.Logger, planRepo repository.PlanRepository, analysisPlanTaskManager *scheduler.AnalysisPlanTaskManager) *Controller { func NewController(logger *logs.Logger, planService service.PlanService) *Controller {
return &Controller{ return &Controller{
logger: logger, logger: logger,
planRepo: planRepo, planService: planService,
analysisPlanTaskManager: analysisPlanTaskManager,
} }
} }
@@ -52,46 +47,19 @@ func (c *Controller) CreatePlan(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
} }
// 使用已有的转换函数,它已经包含了验证和重排逻辑 // 调用服务层创建计划
planToCreate, err := dto.NewPlanFromCreateRequest(&req) resp, err := c.planService.CreatePlan(&req)
if err != nil { if err != nil {
c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) c.logger.Errorf("%s: 服务层创建计划失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req) // 根据服务层返回的错误类型转换为相应的HTTP状态码
} if errors.Is(err, service.ErrPlanNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划数据校验失败或关联计划不存在", req)
// --- 业务规则处理 --- }
// 1. 设置计划类型:用户创建计划永远是自定义计划 return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "服务层创建计划失败", req)
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 := c.planRepo.CreatePlan(planToCreate); err != nil {
c.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "数据库创建计划失败", planToCreate)
}
// 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
if err := c.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil {
// 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功
c.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err)
}
// 使用已有的转换函数将创建后的模型转换为响应对象
resp, err := dto.NewPlanToResponse(planToCreate)
if err != nil {
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, planToCreate)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate)
} }
// 使用统一的成功响应函数 // 使用统一的成功响应函数
c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID) c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "计划创建成功", resp, actionType, "计划创建成功", resp) return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "计划创建成功", resp, actionType, "计划创建成功", resp)
} }
@@ -114,24 +82,14 @@ func (c *Controller) GetPlan(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr) return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
} }
// 2. 调用仓库层获取计划详情 // 调用服务层获取计划详情
plan, err := c.planRepo.GetPlanByID(uint(id)) resp, err := c.planService.GetPlanByID(uint(id))
if err != nil { if err != nil {
// 判断是否为“未找到”错误 c.logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, service.ErrPlanNotFound) {
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
} }
// 其他数据库错误视为内部错误 return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: "+err.Error(), actionType, "服务层获取计划详情失败", id)
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误", actionType, "数据库查询失败", id)
}
// 3. 将模型转换为响应 DTO
resp, err := dto.NewPlanToResponse(plan)
if err != nil {
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan)
} }
// 4. 发送成功响应 // 4. 发送成功响应
@@ -156,31 +114,14 @@ func (c *Controller) ListPlans(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", query) return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", query)
} }
// 1. 调用仓库层获取所有计划 // 调用服务层获取计划列表
opts := repository.ListPlansOptions{PlanType: query.PlanType} resp, err := c.planService.ListPlans(&query)
plans, total, err := c.planRepo.ListPlans(opts, query.Page, query.PageSize)
if err != nil { if err != nil {
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) c.logger.Errorf("%s: 服务层获取计划列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表时发生内部错误", actionType, "数据库查询失败", nil) return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: "+err.Error(), actionType, "服务层获取计划列表失败", nil)
} }
// 2. 将模型转换为响应 DTO c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(resp.Plans))
planResponses := make([]dto.PlanResponse, 0, len(plans))
for _, p := range plans {
resp, err := dto.NewPlanToResponse(&p)
if err != nil {
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p)
}
planResponses = append(planResponses, *resp)
}
// 3. 构造并发送成功响应
resp := dto.ListPlansResponse{
Plans: planResponses,
Total: total,
}
c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划列表成功", resp, actionType, "获取计划列表成功", resp) return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划列表成功", resp, actionType, "获取计划列表成功", resp)
} }
@@ -212,71 +153,20 @@ func (c *Controller) UpdatePlan(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
} }
// 3. 检查计划是否存在 // 调用服务层更新计划
existingPlan, err := c.planRepo.GetBasicPlanByID(uint(id)) resp, err := c.planService.UpdatePlan(uint(id), &req)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { c.logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id)
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) if errors.Is(err, service.ErrPlanNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
} else if errors.Is(err, service.ErrPlanCannotBeModified) {
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许修改", id)
} }
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "服务层更新计划失败", req)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
}
// 4. 业务规则:系统计划不允许修改
if existingPlan.PlanType == models.PlanTypeSystem {
c.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, id)
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许修改", actionType, "尝试修改系统计划", id)
}
// 5. 将请求转换为模型(转换函数带校验)
planToUpdate, err := dto.NewPlanFromUpdateRequest(&req)
if err != nil {
c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
}
planToUpdate.ID = uint(id) // 确保ID被设置
// --- 自动判断 ContentType ---
if len(req.SubPlanIDs) > 0 {
planToUpdate.ContentType = models.PlanContentTypeSubPlans
} else {
// 如果 SubPlanIDs 未提供,则默认为 Tasks 类型(即使 Tasks 字段也未提供)
planToUpdate.ContentType = models.PlanContentTypeTasks
}
// 6. 调用仓库方法更新计划
// 只要是更新任务,就重置执行计数器
planToUpdate.ExecuteCount = 0 // 重置计数器
c.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID)
if err := c.planRepo.UpdatePlan(planToUpdate); err != nil {
c.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "数据库更新计划失败", planToUpdate)
}
// 更新成功后,调用 manager 确保触发器任务定义存在
if err := c.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil {
// 这是一个非阻塞性错误,我们只记录日志
c.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err)
}
// 7. 获取更新后的完整计划用于响应
updatedPlan, err := c.planRepo.GetPlanByID(uint(id))
if err != nil {
c.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, id)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取更新后计划详情时发生内部错误", actionType, "获取更新后计划详情失败", id)
}
// 8. 将模型转换为响应 DTO
resp, err := dto.NewPlanToResponse(updatedPlan)
if err != nil {
c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan)
} }
// 9. 发送成功响应 // 9. 发送成功响应
c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID) c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划更新成功", resp, actionType, "计划更新成功", resp) return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划更新成功", resp, actionType, "计划更新成功", resp)
} }
@@ -299,35 +189,16 @@ func (c *Controller) DeletePlan(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr) return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
} }
// 2. 检查计划是否存在 // 调用服务层删除计划
plan, err := c.planRepo.GetBasicPlanByID(uint(id)) err = c.planService.DeletePlan(uint(id))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { c.logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id)
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) if errors.Is(err, service.ErrPlanNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
} else if errors.Is(err, service.ErrPlanCannotBeDeleted) {
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许删除", id)
} }
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划失败: "+err.Error(), actionType, "服务层删除计划失败", id)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
}
// 3. 业务规则:系统计划不允许删除
if plan.PlanType == models.PlanTypeSystem {
c.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id)
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许删除", actionType, "尝试删除系统计划", id)
}
// 4. 停止这个计划
if plan.Status == models.PlanStatusEnabled {
if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil {
c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id)
}
}
// 5. 调用仓库层删除计划
if err := c.planRepo.DeletePlan(uint(id)); err != nil {
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划时发生内部错误", actionType, "数据库删除失败", id)
} }
// 6. 发送成功响应 // 6. 发送成功响应
@@ -354,56 +225,18 @@ func (c *Controller) StartPlan(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr) return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
} }
// 2. 检查计划是否存在 // 调用服务层启动计划
plan, err := c.planRepo.GetBasicPlanByID(uint(id)) err = c.planService.StartPlan(uint(id))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { c.logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id)
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) if errors.Is(err, service.ErrPlanNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
} else if errors.Is(err, service.ErrPlanCannotBeStarted) {
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许手动启动", id)
} else if errors.Is(err, service.ErrPlanAlreadyEnabled) {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "计划已处于启动状态", id)
} }
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "启动计划失败: "+err.Error(), actionType, "服务层启动计划失败", id)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
}
// 3. 业务规则检查
if plan.PlanType == models.PlanTypeSystem {
c.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id)
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许手动启动", actionType, "尝试手动启动系统计划", id)
}
if plan.Status == models.PlanStatusEnabled {
c.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", id)
}
// 4. 检查并重置执行计数器,然后更新计划状态为“已启动”
// 只有当计划是从非 Enabled 状态(如 Disabled, Stopeed, Failed启动时才需要重置计数器
if plan.Status != models.PlanStatusEnabled {
// 如果计划是从停止或失败状态重新启动且计数器不为0则重置执行计数
if plan.ExecuteCount > 0 {
if err := c.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil {
c.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "重置计划执行计数失败", actionType, "重置执行计数失败", plan.ID)
}
c.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID)
}
// 更新计划状态为“已启动”
if err := c.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil {
c.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划状态失败", actionType, "更新计划状态失败", plan.ID)
}
c.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID)
} else {
// 如果计划已经处于 Enabled 状态,则无需更新
c.logger.Infof("计划 #%d 已处于启动状态,无需重复操作。", plan.ID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", plan.ID)
}
// 5. 为计划创建或更新触发器
if err := c.analysisPlanTaskManager.CreateOrUpdateTrigger(plan.ID); err != nil {
// 此处错误不回滚状态,因为状态更新已成功,但需要明确告知用户触发器创建失败
c.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划状态已更新,但创建执行触发器失败,请检查计划配置或稍后重试", actionType, "创建执行触发器失败", plan.ID)
} }
// 6. 发送成功响应 // 6. 发送成功响应
@@ -430,33 +263,18 @@ func (c *Controller) StopPlan(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr) return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
} }
// 2. 检查计划是否存在 // 调用服务层停止计划
plan, err := c.planRepo.GetBasicPlanByID(uint(id)) err = c.planService.StopPlan(uint(id))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { c.logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id)
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) if errors.Is(err, service.ErrPlanNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
} else if errors.Is(err, service.ErrPlanCannotBeStopped) {
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许停止", id)
} else if errors.Is(err, service.ErrPlanNotEnabled) {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "计划未启用", id)
} }
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划失败: "+err.Error(), actionType, "服务层停止计划失败", id)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
}
// 3. 业务规则:系统计划不允许停止
if plan.PlanType == models.PlanTypeSystem {
c.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id)
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许停止", actionType, "尝试停止系统计划", id)
}
// 4. 检查计划当前状态
if plan.Status != models.PlanStatusEnabled {
c.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划当前不是启用状态", actionType, "计划未启用", id)
}
// 5. 调用仓库层方法,该方法内部处理事务
if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil {
c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id)
} }
// 6. 发送成功响应 // 6. 发送成功响应

View File

@@ -0,0 +1,344 @@
package service
import (
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/scheduler"
"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 定义了计划相关的应用服务接口
type PlanService interface {
// CreatePlan 创建一个新的计划
CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error)
// GetPlanByID 根据ID获取计划详情
GetPlanByID(id uint) (*dto.PlanResponse, error)
// ListPlans 获取计划列表,支持过滤和分页
ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error)
// UpdatePlan 更新计划
UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error)
// DeletePlan 删除计划(软删除)
DeletePlan(id uint) error
// StartPlan 启动计划
StartPlan(id uint) error
// StopPlan 停止计划
StopPlan(id uint) error
}
// planService 是 PlanService 接口的实现
type planService struct {
logger *logs.Logger
planRepo repository.PlanRepository
analysisPlanTaskManager *scheduler.AnalysisPlanTaskManager
}
// NewPlanService 创建一个新的 PlanService 实例
func NewPlanService(
logger *logs.Logger,
planRepo repository.PlanRepository,
analysisPlanTaskManager *scheduler.AnalysisPlanTaskManager,
) PlanService {
return &planService{
logger: logger,
planRepo: planRepo,
analysisPlanTaskManager: analysisPlanTaskManager,
}
}
// CreatePlan 创建一个新的计划
func (s *planService) CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error) {
const actionType = "服务层:创建计划"
// 使用已有的转换函数,它已经包含了验证和重排逻辑
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)
if err != nil {
s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, planToCreate)
return nil, errors.New("计划创建成功,但响应生成失败")
}
s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID)
return resp, nil
}
// GetPlanByID 根据ID获取计划详情
func (s *planService) GetPlanByID(id uint) (*dto.PlanResponse, 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
}
resp, err := dto.NewPlanToResponse(plan)
if err != nil {
s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
return nil, errors.New("获取计划详情失败: 内部数据格式错误")
}
s.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
return resp, nil
}
// ListPlans 获取计划列表,支持过滤和分页
func (s *planService) ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error) {
const actionType = "服务层:获取计划列表"
opts := repository.ListPlansOptions{PlanType: query.PlanType}
plans, total, err := s.planRepo.ListPlans(opts, query.Page, query.PageSize)
if err != nil {
s.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
return nil, err
}
planResponses := make([]dto.PlanResponse, 0, len(plans))
for _, p := range plans {
resp, err := dto.NewPlanToResponse(&p)
if err != nil {
s.logger.Errorf("%s: 序列化单个计划响应失败: %v, Plan: %+v", actionType, err, p)
// 这里选择跳过有问题的计划,并记录错误,而不是中断整个列表的返回
continue
}
planResponses = append(planResponses, *resp)
}
resp := &dto.ListPlansResponse{
Plans: planResponses,
Total: total,
}
s.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses))
return resp, nil
}
// 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
}
planToUpdate, err := dto.NewPlanFromUpdateRequest(req)
if err != nil {
s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
return nil, err
}
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.UpdatePlan(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)
if err != nil {
s.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, id)
return nil, errors.New("获取更新后计划详情时发生内部错误")
}
resp, err := dto.NewPlanToResponse(updatedPlan)
if err != nil {
s.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
return nil, errors.New("计划更新成功,但响应生成失败")
}
s.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
return resp, nil
}
// DeletePlan 删除计划(软删除)
func (s *planService) 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 *planService) 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 {
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.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
return nil
}
// StopPlan 停止计划
func (s *planService) 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
}

View File

@@ -45,16 +45,15 @@ func NewApplication(configPath string) (*Application, error) {
cfg.Server, cfg.Server,
logger, logger,
infra.Repos.UserRepo, infra.Repos.UserRepo,
infra.Repos.PlanRepo,
appServices.PigFarmService, appServices.PigFarmService,
appServices.PigBatchService, appServices.PigBatchService,
appServices.MonitorService, appServices.MonitorService,
appServices.DeviceService, appServices.DeviceService,
appServices.PlanService,
infra.TokenService, infra.TokenService,
appServices.AuditService, appServices.AuditService,
infra.NotifyService, infra.NotifyService,
infra.Lora.ListenHandler, infra.Lora.ListenHandler,
domain.AnalysisPlanTaskManager,
) )
// 4. 组装 Application 对象 // 4. 组装 Application 对象

View File

@@ -187,6 +187,7 @@ type AppServices struct {
MonitorService service.MonitorService MonitorService service.MonitorService
DeviceService service.DeviceService DeviceService service.DeviceService
AuditService audit.Service AuditService audit.Service
PlanService service.PlanService
} }
// initAppServices 初始化所有的应用服务。 // initAppServices 初始化所有的应用服务。
@@ -216,6 +217,7 @@ func initAppServices(infra *Infrastructure, domainServices *DomainServices, logg
domainServices.GeneralDeviceService, domainServices.GeneralDeviceService,
) )
auditService := audit.NewService(infra.Repos.UserActionLogRepo, logger) auditService := audit.NewService(infra.Repos.UserActionLogRepo, logger)
planService := service.NewPlanService(logger, infra.Repos.PlanRepo, domainServices.AnalysisPlanTaskManager)
return &AppServices{ return &AppServices{
PigFarmService: pigFarmService, PigFarmService: pigFarmService,
@@ -223,6 +225,7 @@ func initAppServices(infra *Infrastructure, domainServices *DomainServices, logg
MonitorService: monitorService, MonitorService: monitorService,
DeviceService: deviceService, DeviceService: deviceService,
AuditService: auditService, AuditService: auditService,
PlanService: planService,
} }
} }

View File

@@ -229,3 +229,82 @@
### Open Questions ### Open Questions
- 暂无。 - 暂无。
---
## `plan` 模块重构设计
### Context
`plan_controller.go` 当前包含了大量的业务逻辑,这违反了控制器层应只负责请求处理和响应发送的原则。具体问题包括:
- **业务规则判断**:控制器中直接判断计划类型(如 `models.PlanTypeSystem`)、计划状态(如 `models.PlanStatusEnabled`)以及 `ContentType` 的自动判断。
- **领域对象创建与转换**:控制器直接使用 `dto.NewPlanFromCreateRequest``dto.NewPlanFromUpdateRequest` 将请求 DTO 转换为 `models.Plan`,并在响应前将 `models.Plan` 转换为 `dto.PlanResponse`
- **直接调用仓库层**:控制器直接调用 `planRepo``CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, `GetBasicPlanByID`, `UpdatePlanStatus`, `UpdateExecuteCount`, `StopPlanTransactionally` 等方法。
- **协调领域服务**:控制器直接协调 `analysisPlanTaskManager``EnsureAnalysisTaskDefinition``CreateOrUpdateTrigger` 方法。
- **错误处理**:控制器直接通过 `errors.Is(err, gorm.ErrRecordNotFound)` 判断仓库层错误,并根据错误类型返回不同的 HTTP 状态码。
- **执行计数器重置**:在 `UpdatePlan``StartPlan` 中,控制器直接处理 `ExecuteCount` 的重置逻辑。
这种设计导致控制器层职责过重,业务逻辑分散,难以维护和测试。
### Goals / Non-Goals
#### Goals
- **创建应用服务层**:引入一个新的 `internal/app/service/plan_service.go` 来封装 `plan` 模块的所有业务逻辑。
- **迁移业务逻辑**:将 `plan_controller.go` 中识别出的所有业务规则判断、领域对象创建与转换、对仓库层的直接调用、对 `analysisPlanTaskManager` 的协调以及错误处理逻辑,全部迁移到新的 `PlanService` 中。
- **简化控制器**:使 `plan_controller.go` 只负责 HTTP 请求处理、参数绑定、调用新的 `PlanService` 方法,并处理服务层返回的 DTO。
- **统一服务层接口**`PlanService` 的方法将接收 DTO 作为输入,并返回 DTO 作为输出,实现服务层接口的标准化。
#### Non-Goals
- **不修改业务逻辑**:本次重构不涉及任何已有业务规则的变更。所有业务逻辑将原封不动地从控制器迁移到服务层。
- **不改变 API 契约**:对外暴露的 API 接口、请求参数和响应结构对最终用户保持不变。
- **不改变领域服务**:不对 `internal/domain/scheduler/analysis_plan_task_manager.go` 的接口和实现进行任何修改。
- **不改变仓库层接口**:不对 `internal/infra/repository/plan_repository.go` 的接口进行任何修改。
### Decisions
- **决策:引入新的应用服务 `PlanService`**
- **理由**:这是解决控制器职责过重和分层不清问题的标准做法。`PlanService` 将作为应用层门面,协调 `PlanRepository``AnalysisPlanTaskManager`,并为控制器提供一个清晰、稳定的接口。
- **结构**`PlanService` 将依赖于 `PlanRepository``AnalysisPlanTaskManager`
- **决策:`PlanService` 接口全面采用 DTO**
- **具体实现**:接口方法将接收 `dto.CreatePlanRequest`, `dto.UpdatePlanRequest`, `dto.ListPlansQuery` 等请求 DTO并返回 `*dto.PlanResponse`, `*dto.ListPlansResponse` 等响应 DTO。
- **理由**:这与 `monitor``device``pig-farm` 模块的重构决策一致,可以确保应用服务层的接口统一、清晰,并与上层(控制器)和下层(领域/仓库)完全解耦。服务层内部将负责 `DTO``models` 的转换以及 `models``DTO` 的转换。
- **决策:将控制器中的业务规则判断和错误处理下沉到服务层**
- **理由**:控制器应专注于 HTTP 协议相关的职责。所有业务规则的判断如计划类型、状态检查、ContentType 自动判断、执行计数器重置)以及对底层错误的具体判断(如 `gorm.ErrRecordNotFound`)都属于业务逻辑范畴,应由服务层处理。服务层将返回更抽象的业务错误,控制器只需根据这些抽象错误进行统一的 HTTP 响应处理。
### Risks / Trade-offs
- **风险:意外修改或丢失现有业务逻辑**
- **描述**:在将控制器中分散的业务逻辑迁移到服务层时,存在逻辑被无意中删除、修改或遗漏的风险,尤其是在处理计划状态转换、执行计数器重置和 `ContentType` 自动判断等复杂逻辑时。
- **缓解措施**
1. **逐行迁移与比对**:在迁移过程中,将控制器中的每一段业务逻辑代码逐行复制到服务层,并仔细比对,确保逻辑的等效性。
2. **详细注释**:在服务层中对迁移过来的业务逻辑添加详细注释,解释其来源和作用。
3. **回归测试**:在完成重构后,必须进行完整的回归测试,确保所有受影响的 API 端点的行为与重构前完全一致。
### Migration Plan
1. **创建 `internal/app/service/plan_service.go` 文件**
- 定义 `PlanService` 接口,包含 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, `StartPlan`, `StopPlan` 等方法。
- 定义 `planService` 结构体,并实现 `PlanService` 接口。
-`planService` 的实现中,将 `plan_controller.go` 中所有相关的业务逻辑(包括 DTO 转换、业务规则判断、对 `planRepo``analysisPlanTaskManager` 的调用、错误处理)精确迁移到对应的方法中。
2. **修改 `internal/app/controller/plan/plan_controller.go`**
- 更新 `Controller` 结构体,将 `planRepo``analysisPlanTaskManager` 替换为 `service.PlanService`
- 修改 `NewController` 函数,注入 `service.PlanService`
- 简化所有处理器方法,移除所有业务逻辑,只保留请求参数绑定、调用 `service.PlanService` 方法、错误处理和响应构建。
3. **修改 `internal/core/component_initializers.go`**
-`AppServices` 结构体中添加 `PlanService service.PlanService` 字段。
-`initAppServices` 函数中,初始化 `PlanService` 实例,并将其注入到 `AppServices` 中。
4. **修改 `internal/app/api/api.go`**
- 更新 `NewAPI` 函数的参数,移除 `planRepository``analysisTaskManager`,添加 `service.PlanService`
- 更新 `plan.NewController` 的调用,传入新的 `service.PlanService` 依赖。
### Open Questions
- 暂无。

View File

@@ -65,32 +65,32 @@
### 2.4 `plan` 模块 ### 2.4 `plan` 模块
- [ ] 2.4.1 **创建并修改 `internal/app/service/plan_service.go`** - [x] 2.4.1 **创建并修改 `internal/app/service/plan_service.go`**
- [ ] 定义 `PlanService` 接口,包含 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, - [x] 定义 `PlanService` 接口,包含 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`,
`StartPlan`, `StopPlan` 等方法。 `StartPlan`, `StopPlan` 等方法。
- [ ]`CreatePlan`, `UpdatePlan` 方法定义并接收 DTO 作为输入。 - [x]`CreatePlan`, `UpdatePlan` 方法定义并接收 DTO 作为输入。
- [ ]`GetPlanByID`, `ListPlans` 方法的返回值 `models.Plan``[]models.Plan` 替换为 `dto.PlanResponse` - [x]`GetPlanByID`, `ListPlans` 方法的返回值 `models.Plan``[]models.Plan` 替换为 `dto.PlanResponse`
`[]dto.PlanResponse` `[]dto.PlanResponse`
- [ ] 调整 `ListPlans` 方法的 `opts repository.ListPlansOptions` 参数替换为服务层自定义的查询 DTO 或一系列基本参数。 - [x] 调整 `ListPlans` 方法的 `opts repository.ListPlansOptions` 参数替换为服务层自定义的查询 DTO 或一系列基本参数。
- [ ] 调整 `DeletePlan`, `StartPlan`, `StopPlan` 方法,使其接收 DTO 或基本参数,并封装所有业务逻辑。 - [x] 调整 `DeletePlan`, `StartPlan`, `StopPlan` 方法,使其接收 DTO 或基本参数,并封装所有业务逻辑。
- [ ] 实现 `PlanService` 接口。 - [x] 实现 `PlanService` 接口。
- [ ] 在服务层内部将输入 DTO 转换为 `models` 对象。 - [x] 在服务层内部将输入 DTO 转换为 `models` 对象。
- [ ] 在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse` - [x] 在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`
- [ ]`internal/app/controller/plan/plan_controller.go` 中所有的业务规则判断计划类型检查、状态检查、执行计数器重置、ContentType - [x]`internal/app/controller/plan/plan_controller.go` 中所有的业务规则判断计划类型检查、状态检查、执行计数器重置、ContentType
自动判断)移入服务层。 自动判断)移入服务层。
- [ ]`internal/app/controller/plan/plan_controller.go` 中对 `repository` 方法的直接调用移入服务层。 - [x]`internal/app/controller/plan/plan_controller.go` 中对 `repository` 方法的直接调用移入服务层。
- [ ]`internal/app/controller/plan/plan_controller.go` 中对 `analysisPlanTaskManager` 的协调移入服务层。 - [x]`internal/app/controller/plan/plan_controller.go` 中对 `analysisPlanTaskManager` 的协调移入服务层。
- [ ]`internal/app/controller/plan/plan_controller.go` 中处理仓库层特有错误(`gorm.ErrRecordNotFound`)的逻辑移入服务层。 - [x]`internal/app/controller/plan/plan_controller.go` 中处理仓库层特有错误(`gorm.ErrRecordNotFound`)的逻辑移入服务层。
- [ ] 2.4.2 **修改 `internal/app/controller/plan/plan_controller.go`** - [x] 2.4.2 **修改 `internal/app/controller/plan/plan_controller.go`**
- [ ] 引入并使用新创建的 `plan_service` - [x] 引入并使用新创建的 `plan_service`
- [ ] 移除控制器中直接创建 `models.Plan` 对象和 `repository.ListPlansOptions` 的逻辑。 - [x] 移除控制器中直接创建 `models.Plan` 对象和 `repository.ListPlansOptions` 的逻辑。
- [ ] 移除控制器中所有的业务规则判断。 - [x] 移除控制器中所有的业务规则判断。
- [ ] 移除控制器中直接调用 `repository` 方法的逻辑。 - [x] 移除控制器中直接调用 `repository` 方法的逻辑。
- [ ] 移除控制器中直接协调 `analysisPlanTaskManager` 的逻辑。 - [x] 移除控制器中直接协调 `analysisPlanTaskManager` 的逻辑。
- [ ] 移除控制器中直接处理仓库层特有错误的逻辑。 - [x] 移除控制器中直接处理仓库层特有错误的逻辑。
- [ ] 调整服务层方法的调用,使其接收新的服务层输入 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse` - [x] 调整服务层方法的调用,使其接收新的服务层输入 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`
- [ ] 2.4.3 **修改 `internal/core/component_initializers.go`**:创建并提供新的 `PlanService` - [x] 2.4.3 **修改 `internal/core/component_initializers.go`**:创建并提供新的 `PlanService`
- [ ] 2.4.4 **修改 `internal/app/api/api.go`**:更新 `PlanController` 的依赖注入。 - [x] 2.4.4 **修改 `internal/app/api/api.go`**:更新 `PlanController` 的依赖注入。
### 2.5 `user` 模块 ### 2.5 `user` 模块