Compare commits
	
		
			3 Commits
		
	
	
		
			01b11b6e42
			...
			ec2595a751
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ec2595a751 | |||
| 5e8ed88832 | |||
| 2228d8e879 | 
@@ -1,12 +1,20 @@
 | 
				
			|||||||
package repository
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 定义仓库层可导出的公共错误
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						ErrUpdateWithInvalidRoot = errors.New("更新操作的目标根计划无效或ID为0")
 | 
				
			||||||
 | 
						ErrNewSubPlanInUpdate    = errors.New("计划树中包含一个ID为0的新子计划,更新操作只允许关联已存在的计划")
 | 
				
			||||||
 | 
						ErrNodeDoesNotExist      = errors.New("计划树中包含一个或多个在数据库中不存在的计划")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PlanRepository 定义了与计划模型相关的数据库操作接口
 | 
					// PlanRepository 定义了与计划模型相关的数据库操作接口
 | 
				
			||||||
// 这是为了让业务逻辑层依赖于抽象,而不是具体的数据库实现
 | 
					// 这是为了让业务逻辑层依赖于抽象,而不是具体的数据库实现
 | 
				
			||||||
type PlanRepository interface {
 | 
					type PlanRepository interface {
 | 
				
			||||||
@@ -16,6 +24,8 @@ type PlanRepository interface {
 | 
				
			|||||||
	GetBasicPlanByID(id uint) (*models.Plan, error)
 | 
						GetBasicPlanByID(id uint) (*models.Plan, error)
 | 
				
			||||||
	// GetPlanByID 根据ID获取计划,包含子计划和任务详情
 | 
						// GetPlanByID 根据ID获取计划,包含子计划和任务详情
 | 
				
			||||||
	GetPlanByID(id uint) (*models.Plan, error)
 | 
						GetPlanByID(id uint) (*models.Plan, error)
 | 
				
			||||||
 | 
						// UpdatePlan 更新计划,包括子计划和任务
 | 
				
			||||||
 | 
						UpdatePlan(plan *models.Plan) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// gormPlanRepository 是 PlanRepository 的 GORM 实现
 | 
					// gormPlanRepository 是 PlanRepository 的 GORM 实现
 | 
				
			||||||
@@ -96,3 +106,196 @@ func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return &plan, nil
 | 
						return &plan, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePlan 是更新计划的公共入口点
 | 
				
			||||||
 | 
					func (r *gormPlanRepository) UpdatePlan(plan *models.Plan) error {
 | 
				
			||||||
 | 
						return r.db.Transaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							return r.updatePlanTx(tx, plan)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// updatePlanTx 在事务中协调整个更新过程
 | 
				
			||||||
 | 
					func (r *gormPlanRepository) updatePlanTx(tx *gorm.DB, plan *models.Plan) error {
 | 
				
			||||||
 | 
						if err := r.validatePlanTree(tx, plan); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.reconcilePlanNode(tx, plan)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// validatePlanTree 对整个计划树进行全面的只读健康检查
 | 
				
			||||||
 | 
					func (r *gormPlanRepository) validatePlanTree(tx *gorm.DB, plan *models.Plan) error {
 | 
				
			||||||
 | 
						// 1. 检查根节点
 | 
				
			||||||
 | 
						if plan == nil || plan.ID == 0 {
 | 
				
			||||||
 | 
							return ErrUpdateWithInvalidRoot
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(plan.Tasks) > 0 && len(plan.SubPlans) > 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("计划 (ID: %d) 不能同时包含任务和子计划", plan.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 递归验证所有子节点,并检测循环引用
 | 
				
			||||||
 | 
						allIDs := make(map[uint]bool)
 | 
				
			||||||
 | 
						recursionStack := make(map[uint]bool)
 | 
				
			||||||
 | 
						if err := validateNodeAndDetectCycles(plan, allIDs, recursionStack); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3. 一次性数据库存在性校验
 | 
				
			||||||
 | 
						var idsToCheck []uint
 | 
				
			||||||
 | 
						for id := range allIDs {
 | 
				
			||||||
 | 
							idsToCheck = append(idsToCheck, id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(idsToCheck) > 0 {
 | 
				
			||||||
 | 
							var count int64
 | 
				
			||||||
 | 
							if err := tx.Model(&models.Plan{}).Where("id IN ?", idsToCheck).Count(&count).Error; err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("检查计划存在性时出错: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if int(count) != len(idsToCheck) {
 | 
				
			||||||
 | 
								return ErrNodeDoesNotExist
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// validateNodeAndDetectCycles 递归地验证节点有效性并检测循环引用
 | 
				
			||||||
 | 
					func validateNodeAndDetectCycles(plan *models.Plan, allIDs, recursionStack map[uint]bool) error {
 | 
				
			||||||
 | 
						if plan == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if plan.ID == 0 {
 | 
				
			||||||
 | 
							return ErrNewSubPlanInUpdate
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if recursionStack[plan.ID] {
 | 
				
			||||||
 | 
							return fmt.Errorf("检测到循环引用:计划 (ID: %d) 是其自身的祖先", plan.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if allIDs[plan.ID] {
 | 
				
			||||||
 | 
							return nil // 已经验证过这个节点及其子树,无需重复
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						recursionStack[plan.ID] = true
 | 
				
			||||||
 | 
						allIDs[plan.ID] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, subPlanLink := range plan.SubPlans {
 | 
				
			||||||
 | 
							if err := validateNodeAndDetectCycles(subPlanLink.ChildPlan, allIDs, recursionStack); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						delete(recursionStack, plan.ID) // 回溯,将节点移出当前递归路径
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reconcilePlanNode 递归地同步数据库状态以匹配给定的计划节点
 | 
				
			||||||
 | 
					func (r *gormPlanRepository) reconcilePlanNode(tx *gorm.DB, plan *models.Plan) error {
 | 
				
			||||||
 | 
						if plan == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 1. 更新节点本身的基础字段
 | 
				
			||||||
 | 
						if err := tx.Model(plan).Select("Name", "Description", "ExecutionType", "CronExpression", "ContentType").Updates(plan).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 根据内容类型协调子项
 | 
				
			||||||
 | 
						switch plan.ContentType {
 | 
				
			||||||
 | 
						case models.PlanContentTypeTasks:
 | 
				
			||||||
 | 
							// 清理旧的子计划关联
 | 
				
			||||||
 | 
							if err := tx.Where("parent_plan_id = ?", plan.ID).Delete(&models.SubPlan{}).Error; err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新时清理旧的子计划关联失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 协调任务列表
 | 
				
			||||||
 | 
							return r.reconcileTasks(tx, plan)
 | 
				
			||||||
 | 
						case models.PlanContentTypeSubPlans:
 | 
				
			||||||
 | 
							// 清理旧的任务
 | 
				
			||||||
 | 
							if err := tx.Where("plan_id = ?", plan.ID).Delete(&models.Task{}).Error; err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("更新时清理旧的任务失败: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 协调子计划关联
 | 
				
			||||||
 | 
							return r.reconcileSubPlans(tx, plan)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reconcileTasks 精确同步任务列表
 | 
				
			||||||
 | 
					func (r *gormPlanRepository) reconcileTasks(tx *gorm.DB, plan *models.Plan) error {
 | 
				
			||||||
 | 
						var existingTasks []models.Task
 | 
				
			||||||
 | 
						if err := tx.Where("plan_id = ?", plan.ID).Find(&existingTasks).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						existingTaskMap := make(map[uint]bool)
 | 
				
			||||||
 | 
						for _, task := range existingTasks {
 | 
				
			||||||
 | 
							existingTaskMap[task.ID] = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := range plan.Tasks {
 | 
				
			||||||
 | 
							task := &plan.Tasks[i]
 | 
				
			||||||
 | 
							if task.ID == 0 {
 | 
				
			||||||
 | 
								task.PlanID = plan.ID
 | 
				
			||||||
 | 
								if err := tx.Create(task).Error; err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								delete(existingTaskMap, task.ID) // 从待删除map中移除
 | 
				
			||||||
 | 
								if err := tx.Model(task).Updates(task).Error; err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var tasksToDelete []uint
 | 
				
			||||||
 | 
						for id := range existingTaskMap {
 | 
				
			||||||
 | 
							tasksToDelete = append(tasksToDelete, id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(tasksToDelete) > 0 {
 | 
				
			||||||
 | 
							if err := tx.Delete(&models.Task{}, tasksToDelete).Error; err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reconcileSubPlans 精确同步子计划关联并递归
 | 
				
			||||||
 | 
					func (r *gormPlanRepository) reconcileSubPlans(tx *gorm.DB, plan *models.Plan) error {
 | 
				
			||||||
 | 
						var existingLinks []models.SubPlan
 | 
				
			||||||
 | 
						if err := tx.Where("parent_plan_id = ?", plan.ID).Find(&existingLinks).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						existingLinkMap := make(map[uint]bool)
 | 
				
			||||||
 | 
						for _, link := range existingLinks {
 | 
				
			||||||
 | 
							existingLinkMap[link.ID] = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := range plan.SubPlans {
 | 
				
			||||||
 | 
							link := &plan.SubPlans[i]
 | 
				
			||||||
 | 
							link.ParentPlanID = plan.ID
 | 
				
			||||||
 | 
							if link.ID == 0 {
 | 
				
			||||||
 | 
								if err := tx.Create(link).Error; err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								delete(existingLinkMap, link.ID) // 从待删除map中移除
 | 
				
			||||||
 | 
								if err := tx.Model(link).Updates(link).Error; err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 递归协调子计划节点
 | 
				
			||||||
 | 
							if err := r.reconcilePlanNode(tx, link.ChildPlan); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var linksToDelete []uint
 | 
				
			||||||
 | 
						for id := range existingLinkMap {
 | 
				
			||||||
 | 
							linksToDelete = append(linksToDelete, id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(linksToDelete) > 0 {
 | 
				
			||||||
 | 
							if err := tx.Delete(&models.SubPlan{}, linksToDelete).Error; err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -398,3 +398,629 @@ func cleanPlanForComparison(p *models.Plan) {
 | 
				
			|||||||
		cleanPlanForComparison(p.SubPlans[i].ChildPlan)
 | 
							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},
 | 
				
			||||||
 | 
										{ChildPlanID: 3, ChildPlan: planC},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									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},
 | 
				
			||||||
 | 
										{ChildPlanID: 3, ChildPlan: planC}, // 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: "不能同时包含任务和子计划",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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, "新任务应被创建")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "递归更新-深层节点的变更",
 | 
				
			||||||
 | 
								setupDB: func(db *gorm.DB) uint {
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "A", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "B", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 3}, Name: "C", ContentType: models.PlanContentTypeTasks})
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{Model: gorm.Model{ID: 101}, ParentPlanID: 1, ChildPlanID: 2, ExecutionOrder: 1})
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{Model: gorm.Model{ID: 102}, ParentPlanID: 2, ChildPlanID: 3, ExecutionOrder: 1})
 | 
				
			||||||
 | 
									return 1
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								buildInput: func(db *gorm.DB) *models.Plan {
 | 
				
			||||||
 | 
									planC := &models.Plan{Model: gorm.Model{ID: 3}, Name: "C Updated", ContentType: models.PlanContentTypeTasks}
 | 
				
			||||||
 | 
									planB := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 2},
 | 
				
			||||||
 | 
										Name:        "B",
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans:    []models.SubPlan{{Model: gorm.Model{ID: 102}, ParentPlanID: 2, ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									planA := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 1},
 | 
				
			||||||
 | 
										Name:        "A Updated",
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans:    []models.SubPlan{{Model: gorm.Model{ID: 101}, ParentPlanID: 1, ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return planA
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
 | 
				
			||||||
 | 
									var finalA, finalC models.Plan
 | 
				
			||||||
 | 
									var finalLinkB models.SubPlan
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									db.First(&finalA, 1)
 | 
				
			||||||
 | 
									assert.Equal(t, "A Updated", finalA.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									db.First(&finalLinkB, 102)
 | 
				
			||||||
 | 
									assert.Equal(t, 2, finalLinkB.ExecutionOrder)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									db.First(&finalC, 3)
 | 
				
			||||||
 | 
									assert.Equal(t, "C Updated", finalC.Name)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "递归更新-深层节点的类型转换(C从SubPlans变为Tasks)",
 | 
				
			||||||
 | 
								setupDB: func(db *gorm.DB) uint {
 | 
				
			||||||
 | 
									// 初始状态: A -> B -> C -> D
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "A", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "B", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 3}, Name: "C", ContentType: models.PlanContentTypeSubPlans}) // C的初始类型
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 4}, Name: "D"})
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 2})
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{ParentPlanID: 2, ChildPlanID: 3})
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{Model: gorm.Model{ID: 99}, ParentPlanID: 3, ChildPlanID: 4}) // C->D 的关联
 | 
				
			||||||
 | 
									return 1
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								buildInput: func(db *gorm.DB) *models.Plan {
 | 
				
			||||||
 | 
									// 更新操作: C的类型变为Tasks,并增加一个新Task
 | 
				
			||||||
 | 
									planC := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 3},
 | 
				
			||||||
 | 
										Name:        "C",
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeTasks, // 类型变更
 | 
				
			||||||
 | 
										Tasks:       []models.Task{{Name: "C's New Task"}},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									planB := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 2},
 | 
				
			||||||
 | 
										Name:        "B",
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans:    []models.SubPlan{{ChildPlanID: 3, ChildPlan: planC}},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									planA := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 1},
 | 
				
			||||||
 | 
										Name:        "A",
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans:    []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return planA
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
 | 
				
			||||||
 | 
									// 1. 验证 C->D 的旧关联已被删除
 | 
				
			||||||
 | 
									var linkCount int64
 | 
				
			||||||
 | 
									db.Model(&models.SubPlan{}).Where("id = ?", 99).Count(&linkCount)
 | 
				
			||||||
 | 
									assert.Equal(t, int64(0), linkCount, "C的旧子计划关联应被清理")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 2. 验证 C 的新Task已被创建
 | 
				
			||||||
 | 
									var task models.Task
 | 
				
			||||||
 | 
									err := db.Where("plan_id = ?", 3).First(&task).Error
 | 
				
			||||||
 | 
									assert.NoError(t, err, "C的新任务应该被创建")
 | 
				
			||||||
 | 
									assert.Equal(t, "C's New Task", task.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 3. 验证 D 计划本身依然存在
 | 
				
			||||||
 | 
									var planDCount int64
 | 
				
			||||||
 | 
									db.Model(&models.Plan{}).Where("id = ?", 4).Count(&planDCount)
 | 
				
			||||||
 | 
									assert.Equal(t, int64(1), planDCount, "计划D本身不应被删除")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								name: "递归更新-中间层分支替换(A->B变为A->D)",
 | 
				
			||||||
 | 
								setupDB: func(db *gorm.DB) uint {
 | 
				
			||||||
 | 
									// 初始状态: A -> B -> C
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "A", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "B", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 3}, Name: "C"})
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{Model: gorm.Model{ID: 101}, ParentPlanID: 1, ChildPlanID: 2}) // A->B
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{ParentPlanID: 2, ChildPlanID: 3})                             // B->C
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 准备用于替换的分支: D -> E
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 4}, Name: "D", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 5}, Name: "E"})
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{ParentPlanID: 4, ChildPlanID: 5}) // D->E
 | 
				
			||||||
 | 
									return 1
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								buildInput: func(db *gorm.DB) *models.Plan {
 | 
				
			||||||
 | 
									// 更新操作: A的子计划从 B 替换为 D
 | 
				
			||||||
 | 
									planE := &models.Plan{Model: gorm.Model{ID: 5}, Name: "E Updated"} // 同时更新深层节点
 | 
				
			||||||
 | 
									planD := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 4},
 | 
				
			||||||
 | 
										Name:        "D",
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans:    []models.SubPlan{{ChildPlanID: 5, ChildPlan: planE}},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									planA := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 1},
 | 
				
			||||||
 | 
										Name:        "A",
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans:    []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}}, // 新关联 A->D
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return planA
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
 | 
				
			||||||
 | 
									// 1. 验证 A->B 的旧关联已被删除
 | 
				
			||||||
 | 
									var linkCount int64
 | 
				
			||||||
 | 
									db.Model(&models.SubPlan{}).Where("id = ?", 101).Count(&linkCount)
 | 
				
			||||||
 | 
									assert.Equal(t, int64(0), linkCount, "A->B 的旧关联应被删除")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 2. 验证 A->D 的新关联已创建
 | 
				
			||||||
 | 
									var newLink models.SubPlan
 | 
				
			||||||
 | 
									err := db.Where("parent_plan_id = ? AND child_plan_id = ?", 1, 4).First(&newLink).Error
 | 
				
			||||||
 | 
									assert.NoError(t, err, "A->D 的新关联应被创建")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 3. 验证 B, C, D, E 计划本身都依然存在
 | 
				
			||||||
 | 
									var planCount int64
 | 
				
			||||||
 | 
									db.Model(&models.Plan{}).Where("id IN ?", []uint{2, 3, 4, 5}).Count(&planCount)
 | 
				
			||||||
 | 
									assert.Equal(t, int64(4), planCount, "所有被引用或解引用的计划本身都不应被删除")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 4. 验证对新分支深层节点的递归更新已生效
 | 
				
			||||||
 | 
									var finalE models.Plan
 | 
				
			||||||
 | 
									db.First(&finalE, 5)
 | 
				
			||||||
 | 
									assert.Equal(t, "E Updated", finalE.Name)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "递归更新-菱形依赖下的冲突更新",
 | 
				
			||||||
 | 
								setupDB: func(db *gorm.DB) uint {
 | 
				
			||||||
 | 
									// 初始状态: A -> B -> D, A -> C -> D
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "A", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "B", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 3}, Name: "C", ContentType: models.PlanContentTypeSubPlans})
 | 
				
			||||||
 | 
									db.Create(&models.Plan{Model: gorm.Model{ID: 4}, Name: "D (Original)"}) // D的初始名字
 | 
				
			||||||
 | 
									// 创建关联
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 2, ExecutionOrder: 1}) // A->B
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 3, ExecutionOrder: 2}) // A->C
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{ParentPlanID: 2, ChildPlanID: 4})                    // B->D
 | 
				
			||||||
 | 
									db.Create(&models.SubPlan{ParentPlanID: 3, ChildPlanID: 4})                    // C->D
 | 
				
			||||||
 | 
									return 1
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								buildInput: func(db *gorm.DB) *models.Plan {
 | 
				
			||||||
 | 
									// 在一次调用中,通过不同路径对 D 进行不同的更新
 | 
				
			||||||
 | 
									planD_fromB := &models.Plan{Model: gorm.Model{ID: 4}, Name: "D (Updated from B)"}
 | 
				
			||||||
 | 
									planD_fromC := &models.Plan{Model: gorm.Model{ID: 4}, Name: "D (Updated from C)"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									planB := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 2},
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans:    []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD_fromB}},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									planC := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 3},
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans:    []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD_fromC}},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									planA := &models.Plan{
 | 
				
			||||||
 | 
										Model:       gorm.Model{ID: 1},
 | 
				
			||||||
 | 
										ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
										SubPlans: []models.SubPlan{
 | 
				
			||||||
 | 
											{ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1},
 | 
				
			||||||
 | 
											{ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, // C 在 B 之后
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return planA
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
 | 
				
			||||||
 | 
									// 验证:D 的最终名字应该符合“最后一次更新获胜”的原则
 | 
				
			||||||
 | 
									// 由于 planC 在 planB 之后被处理,D 的名字应该是 "D (Updated from C)"
 | 
				
			||||||
 | 
									var finalD models.Plan
 | 
				
			||||||
 | 
									db.First(&finalD, 4)
 | 
				
			||||||
 | 
									assert.Equal(t, "D (Updated from C)", finalD.Name, "共享下游节点的更新应以后一次执行为准")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 确保所有关联依然存在
 | 
				
			||||||
 | 
									var linkCount int64
 | 
				
			||||||
 | 
									db.Model(&models.SubPlan{}).Where("child_plan_id = ?", 4).Count(&linkCount)
 | 
				
			||||||
 | 
									assert.Equal(t, int64(2), linkCount, "D 的两个上游关联都应继续存在")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user