diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..badb4a6 --- /dev/null +++ b/.golangci.yml @@ -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 \ No newline at end of file diff --git a/Makefile b/Makefile index 4ed8fd6..a050764 100644 --- a/Makefile +++ b/Makefile @@ -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 ./... \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index f03b544..cd81b29 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -39,7 +39,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/device.AreaControllerResponse" + "$ref": "#/definitions/dto.AreaControllerResponse" } } } @@ -68,7 +68,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.CreateAreaControllerRequest" + "$ref": "#/definitions/dto.CreateAreaControllerRequest" } } ], @@ -84,7 +84,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.AreaControllerResponse" + "$ref": "#/definitions/dto.AreaControllerResponse" } } } @@ -125,7 +125,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.AreaControllerResponse" + "$ref": "#/definitions/dto.AreaControllerResponse" } } } @@ -160,7 +160,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.UpdateAreaControllerRequest" + "$ref": "#/definitions/dto.UpdateAreaControllerRequest" } } ], @@ -176,7 +176,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.AreaControllerResponse" + "$ref": "#/definitions/dto.AreaControllerResponse" } } } @@ -237,7 +237,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/device.DeviceTemplateResponse" + "$ref": "#/definitions/dto.DeviceTemplateResponse" } } } @@ -266,7 +266,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.CreateDeviceTemplateRequest" + "$ref": "#/definitions/dto.CreateDeviceTemplateRequest" } } ], @@ -282,7 +282,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" + "$ref": "#/definitions/dto.DeviceTemplateResponse" } } } @@ -323,7 +323,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" + "$ref": "#/definitions/dto.DeviceTemplateResponse" } } } @@ -358,7 +358,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.UpdateDeviceTemplateRequest" + "$ref": "#/definitions/dto.UpdateDeviceTemplateRequest" } } ], @@ -374,7 +374,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" + "$ref": "#/definitions/dto.DeviceTemplateResponse" } } } @@ -435,7 +435,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + "$ref": "#/definitions/dto.DeviceResponse" } } } @@ -464,7 +464,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.CreateDeviceRequest" + "$ref": "#/definitions/dto.CreateDeviceRequest" } } ], @@ -480,7 +480,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + "$ref": "#/definitions/dto.DeviceResponse" } } } @@ -521,7 +521,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + "$ref": "#/definitions/dto.DeviceResponse" } } } @@ -556,7 +556,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.UpdateDeviceRequest" + "$ref": "#/definitions/dto.UpdateDeviceRequest" } } ], @@ -572,7 +572,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + "$ref": "#/definitions/dto.DeviceResponse" } } } @@ -609,6 +609,1230 @@ const docTemplate = `{ } } }, + "/api/v1/pens": { + "get": { + "description": "获取所有猪栏的列表", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取猪栏列表", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "创建猪栏", + "parameters": [ + { + "description": "猪栏信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePenRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pens/{id}": { + "get": { + "description": "根据ID获取单个猪栏信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取单个猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪栏信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪栏信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePenRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪栏", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "删除猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pens/{id}/status": { + "put": { + "description": "更新指定猪栏的当前状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪栏状态", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新的猪栏状态", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePenStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pig-batches": { + "get": { + "description": "获取所有猪批次的列表,支持按活跃状态筛选", + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "获取猪批次列表", + "parameters": [ + { + "type": "boolean", + "description": "是否活跃 (true/false)", + "name": "is_active", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "创建猪批次", + "parameters": [ + { + "description": "猪批次信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchCreateDTO" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pig-batches/{batchID}/remove-pen/{penID}": { + "delete": { + "description": "将一个空闲猪栏从指定的猪批次中移除", + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "从猪批次移除空栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "batchID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "待移除的猪栏ID", + "name": "penID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "移除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{fromBatchID}/reclassify-pen": { + "post": { + "description": "将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "将猪栏划拨到新批次", + "parameters": [ + { + "type": "integer", + "description": "源猪批次ID", + "name": "fromBatchID", + "in": "path", + "required": true + }, + { + "description": "划拨请求信息 (包含目标批次ID、猪栏ID和备注)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ReclassifyPenToNewBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "划拨成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}": { + "get": { + "description": "根据ID获取单个猪批次信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "获取单个猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪批次信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "更新猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪批次信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchUpdateDTO" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪批次", + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "删除猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/assign-pens": { + "post": { + "description": "将一个或多个空闲猪栏分配给指定的猪批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "为猪批次分配空栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "待分配的猪栏ID列表", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AssignEmptyPensToBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "分配成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/buy-pigs": { + "post": { + "description": "记录猪批次中的猪只购买事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "处理买猪的业务逻辑", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "买猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BuyPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "买猪成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/move-pigs-into-pen": { + "post": { + "description": "将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "将猪只从“虚拟库存”移入指定猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "移入猪只请求信息 (包含目标猪栏ID、数量和备注)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MovePigsIntoPenRequest" + } + } + ], + "responses": { + "200": { + "description": "移入成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-cull": { + "post": { + "description": "记录猪批次中正常猪只淘汰的数量和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录正常猪只淘汰事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录正常猪只淘汰请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordCullRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-death": { + "post": { + "description": "记录猪批次中正常猪只死亡的数量和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录正常猪只死亡事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录正常猪只死亡请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordDeathRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-cull": { + "post": { + "description": "记录猪批次中病猪淘汰的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪淘汰事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪淘汰请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigCullRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-death": { + "post": { + "description": "记录猪批次中病猪死亡的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪死亡事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪死亡请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigDeathRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-recovery": { + "post": { + "description": "记录猪批次中病猪康复的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪康复事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪康复请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigRecoveryRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pigs": { + "post": { + "description": "记录猪批次中新增病猪的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录新增病猪事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/sell-pigs": { + "post": { + "description": "记录猪批次中的猪只出售事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "处理卖猪的业务逻辑", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "卖猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SellPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "卖猪成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/transfer-within-batch": { + "post": { + "description": "将指定数量的猪只在同一个猪群的不同猪栏间调动", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "群内调栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "群内调栏请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TransferPigsWithinBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "调栏成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{sourceBatchID}/transfer-across-batches": { + "post": { + "description": "将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "跨猪群调栏", + "parameters": [ + { + "type": "integer", + "description": "源猪批次ID", + "name": "sourceBatchID", + "in": "path", + "required": true + }, + { + "description": "跨群调栏请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TransferPigsAcrossBatchesRequest" + } + } + ], + "responses": { + "200": { + "description": "调栏成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-houses": { + "get": { + "description": "获取所有猪舍的列表", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取猪舍列表", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪舍", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "创建猪舍", + "parameters": [ + { + "description": "猪舍信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigHouseRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pig-houses/{id}": { + "get": { + "description": "根据ID获取单个猪舍信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取单个猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪舍信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪舍信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigHouseRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪舍", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "删除猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/plans": { "get": { "description": "获取所有计划的列表", @@ -631,7 +1855,10 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/plan.ListPlansResponse" + "type": "array", + "items": { + "$ref": "#/definitions/dto.PlanResponse" + } } } } @@ -659,7 +1886,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/plan.CreatePlanRequest" + "$ref": "#/definitions/dto.CreatePlanRequest" } } ], @@ -675,7 +1902,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.PlanResponse" } } } @@ -716,7 +1943,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.PlanResponse" } } } @@ -751,7 +1978,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/plan.UpdatePlanRequest" + "$ref": "#/definitions/dto.UpdatePlanRequest" } } ], @@ -767,7 +1994,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.PlanResponse" } } } @@ -882,7 +2109,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.CreateUserRequest" + "$ref": "#/definitions/dto.CreateUserRequest" } } ], @@ -898,7 +2125,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/user.CreateUserResponse" + "$ref": "#/definitions/dto.CreateUserResponse" } } } @@ -928,7 +2155,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.LoginRequest" + "$ref": "#/definitions/dto.LoginRequest" } } ], @@ -944,7 +2171,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/user.LoginResponse" + "$ref": "#/definitions/dto.LoginResponse" } } } @@ -1005,7 +2232,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/user.ListHistoryResponse" + "$ref": "#/definitions/dto.ListHistoryResponse" } } } @@ -1080,7 +2307,7 @@ const docTemplate = `{ "CodeServiceUnavailable" ] }, - "device.AreaControllerResponse": { + "dto.AreaControllerResponse": { "type": "object", "properties": { "created_at": { @@ -1110,7 +2337,54 @@ const docTemplate = `{ } } }, - "device.CreateAreaControllerRequest": { + "dto.AssignEmptyPensToBatchRequest": { + "type": "object" + }, + "dto.BuyPigsRequest": { + "type": "object", + "required": [ + "penID", + "quantity", + "totalPrice", + "tradeDate", + "traderName", + "unitPrice" + ], + "properties": { + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "买入猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "totalPrice": { + "description": "总价", + "type": "number", + "minimum": 0 + }, + "tradeDate": { + "description": "交易日期", + "type": "string" + }, + "traderName": { + "description": "交易方名称", + "type": "string" + }, + "unitPrice": { + "description": "单价", + "type": "number", + "minimum": 0 + } + } + }, + "dto.CreateAreaControllerRequest": { "type": "object", "required": [ "name", @@ -1132,7 +2406,7 @@ const docTemplate = `{ } } }, - "device.CreateDeviceRequest": { + "dto.CreateDeviceRequest": { "type": "object", "required": [ "area_controller_id", @@ -1158,7 +2432,7 @@ const docTemplate = `{ } } }, - "device.CreateDeviceTemplateRequest": { + "dto.CreateDeviceTemplateRequest": { "type": "object", "required": [ "category", @@ -1190,123 +2464,115 @@ const docTemplate = `{ } } }, - "device.DeviceTemplateResponse": { + "dto.CreatePenRequest": { "type": "object", + "required": [ + "capacity", + "house_id", + "pen_number" + ], "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" + "capacity": { + "type": "integer" }, - "commands": { - "type": "object", - "additionalProperties": true + "house_id": { + "type": "integer" }, - "created_at": { + "pen_number": { "type": "string" - }, + } + } + }, + "dto.CreatePigHouseRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { "description": { "type": "string" }, + "name": { + "type": "string" + } + } + }, + "dto.CreatePlanRequest": { + "type": "object", + "required": [ + "execution_type", + "name" + ], + "properties": { + "cron_expression": { + "type": "string", + "example": "0 0 6 * * *" + }, + "description": { + "type": "string", + "example": "根据温度自动调节风扇和加热器" + }, + "execute_num": { + "type": "integer", + "example": 10 + }, + "execution_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanExecutionType" + } + ], + "example": "自动" + }, + "name": { + "type": "string", + "example": "猪舍温度控制计划" + }, + "sub_plan_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TaskRequest" + } + } + } + }, + "dto.CreateUserRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "example": "password123" + }, + "username": { + "type": "string", + "example": "newuser" + } + } + }, + "dto.CreateUserResponse": { + "type": "object", + "properties": { "id": { - "type": "integer" + "type": "integer", + "example": 1 }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } + "username": { + "type": "string", + "example": "newuser" } } }, - "device.UpdateAreaControllerRequest": { - "type": "object", - "required": [ - "name", - "network_id" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "network_id": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.UpdateDeviceRequest": { - "type": "object", - "required": [ - "area_controller_id", - "device_template_id", - "name" - ], - "properties": { - "area_controller_id": { - "type": "integer" - }, - "device_template_id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.UpdateDeviceTemplateRequest": { - "type": "object", - "required": [ - "category", - "commands", - "name" - ], - "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" - }, - "commands": { - "type": "object", - "additionalProperties": true - }, - "description": { - "type": "string" - }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } - } - } - }, - "git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": { + "dto.DeviceResponse": { "type": "object", "properties": { "area_controller_id": { @@ -1342,205 +2608,74 @@ const docTemplate = `{ } } }, - "models.DeviceCategory": { - "type": "string", - "enum": [ - "actuator", - "sensor" - ], - "x-enum-varnames": [ - "CategoryActuator", - "CategorySensor" - ] - }, - "models.PlanContentType": { - "type": "string", - "enum": [ - "sub_plans", - "tasks" - ], - "x-enum-comments": { - "PlanContentTypeSubPlans": "计划包含子计划", - "PlanContentTypeTasks": "计划包含任务" - }, - "x-enum-descriptions": [ - "计划包含子计划", - "计划包含任务" - ], - "x-enum-varnames": [ - "PlanContentTypeSubPlans", - "PlanContentTypeTasks" - ] - }, - "models.PlanExecutionType": { - "type": "string", - "enum": [ - "automatic", - "manual" - ], - "x-enum-comments": { - "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", - "PlanExecutionTypeManual": "手动执行" - }, - "x-enum-descriptions": [ - "自动执行 (包含定时和循环)", - "手动执行" - ], - "x-enum-varnames": [ - "PlanExecutionTypeAutomatic", - "PlanExecutionTypeManual" - ] - }, - "models.PlanStatus": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-comments": { - "PlanStatusDisabled": "禁用计划", - "PlanStatusEnabled": "启用计划", - "PlanStatusFailed": "执行失败", - "PlanStatusStopeed": "执行完毕" - }, - "x-enum-descriptions": [ - "禁用计划", - "启用计划", - "执行完毕", - "执行失败" - ], - "x-enum-varnames": [ - "PlanStatusDisabled", - "PlanStatusEnabled", - "PlanStatusStopeed", - "PlanStatusFailed" - ] - }, - "models.SensorType": { - "type": "string", - "enum": [ - "signal_metrics", - "battery_level", - "temperature", - "humidity", - "weight" - ], - "x-enum-comments": { - "SensorTypeBatteryLevel": "电池电量", - "SensorTypeHumidity": "湿度", - "SensorTypeSignalMetrics": "信号强度", - "SensorTypeTemperature": "温度", - "SensorTypeWeight": "重量" - }, - "x-enum-descriptions": [ - "信号强度", - "电池电量", - "温度", - "湿度", - "重量" - ], - "x-enum-varnames": [ - "SensorTypeSignalMetrics", - "SensorTypeBatteryLevel", - "SensorTypeTemperature", - "SensorTypeHumidity", - "SensorTypeWeight" - ] - }, - "models.TaskType": { - "type": "string", - "enum": [ - "plan_analysis", - "waiting", - "release_feed_weight" - ], - "x-enum-comments": { - "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", - "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", - "TaskTypeWaiting": "等待任务" - }, - "x-enum-descriptions": [ - "解析Plan的Task列表并添加到待执行队列的特殊任务", - "等待任务", - "下料口释放指定重量任务" - ], - "x-enum-varnames": [ - "TaskPlanAnalysis", - "TaskTypeWaiting", - "TaskTypeReleaseFeedWeight" - ] - }, - "models.ValueDescriptor": { + "dto.DeviceTemplateResponse": { "type": "object", "properties": { - "multiplier": { - "description": "乘数,用于原始数据转换", - "type": "number" + "category": { + "$ref": "#/definitions/models.DeviceCategory" }, - "offset": { - "description": "偏移量,用于原始数据转换", - "type": "number" + "commands": { + "type": "object", + "additionalProperties": true }, - "type": { - "$ref": "#/definitions/models.SensorType" + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } } } }, - "plan.CreatePlanRequest": { + "dto.HistoryResponse": { "type": "object", - "required": [ - "execution_type", - "name" - ], "properties": { - "cron_expression": { + "action_type": { "type": "string", - "example": "0 0 6 * * *" + "example": "更新设备" }, "description": { "type": "string", - "example": "根据温度自动调节风扇和加热器" + "example": "设备更新成功" }, - "execute_num": { + "target_resource": {}, + "time": { + "type": "string" + }, + "user_id": { "type": "integer", - "example": 10 + "example": 101 }, - "execution_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanExecutionType" - } - ], - "example": "automatic" - }, - "name": { + "username": { "type": "string", - "example": "猪舍温度控制计划" - }, - "sub_plan_ids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "tasks": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.TaskRequest" - } + "example": "testuser" } } }, - "plan.ListPlansResponse": { + "dto.ListHistoryResponse": { "type": "object", "properties": { - "plans": { + "history": { "type": "array", "items": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.HistoryResponse" } }, "total": { @@ -1549,7 +2684,232 @@ const docTemplate = `{ } } }, - "plan.PlanResponse": { + "dto.LoginRequest": { + "type": "object", + "required": [ + "identifier", + "password" + ], + "properties": { + "identifier": { + "description": "Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号", + "type": "string", + "example": "testuser" + }, + "password": { + "type": "string", + "example": "password123" + } + } + }, + "dto.LoginResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "username": { + "type": "string", + "example": "testuser" + } + } + }, + "dto.MovePigsIntoPenRequest": { + "type": "object", + "required": [ + "quantity", + "toPenID" + ], + "properties": { + "quantity": { + "description": "移入猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, + "dto.PenResponse": { + "type": "object", + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "pig_batch_id": { + "type": "integer" + }, + "status": { + "$ref": "#/definitions/models.PenStatus" + } + } + }, + "dto.PigBatchCreateDTO": { + "type": "object", + "required": [ + "batch_number", + "initial_count", + "origin_type", + "start_date", + "status" + ], + "properties": { + "batch_number": { + "description": "批次编号,必填", + "type": "string" + }, + "initial_count": { + "description": "初始数量,必填,最小为1", + "type": "integer", + "minimum": 1 + }, + "origin_type": { + "description": "批次来源,必填", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期,必填", + "type": "string" + }, + "status": { + "description": "批次状态,必填", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + } + } + }, + "dto.PigBatchResponseDTO": { + "type": "object", + "properties": { + "batch_number": { + "description": "批次编号", + "type": "string" + }, + "create_time": { + "description": "创建时间", + "type": "string" + }, + "end_date": { + "description": "批次结束日期", + "type": "string" + }, + "id": { + "description": "批次ID", + "type": "integer" + }, + "initial_count": { + "description": "初始数量", + "type": "integer" + }, + "is_active": { + "description": "是否活跃", + "type": "boolean" + }, + "origin_type": { + "description": "批次来源", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期", + "type": "string" + }, + "status": { + "description": "批次状态", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + }, + "update_time": { + "description": "更新时间", + "type": "string" + } + } + }, + "dto.PigBatchUpdateDTO": { + "type": "object", + "properties": { + "batch_number": { + "description": "批次编号,可选", + "type": "string" + }, + "end_date": { + "description": "批次结束日期,可选", + "type": "string" + }, + "initial_count": { + "description": "初始数量,可选", + "type": "integer" + }, + "origin_type": { + "description": "批次来源,可选", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期,可选", + "type": "string" + }, + "status": { + "description": "批次状态,可选", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + } + } + }, + "dto.PigHouseResponse": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.PlanResponse": { "type": "object", "properties": { "content_type": { @@ -1558,7 +2918,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanContentType" } ], - "example": "tasks" + "example": "任务" }, "cron_expression": { "type": "string", @@ -1582,7 +2942,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "id": { "type": "integer", @@ -1598,27 +2958,290 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanStatus" } ], - "example": 0 + "example": "已启用" }, "sub_plans": { "type": "array", "items": { - "$ref": "#/definitions/plan.SubPlanResponse" + "$ref": "#/definitions/dto.SubPlanResponse" } }, "tasks": { "type": "array", "items": { - "$ref": "#/definitions/plan.TaskResponse" + "$ref": "#/definitions/dto.TaskResponse" } } } }, - "plan.SubPlanResponse": { + "dto.ReclassifyPenToNewBatchRequest": { + "type": "object", + "required": [ + "penID", + "toBatchID" + ], + "properties": { + "penID": { + "description": "待划拨的猪栏ID", + "type": "integer" + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toBatchID": { + "description": "目标猪批次ID", + "type": "integer" + } + } + }, + "dto.RecordCullRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "淘汰猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + } + } + }, + "dto.RecordDeathRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "死亡猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + } + } + }, + "dto.RecordSickPigCullRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "淘汰猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigDeathRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "死亡猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigRecoveryRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "康复猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigsRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "病猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.SellPigsRequest": { + "type": "object", + "required": [ + "penID", + "quantity", + "totalPrice", + "tradeDate", + "traderName", + "unitPrice" + ], + "properties": { + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "卖出猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "totalPrice": { + "description": "总价", + "type": "number", + "minimum": 0 + }, + "tradeDate": { + "description": "交易日期", + "type": "string" + }, + "traderName": { + "description": "交易方名称", + "type": "string" + }, + "unitPrice": { + "description": "单价", + "type": "number", + "minimum": 0 + } + } + }, + "dto.SubPlanResponse": { "type": "object", "properties": { "child_plan": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.PlanResponse" }, "child_plan_id": { "type": "integer", @@ -1638,7 +3261,7 @@ const docTemplate = `{ } } }, - "plan.TaskRequest": { + "dto.TaskRequest": { "type": "object", "properties": { "description": { @@ -1663,11 +3286,11 @@ const docTemplate = `{ "$ref": "#/definitions/models.TaskType" } ], - "example": "waiting" + "example": "等待" } } }, - "plan.TaskResponse": { + "dto.TaskResponse": { "type": "object", "properties": { "description": { @@ -1700,12 +3323,228 @@ const docTemplate = `{ "$ref": "#/definitions/models.TaskType" } ], - "example": "waiting" + "example": "等待" } } }, - "plan.UpdatePlanRequest": { + "dto.TransferPigsAcrossBatchesRequest": { "type": "object", + "required": [ + "destBatchID", + "fromPenID", + "quantity", + "toPenID" + ], + "properties": { + "destBatchID": { + "description": "目标猪批次ID", + "type": "integer" + }, + "fromPenID": { + "description": "源猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "调栏猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, + "dto.TransferPigsWithinBatchRequest": { + "type": "object", + "required": [ + "fromPenID", + "quantity", + "toPenID" + ], + "properties": { + "fromPenID": { + "description": "源猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "调栏猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, + "dto.UpdateAreaControllerRequest": { + "type": "object", + "required": [ + "name", + "network_id" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "network_id": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.UpdateDeviceRequest": { + "type": "object", + "required": [ + "area_controller_id", + "device_template_id", + "name" + ], + "properties": { + "area_controller_id": { + "type": "integer" + }, + "device_template_id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.UpdateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "dto.UpdatePenRequest": { + "type": "object", + "required": [ + "capacity", + "house_id", + "pen_number", + "status" + ], + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "status": { + "description": "添加oneof校验", + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PenStatus" + } + ] + } + } + }, + "dto.UpdatePenStatusRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PenStatus" + } + ], + "example": "病猪栏" + } + } + }, + "dto.UpdatePigHouseRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.UpdatePlanRequest": { + "type": "object", + "required": [ + "execution_type" + ], "properties": { "cron_expression": { "type": "string", @@ -1725,7 +3564,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "name": { "type": "string", @@ -1740,113 +3579,229 @@ const docTemplate = `{ "tasks": { "type": "array", "items": { - "$ref": "#/definitions/plan.TaskRequest" + "$ref": "#/definitions/dto.TaskRequest" } } } }, - "user.CreateUserRequest": { - "type": "object", - "required": [ - "password", - "username" + "models.DeviceCategory": { + "type": "string", + "enum": [ + "执行器", + "传感器" ], - "properties": { - "password": { - "type": "string", - "example": "password123" - }, - "username": { - "type": "string", - "example": "newuser" - } - } + "x-enum-varnames": [ + "CategoryActuator", + "CategorySensor" + ] }, - "user.CreateUserResponse": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "username": { - "type": "string", - "example": "newuser" - } - } - }, - "user.HistoryResponse": { - "type": "object", - "properties": { - "action_type": { - "type": "string", - "example": "更新设备" - }, - "description": { - "type": "string", - "example": "设备更新成功" - }, - "target_resource": {}, - "time": { - "type": "string" - }, - "user_id": { - "type": "integer", - "example": 101 - }, - "username": { - "type": "string", - "example": "testuser" - } - } - }, - "user.ListHistoryResponse": { - "type": "object", - "properties": { - "history": { - "type": "array", - "items": { - "$ref": "#/definitions/user.HistoryResponse" - } - }, - "total": { - "type": "integer", - "example": 100 - } - } - }, - "user.LoginRequest": { - "type": "object", - "required": [ - "identifier", - "password" + "models.PenStatus": { + "type": "string", + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" ], - "properties": { - "identifier": { - "description": "Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号", - "type": "string", - "example": "testuser" - }, - "password": { - "type": "string", - "example": "password123" - } - } + "x-enum-varnames": [ + "PenStatusEmpty", + "PenStatusOccupied", + "PenStatusSickPen", + "PenStatusRecovering", + "PenStatusCleaning", + "PenStatusUnderMaint" + ] }, - "user.LoginResponse": { + "models.PigBatchOriginType": { + "type": "string", + "enum": [ + "自繁", + "外购" + ], + "x-enum-varnames": [ + "OriginTypeSelfFarrowed", + "OriginTypePurchased" + ] + }, + "models.PigBatchSickPigTreatmentLocation": { + "type": "string", + "enum": [ + "原地治疗", + "病猪栏治疗" + ], + "x-enum-varnames": [ + "TreatmentLocationOnSite", + "TreatmentLocationSickBay" + ] + }, + "models.PigBatchStatus": { + "type": "string", + "enum": [ + "保育", + "生长", + "育肥", + "待售", + "已出售", + "已归档" + ], + "x-enum-comments": { + "BatchStatusArchived": "批次结束(如全群淘汰等)", + "BatchStatusFinishing": "最后的育肥阶段", + "BatchStatusForSale": "达到出栏标准", + "BatchStatusGrowing": "生长育肥阶段", + "BatchStatusWeaning": "从断奶到保育结束" + }, + "x-enum-descriptions": [ + "从断奶到保育结束", + "生长育肥阶段", + "最后的育肥阶段", + "达到出栏标准", + "", + "批次结束(如全群淘汰等)" + ], + "x-enum-varnames": [ + "BatchStatusWeaning", + "BatchStatusGrowing", + "BatchStatusFinishing", + "BatchStatusForSale", + "BatchStatusSold", + "BatchStatusArchived" + ] + }, + "models.PlanContentType": { + "type": "string", + "enum": [ + "子计划", + "任务" + ], + "x-enum-comments": { + "PlanContentTypeSubPlans": "计划包含子计划", + "PlanContentTypeTasks": "计划包含任务" + }, + "x-enum-descriptions": [ + "计划包含子计划", + "计划包含任务" + ], + "x-enum-varnames": [ + "PlanContentTypeSubPlans", + "PlanContentTypeTasks" + ] + }, + "models.PlanExecutionType": { + "type": "string", + "enum": [ + "自动", + "手动" + ], + "x-enum-comments": { + "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", + "PlanExecutionTypeManual": "手动执行" + }, + "x-enum-descriptions": [ + "自动执行 (包含定时和循环)", + "手动执行" + ], + "x-enum-varnames": [ + "PlanExecutionTypeAutomatic", + "PlanExecutionTypeManual" + ] + }, + "models.PlanStatus": { + "type": "string", + "enum": [ + "已禁用", + "已启用", + "执行完毕", + "执行失败" + ], + "x-enum-comments": { + "PlanStatusDisabled": "禁用计划", + "PlanStatusEnabled": "启用计划", + "PlanStatusFailed": "执行失败", + "PlanStatusStopped": "执行完毕" + }, + "x-enum-descriptions": [ + "禁用计划", + "启用计划", + "执行完毕", + "执行失败" + ], + "x-enum-varnames": [ + "PlanStatusDisabled", + "PlanStatusEnabled", + "PlanStatusStopped", + "PlanStatusFailed" + ] + }, + "models.SensorType": { + "type": "string", + "enum": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-comments": { + "SensorTypeBatteryLevel": "电池电量", + "SensorTypeHumidity": "湿度", + "SensorTypeSignalMetrics": "信号强度", + "SensorTypeTemperature": "温度", + "SensorTypeWeight": "重量" + }, + "x-enum-descriptions": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-varnames": [ + "SensorTypeSignalMetrics", + "SensorTypeBatteryLevel", + "SensorTypeTemperature", + "SensorTypeHumidity", + "SensorTypeWeight" + ] + }, + "models.TaskType": { + "type": "string", + "enum": [ + "计划分析", + "等待", + "下料" + ], + "x-enum-comments": { + "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", + "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", + "TaskTypeWaiting": "等待任务" + }, + "x-enum-descriptions": [ + "解析Plan的Task列表并添加到待执行队列的特殊任务", + "等待任务", + "下料口释放指定重量任务" + ], + "x-enum-varnames": [ + "TaskPlanAnalysis", + "TaskTypeWaiting", + "TaskTypeReleaseFeedWeight" + ] + }, + "models.ValueDescriptor": { "type": "object", "properties": { - "id": { - "type": "integer", - "example": 1 + "multiplier": { + "description": "乘数,用于原始数据转换", + "type": "number" }, - "token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + "offset": { + "description": "偏移量,用于原始数据转换", + "type": "number" }, - "username": { - "type": "string", - "example": "testuser" + "type": { + "$ref": "#/definitions/models.SensorType" } } } diff --git a/docs/swagger.json b/docs/swagger.json index 080e250..c14cfa3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -28,7 +28,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/device.AreaControllerResponse" + "$ref": "#/definitions/dto.AreaControllerResponse" } } } @@ -57,7 +57,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.CreateAreaControllerRequest" + "$ref": "#/definitions/dto.CreateAreaControllerRequest" } } ], @@ -73,7 +73,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.AreaControllerResponse" + "$ref": "#/definitions/dto.AreaControllerResponse" } } } @@ -114,7 +114,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.AreaControllerResponse" + "$ref": "#/definitions/dto.AreaControllerResponse" } } } @@ -149,7 +149,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.UpdateAreaControllerRequest" + "$ref": "#/definitions/dto.UpdateAreaControllerRequest" } } ], @@ -165,7 +165,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.AreaControllerResponse" + "$ref": "#/definitions/dto.AreaControllerResponse" } } } @@ -226,7 +226,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/device.DeviceTemplateResponse" + "$ref": "#/definitions/dto.DeviceTemplateResponse" } } } @@ -255,7 +255,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.CreateDeviceTemplateRequest" + "$ref": "#/definitions/dto.CreateDeviceTemplateRequest" } } ], @@ -271,7 +271,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" + "$ref": "#/definitions/dto.DeviceTemplateResponse" } } } @@ -312,7 +312,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" + "$ref": "#/definitions/dto.DeviceTemplateResponse" } } } @@ -347,7 +347,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.UpdateDeviceTemplateRequest" + "$ref": "#/definitions/dto.UpdateDeviceTemplateRequest" } } ], @@ -363,7 +363,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" + "$ref": "#/definitions/dto.DeviceTemplateResponse" } } } @@ -424,7 +424,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + "$ref": "#/definitions/dto.DeviceResponse" } } } @@ -453,7 +453,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.CreateDeviceRequest" + "$ref": "#/definitions/dto.CreateDeviceRequest" } } ], @@ -469,7 +469,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + "$ref": "#/definitions/dto.DeviceResponse" } } } @@ -510,7 +510,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + "$ref": "#/definitions/dto.DeviceResponse" } } } @@ -545,7 +545,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/device.UpdateDeviceRequest" + "$ref": "#/definitions/dto.UpdateDeviceRequest" } } ], @@ -561,7 +561,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + "$ref": "#/definitions/dto.DeviceResponse" } } } @@ -598,6 +598,1230 @@ } } }, + "/api/v1/pens": { + "get": { + "description": "获取所有猪栏的列表", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取猪栏列表", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "创建猪栏", + "parameters": [ + { + "description": "猪栏信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePenRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pens/{id}": { + "get": { + "description": "根据ID获取单个猪栏信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取单个猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪栏信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪栏信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePenRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪栏", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "删除猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pens/{id}/status": { + "put": { + "description": "更新指定猪栏的当前状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪栏状态", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新的猪栏状态", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePenStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pig-batches": { + "get": { + "description": "获取所有猪批次的列表,支持按活跃状态筛选", + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "获取猪批次列表", + "parameters": [ + { + "type": "boolean", + "description": "是否活跃 (true/false)", + "name": "is_active", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "创建猪批次", + "parameters": [ + { + "description": "猪批次信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchCreateDTO" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pig-batches/{batchID}/remove-pen/{penID}": { + "delete": { + "description": "将一个空闲猪栏从指定的猪批次中移除", + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "从猪批次移除空栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "batchID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "待移除的猪栏ID", + "name": "penID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "移除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{fromBatchID}/reclassify-pen": { + "post": { + "description": "将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "将猪栏划拨到新批次", + "parameters": [ + { + "type": "integer", + "description": "源猪批次ID", + "name": "fromBatchID", + "in": "path", + "required": true + }, + { + "description": "划拨请求信息 (包含目标批次ID、猪栏ID和备注)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ReclassifyPenToNewBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "划拨成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}": { + "get": { + "description": "根据ID获取单个猪批次信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "获取单个猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪批次信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "更新猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪批次信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchUpdateDTO" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪批次", + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "删除猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/assign-pens": { + "post": { + "description": "将一个或多个空闲猪栏分配给指定的猪批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "为猪批次分配空栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "待分配的猪栏ID列表", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AssignEmptyPensToBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "分配成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/buy-pigs": { + "post": { + "description": "记录猪批次中的猪只购买事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "处理买猪的业务逻辑", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "买猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BuyPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "买猪成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/move-pigs-into-pen": { + "post": { + "description": "将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪群管理" + ], + "summary": "将猪只从“虚拟库存”移入指定猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "移入猪只请求信息 (包含目标猪栏ID、数量和备注)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MovePigsIntoPenRequest" + } + } + ], + "responses": { + "200": { + "description": "移入成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-cull": { + "post": { + "description": "记录猪批次中正常猪只淘汰的数量和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录正常猪只淘汰事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录正常猪只淘汰请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordCullRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-death": { + "post": { + "description": "记录猪批次中正常猪只死亡的数量和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录正常猪只死亡事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录正常猪只死亡请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordDeathRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-cull": { + "post": { + "description": "记录猪批次中病猪淘汰的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪淘汰事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪淘汰请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigCullRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-death": { + "post": { + "description": "记录猪批次中病猪死亡的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪死亡事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪死亡请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigDeathRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-recovery": { + "post": { + "description": "记录猪批次中病猪康复的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪康复事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪康复请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigRecoveryRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pigs": { + "post": { + "description": "记录猪批次中新增病猪的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录新增病猪事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/sell-pigs": { + "post": { + "description": "记录猪批次中的猪只出售事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "处理卖猪的业务逻辑", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "卖猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SellPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "卖猪成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/transfer-within-batch": { + "post": { + "description": "将指定数量的猪只在同一个猪群的不同猪栏间调动", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "群内调栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "群内调栏请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TransferPigsWithinBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "调栏成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{sourceBatchID}/transfer-across-batches": { + "post": { + "description": "将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "跨猪群调栏", + "parameters": [ + { + "type": "integer", + "description": "源猪批次ID", + "name": "sourceBatchID", + "in": "path", + "required": true + }, + { + "description": "跨群调栏请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TransferPigsAcrossBatchesRequest" + } + } + ], + "responses": { + "200": { + "description": "调栏成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-houses": { + "get": { + "description": "获取所有猪舍的列表", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取猪舍列表", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪舍", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "创建猪舍", + "parameters": [ + { + "description": "猪舍信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigHouseRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pig-houses/{id}": { + "get": { + "description": "根据ID获取单个猪舍信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取单个猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪舍信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪舍信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigHouseRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪舍", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "删除猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/plans": { "get": { "description": "获取所有计划的列表", @@ -620,7 +1844,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/plan.ListPlansResponse" + "type": "array", + "items": { + "$ref": "#/definitions/dto.PlanResponse" + } } } } @@ -648,7 +1875,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/plan.CreatePlanRequest" + "$ref": "#/definitions/dto.CreatePlanRequest" } } ], @@ -664,7 +1891,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.PlanResponse" } } } @@ -705,7 +1932,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.PlanResponse" } } } @@ -740,7 +1967,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/plan.UpdatePlanRequest" + "$ref": "#/definitions/dto.UpdatePlanRequest" } } ], @@ -756,7 +1983,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.PlanResponse" } } } @@ -871,7 +2098,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.CreateUserRequest" + "$ref": "#/definitions/dto.CreateUserRequest" } } ], @@ -887,7 +2114,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/user.CreateUserResponse" + "$ref": "#/definitions/dto.CreateUserResponse" } } } @@ -917,7 +2144,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.LoginRequest" + "$ref": "#/definitions/dto.LoginRequest" } } ], @@ -933,7 +2160,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/user.LoginResponse" + "$ref": "#/definitions/dto.LoginResponse" } } } @@ -994,7 +2221,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/user.ListHistoryResponse" + "$ref": "#/definitions/dto.ListHistoryResponse" } } } @@ -1069,7 +2296,7 @@ "CodeServiceUnavailable" ] }, - "device.AreaControllerResponse": { + "dto.AreaControllerResponse": { "type": "object", "properties": { "created_at": { @@ -1099,7 +2326,54 @@ } } }, - "device.CreateAreaControllerRequest": { + "dto.AssignEmptyPensToBatchRequest": { + "type": "object" + }, + "dto.BuyPigsRequest": { + "type": "object", + "required": [ + "penID", + "quantity", + "totalPrice", + "tradeDate", + "traderName", + "unitPrice" + ], + "properties": { + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "买入猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "totalPrice": { + "description": "总价", + "type": "number", + "minimum": 0 + }, + "tradeDate": { + "description": "交易日期", + "type": "string" + }, + "traderName": { + "description": "交易方名称", + "type": "string" + }, + "unitPrice": { + "description": "单价", + "type": "number", + "minimum": 0 + } + } + }, + "dto.CreateAreaControllerRequest": { "type": "object", "required": [ "name", @@ -1121,7 +2395,7 @@ } } }, - "device.CreateDeviceRequest": { + "dto.CreateDeviceRequest": { "type": "object", "required": [ "area_controller_id", @@ -1147,7 +2421,7 @@ } } }, - "device.CreateDeviceTemplateRequest": { + "dto.CreateDeviceTemplateRequest": { "type": "object", "required": [ "category", @@ -1179,123 +2453,115 @@ } } }, - "device.DeviceTemplateResponse": { + "dto.CreatePenRequest": { "type": "object", + "required": [ + "capacity", + "house_id", + "pen_number" + ], "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" + "capacity": { + "type": "integer" }, - "commands": { - "type": "object", - "additionalProperties": true + "house_id": { + "type": "integer" }, - "created_at": { + "pen_number": { "type": "string" - }, + } + } + }, + "dto.CreatePigHouseRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { "description": { "type": "string" }, + "name": { + "type": "string" + } + } + }, + "dto.CreatePlanRequest": { + "type": "object", + "required": [ + "execution_type", + "name" + ], + "properties": { + "cron_expression": { + "type": "string", + "example": "0 0 6 * * *" + }, + "description": { + "type": "string", + "example": "根据温度自动调节风扇和加热器" + }, + "execute_num": { + "type": "integer", + "example": 10 + }, + "execution_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanExecutionType" + } + ], + "example": "自动" + }, + "name": { + "type": "string", + "example": "猪舍温度控制计划" + }, + "sub_plan_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TaskRequest" + } + } + } + }, + "dto.CreateUserRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "example": "password123" + }, + "username": { + "type": "string", + "example": "newuser" + } + } + }, + "dto.CreateUserResponse": { + "type": "object", + "properties": { "id": { - "type": "integer" + "type": "integer", + "example": 1 }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } + "username": { + "type": "string", + "example": "newuser" } } }, - "device.UpdateAreaControllerRequest": { - "type": "object", - "required": [ - "name", - "network_id" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "network_id": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.UpdateDeviceRequest": { - "type": "object", - "required": [ - "area_controller_id", - "device_template_id", - "name" - ], - "properties": { - "area_controller_id": { - "type": "integer" - }, - "device_template_id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.UpdateDeviceTemplateRequest": { - "type": "object", - "required": [ - "category", - "commands", - "name" - ], - "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" - }, - "commands": { - "type": "object", - "additionalProperties": true - }, - "description": { - "type": "string" - }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } - } - } - }, - "git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": { + "dto.DeviceResponse": { "type": "object", "properties": { "area_controller_id": { @@ -1331,205 +2597,74 @@ } } }, - "models.DeviceCategory": { - "type": "string", - "enum": [ - "actuator", - "sensor" - ], - "x-enum-varnames": [ - "CategoryActuator", - "CategorySensor" - ] - }, - "models.PlanContentType": { - "type": "string", - "enum": [ - "sub_plans", - "tasks" - ], - "x-enum-comments": { - "PlanContentTypeSubPlans": "计划包含子计划", - "PlanContentTypeTasks": "计划包含任务" - }, - "x-enum-descriptions": [ - "计划包含子计划", - "计划包含任务" - ], - "x-enum-varnames": [ - "PlanContentTypeSubPlans", - "PlanContentTypeTasks" - ] - }, - "models.PlanExecutionType": { - "type": "string", - "enum": [ - "automatic", - "manual" - ], - "x-enum-comments": { - "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", - "PlanExecutionTypeManual": "手动执行" - }, - "x-enum-descriptions": [ - "自动执行 (包含定时和循环)", - "手动执行" - ], - "x-enum-varnames": [ - "PlanExecutionTypeAutomatic", - "PlanExecutionTypeManual" - ] - }, - "models.PlanStatus": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-comments": { - "PlanStatusDisabled": "禁用计划", - "PlanStatusEnabled": "启用计划", - "PlanStatusFailed": "执行失败", - "PlanStatusStopeed": "执行完毕" - }, - "x-enum-descriptions": [ - "禁用计划", - "启用计划", - "执行完毕", - "执行失败" - ], - "x-enum-varnames": [ - "PlanStatusDisabled", - "PlanStatusEnabled", - "PlanStatusStopeed", - "PlanStatusFailed" - ] - }, - "models.SensorType": { - "type": "string", - "enum": [ - "signal_metrics", - "battery_level", - "temperature", - "humidity", - "weight" - ], - "x-enum-comments": { - "SensorTypeBatteryLevel": "电池电量", - "SensorTypeHumidity": "湿度", - "SensorTypeSignalMetrics": "信号强度", - "SensorTypeTemperature": "温度", - "SensorTypeWeight": "重量" - }, - "x-enum-descriptions": [ - "信号强度", - "电池电量", - "温度", - "湿度", - "重量" - ], - "x-enum-varnames": [ - "SensorTypeSignalMetrics", - "SensorTypeBatteryLevel", - "SensorTypeTemperature", - "SensorTypeHumidity", - "SensorTypeWeight" - ] - }, - "models.TaskType": { - "type": "string", - "enum": [ - "plan_analysis", - "waiting", - "release_feed_weight" - ], - "x-enum-comments": { - "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", - "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", - "TaskTypeWaiting": "等待任务" - }, - "x-enum-descriptions": [ - "解析Plan的Task列表并添加到待执行队列的特殊任务", - "等待任务", - "下料口释放指定重量任务" - ], - "x-enum-varnames": [ - "TaskPlanAnalysis", - "TaskTypeWaiting", - "TaskTypeReleaseFeedWeight" - ] - }, - "models.ValueDescriptor": { + "dto.DeviceTemplateResponse": { "type": "object", "properties": { - "multiplier": { - "description": "乘数,用于原始数据转换", - "type": "number" + "category": { + "$ref": "#/definitions/models.DeviceCategory" }, - "offset": { - "description": "偏移量,用于原始数据转换", - "type": "number" + "commands": { + "type": "object", + "additionalProperties": true }, - "type": { - "$ref": "#/definitions/models.SensorType" + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } } } }, - "plan.CreatePlanRequest": { + "dto.HistoryResponse": { "type": "object", - "required": [ - "execution_type", - "name" - ], "properties": { - "cron_expression": { + "action_type": { "type": "string", - "example": "0 0 6 * * *" + "example": "更新设备" }, "description": { "type": "string", - "example": "根据温度自动调节风扇和加热器" + "example": "设备更新成功" }, - "execute_num": { + "target_resource": {}, + "time": { + "type": "string" + }, + "user_id": { "type": "integer", - "example": 10 + "example": 101 }, - "execution_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanExecutionType" - } - ], - "example": "automatic" - }, - "name": { + "username": { "type": "string", - "example": "猪舍温度控制计划" - }, - "sub_plan_ids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "tasks": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.TaskRequest" - } + "example": "testuser" } } }, - "plan.ListPlansResponse": { + "dto.ListHistoryResponse": { "type": "object", "properties": { - "plans": { + "history": { "type": "array", "items": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.HistoryResponse" } }, "total": { @@ -1538,7 +2673,232 @@ } } }, - "plan.PlanResponse": { + "dto.LoginRequest": { + "type": "object", + "required": [ + "identifier", + "password" + ], + "properties": { + "identifier": { + "description": "Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号", + "type": "string", + "example": "testuser" + }, + "password": { + "type": "string", + "example": "password123" + } + } + }, + "dto.LoginResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "username": { + "type": "string", + "example": "testuser" + } + } + }, + "dto.MovePigsIntoPenRequest": { + "type": "object", + "required": [ + "quantity", + "toPenID" + ], + "properties": { + "quantity": { + "description": "移入猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, + "dto.PenResponse": { + "type": "object", + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "pig_batch_id": { + "type": "integer" + }, + "status": { + "$ref": "#/definitions/models.PenStatus" + } + } + }, + "dto.PigBatchCreateDTO": { + "type": "object", + "required": [ + "batch_number", + "initial_count", + "origin_type", + "start_date", + "status" + ], + "properties": { + "batch_number": { + "description": "批次编号,必填", + "type": "string" + }, + "initial_count": { + "description": "初始数量,必填,最小为1", + "type": "integer", + "minimum": 1 + }, + "origin_type": { + "description": "批次来源,必填", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期,必填", + "type": "string" + }, + "status": { + "description": "批次状态,必填", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + } + } + }, + "dto.PigBatchResponseDTO": { + "type": "object", + "properties": { + "batch_number": { + "description": "批次编号", + "type": "string" + }, + "create_time": { + "description": "创建时间", + "type": "string" + }, + "end_date": { + "description": "批次结束日期", + "type": "string" + }, + "id": { + "description": "批次ID", + "type": "integer" + }, + "initial_count": { + "description": "初始数量", + "type": "integer" + }, + "is_active": { + "description": "是否活跃", + "type": "boolean" + }, + "origin_type": { + "description": "批次来源", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期", + "type": "string" + }, + "status": { + "description": "批次状态", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + }, + "update_time": { + "description": "更新时间", + "type": "string" + } + } + }, + "dto.PigBatchUpdateDTO": { + "type": "object", + "properties": { + "batch_number": { + "description": "批次编号,可选", + "type": "string" + }, + "end_date": { + "description": "批次结束日期,可选", + "type": "string" + }, + "initial_count": { + "description": "初始数量,可选", + "type": "integer" + }, + "origin_type": { + "description": "批次来源,可选", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期,可选", + "type": "string" + }, + "status": { + "description": "批次状态,可选", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + } + } + }, + "dto.PigHouseResponse": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.PlanResponse": { "type": "object", "properties": { "content_type": { @@ -1547,7 +2907,7 @@ "$ref": "#/definitions/models.PlanContentType" } ], - "example": "tasks" + "example": "任务" }, "cron_expression": { "type": "string", @@ -1571,7 +2931,7 @@ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "id": { "type": "integer", @@ -1587,27 +2947,290 @@ "$ref": "#/definitions/models.PlanStatus" } ], - "example": 0 + "example": "已启用" }, "sub_plans": { "type": "array", "items": { - "$ref": "#/definitions/plan.SubPlanResponse" + "$ref": "#/definitions/dto.SubPlanResponse" } }, "tasks": { "type": "array", "items": { - "$ref": "#/definitions/plan.TaskResponse" + "$ref": "#/definitions/dto.TaskResponse" } } } }, - "plan.SubPlanResponse": { + "dto.ReclassifyPenToNewBatchRequest": { + "type": "object", + "required": [ + "penID", + "toBatchID" + ], + "properties": { + "penID": { + "description": "待划拨的猪栏ID", + "type": "integer" + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toBatchID": { + "description": "目标猪批次ID", + "type": "integer" + } + } + }, + "dto.RecordCullRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "淘汰猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + } + } + }, + "dto.RecordDeathRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "死亡猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + } + } + }, + "dto.RecordSickPigCullRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "淘汰猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigDeathRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "死亡猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigRecoveryRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "康复猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigsRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "病猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.SellPigsRequest": { + "type": "object", + "required": [ + "penID", + "quantity", + "totalPrice", + "tradeDate", + "traderName", + "unitPrice" + ], + "properties": { + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "卖出猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "totalPrice": { + "description": "总价", + "type": "number", + "minimum": 0 + }, + "tradeDate": { + "description": "交易日期", + "type": "string" + }, + "traderName": { + "description": "交易方名称", + "type": "string" + }, + "unitPrice": { + "description": "单价", + "type": "number", + "minimum": 0 + } + } + }, + "dto.SubPlanResponse": { "type": "object", "properties": { "child_plan": { - "$ref": "#/definitions/plan.PlanResponse" + "$ref": "#/definitions/dto.PlanResponse" }, "child_plan_id": { "type": "integer", @@ -1627,7 +3250,7 @@ } } }, - "plan.TaskRequest": { + "dto.TaskRequest": { "type": "object", "properties": { "description": { @@ -1652,11 +3275,11 @@ "$ref": "#/definitions/models.TaskType" } ], - "example": "waiting" + "example": "等待" } } }, - "plan.TaskResponse": { + "dto.TaskResponse": { "type": "object", "properties": { "description": { @@ -1689,12 +3312,228 @@ "$ref": "#/definitions/models.TaskType" } ], - "example": "waiting" + "example": "等待" } } }, - "plan.UpdatePlanRequest": { + "dto.TransferPigsAcrossBatchesRequest": { "type": "object", + "required": [ + "destBatchID", + "fromPenID", + "quantity", + "toPenID" + ], + "properties": { + "destBatchID": { + "description": "目标猪批次ID", + "type": "integer" + }, + "fromPenID": { + "description": "源猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "调栏猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, + "dto.TransferPigsWithinBatchRequest": { + "type": "object", + "required": [ + "fromPenID", + "quantity", + "toPenID" + ], + "properties": { + "fromPenID": { + "description": "源猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "调栏猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, + "dto.UpdateAreaControllerRequest": { + "type": "object", + "required": [ + "name", + "network_id" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "network_id": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.UpdateDeviceRequest": { + "type": "object", + "required": [ + "area_controller_id", + "device_template_id", + "name" + ], + "properties": { + "area_controller_id": { + "type": "integer" + }, + "device_template_id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.UpdateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "dto.UpdatePenRequest": { + "type": "object", + "required": [ + "capacity", + "house_id", + "pen_number", + "status" + ], + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "status": { + "description": "添加oneof校验", + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PenStatus" + } + ] + } + } + }, + "dto.UpdatePenStatusRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PenStatus" + } + ], + "example": "病猪栏" + } + } + }, + "dto.UpdatePigHouseRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.UpdatePlanRequest": { + "type": "object", + "required": [ + "execution_type" + ], "properties": { "cron_expression": { "type": "string", @@ -1714,7 +3553,7 @@ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "name": { "type": "string", @@ -1729,113 +3568,229 @@ "tasks": { "type": "array", "items": { - "$ref": "#/definitions/plan.TaskRequest" + "$ref": "#/definitions/dto.TaskRequest" } } } }, - "user.CreateUserRequest": { - "type": "object", - "required": [ - "password", - "username" + "models.DeviceCategory": { + "type": "string", + "enum": [ + "执行器", + "传感器" ], - "properties": { - "password": { - "type": "string", - "example": "password123" - }, - "username": { - "type": "string", - "example": "newuser" - } - } + "x-enum-varnames": [ + "CategoryActuator", + "CategorySensor" + ] }, - "user.CreateUserResponse": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "username": { - "type": "string", - "example": "newuser" - } - } - }, - "user.HistoryResponse": { - "type": "object", - "properties": { - "action_type": { - "type": "string", - "example": "更新设备" - }, - "description": { - "type": "string", - "example": "设备更新成功" - }, - "target_resource": {}, - "time": { - "type": "string" - }, - "user_id": { - "type": "integer", - "example": 101 - }, - "username": { - "type": "string", - "example": "testuser" - } - } - }, - "user.ListHistoryResponse": { - "type": "object", - "properties": { - "history": { - "type": "array", - "items": { - "$ref": "#/definitions/user.HistoryResponse" - } - }, - "total": { - "type": "integer", - "example": 100 - } - } - }, - "user.LoginRequest": { - "type": "object", - "required": [ - "identifier", - "password" + "models.PenStatus": { + "type": "string", + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" ], - "properties": { - "identifier": { - "description": "Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号", - "type": "string", - "example": "testuser" - }, - "password": { - "type": "string", - "example": "password123" - } - } + "x-enum-varnames": [ + "PenStatusEmpty", + "PenStatusOccupied", + "PenStatusSickPen", + "PenStatusRecovering", + "PenStatusCleaning", + "PenStatusUnderMaint" + ] }, - "user.LoginResponse": { + "models.PigBatchOriginType": { + "type": "string", + "enum": [ + "自繁", + "外购" + ], + "x-enum-varnames": [ + "OriginTypeSelfFarrowed", + "OriginTypePurchased" + ] + }, + "models.PigBatchSickPigTreatmentLocation": { + "type": "string", + "enum": [ + "原地治疗", + "病猪栏治疗" + ], + "x-enum-varnames": [ + "TreatmentLocationOnSite", + "TreatmentLocationSickBay" + ] + }, + "models.PigBatchStatus": { + "type": "string", + "enum": [ + "保育", + "生长", + "育肥", + "待售", + "已出售", + "已归档" + ], + "x-enum-comments": { + "BatchStatusArchived": "批次结束(如全群淘汰等)", + "BatchStatusFinishing": "最后的育肥阶段", + "BatchStatusForSale": "达到出栏标准", + "BatchStatusGrowing": "生长育肥阶段", + "BatchStatusWeaning": "从断奶到保育结束" + }, + "x-enum-descriptions": [ + "从断奶到保育结束", + "生长育肥阶段", + "最后的育肥阶段", + "达到出栏标准", + "", + "批次结束(如全群淘汰等)" + ], + "x-enum-varnames": [ + "BatchStatusWeaning", + "BatchStatusGrowing", + "BatchStatusFinishing", + "BatchStatusForSale", + "BatchStatusSold", + "BatchStatusArchived" + ] + }, + "models.PlanContentType": { + "type": "string", + "enum": [ + "子计划", + "任务" + ], + "x-enum-comments": { + "PlanContentTypeSubPlans": "计划包含子计划", + "PlanContentTypeTasks": "计划包含任务" + }, + "x-enum-descriptions": [ + "计划包含子计划", + "计划包含任务" + ], + "x-enum-varnames": [ + "PlanContentTypeSubPlans", + "PlanContentTypeTasks" + ] + }, + "models.PlanExecutionType": { + "type": "string", + "enum": [ + "自动", + "手动" + ], + "x-enum-comments": { + "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", + "PlanExecutionTypeManual": "手动执行" + }, + "x-enum-descriptions": [ + "自动执行 (包含定时和循环)", + "手动执行" + ], + "x-enum-varnames": [ + "PlanExecutionTypeAutomatic", + "PlanExecutionTypeManual" + ] + }, + "models.PlanStatus": { + "type": "string", + "enum": [ + "已禁用", + "已启用", + "执行完毕", + "执行失败" + ], + "x-enum-comments": { + "PlanStatusDisabled": "禁用计划", + "PlanStatusEnabled": "启用计划", + "PlanStatusFailed": "执行失败", + "PlanStatusStopped": "执行完毕" + }, + "x-enum-descriptions": [ + "禁用计划", + "启用计划", + "执行完毕", + "执行失败" + ], + "x-enum-varnames": [ + "PlanStatusDisabled", + "PlanStatusEnabled", + "PlanStatusStopped", + "PlanStatusFailed" + ] + }, + "models.SensorType": { + "type": "string", + "enum": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-comments": { + "SensorTypeBatteryLevel": "电池电量", + "SensorTypeHumidity": "湿度", + "SensorTypeSignalMetrics": "信号强度", + "SensorTypeTemperature": "温度", + "SensorTypeWeight": "重量" + }, + "x-enum-descriptions": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-varnames": [ + "SensorTypeSignalMetrics", + "SensorTypeBatteryLevel", + "SensorTypeTemperature", + "SensorTypeHumidity", + "SensorTypeWeight" + ] + }, + "models.TaskType": { + "type": "string", + "enum": [ + "计划分析", + "等待", + "下料" + ], + "x-enum-comments": { + "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", + "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", + "TaskTypeWaiting": "等待任务" + }, + "x-enum-descriptions": [ + "解析Plan的Task列表并添加到待执行队列的特殊任务", + "等待任务", + "下料口释放指定重量任务" + ], + "x-enum-varnames": [ + "TaskPlanAnalysis", + "TaskTypeWaiting", + "TaskTypeReleaseFeedWeight" + ] + }, + "models.ValueDescriptor": { "type": "object", "properties": { - "id": { - "type": "integer", - "example": 1 + "multiplier": { + "description": "乘数,用于原始数据转换", + "type": "number" }, - "token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + "offset": { + "description": "偏移量,用于原始数据转换", + "type": "number" }, - "username": { - "type": "string", - "example": "testuser" + "type": { + "$ref": "#/definitions/models.SensorType" } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a17b732..e5dcd7c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -49,7 +49,7 @@ definitions: - CodeConflict - CodeInternalError - CodeServiceUnavailable - device.AreaControllerResponse: + dto.AreaControllerResponse: properties: created_at: type: string @@ -69,7 +69,43 @@ definitions: updated_at: type: string type: object - device.CreateAreaControllerRequest: + dto.AssignEmptyPensToBatchRequest: + type: object + dto.BuyPigsRequest: + properties: + penID: + description: 猪栏ID + type: integer + quantity: + description: 买入猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + totalPrice: + description: 总价 + minimum: 0 + type: number + tradeDate: + description: 交易日期 + type: string + traderName: + description: 交易方名称 + type: string + unitPrice: + description: 单价 + minimum: 0 + type: number + required: + - penID + - quantity + - totalPrice + - tradeDate + - traderName + - unitPrice + type: object + dto.CreateAreaControllerRequest: properties: location: type: string @@ -84,7 +120,7 @@ definitions: - name - network_id type: object - device.CreateDeviceRequest: + dto.CreateDeviceRequest: properties: area_controller_id: type: integer @@ -102,7 +138,7 @@ definitions: - device_template_id - name type: object - device.CreateDeviceTemplateRequest: + dto.CreateDeviceTemplateRequest: properties: category: $ref: '#/definitions/models.DeviceCategory' @@ -124,86 +160,80 @@ definitions: - commands - name type: object - device.DeviceTemplateResponse: + dto.CreatePenRequest: properties: - category: - $ref: '#/definitions/models.DeviceCategory' - commands: - additionalProperties: true - type: object - created_at: + capacity: + type: integer + house_id: + type: integer + pen_number: type: string + required: + - capacity + - house_id + - pen_number + type: object + dto.CreatePigHouseRequest: + properties: description: type: string + name: + type: string + required: + - name + type: object + dto.CreatePlanRequest: + properties: + cron_expression: + example: 0 0 6 * * * + type: string + description: + example: 根据温度自动调节风扇和加热器 + type: string + execute_num: + example: 10 + type: integer + execution_type: + allOf: + - $ref: '#/definitions/models.PlanExecutionType' + example: 自动 + name: + example: 猪舍温度控制计划 + type: string + sub_plan_ids: + items: + type: integer + type: array + tasks: + items: + $ref: '#/definitions/dto.TaskRequest' + type: array + required: + - execution_type + - name + type: object + dto.CreateUserRequest: + properties: + password: + example: password123 + type: string + username: + example: newuser + type: string + required: + - password + - username + type: object + dto.CreateUserResponse: + properties: id: + example: 1 type: integer - manufacturer: + username: + example: newuser type: string - name: - type: string - updated_at: - type: string - values: - items: - $ref: '#/definitions/models.ValueDescriptor' - type: array type: object - device.UpdateAreaControllerRequest: - properties: - location: - type: string - name: - type: string - network_id: - type: string - properties: - additionalProperties: true - type: object - required: - - name - - network_id - type: object - device.UpdateDeviceRequest: - properties: - area_controller_id: - type: integer - device_template_id: - type: integer - location: - type: string - name: - type: string - properties: - additionalProperties: true - type: object - required: - - area_controller_id - - device_template_id - - name - type: object - device.UpdateDeviceTemplateRequest: - properties: - category: - $ref: '#/definitions/models.DeviceCategory' - commands: - additionalProperties: true - type: object - description: - type: string - manufacturer: - type: string - name: - type: string - values: - items: - $ref: '#/definitions/models.ValueDescriptor' - type: array - required: - - category - - commands - - name - type: object - git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse: + dto.DeviceResponse: properties: area_controller_id: type: integer @@ -227,18 +257,750 @@ definitions: updated_at: type: string type: object + dto.DeviceTemplateResponse: + properties: + category: + $ref: '#/definitions/models.DeviceCategory' + commands: + additionalProperties: true + type: object + created_at: + type: string + description: + type: string + id: + type: integer + manufacturer: + type: string + name: + type: string + updated_at: + type: string + values: + items: + $ref: '#/definitions/models.ValueDescriptor' + type: array + type: object + dto.HistoryResponse: + properties: + action_type: + example: 更新设备 + type: string + description: + example: 设备更新成功 + type: string + target_resource: {} + time: + type: string + user_id: + example: 101 + type: integer + username: + example: testuser + type: string + type: object + dto.ListHistoryResponse: + properties: + history: + items: + $ref: '#/definitions/dto.HistoryResponse' + type: array + total: + example: 100 + type: integer + type: object + dto.LoginRequest: + properties: + identifier: + description: Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号 + example: testuser + type: string + password: + example: password123 + type: string + required: + - identifier + - password + type: object + dto.LoginResponse: + properties: + id: + example: 1 + type: integer + token: + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + type: string + username: + example: testuser + type: string + type: object + dto.MovePigsIntoPenRequest: + properties: + quantity: + description: 移入猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + toPenID: + description: 目标猪栏ID + type: integer + required: + - quantity + - toPenID + type: object + dto.PenResponse: + properties: + capacity: + type: integer + house_id: + type: integer + id: + type: integer + pen_number: + type: string + pig_batch_id: + type: integer + status: + $ref: '#/definitions/models.PenStatus' + type: object + dto.PigBatchCreateDTO: + properties: + batch_number: + description: 批次编号,必填 + type: string + initial_count: + description: 初始数量,必填,最小为1 + minimum: 1 + type: integer + origin_type: + allOf: + - $ref: '#/definitions/models.PigBatchOriginType' + description: 批次来源,必填 + start_date: + description: 批次开始日期,必填 + type: string + status: + allOf: + - $ref: '#/definitions/models.PigBatchStatus' + description: 批次状态,必填 + required: + - batch_number + - initial_count + - origin_type + - start_date + - status + type: object + dto.PigBatchResponseDTO: + properties: + batch_number: + description: 批次编号 + type: string + create_time: + description: 创建时间 + type: string + end_date: + description: 批次结束日期 + type: string + id: + description: 批次ID + type: integer + initial_count: + description: 初始数量 + type: integer + is_active: + description: 是否活跃 + type: boolean + origin_type: + allOf: + - $ref: '#/definitions/models.PigBatchOriginType' + description: 批次来源 + start_date: + description: 批次开始日期 + type: string + status: + allOf: + - $ref: '#/definitions/models.PigBatchStatus' + description: 批次状态 + update_time: + description: 更新时间 + type: string + type: object + dto.PigBatchUpdateDTO: + properties: + batch_number: + description: 批次编号,可选 + type: string + end_date: + description: 批次结束日期,可选 + type: string + initial_count: + description: 初始数量,可选 + type: integer + origin_type: + allOf: + - $ref: '#/definitions/models.PigBatchOriginType' + description: 批次来源,可选 + start_date: + description: 批次开始日期,可选 + type: string + status: + allOf: + - $ref: '#/definitions/models.PigBatchStatus' + description: 批次状态,可选 + type: object + dto.PigHouseResponse: + properties: + description: + type: string + id: + type: integer + name: + type: string + type: object + dto.PlanResponse: + properties: + content_type: + allOf: + - $ref: '#/definitions/models.PlanContentType' + example: 任务 + cron_expression: + example: 0 0 6 * * * + type: string + description: + example: 根据温度自动调节风扇和加热器 + type: string + execute_count: + example: 0 + type: integer + execute_num: + example: 10 + type: integer + execution_type: + allOf: + - $ref: '#/definitions/models.PlanExecutionType' + example: 自动 + id: + example: 1 + type: integer + name: + example: 猪舍温度控制计划 + type: string + status: + allOf: + - $ref: '#/definitions/models.PlanStatus' + example: 已启用 + sub_plans: + items: + $ref: '#/definitions/dto.SubPlanResponse' + type: array + tasks: + items: + $ref: '#/definitions/dto.TaskResponse' + type: array + type: object + dto.ReclassifyPenToNewBatchRequest: + properties: + penID: + description: 待划拨的猪栏ID + type: integer + remarks: + description: 备注 + type: string + toBatchID: + description: 目标猪批次ID + type: integer + required: + - penID + - toBatchID + type: object + dto.RecordCullRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 淘汰猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + required: + - happenedAt + - penID + - quantity + type: object + dto.RecordDeathRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 死亡猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + required: + - happenedAt + - penID + - quantity + type: object + dto.RecordSickPigCullRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 淘汰猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + treatmentLocation: + allOf: + - $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation' + description: 治疗地点 + required: + - happenedAt + - penID + - quantity + - treatmentLocation + type: object + dto.RecordSickPigDeathRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 死亡猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + treatmentLocation: + allOf: + - $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation' + description: 治疗地点 + required: + - happenedAt + - penID + - quantity + - treatmentLocation + type: object + dto.RecordSickPigRecoveryRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 康复猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + treatmentLocation: + allOf: + - $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation' + description: 治疗地点 + required: + - happenedAt + - penID + - quantity + - treatmentLocation + type: object + dto.RecordSickPigsRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 病猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + treatmentLocation: + allOf: + - $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation' + description: 治疗地点 + required: + - happenedAt + - penID + - quantity + - treatmentLocation + type: object + dto.SellPigsRequest: + properties: + penID: + description: 猪栏ID + type: integer + quantity: + description: 卖出猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + totalPrice: + description: 总价 + minimum: 0 + type: number + tradeDate: + description: 交易日期 + type: string + traderName: + description: 交易方名称 + type: string + unitPrice: + description: 单价 + minimum: 0 + type: number + required: + - penID + - quantity + - totalPrice + - tradeDate + - traderName + - unitPrice + type: object + dto.SubPlanResponse: + properties: + child_plan: + $ref: '#/definitions/dto.PlanResponse' + child_plan_id: + example: 2 + type: integer + execution_order: + example: 1 + type: integer + id: + example: 1 + type: integer + parent_plan_id: + example: 1 + type: integer + type: object + dto.TaskRequest: + properties: + description: + example: 打开1号风扇 + type: string + execution_order: + example: 1 + type: integer + name: + example: 打开风扇 + type: string + parameters: + additionalProperties: true + type: object + type: + allOf: + - $ref: '#/definitions/models.TaskType' + example: 等待 + type: object + dto.TaskResponse: + properties: + description: + example: 打开1号风扇 + type: string + execution_order: + example: 1 + type: integer + id: + example: 1 + type: integer + name: + example: 打开风扇 + type: string + parameters: + additionalProperties: true + type: object + plan_id: + example: 1 + type: integer + type: + allOf: + - $ref: '#/definitions/models.TaskType' + example: 等待 + type: object + dto.TransferPigsAcrossBatchesRequest: + properties: + destBatchID: + description: 目标猪批次ID + type: integer + fromPenID: + description: 源猪栏ID + type: integer + quantity: + description: 调栏猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + toPenID: + description: 目标猪栏ID + type: integer + required: + - destBatchID + - fromPenID + - quantity + - toPenID + type: object + dto.TransferPigsWithinBatchRequest: + properties: + fromPenID: + description: 源猪栏ID + type: integer + quantity: + description: 调栏猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + toPenID: + description: 目标猪栏ID + type: integer + required: + - fromPenID + - quantity + - toPenID + type: object + dto.UpdateAreaControllerRequest: + properties: + location: + type: string + name: + type: string + network_id: + type: string + properties: + additionalProperties: true + type: object + required: + - name + - network_id + type: object + dto.UpdateDeviceRequest: + properties: + area_controller_id: + type: integer + device_template_id: + type: integer + location: + type: string + name: + type: string + properties: + additionalProperties: true + type: object + required: + - area_controller_id + - device_template_id + - name + type: object + dto.UpdateDeviceTemplateRequest: + properties: + category: + $ref: '#/definitions/models.DeviceCategory' + commands: + additionalProperties: true + type: object + description: + type: string + manufacturer: + type: string + name: + type: string + values: + items: + $ref: '#/definitions/models.ValueDescriptor' + type: array + required: + - category + - commands + - name + type: object + dto.UpdatePenRequest: + properties: + capacity: + type: integer + house_id: + type: integer + pen_number: + type: string + status: + allOf: + - $ref: '#/definitions/models.PenStatus' + description: 添加oneof校验 + enum: + - 空闲 + - 使用中 + - 病猪栏 + - 康复栏 + - 清洗消毒 + - 维修中 + required: + - capacity + - house_id + - pen_number + - status + type: object + dto.UpdatePenStatusRequest: + properties: + status: + allOf: + - $ref: '#/definitions/models.PenStatus' + enum: + - 空闲 + - 使用中 + - 病猪栏 + - 康复栏 + - 清洗消毒 + - 维修中 + example: 病猪栏 + required: + - status + type: object + dto.UpdatePigHouseRequest: + properties: + description: + type: string + name: + type: string + required: + - name + type: object + dto.UpdatePlanRequest: + properties: + cron_expression: + example: 0 0 6 * * * + type: string + description: + example: 更新后的描述 + type: string + execute_num: + example: 10 + type: integer + execution_type: + allOf: + - $ref: '#/definitions/models.PlanExecutionType' + example: 自动 + name: + example: 猪舍温度控制计划V2 + type: string + sub_plan_ids: + items: + type: integer + type: array + tasks: + items: + $ref: '#/definitions/dto.TaskRequest' + type: array + required: + - execution_type + type: object models.DeviceCategory: enum: - - actuator - - sensor + - 执行器 + - 传感器 type: string x-enum-varnames: - CategoryActuator - CategorySensor + models.PenStatus: + enum: + - 空闲 + - 使用中 + - 病猪栏 + - 康复栏 + - 清洗消毒 + - 维修中 + type: string + x-enum-varnames: + - PenStatusEmpty + - PenStatusOccupied + - PenStatusSickPen + - PenStatusRecovering + - PenStatusCleaning + - PenStatusUnderMaint + models.PigBatchOriginType: + enum: + - 自繁 + - 外购 + type: string + x-enum-varnames: + - OriginTypeSelfFarrowed + - OriginTypePurchased + models.PigBatchSickPigTreatmentLocation: + enum: + - 原地治疗 + - 病猪栏治疗 + type: string + x-enum-varnames: + - TreatmentLocationOnSite + - TreatmentLocationSickBay + models.PigBatchStatus: + enum: + - 保育 + - 生长 + - 育肥 + - 待售 + - 已出售 + - 已归档 + type: string + x-enum-comments: + BatchStatusArchived: 批次结束(如全群淘汰等) + BatchStatusFinishing: 最后的育肥阶段 + BatchStatusForSale: 达到出栏标准 + BatchStatusGrowing: 生长育肥阶段 + BatchStatusWeaning: 从断奶到保育结束 + x-enum-descriptions: + - 从断奶到保育结束 + - 生长育肥阶段 + - 最后的育肥阶段 + - 达到出栏标准 + - "" + - 批次结束(如全群淘汰等) + x-enum-varnames: + - BatchStatusWeaning + - BatchStatusGrowing + - BatchStatusFinishing + - BatchStatusForSale + - BatchStatusSold + - BatchStatusArchived models.PlanContentType: enum: - - sub_plans - - tasks + - 子计划 + - 任务 type: string x-enum-comments: PlanContentTypeSubPlans: 计划包含子计划 @@ -251,8 +1013,8 @@ definitions: - PlanContentTypeTasks models.PlanExecutionType: enum: - - automatic - - manual + - 自动 + - 手动 type: string x-enum-comments: PlanExecutionTypeAutomatic: 自动执行 (包含定时和循环) @@ -265,17 +1027,16 @@ definitions: - PlanExecutionTypeManual models.PlanStatus: enum: - - 0 - - 1 - - 2 - - 3 - format: int32 - type: integer + - 已禁用 + - 已启用 + - 执行完毕 + - 执行失败 + type: string x-enum-comments: PlanStatusDisabled: 禁用计划 PlanStatusEnabled: 启用计划 PlanStatusFailed: 执行失败 - PlanStatusStopeed: 执行完毕 + PlanStatusStopped: 执行完毕 x-enum-descriptions: - 禁用计划 - 启用计划 @@ -284,15 +1045,15 @@ definitions: x-enum-varnames: - PlanStatusDisabled - PlanStatusEnabled - - PlanStatusStopeed + - PlanStatusStopped - PlanStatusFailed models.SensorType: enum: - - signal_metrics - - battery_level - - temperature - - humidity - - weight + - 信号强度 + - 电池电量 + - 温度 + - 湿度 + - 重量 type: string x-enum-comments: SensorTypeBatteryLevel: 电池电量 @@ -314,9 +1075,9 @@ definitions: - SensorTypeWeight models.TaskType: enum: - - plan_analysis - - waiting - - release_feed_weight + - 计划分析 + - 等待 + - 下料 type: string x-enum-comments: TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务 @@ -341,249 +1102,6 @@ definitions: type: $ref: '#/definitions/models.SensorType' type: object - plan.CreatePlanRequest: - properties: - cron_expression: - example: 0 0 6 * * * - type: string - description: - example: 根据温度自动调节风扇和加热器 - type: string - execute_num: - example: 10 - type: integer - execution_type: - allOf: - - $ref: '#/definitions/models.PlanExecutionType' - example: automatic - name: - example: 猪舍温度控制计划 - type: string - sub_plan_ids: - items: - type: integer - type: array - tasks: - items: - $ref: '#/definitions/plan.TaskRequest' - type: array - required: - - execution_type - - name - type: object - plan.ListPlansResponse: - properties: - plans: - items: - $ref: '#/definitions/plan.PlanResponse' - type: array - total: - example: 100 - type: integer - type: object - plan.PlanResponse: - properties: - content_type: - allOf: - - $ref: '#/definitions/models.PlanContentType' - example: tasks - cron_expression: - example: 0 0 6 * * * - type: string - description: - example: 根据温度自动调节风扇和加热器 - type: string - execute_count: - example: 0 - type: integer - execute_num: - example: 10 - type: integer - execution_type: - allOf: - - $ref: '#/definitions/models.PlanExecutionType' - example: automatic - id: - example: 1 - type: integer - name: - example: 猪舍温度控制计划 - type: string - status: - allOf: - - $ref: '#/definitions/models.PlanStatus' - example: 0 - sub_plans: - items: - $ref: '#/definitions/plan.SubPlanResponse' - type: array - tasks: - items: - $ref: '#/definitions/plan.TaskResponse' - type: array - type: object - plan.SubPlanResponse: - properties: - child_plan: - $ref: '#/definitions/plan.PlanResponse' - child_plan_id: - example: 2 - type: integer - execution_order: - example: 1 - type: integer - id: - example: 1 - type: integer - parent_plan_id: - example: 1 - type: integer - type: object - plan.TaskRequest: - properties: - description: - example: 打开1号风扇 - type: string - execution_order: - example: 1 - type: integer - name: - example: 打开风扇 - type: string - parameters: - additionalProperties: true - type: object - type: - allOf: - - $ref: '#/definitions/models.TaskType' - example: waiting - type: object - plan.TaskResponse: - properties: - description: - example: 打开1号风扇 - type: string - execution_order: - example: 1 - type: integer - id: - example: 1 - type: integer - name: - example: 打开风扇 - type: string - parameters: - additionalProperties: true - type: object - plan_id: - example: 1 - type: integer - type: - allOf: - - $ref: '#/definitions/models.TaskType' - example: waiting - type: object - plan.UpdatePlanRequest: - properties: - cron_expression: - example: 0 0 6 * * * - type: string - description: - example: 更新后的描述 - type: string - execute_num: - example: 10 - type: integer - execution_type: - allOf: - - $ref: '#/definitions/models.PlanExecutionType' - example: automatic - name: - example: 猪舍温度控制计划V2 - type: string - sub_plan_ids: - items: - type: integer - type: array - tasks: - items: - $ref: '#/definitions/plan.TaskRequest' - type: array - type: object - user.CreateUserRequest: - properties: - password: - example: password123 - type: string - username: - example: newuser - type: string - required: - - password - - username - type: object - user.CreateUserResponse: - properties: - id: - example: 1 - type: integer - username: - example: newuser - type: string - type: object - user.HistoryResponse: - properties: - action_type: - example: 更新设备 - type: string - description: - example: 设备更新成功 - type: string - target_resource: {} - time: - type: string - user_id: - example: 101 - type: integer - username: - example: testuser - type: string - type: object - user.ListHistoryResponse: - properties: - history: - items: - $ref: '#/definitions/user.HistoryResponse' - type: array - total: - example: 100 - type: integer - type: object - user.LoginRequest: - properties: - identifier: - description: Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号 - example: testuser - type: string - password: - example: password123 - type: string - required: - - identifier - - password - type: object - user.LoginResponse: - properties: - id: - example: 1 - type: integer - token: - example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - type: string - username: - example: testuser - type: string - type: object info: contact: {} paths: @@ -601,7 +1119,7 @@ paths: - properties: data: items: - $ref: '#/definitions/device.AreaControllerResponse' + $ref: '#/definitions/dto.AreaControllerResponse' type: array type: object summary: 获取所有区域主控列表 @@ -617,7 +1135,7 @@ paths: name: areaController required: true schema: - $ref: '#/definitions/device.CreateAreaControllerRequest' + $ref: '#/definitions/dto.CreateAreaControllerRequest' produces: - application/json responses: @@ -628,7 +1146,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/device.AreaControllerResponse' + $ref: '#/definitions/dto.AreaControllerResponse' type: object summary: 创建新区域主控 tags: @@ -670,7 +1188,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/device.AreaControllerResponse' + $ref: '#/definitions/dto.AreaControllerResponse' type: object summary: 获取区域主控信息 tags: @@ -690,7 +1208,7 @@ paths: name: areaController required: true schema: - $ref: '#/definitions/device.UpdateAreaControllerRequest' + $ref: '#/definitions/dto.UpdateAreaControllerRequest' produces: - application/json responses: @@ -701,7 +1219,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/device.AreaControllerResponse' + $ref: '#/definitions/dto.AreaControllerResponse' type: object summary: 更新区域主控信息 tags: @@ -720,7 +1238,7 @@ paths: - properties: data: items: - $ref: '#/definitions/device.DeviceTemplateResponse' + $ref: '#/definitions/dto.DeviceTemplateResponse' type: array type: object summary: 获取设备模板列表 @@ -736,7 +1254,7 @@ paths: name: deviceTemplate required: true schema: - $ref: '#/definitions/device.CreateDeviceTemplateRequest' + $ref: '#/definitions/dto.CreateDeviceTemplateRequest' produces: - application/json responses: @@ -747,7 +1265,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/device.DeviceTemplateResponse' + $ref: '#/definitions/dto.DeviceTemplateResponse' type: object summary: 创建新设备模板 tags: @@ -789,7 +1307,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/device.DeviceTemplateResponse' + $ref: '#/definitions/dto.DeviceTemplateResponse' type: object summary: 获取设备模板信息 tags: @@ -809,7 +1327,7 @@ paths: name: deviceTemplate required: true schema: - $ref: '#/definitions/device.UpdateDeviceTemplateRequest' + $ref: '#/definitions/dto.UpdateDeviceTemplateRequest' produces: - application/json responses: @@ -820,7 +1338,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/device.DeviceTemplateResponse' + $ref: '#/definitions/dto.DeviceTemplateResponse' type: object summary: 更新设备模板信息 tags: @@ -839,7 +1357,7 @@ paths: - properties: data: items: - $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' + $ref: '#/definitions/dto.DeviceResponse' type: array type: object summary: 获取设备列表 @@ -855,7 +1373,7 @@ paths: name: device required: true schema: - $ref: '#/definitions/device.CreateDeviceRequest' + $ref: '#/definitions/dto.CreateDeviceRequest' produces: - application/json responses: @@ -866,7 +1384,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' + $ref: '#/definitions/dto.DeviceResponse' type: object summary: 创建新设备 tags: @@ -908,7 +1426,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' + $ref: '#/definitions/dto.DeviceResponse' type: object summary: 获取设备信息 tags: @@ -928,7 +1446,7 @@ paths: name: device required: true schema: - $ref: '#/definitions/device.UpdateDeviceRequest' + $ref: '#/definitions/dto.UpdateDeviceRequest' produces: - application/json responses: @@ -939,11 +1457,780 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' + $ref: '#/definitions/dto.DeviceResponse' type: object summary: 更新设备信息 tags: - 设备管理 + /api/v1/pens: + get: + description: 获取所有猪栏的列表 + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.PenResponse' + type: array + type: object + summary: 获取猪栏列表 + tags: + - 猪场管理 + post: + consumes: + - application/json + description: 创建一个新的猪栏 + parameters: + - description: 猪栏信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.CreatePenRequest' + produces: + - application/json + responses: + "201": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PenResponse' + type: object + summary: 创建猪栏 + tags: + - 猪场管理 + /api/v1/pens/{id}: + delete: + description: 根据ID删除一个猪栏 + parameters: + - description: 猪栏ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 删除猪栏 + tags: + - 猪场管理 + get: + description: 根据ID获取单个猪栏信息 + parameters: + - description: 猪栏ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PenResponse' + type: object + summary: 获取单个猪栏 + tags: + - 猪场管理 + put: + consumes: + - application/json + description: 更新一个已存在的猪栏信息 + parameters: + - description: 猪栏ID + in: path + name: id + required: true + type: integer + - description: 猪栏信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdatePenRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PenResponse' + type: object + summary: 更新猪栏 + tags: + - 猪场管理 + /api/v1/pens/{id}/status: + put: + consumes: + - application/json + description: 更新指定猪栏的当前状态 + parameters: + - description: 猪栏ID + in: path + name: id + required: true + type: integer + - description: 新的猪栏状态 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdatePenStatusRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PenResponse' + type: object + summary: 更新猪栏状态 + tags: + - 猪场管理 + /api/v1/pig-batches: + get: + description: 获取所有猪批次的列表,支持按活跃状态筛选 + parameters: + - description: 是否活跃 (true/false) + in: query + name: is_active + type: boolean + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.PigBatchResponseDTO' + type: array + type: object + summary: 获取猪批次列表 + tags: + - 猪群管理 + post: + consumes: + - application/json + description: 创建一个新的猪批次 + parameters: + - description: 猪批次信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.PigBatchCreateDTO' + produces: + - application/json + responses: + "201": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBatchResponseDTO' + type: object + summary: 创建猪批次 + tags: + - 猪群管理 + /api/v1/pig-batches/{batchID}/remove-pen/{penID}: + delete: + description: 将一个空闲猪栏从指定的猪批次中移除 + parameters: + - description: 猪批次ID + in: path + name: batchID + required: true + type: integer + - description: 待移除的猪栏ID + in: path + name: penID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 移除成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 从猪批次移除空栏 + tags: + - 猪群管理 + /api/v1/pig-batches/{fromBatchID}/reclassify-pen: + post: + consumes: + - application/json + description: 将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次 + parameters: + - description: 源猪批次ID + in: path + name: fromBatchID + required: true + type: integer + - description: 划拨请求信息 (包含目标批次ID、猪栏ID和备注) + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.ReclassifyPenToNewBatchRequest' + produces: + - application/json + responses: + "200": + description: 划拨成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 将猪栏划拨到新批次 + tags: + - 猪群管理 + /api/v1/pig-batches/{id}: + delete: + description: 根据ID删除一个猪批次 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 删除猪批次 + tags: + - 猪群管理 + get: + description: 根据ID获取单个猪批次信息 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBatchResponseDTO' + type: object + summary: 获取单个猪批次 + tags: + - 猪群管理 + put: + consumes: + - application/json + description: 更新一个已存在的猪批次信息 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 猪批次信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.PigBatchUpdateDTO' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBatchResponseDTO' + type: object + summary: 更新猪批次 + tags: + - 猪群管理 + /api/v1/pig-batches/{id}/assign-pens: + post: + consumes: + - application/json + description: 将一个或多个空闲猪栏分配给指定的猪批次 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 待分配的猪栏ID列表 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.AssignEmptyPensToBatchRequest' + produces: + - application/json + responses: + "200": + description: 分配成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 为猪批次分配空栏 + tags: + - 猪群管理 + /api/v1/pig-batches/{id}/buy-pigs: + post: + consumes: + - application/json + description: 记录猪批次中的猪只购买事件 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 买猪请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.BuyPigsRequest' + produces: + - application/json + responses: + "200": + description: 买猪成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 处理买猪的业务逻辑 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/move-pigs-into-pen: + post: + consumes: + - application/json + description: 将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 移入猪只请求信息 (包含目标猪栏ID、数量和备注) + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.MovePigsIntoPenRequest' + produces: + - application/json + responses: + "200": + description: 移入成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 将猪只从“虚拟库存”移入指定猪栏 + tags: + - 猪群管理 + /api/v1/pig-batches/{id}/record-cull: + post: + consumes: + - application/json + description: 记录猪批次中正常猪只淘汰的数量和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录正常猪只淘汰请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordCullRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录正常猪只淘汰事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-death: + post: + consumes: + - application/json + description: 记录猪批次中正常猪只死亡的数量和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录正常猪只死亡请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordDeathRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录正常猪只死亡事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-sick-pig-cull: + post: + consumes: + - application/json + description: 记录猪批次中病猪淘汰的数量、治疗地点和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录病猪淘汰请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordSickPigCullRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录病猪淘汰事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-sick-pig-death: + post: + consumes: + - application/json + description: 记录猪批次中病猪死亡的数量、治疗地点和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录病猪死亡请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordSickPigDeathRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录病猪死亡事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-sick-pig-recovery: + post: + consumes: + - application/json + description: 记录猪批次中病猪康复的数量、治疗地点和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录病猪康复请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordSickPigRecoveryRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录病猪康复事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-sick-pigs: + post: + consumes: + - application/json + description: 记录猪批次中新增病猪的数量、治疗地点和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录病猪请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordSickPigsRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录新增病猪事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/sell-pigs: + post: + consumes: + - application/json + description: 记录猪批次中的猪只出售事件 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 卖猪请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.SellPigsRequest' + produces: + - application/json + responses: + "200": + description: 卖猪成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 处理卖猪的业务逻辑 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/transfer-within-batch: + post: + consumes: + - application/json + description: 将指定数量的猪只在同一个猪群的不同猪栏间调动 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 群内调栏请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.TransferPigsWithinBatchRequest' + produces: + - application/json + responses: + "200": + description: 调栏成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 群内调栏 + tags: + - 猪批次管理 + /api/v1/pig-batches/{sourceBatchID}/transfer-across-batches: + post: + consumes: + - application/json + description: 将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏 + parameters: + - description: 源猪批次ID + in: path + name: sourceBatchID + required: true + type: integer + - description: 跨群调栏请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.TransferPigsAcrossBatchesRequest' + produces: + - application/json + responses: + "200": + description: 调栏成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 跨猪群调栏 + tags: + - 猪批次管理 + /api/v1/pig-houses: + get: + description: 获取所有猪舍的列表 + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.PigHouseResponse' + type: array + type: object + summary: 获取猪舍列表 + tags: + - 猪场管理 + post: + consumes: + - application/json + description: 创建一个新的猪舍 + parameters: + - description: 猪舍信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.CreatePigHouseRequest' + produces: + - application/json + responses: + "201": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigHouseResponse' + type: object + summary: 创建猪舍 + tags: + - 猪场管理 + /api/v1/pig-houses/{id}: + delete: + description: 根据ID删除一个猪舍 + parameters: + - description: 猪舍ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 删除猪舍 + tags: + - 猪场管理 + get: + description: 根据ID获取单个猪舍信息 + parameters: + - description: 猪舍ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigHouseResponse' + type: object + summary: 获取单个猪舍 + tags: + - 猪场管理 + put: + consumes: + - application/json + description: 更新一个已存在的猪舍信息 + parameters: + - description: 猪舍ID + in: path + name: id + required: true + type: integer + - description: 猪舍信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdatePigHouseRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigHouseResponse' + type: object + summary: 更新猪舍 + tags: + - 猪场管理 /api/v1/plans: get: description: 获取所有计划的列表 @@ -957,7 +2244,9 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/plan.ListPlansResponse' + items: + $ref: '#/definitions/dto.PlanResponse' + type: array type: object summary: 获取计划列表 tags: @@ -972,7 +2261,7 @@ paths: name: plan required: true schema: - $ref: '#/definitions/plan.CreatePlanRequest' + $ref: '#/definitions/dto.CreatePlanRequest' produces: - application/json responses: @@ -983,7 +2272,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/plan.PlanResponse' + $ref: '#/definitions/dto.PlanResponse' type: object summary: 创建计划 tags: @@ -1025,7 +2314,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/plan.PlanResponse' + $ref: '#/definitions/dto.PlanResponse' type: object summary: 获取计划详情 tags: @@ -1045,7 +2334,7 @@ paths: name: plan required: true schema: - $ref: '#/definitions/plan.UpdatePlanRequest' + $ref: '#/definitions/dto.UpdatePlanRequest' produces: - application/json responses: @@ -1056,7 +2345,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/plan.PlanResponse' + $ref: '#/definitions/dto.PlanResponse' type: object summary: 更新计划 tags: @@ -1110,7 +2399,7 @@ paths: name: user required: true schema: - $ref: '#/definitions/user.CreateUserRequest' + $ref: '#/definitions/dto.CreateUserRequest' produces: - application/json responses: @@ -1121,7 +2410,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/user.CreateUserResponse' + $ref: '#/definitions/dto.CreateUserResponse' type: object summary: 创建新用户 tags: @@ -1159,7 +2448,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/user.ListHistoryResponse' + $ref: '#/definitions/dto.ListHistoryResponse' type: object summary: 获取指定用户的操作历史 tags: @@ -1175,7 +2464,7 @@ paths: name: credentials required: true schema: - $ref: '#/definitions/user.LoginRequest' + $ref: '#/definitions/dto.LoginRequest' produces: - application/json responses: @@ -1186,7 +2475,7 @@ paths: - $ref: '#/definitions/controller.Response' - properties: data: - $ref: '#/definitions/user.LoginResponse' + $ref: '#/definitions/dto.LoginResponse' type: object summary: 用户登录 tags: diff --git a/internal/app/api/api.go b/internal/app/api/api.go index f53a5ed..53da648 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -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" @@ -35,18 +37,20 @@ import ( // API 结构体定义了 HTTP 服务器及其依赖 type API struct { - engine *gin.Engine // Gin 引擎实例,用于处理 HTTP 请求 - logger *logs.Logger // 日志记录器,用于输出日志信息 - userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作 - tokenService token.TokenService // Token 服务接口,用于 JWT token 的生成和解析 - auditService audit.Service // 审计服务,用于记录用户操作 - httpServer *http.Server // 标准库的 HTTP 服务器实例,用于启动和停止服务 - config config.ServerConfig // API 服务器的配置,使用 infra/config 包中的 ServerConfig - userController *user.Controller // 用户控制器实例 - deviceController *device.Controller // 设备控制器实例 - planController *plan.Controller // 计划控制器实例 - listenHandler transport.ListenHandler // 设备上行事件监听器 - analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例 + engine *gin.Engine // Gin 引擎实例,用于处理 HTTP 请求 + logger *logs.Logger // 日志记录器,用于输出日志信息 + userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作 + tokenService token.TokenService // Token 服务接口,用于 JWT token 的生成和解析 + auditService audit.Service // 审计服务,用于记录用户操作 + httpServer *http.Server // 标准库的 HTTP 服务器实例,用于启动和停止服务 + config config.ServerConfig // API 服务器的配置,使用 infra/config 包中的 ServerConfig + userController *user.Controller // 用户控制器实例 + deviceController *device.Controller // 设备控制器实例 + planController *plan.Controller // 计划控制器实例 + pigFarmController *management.PigFarmController // 猪场管理控制器实例 + pigBatchController *management.PigBatchController // 猪群控制器实例 + listenHandler webhook.ListenHandler // 设备上行事件监听器 + analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例 } // NewAPI 创建并返回一个新的 API 实例 @@ -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("猪群相关接口注册成功 (需要认证和审计)") + } } diff --git a/internal/app/controller/auth_utils.go b/internal/app/controller/auth_utils.go new file mode 100644 index 0000000..08a9589 --- /dev/null +++ b/internal/app/controller/auth_utils.go @@ -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 +} diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index ddbbc25..7d49124 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -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) diff --git a/internal/app/controller/device/device_controller_test.go b/internal/app/controller/device/device_controller_test.go index e943533..7bb1239 100644 --- a/internal/app/controller/device/device_controller_test.go +++ b/internal/app/controller/device/device_controller_test.go @@ -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) diff --git a/internal/app/controller/management/controller_helpers.go b/internal/app/controller/management/controller_helpers.go new file mode 100644 index 0000000..a37bd27 --- /dev/null +++ b/internal/app/controller/management/controller_helpers.go @@ -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) +} diff --git a/internal/app/controller/management/feed_controller.go b/internal/app/controller/management/feed_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/feed_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/medication_controller.go b/internal/app/controller/management/medication_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/medication_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go new file mode 100644 index 0000000..79a6bff --- /dev/null +++ b/internal/app/controller/management/pig_batch_controller.go @@ -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 + ) +} diff --git a/internal/app/controller/management/pig_batch_health_controller.go b/internal/app/controller/management/pig_batch_health_controller.go new file mode 100644 index 0000000..0bcff82 --- /dev/null +++ b/internal/app/controller/management/pig_batch_health_controller.go @@ -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 + ) +} diff --git a/internal/app/controller/management/pig_batch_trade_controller.go b/internal/app/controller/management/pig_batch_trade_controller.go new file mode 100644 index 0000000..74e5bd8 --- /dev/null +++ b/internal/app/controller/management/pig_batch_trade_controller.go @@ -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 + ) +} diff --git a/internal/app/controller/management/pig_batch_transfer_controller.go b/internal/app/controller/management/pig_batch_transfer_controller.go new file mode 100644 index 0000000..9089791 --- /dev/null +++ b/internal/app/controller/management/pig_batch_transfer_controller.go @@ -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 + ) +} diff --git a/internal/app/controller/management/pig_controller.go b/internal/app/controller/management/pig_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/pig_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go new file mode 100644 index 0000000..84a6a05 --- /dev/null +++ b/internal/app/controller/management/pig_farm_controller.go @@ -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) +} diff --git a/internal/app/controller/plan/converter_test.go b/internal/app/controller/plan/converter_test.go deleted file mode 100644 index 2626fe8..0000000 --- a/internal/app/controller/plan/converter_test.go +++ /dev/null @@ -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) - }) -} diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 6956e6a..e7a8862 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -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) diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index 1f84e68..27689d4 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -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, } diff --git a/internal/app/controller/user/user_controller_test.go b/internal/app/controller/user/user_controller_test.go index 2cd004c..c447f11 100644 --- a/internal/app/controller/user/user_controller_test.go +++ b/internal/app/controller/user/user_controller_test.go @@ -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() }, diff --git a/internal/app/dto/device_converter.go b/internal/app/dto/device_converter.go new file mode 100644 index 0000000..20a5ce3 --- /dev/null +++ b/internal/app/dto/device_converter.go @@ -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 +} diff --git a/internal/app/dto/device_dto.go b/internal/app/dto/device_dto.go new file mode 100644 index 0000000..d37f3f6 --- /dev/null +++ b/internal/app/dto/device_dto.go @@ -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"` +} diff --git a/internal/app/dto/pig_batch_dto.go b/internal/app/dto/pig_batch_dto.go new file mode 100644 index 0000000..8485d87 --- /dev/null +++ b/internal/app/dto/pig_batch_dto.go @@ -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"` // 备注 +} diff --git a/internal/app/dto/pig_farm_dto.go b/internal/app/dto/pig_farm_dto.go new file mode 100644 index 0000000..09f450a --- /dev/null +++ b/internal/app/dto/pig_farm_dto.go @@ -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:"病猪栏"` +} diff --git a/internal/app/controller/plan/converter.go b/internal/app/dto/plan_converter.go similarity index 90% rename from internal/app/controller/plan/converter.go rename to internal/app/dto/plan_converter.go index 57dbf96..2ead166 100644 --- a/internal/app/controller/plan/converter.go +++ b/internal/app/dto/plan_converter.go @@ -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 } diff --git a/internal/app/dto/plan_dto.go b/internal/app/dto/plan_dto.go new file mode 100644 index 0000000..37935a2 --- /dev/null +++ b/internal/app/dto/plan_dto.go @@ -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"` +} diff --git a/internal/app/dto/user_dto.go b/internal/app/dto/user_dto.go new file mode 100644 index 0000000..20d5fe7 --- /dev/null +++ b/internal/app/dto/user_dto.go @@ -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"` +} diff --git a/internal/app/middleware/audit.go b/internal/app/middleware/audit.go index db2a8b2..44ee93b 100644 --- a/internal/app/middleware/audit.go +++ b/internal/app/middleware/audit.go @@ -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" ) diff --git a/internal/app/middleware/auth.go b/internal/app/middleware/auth.go index 8684acb..2755313 100644 --- a/internal/app/middleware/auth.go +++ b/internal/app/middleware/auth.go @@ -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" diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go new file mode 100644 index 0000000..4f362c5 --- /dev/null +++ b/internal/app/service/pig_batch_service.go @@ -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 +} diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go new file mode 100644 index 0000000..5f4b0c5 --- /dev/null +++ b/internal/app/service/pig_farm_service.go @@ -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 +} diff --git a/internal/app/service/pig_service.go b/internal/app/service/pig_service.go new file mode 100644 index 0000000..37e9dc3 --- /dev/null +++ b/internal/app/service/pig_service.go @@ -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 // 对于未知的领域错误,直接返回 + } +} diff --git a/internal/app/service/transport/chirp_stack.go b/internal/app/webhook/chirp_stack.go similarity index 99% rename from internal/app/service/transport/chirp_stack.go rename to internal/app/webhook/chirp_stack.go index bf5779e..de07597 100644 --- a/internal/app/service/transport/chirp_stack.go +++ b/internal/app/webhook/chirp_stack.go @@ -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" diff --git a/internal/app/service/transport/chirp_stack_types.go b/internal/app/webhook/chirp_stack_types.go similarity index 99% rename from internal/app/service/transport/chirp_stack_types.go rename to internal/app/webhook/chirp_stack_types.go index a8b5bdd..55d7de5 100644 --- a/internal/app/service/transport/chirp_stack_types.go +++ b/internal/app/webhook/chirp_stack_types.go @@ -1,4 +1,4 @@ -package transport +package webhook import ( "encoding/json" diff --git a/internal/app/service/transport/transport.go b/internal/app/webhook/transport.go similarity index 89% rename from internal/app/service/transport/transport.go rename to internal/app/webhook/transport.go index b60fd29..7583c06 100644 --- a/internal/app/service/transport/transport.go +++ b/internal/app/webhook/transport.go @@ -1,4 +1,4 @@ -package transport +package webhook import "net/http" diff --git a/internal/core/application.go b/internal/core/application.go index a68c4f2..25e596f 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -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) } diff --git a/internal/app/service/audit/service.go b/internal/domain/audit/service.go similarity index 100% rename from internal/app/service/audit/service.go rename to internal/domain/audit/service.go diff --git a/internal/app/service/device/device_service.go b/internal/domain/device/device_service.go similarity index 100% rename from internal/app/service/device/device_service.go rename to internal/domain/device/device_service.go diff --git a/internal/app/service/device/general_device_service.go b/internal/domain/device/general_device_service.go similarity index 99% rename from internal/app/service/device/general_device_service.go rename to internal/domain/device/general_device_service.go index 3ea8069..d838476 100644 --- a/internal/app/service/device/general_device_service.go +++ b/internal/domain/device/general_device_service.go @@ -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" diff --git a/internal/app/service/device/proto/device.pb.go b/internal/domain/device/proto/device.pb.go similarity index 99% rename from internal/app/service/device/proto/device.pb.go rename to internal/domain/device/proto/device.pb.go index 573d4ff..712f749 100644 --- a/internal/app/service/device/proto/device.pb.go +++ b/internal/domain/device/proto/device.pb.go @@ -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 diff --git a/internal/app/service/device/proto/device.proto b/internal/domain/device/proto/device.proto similarity index 96% rename from internal/app/service/device/proto/device.proto rename to internal/domain/device/proto/device.proto index af23a51..a906255 100644 --- a/internal/app/service/device/proto/device.proto +++ b/internal/domain/device/proto/device.proto @@ -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"; // --- 通用指令结构 --- diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go new file mode 100644 index 0000000..9de7fbd --- /dev/null +++ b/internal/domain/pig/pen_transfer_manager.go @@ -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 +} diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go new file mode 100644 index 0000000..d0911d1 --- /dev/null +++ b/internal/domain/pig/pig_batch_service.go @@ -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 + }) +} diff --git a/internal/domain/pig/pig_batch_service_method.go b/internal/domain/pig/pig_batch_service_method.go new file mode 100644 index 0000000..5b4422e --- /dev/null +++ b/internal/domain/pig/pig_batch_service_method.go @@ -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) +} diff --git a/internal/domain/pig/pig_batch_service_pen_transfer.go b/internal/domain/pig/pig_batch_service_pen_transfer.go new file mode 100644 index 0000000..0ef76a7 --- /dev/null +++ b/internal/domain/pig/pig_batch_service_pen_transfer.go @@ -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 + }) +} diff --git a/internal/domain/pig/pig_batch_service_pig_sick.go b/internal/domain/pig/pig_batch_service_pig_sick.go new file mode 100644 index 0000000..b5bbe78 --- /dev/null +++ b/internal/domain/pig/pig_batch_service_pig_sick.go @@ -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 +} diff --git a/internal/domain/pig/pig_batch_service_pig_trade.go b/internal/domain/pig/pig_batch_service_pig_trade.go new file mode 100644 index 0000000..d2cec43 --- /dev/null +++ b/internal/domain/pig/pig_batch_service_pig_trade.go @@ -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 + }) +} diff --git a/internal/domain/pig/pig_sick_manager.go b/internal/domain/pig/pig_sick_manager.go new file mode 100644 index 0000000..5f10870 --- /dev/null +++ b/internal/domain/pig/pig_sick_manager.go @@ -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 +} diff --git a/internal/domain/pig/pig_trade_manager.go b/internal/domain/pig/pig_trade_manager.go new file mode 100644 index 0000000..394b411 --- /dev/null +++ b/internal/domain/pig/pig_trade_manager.go @@ -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) +} diff --git a/internal/app/service/task/analysis_plan_task_manager.go b/internal/domain/task/analysis_plan_task_manager.go similarity index 100% rename from internal/app/service/task/analysis_plan_task_manager.go rename to internal/domain/task/analysis_plan_task_manager.go diff --git a/internal/app/service/task/delay_task.go b/internal/domain/task/delay_task.go similarity index 100% rename from internal/app/service/task/delay_task.go rename to internal/domain/task/delay_task.go diff --git a/internal/app/service/task/delay_task_test.go b/internal/domain/task/delay_task_test.go similarity index 100% rename from internal/app/service/task/delay_task_test.go rename to internal/domain/task/delay_task_test.go diff --git a/internal/app/service/task/release_feed_weight_task.go b/internal/domain/task/release_feed_weight_task.go similarity index 98% rename from internal/app/service/task/release_feed_weight_task.go rename to internal/domain/task/release_feed_weight_task.go index f0c2fd4..a8c7d15 100644 --- a/internal/app/service/task/release_feed_weight_task.go +++ b/internal/domain/task/release_feed_weight_task.go @@ -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" diff --git a/internal/app/service/task/scheduler.go b/internal/domain/task/scheduler.go similarity index 99% rename from internal/app/service/task/scheduler.go rename to internal/domain/task/scheduler.go index d60d6c4..ebb8ca6 100644 --- a/internal/app/service/task/scheduler.go +++ b/internal/domain/task/scheduler.go @@ -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) } diff --git a/internal/app/service/task/task.go b/internal/domain/task/task.go similarity index 100% rename from internal/app/service/task/task.go rename to internal/domain/task/task.go diff --git a/internal/app/service/token/token_service.go b/internal/domain/token/token_service.go similarity index 100% rename from internal/app/service/token/token_service.go rename to internal/domain/token/token_service.go diff --git a/internal/app/service/token/token_service_test.go b/internal/domain/token/token_service_test.go similarity index 100% rename from internal/app/service/token/token_service_test.go rename to internal/domain/token/token_service_test.go diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 06ad895..5b868c5 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -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 } diff --git a/internal/infra/models/device_template.go b/internal/infra/models/device_template.go index 5eae4f5..ceb49e4 100644 --- a/internal/infra/models/device_template.go +++ b/internal/infra/models/device_template.go @@ -15,9 +15,9 @@ type DeviceCategory string const ( // CategoryActuator 代表一个执行器,可以被控制(例如:风机、阀门) - CategoryActuator DeviceCategory = "actuator" + CategoryActuator DeviceCategory = "执行器" // CategorySensor 代表一个传感器,用于报告测量值(例如:温度计) - CategorySensor DeviceCategory = "sensor" + CategorySensor DeviceCategory = "传感器" ) // ValueDescriptor 描述了传感器可以报告的单个数值。 diff --git a/internal/infra/models/execution.go b/internal/infra/models/execution.go index 9a8a58d..721441d 100644 --- a/internal/infra/models/execution.go +++ b/internal/infra/models/execution.go @@ -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 = "失败" ) // --- 审计日志相关上下文键 --- diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go new file mode 100644 index 0000000..7d5731c --- /dev/null +++ b/internal/infra/models/farm_asset.go @@ -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:猪栏当前状态"` +} diff --git a/internal/infra/models/feed.go b/internal/infra/models/feed.go new file mode 100644 index 0000000..a16378f --- /dev/null +++ b/internal/infra/models/feed.go @@ -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" +} diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go new file mode 100644 index 0000000..bd60718 --- /dev/null +++ b/internal/infra/models/medication.go @@ -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" +} diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 8f21ca5..7344a8a 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -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{}, } } diff --git a/internal/infra/models/pig_batch.go b/internal/infra/models/pig_batch.go new file mode 100644 index 0000000..658c31b --- /dev/null +++ b/internal/infra/models/pig_batch.go @@ -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" +} diff --git a/internal/infra/models/pig_sick.go b/internal/infra/models/pig_sick.go new file mode 100644 index 0000000..caf21a9 --- /dev/null +++ b/internal/infra/models/pig_sick.go @@ -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" +} diff --git a/internal/infra/models/pig_trade.go b/internal/infra/models/pig_trade.go new file mode 100644 index 0000000..4d4e835 --- /dev/null +++ b/internal/infra/models/pig_trade.go @@ -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" +} diff --git a/internal/infra/models/pig_transfer.go b/internal/infra/models/pig_transfer.go new file mode 100644 index 0000000..3fc3a04 --- /dev/null +++ b/internal/infra/models/pig_transfer.go @@ -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" +} diff --git a/internal/infra/models/plan.go b/internal/infra/models/plan.go index bf2ce42..15189fc 100644 --- a/internal/infra/models/plan.go +++ b/internal/infra/models/plan.go @@ -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,9 +59,9 @@ 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"` // 计划是否被启动 - ExecuteNum uint `gorm:"default:0" json:"execute_num"` // 计划预期执行次数 - ExecuteCount uint `gorm:"default:0" json:"execute_count"` // 执行计数器 + Status PlanStatus `gorm:"default:'已禁用';index" json:"status"` // 计划是否被启动 + ExecuteNum uint `gorm:"default:0" json:"execute_num"` // 计划预期执行次数 + ExecuteCount uint `gorm:"default:0" json:"execute_count"` // 执行计数器 // 针对 PlanExecutionTypeAutomatic,使用 Cron 表达式定义调度规则 CronExpression string `json:"cron_expression"` diff --git a/internal/infra/models/sensor_data.go b/internal/infra/models/sensor_data.go index 48be4f5..4c50221 100644 --- a/internal/infra/models/sensor_data.go +++ b/internal/infra/models/sensor_data.go @@ -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 存储信号强度数据 diff --git a/internal/infra/repository/execution_log_repository.go b/internal/infra/repository/execution_log_repository.go index d804247..33323e6 100644 --- a/internal/infra/repository/execution_log_repository.go +++ b/internal/infra/repository/execution_log_repository.go @@ -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 } diff --git a/internal/infra/repository/group_medication_log_repository.go b/internal/infra/repository/group_medication_log_repository.go new file mode 100644 index 0000000..2b048f5 --- /dev/null +++ b/internal/infra/repository/group_medication_log_repository.go @@ -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 +} diff --git a/internal/infra/repository/pig_batch_log_repository.go b/internal/infra/repository/pig_batch_log_repository.go new file mode 100644 index 0000000..34b4b59 --- /dev/null +++ b/internal/infra/repository/pig_batch_log_repository.go @@ -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 +} diff --git a/internal/infra/repository/pig_batch_repository.go b/internal/infra/repository/pig_batch_repository.go new file mode 100644 index 0000000..1c46c5a --- /dev/null +++ b/internal/infra/repository/pig_batch_repository.go @@ -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 +} diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go new file mode 100644 index 0000000..954f1f5 --- /dev/null +++ b/internal/infra/repository/pig_farm_repository.go @@ -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 +} diff --git a/internal/infra/repository/pig_pen_repository.go b/internal/infra/repository/pig_pen_repository.go new file mode 100644 index 0000000..84a1b69 --- /dev/null +++ b/internal/infra/repository/pig_pen_repository.go @@ -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 +} diff --git a/internal/infra/repository/pig_sick_repository.go b/internal/infra/repository/pig_sick_repository.go new file mode 100644 index 0000000..0e5dd36 --- /dev/null +++ b/internal/infra/repository/pig_sick_repository.go @@ -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 +} diff --git a/internal/infra/repository/pig_trade_repository.go b/internal/infra/repository/pig_trade_repository.go new file mode 100644 index 0000000..a1d7b68 --- /dev/null +++ b/internal/infra/repository/pig_trade_repository.go @@ -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 +} diff --git a/internal/infra/repository/pig_transfer_log_repository.go b/internal/infra/repository/pig_transfer_log_repository.go new file mode 100644 index 0000000..4d934fe --- /dev/null +++ b/internal/infra/repository/pig_transfer_log_repository.go @@ -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 +} diff --git a/internal/infra/repository/plan_repository_test.go b/internal/infra/repository/plan_repository_test.go deleted file mode 100644 index a7b5389..0000000 --- a/internal/infra/repository/plan_repository_test.go +++ /dev/null @@ -1,1225 +0,0 @@ -package repository_test - -import ( - "errors" - "fmt" - "testing" - "time" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" - "github.com/stretchr/testify/assert" - "gorm.io/gorm" -) - -// createTestPlan 是一个辅助函数,用于创建测试计划。 -func createTestPlan(name, description string, execType models.PlanExecutionType, contentType models.PlanContentType) models.Plan { - return models.Plan{ - Name: name, - Description: description, - ExecutionType: execType, - ContentType: contentType, - } -} - -// TestListBasicPlans 测试 ListBasicPlans 方法,确保它能正确返回所有计划的基本信息。 -func TestListBasicPlans(t *testing.T) { - tests := []struct { - name string - setupPlans []models.Plan - expectedCount int - expectedError error - }{ - { - name: "数据库中没有计划", - setupPlans: []models.Plan{}, - expectedCount: 0, - expectedError: nil, - }, - { - name: "数据库中有多个计划", - setupPlans: []models.Plan{ - createTestPlan("计划 A", "描述 A", models.PlanExecutionTypeAutomatic, models.PlanContentTypeTasks), - createTestPlan("计划 B", "描述 B", models.PlanExecutionTypeManual, models.PlanContentTypeSubPlans), - }, - expectedCount: 2, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := setupTestDB(t) - repo := repository.NewGormPlanRepository(db) - - for i := range tt.setupPlans { - err := db.Create(&tt.setupPlans[i]).Error - assert.NoError(t, err, "插入设置计划失败") - } - - plans, err := repo.ListBasicPlans() - - if tt.expectedError != nil { - assert.Error(t, err) - assert.True(t, errors.Is(err, tt.expectedError)) - } else { - assert.NoError(t, err) - assert.Len(t, plans, tt.expectedCount) - if tt.expectedCount > 0 { - // 验证返回的计划是否包含预期的ID - var actualIDs []uint - for _, p := range plans { - actualIDs = append(actualIDs, p.ID) - assert.Empty(t, p.SubPlans, "ListBasicPlans 不应加载子计划") - assert.Empty(t, p.Tasks, "ListBasicPlans 不应加载任务") - } - for _, setupPlan := range tt.setupPlans { - assert.Contains(t, actualIDs, setupPlan.ID, "返回的计划应包含设置计划的ID") - } - } - } - }) - } -} - -// TestGetBasicPlanByID 测试 GetBasicPlanByID 方法,确保它能根据ID正确返回计划的基本信息。 -func TestGetBasicPlanByID(t *testing.T) { - tests := []struct { - name string - setupPlan models.Plan - idToFetch uint - expectFound bool - expectedError error - }{ - { - name: "通过ID找到计划", - setupPlan: createTestPlan("计划 C", "描述 C", models.PlanExecutionTypeAutomatic, models.PlanContentTypeTasks), - idToFetch: 0, // 创建后设置 - expectFound: true, - }, - { - name: "通过ID未找到计划", - setupPlan: models.Plan{}, // 此情况下无需设置计划 - idToFetch: 999, - expectFound: false, - expectedError: gorm.ErrRecordNotFound, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := setupTestDB(t) - repo := repository.NewGormPlanRepository(db) - - if tt.setupPlan.Name != "" { // 仅在有效设置时创建计划 - err := db.Create(&tt.setupPlan).Error - assert.NoError(t, err, "插入设置计划失败") - tt.idToFetch = tt.setupPlan.ID // 使用数据库生成的ID - } - - fetchedPlan, err := repo.GetBasicPlanByID(tt.idToFetch) - - if tt.expectedError != nil { - assert.Error(t, err) - assert.True(t, errors.Is(err, tt.expectedError), "预期错误类型不匹配") - assert.Nil(t, fetchedPlan) - } else { - assert.NoError(t, err) - assert.NotNil(t, fetchedPlan) - assert.Equal(t, tt.setupPlan.Name, fetchedPlan.Name) - assert.Equal(t, tt.setupPlan.Description, fetchedPlan.Description) - assert.Equal(t, tt.setupPlan.ExecutionType, fetchedPlan.ExecutionType) - assert.Equal(t, tt.setupPlan.ContentType, fetchedPlan.ContentType) - assert.Empty(t, fetchedPlan.SubPlans, "GetBasicPlanByID 不应加载子计划") - assert.Empty(t, fetchedPlan.Tasks, "GetBasicPlanByID 不应加载任务") - } - }) - } -} - -// TestGetPlanByID 测试 GetPlanByID 方法,确保它能根据ID正确返回计划的完整信息,包括关联数据。 -func TestGetPlanByID(t *testing.T) { - type testCase struct { - name string - setupData func(db *gorm.DB) // 用于在测试前插入数据 - planID uint - expectedPlan *models.Plan - expectedError string - } - - testCases := []testCase{ - { - name: "PlanNotFound", - setupData: func(db *gorm.DB) { - // 不插入任何数据 - }, - planID: 999, - expectedPlan: nil, - expectedError: "record not found", - }, - { - name: "PlanWithTasks", - setupData: func(db *gorm.DB) { - // 使用硬编码的ID创建计划,使测试可预测 - plan := models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan A", - ContentType: models.PlanContentTypeTasks, - } - db.Create(&plan) - // 创建任务,它们的ID将由数据库自动生成 - db.Create(&models.Task{PlanID: 1, Name: "Task 2", ExecutionOrder: 1}) - db.Create(&models.Task{PlanID: 1, Name: "Task 1", ExecutionOrder: 2}) - }, - planID: 1, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan A", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - // 期望按 "order" 字段升序排序 - {PlanID: 1, Name: "Task 2", ExecutionOrder: 1}, - {PlanID: 1, Name: "Task 1", ExecutionOrder: 2}, - }, - }, - expectedError: "", - }, - { - name: "PlanWithMultiLevelSubPlans", - setupData: func(db *gorm.DB) { - // 创建一个三层结构的计划 - db.Create(&models.Plan{Model: gorm.Model{ID: 20}, Name: "Grandparent Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 21}, Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 22}, Name: "Child Plan With Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 22, Name: "Grandchild's Task", ExecutionOrder: 1}) - - // 创建关联关系 - db.Create(&models.SubPlan{ParentPlanID: 20, ChildPlanID: 21, ExecutionOrder: 1}) - db.Create(&models.SubPlan{ParentPlanID: 21, ChildPlanID: 22, ExecutionOrder: 1}) - }, - planID: 20, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 20}, - Name: "Grandparent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 20, - ChildPlanID: 21, - ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 21}, - Name: "Parent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 21, - ChildPlanID: 22, - ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 22}, - Name: "Child Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {PlanID: 22, Name: "Grandchild's Task", ExecutionOrder: 1}, - }, - }, - }, - }, - }, - }, - }, - }, - expectedError: "", - }, - { - name: "UnknownContentType", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{ - Model: gorm.Model{ID: 30}, - Name: "Unknown Type Plan", - ContentType: "INVALID_TYPE", - }) - }, - planID: 30, - expectedPlan: nil, - expectedError: fmt.Sprintf("未知的计划内容类型: INVALID_TYPE; 计划ID: 30"), - }, - // 新增场景:测试空的关联数据 - { - name: "PlanWithTasksType_ButNoTasks", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{ - Model: gorm.Model{ID: 50}, - Name: "Plan with empty tasks", - ContentType: models.PlanContentTypeTasks, - }) - }, - planID: 50, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 50}, - Name: "Plan with empty tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{}, // 期望一个空的切片 - }, - expectedError: "", - }, - // 新增场景:测试复杂的同级排序 - { - name: "PlanWithSubPlans_ComplexSorting", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 60}, Name: "Main Plan For Sorting", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 61}, Name: "SubPlan C", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Plan{Model: gorm.Model{ID: 62}, Name: "SubPlan A", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Plan{Model: gorm.Model{ID: 63}, Name: "SubPlan B", ContentType: models.PlanContentTypeTasks}) - // 故意打乱顺序插入 - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 61, ExecutionOrder: 3}) - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 62, ExecutionOrder: 1}) - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 63, ExecutionOrder: 2}) - }, - planID: 60, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 60}, - Name: "Main Plan For Sorting", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ParentPlanID: 60, ChildPlanID: 62, ExecutionOrder: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 62}, Name: "SubPlan A", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - {ParentPlanID: 60, ChildPlanID: 63, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 63}, Name: "SubPlan B", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - {ParentPlanID: 60, ChildPlanID: 61, ExecutionOrder: 3, ChildPlan: &models.Plan{Model: gorm.Model{ID: 61}, Name: "SubPlan C", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - }, - }, - expectedError: "", - }, - // 新增场景:测试混合内容的子计划树 - { - name: "PlanWithSubPlans_MixedContentTypes", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 70}, Name: "Mixed Main Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 71}, Name: "Child with SubPlans", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 72}, Name: "Grandchild with Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 72, Name: "Grandchild's Task", ExecutionOrder: 1}) - db.Create(&models.Plan{Model: gorm.Model{ID: 73}, Name: "Child with Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 73, Name: "Child's Task", ExecutionOrder: 1}) - - // 创建关联 - db.Create(&models.SubPlan{ParentPlanID: 70, ChildPlanID: 71, ExecutionOrder: 1}) // Main -> Child with SubPlans - db.Create(&models.SubPlan{ParentPlanID: 70, ChildPlanID: 73, ExecutionOrder: 2}) // Main -> Child with Tasks - db.Create(&models.SubPlan{ParentPlanID: 71, ChildPlanID: 72, ExecutionOrder: 1}) // Child with SubPlans -> Grandchild - }, - planID: 70, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 70}, - Name: "Mixed Main Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 70, ChildPlanID: 71, ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 71}, Name: "Child with SubPlans", ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ParentPlanID: 71, ChildPlanID: 72, ExecutionOrder: 1, ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 72}, Name: "Grandchild with Tasks", ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{{PlanID: 72, Name: "Grandchild's Task", ExecutionOrder: 1}}, - }}, - }, - }, - }, - { - ParentPlanID: 70, ChildPlanID: 73, ExecutionOrder: 2, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 73}, Name: "Child with Tasks", ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{{PlanID: 73, Name: "Child's Task", ExecutionOrder: 1}}, - }, - }, - }, - }, - expectedError: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - // 使用 defer 确保数据库连接在测试结束后关闭 - sqlDB, _ := db.DB() - defer sqlDB.Close() - - tc.setupData(db) - - repo := repository.NewGormPlanRepository(db) - plan, err := repo.GetPlanByID(tc.planID) - - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - assert.Nil(t, plan) - } else { - assert.NoError(t, err) - assert.NotNil(t, plan) - - // 在比较之前,清理实际结果和期望结果中所有不确定的、由数据库自动生成的字段 - cleanPlanForComparison(plan) - cleanPlanForComparison(tc.expectedPlan) - - assert.Equal(t, tc.expectedPlan, plan) - } - }) - } -} - -// cleanPlanForComparison 递归地重置 Plan 对象及其关联对象中由数据库自动生成的字段(如ID和时间戳), -// 以便在测试中断言它们与期望值相等。 -func cleanPlanForComparison(p *models.Plan) { - if p == nil { - return - } - - // 重置 Plan 自身的时间戳 - p.CreatedAt = time.Time{} - p.UpdatedAt = time.Time{} - p.DeletedAt = gorm.DeletedAt{} - - // 重置所有 Task 的自动生成字段 - for i := range p.Tasks { - p.Tasks[i].ID = 0 // ID是自动生成的,必须重置为0才能与期望值匹配 - p.Tasks[i].CreatedAt = time.Time{} - p.Tasks[i].UpdatedAt = time.Time{} - p.Tasks[i].DeletedAt = gorm.DeletedAt{} - } - - // 重置所有 SubPlan 的自动生成字段,并递归清理子计划 - for i := range p.SubPlans { - p.SubPlans[i].ID = 0 // SubPlan 连接记录的ID也是自动生成的 - p.SubPlans[i].CreatedAt = time.Time{} - p.SubPlans[i].UpdatedAt = time.Time{} - p.SubPlans[i].DeletedAt = gorm.DeletedAt{} - - // 递归调用以清理嵌套的子计划 - cleanPlanForComparison(p.SubPlans[i].ChildPlan) - } -} - -// TestUpdatePlan_Validation 专注于测试 UpdatePlan 中前置检查逻辑的各种失败和成功场景。 -func TestUpdatePlan_Validation(t *testing.T) { - // 定义Go测试中使用的计划实体 - planA := &models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan A", ContentType: models.PlanContentTypeSubPlans} - planB := &models.Plan{Model: gorm.Model{ID: 2}, Name: "Plan B", ContentType: models.PlanContentTypeSubPlans} - planC := &models.Plan{Model: gorm.Model{ID: 3}, Name: "Plan C", ContentType: models.PlanContentTypeSubPlans} - planD := &models.Plan{Model: gorm.Model{ID: 4}, Name: "Plan D", ContentType: models.PlanContentTypeTasks} - planNew := &models.Plan{Model: gorm.Model{ID: 0}, Name: "New Plan"} // ID为0的新计划 - - type testCase struct { - name string - setupDB func(db *gorm.DB) - buildInput func() *models.Plan // 修改为构建函数 - expectedError string // 保持 string 类型 - } - - testCases := []testCase{ - { - name: "成功-合法的菱形依赖树", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 3}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 4}}) - }, - buildInput: func() *models.Plan { - planD.ContentType = models.PlanContentTypeTasks - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}} - planC.ContentType = models.PlanContentTypeSubPlans - planC.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}} - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}, - {ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, - } - return planA - }, - expectedError: "", // 期望没有错误 - }, - { - name: "错误-根节点ID为零", - setupDB: func(db *gorm.DB) {}, - buildInput: func() *models.Plan { return planNew }, - expectedError: repository.ErrUpdateWithInvalidRoot.Error(), - }, - { - name: "错误-子计划ID为零", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlan: planNew}} - return planA - }, - expectedError: repository.ErrNewSubPlanInUpdate.Error(), - }, - { - name: "错误-节点在数据库中不存在", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}, - {ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, // C 不存在 - } - return planA - }, - expectedError: repository.ErrNodeDoesNotExist.Error(), - }, - { - name: "错误-简单循环引用(A->B->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-复杂循环引用(A->B->C->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 3}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 3, ChildPlan: planC}} - planC.ContentType = models.PlanContentTypeSubPlans - planC.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-自引用(A->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-根节点内容混合", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planA.Tasks = []models.Task{{Name: "A's Task"}} - return planA - }, - expectedError: "不能同时包含任务和子计划", - }, - { - name: "错误-任务执行顺序重复", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeTasks - planA.Tasks = []models.Task{ - {Name: "Task 1", ExecutionOrder: 1}, - {Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序 - } - return planA - }, - expectedError: fmt.Sprintf("任务执行顺序重复: %d", 1), - }, { - name: "错误-子计划执行顺序重复", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 10}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 11}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 10, ChildPlan: &models.Plan{Model: gorm.Model{ID: 10}}, ExecutionOrder: 1}, - {ChildPlanID: 11, ChildPlan: &models.Plan{Model: gorm.Model{ID: 11}}, ExecutionOrder: 1}, // 重复的顺序 - } - return planA - }, - expectedError: fmt.Sprintf("子计划执行顺序重复: %d", 1), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // 1. 为每个测试用例重置基础对象的状态 - *planA = models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan A"} - *planB = models.Plan{Model: gorm.Model{ID: 2}, Name: "Plan B"} - *planC = models.Plan{Model: gorm.Model{ID: 3}, Name: "Plan C"} - *planD = models.Plan{Model: gorm.Model{ID: 4}, Name: "Plan D"} - *planNew = models.Plan{Model: gorm.Model{ID: 0}, Name: "New Plan"} - - // 2. 设置数据库 - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - tc.setupDB(db) - - // 3. 在对象重置后,构建本次测试需要的输入结构 - input := tc.buildInput() - - // 4. 执行测试 - repo := repository.NewGormPlanRepository(db) - err := repo.UpdatePlan(input) - - // 5. 断言结果 - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - } else { - assert.NoError(t, err) - } - }) - } -} - -// TestUpdatePlan_Reconciliation 专注于测试 UpdatePlan 成功执行后,数据库状态是否与预期一致。 -func TestUpdatePlan_Reconciliation(t *testing.T) { - type testCase struct { - name string - setupDB func(db *gorm.DB) (rootPlanID uint) - buildInput func(db *gorm.DB) *models.Plan - verifyDB func(t *testing.T, db *gorm.DB, rootPlanID uint) - } - - testCases := []testCase{ - { - name: "任务协调-新增一个任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan With Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: 1, Name: "Task 1", ExecutionOrder: 1}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 1}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}, - {Name: "New Task 2", ExecutionOrder: 2}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks").First(&finalPlan, rootPlanID) - assert.Len(t, finalPlan.Tasks, 2) - assert.Equal(t, "New Task 2", finalPlan.Tasks[1].Name) - }, - }, - { - name: "任务协调-删除一个任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan With Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "Task to Delete", ExecutionOrder: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var tasks []models.Task - db.Where("plan_id = ?", rootPlanID).Find(&tasks) - assert.Len(t, tasks, 1) - var count int64 - db.Model(&models.Task{}).Where("id = ?", 11).Count(&count) - assert.Equal(t, int64(0), count, "被删除的任务不应再存在") - }, - }, - { - name: "任务协调-更新并重排序任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "A", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "B", ExecutionOrder: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 11}, PlanID: 1, Name: "B Updated", ExecutionOrder: 1}, - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "A", ExecutionOrder: 2}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks", func(db *gorm.DB) *gorm.DB { - return db.Order("execution_order") - }).First(&finalPlan, rootPlanID) - assert.Len(t, finalPlan.Tasks, 2) - assert.Equal(t, "B Updated", finalPlan.Tasks[0].Name) - assert.Equal(t, uint(11), finalPlan.Tasks[0].ID) - }, - }, - { - name: "任务协调-混沌的同步操作(增删改重排)", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1 (Original)", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "Task 2 (To Be Deleted)", ExecutionOrder: 2}) - db.Create(&models.Task{Model: gorm.Model{ID: 12}, PlanID: 1, Name: "Task 3 (Original)", ExecutionOrder: 3}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - // T4 (新) -> T3 (不变) -> T1 (更新) - {Name: "Task 4 (New)", ExecutionOrder: 1}, - {Model: gorm.Model{ID: 12}, PlanID: 1, Name: "Task 3 (Original)", ExecutionOrder: 2}, - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1 (Updated)", ExecutionOrder: 3}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks", func(db *gorm.DB) *gorm.DB { - return db.Order("execution_order") - }).First(&finalPlan, rootPlanID) - - // 验证最终数量 - assert.Len(t, finalPlan.Tasks, 3) - - // 验证被删除的 T2 不存在 - var count int64 - db.Model(&models.Task{}).Where("id = ?", 11).Count(&count) - assert.Equal(t, int64(0), count) - - // 验证顺序和内容 - assert.Equal(t, "Task 4 (New)", finalPlan.Tasks[0].Name) - assert.Equal(t, "Task 3 (Original)", finalPlan.Tasks[1].Name) - assert.Equal(t, "Task 1 (Updated)", finalPlan.Tasks[2].Name) - }, - }, - { - name: "子计划协调-新增一个关联", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Parent", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "Existing Child"}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Parent", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{{ChildPlanID: 2}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var links []models.SubPlan - db.Where("parent_plan_id = ?", rootPlanID).Find(&links) - assert.Len(t, links, 1) - assert.Equal(t, uint(2), links[0].ChildPlanID) - }, - }, - { - name: "子计划协调-删除一个关联", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Parent", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "Child To Unlink"}) - db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Parent", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(0), linkCount) - - var planCount int64 - db.Model(&models.Plan{}).Where("id = ?", 2).Count(&planCount) - assert.Equal(t, int64(1), planCount, "子计划本身不应被删除") - }, - }, - { - name: "类型转换-从任务切换到子计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: 1, Name: "Old Task"}) - db.Create(&models.Plan{Model: gorm.Model{ID: 10}, Name: "New Child"}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{{ChildPlanID: 10}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", rootPlanID).Count(&taskCount) - assert.Equal(t, int64(0), taskCount, "旧任务应被清理") - - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(1), linkCount, "新关联应被创建") - }, - }, - { - name: "类型转换-从子计划切换到任务", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 10}, Name: "Old Child"}) - db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 10}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, // 类型变更 - Tasks: []models.Task{{Name: "New Task"}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(0), linkCount, "旧的子计划关联应被清理") - - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", rootPlanID).Count(&taskCount) - assert.Equal(t, int64(1), taskCount, "新任务应被创建") - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - - rootID := tc.setupDB(db) - input := tc.buildInput(db) - - repo := repository.NewGormPlanRepository(db) - err := repo.UpdatePlan(input) - assert.NoError(t, err) - - tc.verifyDB(t, db, rootID) - }) - } -} - -// createExistingPlan 辅助函数,用于在数据库中创建已存在的计划 -func createExistingPlan(db *gorm.DB, name string, contentType models.PlanContentType) *models.Plan { - plan := &models.Plan{ - Name: name, - ContentType: contentType, - } - db.Create(plan) - return plan -} - -func TestPlanRepository_Create(t *testing.T) { - type testCase struct { - name string - setupDB func(db *gorm.DB) // 准备数据库的初始状态 - inputPlan *models.Plan // 传入 Create 方法的计划对象 - expectedError error // 期望的错误类型 - verifyDB func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) // 验证数据库状态 - } - - testCases := []testCase{ - { - name: "成功创建-只包含基本信息", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Name: "简单计划", - Description: "一个不包含任务或子计划的简单计划", - ContentType: models.PlanContentTypeTasks, // 修改为有效的 ContentType - Tasks: []models.Task{}, // 明确为空任务列表 - }, - expectedError: nil, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - assert.NotZero(t, createdPlan.ID, "创建后计划ID不应为0") - var foundPlan models.Plan - err := db.First(&foundPlan, createdPlan.ID).Error - assert.NoError(t, err) - assert.Equal(t, "简单计划", foundPlan.Name) - assert.Equal(t, models.PlanContentTypeTasks, foundPlan.ContentType) - var tasks []models.Task - db.Where("plan_id = ?", createdPlan.ID).Find(&tasks) - assert.Len(t, tasks, 0, "不应创建任何任务") - }, - }, - { - name: "成功创建-包含任务", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Name: "任务计划", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Name: "任务A", ExecutionOrder: 1}, - {Name: "任务B", ExecutionOrder: 2}, - }, - }, - expectedError: nil, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - assert.NotZero(t, createdPlan.ID, "计划ID不应为0") - var foundPlan models.Plan - db.Preload("Tasks").First(&foundPlan, createdPlan.ID) - assert.Len(t, foundPlan.Tasks, 2, "应创建两个任务") - assert.NotZero(t, foundPlan.Tasks[0].ID, "任务ID不应为0") - assert.Equal(t, "任务A", foundPlan.Tasks[0].Name) - }, - }, - { - name: "成功创建-包含子计划关联", - setupDB: func(db *gorm.DB) { - // 预先创建子计划实体,使用有效的 ContentType - createExistingPlan(db, "子计划1", models.PlanContentTypeTasks) - createExistingPlan(db, "子计划2", models.PlanContentTypeTasks) - }, - inputPlan: &models.Plan{ - Name: "父计划", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ChildPlanID: 1, ExecutionOrder: 1}, // 关联已存在的子计划1 - {ChildPlanID: 2, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 2}}}, // 关联已存在的子计划2 - }, - }, - expectedError: nil, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - assert.NotZero(t, createdPlan.ID, "创建后计划ID不应为0") - - // 直接查询 SubPlan 关联记录 - var foundSubPlanLinks []models.SubPlan - err := db.Where("parent_plan_id = ?", createdPlan.ID).Find(&foundSubPlanLinks).Error - assert.NoError(t, err) - - assert.Len(t, foundSubPlanLinks, 2, "应创建两个子计划关联") - assert.NotZero(t, foundSubPlanLinks[0].ID, "子计划关联ID不应为0") - assert.Equal(t, createdPlan.ID, foundSubPlanLinks[0].ParentPlanID) - assert.Equal(t, uint(1), foundSubPlanLinks[0].ChildPlanID) - }, - }, - { - name: "失败-计划ID不为0", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Model: gorm.Model{ID: 100}, // ID不为0 - Name: "无效计划", - }, - expectedError: repository.ErrCreateWithNonZeroID, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - // 验证数据库中没有创建该计划 - var count int64 - db.Model(&models.Plan{}).Where("id = ?", 100).Count(&count) - assert.Equal(t, int64(0), count, "计划不应被创建") - }, - }, - { - name: "失败-同时包含任务和子计划", - setupDB: func(db *gorm.DB) { - createExistingPlan(db, "子计划", models.PlanContentTypeTasks) // 使用有效的 ContentType - }, - inputPlan: &models.Plan{ - Name: "混合内容计划", - ContentType: models.PlanContentTypeTasks, // 声明为任务类型 - Tasks: []models.Task{{Name: "任务A"}}, - SubPlans: []models.SubPlan{{ChildPlanID: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 1}}}}, // 但也包含子计划 - }, - expectedError: repository.ErrMixedContent, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - // 验证数据库中没有创建该计划 - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "混合内容计划").Count(&count) - assert.Equal(t, int64(0), count, "计划不应被创建") - }, - }, - { - name: "失败-子计划ID为0", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Name: "无效子计划关联", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ChildPlanID: 0, ChildPlan: &models.Plan{Model: gorm.Model{ID: 0}}}, // 子计划ID为0 - }, - }, - expectedError: repository.ErrSubPlanIDIsZeroOnCreate, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "无效子计划关联").Count(&count) - assert.Equal(t, int64(0), count, "计划不应被创建") - }, - }, - { - name: "失败-子计划在数据库中不存在", - setupDB: func(db *gorm.DB) { - // 不创建ID为999的计划 - }, - inputPlan: &models.Plan{ - Name: "不存在的子计划", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ChildPlanID: 999, ChildPlan: &models.Plan{Model: gorm.Model{ID: 999}}}, // 关联一个不存在的ID - }, - }, - expectedError: repository.ErrNodeDoesNotExist, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "不存在的子计划").Count(&count) - assert.Equal(t, int64(0), count, "计划不应被创建") - }, - }, - { - name: "失败-任务执行顺序重复", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Name: "重复任务顺序计划", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Name: "Task 1", ExecutionOrder: 1}, - {Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序 - }, - }, - expectedError: fmt.Errorf("任务执行顺序重复: %d", 1), // 假设 Create 方法会返回此错误 - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "重复任务顺序计划").Count(&count) - assert.Equal(t, int64(0), count, "重复任务顺序的计划不应被创建") - }, - }, - { - name: "失败-子计划执行顺序重复", - setupDB: func(db *gorm.DB) { - createExistingPlan(db, "子计划A", models.PlanContentTypeTasks) - createExistingPlan(db, "子计划B", models.PlanContentTypeTasks) - }, - inputPlan: &models.Plan{ - Name: "重复子计划顺序计划", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ChildPlanID: 1, ExecutionOrder: 1}, - {ChildPlanID: 2, ExecutionOrder: 1}, // 重复的顺序 - }, - }, - expectedError: fmt.Errorf("子计划执行顺序重复: %d", 1), // 假设 Create 方法会返回此错误 - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "重复子计划顺序计划").Count(&count) - assert.Equal(t, int64(0), count, "重复子计划顺序的计划不应被创建") - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - repo := repository.NewGormPlanRepository(db) - - // 准备数据库状态 - tc.setupDB(db) - - // 执行 Create 操作 - err := repo.CreatePlan(tc.inputPlan) - - // 断言错误 - if tc.expectedError != nil { - assert.Error(t, err) - // 使用 Contains 检查错误信息,因为 fmt.Errorf 会创建新的错误实例 - assert.Contains(t, err.Error(), tc.expectedError.Error()) - } else { - assert.NoError(t, err) - } - - // 验证数据库状态 - tc.verifyDB(t, db, tc.inputPlan) - }) - } -} - -func TestPlanRepository_DeletePlan(t *testing.T) { - type testCase struct { - name string - setupDB func(db *gorm.DB) (planToDeleteID uint) - expectedError string - verifyDB func(t *testing.T, db *gorm.DB, planToDeleteID uint) - } - - testCases := []testCase{ - { - name: "成功删除-包含任务的计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Name: "Plan with Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: plan.ID, Name: "Task 1"}) - db.Create(&models.Task{PlanID: plan.ID, Name: "Task 2"}) - return plan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var plan models.Plan - err := db.First(&plan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "计划应被删除") - - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", planToDeleteID).Count(&taskCount) - assert.Equal(t, int64(0), taskCount, "关联任务应被删除") - }, - }, - { - name: "成功删除-包含子计划链接的计划", - setupDB: func(db *gorm.DB) uint { - parentPlan := models.Plan{Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans} - childPlan := models.Plan{Name: "Child Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&parentPlan) - db.Create(&childPlan) - db.Create(&models.SubPlan{ParentPlanID: parentPlan.ID, ChildPlanID: childPlan.ID}) - return parentPlan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var parentPlan models.Plan - err := db.First(&parentPlan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "父计划应被删除") - - var subPlanLinkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", planToDeleteID).Count(&subPlanLinkCount) - assert.Equal(t, int64(0), subPlanLinkCount, "子计划链接应被删除") - - // 验证子计划本身未被删除 - var childPlan models.Plan - err = db.First(&childPlan, 2).Error // Assuming childPlan.ID is 2 from setup - assert.NoError(t, err, "子计划本身不应被删除") - }, - }, - { - name: "失败删除-作为子计划的计划", - setupDB: func(db *gorm.DB) uint { - parentPlan := models.Plan{Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans} - childPlan := models.Plan{Name: "Child Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&parentPlan) - db.Create(&childPlan) - db.Create(&models.SubPlan{ParentPlanID: parentPlan.ID, ChildPlanID: childPlan.ID}) - return childPlan.ID // 尝试删除子计划 - }, - expectedError: repository.ErrDeleteWithReferencedPlan.Error(), - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var childPlan models.Plan - err := db.First(&childPlan, planToDeleteID).Error - assert.NoError(t, err, "子计划不应被删除") - - var subPlanLinkCount int64 - db.Model(&models.SubPlan{}).Where("child_plan_id = ?", planToDeleteID).Count(&subPlanLinkCount) - assert.Equal(t, int64(1), subPlanLinkCount, "子计划链接不应被删除") - }, - }, - { - name: "失败删除-不存在的计划", - setupDB: func(db *gorm.DB) uint { - return 999 // 不存在的ID - }, - expectedError: "record not found", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - // 数据库状态应保持不变 - }, - }, - { - name: "成功删除-不含任何关联的计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Name: "Simple Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - return plan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var plan models.Plan - err := db.First(&plan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "计划应被删除") - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - - planToDeleteID := tc.setupDB(db) - - repo := repository.NewGormPlanRepository(db) - err := repo.DeletePlan(planToDeleteID) - - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - } else { - assert.NoError(t, err) - } - - tc.verifyDB(t, db, planToDeleteID) - }) - } -} diff --git a/internal/infra/repository/unit_of_work.go b/internal/infra/repository/unit_of_work.go new file mode 100644 index 0000000..7929476 --- /dev/null +++ b/internal/infra/repository/unit_of_work.go @@ -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 +}