package repository import ( "context" "errors" "fmt" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "gorm.io/gorm" ) // RawMaterialListOptions 定义了查询原料列表时的筛选条件 type RawMaterialListOptions struct { Name *string NutrientName *string MinReferencePrice *float32 // 参考价格最小值 MaxReferencePrice *float32 // 参考价格最大值 OrderBy string } // StockLogListOptions 定义了查询库存日志列表时的筛选条件 type StockLogListOptions struct { RawMaterialID *uint32 RawMaterialName *string SourceTypes []models.StockLogSourceType StartTime *time.Time EndTime *time.Time OrderBy string } // RawMaterialRepository 定义了与原料相关的数据库操作接口 type RawMaterialRepository interface { CreateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error GetRawMaterialByID(ctx context.Context, id uint32) (*models.RawMaterial, error) GetRawMaterialByName(ctx context.Context, name string) (*models.RawMaterial, error) ListRawMaterials(ctx context.Context, opts RawMaterialListOptions, page, pageSize int) ([]models.RawMaterial, int64, error) UpdateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error DeleteRawMaterial(ctx context.Context, id uint32) error DeleteNutrientsByRawMaterialIDTx(ctx context.Context, db *gorm.DB, rawMaterialID uint32) error CreateBatchRawMaterialNutrientsTx(ctx context.Context, db *gorm.DB, nutrients []models.RawMaterialNutrient) error // 库存日志相关方法 CreateRawMaterialStockLog(ctx context.Context, log *models.RawMaterialStockLog) error GetLatestRawMaterialStockLog(ctx context.Context, rawMaterialID uint32) (*models.RawMaterialStockLog, error) // BatchGetLatestStockLogsForMaterials 批量获取一组原料的最新库存日志 BatchGetLatestStockLogsForMaterials(ctx context.Context, materialIDs []uint32) (map[uint32]models.RawMaterialStockLog, error) // ListStockLogs 分页列出库存变动日志 ListStockLogs(ctx context.Context, opts StockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) } // gormRawMaterialRepository 是 RawMaterialRepository 的 GORM 实现 type gormRawMaterialRepository struct { ctx context.Context db *gorm.DB } // NewGormRawMaterialRepository 创建一个新的 RawMaterialRepository GORM 实现实例 func NewGormRawMaterialRepository(ctx context.Context, db *gorm.DB) RawMaterialRepository { return &gormRawMaterialRepository{ctx: ctx, db: db} } // CreateRawMaterial 创建一个新的原料 func (r *gormRawMaterialRepository) CreateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateRawMaterial") return r.db.WithContext(repoCtx).Create(rawMaterial).Error } // GetRawMaterialByID 根据ID获取单个原料,并预加载关联的营养素信息 func (r *gormRawMaterialRepository) GetRawMaterialByID(ctx context.Context, id uint32) (*models.RawMaterial, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetRawMaterialByID") var rawMaterial models.RawMaterial // 如果记录未找到,GORM 会返回 gorm.ErrRecordNotFound 错误 if err := r.db.WithContext(repoCtx).Preload("RawMaterialNutrients.Nutrient").First(&rawMaterial, id).Error; err != nil { return nil, err } return &rawMaterial, nil } // GetRawMaterialByName 根据名称获取单个原料,并预加载关联的营养素信息 func (r *gormRawMaterialRepository) GetRawMaterialByName(ctx context.Context, name string) (*models.RawMaterial, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetRawMaterialByName") var rawMaterial models.RawMaterial // 如果记录未找到,GORM 会返回 gorm.ErrRecordNotFound 错误 if err := r.db.WithContext(repoCtx).Preload("RawMaterialNutrients.Nutrient").Where("name = ?", name).First(&rawMaterial).Error; err != nil { return nil, err } return &rawMaterial, nil } // ListRawMaterials 列出所有原料(分页),支持按名称和营养名称筛选 func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, opts RawMaterialListOptions, page, pageSize int) ([]models.RawMaterial, int64, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterials") var rawMaterials []models.RawMaterial var total int64 db := r.db.WithContext(repoCtx).Model(&models.RawMaterial{}) // 应用筛选条件 if opts.Name != nil && *opts.Name != "" { db = db.Where("name LIKE ?", "%"+*opts.Name+"%") } // 如果传入了营养名称,则使用子查询进行筛选 if opts.NutrientName != nil && *opts.NutrientName != "" { // 子查询:从 raw_material_nutrients 和 nutrients 表中找到所有包含该营养的 raw_material_id subQuery := r.db.Model(&models.RawMaterialNutrient{}). Select("raw_material_id"). Joins("JOIN nutrients ON nutrients.id = raw_material_nutrients.nutrient_id"). Where("nutrients.name LIKE ?", "%"+*opts.NutrientName+"%") db = db.Where("id IN (?)", subQuery) } // 筛选参考价格 if opts.MinReferencePrice != nil { db = db.Where("reference_price >= ?", *opts.MinReferencePrice) } if opts.MaxReferencePrice != nil { db = db.Where("reference_price <= ?", *opts.MaxReferencePrice) } // 首先计算总数 if err := db.Count(&total).Error; err != nil { return nil, 0, err } // 然后应用排序、分页并获取数据 if opts.OrderBy != "" { db = db.Order(opts.OrderBy) } offset := (page - 1) * pageSize if err := db.Preload("RawMaterialNutrients.Nutrient").Offset(offset).Limit(pageSize).Find(&rawMaterials).Error; err != nil { return nil, 0, err } return rawMaterials, total, nil } // UpdateRawMaterial 更新一个原料 func (r *gormRawMaterialRepository) UpdateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRawMaterial") // 使用 map 更新以避免 GORM 的零值问题,并确保只更新指定字段 updateData := map[string]interface{}{ "name": rawMaterial.Name, "description": rawMaterial.Description, "reference_price": rawMaterial.ReferencePrice, } result := r.db.WithContext(repoCtx).Model(&models.RawMaterial{}).Where("id = ?", rawMaterial.ID).Updates(updateData) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return fmt.Errorf("未找到要更新的原料,ID: %d", rawMaterial.ID) } return nil } // DeleteRawMaterial 根据ID删除一个原料,并级联软删除关联的 RawMaterialNutrient 和 RawMaterialStockLog 记录 func (r *gormRawMaterialRepository) DeleteRawMaterial(ctx context.Context, id uint32) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteRawMaterial") return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { // 1. 查找 RawMaterial 记录,确保其存在 var rawMaterial models.RawMaterial if err := tx.First(&rawMaterial, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("未找到要删除的原料,ID: %d", id) } return fmt.Errorf("查询原料失败: %w", err) } // 2. 软删除所有关联的 RawMaterialNutrient 记录 if err := tx.Where("raw_material_id = ?", id).Delete(&models.RawMaterialNutrient{}).Error; err != nil { return fmt.Errorf("软删除关联的原料营养素记录失败: %w", err) } // 3. 软删除所有关联的 RawMaterialStockLog 记录 if err := tx.Where("raw_material_id = ?", id).Delete(&models.RawMaterialStockLog{}).Error; err != nil { return fmt.Errorf("软删除关联的原料库存日志记录失败: %w", err) } // 4. 软删除 RawMaterial 记录本身 if err := tx.Delete(&rawMaterial).Error; err != nil { return fmt.Errorf("软删除原料失败: %w", err) } return nil }) } // DeleteNutrientsByRawMaterialIDTx 在事务中软删除指定原料的所有营养成分 func (r *gormRawMaterialRepository) DeleteNutrientsByRawMaterialIDTx(ctx context.Context, db *gorm.DB, rawMaterialID uint32) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteNutrientsByRawMaterialIDTx") tx := db.WithContext(repoCtx) if err := tx.Where("raw_material_id = ?", rawMaterialID).Delete(&models.RawMaterialNutrient{}).Error; err != nil { return fmt.Errorf("软删除原料营养成分失败: %w", err) } return nil } // CreateBatchRawMaterialNutrientsTx 在事务中批量创建原料营养成分 func (r *gormRawMaterialRepository) CreateBatchRawMaterialNutrientsTx(ctx context.Context, db *gorm.DB, nutrients []models.RawMaterialNutrient) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateBatchRawMaterialNutrientsTx") // 如果没有要创建的记录,直接返回成功,避免执行空的Create语句 if len(nutrients) == 0 { return nil } // 确保每个营养都关联到正确的原料ID // 注意:这里假设传入的 nutrients 已经设置了正确的 RawMaterialID for i := range nutrients { if nutrients[i].RawMaterialID == 0 { return fmt.Errorf("创建原料营养时 RecipeID 不能为空") } } if err := db.WithContext(repoCtx).Create(&nutrients).Error; err != nil { return fmt.Errorf("批量创建原料营养成分失败: %w", err) } return nil } // CreateRawMaterialStockLog 创建一条新的原料库存日志 func (r *gormRawMaterialRepository) CreateRawMaterialStockLog(ctx context.Context, log *models.RawMaterialStockLog) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateRawMaterialStockLog") return r.db.WithContext(repoCtx).Create(log).Error } // GetLatestRawMaterialStockLog 获取指定原料的最新一条库存日志 func (r *gormRawMaterialRepository) GetLatestRawMaterialStockLog(ctx context.Context, rawMaterialID uint32) (*models.RawMaterialStockLog, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLatestRawMaterialStockLog") var latestLog models.RawMaterialStockLog err := r.db.WithContext(repoCtx). Where("raw_material_id = ?", rawMaterialID). Order("happened_at DESC, id DESC"). // 优先按时间降序,然后按ID降序确保唯一最新 First(&latestLog).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil // 如果没有日志记录,不视为错误,返回nil } return nil, err } return &latestLog, nil } // BatchGetLatestStockLogsForMaterials 批量获取一组原料的最新库存日志 func (r *gormRawMaterialRepository) BatchGetLatestStockLogsForMaterials(ctx context.Context, materialIDs []uint32) (map[uint32]models.RawMaterialStockLog, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "BatchGetLatestStockLogsForMaterials") if len(materialIDs) == 0 { return make(map[uint32]models.RawMaterialStockLog), nil } var latestLogs []models.RawMaterialStockLog // 使用窗口函数 ROW_NUMBER() 来为每个原料的日志分区,并按时间倒序排名。 // 这样可以高效地一次性查询出每个原料的最新一条日志。 subQuery := r.db.Model(&models.RawMaterialStockLog{}). Select("*, ROW_NUMBER() OVER(PARTITION BY raw_material_id ORDER BY happened_at DESC, id DESC) as rn"). Where("raw_material_id IN ?", materialIDs) err := r.db.WithContext(repoCtx). Table("(?) as sub", subQuery). Where("rn = 1"). Find(&latestLogs).Error if err != nil { return nil, fmt.Errorf("批量获取最新库存日志失败: %w", err) } // 将结果转换为 map[uint32]models.RawMaterialStockLog 以方便查找 logMap := make(map[uint32]models.RawMaterialStockLog, len(latestLogs)) for _, log := range latestLogs { logMap[log.RawMaterialID] = log } return logMap, nil } // ListStockLogs 分页列出库存变动日志 func (r *gormRawMaterialRepository) ListStockLogs(ctx context.Context, opts StockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "ListStockLogs") var logs []models.RawMaterialStockLog var total int64 db := r.db.WithContext(repoCtx).Model(&models.RawMaterialStockLog{}) // 应用筛选条件 if opts.RawMaterialID != nil { db = db.Where("raw_material_id = ?", *opts.RawMaterialID) } // 新增:按原料名称模糊搜索 if opts.RawMaterialName != nil && *opts.RawMaterialName != "" { // 使用子查询找到匹配的原料ID subQuery := r.db.Model(&models.RawMaterial{}).Select("id").Where("name LIKE ?", "%"+*opts.RawMaterialName+"%") db = db.Where("raw_material_id IN (?)", subQuery) } if len(opts.SourceTypes) > 0 { db = db.Where("source_type IN ?", opts.SourceTypes) } if opts.StartTime != nil { db = db.Where("happened_at >= ?", *opts.StartTime) } if opts.EndTime != nil { db = db.Where("happened_at <= ?", *opts.EndTime) } // 首先计算总数 if err := db.Count(&total).Error; err != nil { return nil, 0, fmt.Errorf("统计库存日志总数失败: %w", err) } // 然后应用排序、分页并获取数据 if opts.OrderBy != "" { db = db.Order(opts.OrderBy) } else { // 默认排序 db = db.Order("happened_at DESC, id DESC") } offset := (page - 1) * pageSize if err := db.Preload("RawMaterial").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil { return nil, 0, fmt.Errorf("查询库存日志列表失败: %w", err) } return logs, total, nil }