diff --git a/internal/infra/models/plan.go b/internal/infra/models/plan.go index 0190122..dc4719d 100644 --- a/internal/infra/models/plan.go +++ b/internal/infra/models/plan.go @@ -1,6 +1,8 @@ package models import ( + "fmt" + "gorm.io/datatypes" "gorm.io/gorm" ) @@ -54,6 +56,29 @@ func (Plan) TableName() string { return "plans" } +// ValidateExecutionOrder 校验计划中的步骤或子计划顺序不能有重复的 +func (p Plan) ValidateExecutionOrder() error { + orderMap := make(map[int]bool) + + switch p.ContentType { + case PlanContentTypeTasks: + for _, task := range p.Tasks { + if orderMap[task.ExecutionOrder] { + return fmt.Errorf("任务执行顺序重复: %d", task.ExecutionOrder) + } + orderMap[task.ExecutionOrder] = true + } + case PlanContentTypeSubPlans: + for _, subPlan := range p.SubPlans { + if orderMap[subPlan.ExecutionOrder] { + return fmt.Errorf("子计划执行顺序重复: %d", subPlan.ExecutionOrder) + } + orderMap[subPlan.ExecutionOrder] = true + } + } + return nil +} + // SubPlan 代表作为另一个计划一部分的子计划,具有执行顺序 type SubPlan struct { gorm.Model diff --git a/internal/infra/models/plan_test.go b/internal/infra/models/plan_test.go new file mode 100644 index 0000000..9609e58 --- /dev/null +++ b/internal/infra/models/plan_test.go @@ -0,0 +1,103 @@ +package models_test + +import ( + "fmt" + "testing" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "github.com/stretchr/testify/assert" +) + +func TestPlan_ValidateExecutionOrder(t *testing.T) { + tests := []struct { + name string + plan models.Plan + expectedError string // 期望的错误信息,如果为nil则表示不期望错误 + }{ + { + name: "任务类型-无重复执行顺序", + plan: models.Plan{ + ContentType: models.PlanContentTypeTasks, + Tasks: []models.Task{ + {ExecutionOrder: 1}, + {ExecutionOrder: 2}, + {ExecutionOrder: 3}, + }, + }, + expectedError: "", + }, + { + name: "任务类型-重复执行顺序", + plan: models.Plan{ + ContentType: models.PlanContentTypeTasks, + Tasks: []models.Task{ + {ExecutionOrder: 1}, + {ExecutionOrder: 2}, + {ExecutionOrder: 1}, // 重复 + }, + }, + expectedError: fmt.Sprintf("任务执行顺序重复: %d", 1), + }, + { + name: "任务类型-空任务列表", + plan: models.Plan{ + ContentType: models.PlanContentTypeTasks, + Tasks: []models.Task{}, + }, + expectedError: "", + }, + { + name: "子计划类型-无重复执行顺序", + plan: models.Plan{ + ContentType: models.PlanContentTypeSubPlans, + SubPlans: []models.SubPlan{ + {ExecutionOrder: 1}, + {ExecutionOrder: 2}, + {ExecutionOrder: 3}, + }, + }, + expectedError: "", + }, + { + name: "子计划类型-重复执行顺序", + plan: models.Plan{ + ContentType: models.PlanContentTypeSubPlans, + SubPlans: []models.SubPlan{ + {ExecutionOrder: 1}, + {ExecutionOrder: 2}, + {ExecutionOrder: 1}, // 重复 + }, + }, + expectedError: fmt.Sprintf("子计划执行顺序重复: %d", 1), + }, + { + name: "子计划类型-空子计划列表", + plan: models.Plan{ + ContentType: models.PlanContentTypeSubPlans, + SubPlans: []models.SubPlan{}, + }, + expectedError: "", + }, + { + name: "未知内容类型", + plan: models.Plan{ + ContentType: "UNKNOWN_TYPE", // 未知类型 + Tasks: []models.Task{{ExecutionOrder: 1}}, + }, + expectedError: "", // 对于未知类型,ValidateExecutionOrder 不会返回错误,因为它只处理已知类型 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.plan.ValidateExecutionOrder() + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/infra/repository/plan_repository.go b/internal/infra/repository/plan_repository.go index d38d523..6b3f802 100644 --- a/internal/infra/repository/plan_repository.go +++ b/internal/infra/repository/plan_repository.go @@ -125,6 +125,11 @@ func (r *gormPlanRepository) Create(plan *models.Plan) error { return ErrMixedContent } + // 检查是否有重复的执行顺序 + 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) @@ -156,28 +161,6 @@ func (r *gormPlanRepository) Create(plan *models.Plan) error { if err := tx.Create(plan).Error; err != nil { return err } - - // 3. 处理子计划关联 (如果 ContentType 是 sub_plans) - // GORM 不会自动创建 SubPlan 关联记录,需要手动处理 - if plan.ContentType == models.PlanContentTypeSubPlans { - // 收集所有 SubPlan 关联,设置 ParentPlanID - var subPlanLinksToCreate []models.SubPlan - for i := range plan.SubPlans { - plan.SubPlans[i].ParentPlanID = plan.ID // 设置父计划ID - // 确保 ChildPlanID 被正确设置,因为 ChildPlan 对象可能只在内存中 - if plan.SubPlans[i].ChildPlanID == 0 && plan.SubPlans[i].ChildPlan != nil { - plan.SubPlans[i].ChildPlanID = plan.SubPlans[i].ChildPlan.ID - } - subPlanLinksToCreate = append(subPlanLinksToCreate, plan.SubPlans[i]) - } - - // 批量创建 SubPlan 关联记录 - if len(subPlanLinksToCreate) > 0 { - if err := tx.CreateInBatches(subPlanLinksToCreate, 100).Error; err != nil { // 批量大小100 - return err - } - } - } return nil }) } @@ -214,7 +197,12 @@ func (r *gormPlanRepository) validatePlanTree(tx *gorm.DB, plan *models.Plan) er return err } - // 3. 一次性数据库存在性校验 + // 3. 检查是否有重复的执行顺序 + if err := plan.ValidateExecutionOrder(); err != nil { + return fmt.Errorf("计划 (ID: %d) 的执行顺序无效: %w", plan.ID, err) + } + + // 4. 一次性数据库存在性校验 var idsToCheck []uint for id := range allIDs { idsToCheck = append(idsToCheck, id) @@ -229,7 +217,6 @@ func (r *gormPlanRepository) validatePlanTree(tx *gorm.DB, plan *models.Plan) er return ErrNodeDoesNotExist } } - return nil } diff --git a/internal/infra/repository/plan_repository_test.go b/internal/infra/repository/plan_repository_test.go index b436ff4..abcb2e1 100644 --- a/internal/infra/repository/plan_repository_test.go +++ b/internal/infra/repository/plan_repository_test.go @@ -432,8 +432,8 @@ func TestUpdatePlan_Validation(t *testing.T) { planC.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}} planA.ContentType = models.PlanContentTypeSubPlans planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 2, ChildPlan: planB}, - {ChildPlanID: 3, ChildPlan: planC}, + {ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}, + {ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, } return planA }, @@ -466,8 +466,8 @@ func TestUpdatePlan_Validation(t *testing.T) { buildInput: func() *models.Plan { planA.ContentType = models.PlanContentTypeSubPlans planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 2, ChildPlan: planB}, - {ChildPlanID: 3, ChildPlan: planC}, // C 不存在 + {ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}, + {ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, // C 不存在 } return planA }, @@ -532,6 +532,37 @@ func TestUpdatePlan_Validation(t *testing.T) { }, expectedError: "不能同时包含任务和子计划", }, + { + name: "错误-任务执行顺序重复", + setupDB: func(db *gorm.DB) { + db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) + }, + buildInput: func() *models.Plan { + planA.ContentType = models.PlanContentTypeTasks + planA.Tasks = []models.Task{ + {Name: "Task 1", ExecutionOrder: 1}, + {Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序 + } + return planA + }, + expectedError: fmt.Sprintf("任务执行顺序重复: %d", 1), + }, { + name: "错误-子计划执行顺序重复", + setupDB: func(db *gorm.DB) { + db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) + db.Create(&models.Plan{Model: gorm.Model{ID: 10}}) + db.Create(&models.Plan{Model: gorm.Model{ID: 11}}) + }, + buildInput: func() *models.Plan { + planA.ContentType = models.PlanContentTypeSubPlans + planA.SubPlans = []models.SubPlan{ + {ChildPlanID: 10, ChildPlan: &models.Plan{Model: gorm.Model{ID: 10}}, ExecutionOrder: 1}, + {ChildPlanID: 11, ChildPlan: &models.Plan{Model: gorm.Model{ID: 11}}, ExecutionOrder: 1}, // 重复的顺序 + } + return planA + }, + expectedError: fmt.Sprintf("子计划执行顺序重复: %d", 1), + }, } for _, tc := range testCases { @@ -1024,3 +1055,243 @@ func TestUpdatePlan_Reconciliation(t *testing.T) { }) } } + +// createExistingPlan 辅助函数,用于在数据库中创建已存在的计划 +func createExistingPlan(db *gorm.DB, name string, contentType models.PlanContentType) *models.Plan { + plan := &models.Plan{ + Name: name, + ContentType: contentType, + } + db.Create(plan) + return plan +} + +func TestPlanRepository_Create(t *testing.T) { + type testCase struct { + name string + setupDB func(db *gorm.DB) // 准备数据库的初始状态 + inputPlan *models.Plan // 传入 Create 方法的计划对象 + expectedError error // 期望的错误类型 + verifyDB func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) // 验证数据库状态 + } + + testCases := []testCase{ + { + name: "成功创建-只包含基本信息", + setupDB: func(db *gorm.DB) { + // 无需额外设置 + }, + inputPlan: &models.Plan{ + Name: "简单计划", + Description: "一个不包含任务或子计划的简单计划", + ContentType: models.PlanContentTypeTasks, // 修改为有效的 ContentType + Tasks: []models.Task{}, // 明确为空任务列表 + }, + expectedError: nil, + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + assert.NotZero(t, createdPlan.ID, "创建后计划ID不应为0") + var foundPlan models.Plan + err := db.First(&foundPlan, createdPlan.ID).Error + assert.NoError(t, err) + assert.Equal(t, "简单计划", foundPlan.Name) + assert.Equal(t, models.PlanContentTypeTasks, foundPlan.ContentType) + var tasks []models.Task + db.Where("plan_id = ?", createdPlan.ID).Find(&tasks) + assert.Len(t, tasks, 0, "不应创建任何任务") + }, + }, + { + name: "成功创建-包含任务", + setupDB: func(db *gorm.DB) { + // 无需额外设置 + }, + inputPlan: &models.Plan{ + Name: "任务计划", + ContentType: models.PlanContentTypeTasks, + Tasks: []models.Task{ + {Name: "任务A", ExecutionOrder: 1}, + {Name: "任务B", ExecutionOrder: 2}, + }, + }, + expectedError: nil, + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + assert.NotZero(t, createdPlan.ID, "计划ID不应为0") + var foundPlan models.Plan + db.Preload("Tasks").First(&foundPlan, createdPlan.ID) + assert.Len(t, foundPlan.Tasks, 2, "应创建两个任务") + assert.NotZero(t, foundPlan.Tasks[0].ID, "任务ID不应为0") + assert.Equal(t, "任务A", foundPlan.Tasks[0].Name) + }, + }, + { + name: "成功创建-包含子计划关联", + setupDB: func(db *gorm.DB) { + // 预先创建子计划实体,使用有效的 ContentType + createExistingPlan(db, "子计划1", models.PlanContentTypeTasks) + createExistingPlan(db, "子计划2", models.PlanContentTypeTasks) + }, + inputPlan: &models.Plan{ + Name: "父计划", + ContentType: models.PlanContentTypeSubPlans, + SubPlans: []models.SubPlan{ + {ChildPlanID: 1, ExecutionOrder: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 1}}}, // 关联已存在的子计划1 + {ChildPlanID: 2, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 2}}}, // 关联已存在的子计划2 + }, + }, + expectedError: nil, + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + assert.NotZero(t, createdPlan.ID, "创建后计划ID不应为0") + + // 直接查询 SubPlan 关联记录 + var foundSubPlanLinks []models.SubPlan + err := db.Where("parent_plan_id = ?", createdPlan.ID).Find(&foundSubPlanLinks).Error + assert.NoError(t, err) + + assert.Len(t, foundSubPlanLinks, 2, "应创建两个子计划关联") + assert.NotZero(t, foundSubPlanLinks[0].ID, "子计划关联ID不应为0") + assert.Equal(t, createdPlan.ID, foundSubPlanLinks[0].ParentPlanID) + assert.Equal(t, uint(1), foundSubPlanLinks[0].ChildPlanID) + }, + }, + { + name: "失败-计划ID不为0", + setupDB: func(db *gorm.DB) { + // 无需额外设置 + }, + inputPlan: &models.Plan{ + Model: gorm.Model{ID: 100}, // ID不为0 + Name: "无效计划", + }, + expectedError: repository.ErrCreateWithNonZeroID, + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + // 验证数据库中没有创建该计划 + var count int64 + db.Model(&models.Plan{}).Where("id = ?", 100).Count(&count) + assert.Equal(t, int64(0), count, "计划不应被创建") + }, + }, + { + name: "失败-同时包含任务和子计划", + setupDB: func(db *gorm.DB) { + createExistingPlan(db, "子计划", models.PlanContentTypeTasks) // 使用有效的 ContentType + }, + inputPlan: &models.Plan{ + Name: "混合内容计划", + ContentType: models.PlanContentTypeTasks, // 声明为任务类型 + Tasks: []models.Task{{Name: "任务A"}}, + SubPlans: []models.SubPlan{{ChildPlanID: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 1}}}}, // 但也包含子计划 + }, + expectedError: repository.ErrMixedContent, + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + // 验证数据库中没有创建该计划 + var count int64 + db.Model(&models.Plan{}).Where("name = ?", "混合内容计划").Count(&count) + assert.Equal(t, int64(0), count, "计划不应被创建") + }, + }, + { + name: "失败-子计划ID为0", + setupDB: func(db *gorm.DB) { + // 无需额外设置 + }, + inputPlan: &models.Plan{ + Name: "无效子计划关联", + ContentType: models.PlanContentTypeSubPlans, + SubPlans: []models.SubPlan{ + {ChildPlanID: 0, ChildPlan: &models.Plan{Model: gorm.Model{ID: 0}}}, // 子计划ID为0 + }, + }, + expectedError: repository.ErrSubPlanIDIsZeroOnCreate, + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + var count int64 + db.Model(&models.Plan{}).Where("name = ?", "无效子计划关联").Count(&count) + assert.Equal(t, int64(0), count, "计划不应被创建") + }, + }, + { + name: "失败-子计划在数据库中不存在", + setupDB: func(db *gorm.DB) { + // 不创建ID为999的计划 + }, + inputPlan: &models.Plan{ + Name: "不存在的子计划", + ContentType: models.PlanContentTypeSubPlans, + SubPlans: []models.SubPlan{ + {ChildPlanID: 999, ChildPlan: &models.Plan{Model: gorm.Model{ID: 999}}}, // 关联一个不存在的ID + }, + }, + expectedError: repository.ErrNodeDoesNotExist, + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + var count int64 + db.Model(&models.Plan{}).Where("name = ?", "不存在的子计划").Count(&count) + assert.Equal(t, int64(0), count, "计划不应被创建") + }, + }, + { + name: "失败-任务执行顺序重复", + setupDB: func(db *gorm.DB) { + // 无需额外设置 + }, + inputPlan: &models.Plan{ + Name: "重复任务顺序计划", + ContentType: models.PlanContentTypeTasks, + Tasks: []models.Task{ + {Name: "Task 1", ExecutionOrder: 1}, + {Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序 + }, + }, + expectedError: fmt.Errorf("任务执行顺序重复: %d", 1), // 假设 Create 方法会返回此错误 + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + var count int64 + db.Model(&models.Plan{}).Where("name = ?", "重复任务顺序计划").Count(&count) + assert.Equal(t, int64(0), count, "重复任务顺序的计划不应被创建") + }, + }, + { + name: "失败-子计划执行顺序重复", + setupDB: func(db *gorm.DB) { + createExistingPlan(db, "子计划A", models.PlanContentTypeTasks) + createExistingPlan(db, "子计划B", models.PlanContentTypeTasks) + }, + inputPlan: &models.Plan{ + Name: "重复子计划顺序计划", + ContentType: models.PlanContentTypeSubPlans, + SubPlans: []models.SubPlan{ + {ChildPlanID: 1, ExecutionOrder: 1}, + {ChildPlanID: 2, ExecutionOrder: 1}, // 重复的顺序 + }, + }, + expectedError: fmt.Errorf("子计划执行顺序重复: %d", 1), // 假设 Create 方法会返回此错误 + verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { + var count int64 + db.Model(&models.Plan{}).Where("name = ?", "重复子计划顺序计划").Count(&count) + assert.Equal(t, int64(0), count, "重复子计划顺序的计划不应被创建") + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + db := setupTestDB(t) + repo := repository.NewGormPlanRepository(db) + + // 准备数据库状态 + tc.setupDB(db) + + // 执行 Create 操作 + err := repo.Create(tc.inputPlan) + + // 断言错误 + if tc.expectedError != nil { + assert.Error(t, err) + // 使用 Contains 检查错误信息,因为 fmt.Errorf 会创建新的错误实例 + assert.Contains(t, err.Error(), tc.expectedError.Error()) + } else { + assert.NoError(t, err) + } + + // 验证数据库状态 + tc.verifyDB(t, db, tc.inputPlan) + }) + } +}