Compare commits
	
		
			6 Commits
		
	
	
		
			69bdc50b3e
			...
			eb1be3f366
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eb1be3f366 | |||
| 4ddb2c5448 | |||
| 8ceff5c3c7 | |||
| 352c9d9246 | |||
| 67707c0d41 | |||
| 1dbef11a8c | 
							
								
								
									
										186
									
								
								internal/app/controller/plan/converter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								internal/app/controller/plan/converter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
				
			|||||||
 | 
					package plan
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"gorm.io/datatypes"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PlanToResponse 将Plan模型转换为PlanResponse
 | 
				
			||||||
 | 
					func PlanToResponse(plan *models.Plan) *PlanResponse {
 | 
				
			||||||
 | 
						if plan == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						response := &PlanResponse{
 | 
				
			||||||
 | 
							ID:             plan.ID,
 | 
				
			||||||
 | 
							Name:           plan.Name,
 | 
				
			||||||
 | 
							Description:    plan.Description,
 | 
				
			||||||
 | 
							ExecutionType:  plan.ExecutionType,
 | 
				
			||||||
 | 
							CronExpression: plan.CronExpression,
 | 
				
			||||||
 | 
							ContentType:    plan.ContentType,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换子计划
 | 
				
			||||||
 | 
						if plan.ContentType == models.PlanContentTypeSubPlans {
 | 
				
			||||||
 | 
							response.SubPlans = make([]SubPlanResponse, len(plan.SubPlans))
 | 
				
			||||||
 | 
							for i, subPlan := range plan.SubPlans {
 | 
				
			||||||
 | 
								response.SubPlans[i] = SubPlanToResponse(&subPlan)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换任务
 | 
				
			||||||
 | 
						if plan.ContentType == models.PlanContentTypeTasks {
 | 
				
			||||||
 | 
							response.Tasks = make([]TaskResponse, len(plan.Tasks))
 | 
				
			||||||
 | 
							for i, task := range plan.Tasks {
 | 
				
			||||||
 | 
								response.Tasks[i] = TaskToResponse(&task)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return response
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
				
			||||||
 | 
					func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
 | 
				
			||||||
 | 
						if req == nil {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						plan := &models.Plan{
 | 
				
			||||||
 | 
							Name:           req.Name,
 | 
				
			||||||
 | 
							Description:    req.Description,
 | 
				
			||||||
 | 
							ExecutionType:  req.ExecutionType,
 | 
				
			||||||
 | 
							CronExpression: req.CronExpression,
 | 
				
			||||||
 | 
							ContentType:    req.ContentType,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 处理子计划 (通过ID引用)
 | 
				
			||||||
 | 
						if req.ContentType == models.PlanContentTypeSubPlans && req.SubPlanIDs != nil {
 | 
				
			||||||
 | 
							plan.SubPlans = make([]models.SubPlan, len(req.SubPlanIDs))
 | 
				
			||||||
 | 
							for i, childPlanID := range req.SubPlanIDs {
 | 
				
			||||||
 | 
								plan.SubPlans[i] = models.SubPlan{
 | 
				
			||||||
 | 
									ChildPlanID:    childPlanID,
 | 
				
			||||||
 | 
									ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 处理任务
 | 
				
			||||||
 | 
						if req.ContentType == models.PlanContentTypeTasks && req.Tasks != nil {
 | 
				
			||||||
 | 
							plan.Tasks = make([]models.Task, len(req.Tasks))
 | 
				
			||||||
 | 
							for i, taskReq := range req.Tasks {
 | 
				
			||||||
 | 
								// 使用来自请求的ExecutionOrder
 | 
				
			||||||
 | 
								plan.Tasks[i] = TaskFromRequest(&taskReq)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 1. 首先,执行重复性验证
 | 
				
			||||||
 | 
						if err := plan.ValidateExecutionOrder(); err != nil {
 | 
				
			||||||
 | 
							// 如果检测到重复,立即返回错误
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 然后,调用方法来修复顺序断层
 | 
				
			||||||
 | 
						plan.ReorderSteps()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return plan, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
				
			||||||
 | 
					func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
 | 
				
			||||||
 | 
						if req == nil {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						plan := &models.Plan{
 | 
				
			||||||
 | 
							Name:           req.Name,
 | 
				
			||||||
 | 
							Description:    req.Description,
 | 
				
			||||||
 | 
							ExecutionType:  req.ExecutionType,
 | 
				
			||||||
 | 
							CronExpression: req.CronExpression,
 | 
				
			||||||
 | 
							ContentType:    req.ContentType,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 处理子计划 (通过ID引用)
 | 
				
			||||||
 | 
						if req.ContentType == models.PlanContentTypeSubPlans && req.SubPlanIDs != nil {
 | 
				
			||||||
 | 
							plan.SubPlans = make([]models.SubPlan, len(req.SubPlanIDs))
 | 
				
			||||||
 | 
							for i, childPlanID := range req.SubPlanIDs {
 | 
				
			||||||
 | 
								plan.SubPlans[i] = models.SubPlan{
 | 
				
			||||||
 | 
									ChildPlanID:    childPlanID,
 | 
				
			||||||
 | 
									ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 处理任务
 | 
				
			||||||
 | 
						if req.ContentType == models.PlanContentTypeTasks && req.Tasks != nil {
 | 
				
			||||||
 | 
							plan.Tasks = make([]models.Task, len(req.Tasks))
 | 
				
			||||||
 | 
							for i, taskReq := range req.Tasks {
 | 
				
			||||||
 | 
								// 使用来自请求的ExecutionOrder
 | 
				
			||||||
 | 
								plan.Tasks[i] = TaskFromRequest(&taskReq)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 1. 首先,执行重复性验证
 | 
				
			||||||
 | 
						if err := plan.ValidateExecutionOrder(); err != nil {
 | 
				
			||||||
 | 
							// 如果检测到重复,立即返回错误
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. 然后,调用方法来修复顺序断层
 | 
				
			||||||
 | 
						plan.ReorderSteps()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return plan, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SubPlanToResponse 将SubPlan模型转换为SubPlanResponse
 | 
				
			||||||
 | 
					func SubPlanToResponse(subPlan *models.SubPlan) SubPlanResponse {
 | 
				
			||||||
 | 
						if subPlan == nil {
 | 
				
			||||||
 | 
							return SubPlanResponse{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						response := SubPlanResponse{
 | 
				
			||||||
 | 
							ID:             subPlan.ID,
 | 
				
			||||||
 | 
							ParentPlanID:   subPlan.ParentPlanID,
 | 
				
			||||||
 | 
							ChildPlanID:    subPlan.ChildPlanID,
 | 
				
			||||||
 | 
							ExecutionOrder: subPlan.ExecutionOrder,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 如果有完整的子计划数据,也进行转换
 | 
				
			||||||
 | 
						if subPlan.ChildPlan != nil {
 | 
				
			||||||
 | 
							response.ChildPlan = PlanToResponse(subPlan.ChildPlan)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return response
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TaskToResponse 将Task模型转换为TaskResponse
 | 
				
			||||||
 | 
					func TaskToResponse(task *models.Task) TaskResponse {
 | 
				
			||||||
 | 
						if task == nil {
 | 
				
			||||||
 | 
							return TaskResponse{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return TaskResponse{
 | 
				
			||||||
 | 
							ID:             task.ID,
 | 
				
			||||||
 | 
							PlanID:         task.PlanID,
 | 
				
			||||||
 | 
							Name:           task.Name,
 | 
				
			||||||
 | 
							Description:    task.Description,
 | 
				
			||||||
 | 
							ExecutionOrder: task.ExecutionOrder,
 | 
				
			||||||
 | 
							Type:           task.Type,
 | 
				
			||||||
 | 
							Parameters:     controller.Properties(task.Parameters),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TaskFromRequest 将TaskRequest转换为Task模型
 | 
				
			||||||
 | 
					func TaskFromRequest(req *TaskRequest) models.Task {
 | 
				
			||||||
 | 
						if req == nil {
 | 
				
			||||||
 | 
							return models.Task{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return models.Task{
 | 
				
			||||||
 | 
							Name:           req.Name,
 | 
				
			||||||
 | 
							Description:    req.Description,
 | 
				
			||||||
 | 
							ExecutionOrder: req.ExecutionOrder,
 | 
				
			||||||
 | 
							Type:           req.Type,
 | 
				
			||||||
 | 
							Parameters:     datatypes.JSON(req.Parameters),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										459
									
								
								internal/app/controller/plan/converter_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										459
									
								
								internal/app/controller/plan/converter_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,459 @@
 | 
				
			|||||||
 | 
					package plan_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"gorm.io/datatypes"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPlanToResponse(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("nil plan", func(t *testing.T) {
 | 
				
			||||||
 | 
							response := plan.PlanToResponse(nil)
 | 
				
			||||||
 | 
							assert.Nil(t, response)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("basic plan without associations", func(t *testing.T) {
 | 
				
			||||||
 | 
							planModel := &models.Plan{
 | 
				
			||||||
 | 
								Model:          gorm.Model{ID: 1},
 | 
				
			||||||
 | 
								Name:           "Test Plan",
 | 
				
			||||||
 | 
								Description:    "A test plan",
 | 
				
			||||||
 | 
								ExecutionType:  models.PlanExecutionTypeAutomatic,
 | 
				
			||||||
 | 
								CronExpression: "0 0 * * *",
 | 
				
			||||||
 | 
								ContentType:    models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							response := plan.PlanToResponse(planModel)
 | 
				
			||||||
 | 
							assert.NotNil(t, response)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(1), response.ID)
 | 
				
			||||||
 | 
							assert.Equal(t, "Test Plan", response.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, "A test plan", response.Description)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanExecutionTypeAutomatic, response.ExecutionType)
 | 
				
			||||||
 | 
							assert.Equal(t, "0 0 * * *", response.CronExpression)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeTasks, response.ContentType)
 | 
				
			||||||
 | 
							assert.Empty(t, response.SubPlans)
 | 
				
			||||||
 | 
							assert.Empty(t, response.Tasks)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with sub plans", func(t *testing.T) {
 | 
				
			||||||
 | 
							childPlan := &models.Plan{
 | 
				
			||||||
 | 
								Model:       gorm.Model{ID: 2},
 | 
				
			||||||
 | 
								Name:        "Child Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel := &models.Plan{
 | 
				
			||||||
 | 
								Model:       gorm.Model{ID: 1},
 | 
				
			||||||
 | 
								Name:        "Parent Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
								SubPlans: []models.SubPlan{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										Model:          gorm.Model{ID: 10},
 | 
				
			||||||
 | 
										ParentPlanID:   1,
 | 
				
			||||||
 | 
										ChildPlanID:    2,
 | 
				
			||||||
 | 
										ExecutionOrder: 1,
 | 
				
			||||||
 | 
										ChildPlan:      childPlan,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							response := plan.PlanToResponse(planModel)
 | 
				
			||||||
 | 
							assert.NotNil(t, response)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(1), response.ID)
 | 
				
			||||||
 | 
							assert.Equal(t, "Parent Plan", response.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeSubPlans, response.ContentType)
 | 
				
			||||||
 | 
							assert.Len(t, response.SubPlans, 1)
 | 
				
			||||||
 | 
							assert.Empty(t, response.Tasks)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							subPlanResp := response.SubPlans[0]
 | 
				
			||||||
 | 
							assert.Equal(t, uint(10), subPlanResp.ID)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(1), subPlanResp.ParentPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(2), subPlanResp.ChildPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, subPlanResp.ExecutionOrder)
 | 
				
			||||||
 | 
							assert.NotNil(t, subPlanResp.ChildPlan)
 | 
				
			||||||
 | 
							assert.Equal(t, "Child Plan", subPlanResp.ChildPlan.Name)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with tasks", func(t *testing.T) {
 | 
				
			||||||
 | 
							params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel := &models.Plan{
 | 
				
			||||||
 | 
								Model:       gorm.Model{ID: 1},
 | 
				
			||||||
 | 
								Name:        "Task Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
								Tasks: []models.Task{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										Model:          gorm.Model{ID: 10},
 | 
				
			||||||
 | 
										PlanID:         1,
 | 
				
			||||||
 | 
										Name:           "Task 1",
 | 
				
			||||||
 | 
										Description:    "First task",
 | 
				
			||||||
 | 
										ExecutionOrder: 1,
 | 
				
			||||||
 | 
										Type:           models.TaskTypeWaiting,
 | 
				
			||||||
 | 
										Parameters:     params,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							response := plan.PlanToResponse(planModel)
 | 
				
			||||||
 | 
							assert.NotNil(t, response)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(1), response.ID)
 | 
				
			||||||
 | 
							assert.Equal(t, "Task Plan", response.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeTasks, response.ContentType)
 | 
				
			||||||
 | 
							assert.Len(t, response.Tasks, 1)
 | 
				
			||||||
 | 
							assert.Empty(t, response.SubPlans)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							taskResp := response.Tasks[0]
 | 
				
			||||||
 | 
							assert.Equal(t, uint(10), taskResp.ID)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(1), taskResp.PlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, "Task 1", taskResp.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, "First task", taskResp.Description)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, taskResp.ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, models.TaskTypeWaiting, taskResp.Type)
 | 
				
			||||||
 | 
							assert.Equal(t, controller.Properties(params), taskResp.Parameters)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPlanFromCreateRequest(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("nil request", func(t *testing.T) {
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromCreateRequest(nil)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.Nil(t, planModel)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("basic plan without associations", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &plan.CreatePlanRequest{
 | 
				
			||||||
 | 
								Name:           "Test Plan",
 | 
				
			||||||
 | 
								Description:    "A test plan",
 | 
				
			||||||
 | 
								ExecutionType:  models.PlanExecutionTypeAutomatic,
 | 
				
			||||||
 | 
								CronExpression: "0 0 * * *",
 | 
				
			||||||
 | 
								ContentType:    models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromCreateRequest(req)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.NotNil(t, planModel)
 | 
				
			||||||
 | 
							assert.Equal(t, "Test Plan", planModel.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, "A test plan", planModel.Description)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanExecutionTypeAutomatic, planModel.ExecutionType)
 | 
				
			||||||
 | 
							assert.Equal(t, "0 0 * * *", planModel.CronExpression)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
				
			||||||
 | 
							assert.Empty(t, planModel.SubPlans)
 | 
				
			||||||
 | 
							assert.Empty(t, planModel.Tasks)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with sub plan IDs", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &plan.CreatePlanRequest{
 | 
				
			||||||
 | 
								Name:        "Parent Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
								SubPlanIDs:  []uint{2, 3},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromCreateRequest(req)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.NotNil(t, planModel)
 | 
				
			||||||
 | 
							assert.Equal(t, "Parent Plan", planModel.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType)
 | 
				
			||||||
 | 
							assert.Len(t, planModel.SubPlans, 2)
 | 
				
			||||||
 | 
							assert.Empty(t, planModel.Tasks)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with tasks", func(t *testing.T) {
 | 
				
			||||||
 | 
							params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := &plan.CreatePlanRequest{
 | 
				
			||||||
 | 
								Name:        "Task Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
								Tasks: []plan.TaskRequest{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										Name:           "Task 1",
 | 
				
			||||||
 | 
										Description:    "First task",
 | 
				
			||||||
 | 
										ExecutionOrder: 1,
 | 
				
			||||||
 | 
										Type:           models.TaskTypeWaiting,
 | 
				
			||||||
 | 
										Parameters:     params,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromCreateRequest(req)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.NotNil(t, planModel)
 | 
				
			||||||
 | 
							assert.Equal(t, "Task Plan", planModel.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
				
			||||||
 | 
							assert.Len(t, planModel.Tasks, 1)
 | 
				
			||||||
 | 
							assert.Empty(t, planModel.SubPlans)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							task := planModel.Tasks[0]
 | 
				
			||||||
 | 
							assert.Equal(t, "Task 1", task.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, "First task", task.Description)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, task.ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, models.TaskTypeWaiting, task.Type)
 | 
				
			||||||
 | 
							assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with tasks with gapped execution order", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &plan.CreatePlanRequest{
 | 
				
			||||||
 | 
								Name:        "Task Plan with Gaps",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
								Tasks: []plan.TaskRequest{
 | 
				
			||||||
 | 
									{Name: "Task 3", ExecutionOrder: 5},
 | 
				
			||||||
 | 
									{Name: "Task 1", ExecutionOrder: 2},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromCreateRequest(req)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.NotNil(t, planModel)
 | 
				
			||||||
 | 
							assert.Len(t, planModel.Tasks, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered.
 | 
				
			||||||
 | 
							assert.Equal(t, "Task 1", planModel.Tasks[0].Name)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, "Task 3", planModel.Tasks[1].Name)
 | 
				
			||||||
 | 
							assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with duplicate task execution order", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &plan.CreatePlanRequest{
 | 
				
			||||||
 | 
								Name:        "Invalid Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
								Tasks: []plan.TaskRequest{
 | 
				
			||||||
 | 
									{Name: "Task 1", ExecutionOrder: 1},
 | 
				
			||||||
 | 
									{Name: "Task 2", ExecutionOrder: 1}, // Duplicate order
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromCreateRequest(req)
 | 
				
			||||||
 | 
							assert.Error(t, err)
 | 
				
			||||||
 | 
							assert.Contains(t, err.Error(), "任务执行顺序重复")
 | 
				
			||||||
 | 
							assert.Nil(t, planModel)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPlanFromUpdateRequest(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("nil request", func(t *testing.T) {
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromUpdateRequest(nil)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.Nil(t, planModel)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("basic plan without associations", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &plan.UpdatePlanRequest{
 | 
				
			||||||
 | 
								Name:           "Updated Plan",
 | 
				
			||||||
 | 
								Description:    "An updated plan",
 | 
				
			||||||
 | 
								ExecutionType:  models.PlanExecutionTypeManual,
 | 
				
			||||||
 | 
								CronExpression: "0 30 * * *",
 | 
				
			||||||
 | 
								ContentType:    models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromUpdateRequest(req)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.NotNil(t, planModel)
 | 
				
			||||||
 | 
							assert.Equal(t, "Updated Plan", planModel.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, "An updated plan", planModel.Description)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanExecutionTypeManual, planModel.ExecutionType)
 | 
				
			||||||
 | 
							assert.Equal(t, "0 30 * * *", planModel.CronExpression)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
				
			||||||
 | 
							assert.Empty(t, planModel.SubPlans)
 | 
				
			||||||
 | 
							assert.Empty(t, planModel.Tasks)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with sub plan IDs", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &plan.UpdatePlanRequest{
 | 
				
			||||||
 | 
								Name:        "Updated Parent Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
								SubPlanIDs:  []uint{2, 3},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromUpdateRequest(req)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.NotNil(t, planModel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.Equal(t, "Updated Parent Plan", planModel.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType)
 | 
				
			||||||
 | 
							assert.Len(t, planModel.SubPlans, 2)
 | 
				
			||||||
 | 
							assert.Empty(t, planModel.Tasks)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with tasks", func(t *testing.T) {
 | 
				
			||||||
 | 
							params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := &plan.UpdatePlanRequest{
 | 
				
			||||||
 | 
								Name:        "Updated Task Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
								Tasks: []plan.TaskRequest{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										Name:           "Task 1",
 | 
				
			||||||
 | 
										Description:    "First task",
 | 
				
			||||||
 | 
										ExecutionOrder: 1,
 | 
				
			||||||
 | 
										Type:           models.TaskTypeWaiting,
 | 
				
			||||||
 | 
										Parameters:     params,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromUpdateRequest(req)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.NotNil(t, planModel)
 | 
				
			||||||
 | 
							assert.Equal(t, "Updated Task Plan", planModel.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
				
			||||||
 | 
							assert.Len(t, planModel.Tasks, 1)
 | 
				
			||||||
 | 
							assert.Empty(t, planModel.SubPlans)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							task := planModel.Tasks[0]
 | 
				
			||||||
 | 
							assert.Equal(t, "Task 1", task.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, task.ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with duplicate task execution order", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &plan.UpdatePlanRequest{
 | 
				
			||||||
 | 
								Name:        "Invalid Updated Plan",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
								Tasks: []plan.TaskRequest{
 | 
				
			||||||
 | 
									{Name: "Task 1", ExecutionOrder: 1},
 | 
				
			||||||
 | 
									{Name: "Task 2", ExecutionOrder: 1}, // Duplicate order
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromUpdateRequest(req)
 | 
				
			||||||
 | 
							assert.Error(t, err)
 | 
				
			||||||
 | 
							assert.Contains(t, err.Error(), "任务执行顺序重复")
 | 
				
			||||||
 | 
							assert.Nil(t, planModel)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("plan with tasks with gapped execution order", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &plan.UpdatePlanRequest{
 | 
				
			||||||
 | 
								Name:        "Updated Task Plan with Gaps",
 | 
				
			||||||
 | 
								ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
								Tasks: []plan.TaskRequest{
 | 
				
			||||||
 | 
									{Name: "Task 3", ExecutionOrder: 5},
 | 
				
			||||||
 | 
									{Name: "Task 1", ExecutionOrder: 2},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							planModel, err := plan.PlanFromUpdateRequest(req)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.NotNil(t, planModel)
 | 
				
			||||||
 | 
							assert.Len(t, planModel.Tasks, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered.
 | 
				
			||||||
 | 
							assert.Equal(t, "Task 1", planModel.Tasks[0].Name)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, "Task 3", planModel.Tasks[1].Name)
 | 
				
			||||||
 | 
							assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSubPlanToResponse(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("nil sub plan", func(t *testing.T) {
 | 
				
			||||||
 | 
							response := plan.SubPlanToResponse(nil)
 | 
				
			||||||
 | 
							assert.Equal(t, plan.SubPlanResponse{}, response)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("sub plan without child plan", func(t *testing.T) {
 | 
				
			||||||
 | 
							subPlan := &models.SubPlan{
 | 
				
			||||||
 | 
								Model:          gorm.Model{ID: 10},
 | 
				
			||||||
 | 
								ParentPlanID:   1,
 | 
				
			||||||
 | 
								ChildPlanID:    2,
 | 
				
			||||||
 | 
								ExecutionOrder: 1,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							response := plan.SubPlanToResponse(subPlan)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(10), response.ID)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(1), response.ParentPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(2), response.ChildPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, response.ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Nil(t, response.ChildPlan)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("sub plan with child plan", func(t *testing.T) {
 | 
				
			||||||
 | 
							childPlan := &models.Plan{
 | 
				
			||||||
 | 
								Model: gorm.Model{ID: 2},
 | 
				
			||||||
 | 
								Name:  "Child Plan",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							subPlan := &models.SubPlan{
 | 
				
			||||||
 | 
								Model:          gorm.Model{ID: 10},
 | 
				
			||||||
 | 
								ParentPlanID:   1,
 | 
				
			||||||
 | 
								ChildPlanID:    2,
 | 
				
			||||||
 | 
								ExecutionOrder: 1,
 | 
				
			||||||
 | 
								ChildPlan:      childPlan,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							response := plan.SubPlanToResponse(subPlan)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(10), response.ID)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(1), response.ParentPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(2), response.ChildPlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, response.ExecutionOrder)
 | 
				
			||||||
 | 
							assert.NotNil(t, response.ChildPlan)
 | 
				
			||||||
 | 
							assert.Equal(t, "Child Plan", response.ChildPlan.Name)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTaskToResponse(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("nil task", func(t *testing.T) {
 | 
				
			||||||
 | 
							response := plan.TaskToResponse(nil)
 | 
				
			||||||
 | 
							assert.Equal(t, plan.TaskResponse{}, response)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("task with parameters", func(t *testing.T) {
 | 
				
			||||||
 | 
							params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`))
 | 
				
			||||||
 | 
							task := &models.Task{
 | 
				
			||||||
 | 
								Model:          gorm.Model{ID: 10},
 | 
				
			||||||
 | 
								PlanID:         1,
 | 
				
			||||||
 | 
								Name:           "Test Task",
 | 
				
			||||||
 | 
								Description:    "A test task",
 | 
				
			||||||
 | 
								ExecutionOrder: 1,
 | 
				
			||||||
 | 
								Type:           models.TaskTypeWaiting,
 | 
				
			||||||
 | 
								Parameters:     params,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							response := plan.TaskToResponse(task)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(10), response.ID)
 | 
				
			||||||
 | 
							assert.Equal(t, uint(1), response.PlanID)
 | 
				
			||||||
 | 
							assert.Equal(t, "Test Task", response.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, "A test task", response.Description)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, response.ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, models.TaskTypeWaiting, response.Type)
 | 
				
			||||||
 | 
							assert.Equal(t, controller.Properties(params), response.Parameters)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTaskFromRequest(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("nil request", func(t *testing.T) {
 | 
				
			||||||
 | 
							task := plan.TaskFromRequest(nil)
 | 
				
			||||||
 | 
							assert.Equal(t, models.Task{}, task)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("task with parameters", func(t *testing.T) {
 | 
				
			||||||
 | 
							params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
				
			||||||
 | 
							req := &plan.TaskRequest{
 | 
				
			||||||
 | 
								Name:           "Test Task",
 | 
				
			||||||
 | 
								Description:    "A test task",
 | 
				
			||||||
 | 
								ExecutionOrder: 1,
 | 
				
			||||||
 | 
								Type:           models.TaskTypeWaiting,
 | 
				
			||||||
 | 
								Parameters:     params,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							task := plan.TaskFromRequest(req)
 | 
				
			||||||
 | 
							assert.Equal(t, "Test Task", task.Name)
 | 
				
			||||||
 | 
							assert.Equal(t, "A test task", task.Description)
 | 
				
			||||||
 | 
							assert.Equal(t, 1, task.ExecutionOrder)
 | 
				
			||||||
 | 
							assert.Equal(t, models.TaskTypeWaiting, task.Type)
 | 
				
			||||||
 | 
							assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,6 +3,7 @@ package plan
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -11,19 +12,25 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// CreatePlanRequest 定义创建计划请求的结构体
 | 
					// CreatePlanRequest 定义创建计划请求的结构体
 | 
				
			||||||
type CreatePlanRequest struct {
 | 
					type CreatePlanRequest struct {
 | 
				
			||||||
	Name        string `json:"name" binding:"required" example:"猪舍温度控制计划"`
 | 
						Name           string                   `json:"name" binding:"required" example:"猪舍温度控制计划"`
 | 
				
			||||||
	Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
						Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
				
			||||||
	// 更多计划基本信息字段
 | 
						ExecutionType  models.PlanExecutionType `json:"execution_type" binding:"required" example:"automatic"`
 | 
				
			||||||
	// SubPlans 或 Tasks 可以在这里嵌套定义,以支持一次性创建
 | 
						CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
				
			||||||
 | 
						ContentType    models.PlanContentType   `json:"content_type" binding:"required" example:"tasks"`
 | 
				
			||||||
 | 
						SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
				
			||||||
 | 
						Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PlanResponse 定义计划详情响应的结构体
 | 
					// PlanResponse 定义计划详情响应的结构体
 | 
				
			||||||
type PlanResponse struct {
 | 
					type PlanResponse struct {
 | 
				
			||||||
	ID          uint   `json:"id" example:"1"`
 | 
						ID             uint                     `json:"id" example:"1"`
 | 
				
			||||||
	Name        string `json:"name" example:"猪舍温度控制计划"`
 | 
						Name           string                   `json:"name" example:"猪舍温度控制计划"`
 | 
				
			||||||
	Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
						Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
				
			||||||
	// 更多计划基本信息字段
 | 
						ExecutionType  models.PlanExecutionType `json:"execution_type" example:"automatic"`
 | 
				
			||||||
	// SubPlans 或 Tasks 也可以在这里返回
 | 
						CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
				
			||||||
 | 
						ContentType    models.PlanContentType   `json:"content_type" example:"tasks"`
 | 
				
			||||||
 | 
						SubPlans       []SubPlanResponse        `json:"sub_plans,omitempty"`
 | 
				
			||||||
 | 
						Tasks          []TaskResponse           `json:"tasks,omitempty"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ListPlansResponse 定义获取计划列表响应的结构体
 | 
					// ListPlansResponse 定义获取计划列表响应的结构体
 | 
				
			||||||
@@ -34,9 +41,42 @@ type ListPlansResponse struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// UpdatePlanRequest 定义更新计划请求的结构体
 | 
					// UpdatePlanRequest 定义更新计划请求的结构体
 | 
				
			||||||
type UpdatePlanRequest struct {
 | 
					type UpdatePlanRequest struct {
 | 
				
			||||||
	Name        string `json:"name" example:"猪舍温度控制计划V2"`
 | 
						Name           string                   `json:"name" example:"猪舍温度控制计划V2"`
 | 
				
			||||||
	Description string `json:"description" example:"更新后的描述"`
 | 
						Description    string                   `json:"description" example:"更新后的描述"`
 | 
				
			||||||
	// 更多可更新字段
 | 
						ExecutionType  models.PlanExecutionType `json:"execution_type" example:"automatic"`
 | 
				
			||||||
 | 
						CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
				
			||||||
 | 
						ContentType    models.PlanContentType   `json:"content_type" example:"tasks"`
 | 
				
			||||||
 | 
						SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
				
			||||||
 | 
						Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SubPlanResponse 定义子计划响应结构体
 | 
				
			||||||
 | 
					type SubPlanResponse struct {
 | 
				
			||||||
 | 
						ID             uint          `json:"id" example:"1"`
 | 
				
			||||||
 | 
						ParentPlanID   uint          `json:"parent_plan_id" example:"1"`
 | 
				
			||||||
 | 
						ChildPlanID    uint          `json:"child_plan_id" example:"2"`
 | 
				
			||||||
 | 
						ExecutionOrder int           `json:"execution_order" example:"1"`
 | 
				
			||||||
 | 
						ChildPlan      *PlanResponse `json:"child_plan,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TaskRequest 定义任务请求结构体
 | 
				
			||||||
 | 
					type TaskRequest struct {
 | 
				
			||||||
 | 
						Name           string                `json:"name" example:"打开风扇"`
 | 
				
			||||||
 | 
						Description    string                `json:"description" example:"打开1号风扇"`
 | 
				
			||||||
 | 
						ExecutionOrder int                   `json:"execution_order" example:"1"`
 | 
				
			||||||
 | 
						Type           models.TaskType       `json:"type" example:"waiting"`
 | 
				
			||||||
 | 
						Parameters     controller.Properties `json:"parameters,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TaskResponse 定义任务响应结构体
 | 
				
			||||||
 | 
					type TaskResponse struct {
 | 
				
			||||||
 | 
						ID             uint                  `json:"id" example:"1"`
 | 
				
			||||||
 | 
						PlanID         uint                  `json:"plan_id" example:"1"`
 | 
				
			||||||
 | 
						Name           string                `json:"name" example:"打开风扇"`
 | 
				
			||||||
 | 
						Description    string                `json:"description" example:"打开1号风扇"`
 | 
				
			||||||
 | 
						ExecutionOrder int                   `json:"execution_order" example:"1"`
 | 
				
			||||||
 | 
						Type           models.TaskType       `json:"type" example:"waiting"`
 | 
				
			||||||
 | 
						Parameters     controller.Properties `json:"parameters,omitempty"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Controller 定义 ---
 | 
					// --- Controller 定义 ---
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ package models
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"gorm.io/datatypes"
 | 
						"gorm.io/datatypes"
 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
@@ -79,6 +80,42 @@ func (p Plan) ValidateExecutionOrder() error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReorderSteps 重新排序计划中的步骤(任务或子计划),使其 ExecutionOrder 从 1 开始且连续。
 | 
				
			||||||
 | 
					// 这个方法假设重复的顺序已经被其他方法验证过,它只负责修复断层和不从1开始的序列。
 | 
				
			||||||
 | 
					func (p *Plan) ReorderSteps() {
 | 
				
			||||||
 | 
						switch p.ContentType {
 | 
				
			||||||
 | 
						case PlanContentTypeTasks:
 | 
				
			||||||
 | 
							if len(p.Tasks) == 0 {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1. 按当前的 ExecutionOrder 对任务进行排序
 | 
				
			||||||
 | 
							sort.Slice(p.Tasks, func(i, j int) bool {
 | 
				
			||||||
 | 
								return p.Tasks[i].ExecutionOrder < p.Tasks[j].ExecutionOrder
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 重新分配连续的 ExecutionOrder
 | 
				
			||||||
 | 
							for i := range p.Tasks {
 | 
				
			||||||
 | 
								p.Tasks[i].ExecutionOrder = i + 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case PlanContentTypeSubPlans:
 | 
				
			||||||
 | 
							if len(p.SubPlans) == 0 {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1. 按当前的 ExecutionOrder 对子计划进行排序
 | 
				
			||||||
 | 
							sort.Slice(p.SubPlans, func(i, j int) bool {
 | 
				
			||||||
 | 
								return p.SubPlans[i].ExecutionOrder < p.SubPlans[j].ExecutionOrder
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2. 重新分配连续的 ExecutionOrder
 | 
				
			||||||
 | 
							for i := range p.SubPlans {
 | 
				
			||||||
 | 
								p.SubPlans[i].ExecutionOrder = i + 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SubPlan 代表作为另一个计划一部分的子计划,具有执行顺序
 | 
					// SubPlan 代表作为另一个计划一部分的子计划,具有执行顺序
 | 
				
			||||||
type SubPlan struct {
 | 
					type SubPlan struct {
 | 
				
			||||||
	gorm.Model
 | 
						gorm.Model
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,22 +1,25 @@
 | 
				
			|||||||
package models_test
 | 
					package models_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"sort"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestPlan_ValidateExecutionOrder(t *testing.T) {
 | 
					func TestPlan_ReorderSteps(t *testing.T) {
 | 
				
			||||||
	tests := []struct {
 | 
						type testCase struct {
 | 
				
			||||||
		name          string
 | 
							name           string
 | 
				
			||||||
		plan          models.Plan
 | 
							initialPlan    *models.Plan
 | 
				
			||||||
		expectedError string // 期望的错误信息,如果为nil则表示不期望错误
 | 
							expectedOrders []int
 | 
				
			||||||
	}{
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []testCase{
 | 
				
			||||||
 | 
							// --- Test Cases for Tasks ---
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "任务类型-无重复执行顺序",
 | 
								name: "Tasks: 完美顺序",
 | 
				
			||||||
			plan: models.Plan{
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
				ContentType: models.PlanContentTypeTasks,
 | 
									ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
				Tasks: []models.Task{
 | 
									Tasks: []models.Task{
 | 
				
			||||||
					{ExecutionOrder: 1},
 | 
										{ExecutionOrder: 1},
 | 
				
			||||||
@@ -24,31 +27,78 @@ func TestPlan_ValidateExecutionOrder(t *testing.T) {
 | 
				
			|||||||
					{ExecutionOrder: 3},
 | 
										{ExecutionOrder: 3},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedError: "",
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "任务类型-重复执行顺序",
 | 
								name: "Tasks: 有间断",
 | 
				
			||||||
			plan: models.Plan{
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
				ContentType: models.PlanContentTypeTasks,
 | 
									ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
				Tasks: []models.Task{
 | 
									Tasks: []models.Task{
 | 
				
			||||||
					{ExecutionOrder: 1},
 | 
										{ExecutionOrder: 1},
 | 
				
			||||||
					{ExecutionOrder: 2},
 | 
										{ExecutionOrder: 3},
 | 
				
			||||||
					{ExecutionOrder: 1}, // 重复
 | 
										{ExecutionOrder: 5},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedError: fmt.Sprintf("任务执行顺序重复: %d", 1),
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "任务类型-空任务列表",
 | 
								name: "Tasks: 从0开始",
 | 
				
			||||||
			plan: models.Plan{
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
 | 
									ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
									Tasks: []models.Task{
 | 
				
			||||||
 | 
										{ExecutionOrder: 0},
 | 
				
			||||||
 | 
										{ExecutionOrder: 1},
 | 
				
			||||||
 | 
										{ExecutionOrder: 2},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "Tasks: 完全无序",
 | 
				
			||||||
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
 | 
									ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
									Tasks: []models.Task{
 | 
				
			||||||
 | 
										{ExecutionOrder: 8},
 | 
				
			||||||
 | 
										{ExecutionOrder: 2},
 | 
				
			||||||
 | 
										{ExecutionOrder: 4},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "Tasks: 包含负数",
 | 
				
			||||||
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
 | 
									ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
									Tasks: []models.Task{
 | 
				
			||||||
 | 
										{ExecutionOrder: -5},
 | 
				
			||||||
 | 
										{ExecutionOrder: 10},
 | 
				
			||||||
 | 
										{ExecutionOrder: 2},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "Tasks: 空切片",
 | 
				
			||||||
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
				ContentType: models.PlanContentTypeTasks,
 | 
									ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
				Tasks:       []models.Task{},
 | 
									Tasks:       []models.Task{},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedError: "",
 | 
								expectedOrders: []int{},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "子计划类型-无重复执行顺序",
 | 
								name: "Tasks: 单个元素",
 | 
				
			||||||
			plan: models.Plan{
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
 | 
									ContentType: models.PlanContentTypeTasks,
 | 
				
			||||||
 | 
									Tasks: []models.Task{
 | 
				
			||||||
 | 
										{ExecutionOrder: 100},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedOrders: []int{1},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							// --- Test Cases for SubPlans ---
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "SubPlans: 完美顺序",
 | 
				
			||||||
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
				ContentType: models.PlanContentTypeSubPlans,
 | 
									ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
				SubPlans: []models.SubPlan{
 | 
									SubPlans: []models.SubPlan{
 | 
				
			||||||
					{ExecutionOrder: 1},
 | 
										{ExecutionOrder: 1},
 | 
				
			||||||
@@ -56,48 +106,97 @@ func TestPlan_ValidateExecutionOrder(t *testing.T) {
 | 
				
			|||||||
					{ExecutionOrder: 3},
 | 
										{ExecutionOrder: 3},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedError: "",
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "子计划类型-重复执行顺序",
 | 
								name: "SubPlans: 有间断",
 | 
				
			||||||
			plan: models.Plan{
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
				ContentType: models.PlanContentTypeSubPlans,
 | 
									ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
				SubPlans: []models.SubPlan{
 | 
									SubPlans: []models.SubPlan{
 | 
				
			||||||
					{ExecutionOrder: 1},
 | 
										{ExecutionOrder: 1},
 | 
				
			||||||
					{ExecutionOrder: 2},
 | 
										{ExecutionOrder: 3},
 | 
				
			||||||
					{ExecutionOrder: 1}, // 重复
 | 
										{ExecutionOrder: 5},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedError: fmt.Sprintf("子计划执行顺序重复: %d", 1),
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "子计划类型-空子计划列表",
 | 
								name: "SubPlans: 从0开始",
 | 
				
			||||||
			plan: models.Plan{
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
 | 
									ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
									SubPlans: []models.SubPlan{
 | 
				
			||||||
 | 
										{ExecutionOrder: 0},
 | 
				
			||||||
 | 
										{ExecutionOrder: 1},
 | 
				
			||||||
 | 
										{ExecutionOrder: 2},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "SubPlans: 完全无序",
 | 
				
			||||||
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
 | 
									ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
									SubPlans: []models.SubPlan{
 | 
				
			||||||
 | 
										{ExecutionOrder: 8},
 | 
				
			||||||
 | 
										{ExecutionOrder: 2},
 | 
				
			||||||
 | 
										{ExecutionOrder: 4},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "SubPlans: 包含负数",
 | 
				
			||||||
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
 | 
									ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
 | 
									SubPlans: []models.SubPlan{
 | 
				
			||||||
 | 
										{ExecutionOrder: -5},
 | 
				
			||||||
 | 
										{ExecutionOrder: 10},
 | 
				
			||||||
 | 
										{ExecutionOrder: 2},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectedOrders: []int{1, 2, 3},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "SubPlans: 空切片",
 | 
				
			||||||
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
				ContentType: models.PlanContentTypeSubPlans,
 | 
									ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
				SubPlans:    []models.SubPlan{},
 | 
									SubPlans:    []models.SubPlan{},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedError: "",
 | 
								expectedOrders: []int{},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "未知内容类型",
 | 
								name: "SubPlans: 单个元素",
 | 
				
			||||||
			plan: models.Plan{
 | 
								initialPlan: &models.Plan{
 | 
				
			||||||
				ContentType: "UNKNOWN_TYPE", // 未知类型
 | 
									ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
				Tasks:       []models.Task{{ExecutionOrder: 1}},
 | 
									SubPlans: []models.SubPlan{
 | 
				
			||||||
 | 
										{ExecutionOrder: 100},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			expectedError: "", // 对于未知类型,ValidateExecutionOrder 不会返回错误,因为它只处理已知类型
 | 
								expectedOrders: []int{1},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, tt := range tests {
 | 
						for _, tc := range testCases {
 | 
				
			||||||
		t.Run(tt.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			err := tt.plan.ValidateExecutionOrder()
 | 
								// 调用被测试的方法
 | 
				
			||||||
 | 
								tc.initialPlan.ReorderSteps()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if tt.expectedError != "" {
 | 
								// 提取并验证最终的顺序
 | 
				
			||||||
				assert.Error(t, err)
 | 
								finalOrders := make([]int, 0)
 | 
				
			||||||
				assert.Contains(t, err.Error(), tt.expectedError)
 | 
								if tc.initialPlan.ContentType == models.PlanContentTypeTasks {
 | 
				
			||||||
			} else {
 | 
									for _, task := range tc.initialPlan.Tasks {
 | 
				
			||||||
				assert.NoError(t, err)
 | 
										finalOrders = append(finalOrders, task.ExecutionOrder)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else if tc.initialPlan.ContentType == models.PlanContentTypeSubPlans {
 | 
				
			||||||
 | 
									for _, subPlan := range tc.initialPlan.SubPlans {
 | 
				
			||||||
 | 
										finalOrders = append(finalOrders, subPlan.ExecutionOrder)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 对 finalOrders 进行排序,以确保比较的一致性,因为 ReorderSteps 后的顺序是固定的
 | 
				
			||||||
 | 
								sort.Ints(finalOrders)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								assert.Equal(t, tc.expectedOrders, finalOrders, "The final execution orders should be a continuous sequence starting from 1.")
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -137,10 +137,10 @@ func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error {
 | 
				
			|||||||
		if plan.ContentType == models.PlanContentTypeSubPlans {
 | 
							if plan.ContentType == models.PlanContentTypeSubPlans {
 | 
				
			||||||
			childIDsToValidate := make(map[uint]bool)
 | 
								childIDsToValidate := make(map[uint]bool)
 | 
				
			||||||
			for _, subPlanLink := range plan.SubPlans {
 | 
								for _, subPlanLink := range plan.SubPlans {
 | 
				
			||||||
				if subPlanLink.ChildPlan == nil || subPlanLink.ChildPlan.ID == 0 {
 | 
									if subPlanLink.ChildPlanID == 0 {
 | 
				
			||||||
					return ErrSubPlanIDIsZeroOnCreate
 | 
										return ErrSubPlanIDIsZeroOnCreate
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				childIDsToValidate[subPlanLink.ChildPlan.ID] = true
 | 
									childIDsToValidate[subPlanLink.ChildPlanID] = true
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			var ids []uint
 | 
								var ids []uint
 | 
				
			||||||
@@ -346,10 +346,6 @@ func (r *gormPlanRepository) reconcileSubPlans(tx *gorm.DB, plan *models.Plan) e
 | 
				
			|||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// 递归协调子计划节点
 | 
					 | 
				
			||||||
		if err := r.reconcilePlanNode(tx, link.ChildPlan); err != nil {
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var linksToDelete []uint
 | 
						var linksToDelete []uint
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -836,206 +836,6 @@ func TestUpdatePlan_Reconciliation(t *testing.T) {
 | 
				
			|||||||
				assert.Equal(t, int64(1), 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 {
 | 
						for _, tc := range testCases {
 | 
				
			||||||
@@ -1134,7 +934,7 @@ func TestPlanRepository_Create(t *testing.T) {
 | 
				
			|||||||
				Name:        "父计划",
 | 
									Name:        "父计划",
 | 
				
			||||||
				ContentType: models.PlanContentTypeSubPlans,
 | 
									ContentType: models.PlanContentTypeSubPlans,
 | 
				
			||||||
				SubPlans: []models.SubPlan{
 | 
									SubPlans: []models.SubPlan{
 | 
				
			||||||
					{ChildPlanID: 1, ExecutionOrder: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 1}}}, // 关联已存在的子计划1
 | 
										{ChildPlanID: 1, ExecutionOrder: 1},                                                    // 关联已存在的子计划1
 | 
				
			||||||
					{ChildPlanID: 2, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 2}}}, // 关联已存在的子计划2
 | 
										{ChildPlanID: 2, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 2}}}, // 关联已存在的子计划2
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user