Compare commits
	
		
			2 Commits
		
	
	
		
			9fc9cda08e
			...
			b0ce191aff
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b0ce191aff | |||
| fc0e8a36ce | 
							
								
								
									
										6
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Makefile
									
									
									
									
									
								
							@@ -10,6 +10,7 @@ help:
 | 
			
		||||
	@echo "  build        Build the application"
 | 
			
		||||
	@echo "  clean        Clean generated files"
 | 
			
		||||
	@echo "  test         Run all tests"
 | 
			
		||||
	@echo "	 swag        Generate swagger docs"
 | 
			
		||||
	@echo "  help         Show this help message"
 | 
			
		||||
 | 
			
		||||
# 运行应用
 | 
			
		||||
@@ -31,3 +32,8 @@ clean:
 | 
			
		||||
.PHONY: test
 | 
			
		||||
test:
 | 
			
		||||
	go test --count=1 ./...
 | 
			
		||||
 | 
			
		||||
# 生成swagger文档
 | 
			
		||||
.PHONY: swag
 | 
			
		||||
swag:
 | 
			
		||||
	swag init
 | 
			
		||||
@@ -10,12 +10,13 @@ import (
 | 
			
		||||
 | 
			
		||||
// 定义仓库层可导出的公共错误
 | 
			
		||||
var (
 | 
			
		||||
	ErrUpdateWithInvalidRoot   = errors.New("更新操作的目标根计划无效或ID为0")
 | 
			
		||||
	ErrNewSubPlanInUpdate      = errors.New("计划树中包含一个ID为0的新子计划,更新操作只允许关联已存在的计划")
 | 
			
		||||
	ErrNodeDoesNotExist        = errors.New("计划树中包含一个或多个在数据库中不存在的计划")
 | 
			
		||||
	ErrCreateWithNonZeroID     = errors.New("创建操作的计划ID必须为0")
 | 
			
		||||
	ErrSubPlanIDIsZeroOnCreate = errors.New("子计划ID为0,创建操作只允许关联已存在的计划")
 | 
			
		||||
	ErrMixedContent            = errors.New("计划不能同时包含任务和子计划")
 | 
			
		||||
	ErrUpdateWithInvalidRoot    = errors.New("更新操作的目标根计划无效或ID为0")
 | 
			
		||||
	ErrNewSubPlanInUpdate       = errors.New("计划树中包含一个ID为0的新子计划,更新操作只允许关联已存在的计划")
 | 
			
		||||
	ErrNodeDoesNotExist         = errors.New("计划树中包含一个或多个在数据库中不存在的计划")
 | 
			
		||||
	ErrCreateWithNonZeroID      = errors.New("创建操作的计划ID必须为0")
 | 
			
		||||
	ErrSubPlanIDIsZeroOnCreate  = errors.New("子计划ID为0,创建操作只允许关联已存在的计划")
 | 
			
		||||
	ErrMixedContent             = errors.New("计划不能同时包含任务和子计划")
 | 
			
		||||
	ErrDeleteWithReferencedPlan = errors.New("禁止删除正在被引用的计划")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PlanRepository 定义了与计划模型相关的数据库操作接口
 | 
			
		||||
@@ -27,10 +28,12 @@ type PlanRepository interface {
 | 
			
		||||
	GetBasicPlanByID(id uint) (*models.Plan, error)
 | 
			
		||||
	// GetPlanByID 根据ID获取计划,包含子计划和任务详情
 | 
			
		||||
	GetPlanByID(id uint) (*models.Plan, error)
 | 
			
		||||
	// Create 创建一个新的计划
 | 
			
		||||
	Create(plan *models.Plan) error
 | 
			
		||||
	// CreatePlan 创建一个新的计划
 | 
			
		||||
	CreatePlan(plan *models.Plan) error
 | 
			
		||||
	// UpdatePlan 更新计划,包括子计划和任务
 | 
			
		||||
	UpdatePlan(plan *models.Plan) error
 | 
			
		||||
	// DeletePlan 根据ID删除计划,同时删除其关联的任务(非子任务)或子计划关联
 | 
			
		||||
	DeletePlan(id uint) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// gormPlanRepository 是 PlanRepository 的 GORM 实现
 | 
			
		||||
@@ -112,8 +115,8 @@ func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
 | 
			
		||||
	return &plan, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建一个新的计划
 | 
			
		||||
func (r *gormPlanRepository) Create(plan *models.Plan) error {
 | 
			
		||||
// CreatePlan 创建一个新的计划
 | 
			
		||||
func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error {
 | 
			
		||||
	return r.db.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		// 1. 前置校验
 | 
			
		||||
		if plan.ID != 0 {
 | 
			
		||||
@@ -362,4 +365,43 @@ func (r *gormPlanRepository) reconcileSubPlans(tx *gorm.DB, plan *models.Plan) e
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// ABC
 | 
			
		||||
// DeletePlan 根据ID删除计划,同时删除其关联的任务(非子任务)或子计划关联
 | 
			
		||||
func (r *gormPlanRepository) DeletePlan(id uint) error {
 | 
			
		||||
	return r.db.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		// 1. 检查该计划是否是其他计划的子计划
 | 
			
		||||
		var count int64
 | 
			
		||||
		if err := tx.Model(&models.SubPlan{}).Where("child_plan_id = ?", id).Count(&count).Error; err != nil {
 | 
			
		||||
			return fmt.Errorf("检查计划是否为子计划失败: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		if count > 0 {
 | 
			
		||||
			return ErrDeleteWithReferencedPlan
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var plan models.Plan
 | 
			
		||||
		// 2. 获取计划以确定其内容类型
 | 
			
		||||
		if err := tx.First(&plan, id).Error; err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 3. 根据内容类型删除关联数据
 | 
			
		||||
		switch plan.ContentType {
 | 
			
		||||
		case models.PlanContentTypeTasks:
 | 
			
		||||
			// 删除与此计划关联的所有非子任务
 | 
			
		||||
			if err := tx.Where("plan_id = ?", id).Delete(&models.Task{}).Error; err != nil {
 | 
			
		||||
				return fmt.Errorf("删除计划ID %d 的任务失败: %w", id, err)
 | 
			
		||||
			}
 | 
			
		||||
		case models.PlanContentTypeSubPlans:
 | 
			
		||||
			// 删除与此计划关联的所有子计划链接
 | 
			
		||||
			if err := tx.Where("parent_plan_id = ?", id).Delete(&models.SubPlan{}).Error; err != nil {
 | 
			
		||||
				return fmt.Errorf("删除计划ID %d 的子计划关联失败: %w", id, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 4. 删除计划本身
 | 
			
		||||
		if err := tx.Delete(&models.Plan{}, id).Error; err != nil {
 | 
			
		||||
			return fmt.Errorf("删除计划ID %d 失败: %w", id, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1279,7 +1279,7 @@ func TestPlanRepository_Create(t *testing.T) {
 | 
			
		||||
			tc.setupDB(db)
 | 
			
		||||
 | 
			
		||||
			// 执行 Create 操作
 | 
			
		||||
			err := repo.Create(tc.inputPlan)
 | 
			
		||||
			err := repo.CreatePlan(tc.inputPlan)
 | 
			
		||||
 | 
			
		||||
			// 断言错误
 | 
			
		||||
			if tc.expectedError != nil {
 | 
			
		||||
@@ -1295,3 +1295,131 @@ func TestPlanRepository_Create(t *testing.T) {
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user