实现 猪舍相关路由组 和 猪圈相关路由组

This commit is contained in:
2025-10-03 18:27:53 +08:00
parent 5754a1d94c
commit 8cbe313c89
13 changed files with 840 additions and 47 deletions

View File

@@ -160,11 +160,16 @@ func (ps *PostgresStorage) creatingHyperTable() error {
{models.TaskExecutionLog{}, "created_at"},
{models.PendingCollection{}, "created_at"},
{models.UserActionLog{}, "time"},
{models.RawMaterialPurchase{}, "purchase_date"},
{models.RawMaterialStockLog{}, "happened_at"},
{models.FeedUsageRecord{}, "recorded_at"},
{models.GroupMedicationLog{}, "happened_at"},
{models.PigBatchLog{}, "happened_at"},
}
for _, table := range tablesToConvert {
tableName := table.model.TableName()
chunkInterval := "1 day" // 统一设置为1
chunkInterval := "7 days" // 统一设置为7
ps.logger.Infow("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval)
sql := fmt.Sprintf("SELECT create_hypertable('%s', '%s', chunk_time_interval => INTERVAL '%s', if_not_exists => TRUE);", tableName, table.timeColumn, chunkInterval)
if err := ps.db.Exec(sql).Error; err != nil {
@@ -193,7 +198,7 @@ func (ps *PostgresStorage) applyCompressionPolicies() error {
for _, policy := range policies {
tableName := policy.model.TableName()
compressAfter := "3 days" // 统一设置为2天后即进入第3天开始压缩
compressAfter := "15 days" // 统一设置为15天后开始压缩
// 1. 开启表的压缩设置,并指定分段列
ps.logger.Infow("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn)
@@ -239,14 +244,5 @@ func (ps *PostgresStorage) creatingIndex() error {
}
ps.logger.Info("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
// 为 devices 表的 properties 字段创建 GIN 索引
//ps.logger.Info("正在为 devices 表的 properties 字段创建 GIN 索引")
//ginDevicePropertiesIndexSQL := "CREATE INDEX IF NOT EXISTS idx_devices_properties_gin ON devices USING GIN (properties);"
//if err := ps.db.Exec(ginDevicePropertiesIndexSQL).Error; err != nil {
// ps.logger.Errorw("为 devices 的 properties 字段创建 GIN 索引失败", "error", err)
// return fmt.Errorf("为 devices 的 properties 字段创建 GIN 索引失败: %w", err)
//}
//ps.logger.Info("成功为 devices 的 properties 字段创建 GIN 索引 (或已存在)")
return nil
}

View File

@@ -19,16 +19,27 @@ type RawMaterial struct {
Quantity float64 `gorm:"not null;comment:库存总量, 单位: g"`
}
func (RawMaterial) TableName() string {
return "raw_materials"
}
// RawMaterialPurchase 记录了原料的每一次采购。
type RawMaterialPurchase struct {
gorm.Model
ID uint `gorm:"primaryKey"`
RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
Supplier string `gorm:"size:100;comment:供应商"`
Amount float64 `gorm:"not null;comment:采购数量, 单位: g"`
UnitPrice float64 `gorm:"comment:单价"`
TotalPrice float64 `gorm:"comment:总价"`
PurchaseDate time.Time `gorm:"not null;comment:采购日期"`
PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (RawMaterialPurchase) TableName() string {
return "raw_material_purchases"
}
// StockLogSourceType 定义了库存日志来源的类型
@@ -45,13 +56,20 @@ const (
// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
type RawMaterialStockLog struct {
gorm.Model
ID uint `gorm:"primaryKey"`
RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"`
ChangeAmount float64 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"`
SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"`
SourceID uint `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"`
HappenedAt time.Time `gorm:"not null;comment:业务发生时间"`
HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"`
Remarks string `gorm:"comment:备注, 如主动领取的理由等"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (RawMaterialStockLog) TableName() string {
return "raw_material_stock_logs"
}
// FeedFormula 代表饲料配方。
@@ -63,6 +81,10 @@ type FeedFormula struct {
Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"`
}
func (FeedFormula) TableName() string {
return "feed_formulas"
}
// FeedFormulaComponent 代表配方中的一种原料及其占比。
type FeedFormulaComponent struct {
gorm.Model
@@ -72,17 +94,28 @@ type FeedFormulaComponent struct {
Percentage float64 `gorm:"not null;comment:该原料在配方中的百分比 (0-1.0)"`
}
func (FeedFormulaComponent) TableName() string {
return "feed_formula_components"
}
// FeedUsageRecord 代表饲料使用记录。
// 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula,
// 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。
type FeedUsageRecord struct {
gorm.Model
ID uint `gorm:"primaryKey"`
PenID uint `gorm:"not null;index;comment:关联的猪栏ID"`
Pen Pen `gorm:"foreignKey:PenID"`
FeedFormulaID uint `gorm:"not null;index;comment:使用的饲料配方ID"`
FeedFormula FeedFormula `gorm:"foreignKey:FeedFormulaID"`
Amount float64 `gorm:"not null;comment:使用数量, 单位: g"`
RecordedAt time.Time `gorm:"not null;comment:记录时间"`
RecordedAt time.Time `gorm:"primaryKey;comment:记录时间"`
OperatorID uint `gorm:"not null;comment:操作员"`
Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (FeedUsageRecord) TableName() string {
return "feed_usage_records"
}

View File

@@ -67,6 +67,10 @@ type Medication struct {
Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"`
}
func (Medication) TableName() string {
return "medications"
}
// MedicationReasonType 定义了用药原因
type MedicationReasonType string
@@ -78,7 +82,7 @@ const (
// GroupMedicationLog 记录了对整个猪批次的用药情况
type GroupMedicationLog struct {
gorm.Model
ID uint `gorm:"primaryKey"`
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
MedicationID uint `gorm:"not null;index;comment:关联的药品ID"`
Medication Medication `gorm:"foreignKey:MedicationID"` // 预加载药品信息
@@ -87,5 +91,12 @@ type GroupMedicationLog struct {
Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"`
Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"`
Operator string `gorm:"size:50;comment:操作员"`
HappenedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:用药时间"`
HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (GroupMedicationLog) TableName() string {
return "group_medication_logs"
}

View File

@@ -12,20 +12,45 @@ import (
// 这个函数用于在数据库初始化时自动迁移所有的表结构。
func GetAllModels() []interface{} {
return []interface{}{
// Core Models
&User{},
&UserActionLog{},
// Device Models
&Device{},
&AreaController{},
&DeviceTemplate{},
&SensorData{},
&DeviceCommandLog{},
// Plan & Task Models
&Plan{},
&SubPlan{},
&Task{},
&PlanExecutionLog{},
&TaskExecutionLog{},
&PendingTask{},
&SensorData{},
&DeviceCommandLog{},
&PendingCollection{},
&AreaController{},
&DeviceTemplate{},
&UserActionLog{},
// Farm Asset Models
&PigHouse{},
&Pen{},
// Pig & Batch Models
&PigBatch{},
&PigBatchLog{},
// Feed Models
&RawMaterial{},
&RawMaterialPurchase{},
&RawMaterialStockLog{},
&FeedFormula{},
&FeedFormulaComponent{},
&FeedUsageRecord{},
// Medication Models
&Medication{},
&GroupMedicationLog{},
}
}

View File

@@ -44,6 +44,10 @@ type PigBatch struct {
Pens []Pen `gorm:"foreignKey:PigBatchID;comment:所在圈舍ID"`
}
func (PigBatch) TableName() string {
return "pig_batches"
}
// LogChangeType 定义了猪批次数量变更的类型
type LogChangeType string
@@ -60,7 +64,7 @@ const (
// PigBatchLog 记录了猪批次数量或状态的每一次变更
type PigBatchLog struct {
gorm.Model
ID uint `gorm:"primaryKey"`
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"`
ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"`
@@ -70,5 +74,12 @@ type PigBatchLog struct {
BeforeSickCount int `gorm:"not null;comment:变更前病猪数"`
AfterSickCount int `gorm:"not null;comment:变更后病猪数"`
Operator string `gorm:"size:50;comment:操作员"`
HappenedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:事件发生时间"`
HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (PigBatchLog) TableName() string {
return "pig_batch_logs"
}

View File

@@ -0,0 +1,157 @@
package repository
import (
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
var (
ErrHouseContainsPens = errors.New("cannot delete a pig house that still contains pens")
ErrHouseNotFound = errors.New("the specified pig house does not exist")
)
// PigFarmRepository 定义了与猪场资产(猪舍、猪栏)相关的数据库操作接口
type PigFarmRepository interface {
// PigHouse methods
CreatePigHouse(house *models.PigHouse) error
GetPigHouseByID(id uint) (*models.PigHouse, error)
ListPigHouses() ([]models.PigHouse, error)
UpdatePigHouse(house *models.PigHouse) error
DeletePigHouse(id uint) error
// Pen methods
CreatePen(pen *models.Pen) error
GetPenByID(id uint) (*models.Pen, error)
ListPens() ([]models.Pen, error)
UpdatePen(pen *models.Pen) error
DeletePen(id uint) error
}
// gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现
type gormPigFarmRepository struct {
db *gorm.DB
}
// NewGormPigFarmRepository 创建一个新的 PigFarmRepository GORM 实现实例
func NewGormPigFarmRepository(db *gorm.DB) PigFarmRepository {
return &gormPigFarmRepository{db: db}
}
// --- PigHouse Implementation ---
func (r *gormPigFarmRepository) CreatePigHouse(house *models.PigHouse) error {
return r.db.Create(house).Error
}
func (r *gormPigFarmRepository) GetPigHouseByID(id uint) (*models.PigHouse, error) {
var house models.PigHouse
if err := r.db.First(&house, id).Error; err != nil {
return nil, err
}
return &house, nil
}
func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) {
var houses []models.PigHouse
if err := r.db.Find(&houses).Error; err != nil {
return nil, err
}
return houses, nil
}
func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) error {
result := r.db.Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *gormPigFarmRepository) DeletePigHouse(id uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var penCount int64
if err := tx.Model(&models.Pen{}).Where("house_id = ?", id).Count(&penCount).Error; err != nil {
return err
}
if penCount > 0 {
return ErrHouseContainsPens
}
result := tx.Delete(&models.PigHouse{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
}
// --- Pen Implementation ---
func (r *gormPigFarmRepository) CreatePen(pen *models.Pen) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 验证所属猪舍是否存在
if err := tx.First(&models.PigHouse{}, pen.HouseID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrHouseNotFound
}
return err
}
return tx.Create(pen).Error
})
}
func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) {
var pen models.Pen
if err := r.db.First(&pen, id).Error; err != nil {
return nil, err
}
return &pen, nil
}
func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) {
var pens []models.Pen
if err := r.db.Find(&pens).Error; err != nil {
return nil, err
}
return pens, nil
}
func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 验证所属猪舍是否存在
if err := tx.First(&models.PigHouse{}, pen.HouseID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrHouseNotFound
}
return err
}
result := tx.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
}
func (r *gormPigFarmRepository) DeletePen(id uint) error {
result := r.db.Delete(&models.Pen{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}