Files
pig-farm-controller/internal/infra/repository/raw_material_repository.go
2025-11-25 20:03:36 +08:00

332 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}