Compare commits
	
		
			11 Commits
		
	
	
		
			8cbe313c89
			...
			c27b5bd708
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c27b5bd708 | |||
| 4e17ddf638 | |||
| d273932693 | |||
| fadc1e2535 | |||
| c4fb237604 | |||
| 645c92978b | |||
| c50366f670 | |||
| 258e350c35 | |||
| aced495cd6 | |||
| 25e9e07cc8 | |||
| 6cc6d719e1 | 
@@ -48,6 +48,7 @@ type API struct {
 | 
			
		||||
	deviceController    *device.Controller             // 设备控制器实例
 | 
			
		||||
	planController      *plan.Controller               // 计划控制器实例
 | 
			
		||||
	pigFarmController   *management.PigFarmController  // 猪场管理控制器实例
 | 
			
		||||
	pigBatchController  *management.PigBatchController // 猪批次控制器实例
 | 
			
		||||
	listenHandler       webhook.ListenHandler          // 设备上行事件监听器
 | 
			
		||||
	analysisTaskManager *task.AnalysisPlanTaskManager  // 计划触发器管理器实例
 | 
			
		||||
}
 | 
			
		||||
@@ -62,6 +63,7 @@ func NewAPI(cfg config.ServerConfig,
 | 
			
		||||
	deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库
 | 
			
		||||
	planRepository repository.PlanRepository,
 | 
			
		||||
	pigFarmService service.PigFarmService,
 | 
			
		||||
	pigBatchService service.PigBatchService, // 添加猪批次服务
 | 
			
		||||
	userActionLogRepository repository.UserActionLogRepository,
 | 
			
		||||
	tokenService token.TokenService,
 | 
			
		||||
	auditService audit.Service, // 注入审计服务
 | 
			
		||||
@@ -96,6 +98,8 @@ func NewAPI(cfg config.ServerConfig,
 | 
			
		||||
		planController: plan.NewController(logger, planRepository, analysisTaskManager),
 | 
			
		||||
		// 在 NewAPI 中初始化猪场管理控制器
 | 
			
		||||
		pigFarmController: management.NewPigFarmController(logger, pigFarmService),
 | 
			
		||||
		// 在 NewAPI 中初始化猪批次控制器
 | 
			
		||||
		pigBatchController: management.NewPigBatchController(logger, pigBatchService),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	api.setupRoutes() // 设置所有路由
 | 
			
		||||
@@ -221,6 +225,17 @@ func (a *API) setupRoutes() {
 | 
			
		||||
		}
 | 
			
		||||
		a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)")
 | 
			
		||||
 | 
			
		||||
		// 猪批次相关路由组
 | 
			
		||||
		pigBatchGroup := authGroup.Group("/pig-batches")
 | 
			
		||||
		{
 | 
			
		||||
			pigBatchGroup.POST("", a.pigBatchController.CreatePigBatch)
 | 
			
		||||
			pigBatchGroup.GET("", a.pigBatchController.ListPigBatches)
 | 
			
		||||
			pigBatchGroup.GET("/:id", a.pigBatchController.GetPigBatch)
 | 
			
		||||
			pigBatchGroup.PUT("/:id", a.pigBatchController.UpdatePigBatch)
 | 
			
		||||
			pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch)
 | 
			
		||||
		}
 | 
			
		||||
		a.logger.Info("猪批次相关接口注册成功 (需要认证和审计)")
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,11 @@ package device
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
			
		||||
@@ -34,243 +33,11 @@ func NewController(
 | 
			
		||||
	return &Controller{
 | 
			
		||||
		deviceRepo:         deviceRepo,
 | 
			
		||||
		areaControllerRepo: areaControllerRepo,
 | 
			
		||||
		deviceTemplateRepo: deviceTemplateRepo, // 初始化设备模板仓库
 | 
			
		||||
		deviceTemplateRepo: deviceTemplateRepo,
 | 
			
		||||
		logger:             logger,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Request DTOs ---
 | 
			
		||||
 | 
			
		||||
// CreateDeviceRequest 定义了创建设备时需要传入的参数
 | 
			
		||||
type CreateDeviceRequest struct {
 | 
			
		||||
	Name             string                 `json:"name" binding:"required"`
 | 
			
		||||
	DeviceTemplateID uint                   `json:"device_template_id" binding:"required"`
 | 
			
		||||
	AreaControllerID uint                   `json:"area_controller_id" binding:"required"`
 | 
			
		||||
	Location         string                 `json:"location,omitempty"`
 | 
			
		||||
	Properties       map[string]interface{} `json:"properties,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateDeviceRequest 定义了更新设备时需要传入的参数
 | 
			
		||||
type UpdateDeviceRequest struct {
 | 
			
		||||
	Name             string                 `json:"name" binding:"required"`
 | 
			
		||||
	DeviceTemplateID uint                   `json:"device_template_id" binding:"required"`
 | 
			
		||||
	AreaControllerID uint                   `json:"area_controller_id" binding:"required"`
 | 
			
		||||
	Location         string                 `json:"location,omitempty"`
 | 
			
		||||
	Properties       map[string]interface{} `json:"properties,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数
 | 
			
		||||
type CreateAreaControllerRequest struct {
 | 
			
		||||
	Name       string                 `json:"name" binding:"required"`
 | 
			
		||||
	NetworkID  string                 `json:"network_id" binding:"required"`
 | 
			
		||||
	Location   string                 `json:"location,omitempty"`
 | 
			
		||||
	Properties map[string]interface{} `json:"properties,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数
 | 
			
		||||
type UpdateAreaControllerRequest struct {
 | 
			
		||||
	Name       string                 `json:"name" binding:"required"`
 | 
			
		||||
	NetworkID  string                 `json:"network_id" binding:"required"`
 | 
			
		||||
	Location   string                 `json:"location,omitempty"`
 | 
			
		||||
	Properties map[string]interface{} `json:"properties,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数
 | 
			
		||||
type CreateDeviceTemplateRequest struct {
 | 
			
		||||
	Name         string                   `json:"name" binding:"required"`
 | 
			
		||||
	Manufacturer string                   `json:"manufacturer,omitempty"`
 | 
			
		||||
	Description  string                   `json:"description,omitempty"`
 | 
			
		||||
	Category     models.DeviceCategory    `json:"category" binding:"required"`
 | 
			
		||||
	Commands     map[string]interface{}   `json:"commands" binding:"required"`
 | 
			
		||||
	Values       []models.ValueDescriptor `json:"values,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数
 | 
			
		||||
type UpdateDeviceTemplateRequest struct {
 | 
			
		||||
	Name         string                   `json:"name" binding:"required"`
 | 
			
		||||
	Manufacturer string                   `json:"manufacturer,omitempty"`
 | 
			
		||||
	Description  string                   `json:"description,omitempty"`
 | 
			
		||||
	Category     models.DeviceCategory    `json:"category" binding:"required"`
 | 
			
		||||
	Commands     map[string]interface{}   `json:"commands" binding:"required"`
 | 
			
		||||
	Values       []models.ValueDescriptor `json:"values,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Response DTOs ---
 | 
			
		||||
 | 
			
		||||
// DeviceResponse 定义了返回给客户端的单个设备信息的结构
 | 
			
		||||
type DeviceResponse struct {
 | 
			
		||||
	ID                 uint                   `json:"id"`
 | 
			
		||||
	Name               string                 `json:"name"`
 | 
			
		||||
	DeviceTemplateID   uint                   `json:"device_template_id"`
 | 
			
		||||
	DeviceTemplateName string                 `json:"device_template_name"`
 | 
			
		||||
	AreaControllerID   uint                   `json:"area_controller_id"`
 | 
			
		||||
	AreaControllerName string                 `json:"area_controller_name"`
 | 
			
		||||
	Location           string                 `json:"location"`
 | 
			
		||||
	Properties         map[string]interface{} `json:"properties"`
 | 
			
		||||
	CreatedAt          string                 `json:"created_at"`
 | 
			
		||||
	UpdatedAt          string                 `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构
 | 
			
		||||
type AreaControllerResponse struct {
 | 
			
		||||
	ID         uint                   `json:"id"`
 | 
			
		||||
	Name       string                 `json:"name"`
 | 
			
		||||
	NetworkID  string                 `json:"network_id"`
 | 
			
		||||
	Location   string                 `json:"location"`
 | 
			
		||||
	Status     string                 `json:"status"`
 | 
			
		||||
	Properties map[string]interface{} `json:"properties"`
 | 
			
		||||
	CreatedAt  string                 `json:"created_at"`
 | 
			
		||||
	UpdatedAt  string                 `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构
 | 
			
		||||
type DeviceTemplateResponse struct {
 | 
			
		||||
	ID           uint                     `json:"id"`
 | 
			
		||||
	Name         string                   `json:"name"`
 | 
			
		||||
	Manufacturer string                   `json:"manufacturer"`
 | 
			
		||||
	Description  string                   `json:"description"`
 | 
			
		||||
	Category     models.DeviceCategory    `json:"category"`
 | 
			
		||||
	Commands     map[string]interface{}   `json:"commands"`
 | 
			
		||||
	Values       []models.ValueDescriptor `json:"values"`
 | 
			
		||||
	CreatedAt    string                   `json:"created_at"`
 | 
			
		||||
	UpdatedAt    string                   `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- DTO 转换函数 ---
 | 
			
		||||
 | 
			
		||||
// newDeviceResponse 从数据库模型创建一个新的设备响应 DTO
 | 
			
		||||
func newDeviceResponse(device *models.Device) (*DeviceResponse, error) {
 | 
			
		||||
	if device == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var props map[string]interface{}
 | 
			
		||||
	if len(device.Properties) > 0 && string(device.Properties) != "null" {
 | 
			
		||||
		if err := device.ParseProperties(&props); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("解析设备属性失败 (ID: %d): %w", device.ID, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 确保 DeviceTemplate 和 AreaController 已预加载
 | 
			
		||||
	deviceTemplateName := ""
 | 
			
		||||
	if device.DeviceTemplate.ID != 0 {
 | 
			
		||||
		deviceTemplateName = device.DeviceTemplate.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	areaControllerName := ""
 | 
			
		||||
	if device.AreaController.ID != 0 {
 | 
			
		||||
		areaControllerName = device.AreaController.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &DeviceResponse{
 | 
			
		||||
		ID:                 device.ID,
 | 
			
		||||
		Name:               device.Name,
 | 
			
		||||
		DeviceTemplateID:   device.DeviceTemplateID,
 | 
			
		||||
		DeviceTemplateName: deviceTemplateName,
 | 
			
		||||
		AreaControllerID:   device.AreaControllerID,
 | 
			
		||||
		AreaControllerName: areaControllerName,
 | 
			
		||||
		Location:           device.Location,
 | 
			
		||||
		Properties:         props,
 | 
			
		||||
		CreatedAt:          device.CreatedAt.Format(time.RFC3339),
 | 
			
		||||
		UpdatedAt:          device.UpdatedAt.Format(time.RFC3339),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newListDeviceResponse 从数据库模型切片创建一个新的设备列表响应 DTO 切片
 | 
			
		||||
func newListDeviceResponse(devices []*models.Device) ([]*DeviceResponse, error) {
 | 
			
		||||
	list := make([]*DeviceResponse, 0, len(devices))
 | 
			
		||||
	for _, device := range devices {
 | 
			
		||||
		resp, err := newDeviceResponse(device)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		list = append(list, resp)
 | 
			
		||||
	}
 | 
			
		||||
	return list, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newAreaControllerResponse 从数据库模型创建一个新的区域主控响应 DTO
 | 
			
		||||
func newAreaControllerResponse(ac *models.AreaController) (*AreaControllerResponse, error) {
 | 
			
		||||
	if ac == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var props map[string]interface{}
 | 
			
		||||
	if len(ac.Properties) > 0 && string(ac.Properties) != "null" {
 | 
			
		||||
		if err := json.Unmarshal(ac.Properties, &props); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("解析区域主控属性失败 (ID: %d): %w", ac.ID, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &AreaControllerResponse{
 | 
			
		||||
		ID:         ac.ID,
 | 
			
		||||
		Name:       ac.Name,
 | 
			
		||||
		NetworkID:  ac.NetworkID,
 | 
			
		||||
		Location:   ac.Location,
 | 
			
		||||
		Status:     ac.Status,
 | 
			
		||||
		Properties: props,
 | 
			
		||||
		CreatedAt:  ac.CreatedAt.Format(time.RFC3339),
 | 
			
		||||
		UpdatedAt:  ac.UpdatedAt.Format(time.RFC3339),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newListAreaControllerResponse 从数据库模型切片创建一个新的区域主控列表响应 DTO 切片
 | 
			
		||||
func newListAreaControllerResponse(acs []*models.AreaController) ([]*AreaControllerResponse, error) {
 | 
			
		||||
	list := make([]*AreaControllerResponse, 0, len(acs))
 | 
			
		||||
	for _, ac := range acs {
 | 
			
		||||
		resp, err := newAreaControllerResponse(ac)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		list = append(list, resp)
 | 
			
		||||
	}
 | 
			
		||||
	return list, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newDeviceTemplateResponse 从数据库模型创建一个新的设备模板响应 DTO
 | 
			
		||||
func newDeviceTemplateResponse(dt *models.DeviceTemplate) (*DeviceTemplateResponse, error) {
 | 
			
		||||
	if dt == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var commands map[string]interface{}
 | 
			
		||||
	if err := dt.ParseCommands(&commands); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("解析设备模板命令失败 (ID: %d): %w", dt.ID, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var values []models.ValueDescriptor
 | 
			
		||||
	if dt.Category == models.CategorySensor {
 | 
			
		||||
		if err := dt.ParseValues(&values); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("解析设备模板值描述符失败 (ID: %d): %w", dt.ID, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &DeviceTemplateResponse{
 | 
			
		||||
		ID:           dt.ID,
 | 
			
		||||
		Name:         dt.Name,
 | 
			
		||||
		Manufacturer: dt.Manufacturer,
 | 
			
		||||
		Description:  dt.Description,
 | 
			
		||||
		Category:     dt.Category,
 | 
			
		||||
		Commands:     commands,
 | 
			
		||||
		Values:       values,
 | 
			
		||||
		CreatedAt:    dt.CreatedAt.Format(time.RFC3339),
 | 
			
		||||
		UpdatedAt:    dt.UpdatedAt.Format(time.RFC3339),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newListDeviceTemplateResponse 从数据库模型切片创建一个新的设备模板列表响应 DTO 切片
 | 
			
		||||
func newListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTemplateResponse, error) {
 | 
			
		||||
	list := make([]*DeviceTemplateResponse, 0, len(dts))
 | 
			
		||||
	for _, dt := range dts {
 | 
			
		||||
		resp, err := newDeviceTemplateResponse(dt)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		list = append(list, resp)
 | 
			
		||||
	}
 | 
			
		||||
	return list, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Controller Methods: Devices ---
 | 
			
		||||
 | 
			
		||||
// CreateDevice godoc
 | 
			
		||||
@@ -279,12 +46,12 @@ func newListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTempl
 | 
			
		||||
// @Tags         设备管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        device body CreateDeviceRequest true "设备信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=DeviceResponse}
 | 
			
		||||
// @Param        device body dto.CreateDeviceRequest true "设备信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.DeviceResponse}
 | 
			
		||||
// @Router       /api/v1/devices [post]
 | 
			
		||||
func (c *Controller) CreateDevice(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "创建设备"
 | 
			
		||||
	var req CreateDeviceRequest
 | 
			
		||||
	var req dto.CreateDeviceRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
			
		||||
@@ -325,9 +92,9 @@ func (c *Controller) CreateDevice(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newDeviceResponse(createdDevice)
 | 
			
		||||
	resp, err := dto.NewDeviceResponse(createdDevice)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, createdDevice)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败", actionType, "响应序列化失败", createdDevice)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -342,7 +109,7 @@ func (c *Controller) CreateDevice(ctx *gin.Context) {
 | 
			
		||||
// @Tags         设备管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path string true "设备ID"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=DeviceResponse}
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.DeviceResponse}
 | 
			
		||||
// @Router       /api/v1/devices/{id} [get]
 | 
			
		||||
func (c *Controller) GetDevice(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取设备"
 | 
			
		||||
@@ -371,7 +138,7 @@ func (c *Controller) GetDevice(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newDeviceResponse(device)
 | 
			
		||||
	resp, err := dto.NewDeviceResponse(device)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, device)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误", actionType, "响应序列化失败", device)
 | 
			
		||||
@@ -387,7 +154,7 @@ func (c *Controller) GetDevice(ctx *gin.Context) {
 | 
			
		||||
// @Description  获取系统中所有设备的列表
 | 
			
		||||
// @Tags         设备管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]DeviceResponse}
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]dto.DeviceResponse}
 | 
			
		||||
// @Router       /api/v1/devices [get]
 | 
			
		||||
func (c *Controller) ListDevices(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取设备列表"
 | 
			
		||||
@@ -398,7 +165,7 @@ func (c *Controller) ListDevices(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newListDeviceResponse(devices)
 | 
			
		||||
	resp, err := dto.NewListDeviceResponse(devices)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Devices: %+v", actionType, err, devices)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误", actionType, "响应序列化失败", devices)
 | 
			
		||||
@@ -416,8 +183,8 @@ func (c *Controller) ListDevices(ctx *gin.Context) {
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path string true "设备ID"
 | 
			
		||||
// @Param        device body UpdateDeviceRequest true "要更新的设备信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=DeviceResponse}
 | 
			
		||||
// @Param        device body dto.UpdateDeviceRequest true "要更新的设备信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.DeviceResponse}
 | 
			
		||||
// @Router       /api/v1/devices/{id} [put]
 | 
			
		||||
func (c *Controller) UpdateDevice(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "更新设备"
 | 
			
		||||
@@ -440,7 +207,7 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var req UpdateDeviceRequest
 | 
			
		||||
	var req dto.UpdateDeviceRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
			
		||||
@@ -479,7 +246,7 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newDeviceResponse(updatedDevice)
 | 
			
		||||
	resp, err := dto.NewDeviceResponse(updatedDevice)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, updatedDevice)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败", actionType, "响应序列化失败", updatedDevice)
 | 
			
		||||
@@ -539,12 +306,12 @@ func (c *Controller) DeleteDevice(ctx *gin.Context) {
 | 
			
		||||
// @Tags         区域主控管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        areaController body CreateAreaControllerRequest true "区域主控信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=AreaControllerResponse}
 | 
			
		||||
// @Param        areaController body dto.CreateAreaControllerRequest true "区域主控信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.AreaControllerResponse}
 | 
			
		||||
// @Router       /api/v1/area-controllers [post]
 | 
			
		||||
func (c *Controller) CreateAreaController(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "创建区域主控"
 | 
			
		||||
	var req CreateAreaControllerRequest
 | 
			
		||||
	var req dto.CreateAreaControllerRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
			
		||||
@@ -577,7 +344,7 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newAreaControllerResponse(ac)
 | 
			
		||||
	resp, err := dto.NewAreaControllerResponse(ac)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控创建成功,但响应生成失败", actionType, "响应序列化失败", ac)
 | 
			
		||||
@@ -594,7 +361,7 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) {
 | 
			
		||||
// @Tags         区域主控管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path string true "区域主控ID"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=AreaControllerResponse}
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.AreaControllerResponse}
 | 
			
		||||
// @Router       /api/v1/area-controllers/{id} [get]
 | 
			
		||||
func (c *Controller) GetAreaController(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取区域主控"
 | 
			
		||||
@@ -619,7 +386,7 @@ func (c *Controller) GetAreaController(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newAreaControllerResponse(ac)
 | 
			
		||||
	resp, err := dto.NewAreaControllerResponse(ac)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, ac)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: 内部数据格式错误", actionType, "响应序列化失败", ac)
 | 
			
		||||
@@ -635,7 +402,7 @@ func (c *Controller) GetAreaController(ctx *gin.Context) {
 | 
			
		||||
// @Description  获取系统中所有区域主控的列表
 | 
			
		||||
// @Tags         区域主控管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]AreaControllerResponse}
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]dto.AreaControllerResponse}
 | 
			
		||||
// @Router       /api/v1/area-controllers [get]
 | 
			
		||||
func (c *Controller) ListAreaControllers(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取区域主控列表"
 | 
			
		||||
@@ -646,7 +413,7 @@ func (c *Controller) ListAreaControllers(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newListAreaControllerResponse(acs)
 | 
			
		||||
	resp, err := dto.NewListAreaControllerResponse(acs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, AreaControllers: %+v", actionType, err, acs)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: 内部数据格式错误", actionType, "响应序列化失败", acs)
 | 
			
		||||
@@ -664,8 +431,8 @@ func (c *Controller) ListAreaControllers(ctx *gin.Context) {
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path string true "区域主控ID"
 | 
			
		||||
// @Param        areaController body UpdateAreaControllerRequest true "要更新的区域主控信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=AreaControllerResponse}
 | 
			
		||||
// @Param        areaController body dto.UpdateAreaControllerRequest true "要更新的区域主控信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.AreaControllerResponse}
 | 
			
		||||
// @Router       /api/v1/area-controllers/{id} [put]
 | 
			
		||||
func (c *Controller) UpdateAreaController(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "更新区域主控"
 | 
			
		||||
@@ -690,7 +457,7 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var req UpdateAreaControllerRequest
 | 
			
		||||
	var req dto.UpdateAreaControllerRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
			
		||||
@@ -721,7 +488,7 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newAreaControllerResponse(existingAC)
 | 
			
		||||
	resp, err := dto.NewAreaControllerResponse(existingAC)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, existingAC)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控更新成功,但响应生成失败", actionType, "响应序列化失败", existingAC)
 | 
			
		||||
@@ -781,12 +548,12 @@ func (c *Controller) DeleteAreaController(ctx *gin.Context) {
 | 
			
		||||
// @Tags         设备模板管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        deviceTemplate body CreateDeviceTemplateRequest true "设备模板信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=DeviceTemplateResponse}
 | 
			
		||||
// @Param        deviceTemplate body dto.CreateDeviceTemplateRequest true "设备模板信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.DeviceTemplateResponse}
 | 
			
		||||
// @Router       /api/v1/device-templates [post]
 | 
			
		||||
func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "创建设备模板"
 | 
			
		||||
	var req CreateDeviceTemplateRequest
 | 
			
		||||
	var req dto.CreateDeviceTemplateRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
			
		||||
@@ -828,7 +595,7 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newDeviceTemplateResponse(deviceTemplate)
 | 
			
		||||
	resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate)
 | 
			
		||||
@@ -845,7 +612,7 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
// @Tags         设备模板管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path string true "设备模板ID"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=DeviceTemplateResponse}
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.DeviceTemplateResponse}
 | 
			
		||||
// @Router       /api/v1/device-templates/{id} [get]
 | 
			
		||||
func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取设备模板"
 | 
			
		||||
@@ -870,7 +637,7 @@ func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newDeviceTemplateResponse(deviceTemplate)
 | 
			
		||||
	resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate)
 | 
			
		||||
@@ -886,7 +653,7 @@ func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
// @Description  获取系统中所有设备模板的列表
 | 
			
		||||
// @Tags         设备模板管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]DeviceTemplateResponse}
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]dto.DeviceTemplateResponse}
 | 
			
		||||
// @Router       /api/v1/device-templates [get]
 | 
			
		||||
func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取设备模板列表"
 | 
			
		||||
@@ -897,7 +664,7 @@ func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newListDeviceTemplateResponse(deviceTemplates)
 | 
			
		||||
	resp, err := dto.NewListDeviceTemplateResponse(deviceTemplates)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates)
 | 
			
		||||
@@ -915,8 +682,8 @@ func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path string true "设备模板ID"
 | 
			
		||||
// @Param        deviceTemplate body UpdateDeviceTemplateRequest true "要更新的设备模板信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=DeviceTemplateResponse}
 | 
			
		||||
// @Param        deviceTemplate body dto.UpdateDeviceTemplateRequest true "要更新的设备模板信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.DeviceTemplateResponse}
 | 
			
		||||
// @Router       /api/v1/device-templates/{id} [put]
 | 
			
		||||
func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "更新设备模板"
 | 
			
		||||
@@ -941,7 +708,7 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var req UpdateDeviceTemplateRequest
 | 
			
		||||
	var req dto.UpdateDeviceTemplateRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
			
		||||
@@ -981,7 +748,7 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := newDeviceTemplateResponse(existingDeviceTemplate)
 | 
			
		||||
	resp, err := dto.NewDeviceTemplateResponse(existingDeviceTemplate)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										190
									
								
								internal/app/controller/management/pig_batch_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								internal/app/controller/management/pig_batch_controller.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,190 @@
 | 
			
		||||
package management
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PigBatchController 负责处理猪批次相关的API请求
 | 
			
		||||
type PigBatchController struct {
 | 
			
		||||
	logger  *logs.Logger
 | 
			
		||||
	service service.PigBatchService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewPigBatchController 创建一个新的 PigBatchController 实例
 | 
			
		||||
func NewPigBatchController(logger *logs.Logger, service service.PigBatchService) *PigBatchController {
 | 
			
		||||
	return &PigBatchController{
 | 
			
		||||
		logger:  logger,
 | 
			
		||||
		service: service,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreatePigBatch godoc
 | 
			
		||||
// @Summary      创建猪批次
 | 
			
		||||
// @Description  创建一个新的猪批次
 | 
			
		||||
// @Tags         猪批次管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        body body dto.PigBatchCreateDTO true "猪批次信息"
 | 
			
		||||
// @Success      201 {object} controller.Response{data=dto.PigBatchResponseDTO} "创建成功"
 | 
			
		||||
// @Failure      400 {object} controller.Response "请求参数错误"
 | 
			
		||||
// @Failure      500 {object} controller.Response "内部服务器错误"
 | 
			
		||||
// @Router       /api/v1/pig-batches [post]
 | 
			
		||||
func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) {
 | 
			
		||||
	const action = "创建猪批次"
 | 
			
		||||
	var req dto.PigBatchCreateDTO
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	respDTO, err := c.service.CreatePigBatch(&req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪批次失败", action, "业务逻辑失败", req)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", respDTO, action, "创建成功", respDTO)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPigBatch godoc
 | 
			
		||||
// @Summary      获取单个猪批次
 | 
			
		||||
// @Description  根据ID获取单个猪批次信息
 | 
			
		||||
// @Tags         猪批次管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "猪批次ID"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功"
 | 
			
		||||
// @Failure      400 {object} controller.Response "无效的ID格式"
 | 
			
		||||
// @Failure      404 {object} controller.Response "猪批次不存在"
 | 
			
		||||
// @Failure      500 {object} controller.Response "内部服务器错误"
 | 
			
		||||
// @Router       /api/v1/pig-batches/{id} [get]
 | 
			
		||||
func (c *PigBatchController) GetPigBatch(ctx *gin.Context) {
 | 
			
		||||
	const action = "获取猪批次"
 | 
			
		||||
	id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	respDTO, err := c.service.GetPigBatch(uint(id))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, service.ErrPigBatchNotFound) {
 | 
			
		||||
			controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次失败", action, "业务逻辑失败", id)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTO, action, "获取成功", respDTO)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePigBatch godoc
 | 
			
		||||
// @Summary      更新猪批次
 | 
			
		||||
// @Description  更新一个已存在的猪批次信息
 | 
			
		||||
// @Tags         猪批次管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "猪批次ID"
 | 
			
		||||
// @Param        body body dto.PigBatchUpdateDTO true "猪批次信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PigBatchResponseDTO} "更新成功"
 | 
			
		||||
// @Failure      400 {object} controller.Response "请求参数错误或无效的ID格式"
 | 
			
		||||
// @Failure      404 {object} controller.Response "猪批次不存在"
 | 
			
		||||
// @Failure      500 {object} controller.Response "内部服务器错误"
 | 
			
		||||
// @Router       /api/v1/pig-batches/{id} [put]
 | 
			
		||||
func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) {
 | 
			
		||||
	const action = "更新猪批次"
 | 
			
		||||
	id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var req dto.PigBatchUpdateDTO
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	respDTO, err := c.service.UpdatePigBatch(uint(id), &req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, service.ErrPigBatchNotFound) {
 | 
			
		||||
			controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪批次失败", action, "业务逻辑失败", req)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", respDTO, action, "更新成功", respDTO)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeletePigBatch godoc
 | 
			
		||||
// @Summary      删除猪批次
 | 
			
		||||
// @Description  根据ID删除一个猪批次
 | 
			
		||||
// @Tags         猪批次管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "猪批次ID"
 | 
			
		||||
// @Success      200 {object} controller.Response "删除成功"
 | 
			
		||||
// @Failure      400 {object} controller.Response "无效的ID格式"
 | 
			
		||||
// @Failure      404 {object} controller.Response "猪批次不存在"
 | 
			
		||||
// @Failure      500 {object} controller.Response "内部服务器错误"
 | 
			
		||||
// @Router       /api/v1/pig-batches/{id} [delete]
 | 
			
		||||
func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) {
 | 
			
		||||
	const action = "删除猪批次"
 | 
			
		||||
	id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.service.DeletePigBatch(uint(id)); err != nil {
 | 
			
		||||
		if errors.Is(err, service.ErrPigBatchNotFound) {
 | 
			
		||||
			controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除猪批次失败", action, "业务逻辑失败", id)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListPigBatches godoc
 | 
			
		||||
// @Summary      获取猪批次列表
 | 
			
		||||
// @Description  获取所有猪批次的列表,支持按活跃状态筛选
 | 
			
		||||
// @Tags         猪批次管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        is_active query bool false "是否活跃 (true/false)"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功"
 | 
			
		||||
// @Failure      500 {object} controller.Response "内部服务器错误"
 | 
			
		||||
// @Router       /api/v1/pig-batches [get]
 | 
			
		||||
func (c *PigBatchController) ListPigBatches(ctx *gin.Context) {
 | 
			
		||||
	const action = "获取猪批次列表"
 | 
			
		||||
	var query dto.PigBatchQueryDTO
 | 
			
		||||
	// ShouldBindQuery 会自动处理 URL 查询参数,例如 ?is_active=true
 | 
			
		||||
	if err := ctx.ShouldBindQuery(&query); err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	respDTOs, err := c.service.ListPigBatches(query.IsActive)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次列表失败", action, "业务逻辑失败", nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTOs, action, "获取成功", respDTOs)
 | 
			
		||||
}
 | 
			
		||||
@@ -5,60 +5,13 @@ import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// --- 数据传输对象 (DTOs) ---
 | 
			
		||||
 | 
			
		||||
// PigHouseResponse 定义了猪舍信息的响应结构
 | 
			
		||||
type PigHouseResponse struct {
 | 
			
		||||
	ID          uint   `json:"id"`
 | 
			
		||||
	Name        string `json:"name"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PenResponse 定义了猪栏信息的响应结构
 | 
			
		||||
type PenResponse struct {
 | 
			
		||||
	ID         uint             `json:"id"`
 | 
			
		||||
	PenNumber  string           `json:"pen_number"`
 | 
			
		||||
	HouseID    uint             `json:"house_id"`
 | 
			
		||||
	Capacity   int              `json:"capacity"`
 | 
			
		||||
	Status     models.PenStatus `json:"status"`
 | 
			
		||||
	PigBatchID uint             `json:"pig_batch_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreatePigHouseRequest 定义了创建猪舍的请求结构
 | 
			
		||||
type CreatePigHouseRequest struct {
 | 
			
		||||
	Name        string `json:"name" binding:"required"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePigHouseRequest 定义了更新猪舍的请求结构
 | 
			
		||||
type UpdatePigHouseRequest struct {
 | 
			
		||||
	Name        string `json:"name" binding:"required"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreatePenRequest 定义了创建猪栏的请求结构
 | 
			
		||||
type CreatePenRequest struct {
 | 
			
		||||
	PenNumber string           `json:"pen_number" binding:"required"`
 | 
			
		||||
	HouseID   uint             `json:"house_id" binding:"required"`
 | 
			
		||||
	Capacity  int              `json:"capacity" binding:"required"`
 | 
			
		||||
	Status    models.PenStatus `json:"status" binding:"required"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePenRequest 定义了更新猪栏的请求结构
 | 
			
		||||
type UpdatePenRequest struct {
 | 
			
		||||
	PenNumber string           `json:"pen_number" binding:"required"`
 | 
			
		||||
	HouseID   uint             `json:"house_id" binding:"required"`
 | 
			
		||||
	Capacity  int              `json:"capacity" binding:"required"`
 | 
			
		||||
	Status    models.PenStatus `json:"status" binding:"required"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 控制器定义 ---
 | 
			
		||||
 | 
			
		||||
// PigFarmController 负责处理猪舍和猪栏相关的API请求
 | 
			
		||||
@@ -83,12 +36,12 @@ func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *
 | 
			
		||||
// @Tags         猪场管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        body body CreatePigHouseRequest true "猪舍信息"
 | 
			
		||||
// @Success      201 {object} controller.Response{data=PigHouseResponse} "创建成功"
 | 
			
		||||
// @Param        body body dto.CreatePigHouseRequest true "猪舍信息"
 | 
			
		||||
// @Success      201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功"
 | 
			
		||||
// @Router       /api/v1/pighouses [post]
 | 
			
		||||
func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
 | 
			
		||||
	const action = "创建猪舍"
 | 
			
		||||
	var req CreatePigHouseRequest
 | 
			
		||||
	var req dto.CreatePigHouseRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
			
		||||
		return
 | 
			
		||||
@@ -101,7 +54,7 @@ func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp := PigHouseResponse{
 | 
			
		||||
	resp := dto.PigHouseResponse{
 | 
			
		||||
		ID:          house.ID,
 | 
			
		||||
		Name:        house.Name,
 | 
			
		||||
		Description: house.Description,
 | 
			
		||||
@@ -115,7 +68,7 @@ func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
 | 
			
		||||
// @Tags         猪场管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "猪舍ID"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=PigHouseResponse} "获取成功"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功"
 | 
			
		||||
// @Router       /api/v1/pighouses/{id} [get]
 | 
			
		||||
func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
 | 
			
		||||
	const action = "获取猪舍"
 | 
			
		||||
@@ -136,7 +89,7 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp := PigHouseResponse{
 | 
			
		||||
	resp := dto.PigHouseResponse{
 | 
			
		||||
		ID:          house.ID,
 | 
			
		||||
		Name:        house.Name,
 | 
			
		||||
		Description: house.Description,
 | 
			
		||||
@@ -149,7 +102,7 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
 | 
			
		||||
// @Description  获取所有猪舍的列表
 | 
			
		||||
// @Tags         猪场管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]PigHouseResponse} "获取成功"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功"
 | 
			
		||||
// @Router       /api/v1/pighouses [get]
 | 
			
		||||
func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
 | 
			
		||||
	const action = "获取猪舍列表"
 | 
			
		||||
@@ -160,9 +113,9 @@ func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var resp []PigHouseResponse
 | 
			
		||||
	var resp []dto.PigHouseResponse
 | 
			
		||||
	for _, house := range houses {
 | 
			
		||||
		resp = append(resp, PigHouseResponse{
 | 
			
		||||
		resp = append(resp, dto.PigHouseResponse{
 | 
			
		||||
			ID:          house.ID,
 | 
			
		||||
			Name:        house.Name,
 | 
			
		||||
			Description: house.Description,
 | 
			
		||||
@@ -179,8 +132,8 @@ func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "猪舍ID"
 | 
			
		||||
// @Param        body body UpdatePigHouseRequest true "猪舍信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=PigHouseResponse} "更新成功"
 | 
			
		||||
// @Param        body body dto.UpdatePigHouseRequest true "猪舍信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功"
 | 
			
		||||
// @Router       /api/v1/pighouses/{id} [put]
 | 
			
		||||
func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
 | 
			
		||||
	const action = "更新猪舍"
 | 
			
		||||
@@ -190,7 +143,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var req UpdatePigHouseRequest
 | 
			
		||||
	var req dto.UpdatePigHouseRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
			
		||||
		return
 | 
			
		||||
@@ -207,7 +160,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp := PigHouseResponse{
 | 
			
		||||
	resp := dto.PigHouseResponse{
 | 
			
		||||
		ID:          house.ID,
 | 
			
		||||
		Name:        house.Name,
 | 
			
		||||
		Description: house.Description,
 | 
			
		||||
@@ -252,12 +205,12 @@ func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) {
 | 
			
		||||
// @Tags         猪场管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        body body CreatePenRequest true "猪栏信息"
 | 
			
		||||
// @Success      201 {object} controller.Response{data=PenResponse} "创建成功"
 | 
			
		||||
// @Param        body body dto.CreatePenRequest true "猪栏信息"
 | 
			
		||||
// @Success      201 {object} controller.Response{data=dto.PenResponse} "创建成功"
 | 
			
		||||
// @Router       /api/v1/pens [post]
 | 
			
		||||
func (c *PigFarmController) CreatePen(ctx *gin.Context) {
 | 
			
		||||
	const action = "创建猪栏"
 | 
			
		||||
	var req CreatePenRequest
 | 
			
		||||
	var req dto.CreatePenRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
			
		||||
		return
 | 
			
		||||
@@ -270,7 +223,7 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp := PenResponse{
 | 
			
		||||
	resp := dto.PenResponse{
 | 
			
		||||
		ID:         pen.ID,
 | 
			
		||||
		PenNumber:  pen.PenNumber,
 | 
			
		||||
		HouseID:    pen.HouseID,
 | 
			
		||||
@@ -287,7 +240,7 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) {
 | 
			
		||||
// @Tags         猪场管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "猪栏ID"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=PenResponse} "获取成功"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PenResponse} "获取成功"
 | 
			
		||||
// @Router       /api/v1/pens/{id} [get]
 | 
			
		||||
func (c *PigFarmController) GetPen(ctx *gin.Context) {
 | 
			
		||||
	const action = "获取猪栏"
 | 
			
		||||
@@ -308,7 +261,7 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp := PenResponse{
 | 
			
		||||
	resp := dto.PenResponse{
 | 
			
		||||
		ID:         pen.ID,
 | 
			
		||||
		PenNumber:  pen.PenNumber,
 | 
			
		||||
		HouseID:    pen.HouseID,
 | 
			
		||||
@@ -324,7 +277,7 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) {
 | 
			
		||||
// @Description  获取所有猪栏的列表
 | 
			
		||||
// @Tags         猪场管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]PenResponse} "获取成功"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]dto.PenResponse} "获取成功"
 | 
			
		||||
// @Router       /api/v1/pens [get]
 | 
			
		||||
func (c *PigFarmController) ListPens(ctx *gin.Context) {
 | 
			
		||||
	const action = "获取猪栏列表"
 | 
			
		||||
@@ -335,9 +288,9 @@ func (c *PigFarmController) ListPens(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var resp []PenResponse
 | 
			
		||||
	var resp []dto.PenResponse
 | 
			
		||||
	for _, pen := range pens {
 | 
			
		||||
		resp = append(resp, PenResponse{
 | 
			
		||||
		resp = append(resp, dto.PenResponse{
 | 
			
		||||
			ID:         pen.ID,
 | 
			
		||||
			PenNumber:  pen.PenNumber,
 | 
			
		||||
			HouseID:    pen.HouseID,
 | 
			
		||||
@@ -357,8 +310,8 @@ func (c *PigFarmController) ListPens(ctx *gin.Context) {
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "猪栏ID"
 | 
			
		||||
// @Param        body body UpdatePenRequest true "猪栏信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=PenResponse} "更新成功"
 | 
			
		||||
// @Param        body body dto.UpdatePenRequest true "猪栏信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PenResponse} "更新成功"
 | 
			
		||||
// @Router       /api/v1/pens/{id} [put]
 | 
			
		||||
func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
 | 
			
		||||
	const action = "更新猪栏"
 | 
			
		||||
@@ -368,7 +321,7 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var req UpdatePenRequest
 | 
			
		||||
	var req dto.UpdatePenRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
 | 
			
		||||
		return
 | 
			
		||||
@@ -385,7 +338,7 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp := PenResponse{
 | 
			
		||||
	resp := dto.PenResponse{
 | 
			
		||||
		ID:         pen.ID,
 | 
			
		||||
		PenNumber:  pen.PenNumber,
 | 
			
		||||
		HouseID:    pen.HouseID,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,459 +0,0 @@
 | 
			
		||||
package plan_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"gorm.io/datatypes"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestPlanToResponse(t *testing.T) {
 | 
			
		||||
	t.Run("nil plan", func(t *testing.T) {
 | 
			
		||||
		response := plan.PlanToResponse(nil)
 | 
			
		||||
		assert.Nil(t, response)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("basic plan without associations", func(t *testing.T) {
 | 
			
		||||
		planModel := &models.Plan{
 | 
			
		||||
			Model:          gorm.Model{ID: 1},
 | 
			
		||||
			Name:           "Test Plan",
 | 
			
		||||
			Description:    "A test plan",
 | 
			
		||||
			ExecutionType:  models.PlanExecutionTypeAutomatic,
 | 
			
		||||
			CronExpression: "0 0 * * *",
 | 
			
		||||
			ContentType:    models.PlanContentTypeTasks,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		response := plan.PlanToResponse(planModel)
 | 
			
		||||
		assert.NotNil(t, response)
 | 
			
		||||
		assert.Equal(t, uint(1), response.ID)
 | 
			
		||||
		assert.Equal(t, "Test Plan", response.Name)
 | 
			
		||||
		assert.Equal(t, "A test plan", response.Description)
 | 
			
		||||
		assert.Equal(t, models.PlanExecutionTypeAutomatic, response.ExecutionType)
 | 
			
		||||
		assert.Equal(t, "0 0 * * *", response.CronExpression)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeTasks, response.ContentType)
 | 
			
		||||
		assert.Empty(t, response.SubPlans)
 | 
			
		||||
		assert.Empty(t, response.Tasks)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with sub plans", func(t *testing.T) {
 | 
			
		||||
		childPlan := &models.Plan{
 | 
			
		||||
			Model:       gorm.Model{ID: 2},
 | 
			
		||||
			Name:        "Child Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeTasks,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel := &models.Plan{
 | 
			
		||||
			Model:       gorm.Model{ID: 1},
 | 
			
		||||
			Name:        "Parent Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeSubPlans,
 | 
			
		||||
			SubPlans: []models.SubPlan{
 | 
			
		||||
				{
 | 
			
		||||
					Model:          gorm.Model{ID: 10},
 | 
			
		||||
					ParentPlanID:   1,
 | 
			
		||||
					ChildPlanID:    2,
 | 
			
		||||
					ExecutionOrder: 1,
 | 
			
		||||
					ChildPlan:      childPlan,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		response := plan.PlanToResponse(planModel)
 | 
			
		||||
		assert.NotNil(t, response)
 | 
			
		||||
		assert.Equal(t, uint(1), response.ID)
 | 
			
		||||
		assert.Equal(t, "Parent Plan", response.Name)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeSubPlans, response.ContentType)
 | 
			
		||||
		assert.Len(t, response.SubPlans, 1)
 | 
			
		||||
		assert.Empty(t, response.Tasks)
 | 
			
		||||
 | 
			
		||||
		subPlanResp := response.SubPlans[0]
 | 
			
		||||
		assert.Equal(t, uint(10), subPlanResp.ID)
 | 
			
		||||
		assert.Equal(t, uint(1), subPlanResp.ParentPlanID)
 | 
			
		||||
		assert.Equal(t, uint(2), subPlanResp.ChildPlanID)
 | 
			
		||||
		assert.Equal(t, 1, subPlanResp.ExecutionOrder)
 | 
			
		||||
		assert.NotNil(t, subPlanResp.ChildPlan)
 | 
			
		||||
		assert.Equal(t, "Child Plan", subPlanResp.ChildPlan.Name)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with tasks", func(t *testing.T) {
 | 
			
		||||
		params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`))
 | 
			
		||||
 | 
			
		||||
		planModel := &models.Plan{
 | 
			
		||||
			Model:       gorm.Model{ID: 1},
 | 
			
		||||
			Name:        "Task Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeTasks,
 | 
			
		||||
			Tasks: []models.Task{
 | 
			
		||||
				{
 | 
			
		||||
					Model:          gorm.Model{ID: 10},
 | 
			
		||||
					PlanID:         1,
 | 
			
		||||
					Name:           "Task 1",
 | 
			
		||||
					Description:    "First task",
 | 
			
		||||
					ExecutionOrder: 1,
 | 
			
		||||
					Type:           models.TaskTypeWaiting,
 | 
			
		||||
					Parameters:     params,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		response := plan.PlanToResponse(planModel)
 | 
			
		||||
		assert.NotNil(t, response)
 | 
			
		||||
		assert.Equal(t, uint(1), response.ID)
 | 
			
		||||
		assert.Equal(t, "Task Plan", response.Name)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeTasks, response.ContentType)
 | 
			
		||||
		assert.Len(t, response.Tasks, 1)
 | 
			
		||||
		assert.Empty(t, response.SubPlans)
 | 
			
		||||
 | 
			
		||||
		taskResp := response.Tasks[0]
 | 
			
		||||
		assert.Equal(t, uint(10), taskResp.ID)
 | 
			
		||||
		assert.Equal(t, uint(1), taskResp.PlanID)
 | 
			
		||||
		assert.Equal(t, "Task 1", taskResp.Name)
 | 
			
		||||
		assert.Equal(t, "First task", taskResp.Description)
 | 
			
		||||
		assert.Equal(t, 1, taskResp.ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, models.TaskTypeWaiting, taskResp.Type)
 | 
			
		||||
		assert.Equal(t, controller.Properties(params), taskResp.Parameters)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPlanFromCreateRequest(t *testing.T) {
 | 
			
		||||
	t.Run("nil request", func(t *testing.T) {
 | 
			
		||||
		planModel, err := plan.PlanFromCreateRequest(nil)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Nil(t, planModel)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("basic plan without associations", func(t *testing.T) {
 | 
			
		||||
		req := &plan.CreatePlanRequest{
 | 
			
		||||
			Name:           "Test Plan",
 | 
			
		||||
			Description:    "A test plan",
 | 
			
		||||
			ExecutionType:  models.PlanExecutionTypeAutomatic,
 | 
			
		||||
			CronExpression: "0 0 * * *",
 | 
			
		||||
			ContentType:    models.PlanContentTypeTasks,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, planModel)
 | 
			
		||||
		assert.Equal(t, "Test Plan", planModel.Name)
 | 
			
		||||
		assert.Equal(t, "A test plan", planModel.Description)
 | 
			
		||||
		assert.Equal(t, models.PlanExecutionTypeAutomatic, planModel.ExecutionType)
 | 
			
		||||
		assert.Equal(t, "0 0 * * *", planModel.CronExpression)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
			
		||||
		assert.Empty(t, planModel.SubPlans)
 | 
			
		||||
		assert.Empty(t, planModel.Tasks)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with sub plan IDs", func(t *testing.T) {
 | 
			
		||||
		req := &plan.CreatePlanRequest{
 | 
			
		||||
			Name:        "Parent Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeSubPlans,
 | 
			
		||||
			SubPlanIDs:  []uint{2, 3},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, planModel)
 | 
			
		||||
		assert.Equal(t, "Parent Plan", planModel.Name)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType)
 | 
			
		||||
		assert.Len(t, planModel.SubPlans, 2)
 | 
			
		||||
		assert.Empty(t, planModel.Tasks)
 | 
			
		||||
 | 
			
		||||
		assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID)
 | 
			
		||||
		assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID)
 | 
			
		||||
		assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with tasks", func(t *testing.T) {
 | 
			
		||||
		params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
			
		||||
 | 
			
		||||
		req := &plan.CreatePlanRequest{
 | 
			
		||||
			Name:        "Task Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeTasks,
 | 
			
		||||
			Tasks: []plan.TaskRequest{
 | 
			
		||||
				{
 | 
			
		||||
					Name:           "Task 1",
 | 
			
		||||
					Description:    "First task",
 | 
			
		||||
					ExecutionOrder: 1,
 | 
			
		||||
					Type:           models.TaskTypeWaiting,
 | 
			
		||||
					Parameters:     params,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, planModel)
 | 
			
		||||
		assert.Equal(t, "Task Plan", planModel.Name)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
			
		||||
		assert.Len(t, planModel.Tasks, 1)
 | 
			
		||||
		assert.Empty(t, planModel.SubPlans)
 | 
			
		||||
 | 
			
		||||
		task := planModel.Tasks[0]
 | 
			
		||||
		assert.Equal(t, "Task 1", task.Name)
 | 
			
		||||
		assert.Equal(t, "First task", task.Description)
 | 
			
		||||
		assert.Equal(t, 1, task.ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, models.TaskTypeWaiting, task.Type)
 | 
			
		||||
		assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with tasks with gapped execution order", func(t *testing.T) {
 | 
			
		||||
		req := &plan.CreatePlanRequest{
 | 
			
		||||
			Name:        "Task Plan with Gaps",
 | 
			
		||||
			ContentType: models.PlanContentTypeTasks,
 | 
			
		||||
			Tasks: []plan.TaskRequest{
 | 
			
		||||
				{Name: "Task 3", ExecutionOrder: 5},
 | 
			
		||||
				{Name: "Task 1", ExecutionOrder: 2},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, planModel)
 | 
			
		||||
		assert.Len(t, planModel.Tasks, 2)
 | 
			
		||||
 | 
			
		||||
		// After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered.
 | 
			
		||||
		assert.Equal(t, "Task 1", planModel.Tasks[0].Name)
 | 
			
		||||
		assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, "Task 3", planModel.Tasks[1].Name)
 | 
			
		||||
		assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with duplicate task execution order", func(t *testing.T) {
 | 
			
		||||
		req := &plan.CreatePlanRequest{
 | 
			
		||||
			Name:        "Invalid Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeTasks,
 | 
			
		||||
			Tasks: []plan.TaskRequest{
 | 
			
		||||
				{Name: "Task 1", ExecutionOrder: 1},
 | 
			
		||||
				{Name: "Task 2", ExecutionOrder: 1}, // Duplicate order
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromCreateRequest(req)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
		assert.Contains(t, err.Error(), "任务执行顺序重复")
 | 
			
		||||
		assert.Nil(t, planModel)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPlanFromUpdateRequest(t *testing.T) {
 | 
			
		||||
	t.Run("nil request", func(t *testing.T) {
 | 
			
		||||
		planModel, err := plan.PlanFromUpdateRequest(nil)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Nil(t, planModel)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("basic plan without associations", func(t *testing.T) {
 | 
			
		||||
		req := &plan.UpdatePlanRequest{
 | 
			
		||||
			Name:           "Updated Plan",
 | 
			
		||||
			Description:    "An updated plan",
 | 
			
		||||
			ExecutionType:  models.PlanExecutionTypeManual,
 | 
			
		||||
			CronExpression: "0 30 * * *",
 | 
			
		||||
			ContentType:    models.PlanContentTypeTasks,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, planModel)
 | 
			
		||||
		assert.Equal(t, "Updated Plan", planModel.Name)
 | 
			
		||||
		assert.Equal(t, "An updated plan", planModel.Description)
 | 
			
		||||
		assert.Equal(t, models.PlanExecutionTypeManual, planModel.ExecutionType)
 | 
			
		||||
		assert.Equal(t, "0 30 * * *", planModel.CronExpression)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
			
		||||
		assert.Empty(t, planModel.SubPlans)
 | 
			
		||||
		assert.Empty(t, planModel.Tasks)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with sub plan IDs", func(t *testing.T) {
 | 
			
		||||
		req := &plan.UpdatePlanRequest{
 | 
			
		||||
			Name:        "Updated Parent Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeSubPlans,
 | 
			
		||||
			SubPlanIDs:  []uint{2, 3},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, planModel)
 | 
			
		||||
 | 
			
		||||
		assert.Equal(t, "Updated Parent Plan", planModel.Name)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType)
 | 
			
		||||
		assert.Len(t, planModel.SubPlans, 2)
 | 
			
		||||
		assert.Empty(t, planModel.Tasks)
 | 
			
		||||
 | 
			
		||||
		assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID)
 | 
			
		||||
		assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID)
 | 
			
		||||
		assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with tasks", func(t *testing.T) {
 | 
			
		||||
		params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
			
		||||
 | 
			
		||||
		req := &plan.UpdatePlanRequest{
 | 
			
		||||
			Name:        "Updated Task Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeTasks,
 | 
			
		||||
			Tasks: []plan.TaskRequest{
 | 
			
		||||
				{
 | 
			
		||||
					Name:           "Task 1",
 | 
			
		||||
					Description:    "First task",
 | 
			
		||||
					ExecutionOrder: 1,
 | 
			
		||||
					Type:           models.TaskTypeWaiting,
 | 
			
		||||
					Parameters:     params,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, planModel)
 | 
			
		||||
		assert.Equal(t, "Updated Task Plan", planModel.Name)
 | 
			
		||||
		assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType)
 | 
			
		||||
		assert.Len(t, planModel.Tasks, 1)
 | 
			
		||||
		assert.Empty(t, planModel.SubPlans)
 | 
			
		||||
 | 
			
		||||
		task := planModel.Tasks[0]
 | 
			
		||||
		assert.Equal(t, "Task 1", task.Name)
 | 
			
		||||
		assert.Equal(t, 1, task.ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with duplicate task execution order", func(t *testing.T) {
 | 
			
		||||
		req := &plan.UpdatePlanRequest{
 | 
			
		||||
			Name:        "Invalid Updated Plan",
 | 
			
		||||
			ContentType: models.PlanContentTypeTasks,
 | 
			
		||||
			Tasks: []plan.TaskRequest{
 | 
			
		||||
				{Name: "Task 1", ExecutionOrder: 1},
 | 
			
		||||
				{Name: "Task 2", ExecutionOrder: 1}, // Duplicate order
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
		assert.Contains(t, err.Error(), "任务执行顺序重复")
 | 
			
		||||
		assert.Nil(t, planModel)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("plan with tasks with gapped execution order", func(t *testing.T) {
 | 
			
		||||
		req := &plan.UpdatePlanRequest{
 | 
			
		||||
			Name:        "Updated Task Plan with Gaps",
 | 
			
		||||
			ContentType: models.PlanContentTypeTasks,
 | 
			
		||||
			Tasks: []plan.TaskRequest{
 | 
			
		||||
				{Name: "Task 3", ExecutionOrder: 5},
 | 
			
		||||
				{Name: "Task 1", ExecutionOrder: 2},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		planModel, err := plan.PlanFromUpdateRequest(req)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, planModel)
 | 
			
		||||
		assert.Len(t, planModel.Tasks, 2)
 | 
			
		||||
 | 
			
		||||
		// After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered.
 | 
			
		||||
		assert.Equal(t, "Task 1", planModel.Tasks[0].Name)
 | 
			
		||||
		assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, "Task 3", planModel.Tasks[1].Name)
 | 
			
		||||
		assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSubPlanToResponse(t *testing.T) {
 | 
			
		||||
	t.Run("nil sub plan", func(t *testing.T) {
 | 
			
		||||
		response := plan.SubPlanToResponse(nil)
 | 
			
		||||
		assert.Equal(t, plan.SubPlanResponse{}, response)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("sub plan without child plan", func(t *testing.T) {
 | 
			
		||||
		subPlan := &models.SubPlan{
 | 
			
		||||
			Model:          gorm.Model{ID: 10},
 | 
			
		||||
			ParentPlanID:   1,
 | 
			
		||||
			ChildPlanID:    2,
 | 
			
		||||
			ExecutionOrder: 1,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		response := plan.SubPlanToResponse(subPlan)
 | 
			
		||||
		assert.Equal(t, uint(10), response.ID)
 | 
			
		||||
		assert.Equal(t, uint(1), response.ParentPlanID)
 | 
			
		||||
		assert.Equal(t, uint(2), response.ChildPlanID)
 | 
			
		||||
		assert.Equal(t, 1, response.ExecutionOrder)
 | 
			
		||||
		assert.Nil(t, response.ChildPlan)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("sub plan with child plan", func(t *testing.T) {
 | 
			
		||||
		childPlan := &models.Plan{
 | 
			
		||||
			Model: gorm.Model{ID: 2},
 | 
			
		||||
			Name:  "Child Plan",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		subPlan := &models.SubPlan{
 | 
			
		||||
			Model:          gorm.Model{ID: 10},
 | 
			
		||||
			ParentPlanID:   1,
 | 
			
		||||
			ChildPlanID:    2,
 | 
			
		||||
			ExecutionOrder: 1,
 | 
			
		||||
			ChildPlan:      childPlan,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		response := plan.SubPlanToResponse(subPlan)
 | 
			
		||||
		assert.Equal(t, uint(10), response.ID)
 | 
			
		||||
		assert.Equal(t, uint(1), response.ParentPlanID)
 | 
			
		||||
		assert.Equal(t, uint(2), response.ChildPlanID)
 | 
			
		||||
		assert.Equal(t, 1, response.ExecutionOrder)
 | 
			
		||||
		assert.NotNil(t, response.ChildPlan)
 | 
			
		||||
		assert.Equal(t, "Child Plan", response.ChildPlan.Name)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestTaskToResponse(t *testing.T) {
 | 
			
		||||
	t.Run("nil task", func(t *testing.T) {
 | 
			
		||||
		response := plan.TaskToResponse(nil)
 | 
			
		||||
		assert.Equal(t, plan.TaskResponse{}, response)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("task with parameters", func(t *testing.T) {
 | 
			
		||||
		params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`))
 | 
			
		||||
		task := &models.Task{
 | 
			
		||||
			Model:          gorm.Model{ID: 10},
 | 
			
		||||
			PlanID:         1,
 | 
			
		||||
			Name:           "Test Task",
 | 
			
		||||
			Description:    "A test task",
 | 
			
		||||
			ExecutionOrder: 1,
 | 
			
		||||
			Type:           models.TaskTypeWaiting,
 | 
			
		||||
			Parameters:     params,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		response := plan.TaskToResponse(task)
 | 
			
		||||
		assert.Equal(t, uint(10), response.ID)
 | 
			
		||||
		assert.Equal(t, uint(1), response.PlanID)
 | 
			
		||||
		assert.Equal(t, "Test Task", response.Name)
 | 
			
		||||
		assert.Equal(t, "A test task", response.Description)
 | 
			
		||||
		assert.Equal(t, 1, response.ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, models.TaskTypeWaiting, response.Type)
 | 
			
		||||
		assert.Equal(t, controller.Properties(params), response.Parameters)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestTaskFromRequest(t *testing.T) {
 | 
			
		||||
	t.Run("nil request", func(t *testing.T) {
 | 
			
		||||
		task := plan.TaskFromRequest(nil)
 | 
			
		||||
		assert.Equal(t, models.Task{}, task)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("task with parameters", func(t *testing.T) {
 | 
			
		||||
		params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`))
 | 
			
		||||
		req := &plan.TaskRequest{
 | 
			
		||||
			Name:           "Test Task",
 | 
			
		||||
			Description:    "A test task",
 | 
			
		||||
			ExecutionOrder: 1,
 | 
			
		||||
			Type:           models.TaskTypeWaiting,
 | 
			
		||||
			Parameters:     params,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		task := plan.TaskFromRequest(req)
 | 
			
		||||
		assert.Equal(t, "Test Task", task.Name)
 | 
			
		||||
		assert.Equal(t, "A test task", task.Description)
 | 
			
		||||
		assert.Equal(t, 1, task.ExecutionOrder)
 | 
			
		||||
		assert.Equal(t, models.TaskTypeWaiting, task.Type)
 | 
			
		||||
		assert.Equal(t, datatypes.JSON(params), task.Parameters)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
@@ -13,80 +14,6 @@ import (
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// --- 请求和响应 DTO 定义 ---
 | 
			
		||||
 | 
			
		||||
// CreatePlanRequest 定义创建计划请求的结构体
 | 
			
		||||
type CreatePlanRequest struct {
 | 
			
		||||
	Name           string                   `json:"name" binding:"required" example:"猪舍温度控制计划"`
 | 
			
		||||
	Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
			
		||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
 | 
			
		||||
	ExecuteNum     uint                     `json:"execute_num,omitempty" example:"10"`
 | 
			
		||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
			
		||||
	SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
			
		||||
	Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PlanResponse 定义计划详情响应的结构体
 | 
			
		||||
type PlanResponse struct {
 | 
			
		||||
	ID             uint                     `json:"id" example:"1"`
 | 
			
		||||
	Name           string                   `json:"name" example:"猪舍温度控制计划"`
 | 
			
		||||
	Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
			
		||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" example:"自动"`
 | 
			
		||||
	Status         models.PlanStatus        `json:"status" example:"已启用"`
 | 
			
		||||
	ExecuteNum     uint                     `json:"execute_num" example:"10"`
 | 
			
		||||
	ExecuteCount   uint                     `json:"execute_count" example:"0"`
 | 
			
		||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
			
		||||
	ContentType    models.PlanContentType   `json:"content_type" example:"任务"`
 | 
			
		||||
	SubPlans       []SubPlanResponse        `json:"sub_plans,omitempty"`
 | 
			
		||||
	Tasks          []TaskResponse           `json:"tasks,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListPlansResponse 定义获取计划列表响应的结构体
 | 
			
		||||
type ListPlansResponse struct {
 | 
			
		||||
	Plans []PlanResponse `json:"plans"`
 | 
			
		||||
	Total int            `json:"total" example:"100"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePlanRequest 定义更新计划请求的结构体
 | 
			
		||||
type UpdatePlanRequest struct {
 | 
			
		||||
	Name           string                   `json:"name" example:"猪舍温度控制计划V2"`
 | 
			
		||||
	Description    string                   `json:"description" example:"更新后的描述"`
 | 
			
		||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" example:"自动"`
 | 
			
		||||
	ExecuteNum     uint                     `json:"execute_num,omitempty" example:"10"`
 | 
			
		||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
			
		||||
	SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
			
		||||
	Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubPlanResponse 定义子计划响应结构体
 | 
			
		||||
type SubPlanResponse struct {
 | 
			
		||||
	ID             uint          `json:"id" example:"1"`
 | 
			
		||||
	ParentPlanID   uint          `json:"parent_plan_id" example:"1"`
 | 
			
		||||
	ChildPlanID    uint          `json:"child_plan_id" example:"2"`
 | 
			
		||||
	ExecutionOrder int           `json:"execution_order" example:"1"`
 | 
			
		||||
	ChildPlan      *PlanResponse `json:"child_plan,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TaskRequest 定义任务请求结构体
 | 
			
		||||
type TaskRequest struct {
 | 
			
		||||
	Name           string                 `json:"name" example:"打开风扇"`
 | 
			
		||||
	Description    string                 `json:"description" example:"打开1号风扇"`
 | 
			
		||||
	ExecutionOrder int                    `json:"execution_order" example:"1"`
 | 
			
		||||
	Type           models.TaskType        `json:"type" example:"等待"`
 | 
			
		||||
	Parameters     map[string]interface{} `json:"parameters,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TaskResponse 定义任务响应结构体
 | 
			
		||||
type TaskResponse struct {
 | 
			
		||||
	ID             int                    `json:"id" example:"1"`
 | 
			
		||||
	PlanID         uint                   `json:"plan_id" example:"1"`
 | 
			
		||||
	Name           string                 `json:"name" example:"打开风扇"`
 | 
			
		||||
	Description    string                 `json:"description" example:"打开1号风扇"`
 | 
			
		||||
	ExecutionOrder int                    `json:"execution_order" example:"1"`
 | 
			
		||||
	Type           models.TaskType        `json:"type" example:"等待"`
 | 
			
		||||
	Parameters     map[string]interface{} `json:"parameters,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Controller 定义 ---
 | 
			
		||||
 | 
			
		||||
// Controller 定义了计划相关的控制器
 | 
			
		||||
@@ -113,11 +40,11 @@ func NewController(logger *logs.Logger, planRepo repository.PlanRepository, anal
 | 
			
		||||
// @Tags         计划管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        plan body CreatePlanRequest true "计划信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=plan.PlanResponse} "业务码为201代表创建成功"
 | 
			
		||||
// @Param        plan body dto.CreatePlanRequest true "计划信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PlanResponse} "业务码为201代表创建成功"
 | 
			
		||||
// @Router       /api/v1/plans [post]
 | 
			
		||||
func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
			
		||||
	var req CreatePlanRequest
 | 
			
		||||
	var req dto.CreatePlanRequest
 | 
			
		||||
	const actionType = "创建计划"
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
			
		||||
@@ -126,7 +53,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 使用已有的转换函数,它已经包含了验证和重排逻辑
 | 
			
		||||
	planToCreate, err := PlanFromCreateRequest(&req)
 | 
			
		||||
	planToCreate, err := dto.NewPlanFromCreateRequest(&req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
 | 
			
		||||
@@ -155,7 +82,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 使用已有的转换函数将创建后的模型转换为响应对象
 | 
			
		||||
	resp, err := PlanToResponse(planToCreate)
 | 
			
		||||
	resp, err := dto.NewPlanToResponse(planToCreate)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate)
 | 
			
		||||
@@ -173,7 +100,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
 | 
			
		||||
// @Tags         计划管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "计划ID"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表成功获取"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表成功获取"
 | 
			
		||||
// @Router       /api/v1/plans/{id} [get]
 | 
			
		||||
func (c *Controller) GetPlan(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取计划详情"
 | 
			
		||||
@@ -202,7 +129,7 @@ func (c *Controller) GetPlan(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 3. 将模型转换为响应 DTO
 | 
			
		||||
	resp, err := PlanToResponse(plan)
 | 
			
		||||
	resp, err := dto.NewPlanToResponse(plan)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan)
 | 
			
		||||
@@ -219,7 +146,7 @@ func (c *Controller) GetPlan(ctx *gin.Context) {
 | 
			
		||||
// @Description  获取所有计划的列表
 | 
			
		||||
// @Tags         计划管理
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Success      200 {object} controller.Response{data=plan.ListPlansResponse} "业务码为200代表成功获取列表"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=[]dto.PlanResponse} "业务码为200代表成功获取列表"
 | 
			
		||||
// @Router       /api/v1/plans [get]
 | 
			
		||||
func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取计划列表"
 | 
			
		||||
@@ -232,9 +159,9 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 2. 将模型转换为响应 DTO
 | 
			
		||||
	planResponses := make([]PlanResponse, 0, len(plans))
 | 
			
		||||
	planResponses := make([]dto.PlanResponse, 0, len(plans))
 | 
			
		||||
	for _, p := range plans {
 | 
			
		||||
		resp, err := PlanToResponse(&p)
 | 
			
		||||
		resp, err := dto.NewPlanToResponse(&p)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p)
 | 
			
		||||
			controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p)
 | 
			
		||||
@@ -244,7 +171,7 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 3. 构造并发送成功响应
 | 
			
		||||
	resp := ListPlansResponse{
 | 
			
		||||
	resp := dto.ListPlansResponse{
 | 
			
		||||
		Plans: planResponses,
 | 
			
		||||
		Total: len(planResponses),
 | 
			
		||||
	}
 | 
			
		||||
@@ -259,8 +186,8 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        id path int true "计划ID"
 | 
			
		||||
// @Param        plan body UpdatePlanRequest true "更新后的计划信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表更新成功"
 | 
			
		||||
// @Param        plan body dto.UpdatePlanRequest true "更新后的计划信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表更新成功"
 | 
			
		||||
// @Router       /api/v1/plans/{id} [put]
 | 
			
		||||
func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "更新计划"
 | 
			
		||||
@@ -274,7 +201,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 2. 绑定请求体
 | 
			
		||||
	var req UpdatePlanRequest
 | 
			
		||||
	var req dto.UpdatePlanRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
 | 
			
		||||
@@ -282,7 +209,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 3. 将请求转换为模型(转换函数带校验)
 | 
			
		||||
	planToUpdate, err := PlanFromUpdateRequest(&req)
 | 
			
		||||
	planToUpdate, err := dto.NewPlanFromUpdateRequest(&req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
 | 
			
		||||
@@ -306,8 +233,8 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
			
		||||
			controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		c.logger.Errorf("%s: 获取计划详情失败: %v, ID: %d", actionType, err, id)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误", actionType, "数据库查询失败", id)
 | 
			
		||||
		c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -337,7 +264,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 7. 将模型转换为响应 DTO
 | 
			
		||||
	resp, err := PlanToResponse(updatedPlan)
 | 
			
		||||
	resp, err := dto.NewPlanToResponse(updatedPlan)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
 | 
			
		||||
		controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
@@ -31,50 +32,6 @@ func NewController(userRepo repository.UserRepository, auditRepo repository.User
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- DTOs ---
 | 
			
		||||
 | 
			
		||||
// CreateUserRequest 定义创建用户请求的结构体
 | 
			
		||||
type CreateUserRequest struct {
 | 
			
		||||
	Username string `json:"username" binding:"required" example:"newuser"`
 | 
			
		||||
	Password string `json:"password" binding:"required" example:"password123"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoginRequest 定义登录请求的结构体
 | 
			
		||||
type LoginRequest struct {
 | 
			
		||||
	// Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号
 | 
			
		||||
	Identifier string `json:"identifier" binding:"required" example:"testuser"`
 | 
			
		||||
	Password   string `json:"password" binding:"required" example:"password123"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateUserResponse 定义创建用户成功响应的结构体
 | 
			
		||||
type CreateUserResponse struct {
 | 
			
		||||
	Username string `json:"username" example:"newuser"`
 | 
			
		||||
	ID       uint   `json:"id" example:"1"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoginResponse 定义登录成功响应的结构体
 | 
			
		||||
type LoginResponse struct {
 | 
			
		||||
	Username string `json:"username" example:"testuser"`
 | 
			
		||||
	ID       uint   `json:"id" example:"1"`
 | 
			
		||||
	Token    string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HistoryResponse 定义单条操作历史的响应结构体
 | 
			
		||||
type HistoryResponse struct {
 | 
			
		||||
	UserID         uint        `json:"user_id" example:"101"`
 | 
			
		||||
	Username       string      `json:"username" example:"testuser"`
 | 
			
		||||
	ActionType     string      `json:"action_type" example:"更新设备"`
 | 
			
		||||
	Description    string      `json:"description" example:"设备更新成功"`
 | 
			
		||||
	TargetResource interface{} `json:"target_resource"`
 | 
			
		||||
	Time           string      `json:"time"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListHistoryResponse 定义操作历史列表的响应结构体
 | 
			
		||||
type ListHistoryResponse struct {
 | 
			
		||||
	History []HistoryResponse `json:"history"`
 | 
			
		||||
	Total   int64             `json:"total" example:"100"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Controller Methods ---
 | 
			
		||||
 | 
			
		||||
// CreateUser godoc
 | 
			
		||||
@@ -83,11 +40,11 @@ type ListHistoryResponse struct {
 | 
			
		||||
// @Tags         用户管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        user body CreateUserRequest true "用户信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=user.CreateUserResponse} "业务码为201代表创建成功"
 | 
			
		||||
// @Param        user body dto.CreateUserRequest true "用户信息"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.CreateUserResponse} "业务码为201代表创建成功"
 | 
			
		||||
// @Router       /api/v1/users [post]
 | 
			
		||||
func (c *Controller) CreateUser(ctx *gin.Context) {
 | 
			
		||||
	var req CreateUserRequest
 | 
			
		||||
	var req dto.CreateUserRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("创建用户: 参数绑定失败: %v", err)
 | 
			
		||||
		controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
 | 
			
		||||
@@ -114,7 +71,7 @@ func (c *Controller) CreateUser(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", CreateUserResponse{
 | 
			
		||||
	controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", dto.CreateUserResponse{
 | 
			
		||||
		Username: user.Username,
 | 
			
		||||
		ID:       user.ID,
 | 
			
		||||
	})
 | 
			
		||||
@@ -126,11 +83,11 @@ func (c *Controller) CreateUser(ctx *gin.Context) {
 | 
			
		||||
// @Tags         用户管理
 | 
			
		||||
// @Accept       json
 | 
			
		||||
// @Produce      json
 | 
			
		||||
// @Param        credentials body LoginRequest true "登录凭证"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=user.LoginResponse} "业务码为200代表登录成功"
 | 
			
		||||
// @Param        credentials body dto.LoginRequest true "登录凭证"
 | 
			
		||||
// @Success      200 {object} controller.Response{data=dto.LoginResponse} "业务码为200代表登录成功"
 | 
			
		||||
// @Router       /api/v1/users/login [post]
 | 
			
		||||
func (c *Controller) Login(ctx *gin.Context) {
 | 
			
		||||
	var req LoginRequest
 | 
			
		||||
	var req dto.LoginRequest
 | 
			
		||||
	if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
			
		||||
		c.logger.Errorf("登录: 参数绑定失败: %v", err)
 | 
			
		||||
		controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
 | 
			
		||||
@@ -162,7 +119,7 @@ func (c *Controller) Login(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", LoginResponse{
 | 
			
		||||
	controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", dto.LoginResponse{
 | 
			
		||||
		Username: user.Username,
 | 
			
		||||
		ID:       user.ID,
 | 
			
		||||
		Token:    tokenString,
 | 
			
		||||
@@ -178,7 +135,7 @@ func (c *Controller) Login(ctx *gin.Context) {
 | 
			
		||||
// @Param        page query     int    false "页码" default(1)
 | 
			
		||||
// @Param        page_size query int    false "每页大小" default(10)
 | 
			
		||||
// @Param        action_type query string false "按操作类型过滤"
 | 
			
		||||
// @Success      200  {object}  controller.Response{data=user.ListHistoryResponse} "业务码为200代表成功获取"
 | 
			
		||||
// @Success      200  {object}  controller.Response{data=dto.ListHistoryResponse} "业务码为200代表成功获取"
 | 
			
		||||
// @Router       /api/v1/users/{id}/history [get]
 | 
			
		||||
func (c *Controller) ListUserHistory(ctx *gin.Context) {
 | 
			
		||||
	const actionType = "获取用户操作历史"
 | 
			
		||||
@@ -221,9 +178,9 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 4. 将数据库模型转换为响应 DTO
 | 
			
		||||
	historyResponses := make([]HistoryResponse, 0, len(logs))
 | 
			
		||||
	historyResponses := make([]dto.HistoryResponse, 0, len(logs))
 | 
			
		||||
	for _, log := range logs {
 | 
			
		||||
		historyResponses = append(historyResponses, HistoryResponse{
 | 
			
		||||
		historyResponses = append(historyResponses, dto.HistoryResponse{
 | 
			
		||||
			UserID:         log.UserID,
 | 
			
		||||
			Username:       log.Username,
 | 
			
		||||
			ActionType:     log.ActionType,
 | 
			
		||||
@@ -234,7 +191,7 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 5. 发送成功响应
 | 
			
		||||
	resp := ListHistoryResponse{
 | 
			
		||||
	resp := dto.ListHistoryResponse{
 | 
			
		||||
		History: historyResponses,
 | 
			
		||||
		Total:   total,
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										142
									
								
								internal/app/dto/device_converter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								internal/app/dto/device_converter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// NewDeviceResponse 从数据库模型创建一个新的设备响应 DTO
 | 
			
		||||
func NewDeviceResponse(device *models.Device) (*DeviceResponse, error) {
 | 
			
		||||
	if device == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var props map[string]interface{}
 | 
			
		||||
	if len(device.Properties) > 0 && string(device.Properties) != "null" {
 | 
			
		||||
		if err := device.ParseProperties(&props); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("解析设备属性失败 (ID: %d): %w", device.ID, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 确保 DeviceTemplate 和 AreaController 已预加载
 | 
			
		||||
	deviceTemplateName := ""
 | 
			
		||||
	if device.DeviceTemplate.ID != 0 {
 | 
			
		||||
		deviceTemplateName = device.DeviceTemplate.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	areaControllerName := ""
 | 
			
		||||
	if device.AreaController.ID != 0 {
 | 
			
		||||
		areaControllerName = device.AreaController.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &DeviceResponse{
 | 
			
		||||
		ID:                 device.ID,
 | 
			
		||||
		Name:               device.Name,
 | 
			
		||||
		DeviceTemplateID:   device.DeviceTemplateID,
 | 
			
		||||
		DeviceTemplateName: deviceTemplateName,
 | 
			
		||||
		AreaControllerID:   device.AreaControllerID,
 | 
			
		||||
		AreaControllerName: areaControllerName,
 | 
			
		||||
		Location:           device.Location,
 | 
			
		||||
		Properties:         props,
 | 
			
		||||
		CreatedAt:          device.CreatedAt.Format(time.RFC3339),
 | 
			
		||||
		UpdatedAt:          device.UpdatedAt.Format(time.RFC3339),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewListDeviceResponse 从数据库模型切片创建一个新的设备列表响应 DTO 切片
 | 
			
		||||
func NewListDeviceResponse(devices []*models.Device) ([]*DeviceResponse, error) {
 | 
			
		||||
	list := make([]*DeviceResponse, 0, len(devices))
 | 
			
		||||
	for _, device := range devices {
 | 
			
		||||
		resp, err := NewDeviceResponse(device)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		list = append(list, resp)
 | 
			
		||||
	}
 | 
			
		||||
	return list, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAreaControllerResponse 从数据库模型创建一个新的区域主控响应 DTO
 | 
			
		||||
func NewAreaControllerResponse(ac *models.AreaController) (*AreaControllerResponse, error) {
 | 
			
		||||
	if ac == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var props map[string]interface{}
 | 
			
		||||
	if len(ac.Properties) > 0 && string(ac.Properties) != "null" {
 | 
			
		||||
		if err := json.Unmarshal(ac.Properties, &props); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("解析区域主控属性失败 (ID: %d): %w", ac.ID, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &AreaControllerResponse{
 | 
			
		||||
		ID:         ac.ID,
 | 
			
		||||
		Name:       ac.Name,
 | 
			
		||||
		NetworkID:  ac.NetworkID,
 | 
			
		||||
		Location:   ac.Location,
 | 
			
		||||
		Status:     ac.Status,
 | 
			
		||||
		Properties: props,
 | 
			
		||||
		CreatedAt:  ac.CreatedAt.Format(time.RFC3339),
 | 
			
		||||
		UpdatedAt:  ac.UpdatedAt.Format(time.RFC3339),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewListAreaControllerResponse 从数据库模型切片创建一个新的区域主控列表响应 DTO 切片
 | 
			
		||||
func NewListAreaControllerResponse(acs []*models.AreaController) ([]*AreaControllerResponse, error) {
 | 
			
		||||
	list := make([]*AreaControllerResponse, 0, len(acs))
 | 
			
		||||
	for _, ac := range acs {
 | 
			
		||||
		resp, err := NewAreaControllerResponse(ac)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		list = append(list, resp)
 | 
			
		||||
	}
 | 
			
		||||
	return list, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewDeviceTemplateResponse 从数据库模型创建一个新的设备模板响应 DTO
 | 
			
		||||
func NewDeviceTemplateResponse(dt *models.DeviceTemplate) (*DeviceTemplateResponse, error) {
 | 
			
		||||
	if dt == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var commands map[string]interface{}
 | 
			
		||||
	if err := dt.ParseCommands(&commands); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("解析设备模板命令失败 (ID: %d): %w", dt.ID, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var values []models.ValueDescriptor
 | 
			
		||||
	if dt.Category == models.CategorySensor {
 | 
			
		||||
		if err := dt.ParseValues(&values); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("解析设备模板值描述符失败 (ID: %d): %w", dt.ID, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &DeviceTemplateResponse{
 | 
			
		||||
		ID:           dt.ID,
 | 
			
		||||
		Name:         dt.Name,
 | 
			
		||||
		Manufacturer: dt.Manufacturer,
 | 
			
		||||
		Description:  dt.Description,
 | 
			
		||||
		Category:     dt.Category,
 | 
			
		||||
		Commands:     commands,
 | 
			
		||||
		Values:       values,
 | 
			
		||||
		CreatedAt:    dt.CreatedAt.Format(time.RFC3339),
 | 
			
		||||
		UpdatedAt:    dt.UpdatedAt.Format(time.RFC3339),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewListDeviceTemplateResponse 从数据库模型切片创建一个新的设备模板列表响应 DTO 切片
 | 
			
		||||
func NewListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTemplateResponse, error) {
 | 
			
		||||
	list := make([]*DeviceTemplateResponse, 0, len(dts))
 | 
			
		||||
	for _, dt := range dts {
 | 
			
		||||
		resp, err := NewDeviceTemplateResponse(dt)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		list = append(list, resp)
 | 
			
		||||
	}
 | 
			
		||||
	return list, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										96
									
								
								internal/app/dto/device_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								internal/app/dto/device_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
 | 
			
		||||
// CreateDeviceRequest 定义了创建设备时需要传入的参数
 | 
			
		||||
type CreateDeviceRequest struct {
 | 
			
		||||
	Name             string                 `json:"name" binding:"required"`
 | 
			
		||||
	DeviceTemplateID uint                   `json:"device_template_id" binding:"required"`
 | 
			
		||||
	AreaControllerID uint                   `json:"area_controller_id" binding:"required"`
 | 
			
		||||
	Location         string                 `json:"location,omitempty"`
 | 
			
		||||
	Properties       map[string]interface{} `json:"properties,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateDeviceRequest 定义了更新设备时需要传入的参数
 | 
			
		||||
type UpdateDeviceRequest struct {
 | 
			
		||||
	Name             string                 `json:"name" binding:"required"`
 | 
			
		||||
	DeviceTemplateID uint                   `json:"device_template_id" binding:"required"`
 | 
			
		||||
	AreaControllerID uint                   `json:"area_controller_id" binding:"required"`
 | 
			
		||||
	Location         string                 `json:"location,omitempty"`
 | 
			
		||||
	Properties       map[string]interface{} `json:"properties,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数
 | 
			
		||||
type CreateAreaControllerRequest struct {
 | 
			
		||||
	Name       string                 `json:"name" binding:"required"`
 | 
			
		||||
	NetworkID  string                 `json:"network_id" binding:"required"`
 | 
			
		||||
	Location   string                 `json:"location,omitempty"`
 | 
			
		||||
	Properties map[string]interface{} `json:"properties,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数
 | 
			
		||||
type UpdateAreaControllerRequest struct {
 | 
			
		||||
	Name       string                 `json:"name" binding:"required"`
 | 
			
		||||
	NetworkID  string                 `json:"network_id" binding:"required"`
 | 
			
		||||
	Location   string                 `json:"location,omitempty"`
 | 
			
		||||
	Properties map[string]interface{} `json:"properties,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数
 | 
			
		||||
type CreateDeviceTemplateRequest struct {
 | 
			
		||||
	Name         string                   `json:"name" binding:"required"`
 | 
			
		||||
	Manufacturer string                   `json:"manufacturer,omitempty"`
 | 
			
		||||
	Description  string                   `json:"description,omitempty"`
 | 
			
		||||
	Category     models.DeviceCategory    `json:"category" binding:"required"`
 | 
			
		||||
	Commands     map[string]interface{}   `json:"commands" binding:"required"`
 | 
			
		||||
	Values       []models.ValueDescriptor `json:"values,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数
 | 
			
		||||
type UpdateDeviceTemplateRequest struct {
 | 
			
		||||
	Name         string                   `json:"name" binding:"required"`
 | 
			
		||||
	Manufacturer string                   `json:"manufacturer,omitempty"`
 | 
			
		||||
	Description  string                   `json:"description,omitempty"`
 | 
			
		||||
	Category     models.DeviceCategory    `json:"category" binding:"required"`
 | 
			
		||||
	Commands     map[string]interface{}   `json:"commands" binding:"required"`
 | 
			
		||||
	Values       []models.ValueDescriptor `json:"values,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeviceResponse 定义了返回给客户端的单个设备信息的结构
 | 
			
		||||
type DeviceResponse struct {
 | 
			
		||||
	ID                 uint                   `json:"id"`
 | 
			
		||||
	Name               string                 `json:"name"`
 | 
			
		||||
	DeviceTemplateID   uint                   `json:"device_template_id"`
 | 
			
		||||
	DeviceTemplateName string                 `json:"device_template_name"`
 | 
			
		||||
	AreaControllerID   uint                   `json:"area_controller_id"`
 | 
			
		||||
	AreaControllerName string                 `json:"area_controller_name"`
 | 
			
		||||
	Location           string                 `json:"location"`
 | 
			
		||||
	Properties         map[string]interface{} `json:"properties"`
 | 
			
		||||
	CreatedAt          string                 `json:"created_at"`
 | 
			
		||||
	UpdatedAt          string                 `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构
 | 
			
		||||
type AreaControllerResponse struct {
 | 
			
		||||
	ID         uint                   `json:"id"`
 | 
			
		||||
	Name       string                 `json:"name"`
 | 
			
		||||
	NetworkID  string                 `json:"network_id"`
 | 
			
		||||
	Location   string                 `json:"location"`
 | 
			
		||||
	Status     string                 `json:"status"`
 | 
			
		||||
	Properties map[string]interface{} `json:"properties"`
 | 
			
		||||
	CreatedAt  string                 `json:"created_at"`
 | 
			
		||||
	UpdatedAt  string                 `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构
 | 
			
		||||
type DeviceTemplateResponse struct {
 | 
			
		||||
	ID           uint                     `json:"id"`
 | 
			
		||||
	Name         string                   `json:"name"`
 | 
			
		||||
	Manufacturer string                   `json:"manufacturer"`
 | 
			
		||||
	Description  string                   `json:"description"`
 | 
			
		||||
	Category     models.DeviceCategory    `json:"category"`
 | 
			
		||||
	Commands     map[string]interface{}   `json:"commands"`
 | 
			
		||||
	Values       []models.ValueDescriptor `json:"values"`
 | 
			
		||||
	CreatedAt    string                   `json:"created_at"`
 | 
			
		||||
	UpdatedAt    string                   `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								internal/app/dto/pig_batch_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								internal/app/dto/pig_batch_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" // 导入 models 包以使用 PigBatchOriginType 和 PigBatchStatus
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PigBatchCreateDTO 定义了创建猪批次的请求结构
 | 
			
		||||
type PigBatchCreateDTO struct {
 | 
			
		||||
	BatchNumber  string                    `json:"batch_number" binding:"required"`        // 批次编号,必填
 | 
			
		||||
	OriginType   models.PigBatchOriginType `json:"origin_type" binding:"required"`         // 批次来源,必填
 | 
			
		||||
	StartDate    time.Time                 `json:"start_date" binding:"required"`          // 批次开始日期,必填
 | 
			
		||||
	InitialCount int                       `json:"initial_count" binding:"required,min=1"` // 初始数量,必填,最小为1
 | 
			
		||||
	Status       models.PigBatchStatus     `json:"status" binding:"required"`              // 批次状态,必填
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PigBatchUpdateDTO 定义了更新猪批次的请求结构
 | 
			
		||||
type PigBatchUpdateDTO struct {
 | 
			
		||||
	BatchNumber  *string                    `json:"batch_number"`  // 批次编号,可选
 | 
			
		||||
	OriginType   *models.PigBatchOriginType `json:"origin_type"`   // 批次来源,可选
 | 
			
		||||
	StartDate    *time.Time                 `json:"start_date"`    // 批次开始日期,可选
 | 
			
		||||
	EndDate      *time.Time                 `json:"end_date"`      // 批次结束日期,可选
 | 
			
		||||
	InitialCount *int                       `json:"initial_count"` // 初始数量,可选
 | 
			
		||||
	Status       *models.PigBatchStatus     `json:"status"`        // 批次状态,可选
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PigBatchQueryDTO 定义了查询猪批次的请求结构
 | 
			
		||||
type PigBatchQueryDTO struct {
 | 
			
		||||
	IsActive *bool `json:"is_active" form:"is_active"` // 是否活跃,可选,用于URL查询参数
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PigBatchResponseDTO 定义了猪批次信息的响应结构
 | 
			
		||||
type PigBatchResponseDTO struct {
 | 
			
		||||
	ID           uint                      `json:"id"`            // 批次ID
 | 
			
		||||
	BatchNumber  string                    `json:"batch_number"`  // 批次编号
 | 
			
		||||
	OriginType   models.PigBatchOriginType `json:"origin_type"`   // 批次来源
 | 
			
		||||
	StartDate    time.Time                 `json:"start_date"`    // 批次开始日期
 | 
			
		||||
	EndDate      time.Time                 `json:"end_date"`      // 批次结束日期
 | 
			
		||||
	InitialCount int                       `json:"initial_count"` // 初始数量
 | 
			
		||||
	Status       models.PigBatchStatus     `json:"status"`        // 批次状态
 | 
			
		||||
	IsActive     bool                      `json:"is_active"`     // 是否活跃
 | 
			
		||||
	CreateTime   time.Time                 `json:"create_time"`   // 创建时间
 | 
			
		||||
	UpdateTime   time.Time                 `json:"update_time"`   // 更新时间
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								internal/app/dto/pig_farm_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								internal/app/dto/pig_farm_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
 | 
			
		||||
// PigHouseResponse 定义了猪舍信息的响应结构
 | 
			
		||||
type PigHouseResponse struct {
 | 
			
		||||
	ID          uint   `json:"id"`
 | 
			
		||||
	Name        string `json:"name"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PenResponse 定义了猪栏信息的响应结构
 | 
			
		||||
type PenResponse struct {
 | 
			
		||||
	ID         uint             `json:"id"`
 | 
			
		||||
	PenNumber  string           `json:"pen_number"`
 | 
			
		||||
	HouseID    uint             `json:"house_id"`
 | 
			
		||||
	Capacity   int              `json:"capacity"`
 | 
			
		||||
	Status     models.PenStatus `json:"status"`
 | 
			
		||||
	PigBatchID uint             `json:"pig_batch_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreatePigHouseRequest 定义了创建猪舍的请求结构
 | 
			
		||||
type CreatePigHouseRequest struct {
 | 
			
		||||
	Name        string `json:"name" binding:"required"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePigHouseRequest 定义了更新猪舍的请求结构
 | 
			
		||||
type UpdatePigHouseRequest struct {
 | 
			
		||||
	Name        string `json:"name" binding:"required"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreatePenRequest 定义了创建猪栏的请求结构
 | 
			
		||||
type CreatePenRequest struct {
 | 
			
		||||
	PenNumber string           `json:"pen_number" binding:"required"`
 | 
			
		||||
	HouseID   uint             `json:"house_id" binding:"required"`
 | 
			
		||||
	Capacity  int              `json:"capacity" binding:"required"`
 | 
			
		||||
	Status    models.PenStatus `json:"status" binding:"required"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePenRequest 定义了更新猪栏的请求结构
 | 
			
		||||
type UpdatePenRequest struct {
 | 
			
		||||
	PenNumber string           `json:"pen_number" binding:"required"`
 | 
			
		||||
	HouseID   uint             `json:"house_id" binding:"required"`
 | 
			
		||||
	Capacity  int              `json:"capacity" binding:"required"`
 | 
			
		||||
	Status    models.PenStatus `json:"status" binding:"required"`
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package plan
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
@@ -7,8 +7,8 @@ import (
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PlanToResponse 将Plan模型转换为PlanResponse
 | 
			
		||||
func PlanToResponse(plan *models.Plan) (*PlanResponse, error) {
 | 
			
		||||
// NewPlanToResponse 将Plan模型转换为PlanResponse
 | 
			
		||||
func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) {
 | 
			
		||||
	if plan == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -52,8 +52,8 @@ func PlanToResponse(plan *models.Plan) (*PlanResponse, error) {
 | 
			
		||||
	return response, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
			
		||||
func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
 | 
			
		||||
// NewPlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
			
		||||
func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
 | 
			
		||||
	if req == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -104,8 +104,8 @@ func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
 | 
			
		||||
	return plan, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
			
		||||
func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
 | 
			
		||||
// NewPlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证
 | 
			
		||||
func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
 | 
			
		||||
	if req == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -171,7 +171,7 @@ func SubPlanToResponse(subPlan *models.SubPlan) (SubPlanResponse, error) {
 | 
			
		||||
 | 
			
		||||
	// 如果有完整的子计划数据,也进行转换
 | 
			
		||||
	if subPlan.ChildPlan != nil {
 | 
			
		||||
		childPlanResp, err := PlanToResponse(subPlan.ChildPlan)
 | 
			
		||||
		childPlanResp, err := NewPlanToResponse(subPlan.ChildPlan)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return SubPlanResponse{}, err
 | 
			
		||||
		}
 | 
			
		||||
							
								
								
									
										75
									
								
								internal/app/dto/plan_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								internal/app/dto/plan_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
 | 
			
		||||
// CreatePlanRequest 定义创建计划请求的结构体
 | 
			
		||||
type CreatePlanRequest struct {
 | 
			
		||||
	Name           string                   `json:"name" binding:"required" example:"猪舍温度控制计划"`
 | 
			
		||||
	Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
			
		||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
 | 
			
		||||
	ExecuteNum     uint                     `json:"execute_num,omitempty" example:"10"`
 | 
			
		||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
			
		||||
	SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
			
		||||
	Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PlanResponse 定义计划详情响应的结构体
 | 
			
		||||
type PlanResponse struct {
 | 
			
		||||
	ID             uint                     `json:"id" example:"1"`
 | 
			
		||||
	Name           string                   `json:"name" example:"猪舍温度控制计划"`
 | 
			
		||||
	Description    string                   `json:"description" example:"根据温度自动调节风扇和加热器"`
 | 
			
		||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" example:"自动"`
 | 
			
		||||
	Status         models.PlanStatus        `json:"status" example:"已启用"`
 | 
			
		||||
	ExecuteNum     uint                     `json:"execute_num" example:"10"`
 | 
			
		||||
	ExecuteCount   uint                     `json:"execute_count" example:"0"`
 | 
			
		||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
			
		||||
	ContentType    models.PlanContentType   `json:"content_type" example:"任务"`
 | 
			
		||||
	SubPlans       []SubPlanResponse        `json:"sub_plans,omitempty"`
 | 
			
		||||
	Tasks          []TaskResponse           `json:"tasks,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListPlansResponse 定义获取计划列表响应的结构体
 | 
			
		||||
type ListPlansResponse struct {
 | 
			
		||||
	Plans []PlanResponse `json:"plans"`
 | 
			
		||||
	Total int            `json:"total" example:"100"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePlanRequest 定义更新计划请求的结构体
 | 
			
		||||
type UpdatePlanRequest struct {
 | 
			
		||||
	Name           string                   `json:"name" example:"猪舍温度控制计划V2"`
 | 
			
		||||
	Description    string                   `json:"description" example:"更新后的描述"`
 | 
			
		||||
	ExecutionType  models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
 | 
			
		||||
	ExecuteNum     uint                     `json:"execute_num,omitempty" example:"10"`
 | 
			
		||||
	CronExpression string                   `json:"cron_expression" example:"0 0 6 * * *"`
 | 
			
		||||
	SubPlanIDs     []uint                   `json:"sub_plan_ids,omitempty"`
 | 
			
		||||
	Tasks          []TaskRequest            `json:"tasks,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubPlanResponse 定义子计划响应结构体
 | 
			
		||||
type SubPlanResponse struct {
 | 
			
		||||
	ID             uint          `json:"id" example:"1"`
 | 
			
		||||
	ParentPlanID   uint          `json:"parent_plan_id" example:"1"`
 | 
			
		||||
	ChildPlanID    uint          `json:"child_plan_id" example:"2"`
 | 
			
		||||
	ExecutionOrder int           `json:"execution_order" example:"1"`
 | 
			
		||||
	ChildPlan      *PlanResponse `json:"child_plan,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TaskRequest 定义任务请求结构体
 | 
			
		||||
type TaskRequest struct {
 | 
			
		||||
	Name           string                 `json:"name" example:"打开风扇"`
 | 
			
		||||
	Description    string                 `json:"description" example:"打开1号风扇"`
 | 
			
		||||
	ExecutionOrder int                    `json:"execution_order" example:"1"`
 | 
			
		||||
	Type           models.TaskType        `json:"type" example:"等待"`
 | 
			
		||||
	Parameters     map[string]interface{} `json:"parameters,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TaskResponse 定义任务响应结构体
 | 
			
		||||
type TaskResponse struct {
 | 
			
		||||
	ID             int                    `json:"id" example:"1"`
 | 
			
		||||
	PlanID         uint                   `json:"plan_id" example:"1"`
 | 
			
		||||
	Name           string                 `json:"name" example:"打开风扇"`
 | 
			
		||||
	Description    string                 `json:"description" example:"打开1号风扇"`
 | 
			
		||||
	ExecutionOrder int                    `json:"execution_order" example:"1"`
 | 
			
		||||
	Type           models.TaskType        `json:"type" example:"等待"`
 | 
			
		||||
	Parameters     map[string]interface{} `json:"parameters,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								internal/app/dto/user_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								internal/app/dto/user_dto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
// CreateUserRequest 定义创建用户请求的结构体
 | 
			
		||||
type CreateUserRequest struct {
 | 
			
		||||
	Username string `json:"username" binding:"required" example:"newuser"`
 | 
			
		||||
	Password string `json:"password" binding:"required" example:"password123"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoginRequest 定义登录请求的结构体
 | 
			
		||||
type LoginRequest struct {
 | 
			
		||||
	// Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号
 | 
			
		||||
	Identifier string `json:"identifier" binding:"required" example:"testuser"`
 | 
			
		||||
	Password   string `json:"password" binding:"required" example:"password123"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateUserResponse 定义创建用户成功响应的结构体
 | 
			
		||||
type CreateUserResponse struct {
 | 
			
		||||
	Username string `json:"username" example:"newuser"`
 | 
			
		||||
	ID       uint   `json:"id" example:"1"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoginResponse 定义登录成功响应的结构体
 | 
			
		||||
type LoginResponse struct {
 | 
			
		||||
	Username string `json:"username" example:"testuser"`
 | 
			
		||||
	ID       uint   `json:"id" example:"1"`
 | 
			
		||||
	Token    string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HistoryResponse 定义单条操作历史的响应结构体
 | 
			
		||||
type HistoryResponse struct {
 | 
			
		||||
	UserID         uint        `json:"user_id" example:"101"`
 | 
			
		||||
	Username       string      `json:"username" example:"testuser"`
 | 
			
		||||
	ActionType     string      `json:"action_type" example:"更新设备"`
 | 
			
		||||
	Description    string      `json:"description" example:"设备更新成功"`
 | 
			
		||||
	TargetResource interface{} `json:"target_resource"`
 | 
			
		||||
	Time           string      `json:"time"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListHistoryResponse 定义操作历史列表的响应结构体
 | 
			
		||||
type ListHistoryResponse struct {
 | 
			
		||||
	History []HistoryResponse `json:"history"`
 | 
			
		||||
	Total   int64             `json:"total" example:"100"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										176
									
								
								internal/app/service/pig_batch_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								internal/app/service/pig_batch_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrPigBatchNotFound = errors.New("指定的猪批次不存在")
 | 
			
		||||
	ErrPigBatchActive   = errors.New("活跃的猪批次不能被删除") // 新增错误:活跃的猪批次不能被删除
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PigBatchService 提供了猪批次管理的业务逻辑
 | 
			
		||||
type PigBatchService interface {
 | 
			
		||||
	CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error)
 | 
			
		||||
	GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error)
 | 
			
		||||
	UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error)
 | 
			
		||||
	DeletePigBatch(id uint) error
 | 
			
		||||
	ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type pigBatchService struct {
 | 
			
		||||
	logger *logs.Logger
 | 
			
		||||
	repo   repository.PigBatchRepository
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewPigBatchService 创建一个新的 PigBatchService 实例
 | 
			
		||||
func NewPigBatchService(repo repository.PigBatchRepository, logger *logs.Logger) PigBatchService {
 | 
			
		||||
	return &pigBatchService{
 | 
			
		||||
		logger: logger,
 | 
			
		||||
		repo:   repo,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// toPigBatchResponseDTO 将 models.PigBatch 转换为 dto.PigBatchResponseDTO
 | 
			
		||||
func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch) *dto.PigBatchResponseDTO {
 | 
			
		||||
	if batch == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return &dto.PigBatchResponseDTO{
 | 
			
		||||
		ID:           batch.ID,
 | 
			
		||||
		BatchNumber:  batch.BatchNumber,
 | 
			
		||||
		OriginType:   batch.OriginType,
 | 
			
		||||
		StartDate:    batch.StartDate,
 | 
			
		||||
		EndDate:      batch.EndDate,
 | 
			
		||||
		InitialCount: batch.InitialCount,
 | 
			
		||||
		Status:       batch.Status,
 | 
			
		||||
		IsActive:     batch.IsActive(), // 使用模型自带的 IsActive 方法
 | 
			
		||||
		CreateTime:   batch.CreatedAt,
 | 
			
		||||
		UpdateTime:   batch.UpdatedAt,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreatePigBatch 处理创建猪批次的业务逻辑
 | 
			
		||||
func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
 | 
			
		||||
	batch := &models.PigBatch{
 | 
			
		||||
		BatchNumber:  dto.BatchNumber,
 | 
			
		||||
		OriginType:   dto.OriginType,
 | 
			
		||||
		StartDate:    dto.StartDate,
 | 
			
		||||
		InitialCount: dto.InitialCount,
 | 
			
		||||
		Status:       dto.Status,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	createdBatch, err := s.repo.CreatePigBatch(batch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.Errorf("创建猪批次失败: %v", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s.toPigBatchResponseDTO(createdBatch), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPigBatch 处理获取单个猪批次的业务逻辑
 | 
			
		||||
func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) {
 | 
			
		||||
	batch, err := s.repo.GetPigBatchByID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
			return nil, ErrPigBatchNotFound
 | 
			
		||||
		}
 | 
			
		||||
		s.logger.Errorf("获取猪批次失败,ID: %d, 错误: %v", id, err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s.toPigBatchResponseDTO(batch), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePigBatch 处理更新猪批次的业务逻辑
 | 
			
		||||
func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
 | 
			
		||||
	existingBatch, err := s.repo.GetPigBatchByID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
			return nil, ErrPigBatchNotFound
 | 
			
		||||
		}
 | 
			
		||||
		s.logger.Errorf("更新猪批次失败,获取原批次信息错误,ID: %d, 错误: %v", id, err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 根据 DTO 中的非空字段更新模型
 | 
			
		||||
	if dto.BatchNumber != nil {
 | 
			
		||||
		existingBatch.BatchNumber = *dto.BatchNumber
 | 
			
		||||
	}
 | 
			
		||||
	if dto.OriginType != nil {
 | 
			
		||||
		existingBatch.OriginType = *dto.OriginType
 | 
			
		||||
	}
 | 
			
		||||
	if dto.StartDate != nil {
 | 
			
		||||
		existingBatch.StartDate = *dto.StartDate
 | 
			
		||||
	}
 | 
			
		||||
	if dto.EndDate != nil {
 | 
			
		||||
		existingBatch.EndDate = *dto.EndDate
 | 
			
		||||
	}
 | 
			
		||||
	if dto.InitialCount != nil {
 | 
			
		||||
		existingBatch.InitialCount = *dto.InitialCount
 | 
			
		||||
	}
 | 
			
		||||
	if dto.Status != nil {
 | 
			
		||||
		existingBatch.Status = *dto.Status
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updatedBatch, err := s.repo.UpdatePigBatch(existingBatch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.Errorf("更新猪批次失败,ID: %d, 错误: %v", id, err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s.toPigBatchResponseDTO(updatedBatch), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeletePigBatch 处理删除猪批次的业务逻辑
 | 
			
		||||
func (s *pigBatchService) DeletePigBatch(id uint) error {
 | 
			
		||||
	// 1. 获取猪批次信息
 | 
			
		||||
	batch, err := s.repo.GetPigBatchByID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
			return ErrPigBatchNotFound
 | 
			
		||||
		}
 | 
			
		||||
		s.logger.Errorf("删除猪批次失败,获取批次信息错误,ID: %d, 错误: %v", id, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 2. 检查猪批次是否活跃
 | 
			
		||||
	if batch.IsActive() {
 | 
			
		||||
		return ErrPigBatchActive // 如果活跃,则不允许删除
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 3. 执行删除操作
 | 
			
		||||
	err = s.repo.DeletePigBatch(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, gorm.ErrRecordNotFound) || errors.New("未找到要删除的猪批次").Error() == err.Error() {
 | 
			
		||||
			return ErrPigBatchNotFound
 | 
			
		||||
		}
 | 
			
		||||
		s.logger.Errorf("删除猪批次失败,ID: %d, 错误: %v", id, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListPigBatches 处理批量查询猪批次的业务逻辑
 | 
			
		||||
func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) {
 | 
			
		||||
	batches, err := s.repo.ListPigBatches(isActive)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.Errorf("批量查询猪批次失败,错误: %v", err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var responseDTOs []*dto.PigBatchResponseDTO
 | 
			
		||||
	for _, batch := range batches {
 | 
			
		||||
		responseDTOs = append(responseDTOs, s.toPigBatchResponseDTO(batch))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return responseDTOs, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,21 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrHouseContainsPens = errors.New("无法删除包含猪栏的猪舍")
 | 
			
		||||
	ErrHouseNotFound     = errors.New("指定的猪舍不存在")
 | 
			
		||||
	ErrPenInUse          = errors.New("猪栏正在被活跃批次使用,无法删除")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PigFarmService 提供了猪场资产管理的业务逻辑
 | 
			
		||||
type PigFarmService interface {
 | 
			
		||||
	// PigHouse methods
 | 
			
		||||
@@ -71,19 +80,42 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*mod
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *pigFarmService) DeletePigHouse(id uint) error {
 | 
			
		||||
	return s.repo.DeletePigHouse(id)
 | 
			
		||||
	// 业务逻辑:检查猪舍是否包含猪栏
 | 
			
		||||
	penCount, err := s.repo.CountPensInHouse(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if penCount > 0 {
 | 
			
		||||
		return ErrHouseContainsPens
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 调用仓库层进行删除
 | 
			
		||||
	err = s.repo.DeletePigHouse(id)
 | 
			
		||||
	if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
		return ErrHouseNotFound // 或者直接返回 gorm.ErrRecordNotFound,取决于业务需求
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Pen Implementation ---
 | 
			
		||||
 | 
			
		||||
func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) {
 | 
			
		||||
	// 业务逻辑:验证所属猪舍是否存在
 | 
			
		||||
	_, err := s.repo.GetPigHouseByID(houseID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
			return nil, ErrHouseNotFound
 | 
			
		||||
		}
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pen := &models.Pen{
 | 
			
		||||
		PenNumber: penNumber,
 | 
			
		||||
		HouseID:   houseID,
 | 
			
		||||
		Capacity:  capacity,
 | 
			
		||||
		Status:    status,
 | 
			
		||||
	}
 | 
			
		||||
	err := s.repo.CreatePen(pen)
 | 
			
		||||
	err = s.repo.CreatePen(pen)
 | 
			
		||||
	return pen, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -96,6 +128,15 @@ func (s *pigFarmService) ListPens() ([]models.Pen, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) {
 | 
			
		||||
	// 业务逻辑:验证所属猪舍是否存在
 | 
			
		||||
	_, err := s.repo.GetPigHouseByID(houseID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
			return nil, ErrHouseNotFound
 | 
			
		||||
		}
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pen := &models.Pen{
 | 
			
		||||
		Model:     gorm.Model{ID: id},
 | 
			
		||||
		PenNumber: penNumber,
 | 
			
		||||
@@ -103,7 +144,7 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa
 | 
			
		||||
		Capacity:  capacity,
 | 
			
		||||
		Status:    status,
 | 
			
		||||
	}
 | 
			
		||||
	err := s.repo.UpdatePen(pen)
 | 
			
		||||
	err = s.repo.UpdatePen(pen)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -112,5 +153,31 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *pigFarmService) DeletePen(id uint) error {
 | 
			
		||||
	return s.repo.DeletePen(id)
 | 
			
		||||
	// 业务逻辑:检查猪栏是否被活跃批次使用
 | 
			
		||||
	pen, err := s.repo.GetPenByID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
			return gorm.ErrRecordNotFound // 猪栏不存在
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查猪栏是否关联了活跃批次
 | 
			
		||||
	if pen.PigBatchID != 0 {
 | 
			
		||||
		pigBatch, err := s.repo.GetPigBatchByID(pen.PigBatchID)
 | 
			
		||||
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		// 如果批次活跃,则不能删除猪栏
 | 
			
		||||
		if pigBatch.IsActive() {
 | 
			
		||||
			return ErrPenInUse
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 调用仓库层进行删除
 | 
			
		||||
	err = s.repo.DeletePen(id)
 | 
			
		||||
	if errors.Is(err, gorm.ErrRecordNotFound) {
 | 
			
		||||
		return gorm.ErrRecordNotFound // 猪栏不存在
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								internal/app/service/pig_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								internal/app/service/pig_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
package service
 | 
			
		||||
@@ -72,9 +72,11 @@ func NewApplication(configPath string) (*Application, error) {
 | 
			
		||||
	deviceCommandLogRepo := repository.NewGormDeviceCommandLogRepository(storage.GetDB())
 | 
			
		||||
	pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB())
 | 
			
		||||
	userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB())
 | 
			
		||||
	pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB())
 | 
			
		||||
 | 
			
		||||
	// --- 业务逻辑处理器初始化 ---
 | 
			
		||||
	pigFarmService := service.NewPigFarmService(pigFarmRepo, logger)
 | 
			
		||||
	pigBatchService := service.NewPigBatchService(pigBatchRepo, logger)
 | 
			
		||||
 | 
			
		||||
	// 初始化审计服务
 | 
			
		||||
	auditService := audit.NewService(userActionLogRepo, logger)
 | 
			
		||||
@@ -121,6 +123,7 @@ func NewApplication(configPath string) (*Application, error) {
 | 
			
		||||
		deviceTemplateRepo,
 | 
			
		||||
		planRepo,
 | 
			
		||||
		pigFarmService,
 | 
			
		||||
		pigBatchService,
 | 
			
		||||
		userActionLogRepo,
 | 
			
		||||
		tokenService,
 | 
			
		||||
		auditService,
 | 
			
		||||
 
 | 
			
		||||
@@ -165,11 +165,13 @@ func (ps *PostgresStorage) creatingHyperTable() error {
 | 
			
		||||
		{models.FeedUsageRecord{}, "recorded_at"},
 | 
			
		||||
		{models.GroupMedicationLog{}, "happened_at"},
 | 
			
		||||
		{models.PigBatchLog{}, "happened_at"},
 | 
			
		||||
		{models.WeighingBatch{}, "weighing_time"},
 | 
			
		||||
		{models.WeighingRecord{}, "weighing_time"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, table := range tablesToConvert {
 | 
			
		||||
		tableName := table.model.TableName()
 | 
			
		||||
		chunkInterval := "7 days" // 统一设置为7天
 | 
			
		||||
		chunkInterval := "1 days" // 统一设置为1天
 | 
			
		||||
		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 {
 | 
			
		||||
@@ -194,11 +196,18 @@ func (ps *PostgresStorage) applyCompressionPolicies() 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.GroupMedicationLog{}, "pig_batch_id"},
 | 
			
		||||
		{models.PigBatchLog{}, "pig_batch_id"},
 | 
			
		||||
		{models.WeighingBatch{}, "pig_batch_id"},
 | 
			
		||||
		{models.WeighingRecord{}, "weighing_batch_id"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, policy := range policies {
 | 
			
		||||
		tableName := policy.model.TableName()
 | 
			
		||||
		compressAfter := "15 days" // 统一设置为15天后开始压缩
 | 
			
		||||
		compressAfter := "3 days" // 统一设置为2天后(即进入第3天)开始压缩
 | 
			
		||||
 | 
			
		||||
		// 1. 开启表的压缩设置,并指定分段列
 | 
			
		||||
		ps.logger.Infow("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn)
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ func (RawMaterial) TableName() string {
 | 
			
		||||
 | 
			
		||||
// RawMaterialPurchase 记录了原料的每一次采购。
 | 
			
		||||
type RawMaterialPurchase struct {
 | 
			
		||||
	ID            uint        `gorm:"primaryKey"`
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	RawMaterialID uint        `gorm:"not null;index;comment:关联的原料ID"`
 | 
			
		||||
	RawMaterial   RawMaterial `gorm:"foreignKey:RawMaterialID"`
 | 
			
		||||
	Supplier      string      `gorm:"size:100;comment:供应商"`
 | 
			
		||||
@@ -34,8 +34,6 @@ type RawMaterialPurchase struct {
 | 
			
		||||
	TotalPrice    float64     `gorm:"comment:总价"`
 | 
			
		||||
	PurchaseDate  time.Time   `gorm:"primaryKey;comment:采购日期"`
 | 
			
		||||
	CreatedAt     time.Time
 | 
			
		||||
	UpdatedAt     time.Time
 | 
			
		||||
	DeletedAt     gorm.DeletedAt `gorm:"index"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (RawMaterialPurchase) TableName() string {
 | 
			
		||||
@@ -56,16 +54,13 @@ const (
 | 
			
		||||
 | 
			
		||||
// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
 | 
			
		||||
type RawMaterialStockLog struct {
 | 
			
		||||
	ID            uint               `gorm:"primaryKey"`
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	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:"primaryKey;comment:业务发生时间"`
 | 
			
		||||
	Remarks       string             `gorm:"comment:备注, 如主动领取的理由等"`
 | 
			
		||||
	CreatedAt     time.Time
 | 
			
		||||
	UpdatedAt     time.Time
 | 
			
		||||
	DeletedAt     gorm.DeletedAt `gorm:"index"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (RawMaterialStockLog) TableName() string {
 | 
			
		||||
@@ -102,7 +97,7 @@ func (FeedFormulaComponent) TableName() string {
 | 
			
		||||
// 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula,
 | 
			
		||||
// 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。
 | 
			
		||||
type FeedUsageRecord struct {
 | 
			
		||||
	ID            uint        `gorm:"primaryKey"`
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	PenID         uint        `gorm:"not null;index;comment:关联的猪栏ID"`
 | 
			
		||||
	Pen           Pen         `gorm:"foreignKey:PenID"`
 | 
			
		||||
	FeedFormulaID uint        `gorm:"not null;index;comment:使用的饲料配方ID"`
 | 
			
		||||
@@ -111,9 +106,6 @@ type FeedUsageRecord struct {
 | 
			
		||||
	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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,7 @@ const (
 | 
			
		||||
 | 
			
		||||
// GroupMedicationLog 记录了对整个猪批次的用药情况
 | 
			
		||||
type GroupMedicationLog struct {
 | 
			
		||||
	ID           uint                 `gorm:"primaryKey"`
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	PigBatchID   uint                 `gorm:"not null;index;comment:关联的猪批次ID"`
 | 
			
		||||
	MedicationID uint                 `gorm:"not null;index;comment:关联的药品ID"`
 | 
			
		||||
	Medication   Medication           `gorm:"foreignKey:MedicationID"` // 预加载药品信息
 | 
			
		||||
@@ -90,11 +90,8 @@ type GroupMedicationLog struct {
 | 
			
		||||
	TargetCount  int                  `gorm:"not null;comment:用药对象数量"`
 | 
			
		||||
	Reason       MedicationReasonType `gorm:"size:20;not null;comment:用药原因"`
 | 
			
		||||
	Description  string               `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"`
 | 
			
		||||
	Operator     string               `gorm:"size:50;comment:操作员"`
 | 
			
		||||
	OperatorID   uint                 `gorm:"comment:操作员ID"`
 | 
			
		||||
	HappenedAt   time.Time            `gorm:"primaryKey;comment:用药时间"`
 | 
			
		||||
	CreatedAt    time.Time
 | 
			
		||||
	UpdatedAt    time.Time
 | 
			
		||||
	DeletedAt    gorm.DeletedAt `gorm:"index"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (GroupMedicationLog) TableName() string {
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,8 @@ func GetAllModels() []interface{} {
 | 
			
		||||
		// Pig & Batch Models
 | 
			
		||||
		&PigBatch{},
 | 
			
		||||
		&PigBatchLog{},
 | 
			
		||||
		&WeighingBatch{},
 | 
			
		||||
		&WeighingRecord{},
 | 
			
		||||
 | 
			
		||||
		// Feed Models
 | 
			
		||||
		&RawMaterial{},
 | 
			
		||||
 
 | 
			
		||||
@@ -36,18 +36,20 @@ type PigBatch struct {
 | 
			
		||||
	BatchNumber  string             `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"`
 | 
			
		||||
	OriginType   PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"`
 | 
			
		||||
	StartDate    time.Time          `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"`
 | 
			
		||||
	EndDate      time.Time          `gorm:"not null;comment:批次结束日期 (全部淘汰或售出)"`
 | 
			
		||||
	InitialCount int                `gorm:"not null;comment:初始数量"`
 | 
			
		||||
	CurrentCount     int                `gorm:"not null;comment:当前存栏数量"`
 | 
			
		||||
	CurrentSickCount int                `gorm:"not null;default:0;comment:当前病猪数量"`
 | 
			
		||||
	AverageWeight    float64            `gorm:"comment:平均体重 (kg)"`
 | 
			
		||||
	Status       PigBatchStatus     `gorm:"size:20;not null;index;comment:批次状态"`
 | 
			
		||||
	Pens             []Pen              `gorm:"foreignKey:PigBatchID;comment:所在圈舍ID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (PigBatch) TableName() string {
 | 
			
		||||
	return "pig_batches"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsActive 判断猪批次是否处于活跃状态
 | 
			
		||||
func (pb PigBatch) IsActive() bool {
 | 
			
		||||
	return pb.Status != BatchStatusSold && pb.Status != BatchStatusArchived
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LogChangeType 定义了猪批次数量变更的类型
 | 
			
		||||
type LogChangeType string
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +66,7 @@ const (
 | 
			
		||||
 | 
			
		||||
// PigBatchLog 记录了猪批次数量或状态的每一次变更
 | 
			
		||||
type PigBatchLog struct {
 | 
			
		||||
	ID              uint          `gorm:"primaryKey"`
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	PigBatchID      uint          `gorm:"not null;index;comment:关联的猪批次ID"`
 | 
			
		||||
	ChangeType      LogChangeType `gorm:"size:20;not null;comment:变更类型"`
 | 
			
		||||
	ChangeCount     int           `gorm:"not null;comment:数量变化,负数表示减少"`
 | 
			
		||||
@@ -73,13 +75,37 @@ type PigBatchLog struct {
 | 
			
		||||
	AfterCount      int           `gorm:"not null;comment:变更后总数"`
 | 
			
		||||
	BeforeSickCount int           `gorm:"not null;comment:变更前病猪数"`
 | 
			
		||||
	AfterSickCount  int           `gorm:"not null;comment:变更后病猪数"`
 | 
			
		||||
	Operator        string        `gorm:"size:50;comment:操作员"`
 | 
			
		||||
	OperatorID      uint          `gorm:"comment:操作员ID"`
 | 
			
		||||
	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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WeighingBatch 记录了一次批次称重的信息
 | 
			
		||||
type WeighingBatch struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"`
 | 
			
		||||
	Description  string    `gorm:"size:255;comment:批次称重描述"`
 | 
			
		||||
	PigBatchID   uint      `gorm:"not null;index;comment:关联的猪批次ID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (WeighingBatch) TableName() string {
 | 
			
		||||
	return "weighing_batches"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WeighingRecord 记录了单次称重信息
 | 
			
		||||
type WeighingRecord struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Weight          float64   `gorm:"not null;comment:单只猪重量 (kg)"`
 | 
			
		||||
	WeighingBatchID uint      `gorm:"not null;index;comment:关联的批次称重ID"`
 | 
			
		||||
	PenID           uint      `gorm:"not null;index;comment:所在猪圈ID"`
 | 
			
		||||
	OperatorID      uint      `gorm:"not null;comment:操作员ID"`
 | 
			
		||||
	Remark          string    `gorm:"size:255;comment:备注"`
 | 
			
		||||
	WeighingTime    time.Time `gorm:"primaryKey;comment:称重时间"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (WeighingRecord) TableName() string {
 | 
			
		||||
	return "weighing_records"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								internal/infra/repository/pig_batch_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								internal/infra/repository/pig_batch_repository.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PigBatchRepository 定义了与猪批次相关的数据库操作接口
 | 
			
		||||
type PigBatchRepository interface {
 | 
			
		||||
	CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error)
 | 
			
		||||
	GetPigBatchByID(id uint) (*models.PigBatch, error)
 | 
			
		||||
	UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error)
 | 
			
		||||
	DeletePigBatch(id uint) error
 | 
			
		||||
	ListPigBatches(isActive *bool) ([]*models.PigBatch, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// gormPigBatchRepository 是 PigBatchRepository 的 GORM 实现
 | 
			
		||||
type gormPigBatchRepository struct {
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewGormPigBatchRepository 创建一个新的 PigBatchRepository GORM 实现实例
 | 
			
		||||
func NewGormPigBatchRepository(db *gorm.DB) PigBatchRepository {
 | 
			
		||||
	return &gormPigBatchRepository{db: db}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreatePigBatch 创建一个新的猪批次
 | 
			
		||||
func (r *gormPigBatchRepository) CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) {
 | 
			
		||||
	if err := r.db.Create(batch).Error; err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return batch, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPigBatchByID 根据ID获取单个猪批次
 | 
			
		||||
func (r *gormPigBatchRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) {
 | 
			
		||||
	var batch models.PigBatch
 | 
			
		||||
	if err := r.db.First(&batch, id).Error; err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &batch, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePigBatch 更新一个猪批次
 | 
			
		||||
func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) {
 | 
			
		||||
	result := r.db.Model(&models.PigBatch{}).Where("id = ?", batch.ID).Updates(batch)
 | 
			
		||||
	if result.Error != nil {
 | 
			
		||||
		return nil, result.Error
 | 
			
		||||
	}
 | 
			
		||||
	if result.RowsAffected == 0 {
 | 
			
		||||
		return nil, errors.New("未找到要更新的猪批次或数据未改变") // 明确返回错误,而不是 gorm.ErrRecordNotFound
 | 
			
		||||
	}
 | 
			
		||||
	return batch, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeletePigBatch 根据ID删除一个猪批次 (GORM 会执行软删除)
 | 
			
		||||
func (r *gormPigBatchRepository) DeletePigBatch(id uint) error {
 | 
			
		||||
	result := r.db.Delete(&models.PigBatch{}, id)
 | 
			
		||||
	if result.Error != nil {
 | 
			
		||||
		return result.Error
 | 
			
		||||
	}
 | 
			
		||||
	if result.RowsAffected == 0 {
 | 
			
		||||
		return errors.New("未找到要删除的猪批次") // 明确返回错误
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListPigBatches 批量查询猪批次,支持根据 IsActive 筛选
 | 
			
		||||
func (r *gormPigBatchRepository) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) {
 | 
			
		||||
	var batches []*models.PigBatch
 | 
			
		||||
	query := r.db.Model(&models.PigBatch{})
 | 
			
		||||
 | 
			
		||||
	if isActive != nil {
 | 
			
		||||
		if *isActive {
 | 
			
		||||
			// 查询活跃的批次:状态不是已出售或已归档
 | 
			
		||||
			query = query.Where("status NOT IN (?) ", []models.PigBatchStatus{models.BatchStatusSold, models.BatchStatusArchived})
 | 
			
		||||
		} else {
 | 
			
		||||
			// 查询非活跃的批次:状态是已出售或已归档
 | 
			
		||||
			query = query.Where("status IN (?) ", []models.PigBatchStatus{models.BatchStatusSold, models.BatchStatusArchived})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := query.Find(&batches).Error; err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return batches, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +1,10 @@
 | 
			
		||||
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
 | 
			
		||||
@@ -20,6 +13,7 @@ type PigFarmRepository interface {
 | 
			
		||||
	ListPigHouses() ([]models.PigHouse, error)
 | 
			
		||||
	UpdatePigHouse(house *models.PigHouse) error
 | 
			
		||||
	DeletePigHouse(id uint) error
 | 
			
		||||
	CountPensInHouse(houseID uint) (int64, error)
 | 
			
		||||
 | 
			
		||||
	// Pen methods
 | 
			
		||||
	CreatePen(pen *models.Pen) error
 | 
			
		||||
@@ -27,6 +21,9 @@ type PigFarmRepository interface {
 | 
			
		||||
	ListPens() ([]models.Pen, error)
 | 
			
		||||
	UpdatePen(pen *models.Pen) error
 | 
			
		||||
	DeletePen(id uint) error
 | 
			
		||||
 | 
			
		||||
	// PigBatch methods
 | 
			
		||||
	GetPigBatchByID(id uint) (*models.PigBatch, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现
 | 
			
		||||
@@ -73,16 +70,7 @@ func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
	result := r.db.Delete(&models.PigHouse{}, id)
 | 
			
		||||
	if result.Error != nil {
 | 
			
		||||
		return result.Error
 | 
			
		||||
	}
 | 
			
		||||
@@ -90,22 +78,18 @@ func (r *gormPigFarmRepository) DeletePigHouse(id uint) error {
 | 
			
		||||
		return gorm.ErrRecordNotFound
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) {
 | 
			
		||||
	var count int64
 | 
			
		||||
	err := r.db.Model(&models.Pen{}).Where("house_id = ?", houseID).Count(&count).Error
 | 
			
		||||
	return count, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 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
 | 
			
		||||
	})
 | 
			
		||||
	return r.db.Create(pen).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) {
 | 
			
		||||
@@ -125,16 +109,7 @@ func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
	result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen)
 | 
			
		||||
	if result.Error != nil {
 | 
			
		||||
		return result.Error
 | 
			
		||||
	}
 | 
			
		||||
@@ -142,7 +117,6 @@ func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error {
 | 
			
		||||
		return gorm.ErrRecordNotFound
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *gormPigFarmRepository) DeletePen(id uint) error {
 | 
			
		||||
@@ -155,3 +129,13 @@ func (r *gormPigFarmRepository) DeletePen(id uint) error {
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- PigBatch Implementation ---
 | 
			
		||||
 | 
			
		||||
func (r *gormPigFarmRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) {
 | 
			
		||||
	var batch models.PigBatch
 | 
			
		||||
	if err := r.db.First(&batch, id).Error; err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &batch, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user