1. 增加重复顺序校验

2. 增加测试用例
This commit is contained in:
2025-09-13 21:14:22 +08:00
parent 287c27a5ab
commit 9fc9cda08e
4 changed files with 414 additions and 28 deletions

View File

@@ -1,6 +1,8 @@
package models
import (
"fmt"
"gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -54,6 +56,29 @@ func (Plan) TableName() string {
return "plans"
}
// ValidateExecutionOrder 校验计划中的步骤或子计划顺序不能有重复的
func (p Plan) ValidateExecutionOrder() error {
orderMap := make(map[int]bool)
switch p.ContentType {
case PlanContentTypeTasks:
for _, task := range p.Tasks {
if orderMap[task.ExecutionOrder] {
return fmt.Errorf("任务执行顺序重复: %d", task.ExecutionOrder)
}
orderMap[task.ExecutionOrder] = true
}
case PlanContentTypeSubPlans:
for _, subPlan := range p.SubPlans {
if orderMap[subPlan.ExecutionOrder] {
return fmt.Errorf("子计划执行顺序重复: %d", subPlan.ExecutionOrder)
}
orderMap[subPlan.ExecutionOrder] = true
}
}
return nil
}
// SubPlan 代表作为另一个计划一部分的子计划,具有执行顺序
type SubPlan struct {
gorm.Model

View File

@@ -0,0 +1,103 @@
package models_test
import (
"fmt"
"testing"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/stretchr/testify/assert"
)
func TestPlan_ValidateExecutionOrder(t *testing.T) {
tests := []struct {
name string
plan models.Plan
expectedError string // 期望的错误信息如果为nil则表示不期望错误
}{
{
name: "任务类型-无重复执行顺序",
plan: models.Plan{
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{ExecutionOrder: 1},
{ExecutionOrder: 2},
{ExecutionOrder: 3},
},
},
expectedError: "",
},
{
name: "任务类型-重复执行顺序",
plan: models.Plan{
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{ExecutionOrder: 1},
{ExecutionOrder: 2},
{ExecutionOrder: 1}, // 重复
},
},
expectedError: fmt.Sprintf("任务执行顺序重复: %d", 1),
},
{
name: "任务类型-空任务列表",
plan: models.Plan{
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{},
},
expectedError: "",
},
{
name: "子计划类型-无重复执行顺序",
plan: models.Plan{
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ExecutionOrder: 1},
{ExecutionOrder: 2},
{ExecutionOrder: 3},
},
},
expectedError: "",
},
{
name: "子计划类型-重复执行顺序",
plan: models.Plan{
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ExecutionOrder: 1},
{ExecutionOrder: 2},
{ExecutionOrder: 1}, // 重复
},
},
expectedError: fmt.Sprintf("子计划执行顺序重复: %d", 1),
},
{
name: "子计划类型-空子计划列表",
plan: models.Plan{
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{},
},
expectedError: "",
},
{
name: "未知内容类型",
plan: models.Plan{
ContentType: "UNKNOWN_TYPE", // 未知类型
Tasks: []models.Task{{ExecutionOrder: 1}},
},
expectedError: "", // 对于未知类型ValidateExecutionOrder 不会返回错误,因为它只处理已知类型
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.plan.ValidateExecutionOrder()
if tt.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -125,6 +125,11 @@ func (r *gormPlanRepository) Create(plan *models.Plan) error {
return ErrMixedContent
}
// 检查是否有重复的执行顺序
if err := plan.ValidateExecutionOrder(); err != nil {
return fmt.Errorf("计划 (ID: %d) 的执行顺序无效: %w", plan.ID, err)
}
// 如果是子计划类型验证所有子计划是否存在且ID不为0
if plan.ContentType == models.PlanContentTypeSubPlans {
childIDsToValidate := make(map[uint]bool)
@@ -156,28 +161,6 @@ func (r *gormPlanRepository) Create(plan *models.Plan) error {
if err := tx.Create(plan).Error; err != nil {
return err
}
// 3. 处理子计划关联 (如果 ContentType 是 sub_plans)
// GORM 不会自动创建 SubPlan 关联记录,需要手动处理
if plan.ContentType == models.PlanContentTypeSubPlans {
// 收集所有 SubPlan 关联,设置 ParentPlanID
var subPlanLinksToCreate []models.SubPlan
for i := range plan.SubPlans {
plan.SubPlans[i].ParentPlanID = plan.ID // 设置父计划ID
// 确保 ChildPlanID 被正确设置,因为 ChildPlan 对象可能只在内存中
if plan.SubPlans[i].ChildPlanID == 0 && plan.SubPlans[i].ChildPlan != nil {
plan.SubPlans[i].ChildPlanID = plan.SubPlans[i].ChildPlan.ID
}
subPlanLinksToCreate = append(subPlanLinksToCreate, plan.SubPlans[i])
}
// 批量创建 SubPlan 关联记录
if len(subPlanLinksToCreate) > 0 {
if err := tx.CreateInBatches(subPlanLinksToCreate, 100).Error; err != nil { // 批量大小100
return err
}
}
}
return nil
})
}
@@ -214,7 +197,12 @@ func (r *gormPlanRepository) validatePlanTree(tx *gorm.DB, plan *models.Plan) er
return err
}
// 3. 一次性数据库存在性校验
// 3. 检查是否有重复的执行顺序
if err := plan.ValidateExecutionOrder(); err != nil {
return fmt.Errorf("计划 (ID: %d) 的执行顺序无效: %w", plan.ID, err)
}
// 4. 一次性数据库存在性校验
var idsToCheck []uint
for id := range allIDs {
idsToCheck = append(idsToCheck, id)
@@ -229,7 +217,6 @@ func (r *gormPlanRepository) validatePlanTree(tx *gorm.DB, plan *models.Plan) er
return ErrNodeDoesNotExist
}
}
return nil
}

View File

@@ -432,8 +432,8 @@ func TestUpdatePlan_Validation(t *testing.T) {
planC.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}}
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{
{ChildPlanID: 2, ChildPlan: planB},
{ChildPlanID: 3, ChildPlan: planC},
{ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1},
{ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2},
}
return planA
},
@@ -466,8 +466,8 @@ func TestUpdatePlan_Validation(t *testing.T) {
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{
{ChildPlanID: 2, ChildPlan: planB},
{ChildPlanID: 3, ChildPlan: planC}, // C 不存在
{ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1},
{ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, // C 不存在
}
return planA
},
@@ -532,6 +532,37 @@ func TestUpdatePlan_Validation(t *testing.T) {
},
expectedError: "不能同时包含任务和子计划",
},
{
name: "错误-任务执行顺序重复",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
},
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeTasks
planA.Tasks = []models.Task{
{Name: "Task 1", ExecutionOrder: 1},
{Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序
}
return planA
},
expectedError: fmt.Sprintf("任务执行顺序重复: %d", 1),
}, {
name: "错误-子计划执行顺序重复",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
db.Create(&models.Plan{Model: gorm.Model{ID: 10}})
db.Create(&models.Plan{Model: gorm.Model{ID: 11}})
},
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{
{ChildPlanID: 10, ChildPlan: &models.Plan{Model: gorm.Model{ID: 10}}, ExecutionOrder: 1},
{ChildPlanID: 11, ChildPlan: &models.Plan{Model: gorm.Model{ID: 11}}, ExecutionOrder: 1}, // 重复的顺序
}
return planA
},
expectedError: fmt.Sprintf("子计划执行顺序重复: %d", 1),
},
}
for _, tc := range testCases {
@@ -1024,3 +1055,243 @@ func TestUpdatePlan_Reconciliation(t *testing.T) {
})
}
}
// createExistingPlan 辅助函数,用于在数据库中创建已存在的计划
func createExistingPlan(db *gorm.DB, name string, contentType models.PlanContentType) *models.Plan {
plan := &models.Plan{
Name: name,
ContentType: contentType,
}
db.Create(plan)
return plan
}
func TestPlanRepository_Create(t *testing.T) {
type testCase struct {
name string
setupDB func(db *gorm.DB) // 准备数据库的初始状态
inputPlan *models.Plan // 传入 Create 方法的计划对象
expectedError error // 期望的错误类型
verifyDB func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) // 验证数据库状态
}
testCases := []testCase{
{
name: "成功创建-只包含基本信息",
setupDB: func(db *gorm.DB) {
// 无需额外设置
},
inputPlan: &models.Plan{
Name: "简单计划",
Description: "一个不包含任务或子计划的简单计划",
ContentType: models.PlanContentTypeTasks, // 修改为有效的 ContentType
Tasks: []models.Task{}, // 明确为空任务列表
},
expectedError: nil,
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
assert.NotZero(t, createdPlan.ID, "创建后计划ID不应为0")
var foundPlan models.Plan
err := db.First(&foundPlan, createdPlan.ID).Error
assert.NoError(t, err)
assert.Equal(t, "简单计划", foundPlan.Name)
assert.Equal(t, models.PlanContentTypeTasks, foundPlan.ContentType)
var tasks []models.Task
db.Where("plan_id = ?", createdPlan.ID).Find(&tasks)
assert.Len(t, tasks, 0, "不应创建任何任务")
},
},
{
name: "成功创建-包含任务",
setupDB: func(db *gorm.DB) {
// 无需额外设置
},
inputPlan: &models.Plan{
Name: "任务计划",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{Name: "任务A", ExecutionOrder: 1},
{Name: "任务B", ExecutionOrder: 2},
},
},
expectedError: nil,
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
assert.NotZero(t, createdPlan.ID, "计划ID不应为0")
var foundPlan models.Plan
db.Preload("Tasks").First(&foundPlan, createdPlan.ID)
assert.Len(t, foundPlan.Tasks, 2, "应创建两个任务")
assert.NotZero(t, foundPlan.Tasks[0].ID, "任务ID不应为0")
assert.Equal(t, "任务A", foundPlan.Tasks[0].Name)
},
},
{
name: "成功创建-包含子计划关联",
setupDB: func(db *gorm.DB) {
// 预先创建子计划实体,使用有效的 ContentType
createExistingPlan(db, "子计划1", models.PlanContentTypeTasks)
createExistingPlan(db, "子计划2", models.PlanContentTypeTasks)
},
inputPlan: &models.Plan{
Name: "父计划",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ChildPlanID: 1, ExecutionOrder: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 1}}}, // 关联已存在的子计划1
{ChildPlanID: 2, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 2}}}, // 关联已存在的子计划2
},
},
expectedError: nil,
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
assert.NotZero(t, createdPlan.ID, "创建后计划ID不应为0")
// 直接查询 SubPlan 关联记录
var foundSubPlanLinks []models.SubPlan
err := db.Where("parent_plan_id = ?", createdPlan.ID).Find(&foundSubPlanLinks).Error
assert.NoError(t, err)
assert.Len(t, foundSubPlanLinks, 2, "应创建两个子计划关联")
assert.NotZero(t, foundSubPlanLinks[0].ID, "子计划关联ID不应为0")
assert.Equal(t, createdPlan.ID, foundSubPlanLinks[0].ParentPlanID)
assert.Equal(t, uint(1), foundSubPlanLinks[0].ChildPlanID)
},
},
{
name: "失败-计划ID不为0",
setupDB: func(db *gorm.DB) {
// 无需额外设置
},
inputPlan: &models.Plan{
Model: gorm.Model{ID: 100}, // ID不为0
Name: "无效计划",
},
expectedError: repository.ErrCreateWithNonZeroID,
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
// 验证数据库中没有创建该计划
var count int64
db.Model(&models.Plan{}).Where("id = ?", 100).Count(&count)
assert.Equal(t, int64(0), count, "计划不应被创建")
},
},
{
name: "失败-同时包含任务和子计划",
setupDB: func(db *gorm.DB) {
createExistingPlan(db, "子计划", models.PlanContentTypeTasks) // 使用有效的 ContentType
},
inputPlan: &models.Plan{
Name: "混合内容计划",
ContentType: models.PlanContentTypeTasks, // 声明为任务类型
Tasks: []models.Task{{Name: "任务A"}},
SubPlans: []models.SubPlan{{ChildPlanID: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 1}}}}, // 但也包含子计划
},
expectedError: repository.ErrMixedContent,
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
// 验证数据库中没有创建该计划
var count int64
db.Model(&models.Plan{}).Where("name = ?", "混合内容计划").Count(&count)
assert.Equal(t, int64(0), count, "计划不应被创建")
},
},
{
name: "失败-子计划ID为0",
setupDB: func(db *gorm.DB) {
// 无需额外设置
},
inputPlan: &models.Plan{
Name: "无效子计划关联",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ChildPlanID: 0, ChildPlan: &models.Plan{Model: gorm.Model{ID: 0}}}, // 子计划ID为0
},
},
expectedError: repository.ErrSubPlanIDIsZeroOnCreate,
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
var count int64
db.Model(&models.Plan{}).Where("name = ?", "无效子计划关联").Count(&count)
assert.Equal(t, int64(0), count, "计划不应被创建")
},
},
{
name: "失败-子计划在数据库中不存在",
setupDB: func(db *gorm.DB) {
// 不创建ID为999的计划
},
inputPlan: &models.Plan{
Name: "不存在的子计划",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ChildPlanID: 999, ChildPlan: &models.Plan{Model: gorm.Model{ID: 999}}}, // 关联一个不存在的ID
},
},
expectedError: repository.ErrNodeDoesNotExist,
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
var count int64
db.Model(&models.Plan{}).Where("name = ?", "不存在的子计划").Count(&count)
assert.Equal(t, int64(0), count, "计划不应被创建")
},
},
{
name: "失败-任务执行顺序重复",
setupDB: func(db *gorm.DB) {
// 无需额外设置
},
inputPlan: &models.Plan{
Name: "重复任务顺序计划",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{Name: "Task 1", ExecutionOrder: 1},
{Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序
},
},
expectedError: fmt.Errorf("任务执行顺序重复: %d", 1), // 假设 Create 方法会返回此错误
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
var count int64
db.Model(&models.Plan{}).Where("name = ?", "重复任务顺序计划").Count(&count)
assert.Equal(t, int64(0), count, "重复任务顺序的计划不应被创建")
},
},
{
name: "失败-子计划执行顺序重复",
setupDB: func(db *gorm.DB) {
createExistingPlan(db, "子计划A", models.PlanContentTypeTasks)
createExistingPlan(db, "子计划B", models.PlanContentTypeTasks)
},
inputPlan: &models.Plan{
Name: "重复子计划顺序计划",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ChildPlanID: 1, ExecutionOrder: 1},
{ChildPlanID: 2, ExecutionOrder: 1}, // 重复的顺序
},
},
expectedError: fmt.Errorf("子计划执行顺序重复: %d", 1), // 假设 Create 方法会返回此错误
verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) {
var count int64
db.Model(&models.Plan{}).Where("name = ?", "重复子计划顺序计划").Count(&count)
assert.Equal(t, int64(0), count, "重复子计划顺序的计划不应被创建")
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
db := setupTestDB(t)
repo := repository.NewGormPlanRepository(db)
// 准备数据库状态
tc.setupDB(db)
// 执行 Create 操作
err := repo.Create(tc.inputPlan)
// 断言错误
if tc.expectedError != nil {
assert.Error(t, err)
// 使用 Contains 检查错误信息,因为 fmt.Errorf 会创建新的错误实例
assert.Contains(t, err.Error(), tc.expectedError.Error())
} else {
assert.NoError(t, err)
}
// 验证数据库状态
tc.verifyDB(t, db, tc.inputPlan)
})
}
}