修复软删除和唯一索引同时存在的bug

This commit is contained in:
2025-11-20 17:37:02 +08:00
parent 1f3d3d8a7c
commit da934a9bbb
7 changed files with 80 additions and 8 deletions

View File

@@ -309,6 +309,78 @@ func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error {
return fmt.Errorf("为 pig_nutrient_requirements 创建部分唯一索引失败: %w", err) return fmt.Errorf("为 pig_nutrient_requirements 创建部分唯一索引失败: %w", err)
} }
logger.Debug("成功为 pig_nutrient_requirements 创建部分唯一索引 (或已存在)") 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 return nil
} }

View File

@@ -21,11 +21,11 @@ type AreaController struct {
Model Model
// Name 是主控的业务名称,例如 "1号猪舍主控" // Name 是主控的业务名称,例如 "1号猪舍主控"
Name string `gorm:"not null;unique" json:"name"` Name string `gorm:"not null" json:"name"`
// NetworkID 是主控在通信网络中的唯一标识,例如 LoRaWAN 的 DevEUI。 // NetworkID 是主控在通信网络中的唯一标识,例如 LoRaWAN 的 DevEUI。
// 这是 transport 层用来寻址的关键。 // 这是 transport 层用来寻址的关键。
NetworkID string `gorm:"not null;unique;index" json:"network_id"` NetworkID string `gorm:"not null;index" json:"network_id"`
// Location 描述了主控的物理安装位置。 // Location 描述了主控的物理安装位置。
Location string `gorm:"index" json:"location"` Location string `gorm:"index" json:"location"`

View File

@@ -108,7 +108,7 @@ type DeviceTemplate struct {
Model Model
// Name 是此模板的唯一名称, 例如 "FanModel-XYZ-2000" 或 "TempSensor-T1" // Name 是此模板的唯一名称, 例如 "FanModel-XYZ-2000" 或 "TempSensor-T1"
Name string `gorm:"not null;unique" json:"name"` Name string `gorm:"not null" json:"name"`
// Manufacturer 是设备的制造商。 // Manufacturer 是设备的制造商。
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`

View File

@@ -7,7 +7,7 @@ package models
// PigHouse 定义了猪舍,是猪栏的集合 // PigHouse 定义了猪舍,是猪栏的集合
type PigHouse struct { type PigHouse struct {
Model Model
Name string `gorm:"size:100;not null;unique;comment:猪舍名称, 如 '育肥舍A栋'"` Name string `gorm:"size:100;not null;comment:猪舍名称, 如 '育肥舍A栋'"`
Description string `gorm:"size:255;comment:描述信息"` Description string `gorm:"size:255;comment:描述信息"`
Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏 Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏
} }

View File

@@ -31,7 +31,7 @@ const (
// PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪 // PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪
type PigBatch struct { type PigBatch struct {
Model Model
BatchNumber string `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"` BatchNumber string `gorm:"size:50;not null;comment:批次编号,如 2024-W25-A01"`
OriginType PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"` OriginType PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"`
StartDate time.Time `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"` StartDate time.Time `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"`
EndDate time.Time `gorm:"not null;comment:批次结束日期 (全部淘汰或售出)"` EndDate time.Time `gorm:"not null;comment:批次结束日期 (全部淘汰或售出)"`

View File

@@ -21,7 +21,7 @@ const (
// RawMaterial 代表一种原料的静态定义,是系统中的原料字典。 // RawMaterial 代表一种原料的静态定义,是系统中的原料字典。
type RawMaterial struct { type RawMaterial struct {
Model Model
Name string `gorm:"size:100;unique;not null;comment:原料名称"` Name string `gorm:"size:100;not null;comment:原料名称"`
Description string `gorm:"size:255;comment:描述"` Description string `gorm:"size:255;comment:描述"`
// RawMaterialNutrients 关联此原料的所有营养素含量信息 // RawMaterialNutrients 关联此原料的所有营养素含量信息
RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:RawMaterialID"` RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:RawMaterialID"`
@@ -36,7 +36,7 @@ func (RawMaterial) TableName() string {
// 约定:宏量营养素(粗蛋白等)单位为百分比(%),微量元素(氨基酸等)单位为毫克/千克(mg/kg)。 // 约定:宏量营养素(粗蛋白等)单位为百分比(%),微量元素(氨基酸等)单位为毫克/千克(mg/kg)。
type Nutrient struct { type Nutrient struct {
Model Model
Name string `gorm:"size:100;unique;not null;comment:营养素名称"` Name string `gorm:"size:100;not null;comment:营养素名称"`
Description string `gorm:"size:255;comment:描述"` Description string `gorm:"size:255;comment:描述"`
// RawMaterialNutrients 记录营养在哪些原料中存在且比例是多少 // RawMaterialNutrients 记录营养在哪些原料中存在且比例是多少
RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:NutrientID"` RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:NutrientID"`

View File

@@ -44,7 +44,7 @@ type User struct {
// Username 是用户的登录名,应该是唯一的 // Username 是用户的登录名,应该是唯一的
// 修正了 gorm 标签的拼写错误 (移除了 gorm 后面的冒号) // 修正了 gorm 标签的拼写错误 (移除了 gorm 后面的冒号)
Username string `gorm:"unique;not null" json:"username"` Username string `gorm:"not null" json:"username"`
// Password 存储的是加密后的密码哈希,而不是明文 // Password 存储的是加密后的密码哈希,而不是明文
// json:"-" 标签确保此字段在序列化为 JSON 时被忽略,防止密码泄露 // json:"-" 标签确保此字段在序列化为 JSON 时被忽略,防止密码泄露