删除原有食物逻辑和模型

新增原料和营养价值表和原料库存日志和营养表定义
This commit is contained in:
2025-11-18 22:22:31 +08:00
parent 200a358933
commit e1399be538
15 changed files with 289 additions and 1898 deletions

View File

@@ -173,9 +173,6 @@ func (a *API) setupRoutes() {
monitorGroup.GET("/task-execution-logs", a.monitorController.ListTaskExecutionLogs)
monitorGroup.GET("/pending-collections", a.monitorController.ListPendingCollections)
monitorGroup.GET("/user-action-logs", a.monitorController.ListUserActionLogs)
monitorGroup.GET("/raw-material-purchases", a.monitorController.ListRawMaterialPurchases)
monitorGroup.GET("raw-material-stock-logs", a.monitorController.ListRawMaterialStockLogs)
monitorGroup.GET("/feed-usage-records", a.monitorController.ListFeedUsageRecords)
monitorGroup.GET("/medication-logs", a.monitorController.ListMedicationLogs)
monitorGroup.GET("/pig-batch-logs", a.monitorController.ListPigBatchLogs)
monitorGroup.GET("/weighing-batches", a.monitorController.ListWeighingBatches)

View File

@@ -231,108 +231,6 @@ func (c *Controller) ListUserActionLogs(ctx echo.Context) error {
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作日志成功", resp, actionType, "获取用户操作日志成功", req)
}
// ListRawMaterialPurchases godoc
// @Summary 获取原料采购记录列表
// @Description 根据提供的过滤条件,分页获取原料采购记录
// @Tags 数据监控
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListRawMaterialPurchaseRequest true "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialPurchaseResponse}
// @Router /api/v1/monitor/raw-material-purchases [get]
func (c *Controller) ListRawMaterialPurchases(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRawMaterialPurchases")
const actionType = "获取原料采购记录列表"
var req dto.ListRawMaterialPurchaseRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListRawMaterialPurchases(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料采购记录成功", resp, actionType, "获取原料采购记录成功", req)
}
// ListRawMaterialStockLogs godoc
// @Summary 获取原料库存日志列表
// @Description 根据提供的过滤条件,分页获取原料库存日志
// @Tags 数据监控
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListRawMaterialStockLogRequest true "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialStockLogResponse}
// @Router /api/v1/monitor/raw-material-stock-logs [get]
func (c *Controller) ListRawMaterialStockLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRawMaterialStockLogs")
const actionType = "获取原料库存日志列表"
var req dto.ListRawMaterialStockLogRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListRawMaterialStockLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料库存日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料库存日志成功", resp, actionType, "获取原料库存日志成功", req)
}
// ListFeedUsageRecords godoc
// @Summary 获取饲料使用记录列表
// @Description 根据提供的过滤条件,分页获取饲料使用记录
// @Tags 数据监控
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListFeedUsageRecordRequest true "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListFeedUsageRecordResponse}
// @Router /api/v1/monitor/feed-usage-records [get]
func (c *Controller) ListFeedUsageRecords(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListFeedUsageRecords")
const actionType = "获取饲料使用记录列表"
var req dto.ListFeedUsageRecordRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListFeedUsageRecords(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取饲料使用记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取饲料使用记录成功", resp, actionType, "获取饲料使用记录成功", req)
}
// ListMedicationLogs godoc
// @Summary 获取用药记录列表
// @Description 根据提供的过滤条件,分页获取用药记录

View File

@@ -170,94 +170,6 @@ func NewListUserActionLogResponse(data []models.UserActionLog, total int64, page
}
}
// NewListRawMaterialPurchaseResponse 从模型数据创建列表响应 DTO
func NewListRawMaterialPurchaseResponse(data []models.RawMaterialPurchase, total int64, page, pageSize int) *ListRawMaterialPurchaseResponse {
dtos := make([]RawMaterialPurchaseDTO, len(data))
for i, item := range data {
dtos[i] = RawMaterialPurchaseDTO{
ID: item.ID,
RawMaterialID: item.RawMaterialID,
RawMaterial: RawMaterialDTO{
ID: item.RawMaterial.ID,
Name: item.RawMaterial.Name,
},
Supplier: item.Supplier,
Amount: item.Amount,
UnitPrice: item.UnitPrice,
TotalPrice: item.TotalPrice,
PurchaseDate: item.PurchaseDate,
CreatedAt: item.CreatedAt,
}
}
return &ListRawMaterialPurchaseResponse{
List: dtos,
Pagination: PaginationDTO{
Total: total,
Page: page,
PageSize: pageSize,
},
}
}
// NewListRawMaterialStockLogResponse 从模型数据创建列表响应 DTO
func NewListRawMaterialStockLogResponse(data []models.RawMaterialStockLog, total int64, page, pageSize int) *ListRawMaterialStockLogResponse {
dtos := make([]RawMaterialStockLogDTO, len(data))
for i, item := range data {
dtos[i] = RawMaterialStockLogDTO{
ID: item.ID,
RawMaterialID: item.RawMaterialID,
ChangeAmount: item.ChangeAmount,
SourceType: item.SourceType,
SourceID: item.SourceID,
HappenedAt: item.HappenedAt,
Remarks: item.Remarks,
}
}
return &ListRawMaterialStockLogResponse{
List: dtos,
Pagination: PaginationDTO{
Total: total,
Page: page,
PageSize: pageSize,
},
}
}
// NewListFeedUsageRecordResponse 从模型数据创建列表响应 DTO
func NewListFeedUsageRecordResponse(data []models.FeedUsageRecord, total int64, page, pageSize int) *ListFeedUsageRecordResponse {
dtos := make([]FeedUsageRecordDTO, len(data))
for i, item := range data {
dtos[i] = FeedUsageRecordDTO{
ID: item.ID,
PenID: item.PenID,
Pen: PenDTO{
ID: item.Pen.ID,
Name: item.Pen.PenNumber,
},
FeedFormulaID: item.FeedFormulaID,
FeedFormula: FeedFormulaDTO{
ID: item.FeedFormula.ID,
Name: item.FeedFormula.Name,
},
Amount: item.Amount,
RecordedAt: item.RecordedAt,
OperatorID: item.OperatorID,
Remarks: item.Remarks,
}
}
return &ListFeedUsageRecordResponse{
List: dtos,
Pagination: PaginationDTO{
Total: total,
Page: page,
PageSize: pageSize,
},
}
}
// NewListMedicationLogResponse 从模型数据创建列表响应 DTO
func NewListMedicationLogResponse(data []models.MedicationLog, total int64, page, pageSize int) *ListMedicationLogResponse {
dtos := make([]MedicationLogDTO, len(data))

View File

@@ -202,120 +202,6 @@ type ListUserActionLogResponse struct {
Pagination PaginationDTO `json:"pagination"`
}
// --- RawMaterialPurchase ---
// ListRawMaterialPurchaseRequest 定义了获取原料采购列表的请求参数
type ListRawMaterialPurchaseRequest struct {
Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"`
RawMaterialID *uint32 `json:"raw_material_id" query:"raw_material_id"`
Supplier *string `json:"supplier" query:"supplier"`
StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"`
}
// RawMaterialDTO 是用于API响应的简化版原料结构
type RawMaterialDTO struct {
ID uint32 `json:"id"`
Name string `json:"name"`
}
// RawMaterialPurchaseDTO 是用于API响应的原料采购结构
type RawMaterialPurchaseDTO struct {
ID uint32 `json:"id"`
RawMaterialID uint32 `json:"raw_material_id"`
RawMaterial RawMaterialDTO `json:"raw_material"`
Supplier string `json:"supplier"`
Amount float32 `json:"amount"`
UnitPrice float32 `json:"unit_price"`
TotalPrice float32 `json:"total_price"`
PurchaseDate time.Time `json:"purchase_date"`
CreatedAt time.Time `json:"created_at"`
}
// ListRawMaterialPurchaseResponse 是获取原料采购列表的响应结构
type ListRawMaterialPurchaseResponse struct {
List []RawMaterialPurchaseDTO `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// --- RawMaterialStockLog ---
// ListRawMaterialStockLogRequest 定义了获取原料库存日志列表的请求参数
type ListRawMaterialStockLogRequest struct {
Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"`
RawMaterialID *uint32 `json:"raw_material_id" query:"raw_material_id"`
SourceType *string `json:"source_type" query:"source_type"`
SourceID *uint32 `json:"source_id" query:"source_id"`
StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"`
}
// RawMaterialStockLogDTO 是用于API响应的原料库存日志结构
type RawMaterialStockLogDTO struct {
ID uint32 `json:"id"`
RawMaterialID uint32 `json:"raw_material_id"`
ChangeAmount float32 `json:"change_amount"`
SourceType models.StockLogSourceType `json:"source_type"`
SourceID uint32 `json:"source_id"`
HappenedAt time.Time `json:"happened_at"`
Remarks string `json:"remarks"`
}
// ListRawMaterialStockLogResponse 是获取原料库存日志列表的响应结构
type ListRawMaterialStockLogResponse struct {
List []RawMaterialStockLogDTO `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// --- FeedUsageRecord ---
// ListFeedUsageRecordRequest 定义了获取饲料使用记录列表的请求参数
type ListFeedUsageRecordRequest struct {
Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"`
PenID *uint32 `json:"pen_id" query:"pen_id"`
FeedFormulaID *uint32 `json:"feed_formula_id" query:"feed_formula_id"`
OperatorID *uint32 `json:"operator_id" query:"operator_id"`
StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"`
}
// PenDTO 是用于API响应的简化版猪栏结构
type PenDTO struct {
ID uint32 `json:"id"`
Name string `json:"name"`
}
// FeedFormulaDTO 是用于API响应的简化版饲料配方结构
type FeedFormulaDTO struct {
ID uint32 `json:"id"`
Name string `json:"name"`
}
// FeedUsageRecordDTO 是用于API响应的饲料使用记录结构
type FeedUsageRecordDTO struct {
ID uint32 `json:"id"`
PenID uint32 `json:"pen_id"`
Pen PenDTO `json:"pen"`
FeedFormulaID uint32 `json:"feed_formula_id"`
FeedFormula FeedFormulaDTO `json:"feed_formula"`
Amount float32 `json:"amount"`
RecordedAt time.Time `json:"recorded_at"`
OperatorID uint32 `json:"operator_id"`
Remarks string `json:"remarks"`
}
// ListFeedUsageRecordResponse 是获取饲料使用记录列表的响应结构
type ListFeedUsageRecordResponse struct {
List []FeedUsageRecordDTO `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// --- MedicationLog ---
// ListMedicationLogRequest 定义了获取用药记录列表的请求参数

View File

@@ -17,9 +17,6 @@ type MonitorService interface {
ListTaskExecutionLogs(ctx context.Context, req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error)
ListPendingCollections(ctx context.Context, req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error)
ListUserActionLogs(ctx context.Context, req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error)
ListRawMaterialPurchases(ctx context.Context, req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error)
ListRawMaterialStockLogs(ctx context.Context, req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error)
ListFeedUsageRecords(ctx context.Context, req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error)
ListMedicationLogs(ctx context.Context, req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error)
ListPigBatchLogs(ctx context.Context, req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error)
ListWeighingBatches(ctx context.Context, req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error)
@@ -40,7 +37,6 @@ type monitorService struct {
planRepository repository.PlanRepository
pendingCollectionRepo repository.PendingCollectionRepository
userActionLogRepo repository.UserActionLogRepository
rawMaterialRepo repository.RawMaterialRepository
medicationRepo repository.MedicationLogRepository
pigBatchRepo repository.PigBatchRepository
pigBatchLogRepo repository.PigBatchLogRepository
@@ -59,7 +55,6 @@ func NewMonitorService(
planRepository repository.PlanRepository,
pendingCollectionRepo repository.PendingCollectionRepository,
userActionLogRepo repository.UserActionLogRepository,
rawMaterialRepo repository.RawMaterialRepository,
medicationRepo repository.MedicationLogRepository,
pigBatchRepo repository.PigBatchRepository,
pigBatchLogRepo repository.PigBatchLogRepository,
@@ -76,7 +71,6 @@ func NewMonitorService(
planRepository: planRepository,
pendingCollectionRepo: pendingCollectionRepo,
userActionLogRepo: userActionLogRepo,
rawMaterialRepo: rawMaterialRepo,
medicationRepo: medicationRepo,
pigBatchRepo: pigBatchRepo,
pigBatchLogRepo: pigBatchLogRepo,
@@ -236,68 +230,6 @@ func (s *monitorService) ListUserActionLogs(ctx context.Context, req *dto.ListUs
return dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize), nil
}
// ListRawMaterialPurchases 负责处理查询原料采购记录列表的业务逻辑
func (s *monitorService) ListRawMaterialPurchases(ctx context.Context, req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterialPurchases")
opts := repository.RawMaterialPurchaseListOptions{
RawMaterialID: req.RawMaterialID,
Supplier: req.Supplier,
OrderBy: req.OrderBy,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
data, total, err := s.rawMaterialRepo.ListRawMaterialPurchases(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
return dto.NewListRawMaterialPurchaseResponse(data, total, req.Page, req.PageSize), nil
}
// ListRawMaterialStockLogs 负责处理查询原料库存日志列表的业务逻辑
func (s *monitorService) ListRawMaterialStockLogs(ctx context.Context, req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterialStockLogs")
opts := repository.RawMaterialStockLogListOptions{
RawMaterialID: req.RawMaterialID,
SourceID: req.SourceID,
OrderBy: req.OrderBy,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
if req.SourceType != nil {
sourceType := models.StockLogSourceType(*req.SourceType)
opts.SourceType = &sourceType
}
data, total, err := s.rawMaterialRepo.ListRawMaterialStockLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
return dto.NewListRawMaterialStockLogResponse(data, total, req.Page, req.PageSize), nil
}
// ListFeedUsageRecords 负责处理查询饲料使用记录列表的业务逻辑
func (s *monitorService) ListFeedUsageRecords(ctx context.Context, req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListFeedUsageRecords")
opts := repository.FeedUsageRecordListOptions{
PenID: req.PenID,
FeedFormulaID: req.FeedFormulaID,
OperatorID: req.OperatorID,
OrderBy: req.OrderBy,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
data, total, err := s.rawMaterialRepo.ListFeedUsageRecords(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
return dto.NewListFeedUsageRecordResponse(data, total, req.Page, req.PageSize), nil
}
// ListMedicationLogs 负责处理查询用药记录列表的业务逻辑
func (s *monitorService) ListMedicationLogs(ctx context.Context, req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListMedicationLogs")

View File

@@ -79,7 +79,6 @@ type Repositories struct {
pigTradeRepo repository.PigTradeRepository
pigSickPigLogRepo repository.PigSickLogRepository
medicationLogRepo repository.MedicationLogRepository
rawMaterialRepo repository.RawMaterialRepository
notificationRepo repository.NotificationRepository
alarmRepo repository.AlarmRepository
unitOfWork repository.UnitOfWork
@@ -108,7 +107,6 @@ func initRepositories(ctx context.Context, db *gorm.DB) *Repositories {
pigTradeRepo: repository.NewGormPigTradeRepository(logs.AddCompName(baseCtx, "PigTradeRepo"), db),
pigSickPigLogRepo: repository.NewGormPigSickLogRepository(logs.AddCompName(baseCtx, "PigSickPigLogRepo"), db),
medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), db),
rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db),
notificationRepo: repository.NewGormNotificationRepository(logs.AddCompName(baseCtx, "NotificationRepo"), db),
alarmRepo: repository.NewGormAlarmRepository(logs.AddCompName(baseCtx, "AlarmRepo"), db),
unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db),
@@ -247,7 +245,6 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
infra.repos.planRepo,
infra.repos.pendingCollectionRepo,
infra.repos.userActionLogRepo,
infra.repos.rawMaterialRepo,
infra.repos.medicationLogRepo,
infra.repos.pigBatchRepo,
infra.repos.pigBatchLogRepo,

View File

@@ -167,9 +167,6 @@ func (ps *PostgresStorage) creatingHyperTable(ctx context.Context) 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.MedicationLog{}, "happened_at"},
{models.PigBatchLog{}, "happened_at"},
{models.WeighingBatch{}, "weighing_time"},
@@ -180,6 +177,7 @@ func (ps *PostgresStorage) creatingHyperTable(ctx context.Context) error {
{models.PigSale{}, "sale_date"},
{models.Notification{}, "alarm_timestamp"},
{models.HistoricalAlarm{}, "trigger_time"},
{models.RawMaterialStockLog{}, "happened_at"},
}
for _, table := range tablesToConvert {
@@ -210,9 +208,6 @@ func (ps *PostgresStorage) applyCompressionPolicies(ctx context.Context) error {
{models.TaskExecutionLog{}, "task_id"},
{models.PendingCollection{}, "device_id"},
{models.UserActionLog{}, "user_id"},
{models.RawMaterialPurchase{}, "raw_material_id"},
{models.RawMaterialStockLog{}, "raw_material_id"},
{models.FeedUsageRecord{}, "pen_id"},
{models.MedicationLog{}, "pig_batch_id"},
{models.PigBatchLog{}, "pig_batch_id"},
{models.WeighingBatch{}, "pig_batch_id"},
@@ -223,6 +218,7 @@ func (ps *PostgresStorage) applyCompressionPolicies(ctx context.Context) error {
{models.PigSale{}, "pig_batch_id"},
{models.Notification{}, "user_id"},
{models.HistoricalAlarm{}, "source_id"},
{models.RawMaterialStockLog{}, "raw_material_id"},
}
for _, policy := range policies {
@@ -258,6 +254,15 @@ func (ps *PostgresStorage) creatingIndex(ctx context.Context) error {
// 使用 IF NOT EXISTS 保证幂等性
// 如果索引已存在,此命令不会报错
// 为 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)
}
logger.Debug("成功为 raw_material_nutrients 创建部分唯一索引 (或已存在)")
// 为 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);"

View File

@@ -1,111 +0,0 @@
package models
import (
"time"
)
/*
饲料和饲喂相关的模型
*/
// RawMaterial 代表饲料的原料。
// 建议:所有重量单位统一存储 (例如, 全部使用 'g'),便于计算和避免转换错误。
type RawMaterial struct {
Model
Name string `gorm:"size:100;unique;not null;comment:原料名称"`
Description string `gorm:"size:255;comment:描述"`
Quantity float32 `gorm:"not null;comment:库存总量, 单位: g"`
}
func (RawMaterial) TableName() string {
return "raw_materials"
}
// RawMaterialPurchase 记录了原料的每一次采购。
type RawMaterialPurchase struct {
Model
RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
Supplier string `gorm:"size:100;comment:供应商"`
Amount float32 `gorm:"not null;comment:采购数量, 单位: g"`
UnitPrice float32 `gorm:"comment:单价"`
TotalPrice float32 `gorm:"comment:总价"`
PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
CreatedAt time.Time
}
func (RawMaterialPurchase) TableName() string {
return "raw_material_purchases"
}
// StockLogSourceType 定义了库存日志来源的类型
type StockLogSourceType string
const (
StockLogSourcePurchase StockLogSourceType = "采购入库"
StockLogSourceFeeding StockLogSourceType = "饲喂出库"
StockLogSourceDeteriorate StockLogSourceType = "变质出库"
StockLogSourceSale StockLogSourceType = "售卖出库"
StockLogSourceMiscellaneous StockLogSourceType = "杂用领取"
StockLogSourceManual StockLogSourceType = "手动盘点"
)
// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
type RawMaterialStockLog struct {
Model
RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
ChangeAmount float32 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"`
SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"`
SourceID uint32 `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"`
HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"`
Remarks string `gorm:"comment:备注, 如主动领取的理由等"`
}
func (RawMaterialStockLog) TableName() string {
return "raw_material_stock_logs"
}
// FeedFormula 代表饲料配方。
// 对于没有配方的外购饲料,可以将其视为一种特殊的 RawMaterial, 并为其创建一个仅包含它自己的 FeedFormula。
type FeedFormula struct {
Model
Name string `gorm:"size:100;unique;not null;comment:配方名称"`
Description string `gorm:"size:255;comment:描述"`
Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"`
}
func (FeedFormula) TableName() string {
return "feed_formulas"
}
// FeedFormulaComponent 代表配方中的一种原料及其占比。
type FeedFormulaComponent struct {
Model
FeedFormulaID uint32 `gorm:"not null;index;comment:外键到 FeedFormula"`
RawMaterialID uint32 `gorm:"not null;index;comment:外键到 RawMaterial"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
Percentage float32 `gorm:"not null;comment:该原料在配方中的百分比 (0-1.0)"`
}
func (FeedFormulaComponent) TableName() string {
return "feed_formula_components"
}
// FeedUsageRecord 代表饲料使用记录。
// 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula,
// 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。
type FeedUsageRecord struct {
Model
PenID uint32 `gorm:"not null;index;comment:关联的猪栏ID"`
Pen Pen `gorm:"foreignKey:PenID"`
FeedFormulaID uint32 `gorm:"not null;index;comment:使用的饲料配方ID"`
FeedFormula FeedFormula `gorm:"foreignKey:FeedFormulaID"`
Amount float32 `gorm:"not null;comment:使用数量, 单位: g"`
RecordedAt time.Time `gorm:"primaryKey;comment:记录时间"`
OperatorID uint32 `gorm:"not null;comment:操作员"`
Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"`
}
func (FeedUsageRecord) TableName() string {
return "feed_usage_records"
}

View File

@@ -63,11 +63,9 @@ func GetAllModels() []interface{} {
// Feed Models
&RawMaterial{},
&RawMaterialPurchase{},
&Nutrient{},
&RawMaterialNutrient{},
&RawMaterialStockLog{},
&FeedFormula{},
&FeedFormulaComponent{},
&FeedUsageRecord{},
// Medication Models
&Medication{},

View File

@@ -0,0 +1,92 @@
package models
import (
"time"
)
// StockLogSourceType 定义了库存日志来源的类型
type StockLogSourceType string
const (
StockLogSourcePurchase StockLogSourceType = "采购入库"
StockLogSourceFeeding StockLogSourceType = "饲喂出库"
StockLogSourceDeteriorate StockLogSourceType = "变质出库"
StockLogSourceSale StockLogSourceType = "售卖出库"
StockLogSourceMiscellaneous StockLogSourceType = "杂用领取"
StockLogSourceManual StockLogSourceType = "手动盘点"
StockLogSourceFermentStart StockLogSourceType = "发酵出库" // 原料投入发酵,从库存中扣除
StockLogSourceFermentEnd StockLogSourceType = "发酵入库" // 发酵料产出,作为新原料计入库存
)
// NutrientType 定义了营养素的分类,用于配方优化和成本控制。
type NutrientType string
const (
PositiveNutrient NutrientType = "正面营养" // 希望在配方中最大化的营养素,如蛋白质、能量
NegativeNutrient NutrientType = "负面营养" // 需要控制上限的营养素,如粗纤维、霉菌毒素
)
// RawMaterial 代表一种原料的静态定义,是系统中的原料字典。
type RawMaterial struct {
Model
Name string `gorm:"size:100;unique;not null;comment:原料名称"`
Description string `gorm:"size:255;comment:描述"`
// Quantity 是当前库存的快照值,用于提供高性能的库存查询。
// 注意:此字段的值必须在数据库事务中与 RawMaterialStockLog 同步更新,以保证数据一致性。
Quantity float32 `gorm:"not null;default:0;comment:当前库存快照, 单位: g"`
}
func (RawMaterial) TableName() string {
return "raw_materials"
}
// Nutrient 代表一种营养素的静态定义,是系统中的营养素字典。
// 注意本系统强制统一营养单位不再单独设置Unit字段。
// 约定:宏量营养素(粗蛋白等)单位为百分比(%),微量元素(氨基酸等)单位为毫克/千克(mg/kg)。
type Nutrient struct {
Model
Name string `gorm:"size:100;unique;not null;comment:营养素名称"`
Type NutrientType `gorm:"size:50;not null;comment:营养素类型 (正面营养/负面营养)"`
Description string `gorm:"size:255;comment:描述"`
}
func (Nutrient) TableName() string {
return "nutrients"
}
// RawMaterialNutrient 存储了特定原料的特定营养素的含量值。
// 这是连接原料和营养素的“营养价值表”。
// 注意:其唯一性由 postgres.go 中的部分唯一索引保证,以兼容软删除。
type RawMaterialNutrient struct {
Model
RawMaterialID uint32 `gorm:"not null;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
NutrientID uint32 `gorm:"not null;comment:关联的营养素ID"`
Nutrient Nutrient `gorm:"foreignKey:NutrientID"`
// Value 存储营养价值含量。单位遵循 Nutrient 表中定义的系统级约定。
Value float32 `gorm:"not null;comment:营养价值含量"`
}
func (RawMaterialNutrient) TableName() string {
return "raw_material_nutrients"
}
// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
// 它是保证数据一致性和可审计性的核心。
type RawMaterialStockLog struct {
Model
RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
ChangeAmount float32 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库, 单位: g"`
// SourceType 告知 SourceID 关联的是哪种类型的业务单据。
SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"`
// SourceID 是一个多态外键关联到触发此次变动的业务单据ID (如采购单ID)。
// 对于无单据的业务(如手动盘点)此字段可为NULL。
SourceID *uint32 `gorm:"index;comment:来源业务单据的ID"`
HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"`
Remarks string `gorm:"comment:备注"`
}
func (RawMaterialStockLog) TableName() string {
return "raw_material_stock_logs"
}

View File

@@ -1,187 +0,0 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// RawMaterialPurchaseListOptions 定义了查询原料采购记录时的可选参数
type RawMaterialPurchaseListOptions struct {
RawMaterialID *uint32
Supplier *string
StartTime *time.Time // 基于 purchase_date 字段
EndTime *time.Time // 基于 purchase_date 字段
OrderBy string // 例如 "purchase_date asc"
}
// RawMaterialStockLogListOptions 定义了查询原料库存日志时的可选参数
type RawMaterialStockLogListOptions struct {
RawMaterialID *uint32
SourceType *models.StockLogSourceType
SourceID *uint32
StartTime *time.Time // 基于 happened_at 字段
EndTime *time.Time // 基于 happened_at 字段
OrderBy string // 例如 "happened_at asc"
}
// FeedUsageRecordListOptions 定义了查询饲料使用记录时的可选参数
type FeedUsageRecordListOptions struct {
PenID *uint32
FeedFormulaID *uint32
OperatorID *uint32
StartTime *time.Time // 基于 recorded_at 字段
EndTime *time.Time // 基于 recorded_at 字段
OrderBy string // 例如 "recorded_at asc"
}
// RawMaterialRepository 定义了与原料相关的数据库操作接口
type RawMaterialRepository interface {
ListRawMaterialPurchases(ctx context.Context, opts RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error)
ListRawMaterialStockLogs(ctx context.Context, opts RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error)
ListFeedUsageRecords(ctx context.Context, opts FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, 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}
}
// ListRawMaterialPurchases 实现了分页和过滤查询原料采购记录的功能
func (r *gormRawMaterialRepository) ListRawMaterialPurchases(ctx context.Context, opts RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterialPurchases")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.RawMaterialPurchase
var total int64
query := r.db.WithContext(repoCtx).Model(&models.RawMaterialPurchase{})
if opts.RawMaterialID != nil {
query = query.Where("raw_material_id = ?", *opts.RawMaterialID)
}
if opts.Supplier != nil {
query = query.Where("supplier LIKE ?", "%"+*opts.Supplier+"%")
}
if opts.StartTime != nil {
query = query.Where("purchase_date >= ?", *opts.StartTime)
}
if opts.EndTime != nil {
query = query.Where("purchase_date <= ?", *opts.EndTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "purchase_date DESC"
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy).Preload("RawMaterial")
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
}
// ListRawMaterialStockLogs 实现了分页和过滤查询原料库存日志的功能
func (r *gormRawMaterialRepository) ListRawMaterialStockLogs(ctx context.Context, opts RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterialStockLogs")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.RawMaterialStockLog
var total int64
query := r.db.WithContext(repoCtx).Model(&models.RawMaterialStockLog{})
if opts.RawMaterialID != nil {
query = query.Where("raw_material_id = ?", *opts.RawMaterialID)
}
if opts.SourceType != nil {
query = query.Where("source_type = ?", *opts.SourceType)
}
if opts.SourceID != nil {
query = query.Where("source_id = ?", *opts.SourceID)
}
if opts.StartTime != nil {
query = query.Where("happened_at >= ?", *opts.StartTime)
}
if opts.EndTime != nil {
query = query.Where("happened_at <= ?", *opts.EndTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "happened_at DESC"
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy)
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
}
// ListFeedUsageRecords 实现了分页和过滤查询饲料使用记录的功能
func (r *gormRawMaterialRepository) ListFeedUsageRecords(ctx context.Context, opts FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListFeedUsageRecords")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.FeedUsageRecord
var total int64
query := r.db.WithContext(repoCtx).Model(&models.FeedUsageRecord{})
if opts.PenID != nil {
query = query.Where("pen_id = ?", *opts.PenID)
}
if opts.FeedFormulaID != nil {
query = query.Where("feed_formula_id = ?", *opts.FeedFormulaID)
}
if opts.OperatorID != nil {
query = query.Where("operator_id = ?", *opts.OperatorID)
}
if opts.StartTime != nil {
query = query.Where("recorded_at >= ?", *opts.StartTime)
}
if opts.EndTime != nil {
query = query.Where("recorded_at <= ?", *opts.EndTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "recorded_at DESC"
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy).Preload("Pen").Preload("FeedFormula")
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
}