diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 6babcd7..b2a513b 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -6,6 +6,7 @@ package database import ( "context" "fmt" + "strings" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -265,144 +266,169 @@ func (ps *PostgresStorage) creatingIndex(ctx context.Context) error { func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error { storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingUniqueIndex") - // 为 raw_material_nutrients 表创建部分唯一索引,以兼容软删除 - logger.Debug("正在为 raw_material_nutrients 表创建部分唯一索引") - partialIndexSQL := "CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_material_nutrients_unique_when_not_deleted ON raw_material_nutrients (raw_material_id, nutrient_id) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 raw_material_nutrients 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 raw_material_nutrients 创建部分唯一索引失败: %w", err) + // uniqueIndexDefinition 结构体定义了唯一索引的详细信息 + type uniqueIndexDefinition struct { + tableName string // 索引所属的表名 + columns []string // 构成唯一索引的列名 + indexName string // 唯一索引的名称 + whereClause string // 可选的 WHERE 子句,用于创建部分索引 + description string // 索引的描述,用于日志记录 } - logger.Debug("成功为 raw_material_nutrients 创建部分唯一索引 (或已存在)") - // 为 pig_breeds 表创建部分唯一索引,以兼容软删除 (name 唯一) - logger.Debug("正在为 pig_breeds 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_breeds_unique_name_when_not_deleted ON pig_breeds (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_breeds 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_breeds 创建部分唯一索引失败: %w", err) + // 定义所有需要创建的唯一索引 + uniqueIndexesToCreate := []uniqueIndexDefinition{ + { + tableName: models.RawMaterialNutrient{}.TableName(), + columns: []string{"raw_material_id", "nutrient_id"}, + indexName: "idx_raw_material_nutrients_unique_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "确保同一原料中的每种营养成分不重复", + }, + { + tableName: models.PigBreed{}.TableName(), + columns: []string{"name"}, + indexName: "idx_pig_breeds_unique_name_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "pig_breeds 表的部分唯一索引 (name 唯一)", + }, + { + tableName: models.PigAgeStage{}.TableName(), + columns: []string{"name"}, + indexName: "idx_pig_age_stages_unique_name_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "pig_age_stages 表的部分唯一索引 (name 唯一)", + }, + { + tableName: models.PigType{}.TableName(), + columns: []string{"breed_id", "age_stage_id"}, + indexName: "idx_pig_types_unique_breed_age_stage_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "pig_types 表的部分唯一索引 (breed_id, age_stage_id 组合唯一)", + }, + { + tableName: models.PigNutrientRequirement{}.TableName(), + columns: []string{"pig_type_id", "nutrient_id"}, + indexName: "idx_pig_nutrient_requirements_unique_type_nutrient_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "pig_nutrient_requirements 表的部分唯一索引 (pig_type_id, nutrient_id 组合唯一)", + }, + { + tableName: models.User{}.TableName(), + columns: []string{"username"}, + indexName: "idx_users_unique_username_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "users 表的部分唯一索引 (username 唯一)", + }, + { + tableName: models.AreaController{}.TableName(), + columns: []string{"name"}, + indexName: "idx_area_controllers_unique_name_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "area_controllers 表的部分唯一索引 (Name 唯一)", + }, + { + tableName: models.AreaController{}.TableName(), + columns: []string{"network_id"}, + indexName: "idx_area_controllers_unique_network_id_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "area_controllers 表的部分唯一索引 (NetworkID 唯一)", + }, + { + tableName: models.DeviceTemplate{}.TableName(), + columns: []string{"name"}, + indexName: "idx_device_templates_unique_name_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "device_templates 表的部分唯一索引 (name 唯一)", + }, + { + tableName: models.PigBatch{}.TableName(), + columns: []string{"batch_number"}, + indexName: "idx_pig_batches_unique_batch_number_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "pig_batches 表的部分唯一索引 (batch_number 唯一)", + }, + { + tableName: models.PigHouse{}.TableName(), + columns: []string{"name"}, + indexName: "idx_pig_houses_unique_name_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "pig_houses 表的部分唯一索引 (name 唯一)", + }, + { + tableName: models.RawMaterial{}.TableName(), + columns: []string{"name"}, + indexName: "idx_raw_materials_unique_name_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "raw_materials 表的部分唯一索引 (name 唯一)", + }, + { + tableName: models.Nutrient{}.TableName(), + columns: []string{"name"}, + indexName: "idx_nutrients_unique_name_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "nutrients 表的部分唯一索引 (name 唯一)", + }, } - logger.Debug("成功为 pig_breeds 创建部分唯一索引 (或已存在)") - // 为 pig_age_stages 表创建部分唯一索引,以兼容软删除 (name 唯一) - logger.Debug("正在为 pig_age_stages 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_age_stages_unique_name_when_not_deleted ON pig_age_stages (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_age_stages 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_age_stages 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 pig_age_stages 创建部分唯一索引 (或已存在)") + for _, indexDef := range uniqueIndexesToCreate { + logger.Debugw("正在为表创建部分唯一索引", "表名", indexDef.tableName, "索引名", indexDef.indexName, "描述", indexDef.description) - // 为 pig_types 表创建部分唯一索引,以兼容软删除 (breed_id, age_stage_id 组合唯一) - logger.Debug("正在为 pig_types 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_types_unique_breed_age_stage_when_not_deleted ON pig_types (breed_id, age_stage_id) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_types 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_types 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 pig_types 创建部分唯一索引 (或已存在)") + // 拼接列名字符串 + columnsStr := strings.Join(indexDef.columns, ", ") + // 构建 SQL 语句 + sql := fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (%s) %s;", + indexDef.indexName, indexDef.tableName, columnsStr, indexDef.whereClause) - // 为 pig_nutrient_requirements 表创建部分唯一索引,以兼容软删除 (pig_type_id, nutrient_id 组合唯一) - logger.Debug("正在为 pig_nutrient_requirements 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_nutrient_requirements_unique_type_nutrient_when_not_deleted ON pig_nutrient_requirements (pig_type_id, nutrient_id) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_nutrient_requirements 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_nutrient_requirements 创建部分唯一索引失败: %w", err) + if err := ps.db.WithContext(storageCtx).Exec(sql).Error; err != nil { + logger.Errorw("创建部分唯一索引失败", "表名", indexDef.tableName, "索引名", indexDef.indexName, "错误", err) + return fmt.Errorf("为 %s 表创建部分唯一索引 %s 失败: %w", indexDef.tableName, indexDef.indexName, err) + } + logger.Debugw("成功为表创建部分唯一索引 (或已存在)", "表名", indexDef.tableName, "索引名", indexDef.indexName) } - logger.Debug("成功为 pig_nutrient_requirements 创建部分唯一索引 (或已存在)") - // 为 users 表创建部分唯一索引 - logger.Debug("正在为 users 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_unique_username_when_not_deleted ON users (username) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 users 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 users 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 users 创建部分唯一索引 (或已存在)") - - // 为 area_controllers 表创建部分唯一索引 (Name) - logger.Debug("正在为 area_controllers 表创建部分唯一索引 (Name)") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_area_controllers_unique_name_when_not_deleted ON area_controllers (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 area_controllers 创建部分唯一索引 (Name) 失败", "error", err) - return fmt.Errorf("为 area_controllers 创建部分唯一索引 (Name) 失败: %w", err) - } - logger.Debug("成功为 area_controllers 创建部分唯一索引 (Name) (或已存在)") - - // 为 area_controllers 表创建部分唯一索引 (NetworkID) - logger.Debug("正在为 area_controllers 表创建部分唯一索引 (NetworkID)") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_area_controllers_unique_network_id_when_not_deleted ON area_controllers (network_id) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 area_controllers 创建部分唯一索引 (NetworkID) 失败", "error", err) - return fmt.Errorf("为 area_controllers 创建部分唯一索引 (NetworkID) 失败: %w", err) - } - logger.Debug("成功为 area_controllers 创建部分唯一索引 (NetworkID) (或已存在)") - - // 为 device_templates 表创建部分唯一索引 - logger.Debug("正在为 device_templates 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_device_templates_unique_name_when_not_deleted ON device_templates (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 device_templates 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 device_templates 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 device_templates 创建部分唯一索引 (或已存在)") - - // 为 pig_batches 表创建部分唯一索引 - logger.Debug("正在为 pig_batches 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_batches_unique_batch_number_when_not_deleted ON pig_batches (batch_number) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_batches 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_batches 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 pig_batches 创建部分唯一索引 (或已存在)") - - // 为 pig_houses 表创建部分唯一索引 - logger.Debug("正在为 pig_houses 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_houses_unique_name_when_not_deleted ON pig_houses (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_houses 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_houses 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 pig_houses 创建部分唯一索引 (或已存在)") - - // 为 raw_materials 表创建部分唯一索引 - logger.Debug("正在为 raw_materials 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_materials_unique_name_when_not_deleted ON raw_materials (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 raw_materials 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 raw_materials 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 raw_materials 创建部分唯一索引 (或已存在)") - - // 为 nutrients 表创建部分唯一索引 - logger.Debug("正在为 nutrients 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_nutrients_unique_name_when_not_deleted ON nutrients (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 nutrients 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 nutrients 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 nutrients 创建部分唯一索引 (或已存在)") return nil } func (ps *PostgresStorage) createGinIndexes(ctx context.Context) error { storageCtx, logger := logs.Trace(ctx, ps.ctx, "createGinIndexes") - // 为 sensor_data 表的 data 字段创建 GIN 索引 - logger.Debug("正在为 sensor_data 表的 data 字段创建 GIN 索引") - ginSensorDataIndexSQL := "CREATE INDEX IF NOT EXISTS idx_sensor_data_data_gin ON sensor_data USING GIN (data);" - if err := ps.db.WithContext(storageCtx).Exec(ginSensorDataIndexSQL).Error; err != nil { - logger.Errorw("为 sensor_data 的 data 字段创建 GIN 索引失败", "error", err) - return fmt.Errorf("为 sensor_data 的 data 字段创建 GIN 索引失败: %w", err) + // ginIndexDefinition 结构体定义了 GIN 索引的详细信息 + type ginIndexDefinition struct { + tableName string // 索引所属的表名 + columnName string // 需要创建 GIN 索引的列名 + indexName string // GIN 索引的名称 + description string // 索引的描述,用于日志记录 } - logger.Debug("成功为 sensor_data 的 data 字段创建 GIN 索引 (或已存在)") - // 为 tasks.parameters 创建 GIN 索引 - logger.Debug("正在为 tasks 表的 parameters 字段创建 GIN 索引") - taskGinIndexSQL := "CREATE INDEX IF NOT EXISTS idx_tasks_parameters_gin ON tasks USING GIN (parameters);" - if err := ps.db.WithContext(storageCtx).Exec(taskGinIndexSQL).Error; err != nil { - logger.Errorw("为 tasks 的 parameters 字段创建 GIN 索引失败", "error", err) - return fmt.Errorf("为 tasks 的 parameters 字段创建 GIN 索引失败: %w", err) + // 定义所有需要创建的 GIN 索引 + ginIndexesToCreate := []ginIndexDefinition{ + { + tableName: "sensor_data", + columnName: "data", + indexName: "idx_sensor_data_data_gin", + description: "为 sensor_data 表的 data 字段创建 GIN 索引", + }, + { + tableName: "tasks", + columnName: "parameters", + indexName: "idx_tasks_parameters_gin", + description: "为 tasks 表的 parameters 字段创建 GIN 索引", + }, } - logger.Debug("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)") + + for _, indexDef := range ginIndexesToCreate { + logger.Debugw("正在创建 GIN 索引", "表名", indexDef.tableName, "列名", indexDef.columnName, "描述", indexDef.description) + + // 构建 SQL 语句 + sql := fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s USING GIN (%s);", + indexDef.indexName, indexDef.tableName, indexDef.columnName) + + if err := ps.db.WithContext(storageCtx).Exec(sql).Error; err != nil { + logger.Errorw("创建 GIN 索引失败", "表名", indexDef.tableName, "索引名", indexDef.indexName, "错误", err) + return fmt.Errorf("为 %s 表的 %s 字段创建 GIN 索引 %s 失败: %w", indexDef.tableName, indexDef.columnName, indexDef.indexName, err) + } + logger.Debugw("成功创建 GIN 索引 (或已存在)", "表名", indexDef.tableName, "索引名", indexDef.indexName) + } + return nil } diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go index 84471d2..52de9dd 100644 --- a/internal/infra/models/farm_asset.go +++ b/internal/infra/models/farm_asset.go @@ -12,6 +12,10 @@ type PigHouse struct { Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏 } +func (ph PigHouse) TableName() string { + return "pig_houses" +} + // PenStatus 定义了猪栏的当前状态 type PenStatus string @@ -33,3 +37,7 @@ type Pen struct { Capacity int `gorm:"not null;comment:设计容量 (头)"` Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"` } + +func (p Pen) TableName() string { + return "pens" +}