diff --git a/internal/core/application.go b/internal/core/application.go index 618b39a..9dcd5f3 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -78,6 +78,8 @@ func NewApplication(configPath string) (*Application, error) { pigPenRepo := repository.NewGormPigPenRepository(storage.GetDB()) pigTransferLogRepo := repository.NewGormPigTransferLogRepository(storage.GetDB()) pigTradeRepo := repository.NewGormPigTradeRepository(storage.GetDB()) + pigSickPigLogRepo := repository.NewGormPigSickLogRepository(storage.GetDB()) + pigGroupMedicationLogRepo := repository.NewGormGroupMedicationLogRepository(storage.GetDB()) // 初始化事务管理器 unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) diff --git a/internal/domain/pig/pig_sick_manager.go b/internal/domain/pig/pig_sick_manager.go new file mode 100644 index 0000000..467de02 --- /dev/null +++ b/internal/domain/pig/pig_sick_manager.go @@ -0,0 +1 @@ +package pig diff --git a/internal/infra/models/pig_sick.go b/internal/infra/models/pig_sick.go index 73eed01..9b8ef3f 100644 --- a/internal/infra/models/pig_sick.go +++ b/internal/infra/models/pig_sick.go @@ -27,20 +27,22 @@ const ( SickPigReasonTypeOther PigBatchSickPigReasonType = "其他" // 其他原因 ) -// PigBatchSickPigLog 记录了猪批次中病猪数量的变化日志 -type PigBatchSickPigLog struct { +// PigSickLog 记录了猪批次中病猪数量的变化日志 +type PigSickLog struct { gorm.Model PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"` PenID uint `gorm:"not null;index;comment:所在猪圈ID"` PigIDs string `gorm:"size:500;comment:涉及的猪只ID列表,逗号分隔"` ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"` Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"` + BeforeCount int `gorm:"comment:变化前的数量"` + AfterCount int `gorm:"comment:变化后的数量"` Remarks string `gorm:"size:255;comment:备注"` TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"` OperatorID uint `gorm:"comment:操作员ID"` HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` } -func (PigBatchSickPigLog) TableName() string { - return "pig_batch_sick_pig_logs" +func (PigSickLog) TableName() string { + return "pig_sick_logs" } diff --git a/internal/infra/repository/group_medication_log_repository.go b/internal/infra/repository/group_medication_log_repository.go new file mode 100644 index 0000000..4b5be01 --- /dev/null +++ b/internal/infra/repository/group_medication_log_repository.go @@ -0,0 +1,26 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// GroupMedicationLogRepository 定义了与群体用药日志模型相关的数据库操作接口。 +type GroupMedicationLogRepository interface { + CreateGroupMedicationLog(log *models.GroupMedicationLog) error +} + +// gormGroupMedicationLogRepository 是 GroupMedicationLogRepository 接口的 GORM 实现。 +type gormGroupMedicationLogRepository struct { + db *gorm.DB +} + +// NewGormGroupMedicationLogRepository 创建一个新的 GroupMedicationLogRepository GORM 实现实例。 +func NewGormGroupMedicationLogRepository(db *gorm.DB) GroupMedicationLogRepository { + return &gormGroupMedicationLogRepository{db: db} +} + +// CreateGroupMedicationLog 创建一条新的群体用药日志记录 +func (r *gormGroupMedicationLogRepository) CreateGroupMedicationLog(log *models.GroupMedicationLog) error { + return r.db.Create(log).Error +} diff --git a/internal/infra/repository/pig_sick_repository.go b/internal/infra/repository/pig_sick_repository.go new file mode 100644 index 0000000..c2ff29d --- /dev/null +++ b/internal/infra/repository/pig_sick_repository.go @@ -0,0 +1,26 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// PigSickLogRepository 定义了与病猪日志模型相关的数据库操作接口。 +type PigSickLogRepository interface { + CreatePigSickLog(log *models.PigSickLog) error +} + +// gormPigSickLogRepository 是 PigSickLogRepository 接口的 GORM 实现。 +type gormPigSickLogRepository struct { + db *gorm.DB +} + +// NewGormPigSickLogRepository 创建一个新的 PigSickLogRepository GORM 实现实例。 +func NewGormPigSickLogRepository(db *gorm.DB) PigSickLogRepository { + return &gormPigSickLogRepository{db: db} +} + +// CreatePigSickLog 创建一条新的病猪日志记录 +func (r *gormPigSickLogRepository) CreatePigSickLog(log *models.PigSickLog) error { + return r.db.Create(log).Error +} diff --git a/internal/infra/repository/plan_repository_test.go b/internal/infra/repository/plan_repository_test.go deleted file mode 100644 index ab511be..0000000 --- a/internal/infra/repository/plan_repository_test.go +++ /dev/null @@ -1,1225 +0,0 @@ -package repository_test - -import ( - "errors" - "fmt" - "testing" - "time" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" - "github.com/stretchr/testify/assert" - "gorm.io/gorm" -) - -// createTestPlan 是一个辅助函数,用于创建测试计划。 -func createTestPlan(name, description string, execType models.PlanExecutionType, contentType models.PlanContentType) models.Plan { - return models.Plan{ - Name: name, - Description: description, - ExecutionType: execType, - ContentType: contentType, - } -} - -// TestListBasicPlans 测试 ListBasicPlans 方法,确保它能正确返回所有计划的基本信息。 -func TestListBasicPlans(t *testing.T) { - tests := []struct { - name string - setupPlans []models.Plan - expectedCount int - expectedError error - }{ - { - name: "数据库中没有计划", - setupPlans: []models.Plan{}, - expectedCount: 0, - expectedError: nil, - }, - { - name: "数据库中有多个计划", - setupPlans: []models.Plan{ - createTestPlan("计划 A", "描述 A", models.PlanExecutionTypeAutomatic, models.PlanContentTypeTasks), - createTestPlan("计划 B", "描述 B", models.PlanExecutionTypeManual, models.PlanContentTypeSubPlans), - }, - expectedCount: 2, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := setupTestDB(t) - repo := repository.NewGormPlanRepository(db) - - for i := range tt.setupPlans { - err := db.Create(&tt.setupPlans[i]).Error - assert.NoError(t, err, "插入设置计划失败") - } - - plans, err := repo.ListBasicPlans() - - if tt.expectedError != nil { - assert.Error(t, err) - assert.True(t, errors.Is(err, tt.expectedError)) - } else { - assert.NoError(t, err) - assert.Len(t, plans, tt.expectedCount) - if tt.expectedCount > 0 { - // 验证返回的计划是否包含预期的ID - var actualIDs []uint - for _, p := range plans { - actualIDs = append(actualIDs, p.ID) - assert.Empty(t, p.SubPlans, "ListBasicPlans 不应加载子计划") - assert.Empty(t, p.Tasks, "ListBasicPlans 不应加载任务") - } - for _, setupPlan := range tt.setupPlans { - assert.Contains(t, actualIDs, setupPlan.ID, "返回的计划应包含设置计划的ID") - } - } - } - }) - } -} - -// TestGetBasicPlanByID 测试 GetBasicPlanByID 方法,确保它能根据ID正确返回计划的基本信息。 -func TestGetBasicPlanByID(t *testing.T) { - tests := []struct { - name string - setupPlan models.Plan - idToFetch uint - expectFound bool - expectedError error - }{ - { - name: "通过ID找到计划", - setupPlan: createTestPlan("计划 C", "描述 C", models.PlanExecutionTypeAutomatic, models.PlanContentTypeTasks), - idToFetch: 0, // 创建后设置 - expectFound: true, - }, - { - name: "通过ID未找到计划", - setupPlan: models.Plan{}, // 此情况下无需设置计划 - idToFetch: 999, - expectFound: false, - expectedError: gorm.ErrRecordNotFound, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := setupTestDB(t) - repo := repository.NewGormPlanRepository(db) - - if tt.setupPlan.Name != "" { // 仅在有效设置时创建计划 - err := db.Create(&tt.setupPlan).Error - assert.NoError(t, err, "插入设置计划失败") - tt.idToFetch = tt.setupPlan.ID // 使用数据库生成的ID - } - - fetchedPlan, err := repo.GetBasicPlanByID(tt.idToFetch) - - if tt.expectedError != nil { - assert.Error(t, err) - assert.True(t, errors.Is(err, tt.expectedError), "预期错误类型不匹配") - assert.Nil(t, fetchedPlan) - } else { - assert.NoError(t, err) - assert.NotNil(t, fetchedPlan) - assert.Equal(t, tt.setupPlan.Name, fetchedPlan.Name) - assert.Equal(t, tt.setupPlan.Description, fetchedPlan.Description) - assert.Equal(t, tt.setupPlan.ExecutionType, fetchedPlan.ExecutionType) - assert.Equal(t, tt.setupPlan.ContentType, fetchedPlan.ContentType) - assert.Empty(t, fetchedPlan.SubPlans, "GetBasicPlanByID 不应加载子计划") - assert.Empty(t, fetchedPlan.Tasks, "GetBasicPlanByID 不应加载任务") - } - }) - } -} - -// TestGetPlanByID 测试 GetPlanByID 方法,确保它能根据ID正确返回计划的完整信息,包括关联数据。 -func TestGetPlanByID(t *testing.T) { - type testCase struct { - name string - setupData func(db *gorm.DB) // 用于在测试前插入数据 - planID uint - expectedPlan *models.Plan - expectedError string - } - - testCases := []testCase{ - { - name: "PlanNotFound", - setupData: func(db *gorm.DB) { - // 不插入任何数据 - }, - planID: 999, - expectedPlan: nil, - expectedError: "record not found", - }, - { - name: "PlanWithTasks", - setupData: func(db *gorm.DB) { - // 使用硬编码的ID创建计划,使测试可预测 - plan := models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan A", - ContentType: models.PlanContentTypeTasks, - } - db.Create(&plan) - // 创建任务,它们的ID将由数据库自动生成 - db.Create(&models.Task{PlanID: 1, Name: "Task 2", ExecutionOrder: 1}) - db.Create(&models.Task{PlanID: 1, Name: "Task 1", ExecutionOrder: 2}) - }, - planID: 1, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan A", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - // 期望按 "order" 字段升序排序 - {PlanID: 1, Name: "Task 2", ExecutionOrder: 1}, - {PlanID: 1, Name: "Task 1", ExecutionOrder: 2}, - }, - }, - expectedError: "", - }, - { - name: "PlanWithMultiLevelSubPlans", - setupData: func(db *gorm.DB) { - // 创建一个三层结构的计划 - db.Create(&models.Plan{Model: gorm.Model{ID: 20}, Name: "Grandparent Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 21}, Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 22}, Name: "Child Plan With Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 22, Name: "Grandchild's Task", ExecutionOrder: 1}) - - // 创建关联关系 - db.Create(&models.SubPlan{ParentPlanID: 20, ChildPlanID: 21, ExecutionOrder: 1}) - db.Create(&models.SubPlan{ParentPlanID: 21, ChildPlanID: 22, ExecutionOrder: 1}) - }, - planID: 20, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 20}, - Name: "Grandparent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 20, - ChildPlanID: 21, - ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 21}, - Name: "Parent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 21, - ChildPlanID: 22, - ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 22}, - Name: "Child Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {PlanID: 22, Name: "Grandchild's Task", ExecutionOrder: 1}, - }, - }, - }, - }, - }, - }, - }, - }, - expectedError: "", - }, - { - name: "UnknownContentType", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{ - Model: gorm.Model{ID: 30}, - Name: "Unknown Type Plan", - ContentType: "INVALID_TYPE", - }) - }, - planID: 30, - expectedPlan: nil, - expectedError: fmt.Sprintf("未知的计划内容类型: INVALID_TYPE; 计划ID: 30"), - }, - // 新增场景:测试空的关联数据 - { - name: "PlanWithTasksType_ButNoTasks", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{ - Model: gorm.Model{ID: 50}, - Name: "Plan with empty tasks", - ContentType: models.PlanContentTypeTasks, - }) - }, - planID: 50, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 50}, - Name: "Plan with empty tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{}, // 期望一个空的切片 - }, - expectedError: "", - }, - // 新增场景:测试复杂的同级排序 - { - name: "PlanWithSubPlans_ComplexSorting", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 60}, Name: "Main Plan For Sorting", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 61}, Name: "SubPlan C", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Plan{Model: gorm.Model{ID: 62}, Name: "SubPlan A", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Plan{Model: gorm.Model{ID: 63}, Name: "SubPlan B", ContentType: models.PlanContentTypeTasks}) - // 故意打乱顺序插入 - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 61, ExecutionOrder: 3}) - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 62, ExecutionOrder: 1}) - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 63, ExecutionOrder: 2}) - }, - planID: 60, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 60}, - Name: "Main Plan For Sorting", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ParentPlanID: 60, ChildPlanID: 62, ExecutionOrder: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 62}, Name: "SubPlan A", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - {ParentPlanID: 60, ChildPlanID: 63, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 63}, Name: "SubPlan B", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - {ParentPlanID: 60, ChildPlanID: 61, ExecutionOrder: 3, ChildPlan: &models.Plan{Model: gorm.Model{ID: 61}, Name: "SubPlan C", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - }, - }, - expectedError: "", - }, - // 新增场景:测试混合内容的子计划树 - { - name: "PlanWithSubPlans_MixedContentTypes", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 70}, Name: "Mixed Main Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 71}, Name: "Child with SubPlans", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 72}, Name: "Grandchild with Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 72, Name: "Grandchild's Task", ExecutionOrder: 1}) - db.Create(&models.Plan{Model: gorm.Model{ID: 73}, Name: "Child with Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 73, Name: "Child's Task", ExecutionOrder: 1}) - - // 创建关联 - db.Create(&models.SubPlan{ParentPlanID: 70, ChildPlanID: 71, ExecutionOrder: 1}) // Main -> Child with SubPlans - db.Create(&models.SubPlan{ParentPlanID: 70, ChildPlanID: 73, ExecutionOrder: 2}) // Main -> Child with Tasks - db.Create(&models.SubPlan{ParentPlanID: 71, ChildPlanID: 72, ExecutionOrder: 1}) // Child with SubPlans -> Grandchild - }, - planID: 70, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 70}, - Name: "Mixed Main Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 70, ChildPlanID: 71, ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 71}, Name: "Child with SubPlans", ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ParentPlanID: 71, ChildPlanID: 72, ExecutionOrder: 1, ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 72}, Name: "Grandchild with Tasks", ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{{PlanID: 72, Name: "Grandchild's Task", ExecutionOrder: 1}}, - }}, - }, - }, - }, - { - ParentPlanID: 70, ChildPlanID: 73, ExecutionOrder: 2, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 73}, Name: "Child with Tasks", ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{{PlanID: 73, Name: "Child's Task", ExecutionOrder: 1}}, - }, - }, - }, - }, - expectedError: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - // 使用 defer 确保数据库连接在测试结束后关闭 - sqlDB, _ := db.DB() - defer sqlDB.Close() - - tc.setupData(db) - - repo := repository.NewGormPlanRepository(db) - plan, err := repo.GetPlanByID(tc.planID) - - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - assert.Nil(t, plan) - } else { - assert.NoError(t, err) - assert.NotNil(t, plan) - - // 在比较之前,清理实际结果和期望结果中所有不确定的、由数据库自动生成的字段 - cleanPlanForComparison(plan) - cleanPlanForComparison(tc.expectedPlan) - - assert.Equal(t, tc.expectedPlan, plan) - } - }) - } -} - -// cleanPlanForComparison 递归地重置 Plan 对象及其关联对象中由数据库自动生成的字段(如ID和时间戳), -// 以便在测试中断言它们与期望值相等。 -func cleanPlanForComparison(p *models.Plan) { - if p == nil { - return - } - - // 重置 Plan 自身的时间戳 - p.CreatedAt = time.Time{} - p.UpdatedAt = time.Time{} - p.DeletedAt = gorm.DeletedAt{} - - // 重置所有 Task 的自动生成字段 - for i := range p.Tasks { - p.Tasks[i].ID = 0 // ID是自动生成的,必须重置为0才能与期望值匹配 - p.Tasks[i].CreatedAt = time.Time{} - p.Tasks[i].UpdatedAt = time.Time{} - p.Tasks[i].DeletedAt = gorm.DeletedAt{} - } - - // 重置所有 SubPlan 的自动生成字段,并递归清理子计划 - for i := range p.SubPlans { - p.SubPlans[i].ID = 0 // SubPlan 连接记录的ID也是自动生成的 - p.SubPlans[i].CreatedAt = time.Time{} - p.SubPlans[i].UpdatedAt = time.Time{} - p.SubPlans[i].DeletedAt = gorm.DeletedAt{} - - // 递归调用以清理嵌套的子计划 - cleanPlanForComparison(p.SubPlans[i].ChildPlan) - } -} - -// TestUpdatePlan_Validation 专注于测试 UpdatePlan 中前置检查逻辑的各种失败和成功场景。 -func TestUpdatePlan_Validation(t *testing.T) { - // 定义Go测试中使用的计划实体 - planA := &models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan A", ContentType: models.PlanContentTypeSubPlans} - planB := &models.Plan{Model: gorm.Model{ID: 2}, Name: "Plan B", ContentType: models.PlanContentTypeSubPlans} - planC := &models.Plan{Model: gorm.Model{ID: 3}, Name: "Plan C", ContentType: models.PlanContentTypeSubPlans} - planD := &models.Plan{Model: gorm.Model{ID: 4}, Name: "Plan D", ContentType: models.PlanContentTypeTasks} - planNew := &models.Plan{Model: gorm.Model{ID: 0}, Name: "New Plan"} // ID为0的新计划 - - type testCase struct { - name string - setupDB func(db *gorm.DB) - buildInput func() *models.Plan // 修改为构建函数 - expectedError string // 保持 string 类型 - } - - testCases := []testCase{ - { - name: "成功-合法的菱形依赖树", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 3}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 4}}) - }, - buildInput: func() *models.Plan { - planD.ContentType = models.PlanContentTypeTasks - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}} - planC.ContentType = models.PlanContentTypeSubPlans - planC.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}} - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}, - {ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, - } - return planA - }, - expectedError: "", // 期望没有错误 - }, - { - name: "错误-根节点ID为零", - setupDB: func(db *gorm.DB) {}, - buildInput: func() *models.Plan { return planNew }, - expectedError: repository.ErrUpdateWithInvalidRoot.Error(), - }, - { - name: "错误-子计划ID为零", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlan: planNew}} - return planA - }, - expectedError: repository.ErrNewSubPlanInUpdate.Error(), - }, - { - name: "错误-节点在数据库中不存在", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}, - {ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, // C 不存在 - } - return planA - }, - expectedError: repository.ErrNodeDoesNotExist.Error(), - }, - { - name: "错误-简单循环引用(A->B->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-复杂循环引用(A->B->C->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 3}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 3, ChildPlan: planC}} - planC.ContentType = models.PlanContentTypeSubPlans - planC.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-自引用(A->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-根节点内容混合", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planA.Tasks = []models.Task{{Name: "A's Task"}} - return planA - }, - 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 { - t.Run(tc.name, func(t *testing.T) { - // 1. 为每个测试用例重置基础对象的状态 - *planA = models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan A"} - *planB = models.Plan{Model: gorm.Model{ID: 2}, Name: "Plan B"} - *planC = models.Plan{Model: gorm.Model{ID: 3}, Name: "Plan C"} - *planD = models.Plan{Model: gorm.Model{ID: 4}, Name: "Plan D"} - *planNew = models.Plan{Model: gorm.Model{ID: 0}, Name: "New Plan"} - - // 2. 设置数据库 - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - tc.setupDB(db) - - // 3. 在对象重置后,构建本次测试需要的输入结构 - input := tc.buildInput() - - // 4. 执行测试 - repo := repository.NewGormPlanRepository(db) - err := repo.UpdatePlan(input) - - // 5. 断言结果 - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - } else { - assert.NoError(t, err) - } - }) - } -} - -// TestUpdatePlan_Reconciliation 专注于测试 UpdatePlan 成功执行后,数据库状态是否与预期一致。 -func TestUpdatePlan_Reconciliation(t *testing.T) { - type testCase struct { - name string - setupDB func(db *gorm.DB) (rootPlanID uint) - buildInput func(db *gorm.DB) *models.Plan - verifyDB func(t *testing.T, db *gorm.DB, rootPlanID uint) - } - - testCases := []testCase{ - { - name: "任务协调-新增一个任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan With Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: 1, Name: "Task 1", ExecutionOrder: 1}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 1}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}, - {Name: "New Task 2", ExecutionOrder: 2}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks").First(&finalPlan, rootPlanID) - assert.Len(t, finalPlan.Tasks, 2) - assert.Equal(t, "New Task 2", finalPlan.Tasks[1].Name) - }, - }, - { - name: "任务协调-删除一个任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan With Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "Task to Delete", ExecutionOrder: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var tasks []models.Task - db.Where("plan_id = ?", rootPlanID).Find(&tasks) - assert.Len(t, tasks, 1) - var count int64 - db.Model(&models.Task{}).Where("id = ?", 11).Count(&count) - assert.Equal(t, int64(0), count, "被删除的任务不应再存在") - }, - }, - { - name: "任务协调-更新并重排序任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "A", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "B", ExecutionOrder: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 11}, PlanID: 1, Name: "B Updated", ExecutionOrder: 1}, - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "A", ExecutionOrder: 2}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks", func(db *gorm.DB) *gorm.DB { - return db.Order("execution_order") - }).First(&finalPlan, rootPlanID) - assert.Len(t, finalPlan.Tasks, 2) - assert.Equal(t, "B Updated", finalPlan.Tasks[0].Name) - assert.Equal(t, uint(11), finalPlan.Tasks[0].ID) - }, - }, - { - name: "任务协调-混沌的同步操作(增删改重排)", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1 (Original)", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "Task 2 (To Be Deleted)", ExecutionOrder: 2}) - db.Create(&models.Task{Model: gorm.Model{ID: 12}, PlanID: 1, Name: "Task 3 (Original)", ExecutionOrder: 3}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - // T4 (新) -> T3 (不变) -> T1 (更新) - {Name: "Task 4 (New)", ExecutionOrder: 1}, - {Model: gorm.Model{ID: 12}, PlanID: 1, Name: "Task 3 (Original)", ExecutionOrder: 2}, - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1 (Updated)", ExecutionOrder: 3}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks", func(db *gorm.DB) *gorm.DB { - return db.Order("execution_order") - }).First(&finalPlan, rootPlanID) - - // 验证最终数量 - assert.Len(t, finalPlan.Tasks, 3) - - // 验证被删除的 T2 不存在 - var count int64 - db.Model(&models.Task{}).Where("id = ?", 11).Count(&count) - assert.Equal(t, int64(0), count) - - // 验证顺序和内容 - assert.Equal(t, "Task 4 (New)", finalPlan.Tasks[0].Name) - assert.Equal(t, "Task 3 (Original)", finalPlan.Tasks[1].Name) - assert.Equal(t, "Task 1 (Updated)", finalPlan.Tasks[2].Name) - }, - }, - { - name: "子计划协调-新增一个关联", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Parent", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "Existing Child"}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Parent", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{{ChildPlanID: 2}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var links []models.SubPlan - db.Where("parent_plan_id = ?", rootPlanID).Find(&links) - assert.Len(t, links, 1) - assert.Equal(t, uint(2), links[0].ChildPlanID) - }, - }, - { - name: "子计划协调-删除一个关联", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Parent", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "Child To Unlink"}) - db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Parent", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(0), linkCount) - - var planCount int64 - db.Model(&models.Plan{}).Where("id = ?", 2).Count(&planCount) - assert.Equal(t, int64(1), planCount, "子计划本身不应被删除") - }, - }, - { - name: "类型转换-从任务切换到子计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: 1, Name: "Old Task"}) - db.Create(&models.Plan{Model: gorm.Model{ID: 10}, Name: "New Child"}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{{ChildPlanID: 10}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", rootPlanID).Count(&taskCount) - assert.Equal(t, int64(0), taskCount, "旧任务应被清理") - - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(1), linkCount, "新关联应被创建") - }, - }, - { - name: "类型转换-从子计划切换到任务", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 10}, Name: "Old Child"}) - db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 10}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, // 类型变更 - Tasks: []models.Task{{Name: "New Task"}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(0), linkCount, "旧的子计划关联应被清理") - - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", rootPlanID).Count(&taskCount) - assert.Equal(t, int64(1), taskCount, "新任务应被创建") - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - - rootID := tc.setupDB(db) - input := tc.buildInput(db) - - repo := repository.NewGormPlanRepository(db) - err := repo.UpdatePlan(input) - assert.NoError(t, err) - - tc.verifyDB(t, db, rootID) - }) - } -} - -// 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 // 传入 CreateTx 方法的计划对象 - 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}, // 关联已存在的子计划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), // 假设 CreateTx 方法会返回此错误 - 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), // 假设 CreateTx 方法会返回此错误 - 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) - - // 执行 CreateTx 操作 - err := repo.CreatePlan(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) - }) - } -} - -func TestPlanRepository_DeletePlan(t *testing.T) { - type testCase struct { - name string - setupDB func(db *gorm.DB) (planToDeleteID uint) - expectedError string - verifyDB func(t *testing.T, db *gorm.DB, planToDeleteID uint) - } - - testCases := []testCase{ - { - name: "成功删除-包含任务的计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Name: "Plan with Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: plan.ID, Name: "Task 1"}) - db.Create(&models.Task{PlanID: plan.ID, Name: "Task 2"}) - return plan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var plan models.Plan - err := db.First(&plan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "计划应被删除") - - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", planToDeleteID).Count(&taskCount) - assert.Equal(t, int64(0), taskCount, "关联任务应被删除") - }, - }, - { - name: "成功删除-包含子计划链接的计划", - setupDB: func(db *gorm.DB) uint { - parentPlan := models.Plan{Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans} - childPlan := models.Plan{Name: "Child Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&parentPlan) - db.Create(&childPlan) - db.Create(&models.SubPlan{ParentPlanID: parentPlan.ID, ChildPlanID: childPlan.ID}) - return parentPlan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var parentPlan models.Plan - err := db.First(&parentPlan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "父计划应被删除") - - var subPlanLinkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", planToDeleteID).Count(&subPlanLinkCount) - assert.Equal(t, int64(0), subPlanLinkCount, "子计划链接应被删除") - - // 验证子计划本身未被删除 - var childPlan models.Plan - err = db.First(&childPlan, 2).Error // Assuming childPlan.ID is 2 from setup - assert.NoError(t, err, "子计划本身不应被删除") - }, - }, - { - name: "失败删除-作为子计划的计划", - setupDB: func(db *gorm.DB) uint { - parentPlan := models.Plan{Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans} - childPlan := models.Plan{Name: "Child Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&parentPlan) - db.Create(&childPlan) - db.Create(&models.SubPlan{ParentPlanID: parentPlan.ID, ChildPlanID: childPlan.ID}) - return childPlan.ID // 尝试删除子计划 - }, - expectedError: repository.ErrDeleteWithReferencedPlan.Error(), - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var childPlan models.Plan - err := db.First(&childPlan, planToDeleteID).Error - assert.NoError(t, err, "子计划不应被删除") - - var subPlanLinkCount int64 - db.Model(&models.SubPlan{}).Where("child_plan_id = ?", planToDeleteID).Count(&subPlanLinkCount) - assert.Equal(t, int64(1), subPlanLinkCount, "子计划链接不应被删除") - }, - }, - { - name: "失败删除-不存在的计划", - setupDB: func(db *gorm.DB) uint { - return 999 // 不存在的ID - }, - expectedError: "record not found", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - // 数据库状态应保持不变 - }, - }, - { - name: "成功删除-不含任何关联的计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Name: "Simple Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - return plan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var plan models.Plan - err := db.First(&plan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "计划应被删除") - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - - planToDeleteID := tc.setupDB(db) - - repo := repository.NewGormPlanRepository(db) - err := repo.DeletePlan(planToDeleteID) - - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - } else { - assert.NoError(t, err) - } - - tc.verifyDB(t, db, planToDeleteID) - }) - } -}