From d94a18779e38974122351bc7618644dd8023c38c Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 19 Sep 2025 12:53:58 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E4=BF=AE=E6=94=B9model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/plan.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/infra/models/plan.go b/internal/infra/models/plan.go index fe3a186..84aaa79 100644 --- a/internal/infra/models/plan.go +++ b/internal/infra/models/plan.go @@ -38,6 +38,14 @@ const ( ParamsPlanID = "plan_id" ) +type PlanStatus uint8 + +const ( + PlanStatusEnabled PlanStatus = 0 // 启用计划 + PlanStatusDisabled PlanStatus = 1 // 禁用计划 + PlanStatusStopeed PlanStatus = 2 // 执行完毕 +) + // Plan 代表系统中的一个计划,可以包含子计划或任务 type Plan struct { gorm.Model @@ -45,6 +53,9 @@ type Plan struct { Name string `gorm:"not null" json:"name"` Description string `json:"description"` ExecutionType PlanExecutionType `gorm:"not null" json:"execution_type"` + Status PlanStatus `gorm:"default:0" 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"` -- 2.49.1 From 11502cb5f079ccb915f81bf9d7bbe041605d9e12 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 19 Sep 2025 13:18:05 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E4=BF=AE=E6=94=B9api=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 100 +++++++++++++----- docs/swagger.json | 100 +++++++++++++----- docs/swagger.yaml | 71 +++++++++---- internal/app/controller/plan/converter.go | 3 + .../app/controller/plan/plan_controller.go | 3 + 5 files changed, 202 insertions(+), 75 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 9d484ed..24d79f2 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -459,7 +459,34 @@ const docTemplate = `{ "device.CreateDeviceRequest": { "type": "object" }, - "device.DeviceResponse": { + "device.UpdateDeviceRequest": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "properties": { + "$ref": "#/definitions/controller.Properties" + }, + "sub_type": { + "$ref": "#/definitions/models.DeviceSubType" + }, + "type": { + "$ref": "#/definitions/models.DeviceType" + } + } + }, + "git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": { "type": "object", "properties": { "created_at": { @@ -491,33 +518,6 @@ const docTemplate = `{ } } }, - "device.UpdateDeviceRequest": { - "type": "object", - "required": [ - "name", - "type" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parent_id": { - "type": "integer" - }, - "properties": { - "$ref": "#/definitions/controller.Properties" - }, - "sub_type": { - "$ref": "#/definitions/models.DeviceSubType" - }, - "type": { - "$ref": "#/definitions/models.DeviceType" - } - } - }, "models.DeviceSubType": { "type": "string", "enum": [ @@ -588,18 +588,46 @@ const docTemplate = `{ "PlanExecutionTypeManual" ] }, + "models.PlanStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-comments": { + "PlanStatusDisabled": "禁用计划", + "PlanStatusEnabled": "启用计划", + "PlanStatusStopeed": "执行完毕" + }, + "x-enum-descriptions": [ + "启用计划", + "禁用计划", + "执行完毕" + ], + "x-enum-varnames": [ + "PlanStatusEnabled", + "PlanStatusDisabled", + "PlanStatusStopeed" + ] + }, "models.TaskType": { "type": "string", "enum": [ + "plan_analysis", "waiting" ], "x-enum-comments": { + "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", "TaskTypeWaiting": "等待任务" }, "x-enum-descriptions": [ + "解析Plan的Task列表并添加到待执行队列的特殊任务", "等待任务" ], "x-enum-varnames": [ + "TaskPlanAnalysis", "TaskTypeWaiting" ] }, @@ -687,6 +715,14 @@ const docTemplate = `{ "type": "string", "example": "根据温度自动调节风扇和加热器" }, + "execute_count": { + "type": "integer", + "example": 0 + }, + "execute_num": { + "type": "integer", + "example": 10 + }, "execution_type": { "allOf": [ { @@ -703,6 +739,14 @@ const docTemplate = `{ "type": "string", "example": "猪舍温度控制计划" }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanStatus" + } + ], + "example": 0 + }, "sub_plans": { "type": "array", "items": { diff --git a/docs/swagger.json b/docs/swagger.json index a8eb9a8..dc7bc1f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -448,7 +448,34 @@ "device.CreateDeviceRequest": { "type": "object" }, - "device.DeviceResponse": { + "device.UpdateDeviceRequest": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "properties": { + "$ref": "#/definitions/controller.Properties" + }, + "sub_type": { + "$ref": "#/definitions/models.DeviceSubType" + }, + "type": { + "$ref": "#/definitions/models.DeviceType" + } + } + }, + "git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": { "type": "object", "properties": { "created_at": { @@ -480,33 +507,6 @@ } } }, - "device.UpdateDeviceRequest": { - "type": "object", - "required": [ - "name", - "type" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parent_id": { - "type": "integer" - }, - "properties": { - "$ref": "#/definitions/controller.Properties" - }, - "sub_type": { - "$ref": "#/definitions/models.DeviceSubType" - }, - "type": { - "$ref": "#/definitions/models.DeviceType" - } - } - }, "models.DeviceSubType": { "type": "string", "enum": [ @@ -577,18 +577,46 @@ "PlanExecutionTypeManual" ] }, + "models.PlanStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-comments": { + "PlanStatusDisabled": "禁用计划", + "PlanStatusEnabled": "启用计划", + "PlanStatusStopeed": "执行完毕" + }, + "x-enum-descriptions": [ + "启用计划", + "禁用计划", + "执行完毕" + ], + "x-enum-varnames": [ + "PlanStatusEnabled", + "PlanStatusDisabled", + "PlanStatusStopeed" + ] + }, "models.TaskType": { "type": "string", "enum": [ + "plan_analysis", "waiting" ], "x-enum-comments": { + "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", "TaskTypeWaiting": "等待任务" }, "x-enum-descriptions": [ + "解析Plan的Task列表并添加到待执行队列的特殊任务", "等待任务" ], "x-enum-varnames": [ + "TaskPlanAnalysis", "TaskTypeWaiting" ] }, @@ -676,6 +704,14 @@ "type": "string", "example": "根据温度自动调节风扇和加热器" }, + "execute_count": { + "type": "integer", + "example": 0 + }, + "execute_num": { + "type": "integer", + "example": 10 + }, "execution_type": { "allOf": [ { @@ -692,6 +728,14 @@ "type": "string", "example": "猪舍温度控制计划" }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanStatus" + } + ], + "example": 0 + }, "sub_plans": { "type": "array", "items": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3f48976..3a53d16 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -14,7 +14,25 @@ definitions: type: object device.CreateDeviceRequest: type: object - device.DeviceResponse: + device.UpdateDeviceRequest: + properties: + location: + type: string + name: + type: string + parent_id: + type: integer + properties: + $ref: '#/definitions/controller.Properties' + sub_type: + $ref: '#/definitions/models.DeviceSubType' + type: + $ref: '#/definitions/models.DeviceType' + required: + - name + - type + type: object + git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse: properties: created_at: type: string @@ -35,24 +53,6 @@ definitions: updated_at: type: string type: object - device.UpdateDeviceRequest: - properties: - location: - type: string - name: - type: string - parent_id: - type: integer - properties: - $ref: '#/definitions/controller.Properties' - sub_type: - $ref: '#/definitions/models.DeviceSubType' - type: - $ref: '#/definitions/models.DeviceType' - required: - - name - - type - type: object models.DeviceSubType: enum: - "" @@ -107,15 +107,38 @@ definitions: x-enum-varnames: - PlanExecutionTypeAutomatic - PlanExecutionTypeManual + models.PlanStatus: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-comments: + PlanStatusDisabled: 禁用计划 + PlanStatusEnabled: 启用计划 + PlanStatusStopeed: 执行完毕 + x-enum-descriptions: + - 启用计划 + - 禁用计划 + - 执行完毕 + x-enum-varnames: + - PlanStatusEnabled + - PlanStatusDisabled + - PlanStatusStopeed models.TaskType: enum: + - plan_analysis - waiting type: string x-enum-comments: + TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务 TaskTypeWaiting: 等待任务 x-enum-descriptions: + - 解析Plan的Task列表并添加到待执行队列的特殊任务 - 等待任务 x-enum-varnames: + - TaskPlanAnalysis - TaskTypeWaiting plan.CreatePlanRequest: properties: @@ -171,6 +194,12 @@ definitions: description: example: 根据温度自动调节风扇和加热器 type: string + execute_count: + example: 0 + type: integer + execute_num: + example: 10 + type: integer execution_type: allOf: - $ref: '#/definitions/models.PlanExecutionType' @@ -181,6 +210,10 @@ definitions: name: example: 猪舍温度控制计划 type: string + status: + allOf: + - $ref: '#/definitions/models.PlanStatus' + example: 0 sub_plans: items: $ref: '#/definitions/plan.SubPlanResponse' diff --git a/internal/app/controller/plan/converter.go b/internal/app/controller/plan/converter.go index 80ad497..4af0d70 100644 --- a/internal/app/controller/plan/converter.go +++ b/internal/app/controller/plan/converter.go @@ -17,6 +17,9 @@ func PlanToResponse(plan *models.Plan) *PlanResponse { Name: plan.Name, Description: plan.Description, ExecutionType: plan.ExecutionType, + Status: plan.Status, + ExecuteNum: plan.ExecuteNum, + ExecuteCount: plan.ExecuteCount, CronExpression: plan.CronExpression, ContentType: plan.ContentType, } diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index d7c9576..42d15f5 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -32,6 +32,9 @@ type PlanResponse struct { 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"` -- 2.49.1 From 3af1b4949faccf2aadf2f6fb6017706e21dd2120 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 19 Sep 2025 13:35:41 +0800 Subject: [PATCH 03/13] =?UTF-8?q?=E5=85=81=E8=AE=B8=E5=88=9B=E5=BB=BA/?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=97=B6=E6=8C=87=E5=AE=9A=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=AC=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/controller/plan/converter.go | 2 ++ internal/app/controller/plan/plan_controller.go | 2 ++ internal/infra/repository/plan_repository.go | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/app/controller/plan/converter.go b/internal/app/controller/plan/converter.go index 4af0d70..8410c14 100644 --- a/internal/app/controller/plan/converter.go +++ b/internal/app/controller/plan/converter.go @@ -53,6 +53,7 @@ func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { Name: req.Name, Description: req.Description, ExecutionType: req.ExecutionType, + ExecuteNum: req.ExecuteNum, CronExpression: req.CronExpression, ContentType: req.ContentType, } @@ -99,6 +100,7 @@ func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { Name: req.Name, Description: req.Description, ExecutionType: req.ExecutionType, + ExecuteNum: req.ExecuteNum, CronExpression: req.CronExpression, ContentType: req.ContentType, } diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 42d15f5..604d6cd 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -20,6 +20,7 @@ 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 * * *"` ContentType models.PlanContentType `json:"content_type" binding:"required" example:"tasks"` SubPlanIDs []uint `json:"sub_plan_ids,omitempty"` @@ -52,6 +53,7 @@ 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 * * *"` ContentType models.PlanContentType `json:"content_type" example:"tasks"` SubPlanIDs []uint `json:"sub_plan_ids,omitempty"` diff --git a/internal/infra/repository/plan_repository.go b/internal/infra/repository/plan_repository.go index f9d7a03..e32236d 100644 --- a/internal/infra/repository/plan_repository.go +++ b/internal/infra/repository/plan_repository.go @@ -275,7 +275,7 @@ func (r *gormPlanRepository) reconcilePlanNode(tx *gorm.DB, plan *models.Plan) e return nil } // 1. 更新节点本身的基础字段 - if err := tx.Model(plan).Select("Name", "Description", "ExecutionType", "CronExpression", "ContentType").Updates(plan).Error; err != nil { + if err := tx.Model(plan).Select("Name", "Description", "ExecutionType", "ExecuteNum", "CronExpression", "ContentType").Updates(plan).Error; err != nil { return err } -- 2.49.1 From 88e0fbfb647481a73b67690b721a564835961132 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 19 Sep 2025 15:55:56 +0800 Subject: [PATCH 04/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dswagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 24 ++++-- docs/swagger.json | 24 ++++-- docs/swagger.yaml | 22 +++-- go.mod | 65 +++++++++----- go.sum | 85 +++++++++++++++++++ internal/app/api/api.go | 2 - .../controller/device/device_controller.go | 10 +-- .../app/controller/plan/plan_controller.go | 14 +-- .../app/controller/user/user_controller.go | 4 +- 9 files changed, 187 insertions(+), 63 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 24d79f2..a2f2894 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,7 +15,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/devices": { + "/api/v1/devices": { "get": { "description": "获取系统中所有设备的列表", "produces": [ @@ -67,7 +67,7 @@ const docTemplate = `{ } } }, - "/devices/{id}": { + "/api/v1/devices/{id}": { "get": { "description": "根据设备ID获取单个设备的详细信息", "produces": [ @@ -162,7 +162,7 @@ const docTemplate = `{ } } }, - "/plans": { + "/api/v1/plans": { "get": { "description": "获取所有计划的列表", "produces": [ @@ -214,7 +214,7 @@ const docTemplate = `{ } } }, - "/plans/{id}": { + "/api/v1/plans/{id}": { "get": { "description": "根据计划ID获取单个计划的详细信息。", "produces": [ @@ -309,7 +309,7 @@ const docTemplate = `{ } } }, - "/plans/{id}/start": { + "/api/v1/plans/{id}/start": { "post": { "description": "根据计划ID启动一个计划的执行。", "produces": [ @@ -338,7 +338,7 @@ const docTemplate = `{ } } }, - "/plans/{id}/stop": { + "/api/v1/plans/{id}/stop": { "post": { "description": "根据计划ID停止一个正在执行的计划。", "produces": [ @@ -367,7 +367,7 @@ const docTemplate = `{ } } }, - "/users": { + "/api/v1/users": { "post": { "description": "根据用户名和密码创建一个新的系统用户。", "consumes": [ @@ -401,7 +401,7 @@ const docTemplate = `{ } } }, - "/users/login": { + "/api/v1/users/login": { "post": { "description": "用户使用用户名和密码登录,成功后返回 JWT 令牌。", "consumes": [ @@ -655,6 +655,10 @@ const docTemplate = `{ "type": "string", "example": "根据温度自动调节风扇和加热器" }, + "execute_num": { + "type": "integer", + "example": 10 + }, "execution_type": { "allOf": [ { @@ -868,6 +872,10 @@ const docTemplate = `{ "type": "string", "example": "更新后的描述" }, + "execute_num": { + "type": "integer", + "example": 10 + }, "execution_type": { "allOf": [ { diff --git a/docs/swagger.json b/docs/swagger.json index dc7bc1f..fc3ad34 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4,7 +4,7 @@ "contact": {} }, "paths": { - "/devices": { + "/api/v1/devices": { "get": { "description": "获取系统中所有设备的列表", "produces": [ @@ -56,7 +56,7 @@ } } }, - "/devices/{id}": { + "/api/v1/devices/{id}": { "get": { "description": "根据设备ID获取单个设备的详细信息", "produces": [ @@ -151,7 +151,7 @@ } } }, - "/plans": { + "/api/v1/plans": { "get": { "description": "获取所有计划的列表", "produces": [ @@ -203,7 +203,7 @@ } } }, - "/plans/{id}": { + "/api/v1/plans/{id}": { "get": { "description": "根据计划ID获取单个计划的详细信息。", "produces": [ @@ -298,7 +298,7 @@ } } }, - "/plans/{id}/start": { + "/api/v1/plans/{id}/start": { "post": { "description": "根据计划ID启动一个计划的执行。", "produces": [ @@ -327,7 +327,7 @@ } } }, - "/plans/{id}/stop": { + "/api/v1/plans/{id}/stop": { "post": { "description": "根据计划ID停止一个正在执行的计划。", "produces": [ @@ -356,7 +356,7 @@ } } }, - "/users": { + "/api/v1/users": { "post": { "description": "根据用户名和密码创建一个新的系统用户。", "consumes": [ @@ -390,7 +390,7 @@ } } }, - "/users/login": { + "/api/v1/users/login": { "post": { "description": "用户使用用户名和密码登录,成功后返回 JWT 令牌。", "consumes": [ @@ -644,6 +644,10 @@ "type": "string", "example": "根据温度自动调节风扇和加热器" }, + "execute_num": { + "type": "integer", + "example": 10 + }, "execution_type": { "allOf": [ { @@ -857,6 +861,10 @@ "type": "string", "example": "更新后的描述" }, + "execute_num": { + "type": "integer", + "example": 10 + }, "execution_type": { "allOf": [ { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3a53d16..c4249e2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -152,6 +152,9 @@ definitions: description: example: 根据温度自动调节风扇和加热器 type: string + execute_num: + example: 10 + type: integer execution_type: allOf: - $ref: '#/definitions/models.PlanExecutionType' @@ -294,6 +297,9 @@ definitions: description: example: 更新后的描述 type: string + execute_num: + example: 10 + type: integer execution_type: allOf: - $ref: '#/definitions/models.PlanExecutionType' @@ -359,7 +365,7 @@ definitions: info: contact: {} paths: - /devices: + /api/v1/devices: get: description: 获取系统中所有设备的列表 produces: @@ -393,7 +399,7 @@ paths: summary: 创建新设备 tags: - 设备管理 - /devices/{id}: + /api/v1/devices/{id}: delete: description: 根据设备ID删除一个设备(软删除) parameters: @@ -456,7 +462,7 @@ paths: summary: 更新设备信息 tags: - 设备管理 - /plans: + /api/v1/plans: get: description: 获取所有计划的列表 produces: @@ -490,7 +496,7 @@ paths: summary: 创建计划 tags: - 计划管理 - /plans/{id}: + /api/v1/plans/{id}: delete: description: 根据计划ID删除计划。 parameters: @@ -553,7 +559,7 @@ paths: summary: 更新计划 tags: - 计划管理 - /plans/{id}/start: + /api/v1/plans/{id}/start: post: description: 根据计划ID启动一个计划的执行。 parameters: @@ -572,7 +578,7 @@ paths: summary: 启动计划 tags: - 计划管理 - /plans/{id}/stop: + /api/v1/plans/{id}/stop: post: description: 根据计划ID停止一个正在执行的计划。 parameters: @@ -591,7 +597,7 @@ paths: summary: 停止计划 tags: - 计划管理 - /users: + /api/v1/users: post: consumes: - application/json @@ -613,7 +619,7 @@ paths: summary: 创建新用户 tags: - 用户管理 - /users/login: + /api/v1/users/login: post: consumes: - application/json diff --git a/go.mod b/go.mod index aa3209c..5370975 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/go-openapi/errors v0.22.2 github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/strfmt v0.23.0 - github.com/go-openapi/swag v0.23.0 + github.com/go-openapi/swag v0.24.1 github.com/go-openapi/validate v0.24.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/panjf2000/ants/v2 v2.11.3 @@ -17,8 +17,8 @@ require ( github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.36.0 - google.golang.org/protobuf v1.34.1 + golang.org/x/crypto v0.42.0 + google.golang.org/protobuf v1.36.9 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 gorm.io/datatypes v1.2.6 @@ -31,25 +31,38 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.1 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -59,9 +72,9 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -69,24 +82,30 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/urfave/cli/v2 v2.27.7 // indirect + github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.26.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/arch v0.21.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.6 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index b0bef21..82c0757 100644 --- a/go.sum +++ b/go.sum @@ -4,23 +4,37 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= +github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -34,8 +48,12 @@ github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrY github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= @@ -46,6 +64,30 @@ github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMg github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -56,11 +98,15 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= @@ -69,6 +115,7 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -91,6 +138,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -100,6 +149,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -121,12 +172,18 @@ github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZ github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -151,6 +208,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= @@ -168,26 +231,38 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= +golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -197,6 +272,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -206,14 +283,20 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -239,3 +322,5 @@ gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s= gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 571a729..56f1dc8 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -8,8 +8,6 @@ package api // @contact.email divano@example.com // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @host localhost:8086 -// @BasePath /api/v1 import ( "context" "fmt" diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index d387286..397b2ae 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -106,7 +106,7 @@ func newListDeviceResponse(devices []*models.Device) []*DeviceResponse { // @Param device body CreateDeviceRequest true "设备信息" // @Success 200 {object} controller.Response{data=DeviceResponse} "业务码为201代表创建成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" -// @Router /devices [post] +// @Router /api/v1/devices [post] func (c *Controller) CreateDevice(ctx *gin.Context) { var req CreateDeviceRequest if err := ctx.ShouldBindJSON(&req); err != nil { @@ -141,7 +141,7 @@ func (c *Controller) CreateDevice(ctx *gin.Context) { // @Param id path string true "设备ID" // @Success 200 {object} controller.Response{data=DeviceResponse} "业务码为200代表获取成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" -// @Router /devices/{id} [get] +// @Router /api/v1/devices/{id} [get] func (c *Controller) GetDevice(ctx *gin.Context) { deviceID := ctx.Param("id") @@ -170,7 +170,7 @@ func (c *Controller) GetDevice(ctx *gin.Context) { // @Produce json // @Success 200 {object} controller.Response{data=[]DeviceResponse} "业务码为200代表获取成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" -// @Router /devices [get] +// @Router /api/v1/devices [get] func (c *Controller) ListDevices(ctx *gin.Context) { devices, err := c.repo.ListAll() if err != nil { @@ -192,7 +192,7 @@ func (c *Controller) ListDevices(ctx *gin.Context) { // @Param device body UpdateDeviceRequest true "要更新的设备信息" // @Success 200 {object} controller.Response{data=DeviceResponse} "业务码为200代表更新成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" -// @Router /devices/{id} [put] +// @Router /api/v1/devices/{id} [put] func (c *Controller) UpdateDevice(ctx *gin.Context) { deviceID := ctx.Param("id") @@ -246,7 +246,7 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) { // @Param id path string true "设备ID" // @Success 200 {object} controller.Response "业务码为200代表删除成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" -// @Router /devices/{id} [delete] +// @Router /api/v1/devices/{id} [delete] func (c *Controller) DeleteDevice(ctx *gin.Context) { deviceID := ctx.Param("id") diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 604d6cd..d33fe40 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -118,7 +118,7 @@ func NewController(logger *logs.Logger, planRepo repository.PlanRepository, anal // @Param plan body CreatePlanRequest true "计划信息" // @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为201代表创建成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 500)" -// @Router /plans [post] +// @Router /api/v1/plans [post] func (c *Controller) CreatePlan(ctx *gin.Context) { var req CreatePlanRequest if err := ctx.ShouldBindJSON(&req); err != nil { @@ -160,7 +160,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) { // @Param id path int true "计划ID" // @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表成功获取" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" -// @Router /plans/{id} [get] +// @Router /api/v1/plans/{id} [get] func (c *Controller) GetPlan(ctx *gin.Context) { // 1. 从 URL 路径中获取 ID idStr := ctx.Param("id") @@ -198,7 +198,7 @@ func (c *Controller) GetPlan(ctx *gin.Context) { // @Produce json // @Success 200 {object} controller.Response{data=plan.ListPlansResponse} "业务码为200代表成功获取列表" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 500)" -// @Router /plans [get] +// @Router /api/v1/plans [get] func (c *Controller) ListPlans(ctx *gin.Context) { // 1. 调用仓库层获取所有计划 plans, err := c.planRepo.ListBasicPlans() @@ -232,7 +232,7 @@ func (c *Controller) ListPlans(ctx *gin.Context) { // @Param plan body UpdatePlanRequest true "更新后的计划信息" // @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表更新成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" -// @Router /plans/{id} [put] +// @Router /api/v1/plans/{id} [put] func (c *Controller) UpdatePlan(ctx *gin.Context) { // 1. 从 URL 路径中获取 ID idStr := ctx.Param("id") @@ -304,7 +304,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { // @Param id path int true "计划ID" // @Success 200 {object} controller.Response "业务码为200代表删除成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" -// @Router /plans/{id} [delete] +// @Router /api/v1/plans/{id} [delete] func (c *Controller) DeletePlan(ctx *gin.Context) { // 1. 从 URL 路径中获取 ID idStr := ctx.Param("id") @@ -333,7 +333,7 @@ func (c *Controller) DeletePlan(ctx *gin.Context) { // @Param id path int true "计划ID" // @Success 200 {object} controller.Response "业务码为200代表成功启动计划" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" -// @Router /plans/{id}/start [post] +// @Router /api/v1/plans/{id}/start [post] func (c *Controller) StartPlan(ctx *gin.Context) { // 占位符:此处应调用服务层或仓库层来启动计划 c.logger.Infof("收到启动计划请求 (占位符)") @@ -348,7 +348,7 @@ func (c *Controller) StartPlan(ctx *gin.Context) { // @Param id path int true "计划ID" // @Success 200 {object} controller.Response "业务码为200代表成功停止计划" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" -// @Router /plans/{id}/stop [post] +// @Router /api/v1/plans/{id}/stop [post] func (c *Controller) StopPlan(ctx *gin.Context) { // 占位符:此处应调用服务层或仓库层来停止计划 c.logger.Infof("收到停止计划请求 (占位符)") diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index 70f818b..ab28bdc 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -60,7 +60,7 @@ type LoginResponse struct { // @Param user body CreateUserRequest true "用户信息" // @Success 200 {object} controller.Response{data=user.CreateUserResponse} "业务码为201代表创建成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 409, 500)" -// @Router /users [post] +// @Router /api/v1/users [post] func (c *Controller) CreateUser(ctx *gin.Context) { var req CreateUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { @@ -104,7 +104,7 @@ func (c *Controller) CreateUser(ctx *gin.Context) { // @Param credentials body LoginRequest true "登录凭证" // @Success 200 {object} controller.Response{data=user.LoginResponse} "业务码为200代表登录成功" // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 401, 500)" -// @Router /users/login [post] +// @Router /api/v1/users/login [post] func (c *Controller) Login(ctx *gin.Context) { var req LoginRequest if err := ctx.ShouldBindJSON(&req); err != nil { -- 2.49.1 From cb63437e0e847f0feceb3299d69adb70de09987d Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 19 Sep 2025 23:51:13 +0800 Subject: [PATCH 05/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dswagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swaggo | 2 + Makefile | 4 +- docs/docs.go | 197 +++++++++++++++--- docs/swagger.json | 197 +++++++++++++++--- docs/swagger.yaml | 116 ++++++++--- .../controller/device/device_controller.go | 15 +- .../app/controller/plan/plan_controller.go | 7 - .../app/controller/user/user_controller.go | 2 - 8 files changed, 446 insertions(+), 94 deletions(-) create mode 100644 .swaggo diff --git a/.swaggo b/.swaggo new file mode 100644 index 0000000..ac502ac --- /dev/null +++ b/.swaggo @@ -0,0 +1,2 @@ +replace encoding/json.RawMessage object +replace git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse device.DeviceResponse \ No newline at end of file diff --git a/Makefile b/Makefile index 5121194..4ed8fd6 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,9 @@ test: # 生成swagger文档 .PHONY: swag swag: - swag init + if exist docs rmdir /s /q docs + swag init --parseInternal --parseDependency + # 生成protobuf文件 .PHONY: proto diff --git a/docs/docs.go b/docs/docs.go index a2f2894..884d006 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -27,9 +27,24 @@ const docTemplate = `{ "summary": "获取设备列表", "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + } + } + } + } + ] } } } @@ -59,9 +74,21 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + } + } + } + ] } } } @@ -88,9 +115,21 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + } + } + } + ] } } } @@ -127,9 +166,21 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + } + } + } + ] } } } @@ -154,7 +205,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { "$ref": "#/definitions/controller.Response" } @@ -174,9 +225,21 @@ const docTemplate = `{ "summary": "获取计划列表", "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 500)", + "description": "业务码为200代表成功获取列表", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plan.ListPlansResponse" + } + } + } + ] } } } @@ -206,9 +269,21 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 500)", + "description": "业务码为201代表创建成功", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plan.PlanResponse" + } + } + } + ] } } } @@ -235,9 +310,21 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表成功获取", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plan.PlanResponse" + } + } + } + ] } } } @@ -274,9 +361,21 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表更新成功", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plan.PlanResponse" + } + } + } + ] } } } @@ -301,7 +400,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表删除成功", "schema": { "$ref": "#/definitions/controller.Response" } @@ -330,7 +429,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表成功启动计划", "schema": { "$ref": "#/definitions/controller.Response" } @@ -359,7 +458,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表成功停止计划", "schema": { "$ref": "#/definitions/controller.Response" } @@ -393,9 +492,21 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 409, 500)", + "description": "业务码为201代表创建成功", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/user.CreateUserResponse" + } + } + } + ] } } } @@ -427,9 +538,21 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 401, 500)", + "description": "业务码为200代表登录成功", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/user.LoginResponse" + } + } + } + ] } } } @@ -457,7 +580,31 @@ const docTemplate = `{ } }, "device.CreateDeviceRequest": { - "type": "object" + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "properties": { + "$ref": "#/definitions/controller.Properties" + }, + "sub_type": { + "$ref": "#/definitions/models.DeviceSubType" + }, + "type": { + "$ref": "#/definitions/models.DeviceType" + } + } }, "device.UpdateDeviceRequest": { "type": "object", diff --git a/docs/swagger.json b/docs/swagger.json index fc3ad34..7fe31ca 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -16,9 +16,24 @@ "summary": "获取设备列表", "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + } + } + } + } + ] } } } @@ -48,9 +63,21 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + } + } + } + ] } } } @@ -77,9 +104,21 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + } + } + } + ] } } } @@ -116,9 +155,21 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" + } + } + } + ] } } } @@ -143,7 +194,7 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体", + "description": "OK", "schema": { "$ref": "#/definitions/controller.Response" } @@ -163,9 +214,21 @@ "summary": "获取计划列表", "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 500)", + "description": "业务码为200代表成功获取列表", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plan.ListPlansResponse" + } + } + } + ] } } } @@ -195,9 +258,21 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 500)", + "description": "业务码为201代表创建成功", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plan.PlanResponse" + } + } + } + ] } } } @@ -224,9 +299,21 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表成功获取", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plan.PlanResponse" + } + } + } + ] } } } @@ -263,9 +350,21 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表更新成功", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plan.PlanResponse" + } + } + } + ] } } } @@ -290,7 +389,7 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表删除成功", "schema": { "$ref": "#/definitions/controller.Response" } @@ -319,7 +418,7 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表成功启动计划", "schema": { "$ref": "#/definitions/controller.Response" } @@ -348,7 +447,7 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 404, 500)", + "description": "业务码为200代表成功停止计划", "schema": { "$ref": "#/definitions/controller.Response" } @@ -382,9 +481,21 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 409, 500)", + "description": "业务码为201代表创建成功", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/user.CreateUserResponse" + } + } + } + ] } } } @@ -416,9 +527,21 @@ ], "responses": { "200": { - "description": "业务失败,具体错误码和信息见响应体(例如400, 401, 500)", + "description": "业务码为200代表登录成功", "schema": { - "$ref": "#/definitions/controller.Response" + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/user.LoginResponse" + } + } + } + ] } } } @@ -446,7 +569,31 @@ } }, "device.CreateDeviceRequest": { - "type": "object" + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "properties": { + "$ref": "#/definitions/controller.Properties" + }, + "sub_type": { + "$ref": "#/definitions/models.DeviceSubType" + }, + "type": { + "$ref": "#/definitions/models.DeviceType" + } + } }, "device.UpdateDeviceRequest": { "type": "object", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c4249e2..5f87c17 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -13,6 +13,22 @@ definitions: type: string type: object device.CreateDeviceRequest: + properties: + location: + type: string + name: + type: string + parent_id: + type: integer + properties: + $ref: '#/definitions/controller.Properties' + sub_type: + $ref: '#/definitions/models.DeviceSubType' + type: + $ref: '#/definitions/models.DeviceType' + required: + - name + - type type: object device.UpdateDeviceRequest: properties: @@ -372,9 +388,16 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体 + description: OK schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' + type: array + type: object summary: 获取设备列表 tags: - 设备管理 @@ -393,9 +416,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体 + description: OK schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' + type: object summary: 创建新设备 tags: - 设备管理 @@ -412,7 +440,7 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体 + description: OK schema: $ref: '#/definitions/controller.Response' summary: 删除设备 @@ -430,9 +458,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体 + description: OK schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' + type: object summary: 获取设备信息 tags: - 设备管理 @@ -456,9 +489,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体 + description: OK schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' + type: object summary: 更新设备信息 tags: - 设备管理 @@ -469,9 +507,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 500) + description: 业务码为200代表成功获取列表 schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/plan.ListPlansResponse' + type: object summary: 获取计划列表 tags: - 计划管理 @@ -490,9 +533,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 500) + description: 业务码为201代表创建成功 schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/plan.PlanResponse' + type: object summary: 创建计划 tags: - 计划管理 @@ -509,7 +557,7 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 404, 500) + description: 业务码为200代表删除成功 schema: $ref: '#/definitions/controller.Response' summary: 删除计划 @@ -527,9 +575,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 404, 500) + description: 业务码为200代表成功获取 schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/plan.PlanResponse' + type: object summary: 获取计划详情 tags: - 计划管理 @@ -553,9 +606,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 404, 500) + description: 业务码为200代表更新成功 schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/plan.PlanResponse' + type: object summary: 更新计划 tags: - 计划管理 @@ -572,7 +630,7 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 404, 500) + description: 业务码为200代表成功启动计划 schema: $ref: '#/definitions/controller.Response' summary: 启动计划 @@ -591,7 +649,7 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 404, 500) + description: 业务码为200代表成功停止计划 schema: $ref: '#/definitions/controller.Response' summary: 停止计划 @@ -613,9 +671,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 409, 500) + description: 业务码为201代表创建成功 schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/user.CreateUserResponse' + type: object summary: 创建新用户 tags: - 用户管理 @@ -635,9 +698,14 @@ paths: - application/json responses: "200": - description: 业务失败,具体错误码和信息见响应体(例如400, 401, 500) + description: 业务码为200代表登录成功 schema: - $ref: '#/definitions/controller.Response' + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/user.LoginResponse' + type: object summary: 用户登录 tags: - 用户管理 diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index 397b2ae..1b854a7 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -104,8 +104,7 @@ func newListDeviceResponse(devices []*models.Device) []*DeviceResponse { // @Accept json // @Produce json // @Param device body CreateDeviceRequest true "设备信息" -// @Success 200 {object} controller.Response{data=DeviceResponse} "业务码为201代表创建成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" +// @Success 200 {object} controller.Response{data=DeviceResponse} // @Router /api/v1/devices [post] func (c *Controller) CreateDevice(ctx *gin.Context) { var req CreateDeviceRequest @@ -139,8 +138,7 @@ func (c *Controller) CreateDevice(ctx *gin.Context) { // @Tags 设备管理 // @Produce json // @Param id path string true "设备ID" -// @Success 200 {object} controller.Response{data=DeviceResponse} "业务码为200代表获取成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" +// @Success 200 {object} controller.Response{data=DeviceResponse} // @Router /api/v1/devices/{id} [get] func (c *Controller) GetDevice(ctx *gin.Context) { deviceID := ctx.Param("id") @@ -168,8 +166,7 @@ func (c *Controller) GetDevice(ctx *gin.Context) { // @Description 获取系统中所有设备的列表 // @Tags 设备管理 // @Produce json -// @Success 200 {object} controller.Response{data=[]DeviceResponse} "业务码为200代表获取成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" +// @Success 200 {object} controller.Response{data=[]DeviceResponse} // @Router /api/v1/devices [get] func (c *Controller) ListDevices(ctx *gin.Context) { devices, err := c.repo.ListAll() @@ -190,8 +187,7 @@ func (c *Controller) ListDevices(ctx *gin.Context) { // @Produce json // @Param id path string true "设备ID" // @Param device body UpdateDeviceRequest true "要更新的设备信息" -// @Success 200 {object} controller.Response{data=DeviceResponse} "业务码为200代表更新成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" +// @Success 200 {object} controller.Response{data=DeviceResponse} // @Router /api/v1/devices/{id} [put] func (c *Controller) UpdateDevice(ctx *gin.Context) { deviceID := ctx.Param("id") @@ -244,8 +240,7 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) { // @Tags 设备管理 // @Produce json // @Param id path string true "设备ID" -// @Success 200 {object} controller.Response "业务码为200代表删除成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体" +// @Success 200 {object} controller.Response // @Router /api/v1/devices/{id} [delete] func (c *Controller) DeleteDevice(ctx *gin.Context) { deviceID := ctx.Param("id") diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index d33fe40..1f6ca10 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -117,7 +117,6 @@ func NewController(logger *logs.Logger, planRepo repository.PlanRepository, anal // @Produce json // @Param plan body CreatePlanRequest true "计划信息" // @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为201代表创建成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 500)" // @Router /api/v1/plans [post] func (c *Controller) CreatePlan(ctx *gin.Context) { var req CreatePlanRequest @@ -159,7 +158,6 @@ func (c *Controller) CreatePlan(ctx *gin.Context) { // @Produce json // @Param id path int true "计划ID" // @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表成功获取" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" // @Router /api/v1/plans/{id} [get] func (c *Controller) GetPlan(ctx *gin.Context) { // 1. 从 URL 路径中获取 ID @@ -197,7 +195,6 @@ func (c *Controller) GetPlan(ctx *gin.Context) { // @Tags 计划管理 // @Produce json // @Success 200 {object} controller.Response{data=plan.ListPlansResponse} "业务码为200代表成功获取列表" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 500)" // @Router /api/v1/plans [get] func (c *Controller) ListPlans(ctx *gin.Context) { // 1. 调用仓库层获取所有计划 @@ -231,7 +228,6 @@ func (c *Controller) ListPlans(ctx *gin.Context) { // @Param id path int true "计划ID" // @Param plan body UpdatePlanRequest true "更新后的计划信息" // @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表更新成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" // @Router /api/v1/plans/{id} [put] func (c *Controller) UpdatePlan(ctx *gin.Context) { // 1. 从 URL 路径中获取 ID @@ -303,7 +299,6 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { // @Produce json // @Param id path int true "计划ID" // @Success 200 {object} controller.Response "业务码为200代表删除成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" // @Router /api/v1/plans/{id} [delete] func (c *Controller) DeletePlan(ctx *gin.Context) { // 1. 从 URL 路径中获取 ID @@ -332,7 +327,6 @@ func (c *Controller) DeletePlan(ctx *gin.Context) { // @Produce json // @Param id path int true "计划ID" // @Success 200 {object} controller.Response "业务码为200代表成功启动计划" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" // @Router /api/v1/plans/{id}/start [post] func (c *Controller) StartPlan(ctx *gin.Context) { // 占位符:此处应调用服务层或仓库层来启动计划 @@ -347,7 +341,6 @@ func (c *Controller) StartPlan(ctx *gin.Context) { // @Produce json // @Param id path int true "计划ID" // @Success 200 {object} controller.Response "业务码为200代表成功停止计划" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" // @Router /api/v1/plans/{id}/stop [post] func (c *Controller) StopPlan(ctx *gin.Context) { // 占位符:此处应调用服务层或仓库层来停止计划 diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index ab28bdc..14579b5 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -59,7 +59,6 @@ type LoginResponse struct { // @Produce json // @Param user body CreateUserRequest true "用户信息" // @Success 200 {object} controller.Response{data=user.CreateUserResponse} "业务码为201代表创建成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 409, 500)" // @Router /api/v1/users [post] func (c *Controller) CreateUser(ctx *gin.Context) { var req CreateUserRequest @@ -103,7 +102,6 @@ func (c *Controller) CreateUser(ctx *gin.Context) { // @Produce json // @Param credentials body LoginRequest true "登录凭证" // @Success 200 {object} controller.Response{data=user.LoginResponse} "业务码为200代表登录成功" -// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 401, 500)" // @Router /api/v1/users/login [post] func (c *Controller) Login(ctx *gin.Context) { var req LoginRequest -- 2.49.1 From 1f2d54d53e36f331bd6b77afc110282aaedaf36f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 17:11:04 +0800 Subject: [PATCH 06/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swaggo | 2 - docs/docs.go | 18 +-- docs/swagger.json | 18 +-- docs/swagger.yaml | 17 ++- .../controller/device/device_controller.go | 125 +++++++++++++----- internal/app/controller/plan/converter.go | 77 +++++++---- .../app/controller/plan/plan_controller.go | 53 +++++--- internal/app/controller/response.go | 4 - internal/infra/models/device.go | 2 +- 9 files changed, 212 insertions(+), 104 deletions(-) diff --git a/.swaggo b/.swaggo index ac502ac..e69de29 100644 --- a/.swaggo +++ b/.swaggo @@ -1,2 +0,0 @@ -replace encoding/json.RawMessage object -replace git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse device.DeviceResponse \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 884d006..68e498f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -560,9 +560,6 @@ const docTemplate = `{ } }, "definitions": { - "controller.Properties": { - "type": "object" - }, "controller.Response": { "type": "object", "properties": { @@ -596,7 +593,8 @@ const docTemplate = `{ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -623,7 +621,8 @@ const docTemplate = `{ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -652,7 +651,8 @@ const docTemplate = `{ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -952,7 +952,8 @@ const docTemplate = `{ "example": "打开风扇" }, "parameters": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "type": { "allOf": [ @@ -984,7 +985,8 @@ const docTemplate = `{ "example": "打开风扇" }, "parameters": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "plan_id": { "type": "integer", diff --git a/docs/swagger.json b/docs/swagger.json index 7fe31ca..4adabab 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -549,9 +549,6 @@ } }, "definitions": { - "controller.Properties": { - "type": "object" - }, "controller.Response": { "type": "object", "properties": { @@ -585,7 +582,8 @@ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -612,7 +610,8 @@ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -641,7 +640,8 @@ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -941,7 +941,8 @@ "example": "打开风扇" }, "parameters": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "type": { "allOf": [ @@ -973,7 +974,8 @@ "example": "打开风扇" }, "parameters": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "plan_id": { "type": "integer", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5f87c17..dcb61c8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,6 +1,4 @@ definitions: - controller.Properties: - type: object controller.Response: properties: code: @@ -21,7 +19,8 @@ definitions: parent_id: type: integer properties: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object sub_type: $ref: '#/definitions/models.DeviceSubType' type: @@ -39,7 +38,8 @@ definitions: parent_id: type: integer properties: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object sub_type: $ref: '#/definitions/models.DeviceSubType' type: @@ -61,7 +61,8 @@ definitions: parent_id: type: integer properties: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object sub_type: $ref: '#/definitions/models.DeviceSubType' type: @@ -271,7 +272,8 @@ definitions: example: 打开风扇 type: string parameters: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object type: allOf: - $ref: '#/definitions/models.TaskType' @@ -292,7 +294,8 @@ definitions: example: 打开风扇 type: string parameters: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object plan_id: example: 1 type: integer diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index 1b854a7..3eac4b5 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -1,7 +1,9 @@ package device import ( + "encoding/json" "errors" + "fmt" "strconv" "strings" "time" @@ -11,7 +13,6 @@ import ( "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" - "gorm.io/datatypes" "gorm.io/gorm" ) @@ -33,46 +34,54 @@ func NewController(repo repository.DeviceRepository, logger *logs.Logger) *Contr // CreateDeviceRequest 定义了创建设备时需要传入的参数 type CreateDeviceRequest struct { - Name string `json:"name" binding:"required"` - Type models.DeviceType `json:"type" binding:"required"` - SubType models.DeviceSubType `json:"sub_type,omitempty"` - ParentID *uint `json:"parent_id,omitempty"` - Location string `json:"location,omitempty"` - Properties controller.Properties `json:"properties,omitempty"` + Name string `json:"name" binding:"required"` + Type models.DeviceType `json:"type" binding:"required"` + SubType models.DeviceSubType `json:"sub_type,omitempty"` + ParentID *uint `json:"parent_id,omitempty"` + Location string `json:"location,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` } // UpdateDeviceRequest 定义了更新设备时需要传入的参数 type UpdateDeviceRequest struct { - Name string `json:"name" binding:"required"` - Type models.DeviceType `json:"type" binding:"required"` - SubType models.DeviceSubType `json:"sub_type,omitempty"` - ParentID *uint `json:"parent_id,omitempty"` - Location string `json:"location,omitempty"` - Properties controller.Properties `json:"properties,omitempty"` + Name string `json:"name" binding:"required"` + Type models.DeviceType `json:"type" binding:"required"` + SubType models.DeviceSubType `json:"sub_type,omitempty"` + ParentID *uint `json:"parent_id,omitempty"` + Location string `json:"location,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` } // --- Response DTOs --- // DeviceResponse 定义了返回给客户端的单个设备信息的结构 type DeviceResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Type models.DeviceType `json:"type"` - SubType models.DeviceSubType `json:"sub_type"` - ParentID *uint `json:"parent_id"` - Location string `json:"location"` - Properties controller.Properties `json:"properties"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID uint `json:"id"` + Name string `json:"name"` + Type models.DeviceType `json:"type"` + SubType models.DeviceSubType `json:"sub_type"` + ParentID *uint `json:"parent_id"` + Location string `json:"location"` + Properties map[string]interface{} `json:"properties"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } // --- DTO 转换函数 --- // newDeviceResponse 从数据库模型创建一个新的设备响应 DTO -func newDeviceResponse(device *models.Device) *DeviceResponse { +func newDeviceResponse(device *models.Device) (*DeviceResponse, error) { if device == nil { - return nil + return nil, nil } + + var props map[string]interface{} + if len(device.Properties) > 0 && string(device.Properties) != "null" { + if err := json.Unmarshal(device.Properties, &props); err != nil { + return nil, fmt.Errorf("解析设备属性失败 (ID: %d): %w", device.ID, err) + } + } + return &DeviceResponse{ ID: device.ID, Name: device.Name, @@ -80,19 +89,23 @@ func newDeviceResponse(device *models.Device) *DeviceResponse { SubType: device.SubType, ParentID: device.ParentID, Location: device.Location, - Properties: controller.Properties(device.Properties), + Properties: props, CreatedAt: device.CreatedAt.Format(time.RFC3339), UpdatedAt: device.UpdatedAt.Format(time.RFC3339), - } + }, nil } // newListDeviceResponse 从数据库模型切片创建一个新的设备列表响应 DTO 切片 -func newListDeviceResponse(devices []*models.Device) []*DeviceResponse { +func newListDeviceResponse(devices []*models.Device) ([]*DeviceResponse, error) { list := make([]*DeviceResponse, 0, len(devices)) for _, device := range devices { - list = append(list, newDeviceResponse(device)) + resp, err := newDeviceResponse(device) + if err != nil { + return nil, err + } + list = append(list, resp) } - return list + return list, nil } // --- Controller Methods --- @@ -114,13 +127,20 @@ func (c *Controller) CreateDevice(ctx *gin.Context) { return } + propertiesJSON, err := json.Marshal(req.Properties) + if err != nil { + c.logger.Errorf("创建设备: 序列化属性失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeBadRequest, "属性字段格式错误") + return + } + device := &models.Device{ Name: req.Name, Type: req.Type, SubType: req.SubType, ParentID: req.ParentID, Location: req.Location, - Properties: datatypes.JSON(req.Properties), + Properties: propertiesJSON, } if err := c.repo.Create(device); err != nil { @@ -129,7 +149,14 @@ func (c *Controller) CreateDevice(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeCreated, "设备创建成功", newDeviceResponse(device)) + resp, err := newDeviceResponse(device) + if err != nil { + c.logger.Errorf("创建设备: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败") + return + } + + controller.SendResponse(ctx, controller.CodeCreated, "设备创建成功", resp) } // GetDevice godoc @@ -158,7 +185,14 @@ func (c *Controller) GetDevice(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeSuccess, "获取设备信息成功", newDeviceResponse(device)) + resp, err := newDeviceResponse(device) + if err != nil { + c.logger.Errorf("获取设备: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误") + return + } + + controller.SendResponse(ctx, controller.CodeSuccess, "获取设备信息成功", resp) } // ListDevices godoc @@ -176,7 +210,14 @@ func (c *Controller) ListDevices(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeSuccess, "获取设备列表成功", newListDeviceResponse(devices)) + resp, err := newListDeviceResponse(devices) + if err != nil { + c.logger.Errorf("获取设备列表: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误") + return + } + + controller.SendResponse(ctx, controller.CodeSuccess, "获取设备列表成功", resp) } // UpdateDevice godoc @@ -216,13 +257,20 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) { return } + propertiesJSON, err := json.Marshal(req.Properties) + if err != nil { + c.logger.Errorf("更新设备: 序列化属性失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeBadRequest, "属性字段格式错误") + return + } + // 3. 更新从数据库中查出的现有设备对象的字段 existingDevice.Name = req.Name existingDevice.Type = req.Type existingDevice.SubType = req.SubType existingDevice.ParentID = req.ParentID existingDevice.Location = req.Location - existingDevice.Properties = datatypes.JSON(req.Properties) + existingDevice.Properties = propertiesJSON // 4. 将修改后的 existingDevice 对象保存回数据库 if err := c.repo.Update(existingDevice); err != nil { @@ -231,7 +279,14 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeSuccess, "设备更新成功", newDeviceResponse(existingDevice)) + resp, err := newDeviceResponse(existingDevice) + if err != nil { + c.logger.Errorf("更新设备: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败") + return + } + + controller.SendResponse(ctx, controller.CodeSuccess, "设备更新成功", resp) } // DeleteDevice godoc diff --git a/internal/app/controller/plan/converter.go b/internal/app/controller/plan/converter.go index 8410c14..6ace996 100644 --- a/internal/app/controller/plan/converter.go +++ b/internal/app/controller/plan/converter.go @@ -1,15 +1,16 @@ package plan import ( - "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" + "encoding/json" + "fmt" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "gorm.io/datatypes" ) // PlanToResponse 将Plan模型转换为PlanResponse -func PlanToResponse(plan *models.Plan) *PlanResponse { +func PlanToResponse(plan *models.Plan) (*PlanResponse, error) { if plan == nil { - return nil + return nil, nil } response := &PlanResponse{ @@ -28,7 +29,11 @@ func PlanToResponse(plan *models.Plan) *PlanResponse { if plan.ContentType == models.PlanContentTypeSubPlans { response.SubPlans = make([]SubPlanResponse, len(plan.SubPlans)) for i, subPlan := range plan.SubPlans { - response.SubPlans[i] = SubPlanToResponse(&subPlan) + subPlanResp, err := SubPlanToResponse(&subPlan) + if err != nil { + return nil, err + } + response.SubPlans[i] = subPlanResp } } @@ -36,11 +41,15 @@ func PlanToResponse(plan *models.Plan) *PlanResponse { if plan.ContentType == models.PlanContentTypeTasks { response.Tasks = make([]TaskResponse, len(plan.Tasks)) for i, task := range plan.Tasks { - response.Tasks[i] = TaskToResponse(&task) + taskResp, err := TaskToResponse(&task) + if err != nil { + return nil, err + } + response.Tasks[i] = taskResp } } - return response + return response, nil } // PlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证 @@ -73,8 +82,11 @@ func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { if req.ContentType == models.PlanContentTypeTasks && req.Tasks != nil { plan.Tasks = make([]models.Task, len(req.Tasks)) for i, taskReq := range req.Tasks { - // 使用来自请求的ExecutionOrder - plan.Tasks[i] = TaskFromRequest(&taskReq) + task, err := TaskFromRequest(&taskReq) + if err != nil { + return nil, err + } + plan.Tasks[i] = task } } @@ -120,8 +132,11 @@ func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { if req.ContentType == models.PlanContentTypeTasks && req.Tasks != nil { plan.Tasks = make([]models.Task, len(req.Tasks)) for i, taskReq := range req.Tasks { - // 使用来自请求的ExecutionOrder - plan.Tasks[i] = TaskFromRequest(&taskReq) + task, err := TaskFromRequest(&taskReq) + if err != nil { + return nil, err + } + plan.Tasks[i] = task } } @@ -138,9 +153,9 @@ func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { } // SubPlanToResponse 将SubPlan模型转换为SubPlanResponse -func SubPlanToResponse(subPlan *models.SubPlan) SubPlanResponse { +func SubPlanToResponse(subPlan *models.SubPlan) (SubPlanResponse, error) { if subPlan == nil { - return SubPlanResponse{} + return SubPlanResponse{}, nil } response := SubPlanResponse{ @@ -152,16 +167,27 @@ func SubPlanToResponse(subPlan *models.SubPlan) SubPlanResponse { // 如果有完整的子计划数据,也进行转换 if subPlan.ChildPlan != nil { - response.ChildPlan = PlanToResponse(subPlan.ChildPlan) + childPlanResp, err := PlanToResponse(subPlan.ChildPlan) + if err != nil { + return SubPlanResponse{}, err + } + response.ChildPlan = childPlanResp } - return response + return response, nil } // TaskToResponse 将Task模型转换为TaskResponse -func TaskToResponse(task *models.Task) TaskResponse { +func TaskToResponse(task *models.Task) (TaskResponse, error) { if task == nil { - return TaskResponse{} + return TaskResponse{}, nil + } + + var params map[string]interface{} + if len(task.Parameters) > 0 && string(task.Parameters) != "null" { + if err := json.Unmarshal(task.Parameters, ¶ms); err != nil { + return TaskResponse{}, fmt.Errorf("parsing task parameters failed (ID: %d): %w", task.ID, err) + } } return TaskResponse{ @@ -171,14 +197,19 @@ func TaskToResponse(task *models.Task) TaskResponse { Description: task.Description, ExecutionOrder: task.ExecutionOrder, Type: task.Type, - Parameters: controller.Properties(task.Parameters), - } + Parameters: params, + }, nil } // TaskFromRequest 将TaskRequest转换为Task模型 -func TaskFromRequest(req *TaskRequest) models.Task { +func TaskFromRequest(req *TaskRequest) (models.Task, error) { if req == nil { - return models.Task{} + return models.Task{}, nil + } + + paramsJSON, err := json.Marshal(req.Parameters) + if err != nil { + return models.Task{}, fmt.Errorf("serializing task parameters failed: %w", err) } return models.Task{ @@ -186,6 +217,6 @@ func TaskFromRequest(req *TaskRequest) models.Task { Description: req.Description, ExecutionOrder: req.ExecutionOrder, Type: req.Type, - Parameters: datatypes.JSON(req.Parameters), - } + Parameters: paramsJSON, + }, nil } diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 1f6ca10..fc5bb08 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -71,22 +71,22 @@ type SubPlanResponse struct { // 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 controller.Properties `json:"parameters,omitempty"` + 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 controller.Properties `json:"parameters,omitempty"` + 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 定义 --- @@ -145,7 +145,12 @@ func (c *Controller) CreatePlan(ctx *gin.Context) { } // 使用已有的转换函数将创建后的模型转换为响应对象 - resp := PlanToResponse(planToCreate) + resp, err := PlanToResponse(planToCreate) + if err != nil { + c.logger.Errorf("创建计划: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败") + return + } // 使用统一的成功响应函数 controller.SendResponse(ctx, controller.CodeCreated, "计划创建成功", resp) @@ -183,7 +188,12 @@ func (c *Controller) GetPlan(ctx *gin.Context) { } // 3. 将模型转换为响应 DTO - resp := PlanToResponse(plan) + resp, err := PlanToResponse(plan) + if err != nil { + c.logger.Errorf("获取计划详情: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误") + return + } // 4. 发送成功响应 controller.SendResponse(ctx, controller.CodeSuccess, "获取计划详情成功", resp) @@ -208,7 +218,13 @@ func (c *Controller) ListPlans(ctx *gin.Context) { // 2. 将模型转换为响应 DTO planResponses := make([]PlanResponse, 0, len(plans)) for _, p := range plans { - planResponses = append(planResponses, *PlanToResponse(&p)) + resp, err := PlanToResponse(&p) + if err != nil { + c.logger.Errorf("获取计划列表: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误") + return + } + planResponses = append(planResponses, *resp) } // 3. 构造并发送成功响应 @@ -286,7 +302,12 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { } // 7. 将模型转换为响应 DTO - resp := PlanToResponse(updatedPlan) + resp, err := PlanToResponse(updatedPlan) + if err != nil { + c.logger.Errorf("更新计划: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败") + return + } // 8. 发送成功响应 controller.SendResponse(ctx, controller.CodeSuccess, "计划更新成功", resp) diff --git a/internal/app/controller/response.go b/internal/app/controller/response.go index b08afd5..136356c 100644 --- a/internal/app/controller/response.go +++ b/internal/app/controller/response.go @@ -1,7 +1,6 @@ package controller import ( - "encoding/json" "net/http" "github.com/gin-gonic/gin" @@ -46,6 +45,3 @@ func SendResponse(ctx *gin.Context, code int, message string, data interface{}) func SendErrorResponse(ctx *gin.Context, code int, message string) { SendResponse(ctx, code, message, nil) } - -// Properties 是一个自定义类型,用于在 Swagger 中正确表示 JSON 对象 -type Properties json.RawMessage diff --git a/internal/infra/models/device.go b/internal/infra/models/device.go index cff3295..469129e 100644 --- a/internal/infra/models/device.go +++ b/internal/infra/models/device.go @@ -70,7 +70,7 @@ type Device struct { gorm.Model // Name 是设备的业务名称,应清晰可读,例如 "1号猪舍温度传感器" 或 "做料车间主控" - Name string `gorm:"unique;not null" json:"name"` + Name string `gorm:"not null" json:"name"` // Type 是设备的高级类别,用于区分区域主控和普通设备。建立索引以优化按类型查询。 Type DeviceType `gorm:"not null;index" json:"type"` -- 2.49.1 From 40a892e09d51080da4990427728e99c1e0468dde Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 18:11:48 +0800 Subject: [PATCH 07/13] todo --- TODO-List.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO-List.txt b/TODO-List.txt index ad8f35d..f98a403 100644 --- a/TODO-List.txt +++ b/TODO-List.txt @@ -12,3 +12,6 @@ 4. 暂时不考虑和区域主控间的同步消息, 假设所有消息都是异步的, 这可能导致无法知道指令是否执行成功 5. 如果系统停机时间很长, 待执行任务表中的任务过期了怎么办, 目前没有任务过期机制 6. 可以用TimescaleDB代替PGSQL, 优化传感器数据存储性能 + + +任务调度器执行触发器任务时要修改一下对应计划的执行次数(如果是指定次数的计划) \ No newline at end of file -- 2.49.1 From e85d4f8ec38da0a7e64e12cebd8d0d9da186b319 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 21:14:58 +0800 Subject: [PATCH 08/13] =?UTF-8?q?=E9=87=8D=E6=9E=84AnalysisPlanTaskManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../task/analysis_plan_task_manager.go | 232 ++++++++++++++---- internal/infra/models/schedule.go | 4 +- .../repository/execution_log_repository.go | 23 ++ .../repository/pending_task_repository.go | 38 +++ internal/infra/repository/plan_repository.go | 63 ++++- 5 files changed, 310 insertions(+), 50 deletions(-) diff --git a/internal/app/service/task/analysis_plan_task_manager.go b/internal/app/service/task/analysis_plan_task_manager.go index 1d4dee6..093490b 100644 --- a/internal/app/service/task/analysis_plan_task_manager.go +++ b/internal/app/service/task/analysis_plan_task_manager.go @@ -1,7 +1,9 @@ package task import ( - "context" + "fmt" + "sync" + "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -9,13 +11,15 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/utils" ) -// AnalysisPlanTaskManager 封装了创建和更新计划分析任务(即触发器)的逻辑。 -// 这是一个可被 Scheduler 和其他应用服务(如 PlanService)共享的无状态组件。 +// AnalysisPlanTaskManager 负责管理分析计划的触发器任务。 +// 它确保数据库中可执行的计划在待执行队列中有对应的触发器,并移除无效的触发器。 +// 这是一个有状态的组件,包含一个互斥锁以确保并发安全。 type AnalysisPlanTaskManager struct { planRepo repository.PlanRepository pendingTaskRepo repository.PendingTaskRepository executionLogRepo repository.ExecutionLogRepository logger *logs.Logger + mu sync.Mutex } // NewAnalysisPlanTaskManager 是 AnalysisPlanTaskManager 的构造函数。 @@ -33,50 +37,196 @@ func NewAnalysisPlanTaskManager( } } -// CreateOrUpdateTrigger 为给定的 planID 创建或更新其关联的下一次触发任务。 -// 这个方法是幂等的,可以安全地被多次调用。 -func (m *AnalysisPlanTaskManager) CreateOrUpdateTrigger(ctx context.Context, planID uint) error { - // 获取计划信息 - plan, err := m.planRepo.GetBasicPlanByID(planID) +// Refresh 同步数据库中的计划状态和待执行队列中的触发器任务。 +// 这是一个编排方法,将复杂的逻辑分解到多个内部方法中。 +func (m *AnalysisPlanTaskManager) Refresh() error { + m.mu.Lock() + defer m.mu.Unlock() + + m.logger.Info("开始同步计划任务管理器...") + + // 1. 一次性获取所有需要的数据 + runnablePlans, invalidPlanIDs, pendingTasks, err := m.getRefreshData() if err != nil { - m.logger.Errorf("[严重] 获取计划失败, 错误: %v", err) - return err + return fmt.Errorf("获取刷新数据失败: %w", err) } - // 获取触发任务 - task, err := m.planRepo.FindPlanAnalysisTaskByParamsPlanID(planID) - if err != nil { - m.logger.Errorf("[严重] 获取计划解析任务失败, 错误: %v", err) - return err + // 2. 清理所有与失效计划相关的待执行任务 + if err := m.cleanupInvalidTasks(invalidPlanIDs, pendingTasks); err != nil { + // 仅记录错误,清理失败不应阻止新任务的添加 + m.logger.Errorf("清理无效任务时出错: %v", err) } - // 写入执行日志 - taskLog := &models.TaskExecutionLog{ - TaskID: task.ID, - Status: models.ExecutionStatusWaiting, - } - if err := m.executionLogRepo.CreateTaskExecutionLogsInBatch([]*models.TaskExecutionLog{taskLog}); err != nil { - m.logger.Errorf("[严重] 创建任务执行日志失败, 错误: %v", err) - return err + // 3. 为应执行但缺失的计划添加新触发器 + if err := m.addNewTriggers(runnablePlans, pendingTasks); err != nil { + return fmt.Errorf("添加新触发器时出错: %w", err) } - // 写入待执行队列 - next, err := utils.GetNextCronTime(plan.CronExpression) - if err != nil { - m.logger.Errorf("[严重] 执行时间解析失败, 错误: %v", err) - return err - } - pendingTask := &models.PendingTask{ - TaskID: task.ID, - ExecuteAt: next, - TaskExecutionLogID: taskLog.ID, - } - err = m.pendingTaskRepo.CreatePendingTasksInBatch([]*models.PendingTask{pendingTask}) - if err != nil { - m.logger.Errorf("[严重] 创建待执行任务失败, 错误: %v", err) - return err - } - - m.logger.Infof("成功为 Plan %d 创建/更新了下一次的触发任务,执行时间: %v", planID, next) + m.logger.Info("计划任务管理器同步完成.") + return nil +} + +// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。 +// 这个方法是幂等的:如果一个有效的触发器已存在,它将不会重复创建。 +func (m *AnalysisPlanTaskManager) CreateOrUpdateTrigger(planID uint) error { + m.mu.Lock() + defer m.mu.Unlock() + + // 检查计划是否可执行 + plan, err := m.planRepo.GetBasicPlanByID(planID) + if err != nil { + return fmt.Errorf("获取计划基本信息失败: %w", err) + } + if plan.Status != models.PlanStatusEnabled { + return fmt.Errorf("计划 #%d 当前状态为 '%d',无法创建触发器", planID, plan.Status) + } + + // 幂等性检查:如果触发器已存在,则直接返回 + existingTrigger, err := m.pendingTaskRepo.FindPendingTriggerByPlanID(planID) + if err != nil { + return fmt.Errorf("查找现有触发器失败: %w", err) + } + if existingTrigger != nil { + m.logger.Infof("计划 #%d 的触发器已存在于待执行队列中,无需重复创建。", planID) + return nil + } + + m.logger.Infof("为计划 #%d 创建新的触发器...", planID) + return m.createTriggerTask(plan) +} + +// --- 内部私有方法 --- + +// getRefreshData 从数据库获取刷新所需的所有数据。 +func (m *AnalysisPlanTaskManager) getRefreshData() (runnablePlans []*models.Plan, invalidPlanIDs []uint, pendingTasks []models.PendingTask, err error) { + runnablePlans, err = m.planRepo.FindRunnablePlans() + if err != nil { + m.logger.Errorf("获取可执行计划列表失败: %v", err) + return + } + + invalidPlans, err := m.planRepo.FindDisabledAndStoppedPlans() + if err != nil { + m.logger.Errorf("获取失效计划列表失败: %v", err) + return + } + invalidPlanIDs = make([]uint, len(invalidPlans)) + for i, p := range invalidPlans { + invalidPlanIDs[i] = p.ID + } + + pendingTasks, err = m.pendingTaskRepo.FindAllPendingTasks() + if err != nil { + m.logger.Errorf("获取所有待执行任务失败: %v", err) + return + } + return +} + +// cleanupInvalidTasks 清理所有与失效计划相关的待执行任务。 +func (m *AnalysisPlanTaskManager) cleanupInvalidTasks(invalidPlanIDs []uint, allPendingTasks []models.PendingTask) error { + if len(invalidPlanIDs) == 0 { + return nil // 没有需要清理的计划 + } + + invalidPlanIDSet := make(map[uint]struct{}, len(invalidPlanIDs)) + for _, id := range invalidPlanIDs { + invalidPlanIDSet[id] = struct{}{} + } + + var tasksToDeleteIDs []uint + var logsToCancelIDs []uint + + for _, pt := range allPendingTasks { + if pt.Task == nil { // 防御性编程,确保 Task 被预加载 + continue + } + if _, isInvalid := invalidPlanIDSet[pt.Task.PlanID]; isInvalid { + tasksToDeleteIDs = append(tasksToDeleteIDs, pt.ID) + logsToCancelIDs = append(logsToCancelIDs, pt.TaskExecutionLogID) + } + } + + if len(tasksToDeleteIDs) == 0 { + return nil // 没有找到需要清理的任务 + } + + m.logger.Infof("准备从待执行队列中清理 %d 个与失效计划相关的任务...", len(tasksToDeleteIDs)) + + // 批量删除待执行任务 + if err := m.pendingTaskRepo.DeletePendingTasksByIDs(tasksToDeleteIDs); err != nil { + return fmt.Errorf("批量删除待执行任务失败: %w", err) + } + + // 批量更新相关执行日志状态为“已取消” + if err := m.executionLogRepo.UpdateLogStatusByIDs(logsToCancelIDs, models.ExecutionStatusCancelled); err != nil { + // 这是一个非关键性错误,只记录日志 + m.logger.Warnf("批量更新日志状态为 'Cancelled' 失败: %v", err) + } + + return nil +} + +// addNewTriggers 检查并为应执行但缺失的计划添加新触发器。 +func (m *AnalysisPlanTaskManager) addNewTriggers(runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error { + // 创建一个集合,存放所有已在队列中的计划触发器 + pendingTriggerPlanIDs := make(map[uint]struct{}) + for _, pt := range allPendingTasks { + if pt.Task != nil && pt.Task.Type == models.TaskPlanAnalysis { + pendingTriggerPlanIDs[pt.Task.PlanID] = struct{}{} + } + } + + for _, plan := range runnablePlans { + if _, exists := pendingTriggerPlanIDs[plan.ID]; !exists { + m.logger.Infof("发现应执行但队列中缺失的计划 #%d,正在为其创建触发器...", plan.ID) + if err := m.createTriggerTask(plan); err != nil { + m.logger.Errorf("为计划 #%d 创建触发器失败: %v", plan.ID, err) + // 继续处理下一个,不因单点失败而中断 + } + } + } + return nil +} + +// createTriggerTask 是创建触发器任务的内部核心逻辑。 +func (m *AnalysisPlanTaskManager) createTriggerTask(plan *models.Plan) error { + analysisTask, err := m.planRepo.FindPlanAnalysisTaskByPlanID(plan.ID) + if err != nil { + return fmt.Errorf("查找计划分析任务失败: %w", err) + } + if analysisTask == nil { + return fmt.Errorf("未找到计划 #%d 关联的 'plan_analysis' 任务", plan.ID) + } + + var executeAt time.Time + if plan.ExecutionType == models.PlanExecutionTypeManual { + executeAt = time.Now() + } else { + next, err := utils.GetNextCronTime(plan.CronExpression) + if err != nil { + return fmt.Errorf("解析 Cron 表达式 '%s' 失败: %w", plan.CronExpression, err) + } + executeAt = next + } + + taskLog := &models.TaskExecutionLog{ + TaskID: analysisTask.ID, + Status: models.ExecutionStatusWaiting, + } + if err := m.executionLogRepo.CreateTaskExecutionLog(taskLog); err != nil { + return fmt.Errorf("创建任务执行日志失败: %w", err) + } + + pendingTask := &models.PendingTask{ + TaskID: analysisTask.ID, + ExecuteAt: executeAt, + TaskExecutionLogID: taskLog.ID, + } + if err := m.pendingTaskRepo.CreatePendingTask(pendingTask); err != nil { + return fmt.Errorf("创建待执行任务失败: %w", err) + } + + m.logger.Infof("成功为计划 #%d 创建触发器 (任务ID: %d),执行时间: %v", plan.ID, analysisTask.ID, executeAt) return nil } diff --git a/internal/infra/models/schedule.go b/internal/infra/models/schedule.go index a3f5f4d..40e43b1 100644 --- a/internal/infra/models/schedule.go +++ b/internal/infra/models/schedule.go @@ -16,11 +16,13 @@ type PendingTask struct { // TaskID 使用 int 类型以容纳特殊的负数ID,代表系统任务 TaskID int `gorm:"index"` + // Task 字段,用于在代码中访问关联的任务详情 + // GORM 会根据 TaskID 字段自动填充此关联 + Task *Task `gorm:"foreignKey:TaskID"` ExecuteAt time.Time `gorm:"index"` // 任务执行时间 TaskExecutionLogID uint `gorm:"unique;not null"` // 对应的执行历史记录ID - // 关联关系定义 // 通过 TaskExecutionLogID 关联到唯一的 TaskExecutionLog 记录 // ON DELETE CASCADE 确保如果日志被删除,这个待办任务也会被自动清理 TaskExecutionLog TaskExecutionLog `gorm:"foreignKey:TaskExecutionLogID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` diff --git a/internal/infra/repository/execution_log_repository.go b/internal/infra/repository/execution_log_repository.go index 4503cb0..d0e4f81 100644 --- a/internal/infra/repository/execution_log_repository.go +++ b/internal/infra/repository/execution_log_repository.go @@ -8,6 +8,9 @@ import ( // ExecutionLogRepository 定义了与执行日志交互的接口。 // 这为服务层提供了一个清晰的契约,并允许在测试中轻松地进行模拟。 type ExecutionLogRepository interface { + UpdateLogStatusByIDs(logIDs []uint, status models.ExecutionStatus) error + UpdateLogStatus(logID uint, status models.ExecutionStatus) error + CreateTaskExecutionLog(log *models.TaskExecutionLog) error CreatePlanExecutionLog(log *models.PlanExecutionLog) error UpdatePlanExecutionLog(log *models.PlanExecutionLog) error CreateTaskExecutionLogsInBatch(logs []*models.TaskExecutionLog) error @@ -26,6 +29,23 @@ func NewGormExecutionLogRepository(db *gorm.DB) ExecutionLogRepository { return &gormExecutionLogRepository{db: db} } +func (r *gormExecutionLogRepository) UpdateLogStatusByIDs(logIDs []uint, status models.ExecutionStatus) error { + if len(logIDs) == 0 { + return nil + } + return r.db.Model(&models.TaskExecutionLog{}). + Where("id IN ?", logIDs). + Update("status", status).Error +} + +func (r *gormExecutionLogRepository) UpdateLogStatus(logID uint, status models.ExecutionStatus) error { + return r.db.Model(&models.TaskExecutionLog{}).Where("id = ?", logID).Update("status", status).Error +} + +func (r *gormExecutionLogRepository) CreateTaskExecutionLog(log *models.TaskExecutionLog) error { + return r.db.Create(log).Error +} + // CreatePlanExecutionLog 为一次计划执行创建一条新的日志条目。 func (r *gormExecutionLogRepository) CreatePlanExecutionLog(log *models.PlanExecutionLog) error { return r.db.Create(log).Error @@ -41,6 +61,9 @@ func (r *gormExecutionLogRepository) UpdatePlanExecutionLog(log *models.PlanExec // CreateTaskExecutionLogsInBatch 在一次数据库调用中创建多个任务执行日志条目。 // 这是“预写日志”步骤的关键。 func (r *gormExecutionLogRepository) CreateTaskExecutionLogsInBatch(logs []*models.TaskExecutionLog) error { + if len(logs) == 0 { + return nil + } // GORM 的 Create 传入一个切片指针会执行批量插入。 return r.db.Create(&logs).Error } diff --git a/internal/infra/repository/pending_task_repository.go b/internal/infra/repository/pending_task_repository.go index 689197b..166496b 100644 --- a/internal/infra/repository/pending_task_repository.go +++ b/internal/infra/repository/pending_task_repository.go @@ -1,6 +1,7 @@ package repository import ( + "errors" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -10,6 +11,10 @@ import ( // PendingTaskRepository 定义了与待执行任务队列交互的接口。 type PendingTaskRepository interface { + FindAllPendingTasks() ([]models.PendingTask, error) + FindPendingTriggerByPlanID(planID uint) (*models.PendingTask, error) + DeletePendingTasksByIDs(ids []uint) error + CreatePendingTask(task *models.PendingTask) error CreatePendingTasksInBatch(tasks []*models.PendingTask) error // ClaimNextAvailableTask 原子地认领下一个可用的任务。 // 它会同时返回被认领任务对应的日志对象,以及被删除的待办任务对象的内存副本。 @@ -28,8 +33,41 @@ func NewGormPendingTaskRepository(db *gorm.DB) PendingTaskRepository { return &gormPendingTaskRepository{db: db} } +func (r *gormPendingTaskRepository) FindAllPendingTasks() ([]models.PendingTask, error) { + var tasks []models.PendingTask + // 预加载 Task 以便后续访问 Task.PlanID + err := r.db.Preload("Task").Find(&tasks).Error + return tasks, err +} + +func (r *gormPendingTaskRepository) FindPendingTriggerByPlanID(planID uint) (*models.PendingTask, error) { + var pendingTask models.PendingTask + err := r.db. + Joins("JOIN tasks ON tasks.id = pending_tasks.task_id"). + Where("tasks.plan_id = ? AND tasks.type = ?", planID, models.TaskPlanAnalysis). + First(&pendingTask).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil // 未找到不是错误 + } + return &pendingTask, err +} + +func (r *gormPendingTaskRepository) DeletePendingTasksByIDs(ids []uint) error { + if len(ids) == 0 { + return nil + } + return r.db.Where("id IN ?", ids).Delete(&models.PendingTask{}).Error +} + +func (r *gormPendingTaskRepository) CreatePendingTask(task *models.PendingTask) error { + return r.db.Create(task).Error +} + // CreatePendingTasksInBatch 在一次数据库调用中创建多个待执行任务条目。 func (r *gormPendingTaskRepository) CreatePendingTasksInBatch(tasks []*models.PendingTask) error { + if len(tasks) == 0 { + return nil + } return r.db.Create(&tasks).Error } diff --git a/internal/infra/repository/plan_repository.go b/internal/infra/repository/plan_repository.go index e32236d..66ebd8d 100644 --- a/internal/infra/repository/plan_repository.go +++ b/internal/infra/repository/plan_repository.go @@ -42,6 +42,12 @@ type PlanRepository interface { DeleteTask(id int) error // FindPlanAnalysisTaskByParamsPlanID 根据Parameters中的ParamsPlanID字段值查找TaskPlanAnalysis类型的Task FindPlanAnalysisTaskByParamsPlanID(paramsPlanID uint) (*models.Task, error) + // FindRunnablePlans 获取所有应执行的计划 + FindRunnablePlans() ([]*models.Plan, error) + // FindDisabledAndStoppedPlans 获取所有已禁用或已停止的计划 + FindDisabledAndStoppedPlans() ([]*models.Plan, error) + // FindPlanAnalysisTaskByPlanID 根据 PlanID 找到其关联的 'plan_analysis' 任务 + FindPlanAnalysisTaskByPlanID(planID uint) (*models.Task, error) } // gormPlanRepository 是 PlanRepository 的 GORM 实现 @@ -565,15 +571,56 @@ func (r *gormPlanRepository) createPlanAnalysisTask(tx *gorm.DB, plan *models.Pl return tx.Create(task).Error } -// updatePlanAnalysisTask 使用简单粗暴的删除再创建方式实现更新, 以控制AnalysisPlanTask的定义全部在createPlanAnalysisTask方法中 +// updatePlanAnalysisTask 使用更安全的方式更新触发器任务 func (r *gormPlanRepository) updatePlanAnalysisTask(tx *gorm.DB, plan *models.Plan) error { - task, err := r.findPlanAnalysisTaskByParamsPlanID(tx, plan.ID) - if err != nil { - return err + task, err := r.findPlanAnalysisTask(tx, plan.ID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("查找现有计划分析任务失败: %w", err) } - err = r.deleteTask(tx, task.ID) - if err != nil { - return err + + // 如果触发器任务不存在,则创建一个 + if task == nil { + return r.createPlanAnalysisTask(tx, plan) } - return r.createPlanAnalysisTask(tx, plan) + + // 如果存在,则更新它的名称和描述以反映计划的最新信息 + task.Name = fmt.Sprintf("'%s'计划触发器", plan.Name) + task.Description = fmt.Sprintf("计划名: %s, 计划ID: %d", plan.Name, plan.ID) + + return tx.Save(task).Error +} + +func (r *gormPlanRepository) FindRunnablePlans() ([]*models.Plan, error) { + var plans []*models.Plan + err := r.db. + Where("status = ?", models.PlanStatusEnabled). + Where( + r.db.Where("execution_type = ?", models.PlanExecutionTypeManual). + Or("execution_type = ? AND (execute_num = 0 OR execute_count < execute_num)", models.PlanExecutionTypeAutomatic), + ). + Find(&plans).Error + return plans, err +} + +func (r *gormPlanRepository) FindDisabledAndStoppedPlans() ([]*models.Plan, error) { + var plans []*models.Plan + err := r.db. + Where("status = ? OR status = ?", models.PlanStatusDisabled, models.PlanStatusStopeed). + Find(&plans).Error + return plans, err +} + +// findPlanAnalysisTask 是一个内部使用的、更高效的查找方法 +func (r *gormPlanRepository) findPlanAnalysisTask(tx *gorm.DB, planID uint) (*models.Task, error) { + var task models.Task + err := tx.Where("plan_id = ? AND type = ?", planID, models.TaskPlanAnalysis).First(&task).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil // 未找到不是错误,返回nil, nil + } + return &task, err +} + +// FindPlanAnalysisTaskByPlanID 是暴露给外部的公共方法 +func (r *gormPlanRepository) FindPlanAnalysisTaskByPlanID(planID uint) (*models.Task, error) { + return r.findPlanAnalysisTask(r.db, planID) } -- 2.49.1 From 6711f55fba069defaf7c8dec4ef1d782a6748122 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 21:45:38 +0800 Subject: [PATCH 09/13] =?UTF-8?q?=E9=87=8D=E6=9E=84AnalysisPlanTaskManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../task/analysis_plan_task_manager.go | 48 ++++++++++--- .../repository/pending_task_repository.go | 13 +++- internal/infra/repository/plan_repository.go | 69 ++++++++++--------- 3 files changed, 85 insertions(+), 45 deletions(-) diff --git a/internal/app/service/task/analysis_plan_task_manager.go b/internal/app/service/task/analysis_plan_task_manager.go index 093490b..2fc1601 100644 --- a/internal/app/service/task/analysis_plan_task_manager.go +++ b/internal/app/service/task/analysis_plan_task_manager.go @@ -57,9 +57,9 @@ func (m *AnalysisPlanTaskManager) Refresh() error { m.logger.Errorf("清理无效任务时出错: %v", err) } - // 3. 为应执行但缺失的计划添加新触发器 - if err := m.addNewTriggers(runnablePlans, pendingTasks); err != nil { - return fmt.Errorf("添加新触发器时出错: %w", err) + // 3. 添加或更新触发器 + if err := m.addOrUpdateTriggers(runnablePlans, pendingTasks); err != nil { + return fmt.Errorf("添加或更新触发器时出错: %w", err) } m.logger.Info("计划任务管理器同步完成.") @@ -167,18 +167,38 @@ func (m *AnalysisPlanTaskManager) cleanupInvalidTasks(invalidPlanIDs []uint, all return nil } -// addNewTriggers 检查并为应执行但缺失的计划添加新触发器。 -func (m *AnalysisPlanTaskManager) addNewTriggers(runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error { - // 创建一个集合,存放所有已在队列中的计划触发器 - pendingTriggerPlanIDs := make(map[uint]struct{}) +// addOrUpdateTriggers 检查、更新或创建触发器。 +func (m *AnalysisPlanTaskManager) addOrUpdateTriggers(runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error { + // 创建一个映射,存放所有已在队列中的计划触发器 + pendingTriggersMap := make(map[uint]models.PendingTask) for _, pt := range allPendingTasks { if pt.Task != nil && pt.Task.Type == models.TaskPlanAnalysis { - pendingTriggerPlanIDs[pt.Task.PlanID] = struct{}{} + pendingTriggersMap[pt.Task.PlanID] = pt } } for _, plan := range runnablePlans { - if _, exists := pendingTriggerPlanIDs[plan.ID]; !exists { + existingTrigger, exists := pendingTriggersMap[plan.ID] + + if exists { + // --- 新增逻辑:检查并更新现有触发器 --- + // 只对自动计划检查时间更新 + if plan.ExecutionType == models.PlanExecutionTypeAutomatic { + next, err := utils.GetNextCronTime(plan.CronExpression) + if err != nil { + m.logger.Errorf("为计划 #%d 解析Cron表达式失败,跳过更新: %v", plan.ID, err) + continue + } + // 如果数据库中记录的执行时间与根据当前Cron表达式计算出的下一次时间不一致,则更新 + if !existingTrigger.ExecuteAt.Equal(next) { + m.logger.Infof("计划 #%d 的执行时间已变更,正在更新触发器 #%d 的执行时间从 %v 到 %v...", plan.ID, existingTrigger.ID, existingTrigger.ExecuteAt, next) + if err := m.pendingTaskRepo.UpdatePendingTaskExecuteAt(existingTrigger.ID, next); err != nil { + m.logger.Errorf("更新触发器 #%d 的执行时间失败: %v", existingTrigger.ID, err) + } + } + } + } else { + // --- 原有逻辑:为缺失的计划创建新触发器 --- m.logger.Infof("发现应执行但队列中缺失的计划 #%d,正在为其创建触发器...", plan.ID) if err := m.createTriggerTask(plan); err != nil { m.logger.Errorf("为计划 #%d 创建触发器失败: %v", plan.ID, err) @@ -195,8 +215,16 @@ func (m *AnalysisPlanTaskManager) createTriggerTask(plan *models.Plan) error { if err != nil { return fmt.Errorf("查找计划分析任务失败: %w", err) } + + // --- 如果触发器任务定义不存在,则自动创建 --- if analysisTask == nil { - return fmt.Errorf("未找到计划 #%d 关联的 'plan_analysis' 任务", plan.ID) + m.logger.Warnf("未找到计划 #%d 关联的 'plan_analysis' 任务定义,将自动创建...", plan.ID) + newAnalysisTask, err := m.planRepo.CreatePlanAnalysisTask(plan) + if err != nil { + return fmt.Errorf("自动创建 'plan_analysis' 任务定义失败: %w", err) + } + analysisTask = newAnalysisTask + m.logger.Infof("已成功为计划 #%d 创建 'plan_analysis' 任务定义 (ID: %d)", plan.ID, analysisTask.ID) } var executeAt time.Time diff --git a/internal/infra/repository/pending_task_repository.go b/internal/infra/repository/pending_task_repository.go index 166496b..cbce386 100644 --- a/internal/infra/repository/pending_task_repository.go +++ b/internal/infra/repository/pending_task_repository.go @@ -2,6 +2,7 @@ package repository import ( "errors" + "fmt" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -16,6 +17,10 @@ type PendingTaskRepository interface { DeletePendingTasksByIDs(ids []uint) error CreatePendingTask(task *models.PendingTask) error CreatePendingTasksInBatch(tasks []*models.PendingTask) error + + // UpdatePendingTaskExecuteAt 更新指定待执行任务的执行时间 + UpdatePendingTaskExecuteAt(id uint, executeAt time.Time) error + // ClaimNextAvailableTask 原子地认领下一个可用的任务。 // 它会同时返回被认领任务对应的日志对象,以及被删除的待办任务对象的内存副本。 ClaimNextAvailableTask(excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error) @@ -42,9 +47,10 @@ func (r *gormPendingTaskRepository) FindAllPendingTasks() ([]models.PendingTask, func (r *gormPendingTaskRepository) FindPendingTriggerByPlanID(planID uint) (*models.PendingTask, error) { var pendingTask models.PendingTask + // 关键修改:通过 JOIN tasks 表并查询 parameters JSON 字段来查找触发器,而不是依赖 task.plan_id err := r.db. Joins("JOIN tasks ON tasks.id = pending_tasks.task_id"). - Where("tasks.plan_id = ? AND tasks.type = ?", planID, models.TaskPlanAnalysis). + Where("tasks.type = ? AND tasks.parameters->>'plan_id' = ?", models.TaskPlanAnalysis, fmt.Sprintf("%d", planID)). First(&pendingTask).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil // 未找到不是错误 @@ -71,6 +77,11 @@ func (r *gormPendingTaskRepository) CreatePendingTasksInBatch(tasks []*models.Pe return r.db.Create(&tasks).Error } +// UpdatePendingTaskExecuteAt 更新指定待执行任务的执行时间 +func (r *gormPendingTaskRepository) UpdatePendingTaskExecuteAt(id uint, executeAt time.Time) error { + return r.db.Model(&models.PendingTask{}).Where("id = ?", id).Update("execute_at", executeAt).Error +} + // ClaimNextAvailableTask 以原子方式认领下一个可用的任务。 func (r *gormPendingTaskRepository) ClaimNextAvailableTask(excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error) { var log models.TaskExecutionLog diff --git a/internal/infra/repository/plan_repository.go b/internal/infra/repository/plan_repository.go index 66ebd8d..ee2e760 100644 --- a/internal/infra/repository/plan_repository.go +++ b/internal/infra/repository/plan_repository.go @@ -48,6 +48,9 @@ type PlanRepository interface { FindDisabledAndStoppedPlans() ([]*models.Plan, error) // FindPlanAnalysisTaskByPlanID 根据 PlanID 找到其关联的 'plan_analysis' 任务 FindPlanAnalysisTaskByPlanID(planID uint) (*models.Task, error) + + // CreatePlanAnalysisTask 创建一个 plan_analysis 类型的任务并返回它 + CreatePlanAnalysisTask(plan *models.Plan) (*models.Task, error) } // gormPlanRepository 是 PlanRepository 的 GORM 实现 @@ -180,7 +183,9 @@ func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error { } // 3. 创建触发器Task - if err := r.createPlanAnalysisTask(tx, plan); err != nil { + // 关键修改:调用 createPlanAnalysisTask 并处理其返回的 Task 对象 + _, err := r.createPlanAnalysisTask(tx, plan) + if err != nil { return err } return nil @@ -523,52 +528,33 @@ func (r *gormPlanRepository) deleteTask(tx *gorm.DB, id int) error { // FindPlanAnalysisTaskByParamsPlanID 根据Parameters中的ParamsPlanID字段值查找TaskPlanAnalysis类型的Task func (r *gormPlanRepository) FindPlanAnalysisTaskByParamsPlanID(paramsPlanID uint) (*models.Task, error) { - return r.findPlanAnalysisTaskByParamsPlanID(r.db, paramsPlanID) -} - -// findPlanAnalysisTaskByParamsPlanID 使用指定db根据Parameters中的ParamsPlanID字段值查找TaskPlanAnalysis类型的Task -func (r *gormPlanRepository) findPlanAnalysisTaskByParamsPlanID(tx *gorm.DB, paramsPlanID uint) (*models.Task, error) { - var task models.Task - - // 构造JSON查询条件,查找Parameters中包含指定ParamsPlanID且Type为TaskPlanAnalysis的任务 - // TODO 在JSON字段中查找特定键值的语法取决于数据库类型,这里使用PostgreSQL的语法 - // TODO 如果使用的是MySQL,则需要相应调整查询条件 - result := tx.Where( - "type = ? AND parameters->>'plan_id' = ?", - models.TaskPlanAnalysis, - fmt.Sprintf("%d", paramsPlanID), - ).First(&task) - - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("未找到Parameters.PlanID为%d的TaskPlanAnalysis类型任务", paramsPlanID) - } - return nil, fmt.Errorf("查找任务时出错: %w", result.Error) - } - - return &task, nil + return r.findPlanAnalysisTask(r.db, paramsPlanID) } // createPlanAnalysisTask 用于创建一个TaskPlanAnalysis类型的Task -func (r *gormPlanRepository) createPlanAnalysisTask(tx *gorm.DB, plan *models.Plan) error { +// 关键修改:Task.PlanID 设置为 0,实际 PlanID 存储在 Parameters 中,并返回创建的 Task +func (r *gormPlanRepository) createPlanAnalysisTask(tx *gorm.DB, plan *models.Plan) (*models.Task, error) { m := map[string]interface{}{ models.ParamsPlanID: plan.ID, } parameters, err := json.Marshal(m) if err != nil { - return err + return nil, err } task := &models.Task{ - PlanID: plan.ID, - Name: fmt.Sprintf("'%v'计划触发器", plan.Name), - Description: fmt.Sprintf("计划名: %v, 计划ID: %v", plan.Name, plan.ID), - ExecutionOrder: 0, + PlanID: 0, // 关键:设置为 0,避免被常规 PlanID 查询查到 + Name: fmt.Sprintf("'%s'计划触发器", plan.Name), + Description: fmt.Sprintf("计划名: %s, 计划ID: %d", plan.Name, plan.ID), + ExecutionOrder: 0, // 触发器任务的执行顺序通常为0或不关心 Type: models.TaskPlanAnalysis, Parameters: datatypes.JSON(parameters), } - return tx.Create(task).Error + if err := tx.Create(task).Error; err != nil { + return nil, err + } + return task, nil } // updatePlanAnalysisTask 使用更安全的方式更新触发器任务 @@ -580,7 +566,8 @@ func (r *gormPlanRepository) updatePlanAnalysisTask(tx *gorm.DB, plan *models.Pl // 如果触发器任务不存在,则创建一个 if task == nil { - return r.createPlanAnalysisTask(tx, plan) + _, err := r.createPlanAnalysisTask(tx, plan) + return err } // 如果存在,则更新它的名称和描述以反映计划的最新信息 @@ -611,9 +598,10 @@ func (r *gormPlanRepository) FindDisabledAndStoppedPlans() ([]*models.Plan, erro } // findPlanAnalysisTask 是一个内部使用的、更高效的查找方法 +// 关键修改:通过查询 parameters JSON 字段来查找 func (r *gormPlanRepository) findPlanAnalysisTask(tx *gorm.DB, planID uint) (*models.Task, error) { var task models.Task - err := tx.Where("plan_id = ? AND type = ?", planID, models.TaskPlanAnalysis).First(&task).Error + err := tx.Where("type = ? AND parameters->>'plan_id' = ?", models.TaskPlanAnalysis, fmt.Sprintf("%d", planID)).First(&task).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil // 未找到不是错误,返回nil, nil } @@ -621,6 +609,19 @@ func (r *gormPlanRepository) findPlanAnalysisTask(tx *gorm.DB, planID uint) (*mo } // FindPlanAnalysisTaskByPlanID 是暴露给外部的公共方法 +// 关键修改:通过查询 parameters JSON 字段来查找 func (r *gormPlanRepository) FindPlanAnalysisTaskByPlanID(planID uint) (*models.Task, error) { return r.findPlanAnalysisTask(r.db, planID) } + +// CreatePlanAnalysisTask 创建一个 plan_analysis 类型的任务并返回它 +// 这个方法是公开的,主要由 TaskManager 在发现触发器任务定义丢失时调用。 +func (r *gormPlanRepository) CreatePlanAnalysisTask(plan *models.Plan) (*models.Task, error) { + var createdTask *models.Task + err := r.db.Transaction(func(tx *gorm.DB) error { + var err error + createdTask, err = r.createPlanAnalysisTask(tx, plan) + return err + }) + return createdTask, err +} -- 2.49.1 From 056279bdc22dfffc87429a8b42bfe07d87c619ab Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 22:32:00 +0800 Subject: [PATCH 10/13] =?UTF-8?q?=E9=87=8D=E6=9E=84AnalysisPlanTaskManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/controller/plan/plan_controller.go | 12 +-- .../task/analysis_plan_task_manager.go | 65 +++++++++++++++- internal/app/service/task/scheduler.go | 74 ++++++++++++------- 3 files changed, 116 insertions(+), 35 deletions(-) diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index fc5bb08..5eb85bd 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -138,10 +138,10 @@ func (c *Controller) CreatePlan(ctx *gin.Context) { return } - // 创建成功后,调用 manager 创建或更新触发器 - if err := c.analysisPlanTaskManager.CreateOrUpdateTrigger(ctx, planToCreate.ID); err != nil { + // 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列 + if err := c.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil { // 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功 - c.logger.Errorf("为新创建的计划 %d 创建触发器失败: %v", planToCreate.ID, err) + c.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err) } // 使用已有的转换函数将创建后的模型转换为响应对象 @@ -287,10 +287,10 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { return } - // 更新成功后,调用 manager 创建或更新触发器 - if err := c.analysisPlanTaskManager.CreateOrUpdateTrigger(ctx, planToUpdate.ID); err != nil { + // 更新成功后,调用 manager 确保触发器任务定义存在 + if err := c.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil { // 这是一个非阻塞性错误,我们只记录日志 - c.logger.Errorf("为更新后的计划 %d 更新触发器失败: %v", planToUpdate.ID, err) + c.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err) } // 6. 获取更新后的完整计划用于响应 diff --git a/internal/app/service/task/analysis_plan_task_manager.go b/internal/app/service/task/analysis_plan_task_manager.go index 2fc1601..6576239 100644 --- a/internal/app/service/task/analysis_plan_task_manager.go +++ b/internal/app/service/task/analysis_plan_task_manager.go @@ -68,6 +68,7 @@ func (m *AnalysisPlanTaskManager) Refresh() error { // CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。 // 这个方法是幂等的:如果一个有效的触发器已存在,它将不会重复创建。 +// 关键修改:如果触发器已存在,会根据计划类型更新其执行时间。 func (m *AnalysisPlanTaskManager) CreateOrUpdateTrigger(planID uint) error { m.mu.Lock() defer m.mu.Unlock() @@ -78,23 +79,79 @@ func (m *AnalysisPlanTaskManager) CreateOrUpdateTrigger(planID uint) error { return fmt.Errorf("获取计划基本信息失败: %w", err) } if plan.Status != models.PlanStatusEnabled { - return fmt.Errorf("计划 #%d 当前状态为 '%d',无法创建触发器", planID, plan.Status) + return fmt.Errorf("计划 #%d 当前状态为 '%d',无法创建或更新触发器", planID, plan.Status) } - // 幂等性检查:如果触发器已存在,则直接返回 + // 查找现有触发器 existingTrigger, err := m.pendingTaskRepo.FindPendingTriggerByPlanID(planID) if err != nil { return fmt.Errorf("查找现有触发器失败: %w", err) } + + // 如果触发器已存在,则根据计划类型更新其执行时间 if existingTrigger != nil { - m.logger.Infof("计划 #%d 的触发器已存在于待执行队列中,无需重复创建。", planID) - return nil + var expectedExecuteAt time.Time + if plan.ExecutionType == models.PlanExecutionTypeManual { + // 手动计划,如果再次触发,则立即执行 + expectedExecuteAt = time.Now() + } else { // 自动计划 + // 自动计划,根据 Cron 表达式计算下一次执行时间 + next, err := utils.GetNextCronTime(plan.CronExpression) + if err != nil { + m.logger.Errorf("为计划 #%d 解析Cron表达式失败,无法更新触发器: %v", plan.ID, err) + return fmt.Errorf("解析 Cron 表达式失败: %w", err) + } + expectedExecuteAt = next + } + + // 如果计算出的执行时间与当前待执行任务的时间不一致,则更新 + if !existingTrigger.ExecuteAt.Equal(expectedExecuteAt) { + m.logger.Infof("计划 #%d 的执行时间已变更,正在更新触发器 #%d 的执行时间从 %v 到 %v...", plan.ID, existingTrigger.ID, existingTrigger.ExecuteAt, expectedExecuteAt) + if err := m.pendingTaskRepo.UpdatePendingTaskExecuteAt(existingTrigger.ID, expectedExecuteAt); err != nil { + m.logger.Errorf("更新触发器 #%d 的执行时间失败: %v", existingTrigger.ID, err) + return fmt.Errorf("更新触发器执行时间失败: %w", err) + } + } else { + m.logger.Infof("计划 #%d 的触发器已存在且执行时间无需更新。", plan.ID) + } + return nil // 触发器已存在且已处理更新,直接返回 } + // 如果触发器不存在,则创建新的触发器 m.logger.Infof("为计划 #%d 创建新的触发器...", planID) return m.createTriggerTask(plan) } +// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。 +// 如果不存在,则会自动创建。此方法不涉及待执行队列。 +func (m *AnalysisPlanTaskManager) EnsureAnalysisTaskDefinition(planID uint) error { + m.mu.Lock() + defer m.mu.Unlock() + + plan, err := m.planRepo.GetBasicPlanByID(planID) + if err != nil { + return fmt.Errorf("确保分析任务定义失败:获取计划 #%d 基本信息时出错: %w", planID, err) + } + + analysisTask, err := m.planRepo.FindPlanAnalysisTaskByPlanID(plan.ID) + if err != nil { + return fmt.Errorf("确保分析任务定义失败:查找计划 #%d 的分析任务时出错: %w", plan.ID, err) + } + + if analysisTask == nil { + m.logger.Infof("未找到计划 #%d 关联的 'plan_analysis' 任务定义,将自动创建...", plan.ID) + _, err := m.planRepo.CreatePlanAnalysisTask(plan) // CreatePlanAnalysisTask returns *models.Task, error + if err != nil { + return fmt.Errorf("自动创建 'plan_analysis' 任务定义失败: %w", err) + } + m.logger.Infof("已成功为计划 #%d 创建 'plan_analysis' 任务定义。", plan.ID) + } else { + m.logger.Infof("计划 #%d 的 'plan_analysis' 任务定义已存在。", plan.ID) + } + + return nil +} + // --- 内部私有方法 --- // getRefreshData 从数据库获取刷新所需的所有数据。 diff --git a/internal/app/service/task/scheduler.go b/internal/app/service/task/scheduler.go index 18523bf..e12300e 100644 --- a/internal/app/service/task/scheduler.go +++ b/internal/app/service/task/scheduler.go @@ -1,7 +1,7 @@ package task import ( - "context" + "encoding/json" "errors" "sync" "time" @@ -112,10 +112,9 @@ type Scheduler struct { progressTracker *ProgressTracker taskFactory func(taskType models.TaskType) Task // 调度器需要注入一个任务工厂,用于创建任务实例 - pool *ants.Pool // 使用 ants 协程池来管理并发 - wg sync.WaitGroup - ctx context.Context - cancel context.CancelFunc + pool *ants.Pool // 使用 ants 协程池来管理并发 + wg sync.WaitGroup + stopChan chan struct{} // 用于停止主循环的信号通道 } // NewScheduler 创建一个新的调度器实例 @@ -128,8 +127,6 @@ func NewScheduler( logger *logs.Logger, interval time.Duration, numWorkers int) *Scheduler { - ctx, cancel := context.WithCancel(context.Background()) - return &Scheduler{ pendingTaskRepo: pendingTaskRepo, executionLogRepo: executionLogRepo, @@ -140,8 +137,7 @@ func NewScheduler( workers: numWorkers, progressTracker: NewProgressTracker(), taskFactory: taskFactory, - ctx: ctx, - cancel: cancel, + stopChan: make(chan struct{}), // 初始化停止信号通道 } } @@ -164,9 +160,9 @@ func (s *Scheduler) Start() { // Stop 优雅地停止调度器 func (s *Scheduler) Stop() { s.logger.Warnf("正在停止任务调度器...") - s.cancel() // 1. 发出取消信号,停止主循环 - s.wg.Wait() // 2. 等待主循环完成 - s.pool.Release() // 3. 释放 ants 池 (等待所有已提交的任务执行完毕) + close(s.stopChan) // 1. 发出停止信号,停止主循环 + s.wg.Wait() // 2. 等待主循环完成 + s.pool.Release() // 3. 释放 ants 池 (等待所有已提交的任务执行完毕) s.logger.Warnf("任务调度器已安全停止") } @@ -178,9 +174,11 @@ func (s *Scheduler) run() { for { select { - case <-s.ctx.Done(): + case <-s.stopChan: + // 收到停止信号,退出循环 return case <-ticker.C: + // 定时触发任务认领和提交 go s.claimAndSubmit() } } @@ -256,10 +254,26 @@ func (s *Scheduler) processTask(claimedLog *models.TaskExecutionLog) { // 任务计数器校验, Plan的任务全部执行完成后需要插入一个新的PlanAnalysisTask用于触发下一次Plan的执行 if s.progressTracker.IsPlanOver(claimedLog.PlanExecutionLogID) { - // 调用共享的 Manager 来处理触发器更新逻辑 - err = s.analysisPlanTaskManager.CreateOrUpdateTrigger(s.ctx, claimedLog.Task.PlanID) + // --- 新增逻辑:更新计划执行次数并判断是否需要触发下一次执行 --- + planID := claimedLog.Task.PlanID + + // 获取计划的最新数据 + plan, err := s.planRepo.GetBasicPlanByID(planID) if err != nil { - s.logger.Errorf("[严重] 创建计划分析任务失败, 当前Plan(%v)将无法进行下次触发, 错误: %v", claimedLog.Task.PlanID, err) + s.logger.Errorf("获取计划 %d 的基本信息失败: %v", planID, err) + return + } + + // 更新计划的执行计数器 + plan.ExecuteCount++ + if err := s.planRepo.UpdatePlan(plan); err != nil { // UpdatePlan 可以更新整个 Plan 对象 + s.logger.Errorf("更新计划 %d 的执行计数失败: %v", planID, err) + return + } + + // 调用共享的 Manager 来处理触发器更新逻辑 + if err := s.analysisPlanTaskManager.CreateOrUpdateTrigger(planID); err != nil { + s.logger.Errorf("为计划 %d 创建/更新触发器失败: %v", planID, err) } } @@ -301,8 +315,18 @@ func (s *Scheduler) runTask(claimedLog *models.TaskExecutionLog) error { // analysisPlan 解析Plan并将解析出的Task列表插入待执行队列中 func (s *Scheduler) analysisPlan(claimedLog *models.TaskExecutionLog) error { // 创建Plan执行记录 + // 从任务的 Parameters 中解析出真实的 PlanID + var params struct { + PlanID uint `json:"plan_id"` + } + if err := json.Unmarshal(claimedLog.Task.Parameters, ¶ms); err != nil { + s.logger.Errorf("解析任务参数中的计划ID失败,日志ID: %d, 错误: %v", claimedLog.ID, err) + return err + } + realPlanID := params.PlanID + planLog := &models.PlanExecutionLog{ - PlanID: claimedLog.Task.PlanID, + PlanID: realPlanID, // 使用从参数中解析出的真实 PlanID Status: models.ExecutionStatusStarted, StartedAt: time.Now(), } @@ -312,7 +336,7 @@ func (s *Scheduler) analysisPlan(claimedLog *models.TaskExecutionLog) error { } // 解析出Task列表 - tasks, err := s.planRepo.FlattenPlanTasks(claimedLog.Task.PlanID) + tasks, err := s.planRepo.FlattenPlanTasks(realPlanID) if err != nil { s.logger.Errorf("[严重] 解析计划失败, 日志ID: %d, 错误: %v", claimedLog.ID, err) return err @@ -320,12 +344,12 @@ func (s *Scheduler) analysisPlan(claimedLog *models.TaskExecutionLog) error { // 写入执行历史 taskLogs := make([]*models.TaskExecutionLog, len(tasks)) - for _, task := range tasks { - taskLogs = append(taskLogs, &models.TaskExecutionLog{ + for i, task := range tasks { + taskLogs[i] = &models.TaskExecutionLog{ PlanExecutionLogID: planLog.ID, TaskID: task.ID, Status: models.ExecutionStatusWaiting, - }) + } } err = s.executionLogRepo.CreateTaskExecutionLogsInBatch(taskLogs) @@ -337,13 +361,13 @@ func (s *Scheduler) analysisPlan(claimedLog *models.TaskExecutionLog) error { // 写入待执行队列 pendingTasks := make([]*models.PendingTask, len(tasks)) for i, task := range tasks { - pendingTasks = append(pendingTasks, &models.PendingTask{ + pendingTasks[i] = &models.PendingTask{ TaskID: task.ID, - TaskExecutionLogID: pendingTasks[i].ID, + TaskExecutionLogID: taskLogs[i].ID, // 使用正确的 TaskExecutionLogID // 待执行队列是通过任务触发时间排序的, 且只要在调度器获取的时间点之前的都可以被触发 ExecuteAt: time.Now().Add(time.Duration(i) * time.Second), - }) + } } err = s.pendingTaskRepo.CreatePendingTasksInBatch(pendingTasks) if err != nil { @@ -352,7 +376,7 @@ func (s *Scheduler) analysisPlan(claimedLog *models.TaskExecutionLog) error { } // 将Task列表加入待执行队列中 - s.progressTracker.AddNewPlan(claimedLog.PlanExecutionLogID, len(tasks)) + s.progressTracker.AddNewPlan(planLog.ID, len(tasks)) return nil } -- 2.49.1 From b0eb135f440613b49957b641a526d81b26d8afae Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 22:32:39 +0800 Subject: [PATCH 11/13] =?UTF-8?q?=E9=87=8D=E6=9E=84AnalysisPlanTaskManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/task/scheduler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/service/task/scheduler.go b/internal/app/service/task/scheduler.go index e12300e..6c8978e 100644 --- a/internal/app/service/task/scheduler.go +++ b/internal/app/service/task/scheduler.go @@ -234,7 +234,7 @@ func (s *Scheduler) handleRequeue(planExecutionLogID uint, taskToRequeue *models s.logger.Warnf("任务 (原始ID: %d) 已成功重新入队,并已释放计划 %d 的锁。", taskToRequeue.ID, planExecutionLogID) } -// processTask 处理单个任务的逻辑 (当前为占位符) +// processTask 处理单个任务的逻辑 func (s *Scheduler) processTask(claimedLog *models.TaskExecutionLog) { s.logger.Warnf("开始处理任务, 日志ID: %d, 任务ID: %d, 任务名称: %s", claimedLog.ID, claimedLog.TaskID, claimedLog.Task.Name) -- 2.49.1 From 74e42de7aaf4ca1e63644466bdba7711306d3e70 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 22:41:03 +0800 Subject: [PATCH 12/13] =?UTF-8?q?=E9=87=8D=E6=9E=84AnalysisPlanTaskManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/task/scheduler.go | 18 ++++++++++++++++-- .../repository/execution_log_repository.go | 7 +++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/internal/app/service/task/scheduler.go b/internal/app/service/task/scheduler.go index 6c8978e..b8e7e05 100644 --- a/internal/app/service/task/scheduler.go +++ b/internal/app/service/task/scheduler.go @@ -266,12 +266,26 @@ func (s *Scheduler) processTask(claimedLog *models.TaskExecutionLog) { // 更新计划的执行计数器 plan.ExecuteCount++ + + // 如果是自动计划且达到执行次数上限,则更新计划状态为已停止 + if (plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteNum > 0 && plan.ExecuteCount >= plan.ExecuteNum) || plan.ExecutionType == models.PlanExecutionTypeManual { + plan.Status = models.PlanStatusStopeed + s.logger.Infof("计划 %d (自动执行) 已达到最大执行次数 %d,状态更新为 '执行完毕'。", planID, plan.ExecuteNum) + } + + // 保存更新后的计划状态和执行计数 if err := s.planRepo.UpdatePlan(plan); err != nil { // UpdatePlan 可以更新整个 Plan 对象 - s.logger.Errorf("更新计划 %d 的执行计数失败: %v", planID, err) + s.logger.Errorf("更新计划 %d 的执行计数和状态失败: %v", planID, err) return } - // 调用共享的 Manager 来处理触发器更新逻辑 + // 更新计划执行日志状态为完成 + if err := s.executionLogRepo.UpdatePlanExecutionLogStatus(claimedLog.PlanExecutionLogID, models.ExecutionStatusCompleted); err != nil { + s.logger.Errorf("更新计划执行日志 %d 状态为 '完成' 失败: %v", claimedLog.PlanExecutionLogID, err) + // 这是一个非阻塞性错误,不中断后续流程 + } + + // 调用共享的 Manager 来处理触发器更新逻辑 (Manager 会根据最新的 Plan 状态决定是否创建新触发器) if err := s.analysisPlanTaskManager.CreateOrUpdateTrigger(planID); err != nil { s.logger.Errorf("为计划 %d 创建/更新触发器失败: %v", planID, err) } diff --git a/internal/infra/repository/execution_log_repository.go b/internal/infra/repository/execution_log_repository.go index d0e4f81..4111901 100644 --- a/internal/infra/repository/execution_log_repository.go +++ b/internal/infra/repository/execution_log_repository.go @@ -16,6 +16,8 @@ type ExecutionLogRepository interface { CreateTaskExecutionLogsInBatch(logs []*models.TaskExecutionLog) error UpdateTaskExecutionLog(log *models.TaskExecutionLog) error FindTaskExecutionLogByID(id uint) (*models.TaskExecutionLog, error) + // UpdatePlanExecutionLogStatus 更新计划执行日志的状态 + UpdatePlanExecutionLogStatus(logID uint, status models.ExecutionStatus) error } // gormExecutionLogRepository 是使用 GORM 的具体实现。 @@ -86,3 +88,8 @@ func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(id uint) (*models. } return &log, nil } + +// UpdatePlanExecutionLogStatus 更新计划执行日志的状态 +func (r *gormExecutionLogRepository) UpdatePlanExecutionLogStatus(logID uint, status models.ExecutionStatus) error { + return r.db.Model(&models.PlanExecutionLog{}).Where("id = ?", logID).Update("status", status).Error +} -- 2.49.1 From 769a0432c8daafab4f2fb8accd40734145f054b1 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 23:50:27 +0800 Subject: [PATCH 13/13] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=BE=85?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E9=98=9F=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/application.go | 120 ++++++++++++++++++ .../repository/execution_log_repository.go | 21 +++ .../repository/pending_task_repository.go | 11 +- internal/infra/repository/plan_repository.go | 18 +++ 4 files changed, 169 insertions(+), 1 deletion(-) diff --git a/internal/core/application.go b/internal/core/application.go index 0c36568..63c8db1 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -25,6 +25,12 @@ type Application struct { Storage database.Storage Executor *task.Scheduler API *api.API // 添加 API 对象 + + // 新增的仓库和管理器字段,以便在 initializePendingTasks 中访问 + planRepo repository.PlanRepository + pendingTaskRepo repository.PendingTaskRepository + executionLogRepo repository.ExecutionLogRepository + analysisPlanTaskManager *task.AnalysisPlanTaskManager } // NewApplication 创建并初始化一个新的 Application 实例。 @@ -82,6 +88,11 @@ func NewApplication(configPath string) (*Application, error) { Storage: storage, Executor: executor, API: apiServer, + // 填充新增的字段 + planRepo: planRepo, + pendingTaskRepo: pendingTaskRepo, + executionLogRepo: executionLogRepo, + analysisPlanTaskManager: analysisPlanTaskManager, } return app, nil @@ -91,6 +102,17 @@ func NewApplication(configPath string) (*Application, error) { func (app *Application) Start() error { app.Logger.Info("应用启动中...") + // --- 新增逻辑:初始化待执行任务列表 --- + if err := app.initializePendingTasks( + app.planRepo, // 传入 planRepo + app.pendingTaskRepo, // 传入 pendingTaskRepo + app.executionLogRepo, // 传入 executionLogRepo + app.analysisPlanTaskManager, // 传入 analysisPlanTaskManager + app.Logger, // 传入 logger + ); err != nil { + return fmt.Errorf("初始化待执行任务列表失败: %w", err) + } + // 启动任务执行器 app.Executor.Start() @@ -128,6 +150,104 @@ func (app *Application) Stop() error { return nil } +// initializePendingTasks 在应用启动时清理并刷新待执行任务列表。 +func (app *Application) initializePendingTasks( + planRepo repository.PlanRepository, + pendingTaskRepo repository.PendingTaskRepository, + executionLogRepo repository.ExecutionLogRepository, + analysisPlanTaskManager *task.AnalysisPlanTaskManager, + logger *logs.Logger, +) error { + logger.Info("开始初始化待执行任务列表...") + + // 阶段一:修正因崩溃导致状态不一致的固定次数计划 + logger.Info("阶段一:开始修正因崩溃导致状态不一致的固定次数计划...") + plansToCorrect, err := planRepo.FindPlansWithPendingTasks() + if err != nil { + return fmt.Errorf("查找需要修正的计划失败: %w", err) + } + + for _, plan := range plansToCorrect { + logger.Infof("发现需要修正的计划 #%d (名称: %s)。", plan.ID, plan.Name) + + // 更新计划的执行计数 + plan.ExecuteCount++ + logger.Infof("计划 #%d 执行计数已从 %d 更新为 %d。", plan.ID, plan.ExecuteCount-1, plan.ExecuteCount) + + if plan.ExecutionType == models.PlanExecutionTypeManual || + (plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteCount >= plan.ExecuteNum) { + // 更新计划状态为已停止 + plan.Status = models.PlanStatusStopeed + logger.Infof("计划 #%d 状态已更新为 '执行完毕'。", plan.ID) + + } + // 保存更新后的计划 + if err := planRepo.UpdatePlan(plan); err != nil { + logger.Errorf("修正计划 #%d 状态失败: %v", plan.ID, err) + // 这是一个非阻塞性错误,继续处理其他计划 + } + } + logger.Info("阶段一:固定次数计划修正完成。") + + // 阶段二:清理所有待执行任务和相关日志 + logger.Info("阶段二:开始清理所有待执行任务和相关日志...") + pendingTasks, err := pendingTaskRepo.FindAllPendingTasks() + if err != nil { + return fmt.Errorf("获取待执行任务失败: %w", err) + } + + var taskLogIDsToCancel []uint + var planLogIDsToFail []uint + + for _, pt := range pendingTasks { + // 确保 Task 和 TaskExecutionLog 已预加载 + if pt.Task == nil || pt.TaskExecutionLog.ID == 0 { // TaskExecutionLog.ID为零说明没加载 + logger.Warnf("待执行任务 %d 缺少关联的 Task 或 TaskExecutionLog,跳过处理。", pt.ID) + continue + } + + // 收集任务执行日志ID,所有未完成的任务都标记为取消 + taskLogIDsToCancel = append(taskLogIDsToCancel, pt.TaskExecutionLog.ID) + + // 收集计划执行日志ID + if pt.TaskExecutionLog.PlanExecutionLogID != 0 { + planLogIDsToFail = append(planLogIDsToFail, pt.TaskExecutionLog.PlanExecutionLogID) + } + } + + // 批量更新 TaskExecutionLog 状态为取消 + if len(taskLogIDsToCancel) > 0 { + if err := executionLogRepo.UpdateLogStatusByIDs(taskLogIDsToCancel, models.ExecutionStatusCancelled); err != nil { + logger.Errorf("批量更新任务执行日志状态为取消失败: %v", err) + // 这是一个非阻塞性错误,继续执行 + } + } + + // 批量更新 PlanExecutionLog 状态为失败 + if len(planLogIDsToFail) > 0 { + if err := executionLogRepo.UpdatePlanExecutionLogsStatusByIDs(planLogIDsToFail, models.ExecutionStatusFailed); err != nil { + logger.Errorf("批量更新计划执行日志状态为失败失败: %v", err) + // 这是一个非阻塞性错误,继续执行 + } + } + + // 清空待执行列表 + if err := pendingTaskRepo.ClearAllPendingTasks(); err != nil { + return fmt.Errorf("清空待执行任务列表失败: %w", err) + } + logger.Info("阶段二:待执行任务和相关日志清理完成。") + + // 阶段三:初始刷新 + logger.Info("阶段三:开始刷新待执行列表...") + if err := analysisPlanTaskManager.Refresh(); err != nil { + return fmt.Errorf("刷新待执行任务列表失败: %w", err) + } + logger.Info("阶段三:待执行任务列表初始化完成。") + + logger.Info("待执行任务列表初始化完成。") + return nil +} + // initStorage 封装了数据库的初始化、连接和迁移逻辑。 func initStorage(cfg config.DatabaseConfig, logger *logs.Logger) (database.Storage, error) { // 创建存储实例 diff --git a/internal/infra/repository/execution_log_repository.go b/internal/infra/repository/execution_log_repository.go index 4111901..5a1cdd6 100644 --- a/internal/infra/repository/execution_log_repository.go +++ b/internal/infra/repository/execution_log_repository.go @@ -18,6 +18,12 @@ type ExecutionLogRepository interface { FindTaskExecutionLogByID(id uint) (*models.TaskExecutionLog, error) // UpdatePlanExecutionLogStatus 更新计划执行日志的状态 UpdatePlanExecutionLogStatus(logID uint, status models.ExecutionStatus) error + + // UpdatePlanExecutionLogsStatusByIDs 批量更新计划执行日志的状态 + UpdatePlanExecutionLogsStatusByIDs(logIDs []uint, status models.ExecutionStatus) error + + // FindIncompletePlanExecutionLogs 查找所有未完成的计划执行日志 + FindIncompletePlanExecutionLogs() ([]models.PlanExecutionLog, error) } // gormExecutionLogRepository 是使用 GORM 的具体实现。 @@ -93,3 +99,18 @@ func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(id uint) (*models. func (r *gormExecutionLogRepository) UpdatePlanExecutionLogStatus(logID uint, status models.ExecutionStatus) error { return r.db.Model(&models.PlanExecutionLog{}).Where("id = ?", logID).Update("status", status).Error } + +// UpdatePlanExecutionLogsStatusByIDs 批量更新计划执行日志的状态 +func (r *gormExecutionLogRepository) UpdatePlanExecutionLogsStatusByIDs(logIDs []uint, status models.ExecutionStatus) error { + if len(logIDs) == 0 { + return nil + } + return r.db.Model(&models.PlanExecutionLog{}).Where("id IN ?", logIDs).Update("status", status).Error +} + +// FindIncompletePlanExecutionLogs 查找所有未完成的计划执行日志 +func (r *gormExecutionLogRepository) FindIncompletePlanExecutionLogs() ([]models.PlanExecutionLog, error) { + var logs []models.PlanExecutionLog + err := r.db.Where("status = ?", models.ExecutionStatusStarted).Find(&logs).Error + return logs, err +} diff --git a/internal/infra/repository/pending_task_repository.go b/internal/infra/repository/pending_task_repository.go index cbce386..eb3d944 100644 --- a/internal/infra/repository/pending_task_repository.go +++ b/internal/infra/repository/pending_task_repository.go @@ -21,6 +21,9 @@ type PendingTaskRepository interface { // UpdatePendingTaskExecuteAt 更新指定待执行任务的执行时间 UpdatePendingTaskExecuteAt(id uint, executeAt time.Time) error + // ClearAllPendingTasks 清空所有待执行任务 + ClearAllPendingTasks() error + // ClaimNextAvailableTask 原子地认领下一个可用的任务。 // 它会同时返回被认领任务对应的日志对象,以及被删除的待办任务对象的内存副本。 ClaimNextAvailableTask(excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error) @@ -41,7 +44,8 @@ func NewGormPendingTaskRepository(db *gorm.DB) PendingTaskRepository { func (r *gormPendingTaskRepository) FindAllPendingTasks() ([]models.PendingTask, error) { var tasks []models.PendingTask // 预加载 Task 以便后续访问 Task.PlanID - err := r.db.Preload("Task").Find(&tasks).Error + // 预加载 TaskExecutionLog 以便后续访问 PlanExecutionLogID + err := r.db.Preload("Task").Preload("TaskExecutionLog").Find(&tasks).Error return tasks, err } @@ -82,6 +86,11 @@ func (r *gormPendingTaskRepository) UpdatePendingTaskExecuteAt(id uint, executeA return r.db.Model(&models.PendingTask{}).Where("id = ?", id).Update("execute_at", executeAt).Error } +// ClearAllPendingTasks 清空所有待执行任务 +func (r *gormPendingTaskRepository) ClearAllPendingTasks() error { + return r.db.Where("1 = 1").Delete(&models.PendingTask{}).Error +} + // ClaimNextAvailableTask 以原子方式认领下一个可用的任务。 func (r *gormPendingTaskRepository) ClaimNextAvailableTask(excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error) { var log models.TaskExecutionLog diff --git a/internal/infra/repository/plan_repository.go b/internal/infra/repository/plan_repository.go index ee2e760..424ee21 100644 --- a/internal/infra/repository/plan_repository.go +++ b/internal/infra/repository/plan_repository.go @@ -51,6 +51,9 @@ type PlanRepository interface { // CreatePlanAnalysisTask 创建一个 plan_analysis 类型的任务并返回它 CreatePlanAnalysisTask(plan *models.Plan) (*models.Task, error) + + // FindPlansWithPendingTasks 查找所有正在执行的计划 + FindPlansWithPendingTasks() ([]*models.Plan, error) } // gormPlanRepository 是 PlanRepository 的 GORM 实现 @@ -625,3 +628,18 @@ func (r *gormPlanRepository) CreatePlanAnalysisTask(plan *models.Plan) (*models. }) return createdTask, err } + +// FindPlansWithPendingTasks 查找所有正在执行的计划 +func (r *gormPlanRepository) FindPlansWithPendingTasks() ([]*models.Plan, error) { + var plans []*models.Plan + + // 关联 pending_tasks, task_execution_logs, tasks 表来查找符合条件的计划 + err := r.db.Table("plans"). + Joins("JOIN tasks ON plans.id = tasks.plan_id"). + Joins("JOIN task_execution_logs ON tasks.id = task_execution_logs.task_id"). + Joins("JOIN pending_tasks ON task_execution_logs.id = pending_tasks.task_execution_log_id"). + Group("plans.id"). // 避免重复,因为一个计划可能有多个待执行任务 + Find(&plans).Error + + return plans, err +} -- 2.49.1