issue_29 #32

Merged
huang merged 65 commits from issue_29 into main 2025-10-07 13:33:25 +08:00
83 changed files with 12030 additions and 3489 deletions

52
.golangci.yml Normal file
View File

@@ -0,0 +1,52 @@
# .golangci.yml - 为你的项目量身定制的 linter 配置
linters-settings:
# 这里可以对特定的 linter 进行微调
errcheck:
# 检查未处理的错误,但可以排除一些常见的、我们确认无需处理的函数
exclude-functions:
- io/ioutil.ReadFile
- io.Copy
- io.WriteString
- os.Create
linters:
# 明确我们想要禁用的 linter
disable:
# --- 暂时禁用的“干扰项” ---
- godox # 禁用对 TODO, FIXME 注释的检查,让我们能专注于代码
# --- 暂时禁用的“风格/复杂度”检查器 ---
- gocyclo # 暂时不检查圈复杂度
- funlen # 暂时不检查函数长度
- lll # 暂时不检查行长度
- wsl # 检查多余的空格和换行,可以后期再处理
- gocritic # 这个检查器包含很多子项,有些可能过于严格,可以先禁用,或在下面精细配置
# 排除路径:分析这些文件但不报告问题(使用 regex 匹配)
exclusions:
paths:
# 排除 docs/ 目录(匹配路径以 docs/ 开头)
- '^docs/'
# 精细排除规则:用于特定文件/文本的 linter 排除
rules:
# 排除对 main.go 中 log.Fatalf 的抱怨(仅针对 goconst linter
- path: '^main\.go$'
text: "log.Fatalf"
linters:
- goconst
# 你也可以明确启用你认为最重要的检查器,形成一个“白名单”
# enable:
# - govet
# - errcheck
# - staticcheck
# - unused
# - gosimple
# - ineffassign
# - typecheck
run:
# 完全跳过测试文件分析(不解析、不报告任何问题)
tests: false

View File

@@ -13,6 +13,7 @@ help:
@echo " swag Generate swagger docs"
@echo " help Show this help message"
@echo " proto Generate protobuf files"
@echo " lint Lint the code"
# 运行应用
.PHONY: run
@@ -44,4 +45,9 @@ swag:
# 生成protobuf文件
.PHONY: proto
proto:
protoc --go_out=internal/app/service/device/proto --go_opt=paths=source_relative --go-grpc_out=internal/app/service/device/proto --go-grpc_opt=paths=source_relative -Iinternal/app/service/device/proto internal/app/service/device/proto/device.proto
protoc --go_out=internal/domain/device/proto --go_opt=paths=source_relative --go-grpc_out=internal/domain/device/proto --go-grpc_opt=paths=source_relative -Iinternal/domain/device/proto internal/domain/device/proto/device.proto
# 运行代码检查
.PHONY: lint
lint:
golangci-lint run ./...

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,13 +17,15 @@ import (
_ "git.huangwc.com/pig/pig-farm-controller/docs" // 引入 swag 生成的 docs
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/management"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user"
"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
@@ -45,7 +47,9 @@ type API struct {
userController *user.Controller // 用户控制器实例
deviceController *device.Controller // 设备控制器实例
planController *plan.Controller // 计划控制器实例
listenHandler transport.ListenHandler // 设备上行事件监听器
pigFarmController *management.PigFarmController // 猪场管理控制器实例
pigBatchController *management.PigBatchController // 猪群控制器实例
listenHandler webhook.ListenHandler // 设备上行事件监听器
analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例
}
@@ -58,10 +62,12 @@ func NewAPI(cfg config.ServerConfig,
areaControllerRepository repository.AreaControllerRepository,
deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库
planRepository repository.PlanRepository,
pigFarmService service.PigFarmService,
pigBatchService service.PigBatchService, // 添加猪群服务
userActionLogRepository repository.UserActionLogRepository,
tokenService token.TokenService,
auditService audit.Service, // 注入审计服务
listenHandler transport.ListenHandler,
listenHandler webhook.ListenHandler,
analysisTaskManager *task.AnalysisPlanTaskManager) *API {
// 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式)
// 从配置中获取 Gin 模式
@@ -90,6 +96,10 @@ func NewAPI(cfg config.ServerConfig,
deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, logger),
// 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员
planController: plan.NewController(logger, planRepository, analysisTaskManager),
// 在 NewAPI 中初始化猪场管理控制器
pigFarmController: management.NewPigFarmController(logger, pigFarmService),
// 在 NewAPI 中初始化猪群控制器
pigBatchController: management.NewPigBatchController(logger, pigBatchService),
}
api.setupRoutes() // 设置所有路由
@@ -104,97 +114,143 @@ func (a *API) setupRoutes() {
// 这些路由不需要身份验证
// 用户注册和登录
a.engine.POST("/api/v1/users", a.userController.CreateUser)
a.engine.POST("/api/v1/users/login", a.userController.Login)
a.engine.POST("/api/v1/users", a.userController.CreateUser) // 注册新用户
a.engine.POST("/api/v1/users/login", a.userController.Login) // 用户登录
a.logger.Info("公开接口注册成功:用户注册、登录")
// 注册 pprof 路由
pprofGroup := a.engine.Group("/debug/pprof")
{
pprofGroup.GET("/", gin.WrapF(pprof.Index))
pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline))
pprofGroup.GET("/profile", gin.WrapF(pprof.Profile))
pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol))
pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol))
pprofGroup.GET("/trace", gin.WrapF(pprof.Trace))
pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs")))
pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block")))
pprofGroup.GET("/", gin.WrapF(pprof.Index)) // pprof 索引页
pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline)) // pprof 命令行参数
pprofGroup.GET("/profile", gin.WrapF(pprof.Profile)) // pprof CPU profile
pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol)) // pprof 符号查找 (POST)
pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol)) // pprof 符号查找 (GET)
pprofGroup.GET("/trace", gin.WrapF(pprof.Trace)) // pprof 跟踪
pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs"))) // pprof 内存分配
pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block"))) // pprof 阻塞
pprofGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap")))
pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex")))
pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap"))) // pprof 堆内存
pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex"))) // pprof 互斥锁
pprofGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
}
a.logger.Info("pprof 接口注册成功")
// 上行事件监听路由
a.engine.POST("/upstream", func(c *gin.Context) {
h := a.listenHandler.Handler()
h.ServeHTTP(c.Writer, c.Request)
})
a.engine.POST("/upstream", gin.WrapH(a.listenHandler.Handler())) // 处理设备上行事件
a.logger.Info("上行事件监听接口注册成功")
// 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到
a.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
a.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // Swagger UI 接口
a.logger.Info("Swagger UI 接口注册成功")
// --- Authenticated Routes ---
// 所有在此注册的路由都需要通过 JWT 身份验证
authGroup := a.engine.Group("/api/v1")
authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证
authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志
authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证中间件
authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志中间件
{
// 用户相关路由组
userGroup := authGroup.Group("/users")
{
userGroup.GET("/:id/history", a.userController.ListUserHistory)
userGroup.GET("/:id/history", a.userController.ListUserHistory) // 获取用户操作历史
}
a.logger.Info("用户相关接口注册成功 (需要认证和审计)")
// 设备相关路由组
deviceGroup := authGroup.Group("/devices")
{
deviceGroup.POST("", a.deviceController.CreateDevice)
deviceGroup.GET("", a.deviceController.ListDevices)
deviceGroup.GET("/:id", a.deviceController.GetDevice)
deviceGroup.PUT("/:id", a.deviceController.UpdateDevice)
deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice)
deviceGroup.POST("", a.deviceController.CreateDevice) // 创建设备
deviceGroup.GET("", a.deviceController.ListDevices) // 获取设备列表
deviceGroup.GET("/:id", a.deviceController.GetDevice) // 获取单个设备
deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) // 更新设备
deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) // 删除设备
}
a.logger.Info("设备相关接口注册成功 (需要认证和审计)")
// 区域主控相关路由组
areaControllerGroup := authGroup.Group("/area-controllers")
{
areaControllerGroup.POST("", a.deviceController.CreateAreaController)
areaControllerGroup.GET("", a.deviceController.ListAreaControllers)
areaControllerGroup.GET("/:id", a.deviceController.GetAreaController)
areaControllerGroup.PUT("/:id", a.deviceController.UpdateAreaController)
areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController)
areaControllerGroup.POST("", a.deviceController.CreateAreaController) // 创建区域主控
areaControllerGroup.GET("", a.deviceController.ListAreaControllers) // 获取区域主控列表
areaControllerGroup.GET("/:id", a.deviceController.GetAreaController) // 获取单个区域主控
areaControllerGroup.PUT("/:id", a.deviceController.UpdateAreaController) // 更新区域主控
areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController) // 删除区域主控
}
a.logger.Info("区域主控相关接口注册成功 (需要认证和审计)")
// 设备模板相关路由组
deviceTemplateGroup := authGroup.Group("/device-templates")
{
deviceTemplateGroup.POST("", a.deviceController.CreateDeviceTemplate)
deviceTemplateGroup.GET("", a.deviceController.ListDeviceTemplates)
deviceTemplateGroup.GET("/:id", a.deviceController.GetDeviceTemplate)
deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate)
deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate)
deviceTemplateGroup.POST("", a.deviceController.CreateDeviceTemplate) // 创建设备模板
deviceTemplateGroup.GET("", a.deviceController.ListDeviceTemplates) // 获取设备模板列表
deviceTemplateGroup.GET("/:id", a.deviceController.GetDeviceTemplate) // 获取单个设备模板
deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate) // 更新设备模板
deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate) // 删除设备模板
}
a.logger.Info("设备模板相关接口注册成功 (需要认证和审计)")
// 计划相关路由组
planGroup := authGroup.Group("/plans")
{
planGroup.POST("", a.planController.CreatePlan)
planGroup.GET("", a.planController.ListPlans)
planGroup.GET("/:id", a.planController.GetPlan)
planGroup.PUT("/:id", a.planController.UpdatePlan)
planGroup.DELETE("/:id", a.planController.DeletePlan)
planGroup.POST("/:id/start", a.planController.StartPlan)
planGroup.POST("/:id/stop", a.planController.StopPlan)
planGroup.POST("", a.planController.CreatePlan) // 创建计划
planGroup.GET("", a.planController.ListPlans) // 获取计划列表
planGroup.GET("/:id", a.planController.GetPlan) // 获取单个计划
planGroup.PUT("/:id", a.planController.UpdatePlan) // 更新计划
planGroup.DELETE("/:id", a.planController.DeletePlan) // 删除计划
planGroup.POST("/:id/start", a.planController.StartPlan) // 启动计划
planGroup.POST("/:id/stop", a.planController.StopPlan) // 停止计划
}
a.logger.Info("计划相关接口注册成功 (需要认证和审计)")
// 猪舍相关路由组
pigHouseGroup := authGroup.Group("/pig-houses")
{
pigHouseGroup.POST("", a.pigFarmController.CreatePigHouse) // 创建猪舍
pigHouseGroup.GET("", a.pigFarmController.ListPigHouses) // 获取猪舍列表
pigHouseGroup.GET("/:id", a.pigFarmController.GetPigHouse) // 获取单个猪舍
pigHouseGroup.PUT("/:id", a.pigFarmController.UpdatePigHouse) // 更新猪舍
pigHouseGroup.DELETE("/:id", a.pigFarmController.DeletePigHouse) // 删除猪舍
}
a.logger.Info("猪舍相关接口注册成功 (需要认证和审计)")
// 猪圈相关路由组
penGroup := authGroup.Group("/pens")
{
penGroup.POST("", a.pigFarmController.CreatePen) // 创建猪圈
penGroup.GET("", a.pigFarmController.ListPens) // 获取猪圈列表
penGroup.GET("/:id", a.pigFarmController.GetPen) // 获取单个猪圈
penGroup.PUT("/:id", a.pigFarmController.UpdatePen) // 更新猪圈
penGroup.DELETE("/:id", a.pigFarmController.DeletePen) // 删除猪圈
penGroup.PUT("/:id/status", a.pigFarmController.UpdatePenStatus) // 更新猪圈状态
}
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) // 删除猪群
pigBatchGroup.POST("/:id/assign-pens", a.pigBatchController.AssignEmptyPensToBatch) // 为猪群分配空栏
pigBatchGroup.POST("/:fromBatchID/reclassify-pen", a.pigBatchController.ReclassifyPenToNewBatch) // 将猪栏划拨到新群
pigBatchGroup.DELETE("/:batchID/remove-pen/:penID", a.pigBatchController.RemoveEmptyPenFromBatch) // 从猪群移除空栏
pigBatchGroup.POST("/:id/move-pigs-into-pen", a.pigBatchController.MovePigsIntoPen) // 将猪只从“虚拟库存”移入指定猪栏
pigBatchGroup.POST("/:id/sell-pigs", a.pigBatchController.SellPigs) // 处理卖猪业务
pigBatchGroup.POST("/:id/buy-pigs", a.pigBatchController.BuyPigs) // 处理买猪业务
pigBatchGroup.POST("/:sourceBatchID/transfer-across-batches", a.pigBatchController.TransferPigsAcrossBatches) // 跨猪群调栏
pigBatchGroup.POST("/:id/transfer-within-batch", a.pigBatchController.TransferPigsWithinBatch) // 群内调栏
pigBatchGroup.POST("/:id/record-sick-pigs", a.pigBatchController.RecordSickPigs) // 记录新增病猪事件
pigBatchGroup.POST("/:id/record-sick-pig-recovery", a.pigBatchController.RecordSickPigRecovery) // 记录病猪康复事件
pigBatchGroup.POST("/:id/record-sick-pig-death", a.pigBatchController.RecordSickPigDeath) // 记录病猪死亡事件
pigBatchGroup.POST("/:id/record-sick-pig-cull", a.pigBatchController.RecordSickPigCull) // 记录病猪淘汰事件
pigBatchGroup.POST("/:id/record-death", a.pigBatchController.RecordDeath) // 记录正常猪只死亡事件
pigBatchGroup.POST("/:id/record-cull", a.pigBatchController.RecordCull) // 记录正常猪只淘汰事件
}
a.logger.Info("猪群相关接口注册成功 (需要认证和审计)")
}
}

View File

@@ -0,0 +1,47 @@
package controller
import (
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/gin-gonic/gin"
)
var (
// ErrUserNotFoundInContext 表示在 gin.Context 中未找到用户信息。
ErrUserNotFoundInContext = errors.New("context中未找到用户信息")
// ErrInvalidUserType 表示从 gin.Context 中获取的用户信息类型不正确。
ErrInvalidUserType = errors.New("context中用户信息类型不正确")
)
// GetOperatorIDFromContext 从 gin.Context 中提取操作者ID。
// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 ID 字段。
func GetOperatorIDFromContext(c *gin.Context) (uint, error) {
userVal, exists := c.Get(models.ContextUserKey.String())
if !exists {
return 0, ErrUserNotFoundInContext
}
user, ok := userVal.(*models.User)
if !ok {
return 0, ErrInvalidUserType
}
return user.ID, nil
}
// GetOperatorFromContext 从 gin.Context 中提取操作者。
// 假设操作者是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 字段。
func GetOperatorFromContext(c *gin.Context) (*models.User, error) {
userVal, exists := c.Get(models.ContextUserKey.String())
if !exists {
return nil, ErrUserNotFoundInContext
}
user, ok := userVal.(*models.User)
if !ok {
return nil, ErrInvalidUserType
}
return user, nil
}

View File

@@ -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)

View File

@@ -26,7 +26,7 @@ type MockDeviceRepository struct {
mock.Mock
}
// Create 模拟 DeviceRepository 的 Create 方法
// CreateTx 模拟 DeviceRepository 的 CreateTx 方法
func (m *MockDeviceRepository) Create(device *models.Device) error {
args := m.Called(device)
return args.Error(0)
@@ -169,7 +169,7 @@ func TestCreateDevice(t *testing.T) {
Properties: controller.Properties(`{"lora_address":"0x1234"}`),
},
mockRepoSetup: func(m *MockDeviceRepository) {
m.On("Create", mock.MatchedBy(func(dev *models.Device) bool {
m.On("CreateTx", mock.MatchedBy(func(dev *models.Device) bool {
// 检查 Name 字段
nameMatch := dev.Name == "主控A"
// 检查 Type 字段
@@ -215,7 +215,7 @@ func TestCreateDevice(t *testing.T) {
Properties: controller.Properties(`{"bus_id":1,"bus_address":10}`),
},
mockRepoSetup: func(m *MockDeviceRepository) {
m.On("Create", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
m.On("CreateTx", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
arg := args.Get(0).(*models.Device)
arg.ID = 2
arg.CreatedAt = time.Now()
@@ -259,7 +259,7 @@ func TestCreateDevice(t *testing.T) {
Type: models.DeviceTypeDevice,
},
mockRepoSetup: func(m *MockDeviceRepository) {
m.On("Create", mock.Anything).Return(errors.New("db error")).Once()
m.On("CreateTx", mock.Anything).Return(errors.New("db error")).Once()
},
expectedStatus: http.StatusOK,
expectedCode: controller.CodeInternalError,
@@ -276,9 +276,9 @@ func TestCreateDevice(t *testing.T) {
Properties: controller.Properties(`{invalid json}`),
},
mockRepoSetup: func(m *MockDeviceRepository) {
// 期望 Create 方法被调用,并返回一个模拟的数据库错误
// 期望 CreateTx 方法被调用,并返回一个模拟的数据库错误
// 这个错误模拟的是数据库层因为 Properties 字段的 JSON 格式无效而拒绝保存
m.On("Create", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
m.On("CreateTx", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
dev := args.Get(0).(*models.Device)
assert.Equal(t, "无效JSON设备", dev.Name)
assert.Equal(t, models.DeviceTypeDevice, dev.Type)

View File

@@ -0,0 +1,238 @@
package management
import (
"errors"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"github.com/gin-gonic/gin"
)
// mapAndSendError 统一映射服务层错误并发送响应。
// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。
func mapAndSendError(c *PigBatchController, ctx *gin.Context, action string, err error, id uint) {
if errors.Is(err, service.ErrPigBatchNotFound) ||
errors.Is(err, service.ErrPenNotFound) ||
errors.Is(err, service.ErrPenNotAssociatedWithBatch) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
} else if errors.Is(err, service.ErrInvalidOperation) ||
errors.Is(err, service.ErrPigBatchActive) ||
errors.Is(err, service.ErrPigBatchNotActive) ||
errors.Is(err, service.ErrPenOccupiedByOtherBatch) ||
errors.Is(err, service.ErrPenStatusInvalidForAllocation) ||
errors.Is(err, service.ErrPenNotEmpty) {
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
} else {
c.logger.Errorf("操作[%s]业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "操作失败", action, err.Error(), id)
}
}
// idExtractorFunc 定义了一个函数类型用于从gin.Context中提取主ID。
type idExtractorFunc func(ctx *gin.Context) (uint, error)
// extractOperatorAndPrimaryID 封装了从gin.Context中提取操作员ID和主ID的通用逻辑。
// 它负责处理ID提取过程中的错误并发送相应的HTTP响应。
//
// 参数:
//
// c: *PigBatchController - 控制器实例,用于访问其日志。
// ctx: *gin.Context - Gin上下文。
// action: string - 当前操作的描述,用于日志和审计。
// idExtractor: idExtractorFunc - 可选函数用于从ctx中提取主ID。如果为nil则尝试从":id"路径参数中提取。
//
// 返回值:
//
// operatorID: uint - 提取到的操作员ID。
// primaryID: uint - 提取到的主ID。
// ok: bool - 如果ID提取成功且没有发送错误响应则为true。
func extractOperatorAndPrimaryID(
c *PigBatchController,
ctx *gin.Context,
action string,
idExtractor idExtractorFunc,
) (operatorID uint, primaryID uint, ok bool) {
// 1. 获取操作员ID
operatorID, err := controller.GetOperatorIDFromContext(ctx)
if err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
return 0, 0, false
}
// 2. 提取主ID
if idExtractor != nil {
primaryID, err = idExtractor(ctx)
if err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error())
return 0, 0, false
}
} else { // 默认从 ":id" 路径参数提取
idParam := ctx.Param("id")
if idParam == "" { // 有些端点可能没有 "id" 参数,例如列表或创建操作
// 如果没有ID参数且没有自定义提取器primaryID保持为0这对于某些操作是可接受的
} else {
parsedID, err := strconv.ParseUint(idParam, 10, 32)
if err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam)
return 0, 0, false
}
primaryID = uint(parsedID)
}
}
return operatorID, primaryID, true
}
// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。
// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。
func handleAPIRequest[Req any](
c *PigBatchController,
ctx *gin.Context,
action string,
reqDTO Req,
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error,
successMsg string,
idExtractor idExtractorFunc,
) {
// 1. 绑定请求体
if err := ctx.ShouldBindJSON(&reqDTO); err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO)
return
}
// 2. 提取操作员ID和主ID
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
if !ok {
return // 错误已在 extractOperatorAndPrimaryID 中处理
}
// 3. 执行服务层逻辑
err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
if err != nil {
mapAndSendError(c, ctx, action, err, primaryID)
return
}
// 4. 发送成功响应
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
}
// handleNoBodyAPIRequest 封装了处理不带请求体但有路径参数和操作员ID的API请求的通用逻辑。
func handleNoBodyAPIRequest(
c *PigBatchController,
ctx *gin.Context,
action string,
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) error,
successMsg string,
idExtractor idExtractorFunc,
) {
// 1. 提取操作员ID和主ID
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
if !ok {
return // 错误已在 extractOperatorAndPrimaryID 中处理
}
// 2. 执行服务层逻辑
err := serviceExecutor(ctx, operatorID, primaryID)
if err != nil {
mapAndSendError(c, ctx, action, err, primaryID)
return
}
// 3. 发送成功响应
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
}
// handleAPIRequestWithResponse 封装了控制器中处理带有请求体、路径参数并返回响应DTO的API请求的通用逻辑。
func handleAPIRequestWithResponse[Req any, Resp any](
c *PigBatchController,
ctx *gin.Context,
action string,
reqDTO Req,
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) (Resp, error), // serviceExecutor现在返回Resp
successMsg string,
idExtractor idExtractorFunc,
) {
// 1. 绑定请求体
if err := ctx.ShouldBindJSON(&reqDTO); err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO)
return
}
// 2. 提取操作员ID和主ID
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
if !ok {
return // 错误已在 extractOperatorAndPrimaryID 中处理
}
// 3. 执行服务层逻辑
respDTO, err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
if err != nil {
mapAndSendError(c, ctx, action, err, primaryID)
return
}
// 4. 发送成功响应
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
}
// handleNoBodyAPIRequestWithResponse 封装了处理不带请求体但有路径参数和操作员ID并返回响应DTO的API请求的通用逻辑。
func handleNoBodyAPIRequestWithResponse[Resp any](
c *PigBatchController,
ctx *gin.Context,
action string,
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) (Resp, error), // serviceExecutor现在返回Resp
successMsg string,
idExtractor idExtractorFunc,
) {
// 1. 提取操作员ID和主ID
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
if !ok {
return // 错误已在 extractOperatorAndPrimaryID 中处理
}
// 2. 执行服务层逻辑
respDTO, err := serviceExecutor(ctx, operatorID, primaryID)
if err != nil {
mapAndSendError(c, ctx, action, err, primaryID)
return
}
// 3. 发送成功响应
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
}
// handleQueryAPIRequestWithResponse 封装了处理带有查询参数并返回响应DTO的API请求的通用逻辑。
func handleQueryAPIRequestWithResponse[Query any, Resp any](
c *PigBatchController,
ctx *gin.Context,
action string,
queryDTO Query,
serviceExecutor func(ctx *gin.Context, operatorID uint, query Query) (Resp, error), // serviceExecutor现在接收queryDTO
successMsg string,
) {
// 1. 绑定查询参数
if err := ctx.ShouldBindQuery(&queryDTO); err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", queryDTO)
return
}
// 2. 获取操作员ID
operatorID, err := controller.GetOperatorIDFromContext(ctx)
if err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
return
}
// 3. 执行服务层逻辑
respDTO, err := serviceExecutor(ctx, operatorID, queryDTO)
if err != nil {
// 对于列表查询通常没有primaryID所以传递0
mapAndSendError(c, ctx, action, err, 0)
return
}
// 4. 发送成功响应
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, nil)
}

View File

@@ -0,0 +1 @@
package management

View File

@@ -0,0 +1 @@
package management

View File

@@ -0,0 +1,251 @@
package management
import (
"strconv"
"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} "创建成功"
// @Router /api/v1/pig-batches [post]
func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) {
const action = "创建猪批次"
var req dto.PigBatchCreateDTO
handleAPIRequestWithResponse(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
// 对于创建操作primaryID通常不从路径中获取而是由服务层生成
return c.service.CreatePigBatch(operatorID, req)
},
"创建成功",
nil, // 无需自定义ID提取器primaryID将为0
)
}
// GetPigBatch godoc
// @Summary 获取单个猪批次
// @Description 根据ID获取单个猪批次信息
// @Tags 猪群管理
// @Produce json
// @Param id path int true "猪批次ID"
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功"
// @Router /api/v1/pig-batches/{id} [get]\
func (c *PigBatchController) GetPigBatch(ctx *gin.Context) {
const action = "获取猪批次"
handleNoBodyAPIRequestWithResponse(
c, ctx, action,
func(ctx *gin.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) {
return c.service.GetPigBatch(primaryID)
},
"获取成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// 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} "更新成功"
// @Router /api/v1/pig-batches/{id} [put]
func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) {
const action = "更新猪批次"
var req dto.PigBatchUpdateDTO
handleAPIRequestWithResponse(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
return c.service.UpdatePigBatch(primaryID, req)
},
"更新成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// DeletePigBatch godoc
// @Summary 删除猪批次
// @Description 根据ID删除一个猪批次
// @Tags 猪群管理
// @Produce json
// @Param id path int true "猪批次ID"
// @Success 200 {object} controller.Response "删除成功"
// @Router /api/v1/pig-batches/{id} [delete]
func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) {
const action = "删除猪批次"
handleNoBodyAPIRequest(
c, ctx, action,
func(ctx *gin.Context, operatorID uint, primaryID uint) error {
return c.service.DeletePigBatch(primaryID)
},
"删除成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// ListPigBatches godoc
// @Summary 获取猪批次列表
// @Description 获取所有猪批次的列表,支持按活跃状态筛选
// @Tags 猪群管理
// @Produce json
// @Param is_active query bool false "是否活跃 (true/false)"
// @Success 200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功"
// @Router /api/v1/pig-batches [get]
func (c *PigBatchController) ListPigBatches(ctx *gin.Context) {
const action = "获取猪批次列表"
var query dto.PigBatchQueryDTO
handleQueryAPIRequestWithResponse(
c, ctx, action, &query,
func(ctx *gin.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
return c.service.ListPigBatches(query.IsActive)
},
"获取成功",
)
}
// AssignEmptyPensToBatch godoc
// @Summary 为猪批次分配空栏
// @Description 将一个或多个空闲猪栏分配给指定的猪批次
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.AssignEmptyPensToBatchRequest true "待分配的猪栏ID列表"
// @Success 200 {object} controller.Response "分配成功"
// @Router /api/v1/pig-batches/{id}/assign-pens [post]
func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) {
const action = "为猪批次分配空栏"
var req dto.AssignEmptyPensToBatchRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error {
return c.service.AssignEmptyPensToBatch(primaryID, req.PenIDs, operatorID)
},
"分配成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// ReclassifyPenToNewBatch godoc
// @Summary 将猪栏划拨到新批次
// @Description 将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param fromBatchID path int true "源猪批次ID"
// @Param body body dto.ReclassifyPenToNewBatchRequest true "划拨请求信息 (包含目标批次ID、猪栏ID和备注)"
// @Success 200 {object} controller.Response "划拨成功"
// @Router /api/v1/pig-batches/{fromBatchID}/reclassify-pen [post]
func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) {
const action = "划拨猪栏到新批次"
var req dto.ReclassifyPenToNewBatchRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error {
// primaryID 在这里是 fromBatchID
return c.service.ReclassifyPenToNewBatch(primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks)
},
"划拨成功",
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器从 ":fromBatchID" 路径参数提取
idParam := ctx.Param("fromBatchID")
parsedID, err := strconv.ParseUint(idParam, 10, 32)
if err != nil {
return 0, err
}
return uint(parsedID), nil
},
)
}
// RemoveEmptyPenFromBatch godoc
// @Summary 从猪批次移除空栏
// @Description 将一个空闲猪栏从指定的猪批次中移除
// @Tags 猪群管理
// @Produce json
// @Param batchID path int true "猪批次ID"
// @Param penID path int true "待移除的猪栏ID"
// @Success 200 {object} controller.Response "移除成功"
// @Router /api/v1/pig-batches/{batchID}/remove-pen/{penID} [delete]
func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) {
const action = "从猪批次移除空栏"
handleNoBodyAPIRequest(
c, ctx, action,
func(ctx *gin.Context, operatorID uint, primaryID uint) error {
// primaryID 在这里是 batchID
penIDParam := ctx.Param("penID")
penID, err := strconv.ParseUint(penIDParam, 10, 32)
if err != nil {
return err // 返回错误,因为 penID 格式无效
}
return c.service.RemoveEmptyPenFromBatch(primaryID, uint(penID))
},
"移除成功",
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器从 ":batchID" 路径参数提取
idParam := ctx.Param("batchID")
parsedID, err := strconv.ParseUint(idParam, 10, 32)
if err != nil {
return 0, err
}
return uint(parsedID), nil
},
)
}
// MovePigsIntoPen godoc
// @Summary 将猪只从“虚拟库存”移入指定猪栏
// @Description 将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.MovePigsIntoPenRequest true "移入猪只请求信息 (包含目标猪栏ID、数量和备注)"
// @Success 200 {object} controller.Response "移入成功"
// @Router /api/v1/pig-batches/{id}/move-pigs-into-pen [post]
func (c *PigBatchController) MovePigsIntoPen(ctx *gin.Context) {
const action = "将猪只移入猪栏"
var req dto.MovePigsIntoPenRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error {
return c.service.MovePigsIntoPen(primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
},
"移入成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}

View File

@@ -0,0 +1,150 @@
package management
import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"github.com/gin-gonic/gin"
)
// RecordSickPigs godoc
// @Summary 记录新增病猪事件
// @Description 记录猪批次中新增病猪的数量、治疗地点和发生时间
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.RecordSickPigsRequest true "记录病猪请求信息"
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/{id}/record-sick-pigs [post]
func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) {
const action = "记录新增病猪事件"
var req dto.RecordSickPigsRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
return c.service.RecordSickPigs(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// RecordSickPigRecovery godoc
// @Summary 记录病猪康复事件
// @Description 记录猪批次中病猪康复的数量、治疗地点和发生时间
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.RecordSickPigRecoveryRequest true "记录病猪康复请求信息"
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/{id}/record-sick-pig-recovery [post]
func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) {
const action = "记录病猪康复事件"
var req dto.RecordSickPigRecoveryRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
return c.service.RecordSickPigRecovery(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// RecordSickPigDeath godoc
// @Summary 记录病猪死亡事件
// @Description 记录猪批次中病猪死亡的数量、治疗地点和发生时间
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.RecordSickPigDeathRequest true "记录病猪死亡请求信息"
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/{id}/record-sick-pig-death [post]
func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) {
const action = "记录病猪死亡事件"
var req dto.RecordSickPigDeathRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
return c.service.RecordSickPigDeath(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// RecordSickPigCull godoc
// @Summary 记录病猪淘汰事件
// @Description 记录猪批次中病猪淘汰的数量、治疗地点和发生时间
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.RecordSickPigCullRequest true "记录病猪淘汰请求信息"
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/{id}/record-sick-pig-cull [post]
func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) {
const action = "记录病猪淘汰事件"
var req dto.RecordSickPigCullRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
return c.service.RecordSickPigCull(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// RecordDeath godoc
// @Summary 记录正常猪只死亡事件
// @Description 记录猪批次中正常猪只死亡的数量和发生时间
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.RecordDeathRequest true "记录正常猪只死亡请求信息"
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/{id}/record-death [post]
func (c *PigBatchController) RecordDeath(ctx *gin.Context) {
const action = "记录正常猪只死亡事件"
var req dto.RecordDeathRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
return c.service.RecordDeath(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// RecordCull godoc
// @Summary 记录正常猪只淘汰事件
// @Description 记录猪批次中正常猪只淘汰的数量和发生时间
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.RecordCullRequest true "记录正常猪只淘汰请求信息"
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/{id}/record-cull [post]
func (c *PigBatchController) RecordCull(ctx *gin.Context) {
const action = "记录正常猪只淘汰事件"
var req dto.RecordCullRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
return c.service.RecordCull(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}

View File

@@ -0,0 +1,54 @@
package management
import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"github.com/gin-gonic/gin"
)
// SellPigs godoc
// @Summary 处理卖猪的业务逻辑
// @Description 记录猪批次中的猪只出售事件
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.SellPigsRequest true "卖猪请求信息"
// @Success 200 {object} controller.Response "卖猪成功"
// @Router /api/v1/pig-batches/{id}/sell-pigs [post]
func (c *PigBatchController) SellPigs(ctx *gin.Context) {
const action = "卖猪"
var req dto.SellPigsRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
return c.service.SellPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
},
"卖猪成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}
// BuyPigs godoc
// @Summary 处理买猪的业务逻辑
// @Description 记录猪批次中的猪只购买事件
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.BuyPigsRequest true "买猪请求信息"
// @Success 200 {object} controller.Response "买猪成功"
// @Router /api/v1/pig-batches/{id}/buy-pigs [post]
func (c *PigBatchController) BuyPigs(ctx *gin.Context) {
const action = "买猪"
var req dto.BuyPigsRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
return c.service.BuyPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
},
"买猪成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}

View File

@@ -0,0 +1,65 @@
package management
import (
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"github.com/gin-gonic/gin"
)
// TransferPigsAcrossBatches godoc
// @Summary 跨猪群调栏
// @Description 将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param sourceBatchID path int true "源猪批次ID"
// @Param body body dto.TransferPigsAcrossBatchesRequest true "跨群调栏请求信息"
// @Success 200 {object} controller.Response "调栏成功"
// @Router /api/v1/pig-batches/{sourceBatchID}/transfer-across-batches [post]
func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) {
const action = "跨猪群调栏"
var req dto.TransferPigsAcrossBatchesRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
// primaryID 在这里是 sourceBatchID
return c.service.TransferPigsAcrossBatches(primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
},
"调栏成功",
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器从 ":sourceBatchID" 路径参数提取
idParam := ctx.Param("sourceBatchID")
parsedID, err := strconv.ParseUint(idParam, 10, 32)
if err != nil {
return 0, err
}
return uint(parsedID), nil
},
)
}
// TransferPigsWithinBatch godoc
// @Summary 群内调栏
// @Description 将指定数量的猪只在同一个猪群的不同猪栏间调动
// @Tags 猪群管理
// @Accept json
// @Produce json
// @Param id path int true "猪批次ID"
// @Param body body dto.TransferPigsWithinBatchRequest true "群内调栏请求信息"
// @Success 200 {object} controller.Response "调栏成功"
// @Router /api/v1/pig-batches/{id}/transfer-within-batch [post]
func (c *PigBatchController) TransferPigsWithinBatch(ctx *gin.Context) {
const action = "群内调栏"
var req dto.TransferPigsWithinBatchRequest
handleAPIRequest(
c, ctx, action, &req,
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
// primaryID 在这里是 batchID
return c.service.TransferPigsWithinBatch(primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
},
"调栏成功",
nil, // 默认从 ":id" 路径参数提取ID
)
}

View File

@@ -0,0 +1 @@
package management

View File

@@ -0,0 +1,443 @@
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"
)
// --- 控制器定义 ---
// PigFarmController 负责处理猪舍和猪栏相关的API请求
type PigFarmController struct {
logger *logs.Logger
service service.PigFarmService
}
// NewPigFarmController 创建一个新的 PigFarmController 实例
func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *PigFarmController {
return &PigFarmController{
logger: logger,
service: service,
}
}
// --- 猪舍 (PigHouse) API 实现 ---
// CreatePigHouse godoc
// @Summary 创建猪舍
// @Description 创建一个新的猪舍
// @Tags 猪场管理
// @Accept json
// @Produce json
// @Param body body dto.CreatePigHouseRequest true "猪舍信息"
// @Success 201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功"
// @Router /api/v1/pig-houses [post]
func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
const action = "创建猪舍"
var req dto.CreatePigHouseRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
return
}
house, err := c.service.CreatePigHouse(req.Name, req.Description)
if err != nil {
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req)
return
}
resp := dto.PigHouseResponse{
ID: house.ID,
Name: house.Name,
Description: house.Description,
}
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
}
// GetPigHouse godoc
// @Summary 获取单个猪舍
// @Description 根据ID获取单个猪舍信息
// @Tags 猪场管理
// @Produce json
// @Param id path int true "猪舍ID"
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功"
// @Router /api/v1/pig-houses/{id} [get]
func (c *PigFarmController) GetPigHouse(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
}
house, err := c.service.GetPigHouseByID(uint(id))
if err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
return
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id)
return
}
resp := dto.PigHouseResponse{
ID: house.ID,
Name: house.Name,
Description: house.Description,
}
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
}
// ListPigHouses godoc
// @Summary 获取猪舍列表
// @Description 获取所有猪舍的列表
// @Tags 猪场管理
// @Produce json
// @Success 200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功"
// @Router /api/v1/pig-houses [get]
func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
const action = "获取猪舍列表"
houses, err := c.service.ListPigHouses()
if err != nil {
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
return
}
var resp []dto.PigHouseResponse
for _, house := range houses {
resp = append(resp, dto.PigHouseResponse{
ID: house.ID,
Name: house.Name,
Description: house.Description,
})
}
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
}
// UpdatePigHouse godoc
// @Summary 更新猪舍
// @Description 更新一个已存在的猪舍信息
// @Tags 猪场管理
// @Accept json
// @Produce json
// @Param id path int true "猪舍ID"
// @Param body body dto.UpdatePigHouseRequest true "猪舍信息"
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功"
// @Router /api/v1/pig-houses/{id} [put]
func (c *PigFarmController) UpdatePigHouse(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.UpdatePigHouseRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
return
}
house, err := c.service.UpdatePigHouse(uint(id), req.Name, req.Description)
if err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
return
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
return
}
resp := dto.PigHouseResponse{
ID: house.ID,
Name: house.Name,
Description: house.Description,
}
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
}
// DeletePigHouse godoc
// @Summary 删除猪舍
// @Description 根据ID删除一个猪舍
// @Tags 猪场管理
// @Produce json
// @Param id path int true "猪舍ID"
// @Success 200 {object} controller.Response "删除成功"
// @Router /api/v1/pig-houses/{id} [delete]
func (c *PigFarmController) DeletePigHouse(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.DeletePigHouse(uint(id)); err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
return
}
// 检查是否是业务逻辑错误
if errors.Is(err, service.ErrHouseContainsPens) {
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), 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)
}
// --- 猪栏 (Pen) API 实现 ---
// CreatePen godoc
// @Summary 创建猪栏
// @Description 创建一个新的猪栏
// @Tags 猪场管理
// @Accept json
// @Produce json
// @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 dto.CreatePenRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
return
}
pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity)
if err != nil {
// 检查是否是业务逻辑错误
if errors.Is(err, service.ErrHouseNotFound) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), req)
return
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req)
return
}
resp := dto.PenResponse{
ID: pen.ID,
PenNumber: pen.PenNumber,
HouseID: pen.HouseID,
Capacity: pen.Capacity,
Status: pen.Status,
PigBatchID: *pen.PigBatchID,
}
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
}
// GetPen godoc
// @Summary 获取单个猪栏
// @Description 根据ID获取单个猪栏信息
// @Tags 猪场管理
// @Produce json
// @Param id path int true "猪栏ID"
// @Success 200 {object} controller.Response{data=dto.PenResponse} "获取成功"
// @Router /api/v1/pens/{id} [get]
func (c *PigFarmController) GetPen(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
}
pen, err := c.service.GetPenByID(uint(id))
if err != nil {
if errors.Is(err, service.ErrPenNotFound) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
return
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id)
return
}
resp := dto.PenResponse{
ID: pen.ID,
PenNumber: pen.PenNumber,
HouseID: pen.HouseID,
Capacity: pen.Capacity,
Status: pen.Status,
PigBatchID: *pen.PigBatchID,
}
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
}
// ListPens godoc
// @Summary 获取猪栏列表
// @Description 获取所有猪栏的列表
// @Tags 猪场管理
// @Produce json
// @Success 200 {object} controller.Response{data=[]dto.PenResponse} "获取成功"
// @Router /api/v1/pens [get]
func (c *PigFarmController) ListPens(ctx *gin.Context) {
const action = "获取猪栏列表"
pens, err := c.service.ListPens()
if err != nil {
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
return
}
var resp []dto.PenResponse
for _, pen := range pens {
resp = append(resp, dto.PenResponse{
ID: pen.ID,
PenNumber: pen.PenNumber,
HouseID: pen.HouseID,
Capacity: pen.Capacity,
Status: pen.Status,
PigBatchID: *pen.PigBatchID,
})
}
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
}
// UpdatePen godoc
// @Summary 更新猪栏
// @Description 更新一个已存在的猪栏信息
// @Tags 猪场管理
// @Accept json
// @Produce json
// @Param id path int true "猪栏ID"
// @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 = "更新猪栏"
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.UpdatePenRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
return
}
pen, err := c.service.UpdatePen(uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
if err != nil {
if errors.Is(err, service.ErrPenNotFound) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
return
}
// 其他业务逻辑错误可以在这里添加处理
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
return
}
resp := dto.PenResponse{
ID: pen.ID,
PenNumber: pen.PenNumber,
HouseID: pen.HouseID,
Capacity: pen.Capacity,
Status: pen.Status,
PigBatchID: *pen.PigBatchID,
}
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
}
// DeletePen godoc
// @Summary 删除猪栏
// @Description 根据ID删除一个猪栏
// @Tags 猪场管理
// @Produce json
// @Param id path int true "猪栏ID"
// @Success 200 {object} controller.Response "删除成功"
// @Router /api/v1/pens/{id} [delete]
func (c *PigFarmController) DeletePen(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.DeletePen(uint(id)); err != nil {
if errors.Is(err, service.ErrPenNotFound) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
return
}
// 检查是否是业务逻辑错误
if errors.Is(err, service.ErrPenInUse) {
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), 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)
}
// UpdatePenStatus godoc
// @Summary 更新猪栏状态
// @Description 更新指定猪栏的当前状态
// @Tags 猪场管理
// @Accept json
// @Produce json
// @Param id path int true "猪栏ID"
// @Param body body dto.UpdatePenStatusRequest true "新的猪栏状态"
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
// @Router /api/v1/pens/{id}/status [put]
func (c *PigFarmController) UpdatePenStatus(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.UpdatePenStatusRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
return
}
pen, err := c.service.UpdatePenStatus(uint(id), req.Status)
if err != nil {
if errors.Is(err, service.ErrPenNotFound) {
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
return
} else if errors.Is(err, service.ErrPenStatusInvalidForOccupiedPen) || errors.Is(err, service.ErrPenStatusInvalidForUnoccupiedPen) {
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
return
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id)
return
}
resp := dto.PenResponse{
ID: pen.ID,
PenNumber: pen.PenNumber,
HouseID: pen.HouseID,
Capacity: pen.Capacity,
Status: pen.Status,
PigBatchID: *pen.PigBatchID,
}
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
}

View File

@@ -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)
})
}

View File

@@ -5,7 +5,8 @@ import (
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
task "git.huangwc.com/pig/pig-farm-controller/internal/app/service/task"
"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"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
@@ -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:"automatic"`
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:"automatic"`
Status models.PlanStatus `json:"status" example:"0"`
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:"tasks"`
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:"automatic"`
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:"waiting"`
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:"waiting"`
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)

View File

@@ -5,7 +5,8 @@ import (
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
"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"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
@@ -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,
}

View File

@@ -25,7 +25,7 @@ type MockUserRepository struct {
mock.Mock
}
// Create 模拟 UserRepository 的 Create 方法
// CreateTx 模拟 UserRepository 的 CreateTx 方法
func (m *MockUserRepository) Create(user *models.User) error {
args := m.Called(user)
return args.Error(0)
@@ -90,8 +90,8 @@ func TestCreateUser(t *testing.T) {
Password: "password123",
},
mockRepoSetup: func(m *MockUserRepository) {
// 模拟 Create 成功
m.On("Create", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) {
// 模拟 CreateTx 成功
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) {
// 模拟数据库自动填充 ID
userArg := args.Get(0).(*models.User)
userArg.ID = 1 // 设置一个非零的 ID
@@ -114,7 +114,7 @@ func TestCreateUser(t *testing.T) {
Password: "123", // 密码少于6位
},
mockRepoSetup: func(m *MockUserRepository) {
// 不会调用 Create 或 FindByUsername
// 不会调用 CreateTx 或 FindByUsername
},
expectedResponse: map[string]interface{}{
"code": float64(controller.CodeBadRequest),
@@ -128,7 +128,7 @@ func TestCreateUser(t *testing.T) {
Password: "password123",
},
mockRepoSetup: func(m *MockUserRepository) {
// 不会调用 Create 或 FindByUsername
// 不会调用 CreateTx 或 FindByUsername
},
expectedResponse: map[string]interface{}{
"code": float64(controller.CodeBadRequest),
@@ -143,8 +143,8 @@ func TestCreateUser(t *testing.T) {
Password: "password123",
},
mockRepoSetup: func(m *MockUserRepository) {
// 模拟 Create 失败,因为用户名已存在
m.On("Create", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once()
// 模拟 CreateTx 失败,因为用户名已存在
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once()
// 模拟 FindByUsername 找到用户,确认是用户名重复
m.On("FindByUsername", "existinguser").Return(&models.User{Username: "existinguser"}, nil).Once()
},
@@ -161,8 +161,8 @@ func TestCreateUser(t *testing.T) {
Password: "password123",
},
mockRepoSetup: func(m *MockUserRepository) {
// 模拟 Create 失败,通用数据库错误
m.On("Create", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once()
// 模拟 CreateTx 失败,通用数据库错误
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once()
// 模拟 FindByUsername 找不到用户,确认不是用户名重复
m.On("FindByUsername", "db_error_user").Return(nil, gorm.ErrRecordNotFound).Once()
},

View 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
}

View 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"`
}

View File

@@ -0,0 +1,160 @@
package dto
import (
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// 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"` // 更新时间
}
// AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体
type AssignEmptyPensToBatchRequest struct {
PenIDs []uint `json:"penIDs" binding:"required,min=1" example:"[1,2,3]"` // 待分配的猪栏ID列表
}
// ReclassifyPenToNewBatchRequest 用于将猪栏划拨到新批次的请求体
type ReclassifyPenToNewBatchRequest struct {
ToBatchID uint `json:"toBatchID" binding:"required"` // 目标猪批次ID
PenID uint `json:"penID" binding:"required"` // 待划拨的猪栏ID
Remarks string `json:"remarks"` // 备注
}
// RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体
type RemoveEmptyPenFromBatchRequest struct {
PenID uint `json:"penID" binding:"required"` // 待移除的猪栏ID
}
// MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体
type MovePigsIntoPenRequest struct {
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 移入猪只数量
Remarks string `json:"remarks"` // 备注
}
// SellPigsRequest 用于处理卖猪的请求体
type SellPigsRequest struct {
PenID uint `json:"penID" binding:"required"` // 猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 卖出猪只数量
UnitPrice float64 `json:"unitPrice" binding:"required,min=0"` // 单价
TotalPrice float64 `json:"totalPrice" binding:"required,min=0"` // 总价
TraderName string `json:"traderName" binding:"required"` // 交易方名称
TradeDate time.Time `json:"tradeDate" binding:"required"` // 交易日期
Remarks string `json:"remarks"` // 备注
}
// BuyPigsRequest 用于处理买猪的请求体
type BuyPigsRequest struct {
PenID uint `json:"penID" binding:"required"` // 猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 买入猪只数量
UnitPrice float64 `json:"unitPrice" binding:"required,min=0"` // 单价
TotalPrice float64 `json:"totalPrice" binding:"required,min=0"` // 总价
TraderName string `json:"traderName" binding:"required"` // 交易方名称
TradeDate time.Time `json:"tradeDate" binding:"required"` // 交易日期
Remarks string `json:"remarks"` // 备注
}
// TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体
type TransferPigsAcrossBatchesRequest struct {
DestBatchID uint `json:"destBatchID" binding:"required"` // 目标猪批次ID
FromPenID uint `json:"fromPenID" binding:"required"` // 源猪栏ID
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
Quantity uint `json:"quantity" binding:"required,min=1"` // 调栏猪只数量
Remarks string `json:"remarks"` // 备注
}
// TransferPigsWithinBatchRequest 用于群内调栏的请求体
type TransferPigsWithinBatchRequest struct {
FromPenID uint `json:"fromPenID" binding:"required"` // 源猪栏ID
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
Quantity uint `json:"quantity" binding:"required,min=1"` // 调栏猪只数量
Remarks string `json:"remarks"` // 备注
}
// RecordSickPigsRequest 用于记录新增病猪事件的请求体
type RecordSickPigsRequest struct {
PenID uint `json:"penID" binding:"required"` // 猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 病猪数量
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
Remarks string `json:"remarks"` // 备注
}
// RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体
type RecordSickPigRecoveryRequest struct {
PenID uint `json:"penID" binding:"required"` // 猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 康复猪数量
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
Remarks string `json:"remarks"` // 备注
}
// RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体
type RecordSickPigDeathRequest struct {
PenID uint `json:"penID" binding:"required"` // 猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 死亡猪数量
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
Remarks string `json:"remarks"` // 备注
}
// RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体
type RecordSickPigCullRequest struct {
PenID uint `json:"penID" binding:"required"` // 猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 淘汰猪数量
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
Remarks string `json:"remarks"` // 备注
}
// RecordDeathRequest 用于记录正常猪只死亡事件的请求体
type RecordDeathRequest struct {
PenID uint `json:"penID" binding:"required"` // 猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 死亡猪数量
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
Remarks string `json:"remarks"` // 备注
}
// RecordCullRequest 用于记录正常猪只淘汰事件的请求体
type RecordCullRequest struct {
PenID uint `json:"penID" binding:"required"` // 猪栏ID
Quantity int `json:"quantity" binding:"required,min=1"` // 淘汰猪数量
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
Remarks string `json:"remarks"` // 备注
}

View File

@@ -0,0 +1,52 @@
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"`
}
// 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,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验
}
// UpdatePenStatusRequest 定义了更新猪栏状态的请求结构
type UpdatePenStatusRequest struct {
Status models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"`
}

View File

@@ -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
}

View 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"`
}

View 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"`
}

View File

@@ -6,7 +6,7 @@ import (
"io"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/gin-gonic/gin"
)

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"strings"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"github.com/gin-gonic/gin"

View File

@@ -0,0 +1,315 @@
package service
import (
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
domain_pig "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// PigBatchService 接口定义保持不变,继续作为应用层对外的契约。
type PigBatchService interface {
CreatePigBatch(operatorID uint, 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)
// Pig Pen Management
AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error
ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
RemoveEmptyPenFromBatch(batchID uint, penID uint) error
MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
// Trade Sub-service
SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
// Transfer Sub-service
TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
// Sick Pig Management
RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
// Normal Pig Management
RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
}
// pigBatchService 的实现现在依赖于领域服务接口。
type pigBatchService struct {
logger *logs.Logger
domainService domain_pig.PigBatchService // 依赖注入领域服务
}
// NewPigBatchService 构造函数被修改,以注入领域服务。
func NewPigBatchService(domainService domain_pig.PigBatchService, logger *logs.Logger) PigBatchService {
return &pigBatchService{
logger: logger,
domainService: domainService,
}
}
// toPigBatchResponseDTO 负责将领域模型转换为应用层DTO这个职责保留在应用层。
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(),
CreateTime: batch.CreatedAt,
UpdateTime: batch.UpdatedAt,
}
}
// CreatePigBatch 现在将请求委托给领域服务处理。
func (s *pigBatchService) CreatePigBatch(operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
// 1. DTO -> 领域模型
batch := &models.PigBatch{
BatchNumber: dto.BatchNumber,
OriginType: dto.OriginType,
StartDate: dto.StartDate,
InitialCount: dto.InitialCount,
Status: dto.Status,
}
// 2. 调用领域服务
createdBatch, err := s.domainService.CreatePigBatch(operatorID, batch)
if err != nil {
s.logger.Errorf("应用层: 创建猪批次失败: %v", err)
return nil, MapDomainError(err)
}
// 3. 领域模型 -> DTO
return s.toPigBatchResponseDTO(createdBatch), nil
}
// GetPigBatch 从领域服务获取数据并转换为DTO同时处理错误转换。
func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) {
batch, err := s.domainService.GetPigBatch(id)
if err != nil {
s.logger.Warnf("应用层: 获取猪批次失败, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
return s.toPigBatchResponseDTO(batch), nil
}
// UpdatePigBatch 协调获取、更新和保存的流程,并处理错误转换。
func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
// 1. 先获取最新的领域模型
existingBatch, err := s.domainService.GetPigBatch(id)
if err != nil {
s.logger.Warnf("应用层: 更新猪批次失败,获取原批次信息错误, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
// 2. 将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
}
// 3. 调用领域服务执行更新
updatedBatch, err := s.domainService.UpdatePigBatch(existingBatch)
if err != nil {
s.logger.Errorf("应用层: 更新猪批次失败, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
// 4. 转换并返回结果
return s.toPigBatchResponseDTO(updatedBatch), nil
}
// DeletePigBatch 将删除操作委托给领域服务,并转换领域错误为应用层错误。
func (s *pigBatchService) DeletePigBatch(id uint) error {
err := s.domainService.DeletePigBatch(id)
if err != nil {
s.logger.Errorf("应用层: 删除猪批次失败, ID: %d, 错误: %v", id, err)
return MapDomainError(err)
}
return nil
}
// ListPigBatches 从领域服务获取列表并进行转换。
func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) {
batches, err := s.domainService.ListPigBatches(isActive)
if err != nil {
s.logger.Errorf("应用层: 批量查询猪批次失败: %v", err)
return nil, MapDomainError(err)
}
var responseDTOs []*dto.PigBatchResponseDTO
for _, batch := range batches {
responseDTOs = append(responseDTOs, s.toPigBatchResponseDTO(batch))
}
return responseDTOs, nil
}
// AssignEmptyPensToBatch 委托给领域服务
func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error {
err := s.domainService.AssignEmptyPensToBatch(batchID, penIDs, operatorID)
if err != nil {
s.logger.Errorf("应用层: 为猪批次分配空栏失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// ReclassifyPenToNewBatch 委托给领域服务
func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
err := s.domainService.ReclassifyPenToNewBatch(fromBatchID, toBatchID, penID, operatorID, remarks)
if err != nil {
s.logger.Errorf("应用层: 划拨猪栏到新批次失败, 源批次ID: %d, 错误: %v", fromBatchID, err)
return MapDomainError(err)
}
return nil
}
// RemoveEmptyPenFromBatch 委托给领域服务
func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) error {
err := s.domainService.RemoveEmptyPenFromBatch(batchID, penID)
if err != nil {
s.logger.Errorf("应用层: 从猪批次移除空栏失败, 批次ID: %d, 猪栏ID: %d, 错误: %v", batchID, penID, err)
return MapDomainError(err)
}
return nil
}
// MovePigsIntoPen 委托给领域服务
func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
err := s.domainService.MovePigsIntoPen(batchID, toPenID, quantity, operatorID, remarks)
if err != nil {
s.logger.Errorf("应用层: 将猪只移入猪栏失败, 批次ID: %d, 目标猪栏ID: %d, 错误: %v", batchID, toPenID, err)
return MapDomainError(err)
}
return nil
}
// SellPigs 委托给领域服务
func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
err := s.domainService.SellPigs(batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
if err != nil {
s.logger.Errorf("应用层: 卖猪失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// BuyPigs 委托给领域服务
func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
err := s.domainService.BuyPigs(batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
if err != nil {
s.logger.Errorf("应用层: 买猪失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// TransferPigsAcrossBatches 委托给领域服务
func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
err := s.domainService.TransferPigsAcrossBatches(sourceBatchID, destBatchID, fromPenID, toPenID, quantity, operatorID, remarks)
if err != nil {
s.logger.Errorf("应用层: 跨群调栏失败, 源批次ID: %d, 错误: %v", sourceBatchID, err)
return MapDomainError(err)
}
return nil
}
// TransferPigsWithinBatch 委托给领域服务
func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
err := s.domainService.TransferPigsWithinBatch(batchID, fromPenID, toPenID, quantity, operatorID, remarks)
if err != nil {
s.logger.Errorf("应用层: 群内调栏失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordSickPigs 委托给领域服务
func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordSickPigs(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
if err != nil {
s.logger.Errorf("应用层: 记录病猪事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordSickPigRecovery 委托给领域服务
func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordSickPigRecovery(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
if err != nil {
s.logger.Errorf("应用层: 记录病猪康复事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordSickPigDeath 委托给领域服务
func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordSickPigDeath(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
if err != nil {
s.logger.Errorf("应用层: 记录病猪死亡事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordSickPigCull 委托给领域服务
func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordSickPigCull(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
if err != nil {
s.logger.Errorf("应用层: 记录病猪淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordDeath 委托给领域服务
func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordDeath(operatorID, batchID, penID, quantity, happenedAt, remarks)
if err != nil {
s.logger.Errorf("应用层: 记录正常猪只死亡事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordCull 委托给领域服务
func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordCull(operatorID, batchID, penID, quantity, happenedAt, remarks)
if err != nil {
s.logger.Errorf("应用层: 记录正常猪只淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}

View File

@@ -0,0 +1,257 @@
package service
import (
"errors"
"fmt"
"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"
)
// PigFarmService 提供了猪场资产管理的业务逻辑
type PigFarmService interface {
// PigHouse methods
CreatePigHouse(name, description string) (*models.PigHouse, error)
GetPigHouseByID(id uint) (*models.PigHouse, error)
ListPigHouses() ([]models.PigHouse, error)
UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error)
DeletePigHouse(id uint) error
// Pen methods
CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error)
GetPenByID(id uint) (*models.Pen, error)
ListPens() ([]models.Pen, error)
UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error)
DeletePen(id uint) error
// UpdatePenStatus 更新猪栏状态
UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error)
}
type pigFarmService struct {
logger *logs.Logger
farmRepository repository.PigFarmRepository
penRepository repository.PigPenRepository
batchRepository repository.PigBatchRepository
uow repository.UnitOfWork // 工作单元,用于事务管理
}
// NewPigFarmService 创建一个新的 PigFarmService 实例
func NewPigFarmService(farmRepository repository.PigFarmRepository,
penRepository repository.PigPenRepository,
batchRepository repository.PigBatchRepository,
uow repository.UnitOfWork,
logger *logs.Logger) PigFarmService {
return &pigFarmService{
logger: logger,
farmRepository: farmRepository,
penRepository: penRepository,
batchRepository: batchRepository,
uow: uow,
}
}
// --- PigHouse Implementation ---
func (s *pigFarmService) CreatePigHouse(name, description string) (*models.PigHouse, error) {
house := &models.PigHouse{
Name: name,
Description: description,
}
err := s.farmRepository.CreatePigHouse(house)
return house, err
}
func (s *pigFarmService) GetPigHouseByID(id uint) (*models.PigHouse, error) {
return s.farmRepository.GetPigHouseByID(id)
}
func (s *pigFarmService) ListPigHouses() ([]models.PigHouse, error) {
return s.farmRepository.ListPigHouses()
}
func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error) {
house := &models.PigHouse{
Model: gorm.Model{ID: id},
Name: name,
Description: description,
}
rowsAffected, err := s.farmRepository.UpdatePigHouse(house)
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, ErrHouseNotFound
}
// 返回更新后的完整信息
return s.farmRepository.GetPigHouseByID(id)
}
func (s *pigFarmService) DeletePigHouse(id uint) error {
// 业务逻辑:检查猪舍是否包含猪栏
penCount, err := s.farmRepository.CountPensInHouse(id)
if err != nil {
return err
}
if penCount > 0 {
return ErrHouseContainsPens
}
// 调用仓库层进行删除
rowsAffected, err := s.farmRepository.DeletePigHouse(id)
if err != nil {
return err
}
if rowsAffected == 0 {
return ErrHouseNotFound
}
return nil
}
// --- Pen Implementation ---
func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error) {
// 业务逻辑:验证所属猪舍是否存在
_, err := s.farmRepository.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: models.PenStatusEmpty,
}
err = s.penRepository.CreatePen(pen)
return pen, err
}
func (s *pigFarmService) GetPenByID(id uint) (*models.Pen, error) {
return s.penRepository.GetPenByID(id)
}
func (s *pigFarmService) ListPens() ([]models.Pen, error) {
return s.penRepository.ListPens()
}
func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) {
// 业务逻辑:验证所属猪舍是否存在
_, err := s.farmRepository.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,
HouseID: houseID,
Capacity: capacity,
Status: status,
}
rowsAffected, err := s.penRepository.UpdatePen(pen)
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, ErrPenNotFound
}
// 返回更新后的完整信息
return s.penRepository.GetPenByID(id)
}
func (s *pigFarmService) DeletePen(id uint) error {
// 业务逻辑:检查猪栏是否被活跃批次使用
pen, err := s.penRepository.GetPenByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound // 猪栏不存在
}
return err
}
// 检查猪栏是否关联了活跃批次
// 注意pen.PigBatchID 是指针类型,需要检查是否为 nil
if pen.PigBatchID != nil && *pen.PigBatchID != 0 {
pigBatch, err := s.batchRepository.GetPigBatchByID(*pen.PigBatchID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// 如果批次活跃,则不能删除猪栏
if pigBatch != nil && pigBatch.IsActive() {
return ErrPenInUse
}
}
// 调用仓库层进行删除
rowsAffected, err := s.penRepository.DeletePen(id)
if err != nil {
return err
}
if rowsAffected == 0 {
return ErrPenNotFound
}
return nil
}
// UpdatePenStatus 更新猪栏状态
func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error) {
var updatedPen *models.Pen
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
pen, err := s.penRepository.GetPenByIDTx(tx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
s.logger.Errorf("更新猪栏状态失败: 获取猪栏 %d 信息错误: %v", id, err)
return fmt.Errorf("获取猪栏 %d 信息失败: %w", id, err)
}
// 业务逻辑:根据猪栏的 PigBatchID 和当前状态,判断是否允许设置为 newStatus
if pen.PigBatchID != nil && *pen.PigBatchID != 0 { // 猪栏已被批次使用
if newStatus == models.PenStatusEmpty { // 猪栏已被批次使用,不能直接设置为空闲
return ErrPenStatusInvalidForOccupiedPen
}
} else { // 猪栏未被批次使用 (PigBatchID == nil)
if newStatus == models.PenStatusOccupied { // 猪栏未被批次使用,不能设置为使用中
return ErrPenStatusInvalidForUnoccupiedPen
}
}
// 如果新状态与旧状态相同,则无需更新
if pen.Status == newStatus {
updatedPen = pen // 返回原始猪栏,因为没有实际更新
return nil
}
updates := map[string]interface{}{
"status": newStatus,
}
if err := s.penRepository.UpdatePenFieldsTx(tx, id, updates); err != nil {
s.logger.Errorf("更新猪栏 %d 状态失败: %v", id, err)
return fmt.Errorf("更新猪栏 %d 状态失败: %w", id, err)
}
// 获取更新后的猪栏信息
updatedPen, err = s.penRepository.GetPenByIDTx(tx, id)
if err != nil {
s.logger.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %v", id, err)
return fmt.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %w", id, err)
}
return nil
})
if err != nil {
return nil, err
}
return updatedPen, nil
}

View File

@@ -0,0 +1,55 @@
package service
import (
"errors"
domain_pig "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
)
var (
ErrHouseContainsPens = errors.New("无法删除包含猪栏的猪舍")
ErrHouseNotFound = errors.New("指定的猪舍不存在")
ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除")
ErrPenNotFound = errors.New("指定的猪栏不存在")
ErrPenStatusInvalidForOccupiedPen = errors.New("猪栏已被批次使用,无法设置为非使用中状态")
ErrPenStatusInvalidForUnoccupiedPen = errors.New("猪栏未被批次使用,无法设置为使用中状态")
ErrPigBatchNotFound = errors.New("指定的猪批次不存在")
ErrPigBatchActive = errors.New("活跃的猪批次不能被删除")
ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏")
ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用")
ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配")
ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联")
ErrPenNotEmpty = errors.New("猪栏内仍有猪只")
ErrInvalidOperation = errors.New("非法操作")
)
// MapDomainError 将领域层的错误转换为应用服务层的公共错误。
func MapDomainError(err error) error {
if err == nil {
return nil
}
switch {
case errors.Is(err, domain_pig.ErrPigBatchNotFound):
return ErrPigBatchNotFound
case errors.Is(err, domain_pig.ErrPigBatchActive):
return ErrPigBatchActive
case errors.Is(err, domain_pig.ErrPigBatchNotActive):
return ErrPigBatchNotActive
case errors.Is(err, domain_pig.ErrPenOccupiedByOtherBatch):
return ErrPenOccupiedByOtherBatch
case errors.Is(err, domain_pig.ErrPenStatusInvalidForAllocation):
return ErrPenStatusInvalidForAllocation
case errors.Is(err, domain_pig.ErrPenNotAssociatedWithBatch):
return ErrPenNotAssociatedWithBatch
case errors.Is(err, domain_pig.ErrPenNotFound):
return ErrPenNotFound
case errors.Is(err, domain_pig.ErrPenNotEmpty):
return ErrPenNotEmpty
case errors.Is(err, domain_pig.ErrInvalidOperation):
return ErrInvalidOperation
// 可以添加更多领域错误到应用层错误的映射
default:
return err // 对于未知的领域错误,直接返回
}
}

View File

@@ -1,4 +1,4 @@
package transport
package webhook
import (
"encoding/base64"
@@ -7,7 +7,7 @@ import (
"net/http"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device/proto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device/proto"
"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"

View File

@@ -1,4 +1,4 @@
package transport
package webhook
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package transport
package webhook
import "net/http"

View File

@@ -8,11 +8,13 @@ import (
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/api"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/database"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
@@ -58,44 +60,46 @@ func NewApplication(configPath string) (*Application, error) {
// 初始化 Token 服务
tokenService := token.NewTokenService([]byte(cfg.App.JWTSecret))
// 初始化用户仓库
// --- 仓库对象初始化 ---
userRepo := repository.NewGormUserRepository(storage.GetDB())
// 初始化设备仓库
deviceRepo := repository.NewGormDeviceRepository(storage.GetDB())
// 初始化区域主控仓库
areaControllerRepo := repository.NewGormAreaControllerRepository(storage.GetDB())
// 初始化设备模板仓库
deviceTemplateRepo := repository.NewGormDeviceTemplateRepository(storage.GetDB())
// 初始化计划仓库
planRepo := repository.NewGormPlanRepository(storage.GetDB())
// 初始化待执行任务仓库
pendingTaskRepo := repository.NewGormPendingTaskRepository(storage.GetDB())
// 初始化执行日志仓库
executionLogRepo := repository.NewGormExecutionLogRepository(storage.GetDB())
// 初始化传感器数据仓库
sensorDataRepo := repository.NewGormSensorDataRepository(storage.GetDB())
// 初始化命令下发历史仓库
deviceCommandLogRepo := repository.NewGormDeviceCommandLogRepository(storage.GetDB())
// 初始化待采集请求仓库
pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB())
// 初始化审计日志仓库
userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB())
pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB())
pigBatchLogRepo := repository.NewGormPigBatchLogRepository(storage.GetDB())
pigFarmRepo := repository.NewGormPigFarmRepository(storage.GetDB())
pigPenRepo := repository.NewGormPigPenRepository(storage.GetDB())
pigTransferLogRepo := repository.NewGormPigTransferLogRepository(storage.GetDB())
pigTradeRepo := repository.NewGormPigTradeRepository(storage.GetDB())
pigSickPigLogRepo := repository.NewGormPigSickLogRepository(storage.GetDB())
medicationLogRepo := repository.NewGormMedicationLogRepository(storage.GetDB())
// 初始化事务管理器
unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger)
// 初始化猪群管理领域
pigPenTransferManager := pig.NewPigPenTransferManager(pigPenRepo, pigTransferLogRepo, pigBatchRepo)
pigTradeManager := pig.NewPigTradeManager(pigTradeRepo)
pigSickManager := pig.NewSickPigManager(pigSickPigLogRepo, medicationLogRepo)
pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork,
pigPenTransferManager, pigTradeManager, pigSickManager)
// --- 业务逻辑处理器初始化 ---
pigFarmService := service.NewPigFarmService(pigFarmRepo, pigPenRepo, pigBatchRepo, unitOfWork, logger)
pigBatchService := service.NewPigBatchService(pigBatchDomain, logger)
// 初始化审计服务
auditService := audit.NewService(userActionLogRepo, logger)
// 初始化设备上行监听器
listenHandler := transport.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, areaControllerRepo, deviceCommandLogRepo, pendingCollectionRepo)
listenHandler := webhook.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, areaControllerRepo, deviceCommandLogRepo, pendingCollectionRepo)
// 初始化计划触发器管理器
analysisPlanTaskManager := task.NewAnalysisPlanTaskManager(planRepo, pendingTaskRepo, executionLogRepo, logger)
@@ -135,6 +139,8 @@ func NewApplication(configPath string) (*Application, error) {
areaControllerRepo,
deviceTemplateRepo,
planRepo,
pigFarmService,
pigBatchService,
userActionLogRepo,
tokenService,
auditService,
@@ -263,7 +269,7 @@ func (app *Application) initializePendingTasks(
if plan.ExecutionType == models.PlanExecutionTypeManual ||
(plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteCount >= plan.ExecuteNum) {
// 更新计划状态为已停止
plan.Status = models.PlanStatusStopeed
plan.Status = models.PlanStatusStopped
logger.Infof("计划 #%d 状态已更新为 '执行完毕'。", plan.ID)
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device/proto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device/proto"
"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"

View File

@@ -354,7 +354,7 @@ const file_device_proto_rawDesc = "" +
"\n" +
"MethodType\x12\x0f\n" +
"\vINSTRUCTION\x10\x00\x12\v\n" +
"\aCOLLECT\x10\x01B#Z!internal/app/service/device/protob\x06proto3"
"\aCOLLECT\x10\x01B\x1eZ\x1cinternal/domain/device/protob\x06proto3"
var (
file_device_proto_rawDescOnce sync.Once

View File

@@ -4,7 +4,7 @@ package device;
import "google/protobuf/any.proto";
option go_package = "internal/app/service/device/proto";
option go_package = "internal/domain/device/proto";
// --- ---

View File

@@ -0,0 +1,175 @@
package pig
import (
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"gorm.io/gorm"
)
// PigPenTransferManager 定义了与猪只位置转移相关的底层数据库操作。
// 它是一个内部服务,被主服务 PigBatchService 调用。
type PigPenTransferManager interface {
// LogTransfer 在数据库中创建一条猪只迁移日志。
LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error
// GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。
GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error)
// GetPensByBatchID 获取一个猪群当前关联的所有猪栏。
GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error)
// UpdatePenFields 更新一个猪栏的指定字段。
UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error
// GetCurrentPigsInPen 通过汇总猪只迁移日志,计算给定猪栏中的当前猪只数量。
GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error)
// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error)
// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。
ReleasePen(tx *gorm.DB, penID uint) error
}
// pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。
// 它作为调栏管理器,处理底层的数据库交互。
type pigPenTransferManager struct {
penRepo repository.PigPenRepository
logRepo repository.PigTransferLogRepository
pigBatchRepo repository.PigBatchRepository
}
// NewPigPenTransferManager 是 pigPenTransferManager 的构造函数。
func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository, pigBatchRepo repository.PigBatchRepository) PigPenTransferManager {
return &pigPenTransferManager{
penRepo: penRepo,
logRepo: logRepo,
pigBatchRepo: pigBatchRepo,
}
}
// LogTransfer 实现了在数据库中创建迁移日志的逻辑。
func (s *pigPenTransferManager) LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error {
return s.logRepo.CreatePigTransferLog(tx, log)
}
// GetPenByID 实现了获取猪栏信息的逻辑。
func (s *pigPenTransferManager) GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) {
return s.penRepo.GetPenByIDTx(tx, penID)
}
// GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。
func (s *pigPenTransferManager) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
return s.penRepo.GetPensByBatchIDTx(tx, batchID)
}
// UpdatePenFields 实现了更新猪栏字段的逻辑。
func (s *pigPenTransferManager) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error {
return s.penRepo.UpdatePenFieldsTx(tx, penID, updates)
}
// GetCurrentPigsInPen 实现了计算猪栏当前猪只数量的逻辑。
func (s *pigPenTransferManager) GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error) {
// 1. 通过猪栏ID查出所属猪群信息
pen, err := s.penRepo.GetPenByIDTx(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, ErrPenNotFound
}
return 0, err
}
// 如果猪栏没有关联任何猪群那么猪只数必为0
if pen.PigBatchID == nil || *pen.PigBatchID == 0 {
return 0, nil
}
currentBatchID := *pen.PigBatchID
// 2. 根据猪群ID获取猪群的起始日期
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, currentBatchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, ErrPigBatchNotFound
}
return 0, err
}
batchStartDate := batch.StartDate
// 3. 调用仓库方法,获取从猪群开始至今,该猪栏的所有倒序日志
logs, err := s.logRepo.GetLogsForPenSince(tx, penID, batchStartDate)
if err != nil {
return 0, err
}
// 如果没有日志猪只数为0
if len(logs) == 0 {
return 0, nil
}
// 4. 在内存中筛选出最后一段连续日志,并进行计算
var totalPigs int
// 再次确认当前猪群ID以最新的日志为准防止在极小时间窗口内猪栏被快速切换
latestBatchID := *pen.PigBatchID
for _, log := range logs {
// 一旦发现日志不属于最新的猪群,立即停止计算
if log.PigBatchID != latestBatchID {
break
}
totalPigs += log.Quantity
}
return totalPigs, nil
}
// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
// 该方法通过遍历猪群下的每个猪栏,并调用 GetCurrentPigsInPen 来累加存栏数。
func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error) {
// 1. 获取该批次下所有猪栏的列表
pensInBatch, err := s.GetPensByBatchID(tx, batchID)
if err != nil {
return 0, fmt.Errorf("获取猪群 %d 下属猪栏失败: %w", batchID, err)
}
totalPigs := 0
// 2. 遍历每个猪栏,累加其存栏数
for _, pen := range pensInBatch {
pigsInPen, err := s.GetCurrentPigsInPen(tx, pen.ID)
if err != nil {
return 0, fmt.Errorf("获取猪栏 %d 存栏数失败: %w", pen.ID, err)
}
totalPigs += pigsInPen
}
return totalPigs, nil
}
// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。
// 此操作通常在猪栏被清空后调用。
func (s *pigPenTransferManager) ReleasePen(tx *gorm.DB, penID uint) error {
// 1. 获取猪栏信息
pen, err := s.penRepo.GetPenByIDTx(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
}
return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
}
// 2. 更新猪栏字段
// 将 pig_batch_id 设置为 nil (SQL NULL)
// 将 status 设置为 PenStatusEmpty
updates := map[string]interface{}{
"pig_batch_id": nil, // 使用 nil 来表示 SQL NULL
"status": models.PenStatusEmpty,
}
if err := s.penRepo.UpdatePenFieldsTx(tx, penID, updates); err != nil {
return fmt.Errorf("释放猪栏 %v 失败: %w", pen.PenNumber, err)
}
return nil
}

View File

@@ -0,0 +1,165 @@
package pig
import (
"errors"
"time"
"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 表示当尝试访问一个不存在的猪批次时发生的错误。
ErrPigBatchNotFound = errors.New("指定的猪批次不存在")
// ErrPigBatchActive 表示当尝试对一个活跃的猪批次执行不允许的操作(如删除)时发生的错误。
ErrPigBatchActive = errors.New("活跃的猪批次不能被删除")
// ErrPigBatchNotActive 表示当猪批次不处于活跃状态,但执行了需要其活跃的操作时发生的错误。
ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏")
// ErrPenOccupiedByOtherBatch 表示当尝试将一个已经被其他批次占用的猪栏分配给新批次时发生的错误。
ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用")
// ErrPenStatusInvalidForAllocation 表示猪栏的当前状态(例如,'维修中')不允许被分配。
ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配")
// ErrPenNotFound 表示猪栏不存在
ErrPenNotFound = errors.New("指定的猪栏不存在")
// ErrPenNotAssociatedWithBatch 表示猪栏未与该批次关联
ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联")
// ErrPenNotEmpty 表示猪栏内仍有猪只,不允许执行当前操作。
ErrPenNotEmpty = errors.New("猪栏内仍有猪只,无法执行此操作")
// ErrInvalidOperation 非法操作
ErrInvalidOperation = errors.New("非法操作")
)
// --- 领域服务接口 ---
// PigBatchService 定义了猪批次管理的核心业务逻辑接口。
// 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。
type PigBatchService interface {
// CreatePigBatch 创建猪批次,并记录初始日志。
CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error)
// GetPigBatch 获取单个猪批次。
GetPigBatch(id uint) (*models.PigBatch, error)
// UpdatePigBatch 更新猪批次信息。
UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error)
// DeletePigBatch 删除猪批次,包含业务规则校验。
DeletePigBatch(id uint) error
// ListPigBatches 批量查询猪批次。
ListPigBatches(isActive *bool) ([]*models.PigBatch, error)
// AssignEmptyPensToBatch 为猪群分配空栏
AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error
// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏
MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群
ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
// RemoveEmptyPenFromBatch 将一个猪栏移除出猪群,此方法需要在猪栏为空的情况下执行。
RemoveEmptyPenFromBatch(batchID uint, penID uint) error
// GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。
GetCurrentPigQuantity(batchID uint) (int, error)
UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error
// ---交易子服务---
// SellPigs 处理卖猪的业务逻辑。
SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
// BuyPigs 处理买猪的业务逻辑。
BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
// ---调栏子服务 ---
TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
// --- 病猪管理相关方法 ---
// RecordSickPigs 记录新增病猪事件。
RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
// RecordSickPigRecovery 记录病猪康复事件。
RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
// RecordSickPigDeath 记录病猪死亡事件。
RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
// RecordSickPigCull 记录病猪淘汰事件。
RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
// --- 正常猪只管理相关方法 ---
// RecordDeath 记录正常猪只死亡事件。
RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
// RecordCull 记录正常猪只淘汰事件。
RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
}
// pigBatchService 是 PigBatchService 接口的具体实现。
// 它作为猪群领域的主服务,封装了所有业务逻辑。
type pigBatchService struct {
pigBatchRepo repository.PigBatchRepository // 猪批次仓库
pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库
uow repository.UnitOfWork // 工作单元,用于管理事务
transferSvc PigPenTransferManager // 调栏子服务
tradeSvc PigTradeManager // 交易子服务
sickSvc SickPigManager // 病猪子服务
}
// NewPigBatchService 是 pigBatchService 的构造函数。
// 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。
func NewPigBatchService(
pigBatchRepo repository.PigBatchRepository,
pigBatchLogRepo repository.PigBatchLogRepository,
uow repository.UnitOfWork,
transferSvc PigPenTransferManager,
tradeSvc PigTradeManager,
sickSvc SickPigManager,
) PigBatchService {
return &pigBatchService{
pigBatchRepo: pigBatchRepo,
pigBatchLogRepo: pigBatchLogRepo,
uow: uow,
transferSvc: transferSvc,
tradeSvc: tradeSvc,
sickSvc: sickSvc,
}
}
func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 检查猪批次是否存在且活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return err
}
if !batch.IsActive() {
return ErrPigBatchNotActive
}
// 2. 检查猪栏是否存在
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return err
}
// 3. 检查猪栏是否与当前批次关联
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return ErrPenNotAssociatedWithBatch
}
// 4. 检查猪栏是否为空
pigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
if err != nil {
return err
}
if pigsInPen > 0 {
return ErrPenNotEmpty
}
// 5. 释放猪栏 (将 pig_batch_id 设置为 nil状态设置为空闲)
if err := s.transferSvc.ReleasePen(tx, penID); err != nil {
return err
}
return nil
})
}

View File

@@ -0,0 +1,196 @@
package pig
import (
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// --- 领域服务实现 ---
// CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。
func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) {
// 业务规则可以在这里添加,例如检查批次号是否唯一等
var createdBatch *models.PigBatch
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 创建猪批次
// 注意: 此处依赖一个假设存在的 pigBatchRepo.CreatePigBatchTx 方法
var err error
createdBatch, err = s.pigBatchRepo.CreatePigBatchTx(tx, batch)
if err != nil {
return fmt.Errorf("创建猪批次失败: %w", err)
}
// 2. 创建初始批次日志
initialLog := &models.PigBatchLog{
PigBatchID: createdBatch.ID,
HappenedAt: time.Now(),
ChangeType: models.ChangeTypeCorrection, // 初始创建可视为一种校正
ChangeCount: createdBatch.InitialCount,
Reason: fmt.Sprintf("创建了新的猪批次 %s初始数量 %d", createdBatch.BatchNumber, createdBatch.InitialCount),
BeforeCount: 0, // 初始创建前数量为0
AfterCount: createdBatch.InitialCount,
OperatorID: operatorID,
}
// 3. 记录批次日志
if err := s.pigBatchLogRepo.CreateTx(tx, initialLog); err != nil {
return fmt.Errorf("记录初始批次日志失败: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return createdBatch, nil
}
// GetPigBatch 实现了获取单个猪批次的逻辑。
func (s *pigBatchService) GetPigBatch(id uint) (*models.PigBatch, error) {
batch, err := s.pigBatchRepo.GetPigBatchByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrPigBatchNotFound
}
return nil, err
}
return batch, nil
}
// UpdatePigBatch 实现了更新猪批次的逻辑。
func (s *pigBatchService) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) {
// 可以在这里添加更新前的业务校验
updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(batch)
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, ErrPigBatchNotFound // 如果没有行被更新,可能意味着记录不存在
}
return updatedBatch, nil
}
// DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。
func (s *pigBatchService) DeletePigBatch(id uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 获取猪批次信息
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, id) // 使用事务内方法
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return err
}
// 2. 核心业务规则:检查猪批次是否为活跃状态
if batch.IsActive() {
return ErrPigBatchActive // 如果活跃,则不允许删除
}
// 3. 释放所有关联的猪栏
// 获取该批次下所有猪栏
pensInBatch, err := s.transferSvc.GetPensByBatchID(tx, id)
if err != nil {
return fmt.Errorf("获取猪批次 %d 关联猪栏失败: %w", id, err)
}
// 逐一释放猪栏
for _, pen := range pensInBatch {
if err := s.transferSvc.ReleasePen(tx, pen.ID); err != nil {
return fmt.Errorf("释放猪栏 %d 失败: %w", pen.ID, err)
}
}
// 4. 执行删除猪批次
rowsAffected, err := s.pigBatchRepo.DeletePigBatchTx(tx, id)
if err != nil {
return err
}
if rowsAffected == 0 {
return ErrPigBatchNotFound
}
return nil
})
}
// ListPigBatches 实现了批量查询猪批次的逻辑。
func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) {
return s.pigBatchRepo.ListPigBatches(isActive)
}
// GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。
func (s *pigBatchService) GetCurrentPigQuantity(batchID uint) (int, error) {
var getErr error
var quantity int
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
quantity, getErr = s.getCurrentPigQuantityTx(tx, batchID)
return getErr
})
if err != nil {
return 0, err
}
return quantity, nil
}
// getCurrentPigQuantityTx 实现了获取指定猪批次的当前猪只数量的逻辑。
func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (int, error) {
// 1. 获取猪批次初始信息
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, ErrPigBatchNotFound
}
return 0, fmt.Errorf("获取猪批次 %d 初始信息失败: %w", batchID, err)
}
// 2. 尝试获取该批次的最后一条日志记录
lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 如果没有找到任何日志记录(除了初始创建),则当前数量就是初始数量
return batch.InitialCount, nil
}
return 0, fmt.Errorf("获取猪批次 %d 最后一条日志失败: %w", batchID, err)
}
// 3. 如果找到最后一条日志,则当前数量为该日志的 AfterCount
return lastLog.AfterCount, nil
}
func (s *pigBatchService) UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
return s.updatePigBatchQuantityTx(tx, operatorID, batchID, changeType, changeAmount, changeReason, happenedAt)
})
}
func (s *pigBatchService) updatePigBatchQuantityTx(tx *gorm.DB, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID)
if err != nil {
return err
}
// 检查数量不应该减到小于零
if changeAmount < 0 {
if lastLog.AfterCount+changeAmount < 0 {
return ErrInvalidOperation
}
}
pigBatchLog := &models.PigBatchLog{
PigBatchID: batchID,
ChangeType: changeType,
ChangeCount: changeAmount,
Reason: changeReason,
BeforeCount: lastLog.AfterCount,
AfterCount: lastLog.AfterCount + changeAmount,
OperatorID: operatorID,
HappenedAt: happenedAt,
}
return s.pigBatchLogRepo.CreateTx(tx, pigBatchLog)
}

View File

@@ -0,0 +1,383 @@
package pig
import (
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
// executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。
func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType models.PigTransferType, operatorID uint, remarks string) error {
// 通用校验:任何调出操作都不能超过源猪栏的当前存栏数
if quantity < 0 { // 当调出时才需要检查
currentPigsInFromPen, err := s.transferSvc.GetCurrentPigsInPen(tx, fromPenID)
if err != nil {
return fmt.Errorf("获取源猪栏 %d 当前猪只数失败: %w", fromPenID, err)
}
if currentPigsInFromPen+quantity < 0 {
return fmt.Errorf("调出数量 %d 超过源猪栏 %d 当前存栏数 %d", -quantity, fromPenID, currentPigsInFromPen)
}
}
// 1. 生成关联ID
correlationID := uuid.New().String()
// 2. 创建调出日志
logOut := &models.PigTransferLog{
TransferTime: time.Now(),
PigBatchID: fromBatchID,
PenID: fromPenID,
Quantity: -quantity, // 调出为负数
Type: transferType,
CorrelationID: correlationID,
OperatorID: operatorID,
Remarks: remarks,
}
// 3. 创建调入日志
logIn := &models.PigTransferLog{
TransferTime: time.Now(),
PigBatchID: toBatchID,
PenID: toPenID,
Quantity: quantity, // 调入为正数
Type: transferType,
CorrelationID: correlationID,
OperatorID: operatorID,
Remarks: remarks,
}
// 4. 调用子服务记录日志
if err := s.transferSvc.LogTransfer(tx, logOut); err != nil {
return fmt.Errorf("记录调出日志失败: %w", err)
}
if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
return fmt.Errorf("记录调入日志失败: %w", err)
}
return nil
}
// TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。
func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
if fromPenID == toPenID {
return errors.New("源猪栏和目标猪栏不能相同")
}
if quantity == 0 {
return errors.New("迁移数量不能为零")
}
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 核心业务规则校验
fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID)
if err != nil {
return fmt.Errorf("获取源猪栏信息失败: %w", err)
}
toPen, err := s.transferSvc.GetPenByID(tx, toPenID)
if err != nil {
return fmt.Errorf("获取目标猪栏信息失败: %w", err)
}
if fromPen.PigBatchID == nil || *fromPen.PigBatchID != batchID {
return fmt.Errorf("源猪栏 %d 不属于指定的猪群 %d", fromPenID, batchID)
}
if toPen.PigBatchID != nil && *toPen.PigBatchID != batchID {
return fmt.Errorf("目标猪栏 %d 已被其他猪群占用", toPenID)
}
// 2. 调用通用辅助方法执行日志记录
err = s.executeTransferAndLog(tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏", operatorID, remarks)
if err != nil {
return err
}
// 3. 群内调栏,猪群总数不变
return nil
})
}
// TransferPigsAcrossBatches 实现了跨猪群的调栏业务。
func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
if sourceBatchID == destBatchID {
return errors.New("源猪群和目标猪群不能相同")
}
if quantity == 0 {
return errors.New("迁移数量不能为零")
}
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 核心业务规则校验
// 1.1 校验猪群存在
if _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, sourceBatchID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("源猪群 %d 不存在", sourceBatchID)
}
return fmt.Errorf("获取源猪群信息失败: %w", err)
}
if _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, destBatchID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("目标猪群 %d 不存在", destBatchID)
}
return fmt.Errorf("获取目标猪群信息失败: %w", err)
}
// 1.2 校验猪栏归属
fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID)
if err != nil {
return fmt.Errorf("获取源猪栏信息失败: %w", err)
}
if fromPen.PigBatchID == nil || *fromPen.PigBatchID != sourceBatchID {
return fmt.Errorf("源猪栏 %d 不属于源猪群 %d", fromPenID, sourceBatchID)
}
// 2. 调用通用辅助方法执行猪只物理转移的日志记录
err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏", operatorID, remarks)
if err != nil {
return err
}
// 3. 通过创建批次日志来修改猪群总数,确保数据可追溯
now := time.Now()
// 3.1 记录源猪群数量减少
reasonOut := fmt.Sprintf("跨群调栏: %d头猪从批次 %d 调出至批次 %d。备注: %s", quantity, sourceBatchID, destBatchID, remarks)
err = s.updatePigBatchQuantityTx(tx, operatorID, sourceBatchID, models.ChangeTypeTransferOut, -int(quantity), reasonOut, now)
if err != nil {
return fmt.Errorf("更新源猪群 %d 数量失败: %w", sourceBatchID, err)
}
// 3.2 记录目标猪群数量增加
reasonIn := fmt.Sprintf("跨群调栏: %d头猪从批次 %d 调入。备注: %s", quantity, sourceBatchID, remarks)
err = s.updatePigBatchQuantityTx(tx, operatorID, destBatchID, models.ChangeTypeTransferIn, int(quantity), reasonIn, now)
if err != nil {
return fmt.Errorf("更新目标猪群 %d 数量失败: %w", destBatchID, err)
}
return nil
})
}
// AssignEmptyPensToBatch 为猪群分配空栏
func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 验证猪批次是否存在且活跃
pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return fmt.Errorf("获取猪批次信息失败: %w", err)
}
if !pigBatch.IsActive() {
return ErrPigBatchNotActive
}
// 2. 遍历并校验每一个待分配的猪栏
for _, penID := range penIDs {
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
}
return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
}
// 核心业务规则:校验猪栏是否完全空闲
if pen.Status != models.PenStatusEmpty {
return fmt.Errorf("猪栏 %s 状态不为空 (%s),无法分配", pen.PenNumber, pen.Status)
}
if pen.PigBatchID != nil {
return fmt.Errorf("猪栏 %s 已被其他批次 %d 占用,无法分配", pen.PenNumber, *pen.PigBatchID)
}
// 3. 更新猪栏的归属
updates := map[string]interface{}{
"pig_batch_id": &batchID,
"status": models.PenStatusOccupied,
}
if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil {
return fmt.Errorf("分配猪栏 %d 失败: %w", penID, err)
}
}
return nil
})
}
// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏
func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
if quantity <= 0 {
return errors.New("迁移数量必须大于零")
}
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 验证猪批次是否存在且活跃
pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return fmt.Errorf("获取猪批次信息失败: %w", err)
}
if !pigBatch.IsActive() {
return ErrPigBatchNotActive
}
// 2. 校验目标猪栏
toPen, err := s.transferSvc.GetPenByID(tx, toPenID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("目标猪栏 %d 不存在: %w", toPenID, ErrPenNotFound)
}
return fmt.Errorf("获取目标猪栏 %d 信息失败: %w", toPenID, err)
}
// 校验目标猪栏的归属和状态
if toPen.PigBatchID == nil {
return fmt.Errorf("目标猪栏 %s 不属于当前批次 %s", toPen.PenNumber, batchID)
}
if toPen.PigBatchID != nil && *toPen.PigBatchID != batchID {
return fmt.Errorf("目标猪栏 %s 已被其他批次 %d 占用,无法移入", toPen.PenNumber, *toPen.PigBatchID)
}
// 3. 校验猪群中有足够的“未分配”猪只
currentBatchTotal, err := s.getCurrentPigQuantityTx(tx, batchID)
if err != nil {
return fmt.Errorf("获取猪群 %d 当前总数量失败: %w", batchID, err)
}
// 获取该批次下所有猪栏的当前总存栏数
totalPigsInPens, err := s.transferSvc.GetTotalPigsInPensForBatchTx(tx, batchID)
if err != nil {
return fmt.Errorf("计算猪群 %d 下属猪栏总存栏失败: %w", batchID, err)
}
unassignedPigs := currentBatchTotal - totalPigsInPens
if unassignedPigs < quantity {
return fmt.Errorf("猪群 %d 未分配猪只不足,当前未分配 %d 头,需要移入 %d 头", batchID, unassignedPigs, quantity)
}
// 4. 记录转移日志
logIn := &models.PigTransferLog{
TransferTime: time.Now(),
PigBatchID: batchID,
PenID: toPenID,
Quantity: quantity, // 调入为正数
Type: models.PigTransferTypeInternal, // 首次入栏
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
return fmt.Errorf("记录入栏日志失败: %w", err)
}
return nil
})
}
// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群
func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
if fromBatchID == toBatchID {
return errors.New("源猪群和目标猪群不能相同")
}
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 核心业务规则校验
// 1.1 校验猪群存在
fromBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, fromBatchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("源猪群 %d 不存在", fromBatchID)
}
return fmt.Errorf("获取源猪群信息失败: %w", err)
}
toBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, toBatchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("目标猪群 %d 不存在", toBatchID)
}
return fmt.Errorf("获取目标猪群信息失败: %w", err)
}
// 1.2 校验猪栏归属
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
}
return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
}
if pen.PigBatchID == nil || *pen.PigBatchID != fromBatchID {
return fmt.Errorf("猪栏 %v 不属于源猪群 %v无法划拨", pen.PenNumber, fromBatch.BatchNumber)
}
// 2. 获取猪栏当前存栏数
quantity, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %v 存栏数失败: %w", pen.PenNumber, err)
}
// 3. 更新猪栏的归属
updates := map[string]interface{}{
"pig_batch_id": &toBatchID,
}
if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil {
return fmt.Errorf("更新猪栏 %v 归属失败: %w", pen.PenNumber, err)
}
// 如果猪栏是空的,则只进行归属变更,不影响猪群数量
if quantity == 0 {
return nil // 空栏划拨,不涉及猪只数量变更
}
// 4. 记录猪只从旧批次“迁出”的猪栏日志
correlationID := uuid.New().String()
logOut := &models.PigTransferLog{
TransferTime: time.Now(),
PigBatchID: fromBatchID,
PenID: penID,
Quantity: -quantity, // 迁出为负数
Type: models.PigTransferTypeCrossBatch,
CorrelationID: correlationID,
OperatorID: operatorID,
Remarks: fmt.Sprintf("整栏划拨迁出: %d头猪从批次 %v 随猪栏 %v 划拨至批次 %v。备注: %s", quantity, fromBatch.BatchNumber, pen.PenNumber, toBatch.BatchNumber, remarks),
}
if err := s.transferSvc.LogTransfer(tx, logOut); err != nil {
return fmt.Errorf("记录猪栏 %d 迁出日志失败: %w", penID, err)
}
// 5. 记录猪只到新批次“迁入”的猪栏日志
logIn := &models.PigTransferLog{
TransferTime: time.Now(),
PigBatchID: toBatchID,
PenID: penID,
Quantity: quantity, // 迁入为正数
Type: models.PigTransferTypeCrossBatch,
CorrelationID: correlationID,
OperatorID: operatorID,
Remarks: fmt.Sprintf("整栏划拨迁入: %v头猪随猪栏 %v 从批次 %v 划拨入。备注: %s", quantity, fromBatch.BatchNumber, pen.PenNumber, remarks),
}
if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
return fmt.Errorf("记录猪栏 %d 迁入日志失败: %w", penID, err)
}
// 7. 通过创建批次日志来修改猪群总数,确保数据可追溯
now := time.Now()
// 7.1 记录源猪群数量减少
reasonOutBatch := fmt.Sprintf("整栏划拨: %d头猪随猪栏 %v 从批次 %v 划拨至批次 %v。备注: %s", quantity, pen.PenNumber, fromBatch.BatchNumber, toBatchID, remarks)
err = s.updatePigBatchQuantityTx(tx, operatorID, fromBatchID, models.ChangeTypeTransferOut, -quantity, reasonOutBatch, now)
if err != nil {
return fmt.Errorf("更新源猪群 %v 数量失败: %w", fromBatch.BatchNumber, err)
}
// 7.2 记录目标猪群数量增加
reasonInBatch := fmt.Sprintf("整栏划拨: %v头猪随猪栏 %v 从批次 %v 划拨入。备注: %s", quantity, pen.PenNumber, fromBatch.BatchNumber, remarks)
err = s.updatePigBatchQuantityTx(tx, operatorID, toBatchID, models.ChangeTypeTransferIn, quantity, reasonInBatch, now)
if err != nil {
return fmt.Errorf("更新目标猪群 %v 数量失败: %w", toBatch.BatchNumber, err)
}
return nil
})
}

View File

@@ -0,0 +1,483 @@
package pig
import (
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// RecordSickPigs 记录新增病猪事件。
func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
if quantity <= 0 {
return errors.New("新增病猪数量必须大于0")
}
var err error
// 1. 开启事务
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1.1 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
}
if !batch.IsActive() {
return fmt.Errorf("批次 %d 不活跃,无法记录病猪事件", batchID)
}
// 1.2 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
}
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
}
// 1.3 检查剩余健康猪不能少于即将转化的病猪数量
totalPigsInBatch, err := s.getCurrentPigQuantityTx(tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 总猪只数量失败: %w", batchID, err)
}
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
}
healthyPigs := totalPigsInBatch - currentSickPigs
if healthyPigs < quantity {
return fmt.Errorf("健康猪数量不足,当前健康猪 %d 头,尝试记录病猪 %d 头", healthyPigs, quantity)
}
// 1.4 创建病猪日志
sickLog := &models.PigSickLog{
PigBatchID: batchID,
PenID: penID,
ChangeCount: quantity, // 新增病猪ChangeCount 为正数
Reason: models.SickPigReasonTypeIllness,
TreatmentLocation: treatmentLocation,
Remarks: remarks,
OperatorID: operatorID,
HappenedAt: happenedAt,
}
if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
return fmt.Errorf("处理病猪日志失败: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("记录新增病猪事件失败: %w", err)
}
return nil
}
// RecordSickPigRecovery 记录病猪康复事件。
func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
if quantity <= 0 {
return errors.New("康复猪只数量必须大于0")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
}
if !batch.IsActive() {
return fmt.Errorf("批次 %d 不活跃,无法记录病猪康复事件", batchID)
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
}
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
}
// 3. 检查当前病猪数量是否足够康复
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
}
if currentSickPigs < quantity {
return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试康复 %d 头", currentSickPigs, quantity)
}
// 4. 创建病猪日志
sickLog := &models.PigSickLog{
PigBatchID: batchID,
PenID: penID,
ChangeCount: -quantity, // 康复病猪ChangeCount 为负数
Reason: models.SickPigReasonTypeRecovery,
TreatmentLocation: treatmentLocation,
Remarks: remarks,
OperatorID: operatorID,
HappenedAt: happenedAt,
}
if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
return fmt.Errorf("处理病猪康复日志失败: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("记录病猪康复事件失败: %w", err)
}
return nil
}
// RecordSickPigDeath 记录病猪死亡事件。
func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
if quantity <= 0 {
return errors.New("死亡猪只数量必须大于0")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
}
if !batch.IsActive() {
return fmt.Errorf("批次 %d 不活跃,无法记录病猪死亡事件", batchID)
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
}
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
}
// 3. 检查当前病猪数量是否足够死亡
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
}
if currentSickPigs < quantity {
return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试记录死亡 %d 头", currentSickPigs, quantity)
}
// 4. 检查猪栏内猪只数量是否足够死亡
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
}
if currentPigsInPen < quantity {
return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录死亡 %d 头", penID, currentPigsInPen, quantity)
}
// 5. 创建病猪日志 (减少病猪数量)
sickLog := &models.PigSickLog{
PigBatchID: batchID,
PenID: penID,
ChangeCount: -quantity, // 死亡病猪ChangeCount 为负数
Reason: models.SickPigReasonTypeDeath,
TreatmentLocation: treatmentLocation,
Remarks: remarks,
OperatorID: operatorID,
HappenedAt: happenedAt,
}
if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
return fmt.Errorf("处理病猪死亡日志失败: %w", err)
}
// 6. 更新批次总猪只数量 (减少批次总数)
if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil {
return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
}
// 7. 记录猪只转移日志 (减少猪栏内猪只数量)
transferLog := &models.PigTransferLog{
TransferTime: happenedAt,
PigBatchID: batchID,
PenID: penID,
Quantity: -quantity, // 减少猪只数量
Type: models.PigTransferTypeDeath,
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
return fmt.Errorf("记录猪只死亡转移日志失败: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("记录病猪死亡事件失败: %w", err)
}
return nil
}
// RecordSickPigCull 记录病猪淘汰事件。
func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
if quantity <= 0 {
return errors.New("淘汰猪只数量必须大于0")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
}
if !batch.IsActive() {
return fmt.Errorf("批次 %d 不活跃,无法记录病猪淘汰事件", batchID)
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
}
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
}
// 3. 检查当前病猪数量是否足够淘汰
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
}
if currentSickPigs < quantity {
return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试淘汰 %d 头", currentSickPigs, quantity)
}
// 4. 检查猪栏内猪只数量是否足够淘汰
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
}
if currentPigsInPen < quantity {
return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录淘汰 %d 头", penID, currentPigsInPen, quantity)
}
// 5. 创建病猪日志 (减少病猪数量)
sickLog := &models.PigSickLog{
PigBatchID: batchID,
PenID: penID,
ChangeCount: -quantity, // 淘汰病猪ChangeCount 为负数
Reason: models.SickPigReasonTypeEliminate,
TreatmentLocation: treatmentLocation,
Remarks: remarks,
OperatorID: operatorID,
HappenedAt: happenedAt,
}
if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
return fmt.Errorf("处理病猪淘汰日志失败: %w", err)
}
// 6. 更新批次总猪只数量 (减少批次总数)
if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil {
return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
}
// 7. 记录猪只转移日志 (减少猪栏内猪只数量)
transferLog := &models.PigTransferLog{
TransferTime: happenedAt,
PigBatchID: batchID,
PenID: penID,
Quantity: -quantity, // 减少猪只数量
Type: models.PigTransferTypeCull, // 淘汰类型
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
return fmt.Errorf("记录猪只淘汰转移日志失败: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("记录病猪淘汰事件失败: %w", err)
}
return nil
}
// RecordDeath 记录正常猪只死亡事件。
func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
if quantity <= 0 {
return errors.New("死亡猪只数量必须大于0")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
}
if !batch.IsActive() {
return fmt.Errorf("批次 %d 不活跃,无法记录死亡事件", batchID)
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
}
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
}
// 3. 检查猪栏内猪只数量是否足够死亡
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
}
if currentPigsInPen < quantity {
return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录死亡 %d 头", penID, currentPigsInPen, quantity)
}
// 4. 更新批次总猪只数量 (减少批次总数)
if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil {
return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
}
// 5. 记录猪只转移日志 (减少猪栏内猪只数量)
transferLog := &models.PigTransferLog{
TransferTime: happenedAt,
PigBatchID: batchID,
PenID: penID,
Quantity: -quantity, // 减少猪只数量
Type: models.PigTransferTypeDeath,
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
return fmt.Errorf("记录猪只死亡转移日志失败: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("记录正常猪只死亡事件失败: %w", err)
}
return nil
}
// RecordCull 记录正常猪只淘汰事件。
func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
if quantity <= 0 {
return errors.New("淘汰猪只数量必须大于0")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
}
return fmt.Errorf("获取批次 %d 失败: %w", batchID, err)
}
if !batch.IsActive() {
return fmt.Errorf("批次 %d 不活跃,无法记录淘汰事件", batchID)
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err)
}
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID)
}
// 3. 检查猪栏内猪只数量是否足够淘汰
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
}
if currentPigsInPen < quantity {
return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录淘汰 %d 头", penID, currentPigsInPen, quantity)
}
// 4. 更新批次总猪只数量 (减少批次总数)
if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil {
return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
}
// 5. 记录猪只转移日志 (减少猪栏内猪只数量)
transferLog := &models.PigTransferLog{
TransferTime: happenedAt,
PigBatchID: batchID,
PenID: penID,
Quantity: -quantity, // 减少猪只数量
Type: models.PigTransferTypeCull,
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
return fmt.Errorf("记录猪只淘汰转移日志失败: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("记录正常猪只淘汰事件失败: %w", err)
}
return nil
}

View File

@@ -0,0 +1,154 @@
package pig
import (
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// SellPigs 处理批量销售猪的业务逻辑。
func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
if quantity <= 0 {
return errors.New("销售数量必须大于0")
}
// 1. 校验猪栏信息
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
}
// 校验猪栏是否属于该批次
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return ErrPenNotAssociatedWithBatch
}
// 2. 业务校验:检查销售数量是否超过猪栏当前猪只数
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err)
}
if quantity > currentPigsInPen {
return fmt.Errorf("销售数量 %d 超过猪栏 %d 当前猪只数 %d", quantity, penID, currentPigsInPen)
}
// 3. 记录销售交易 (财务)
sale := &models.PigSale{
PigBatchID: batchID,
SaleDate: tradeDate,
Buyer: traderName,
Quantity: quantity,
UnitPrice: unitPrice,
TotalPrice: tatalPrice, // 总价不一定是单价x数量, 所以要传进来
Remarks: remarks,
OperatorID: operatorID,
}
if err := s.tradeSvc.SellPig(tx, sale); err != nil {
return fmt.Errorf("记录销售交易失败: %w", err)
}
// 4. 创建猪只转移日志 (物理)
transferLog := &models.PigTransferLog{
TransferTime: tradeDate,
PigBatchID: batchID,
PenID: penID,
Quantity: -quantity, // 销售导致数量减少
Type: models.PigTransferTypeSale,
OperatorID: operatorID,
Remarks: fmt.Sprintf("销售给 %s", traderName),
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
return fmt.Errorf("创建猪只转移日志失败: %w", err)
}
// 5. 记录批次数量变更日志 (逻辑)
if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeSale, -quantity,
fmt.Sprintf("猪批次 %d 从猪栏 %d 销售 %d 头猪给 %s", batchID, penID, quantity, traderName),
tradeDate); err != nil {
return fmt.Errorf("更新猪批次数量失败: %w", err)
}
return nil
})
}
// BuyPigs 处理批量购买猪的业务逻辑。
func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
if quantity <= 0 {
return errors.New("采购数量必须大于0")
}
// 1. 校验猪栏信息
pen, err := s.transferSvc.GetPenByID(tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err)
}
// 校验猪栏是否属于该批次
if pen.PigBatchID == nil || *pen.PigBatchID != batchID {
return ErrPenNotAssociatedWithBatch
}
// 2. 业务校验:检查猪栏容量,如果超出,在备注中记录警告
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err)
}
transferRemarks := fmt.Sprintf("从 %s 采购", traderName)
if currentPigsInPen+quantity > pen.Capacity {
warning := fmt.Sprintf("[警告]猪栏容量超出: 当前 %d, 采购 %d, 容量 %d.", currentPigsInPen, quantity, pen.Capacity)
transferRemarks = fmt.Sprintf("%s %s", transferRemarks, warning)
}
// 3. 记录采购交易 (财务)
purchase := &models.PigPurchase{
PigBatchID: batchID,
PurchaseDate: tradeDate,
Supplier: traderName,
Quantity: quantity,
UnitPrice: unitPrice,
TotalPrice: totalPrice, // 总价不一定是单价x数量, 所以要传进来
Remarks: remarks, // 用户传入的备注
OperatorID: operatorID,
}
if err := s.tradeSvc.BuyPig(tx, purchase); err != nil {
return fmt.Errorf("记录采购交易失败: %w", err)
}
// 4. 创建猪只转移日志 (物理)
transferLog := &models.PigTransferLog{
TransferTime: tradeDate,
PigBatchID: batchID,
PenID: penID,
Quantity: quantity, // 采购导致数量增加
Type: models.PigTransferTypePurchase,
OperatorID: operatorID,
Remarks: transferRemarks, // 包含系统生成的备注和潜在的警告
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
return fmt.Errorf("创建猪只转移日志失败: %w", err)
}
// 5. 记录批次数量变更日志 (逻辑)
if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeBuy, quantity,
fmt.Sprintf("猪批次 %d 在猪栏 %d 采购 %d 头猪从 %s", batchID, penID, quantity, traderName),
tradeDate); err != nil {
return fmt.Errorf("更新猪批次数量失败: %w", err)
}
return nil
})
}

View File

@@ -0,0 +1,127 @@
package pig
import (
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"gorm.io/gorm"
)
// SickPigManager 定义了与病猪管理相关的操作接口。
// 这是一个领域服务,负责协调病猪记录、用药等业务逻辑。
type SickPigManager interface {
// ProcessSickPigLog 处理病猪相关的日志事件。
// log 包含事件的基本信息,如 PigBatchID, PenID, PigIDs, ChangeCount, Reason, TreatmentLocation, Remarks, OperatorID, HappenedAt。
// Manager 内部会计算并填充 BeforeCount 和 AfterCount并进行必要的业务校验和副作用处理。
ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog) error
// GetCurrentSickPigCount 获取指定批次当前患病猪只的总数
GetCurrentSickPigCount(tx *gorm.DB, batchID uint) (int, error)
}
// sickPigManager 是 SickPigManager 接口的具体实现。
// 它依赖于仓库接口来执行数据持久化操作。
type sickPigManager struct {
sickLogRepo repository.PigSickLogRepository
medicationLogRepo repository.MedicationLogRepository
}
// NewSickPigManager 是 sickPigManager 的构造函数。
func NewSickPigManager(
sickLogRepo repository.PigSickLogRepository,
medicationLogRepo repository.MedicationLogRepository,
) SickPigManager {
return &sickPigManager{
sickLogRepo: sickLogRepo,
medicationLogRepo: medicationLogRepo,
}
}
func (s *sickPigManager) ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog) error {
// 1. 输入校验
if log == nil {
return errors.New("病猪日志不能为空")
}
// 关键字段校验
var missingFields []string
if log.PigBatchID == 0 {
missingFields = append(missingFields, "PigBatchID")
}
if log.ChangeCount == 0 {
missingFields = append(missingFields, "ChangeCount")
}
if log.Reason == "" {
missingFields = append(missingFields, "Reason")
}
if log.TreatmentLocation == "" {
missingFields = append(missingFields, "TreatmentLocation")
}
if log.HappenedAt.IsZero() {
missingFields = append(missingFields, "HappenedAt")
}
if log.OperatorID == 0 {
missingFields = append(missingFields, "OperatorID")
}
if log.PenID == 0 {
missingFields = append(missingFields, "PenID")
}
if len(missingFields) > 0 {
return fmt.Errorf("以下关键字段不能为空或零值: %v", missingFields)
}
// 业务规则校验 - ChangeCount 与 Reason 的一致性
switch log.Reason {
case models.SickPigReasonTypeIllness, models.SickPigReasonTypeTransferIn:
if log.ChangeCount < 0 {
return fmt.Errorf("原因 '%s' 的 ChangeCount 必须为正数", log.Reason)
}
case models.SickPigReasonTypeRecovery, models.SickPigReasonTypeDeath, models.SickPigReasonTypeEliminate, models.SickPigReasonTypeTransferOut:
if log.ChangeCount > 0 {
return fmt.Errorf("原因 '%s' 的 ChangeCount 必须为负数", log.Reason)
}
case models.SickPigReasonTypeOther:
// 其他原因ChangeCount 可以是任意值但不能为0
if log.ChangeCount == 0 {
return errors.New("原因 '其他' 的 ChangeCount 不能为零")
}
default:
return fmt.Errorf("未知的病猪日志原因类型: %s", log.Reason)
}
// 2. 获取当前病猪数量 (BeforeCount)
beforeCount, err := s.GetCurrentSickPigCount(tx, log.PigBatchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", log.PigBatchID, err)
}
log.BeforeCount = beforeCount
// 3. 计算变化后的数量 (AfterCount)
log.AfterCount = log.BeforeCount + log.ChangeCount
// 4. 业务规则校验 - 数量合法性
if log.AfterCount < 0 {
return fmt.Errorf("操作后病猪数量不能为负数,当前 %d变化 %d", log.BeforeCount, log.ChangeCount)
}
// 5. 持久化 PigSickLog
if err := s.sickLogRepo.CreatePigSickLogTx(tx, log); err != nil {
return fmt.Errorf("创建 PigSickLog 失败: %w", err)
}
return nil
}
func (s *sickPigManager) GetCurrentSickPigCount(tx *gorm.DB, batchID uint) (int, error) {
lastLog, err := s.sickLogRepo.GetLastLogByBatchTx(tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil // 如果没有找到任何日志表示当前病猪数量为0
}
return 0, fmt.Errorf("获取批次 %d 的最新病猪日志失败: %w", batchID, err)
}
return lastLog.AfterCount, nil
}

View File

@@ -0,0 +1,46 @@
package pig
import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" // 引入基础设施层的仓库接口
"gorm.io/gorm"
)
// PigTradeManager 定义了与猪只交易相关的操作接口。
// 这是一个领域服务,负责协调业务逻辑。
type PigTradeManager interface {
// SellPig 处理卖猪的业务逻辑,通过仓库接口创建 PigSale 记录。
SellPig(tx *gorm.DB, sale *models.PigSale) error
// BuyPig 处理买猪的业务逻辑,通过仓库接口创建 PigPurchase 记录。
BuyPig(tx *gorm.DB, purchase *models.PigPurchase) error
}
// pigTradeManager 是 PigTradeManager 接口的具体实现。
// 它依赖于 repository.PigTradeRepository 接口来执行数据持久化操作。
type pigTradeManager struct {
tradeRepo repository.PigTradeRepository // 依赖于基础设施层定义的仓库接口
}
// NewPigTradeManager 是 pigTradeManager 的构造函数。
func NewPigTradeManager(tradeRepo repository.PigTradeRepository) PigTradeManager {
return &pigTradeManager{
tradeRepo: tradeRepo,
}
}
// SellPig 实现了卖猪的逻辑。
// 它通过调用 tradeRepo 来持久化销售记录。
func (s *pigTradeManager) SellPig(tx *gorm.DB, sale *models.PigSale) error {
// 在此处可以添加更复杂的卖猪前置校验或业务逻辑
// 例如:检查猪只库存、更新猪只状态等。
return s.tradeRepo.CreatePigSaleTx(tx, sale)
}
// BuyPig 实现了买猪的逻辑。
// 它通过调用 tradeRepo 来持久化采购记录。
func (s *pigTradeManager) BuyPig(tx *gorm.DB, purchase *models.PigPurchase) error {
// 在此处可以添加更复杂的买猪前置校验或业务逻辑
// 例如:检查资金、更新猪只状态等。
return s.tradeRepo.CreatePigPurchaseTx(tx, purchase)
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
"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"

View File

@@ -5,7 +5,7 @@ import (
"sync"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
"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"
@@ -435,7 +435,7 @@ func (s *Scheduler) handlePlanCompletion(planLogID uint) {
// 如果是自动计划且达到执行次数上限,或计划是手动类型,则更新计划状态为已停止
if (plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteNum > 0 && newExecuteCount >= plan.ExecuteNum) || plan.ExecutionType == models.PlanExecutionTypeManual {
newStatus = models.PlanStatusStopeed
newStatus = models.PlanStatusStopped
s.logger.Infof("计划 %d 已完成执行,状态更新为 '执行完毕'。", topLevelPlanID)
}

View File

@@ -160,11 +160,22 @@ func (ps *PostgresStorage) creatingHyperTable() error {
{models.TaskExecutionLog{}, "created_at"},
{models.PendingCollection{}, "created_at"},
{models.UserActionLog{}, "time"},
{models.RawMaterialPurchase{}, "purchase_date"},
{models.RawMaterialStockLog{}, "happened_at"},
{models.FeedUsageRecord{}, "recorded_at"},
{models.MedicationLog{}, "happened_at"},
{models.PigBatchLog{}, "happened_at"},
{models.WeighingBatch{}, "weighing_time"},
{models.WeighingRecord{}, "weighing_time"},
{models.PigTransferLog{}, "transfer_time"},
{models.PigSickLog{}, "happened_at"},
{models.PigPurchase{}, "purchase_date"},
{models.PigSale{}, "sale_date"},
}
for _, table := range tablesToConvert {
tableName := table.model.TableName()
chunkInterval := "1 day" // 统一设置为1天
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 {
@@ -189,6 +200,17 @@ 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.MedicationLog{}, "pig_batch_id"},
{models.PigBatchLog{}, "pig_batch_id"},
{models.WeighingBatch{}, "pig_batch_id"},
{models.WeighingRecord{}, "weighing_batch_id"},
{models.PigTransferLog{}, "pig_batch_id"},
{models.PigSickLog{}, "pig_batch_id"},
{models.PigPurchase{}, "pig_batch_id"},
{models.PigSale{}, "pig_batch_id"},
}
for _, policy := range policies {
@@ -239,14 +261,5 @@ func (ps *PostgresStorage) creatingIndex() error {
}
ps.logger.Info("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
// 为 devices 表的 properties 字段创建 GIN 索引
//ps.logger.Info("正在为 devices 表的 properties 字段创建 GIN 索引")
//ginDevicePropertiesIndexSQL := "CREATE INDEX IF NOT EXISTS idx_devices_properties_gin ON devices USING GIN (properties);"
//if err := ps.db.Exec(ginDevicePropertiesIndexSQL).Error; err != nil {
// ps.logger.Errorw("为 devices 的 properties 字段创建 GIN 索引失败", "error", err)
// return fmt.Errorf("为 devices 的 properties 字段创建 GIN 索引失败: %w", err)
//}
//ps.logger.Info("成功为 devices 的 properties 字段创建 GIN 索引 (或已存在)")
return nil
}

View File

@@ -15,9 +15,9 @@ type DeviceCategory string
const (
// CategoryActuator 代表一个执行器,可以被控制(例如:风机、阀门)
CategoryActuator DeviceCategory = "actuator"
CategoryActuator DeviceCategory = "执行器"
// CategorySensor 代表一个传感器,用于报告测量值(例如:温度计)
CategorySensor DeviceCategory = "sensor"
CategorySensor DeviceCategory = "传感器"
)
// ValueDescriptor 描述了传感器可以报告的单个数值。

View File

@@ -17,11 +17,11 @@ const (
type ExecutionStatus string
const (
ExecutionStatusStarted ExecutionStatus = "started" // 开始执行
ExecutionStatusCompleted ExecutionStatus = "completed" // 执行完成
ExecutionStatusFailed ExecutionStatus = "failed" // 执行失败
ExecutionStatusCancelled ExecutionStatus = "cancelled" // 执行取消
ExecutionStatusWaiting ExecutionStatus = "waiting" // 等待执行 (用于预写日志)
ExecutionStatusStarted ExecutionStatus = "已开始" // 开始执行
ExecutionStatusCompleted ExecutionStatus = "已完成" // 执行完成
ExecutionStatusFailed ExecutionStatus = "失败" // 执行失败
ExecutionStatusCancelled ExecutionStatus = "已取消" // 执行取消
ExecutionStatusWaiting ExecutionStatus = "等待中" // 等待执行 (用于预写日志)
)
// PlanExecutionLog 记录整个计划的一次执行历史
@@ -92,9 +92,9 @@ func (log *TaskExecutionLog) AfterFind(tx *gorm.DB) (err error) {
type PendingCollectionStatus string
const (
PendingStatusPending PendingCollectionStatus = "pending" // 请求已发送,等待设备响应
PendingStatusFulfilled PendingCollectionStatus = "fulfilled" // 已收到设备响应并成功处理
PendingStatusTimedOut PendingCollectionStatus = "timed_out" // 请求超时,未收到设备响应
PendingStatusPending PendingCollectionStatus = "等待中" // 请求已发送,等待设备响应
PendingStatusFulfilled PendingCollectionStatus = "已完成" // 已收到设备响应并成功处理
PendingStatusTimedOut PendingCollectionStatus = "已超时" // 请求超时,未收到设备响应
)
// DeviceCommandLog 记录所有“发后即忘”的下行指令日志。
@@ -160,8 +160,8 @@ func (PendingCollection) TableName() string {
type AuditStatus string
const (
AuditStatusSuccess AuditStatus = "success"
AuditStatusFailed AuditStatus = "failed"
AuditStatusSuccess AuditStatus = "成功"
AuditStatusFailed AuditStatus = "失败"
)
// --- 审计日志相关上下文键 ---

View File

@@ -0,0 +1,39 @@
package models
import (
"gorm.io/gorm"
)
/*
猪场固定资产相关模型
*/
// PigHouse 定义了猪舍,是猪栏的集合
type PigHouse struct {
gorm.Model
Name string `gorm:"size:100;not null;unique;comment:猪舍名称, 如 '育肥舍A栋'"`
Description string `gorm:"size:255;comment:描述信息"`
Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏
}
// PenStatus 定义了猪栏的当前状态
type PenStatus string
const (
PenStatusEmpty PenStatus = "空闲"
PenStatusOccupied PenStatus = "使用中"
PenStatusSickPen PenStatus = "病猪栏"
PenStatusRecovering PenStatus = "康复栏"
PenStatusCleaning PenStatus = "清洗消毒"
PenStatusUnderMaint PenStatus = "维修中"
)
// Pen 是猪栏的物理实体模型, 是所有空间相关数据的“锚点”
type Pen struct {
gorm.Model
PenNumber string `gorm:"not null;comment:猪栏的唯一编号, 如 A-01"`
HouseID uint `gorm:"index;comment:所属猪舍ID"`
PigBatchID *uint `gorm:"index;comment:关联的猪批次ID"`
Capacity int `gorm:"not null;comment:设计容量 (头)"`
Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"`
}

View File

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

View File

@@ -0,0 +1,99 @@
package models
import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
/*
所有与药品、疫苗和用药记录相关的模型
*/
// MedicationType 定义了兽药的类型
type MedicationType string
const (
Powder MedicationType = "粉剂"
Injection MedicationType = "针剂"
Vaccine MedicationType = "疫苗"
)
// MedicationCategory 定义了兽药的种类
type MedicationCategory string
const (
Tetracycline MedicationCategory = "四环素类"
Sulfonamide MedicationCategory = "磺胺类"
Penicillin MedicationCategory = "青霉素类"
Macrolide MedicationCategory = "大环内酯类"
Quinolone MedicationCategory = "喹诺酮类"
Anthelmintic MedicationCategory = "驱虫药"
Disinfectant MedicationCategory = "消毒药"
BiologicalProduct MedicationCategory = "生物制品"
)
// MixType 定义了粉剂药物该如何混合
type MixType string
const (
MixFeed = "饲料加药"
MixWater = "水中加药"
)
// PowderInstructions 定义了粉剂使用说明.
// 在程序中, 可以将 Medication.Instructions 字段反序列化为此结构进行操作.
type PowderInstructions struct {
// 出栏前停药期
WithdrawalPeriod time.Duration `json:"withdrawal_period"`
// 拌料使用计量, 每千克体重用多少克药, 单位: g/kg
BodyWeightDosageUsed float64 `json:"body_weight_dosage_used"`
// 拌料使用剂量, 每升水加多少克药或每千克饲料干重加多少克药, 单位: g/kg(L)
MixDosageUsed float64 `json:"mix_dosage_used"`
// 拌料使用方式, 兑水/拌料
MixType MixType `json:"mix_type"`
}
// Medication 定义了兽药/疫苗的基本信息模型
type Medication struct {
gorm.Model
Name string `gorm:"size:100;not null;comment:药品名称" json:"name"`
Type MedicationType `gorm:"size:20;not null;comment:兽药类型 (粉剂, 针剂, 疫苗)" json:"type"`
Category MedicationCategory `gorm:"size:30;not null;comment:兽药种类 (四环素类, 磺胺类等)" json:"category"`
DosagePerUnit float64 `gorm:"size:50;comment:一份药物的计量 (针剂计量单位为毫升, 粉剂为克)" json:"dosage_per_unit"`
ActiveIngredientConcentration float64 `gorm:"size:50;comment:有效成分含量百分比" json:"active_ingredient_concentration"`
Manufacturer string `gorm:"size:100;comment:生产厂家" json:"manufacturer"`
Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"`
}
func (Medication) TableName() string {
return "medications"
}
// MedicationReasonType 定义了用药原因
type MedicationReasonType string
const (
ReasonTypePreventive MedicationReasonType = "预防"
ReasonTypeTreatment MedicationReasonType = "治疗"
ReasonTypeHealthCare MedicationReasonType = "保健"
)
// MedicationLog 记录了对整个猪批次的用药情况
type MedicationLog struct {
gorm.Model
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
MedicationID uint `gorm:"not null;index;comment:关联的药品ID"`
Medication Medication `gorm:"foreignKey:MedicationID"` // 预加载药品信息
DosageUsed float64 `gorm:"not null;comment:使用的总剂量 (单位由药品决定,如g或ml)"`
TargetCount int `gorm:"not null;comment:用药对象数量"`
Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"`
Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"`
OperatorID uint `gorm:"comment:操作员ID"`
HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"`
}
func (MedicationLog) TableName() string {
return "medication_logs"
}

View File

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

View File

@@ -0,0 +1,108 @@
package models
import (
"time"
"gorm.io/gorm"
)
/*
和猪只、猪群本身相关的模型
*/
// PigBatchStatus 定义了猪批次所处的不同阶段或状态
type PigBatchStatus string
const (
BatchStatusWeaning PigBatchStatus = "保育" // 从断奶到保育结束
BatchStatusGrowing PigBatchStatus = "生长" // 生长育肥阶段
BatchStatusFinishing PigBatchStatus = "育肥" // 最后的育肥阶段
BatchStatusForSale PigBatchStatus = "待售" // 达到出栏标准
BatchStatusSold PigBatchStatus = "已出售"
BatchStatusArchived PigBatchStatus = "已归档" // 批次结束(如全群淘汰等)
)
// PigBatchOriginType 定义了猪批次的来源
type PigBatchOriginType string
const (
OriginTypeSelfFarrowed PigBatchOriginType = "自繁"
OriginTypePurchased PigBatchOriginType = "外购"
)
// PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪
type PigBatch struct {
gorm.Model
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:初始数量"`
Status PigBatchStatus `gorm:"size:20;not null;index;comment:批次状态"`
}
func (PigBatch) TableName() string {
return "pig_batches"
}
// IsActive 判断猪批次是否处于活跃状态
func (pb PigBatch) IsActive() bool {
return pb.Status != BatchStatusSold && pb.Status != BatchStatusArchived
}
// LogChangeType 定义了猪批次数量变更的类型
type LogChangeType string
const (
ChangeTypeDeath LogChangeType = "死亡"
ChangeTypeCull LogChangeType = "淘汰"
ChangeTypeSale LogChangeType = "销售"
ChangeTypeBuy LogChangeType = "购买"
ChangeTypeTransferIn LogChangeType = "转入"
ChangeTypeTransferOut LogChangeType = "转出"
ChangeTypeCorrection LogChangeType = "盘点校正"
)
// PigBatchLog 记录了猪批次数量或状态的每一次变更
type PigBatchLog struct {
gorm.Model
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"`
ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"`
Reason string `gorm:"size:255;comment:变更原因描述"`
BeforeCount int `gorm:"not null;comment:变更前总数"`
AfterCount int `gorm:"not null;comment:变更后总数"`
OperatorID uint `gorm:"comment:操作员ID"`
HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"`
}
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"
}

View File

@@ -0,0 +1,47 @@
package models
import (
"time"
"gorm.io/gorm"
)
// PigBatchSickPigTreatmentLocation 定义了病猪治疗地点
type PigBatchSickPigTreatmentLocation string
const (
TreatmentLocationOnSite PigBatchSickPigTreatmentLocation = "原地治疗"
TreatmentLocationSickBay PigBatchSickPigTreatmentLocation = "病猪栏治疗"
)
// PigBatchSickPigReasonType 定义了病猪变化的原因类型
type PigBatchSickPigReasonType string
const (
SickPigReasonTypeIllness PigBatchSickPigReasonType = "患病" // 猪只患病
SickPigReasonTypeRecovery PigBatchSickPigReasonType = "康复" // 猪只康复
SickPigReasonTypeDeath PigBatchSickPigReasonType = "死亡" // 猪只死亡
SickPigReasonTypeEliminate PigBatchSickPigReasonType = "淘汰" // 猪只淘汰
SickPigReasonTypeTransferIn PigBatchSickPigReasonType = "转入" // 病猪转入当前批次
SickPigReasonTypeTransferOut PigBatchSickPigReasonType = "转出" // 病猪转出当前批次 (例如转到其他批次或出售)
SickPigReasonTypeOther PigBatchSickPigReasonType = "其他" // 其他原因
)
// PigSickLog 记录了猪批次中病猪数量的变化日志
type PigSickLog struct {
gorm.Model
PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"`
PenID uint `gorm:"not null;index;comment:所在猪圈ID"`
ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"`
Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"`
BeforeCount int `gorm:"comment:变化前的数量"`
AfterCount int `gorm:"comment:变化后的数量"`
Remarks string `gorm:"size:255;comment:备注"`
TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"`
OperatorID uint `gorm:"comment:操作员ID"`
HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"`
}
func (PigSickLog) TableName() string {
return "pig_sick_logs"
}

View File

@@ -0,0 +1,41 @@
package models
import (
"time"
"gorm.io/gorm"
)
// PigPurchase 记录了猪只采购信息
type PigPurchase struct {
gorm.Model
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
Supplier string `gorm:"comment:供应商"`
Quantity int `gorm:"not null;comment:采购数量"`
UnitPrice float64 `gorm:"not null;comment:单价"`
TotalPrice float64 `gorm:"not null;comment:总价"`
Remarks string `gorm:"size:255;comment:备注"`
OperatorID uint `gorm:"comment:操作员ID"`
}
func (PigPurchase) TableName() string {
return "pig_purchases"
}
// PigSale 记录了猪只销售信息
type PigSale struct {
gorm.Model
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
SaleDate time.Time `gorm:"primaryKey;comment:销售日期"`
Buyer string `gorm:"comment:购买方"`
Quantity int `gorm:"not null;comment:销售数量"`
UnitPrice float64 `gorm:"not null;comment:单价"`
TotalPrice float64 `gorm:"not null;comment:总价"`
Remarks string `gorm:"size:255;comment:备注"`
OperatorID uint `gorm:"comment:操作员ID"`
}
func (PigSale) TableName() string {
return "pig_sales"
}

View File

@@ -0,0 +1,39 @@
package models
import (
"time"
"gorm.io/gorm"
)
// PigTransferType 定义了猪只迁移的类型
type PigTransferType string
const (
PigTransferTypeInternal PigTransferType = "群内调栏" // 同一猪群内猪栏间的调动
PigTransferTypeCrossBatch PigTransferType = "跨群调栏" // 不同猪群间的调动
PigTransferTypeSale PigTransferType = "销售" // 猪只售出
PigTransferTypeDeath PigTransferType = "死亡" // 猪只死亡
PigTransferTypeCull PigTransferType = "淘汰" // 猪只淘汰
PigTransferTypePurchase PigTransferType = "新购入" // 新购入猪只
PigTransferTypeDeliveryRoomTransfor PigTransferType = "产房转入" // 产房转入
// 可以根据业务需求添加更多类型,例如:转出到其他农场等
)
// PigTransferLog 记录了每一次猪只数量在猪栏间的变动事件。
// 它作为事件溯源的基础,用于推算任意时间点猪栏的猪只数量。
type PigTransferLog struct {
gorm.Model
TransferTime time.Time `gorm:"primaryKey;comment:迁移发生时间" json:"transfer_time"` // 迁移发生时间,作为联合主键
PigBatchID uint `gorm:"primaryKey;comment:关联的猪群ID" json:"pig_batch_id"` // 关联的猪群ID作为联合主键
PenID uint `gorm:"primaryKey;comment:发生变动的猪栏ID" json:"pen_id"` // 发生变动的猪栏ID作为联合主键
Quantity int `gorm:"not null;comment:变动数量(正数表示增加,负数表示减少)" json:"quantity"` // 变动数量(正数表示增加,负数减少)
Type PigTransferType `gorm:"not null;comment:变动类型" json:"type"` // 变动类型,使用枚举类型
CorrelationID string `gorm:"comment:用于关联一次完整操作(如一次调栏会产生两条日志)" json:"correlation_id"` // 用于关联一次完整操作
OperatorID uint `gorm:"not null;comment:操作员ID" json:"operator_id"` // 操作员ID
Remarks string `gorm:"comment:备注" json:"remarks"`
}
func (p PigTransferLog) TableName() string {
return "pig_transfer_logs"
}

View File

@@ -15,25 +15,25 @@ import (
type PlanExecutionType string
const (
PlanExecutionTypeAutomatic PlanExecutionType = "automatic" // 自动执行 (包含定时和循环)
PlanExecutionTypeManual PlanExecutionType = "manual" // 手动执行
PlanExecutionTypeAutomatic PlanExecutionType = "自动" // 自动执行 (包含定时和循环)
PlanExecutionTypeManual PlanExecutionType = "手动" // 手动执行
)
// PlanContentType 定义了计划包含的内容类型
type PlanContentType string
const (
PlanContentTypeSubPlans PlanContentType = "sub_plans" // 计划包含子计划
PlanContentTypeTasks PlanContentType = "tasks" // 计划包含任务
PlanContentTypeSubPlans PlanContentType = "子计划" // 计划包含子计划
PlanContentTypeTasks PlanContentType = "任务" // 计划包含任务
)
// TaskType 定义了任务的类型,每个类型可以对应 task 包中的一个具体动作
type TaskType string
const (
TaskPlanAnalysis TaskType = "plan_analysis" // 解析Plan的Task列表并添加到待执行队列的特殊任务
TaskTypeWaiting TaskType = "waiting" // 等待任务
TaskTypeReleaseFeedWeight TaskType = "release_feed_weight" // 下料口释放指定重量任务
TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务
TaskTypeWaiting TaskType = "等待" // 等待任务
TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务
)
// -- Task Parameters --
@@ -42,13 +42,14 @@ const (
ParamsPlanID = "plan_id"
)
type PlanStatus uint8
// PlanStatus 定义了计划的状态
type PlanStatus string
const (
PlanStatusDisabled PlanStatus = 0 // 禁用计划
PlanStatusEnabled PlanStatus = 1 // 启用计划
PlanStatusStopeed PlanStatus = 2 // 执行完毕
PlanStatusFailed PlanStatus = 3 // 执行失败
PlanStatusDisabled PlanStatus = "已禁用" // 禁用计划
PlanStatusEnabled PlanStatus = "已启用" // 启用计划
PlanStatusStopped PlanStatus = "执行完毕" // 执行完毕
PlanStatusFailed PlanStatus = "执行失败" // 执行失败
)
// Plan 代表系统中的一个计划,可以包含子计划或任务
@@ -58,7 +59,7 @@ type Plan struct {
Name string `gorm:"not null" json:"name"`
Description string `json:"description"`
ExecutionType PlanExecutionType `gorm:"not null;index" json:"execution_type"`
Status PlanStatus `gorm:"default:0;index" json:"status"` // 计划是否被启动
Status PlanStatus `gorm:"default:'已禁用';index" json:"status"` // 计划是否被启动
ExecuteNum uint `gorm:"default:0" json:"execute_num"` // 计划预期执行次数
ExecuteCount uint `gorm:"default:0" json:"execute_count"` // 执行计数器

View File

@@ -10,11 +10,11 @@ import (
type SensorType string
const (
SensorTypeSignalMetrics SensorType = "signal_metrics" // 信号强度
SensorTypeBatteryLevel SensorType = "battery_level" // 电池电量
SensorTypeTemperature SensorType = "temperature" // 温度
SensorTypeHumidity SensorType = "humidity" // 湿度
SensorTypeWeight SensorType = "weight" // 重量
SensorTypeSignalMetrics SensorType = "信号强度" // 信号强度
SensorTypeBatteryLevel SensorType = "电池电量" // 电池电量
SensorTypeTemperature SensorType = "温度" // 温度
SensorTypeHumidity SensorType = "湿度" // 湿度
SensorTypeWeight SensorType = "重量" // 重量
)
// SignalMetrics 存储信号强度数据

View File

@@ -96,7 +96,7 @@ func (r *gormExecutionLogRepository) CreateTaskExecutionLogsInBatch(logs []*mode
if len(logs) == 0 {
return nil
}
// GORM 的 Create 传入一个切片指针会执行批量插入。
// GORM 的 CreateTx 传入一个切片指针会执行批量插入。
return r.db.Create(&logs).Error
}

View File

@@ -0,0 +1,26 @@
package repository
import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// MedicationLogRepository 定义了与群体用药日志模型相关的数据库操作接口。
type MedicationLogRepository interface {
CreateMedicationLog(log *models.MedicationLog) error
}
// gormMedicationLogRepository 是 MedicationLogRepository 接口的 GORM 实现。
type gormMedicationLogRepository struct {
db *gorm.DB
}
// NewGormMedicationLogRepository 创建一个新的 MedicationLogRepository GORM 实现实例。
func NewGormMedicationLogRepository(db *gorm.DB) MedicationLogRepository {
return &gormMedicationLogRepository{db: db}
}
// CreateMedicationLog 创建一条新的群体用药日志记录
func (r *gormMedicationLogRepository) CreateMedicationLog(log *models.MedicationLog) error {
return r.db.Create(log).Error
}

View File

@@ -0,0 +1,55 @@
package repository
import (
"time" // 引入 time 包
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigBatchLogRepository 定义了与猪批次日志相关的数据库操作接口。
type PigBatchLogRepository interface {
// CreateTx 在指定的事务中创建一条新的猪批次日志。
CreateTx(tx *gorm.DB, log *models.PigBatchLog) error
// GetLogsByBatchIDAndDateRangeTx 在指定的事务中,获取指定批次在特定时间范围内的所有日志记录。
GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error)
// GetLastLogByBatchIDTx 在指定的事务中,获取某批次的最后一条日志记录。
GetLastLogByBatchIDTx(tx *gorm.DB, batchID uint) (*models.PigBatchLog, error)
}
// gormPigBatchLogRepository 是 PigBatchLogRepository 的 GORM 实现。
type gormPigBatchLogRepository struct {
db *gorm.DB
}
// NewGormPigBatchLogRepository 创建一个新的 PigBatchLogRepository 实例。
func NewGormPigBatchLogRepository(db *gorm.DB) PigBatchLogRepository {
return &gormPigBatchLogRepository{db: db}
}
// Create 实现了创建猪批次日志的逻辑。
func (r *gormPigBatchLogRepository) CreateTx(tx *gorm.DB, log *models.PigBatchLog) error {
return tx.Create(log).Error
}
// GetLogsByBatchIDAndDateRangeTx 实现了在指定的事务中,获取指定批次在特定时间范围内的所有日志记录的逻辑。
func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) {
var logs []*models.PigBatchLog
err := tx.Where("pig_batch_id = ? AND created_at >= ? AND created_at <= ?", batchID, startDate, endDate).Find(&logs).Error
if err != nil {
return nil, err
}
return logs, nil
}
// GetLastLogByBatchIDTx 实现了在指定的事务中,获取某批次的最后一条日志记录的逻辑。
func (r *gormPigBatchLogRepository) GetLastLogByBatchIDTx(tx *gorm.DB, batchID uint) (*models.PigBatchLog, error) {
var log models.PigBatchLog
err := tx.Where("pig_batch_id = ?", batchID).Order("id DESC").First(&log).Error
if err != nil {
return nil, err
}
return &log, nil
}

View File

@@ -0,0 +1,102 @@
package repository
import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigBatchRepository 定义了与猪批次相关的数据库操作接口
type PigBatchRepository interface {
CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error)
CreatePigBatchTx(tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error)
GetPigBatchByID(id uint) (*models.PigBatch, error)
GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error)
// UpdatePigBatch 更新一个猪批次,返回更新后的批次、受影响的行数和错误
UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error)
// DeletePigBatch 根据ID删除一个猪批次返回受影响的行数和错误
DeletePigBatch(id uint) (int64, error)
DeletePigBatchTx(tx *gorm.DB, id uint) (int64, 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) {
return r.CreatePigBatchTx(r.db, batch)
}
// CreatePigBatchTx 在指定的事务中,创建一个新的猪批次
func (r *gormPigBatchRepository) CreatePigBatchTx(tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error) {
if err := tx.Create(batch).Error; err != nil {
return nil, err
}
return batch, nil
}
// GetPigBatchByID 根据ID获取单个猪批次
func (r *gormPigBatchRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) {
return r.GetPigBatchByIDTx(r.db, id)
}
// UpdatePigBatch 更新一个猪批次
func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error) {
result := r.db.Model(&models.PigBatch{}).Where("id = ?", batch.ID).Updates(batch)
if result.Error != nil {
return nil, 0, result.Error
}
// 返回更新后的批次、受影响的行数和错误
return batch, result.RowsAffected, nil
}
// DeletePigBatch 根据ID删除一个猪批次 (GORM 会执行软删除)
func (r *gormPigBatchRepository) DeletePigBatch(id uint) (int64, error) {
return r.DeletePigBatchTx(r.db, id)
}
func (r *gormPigBatchRepository) DeletePigBatchTx(tx *gorm.DB, id uint) (int64, error) {
result := tx.Delete(&models.PigBatch{}, id)
if result.Error != nil {
return 0, result.Error
}
// 返回受影响的行数和错误
return result.RowsAffected, 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
}
// GetPigBatchByIDTx 在指定的事务中通过ID获取单个猪批次
func (r *gormPigBatchRepository) GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) {
var batch models.PigBatch
if err := tx.First(&batch, id).Error; err != nil {
return nil, err
}
return &batch, nil
}

View File

@@ -0,0 +1,79 @@
package repository
import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigFarmRepository 定义了与猪场资产(猪舍、猪栏)相关的数据库操作接口
type PigFarmRepository interface {
// PigHouse methods
CreatePigHouse(house *models.PigHouse) error
GetPigHouseByID(id uint) (*models.PigHouse, error)
ListPigHouses() ([]models.PigHouse, error)
// UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误
UpdatePigHouse(house *models.PigHouse) (int64, error)
// DeletePigHouse 根据ID删除一个猪舍返回受影响的行数和错误
DeletePigHouse(id uint) (int64, error)
CountPensInHouse(houseID uint) (int64, error)
}
// gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现
type gormPigFarmRepository struct {
db *gorm.DB
}
// NewGormPigFarmRepository 创建一个新的 PigFarmRepository GORM 实现实例
func NewGormPigFarmRepository(db *gorm.DB) PigFarmRepository {
return &gormPigFarmRepository{db: db}
}
// --- PigHouse Implementation ---
// CreatePigHouse 创建一个新的猪舍
func (r *gormPigFarmRepository) CreatePigHouse(house *models.PigHouse) error {
return r.db.Create(house).Error
}
// GetPigHouseByID 根据ID获取单个猪舍
func (r *gormPigFarmRepository) GetPigHouseByID(id uint) (*models.PigHouse, error) {
var house models.PigHouse
if err := r.db.First(&house, id).Error; err != nil {
return nil, err
}
return &house, nil
}
// ListPigHouses 列出所有猪舍
func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) {
var houses []models.PigHouse
if err := r.db.Find(&houses).Error; err != nil {
return nil, err
}
return houses, nil
}
// UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误
func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) (int64, error) {
result := r.db.Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house)
if result.Error != nil {
return 0, result.Error
}
return result.RowsAffected, nil
}
// DeletePigHouse 根据ID删除一个猪舍返回受影响的行数和错误
func (r *gormPigFarmRepository) DeletePigHouse(id uint) (int64, error) {
result := r.db.Delete(&models.PigHouse{}, id)
if result.Error != nil {
return 0, result.Error
}
return result.RowsAffected, nil
}
// CountPensInHouse 统计猪舍中的猪栏数量
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
}

View File

@@ -0,0 +1,97 @@
package repository
import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigPenRepository 定义了与猪栏模型相关的数据库操作接口。
type PigPenRepository interface {
CreatePen(pen *models.Pen) error
// GetPenByID 根据ID获取单个猪栏 (非事务性)
GetPenByID(id uint) (*models.Pen, error)
// GetPenByIDTx 根据ID获取单个猪栏 (事务性)
GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error)
ListPens() ([]models.Pen, error)
// UpdatePen 更新一个猪栏,返回受影响的行数和错误
UpdatePen(pen *models.Pen) (int64, error)
// DeletePen 根据ID删除一个猪栏返回受影响的行数和错误
DeletePen(id uint) (int64, error)
// GetPensByBatchIDTx 根据批次ID获取所有关联的猪栏 (事务性)
GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error)
// UpdatePenFieldsTx 更新猪栏的指定字段 (事务性)
UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error
}
// gormPigPenRepository 是 PigPenRepository 接口的 GORM 实现。
type gormPigPenRepository struct {
db *gorm.DB
}
// NewGormPigPenRepository 创建一个新的 PigPenRepository GORM 实现实例。
func NewGormPigPenRepository(db *gorm.DB) PigPenRepository {
return &gormPigPenRepository{db: db}
}
// CreatePen 创建一个新的猪栏
func (r *gormPigPenRepository) CreatePen(pen *models.Pen) error {
return r.db.Create(pen).Error
}
// GetPenByID 根据ID获取单个猪栏 (非事务性)
func (r *gormPigPenRepository) GetPenByID(id uint) (*models.Pen, error) {
return r.GetPenByIDTx(r.db, id) // 非Tx方法直接调用Tx方法
}
// GetPenByIDTx 在指定的事务中通过ID获取单个猪栏信息。
func (r *gormPigPenRepository) GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) {
var pen models.Pen
if err := tx.First(&pen, id).Error; err != nil {
return nil, err
}
return &pen, nil
}
// ListPens 列出所有猪栏
func (r *gormPigPenRepository) ListPens() ([]models.Pen, error) {
var pens []models.Pen
if err := r.db.Find(&pens).Error; err != nil {
return nil, err
}
return pens, nil
}
// UpdatePen 更新一个猪栏,返回受影响的行数和错误
func (r *gormPigPenRepository) UpdatePen(pen *models.Pen) (int64, error) {
result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen)
if result.Error != nil {
return 0, result.Error
}
return result.RowsAffected, nil
}
// DeletePen 根据ID删除一个猪栏返回受影响的行数和错误
func (r *gormPigPenRepository) DeletePen(id uint) (int64, error) {
result := r.db.Delete(&models.Pen{}, id)
if result.Error != nil {
return 0, result.Error
}
return result.RowsAffected, nil
}
// GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。
func (r *gormPigPenRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
var pens []*models.Pen
// 注意PigBatchID 是指针类型,需要处理 nil 值
result := tx.Where("pig_batch_id = ?", batchID).Find(&pens)
if result.Error != nil {
return nil, result.Error
}
return pens, nil
}
// UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。
func (r *gormPigPenRepository) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error {
result := tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates)
return result.Error
}

View File

@@ -0,0 +1,53 @@
package repository
import (
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigSickLogRepository 定义了与病猪日志模型相关的数据库操作接口。
type PigSickLogRepository interface {
// CreatePigSickLog 创建一条新的病猪日志记录
CreatePigSickLog(log *models.PigSickLog) error
CreatePigSickLogTx(tx *gorm.DB, log *models.PigSickLog) error
// GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录
GetLastLogByBatchTx(tx *gorm.DB, batchID uint) (*models.PigSickLog, error)
}
// gormPigSickLogRepository 是 PigSickLogRepository 接口的 GORM 实现。
type gormPigSickLogRepository struct {
db *gorm.DB
}
// NewGormPigSickLogRepository 创建一个新的 PigSickLogRepository GORM 实现实例。
func NewGormPigSickLogRepository(db *gorm.DB) PigSickLogRepository {
return &gormPigSickLogRepository{db: db}
}
// CreatePigSickLog 创建一条新的病猪日志记录
func (r *gormPigSickLogRepository) CreatePigSickLog(log *models.PigSickLog) error {
return r.CreatePigSickLogTx(r.db, log)
}
func (r *gormPigSickLogRepository) CreatePigSickLogTx(tx *gorm.DB, log *models.PigSickLog) error {
return tx.Create(log).Error
}
// GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录
func (r *gormPigSickLogRepository) GetLastLogByBatchTx(tx *gorm.DB, batchID uint) (*models.PigSickLog, error) {
var lastLog models.PigSickLog
err := tx.
Where("pig_batch_id = ?", batchID).
Order("happened_at DESC"). // 按时间降序排列
First(&lastLog).Error // 获取第一条记录 (即最新一条)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound // 明确返回记录未找到错误
}
return nil, err
}
return &lastLog, nil
}

View File

@@ -0,0 +1,36 @@
package repository
import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigTradeRepository 定义了猪只交易数据持久化的接口。
// 领域服务通过此接口与数据层交互,实现解耦。
type PigTradeRepository interface {
// CreatePigSaleTx 在数据库中创建一条猪只销售记录。
CreatePigSaleTx(tx *gorm.DB, sale *models.PigSale) error
// CreatePigPurchaseTx 在数据库中创建一条猪只采购记录。
CreatePigPurchaseTx(tx *gorm.DB, purchase *models.PigPurchase) error
}
// gormPigTradeRepository 是 PigTradeRepository 接口的 GORM 实现。
type gormPigTradeRepository struct {
db *gorm.DB
}
// NewGormPigTradeRepository 创建一个新的 PigTradeRepository GORM 实现实例。
func NewGormPigTradeRepository(db *gorm.DB) PigTradeRepository {
return &gormPigTradeRepository{db: db}
}
// CreatePigSaleTx 实现了在数据库中创建猪只销售记录的逻辑。
func (r *gormPigTradeRepository) CreatePigSaleTx(tx *gorm.DB, sale *models.PigSale) error {
return tx.Create(sale).Error
}
// CreatePigPurchaseTx 实现了在数据库中创建猪只采购记录的逻辑。
func (r *gormPigTradeRepository) CreatePigPurchaseTx(tx *gorm.DB, purchase *models.PigPurchase) error {
return tx.Create(purchase).Error
}

View File

@@ -0,0 +1,39 @@
package repository
import (
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigTransferLogRepository 定义了猪只迁移日志数据持久化的接口。
type PigTransferLogRepository interface {
// CreatePigTransferLog 在数据库中创建一条猪只迁移日志记录。
CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error
// GetLogsForPenSince 获取指定猪栏自特定时间点以来的所有迁移日志,按时间倒序排列。
GetLogsForPenSince(tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error)
}
// gormPigTransferLogRepository 是 PigTransferLogRepository 接口的 GORM 实现。
type gormPigTransferLogRepository struct {
db *gorm.DB
}
// NewGormPigTransferLogRepository 创建一个新的 PigTransferLogRepository GORM 实现实例。
func NewGormPigTransferLogRepository(db *gorm.DB) PigTransferLogRepository {
return &gormPigTransferLogRepository{db: db}
}
// CreatePigTransferLog 实现了在数据库中创建猪只迁移日志记录的逻辑。
func (r *gormPigTransferLogRepository) CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error {
return tx.Create(log).Error
}
// GetLogsForPenSince 实现了获取猪栏自特定时间点以来所有迁移日志的逻辑。
func (r *gormPigTransferLogRepository) GetLogsForPenSince(tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error) {
var logs []*models.PigTransferLog
err := tx.Where("pen_id = ? AND transfer_time >= ?", penID, since).Order("transfer_time DESC").Find(&logs).Error
return logs, err
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
package repository
import (
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"gorm.io/gorm"
)
// UnitOfWork 定义了工作单元接口,用于抽象事务管理
type UnitOfWork interface {
// ExecuteInTransaction 在一个数据库事务中执行给定的函数。
// 如果函数返回错误,事务将被回滚;否则,事务将被提交。
// tx 参数是当前事务的 GORM DB 实例,应传递给所有仓库方法。
ExecuteInTransaction(fn func(tx *gorm.DB) error) error
}
// gormUnitOfWork 是 UnitOfWork 接口的 GORM 实现
type gormUnitOfWork struct {
db *gorm.DB
logger *logs.Logger // 添加日志记录器
}
// NewGormUnitOfWork 创建一个新的 gormUnitOfWork 实例
func NewGormUnitOfWork(db *gorm.DB, logger *logs.Logger) UnitOfWork {
return &gormUnitOfWork{db: db, logger: logger}
}
// ExecuteInTransaction 实现了 UnitOfWork 接口的事务执行逻辑
func (u *gormUnitOfWork) ExecuteInTransaction(fn func(tx *gorm.DB) error) error {
tx := u.db.Begin()
if tx.Error != nil {
u.logger.Errorf("开启数据库事务失败: %v", tx.Error) // 记录错误日志
return fmt.Errorf("开启事务失败: %w", tx.Error)
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
u.logger.Errorf("事务中发生 panic已回滚: %v", r) // 记录 panic 日志
} else if tx.Error != nil { // 如果函数执行过程中返回错误,或者事务本身有错误,则回滚
tx.Rollback()
u.logger.Errorf("事务执行失败,已回滚: %v", tx.Error) // 记录错误日志
}
}()
// 执行业务逻辑函数
if err := fn(tx); err != nil {
tx.Rollback()
return err // 返回业务逻辑函数中的错误
}
// 提交事务
if err := tx.Commit().Error; err != nil {
u.logger.Errorf("提交数据库事务失败: %v", err) // 记录错误日志
return fmt.Errorf("提交事务失败: %w", err)
}
return nil
}