Merge pull request 'issue-5' (#8) from issue-5 into main

Reviewed-on: #8
This commit is contained in:
2025-09-21 12:43:59 +08:00
23 changed files with 1710 additions and 411 deletions

0
.swaggo Normal file
View File

View File

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

View File

@@ -12,3 +12,6 @@
4. 暂时不考虑和区域主控间的同步消息, 假设所有消息都是异步的, 这可能导致无法知道指令是否执行成功
5. 如果系统停机时间很长, 待执行任务表中的任务过期了怎么办, 目前没有任务过期机制
6. 可以用TimescaleDB代替PGSQL, 优化传感器数据存储性能
任务调度器执行触发器任务时要修改一下对应计划的执行次数(如果是指定次数的计划)

View File

@@ -15,7 +15,7 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/devices": {
"/api/v1/devices": {
"get": {
"description": "获取系统中所有设备的列表",
"produces": [
@@ -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,15 +74,27 @@ 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"
}
}
}
]
}
}
}
}
},
"/devices/{id}": {
"/api/v1/devices/{id}": {
"get": {
"description": "根据设备ID获取单个设备的详细信息",
"produces": [
@@ -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"
}
@@ -162,7 +213,7 @@ const docTemplate = `{
}
}
},
"/plans": {
"/api/v1/plans": {
"get": {
"description": "获取所有计划的列表",
"produces": [
@@ -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,15 +269,27 @@ 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"
}
}
}
]
}
}
}
}
},
"/plans/{id}": {
"/api/v1/plans/{id}": {
"get": {
"description": "根据计划ID获取单个计划的详细信息。",
"produces": [
@@ -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"
}
@@ -309,7 +408,7 @@ const docTemplate = `{
}
}
},
"/plans/{id}/start": {
"/api/v1/plans/{id}/start": {
"post": {
"description": "根据计划ID启动一个计划的执行。",
"produces": [
@@ -330,7 +429,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "业务失败具体错误码和信息见响应体例如400, 404, 500",
"description": "业务码为200代表成功启动计划",
"schema": {
"$ref": "#/definitions/controller.Response"
}
@@ -338,7 +437,7 @@ const docTemplate = `{
}
}
},
"/plans/{id}/stop": {
"/api/v1/plans/{id}/stop": {
"post": {
"description": "根据计划ID停止一个正在执行的计划。",
"produces": [
@@ -359,7 +458,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "业务失败具体错误码和信息见响应体例如400, 404, 500",
"description": "业务码为200代表成功停止计划",
"schema": {
"$ref": "#/definitions/controller.Response"
}
@@ -367,7 +466,7 @@ const docTemplate = `{
}
}
},
"/users": {
"/api/v1/users": {
"post": {
"description": "根据用户名和密码创建一个新的系统用户。",
"consumes": [
@@ -393,15 +492,27 @@ 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"
}
}
}
]
}
}
}
}
},
"/users/login": {
"/api/v1/users/login": {
"post": {
"description": "用户使用用户名和密码登录,成功后返回 JWT 令牌。",
"consumes": [
@@ -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"
}
}
}
]
}
}
}
@@ -437,9 +560,6 @@ const docTemplate = `{
}
},
"definitions": {
"controller.Properties": {
"type": "object"
},
"controller.Response": {
"type": "object",
"properties": {
@@ -457,17 +577,12 @@ const docTemplate = `{
}
},
"device.CreateDeviceRequest": {
"type": "object"
},
"device.DeviceResponse": {
"type": "object",
"required": [
"name",
"type"
],
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"location": {
"type": "string"
},
@@ -478,16 +593,14 @@ const docTemplate = `{
"type": "integer"
},
"properties": {
"$ref": "#/definitions/controller.Properties"
"type": "object",
"additionalProperties": true
},
"sub_type": {
"$ref": "#/definitions/models.DeviceSubType"
},
"type": {
"$ref": "#/definitions/models.DeviceType"
},
"updated_at": {
"type": "string"
}
}
},
@@ -508,7 +621,8 @@ const docTemplate = `{
"type": "integer"
},
"properties": {
"$ref": "#/definitions/controller.Properties"
"type": "object",
"additionalProperties": true
},
"sub_type": {
"$ref": "#/definitions/models.DeviceSubType"
@@ -518,6 +632,39 @@ const docTemplate = `{
}
}
},
"git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"location": {
"type": "string"
},
"name": {
"type": "string"
},
"parent_id": {
"type": "integer"
},
"properties": {
"type": "object",
"additionalProperties": true
},
"sub_type": {
"$ref": "#/definitions/models.DeviceSubType"
},
"type": {
"$ref": "#/definitions/models.DeviceType"
},
"updated_at": {
"type": "string"
}
}
},
"models.DeviceSubType": {
"type": "string",
"enum": [
@@ -588,18 +735,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"
]
},
@@ -627,6 +802,10 @@ const docTemplate = `{
"type": "string",
"example": "根据温度自动调节风扇和加热器"
},
"execute_num": {
"type": "integer",
"example": 10
},
"execution_type": {
"allOf": [
{
@@ -687,6 +866,14 @@ const docTemplate = `{
"type": "string",
"example": "根据温度自动调节风扇和加热器"
},
"execute_count": {
"type": "integer",
"example": 0
},
"execute_num": {
"type": "integer",
"example": 10
},
"execution_type": {
"allOf": [
{
@@ -703,6 +890,14 @@ const docTemplate = `{
"type": "string",
"example": "猪舍温度控制计划"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/models.PlanStatus"
}
],
"example": 0
},
"sub_plans": {
"type": "array",
"items": {
@@ -757,7 +952,8 @@ const docTemplate = `{
"example": "打开风扇"
},
"parameters": {
"$ref": "#/definitions/controller.Properties"
"type": "object",
"additionalProperties": true
},
"type": {
"allOf": [
@@ -789,7 +985,8 @@ const docTemplate = `{
"example": "打开风扇"
},
"parameters": {
"$ref": "#/definitions/controller.Properties"
"type": "object",
"additionalProperties": true
},
"plan_id": {
"type": "integer",
@@ -824,6 +1021,10 @@ const docTemplate = `{
"type": "string",
"example": "更新后的描述"
},
"execute_num": {
"type": "integer",
"example": 10
},
"execution_type": {
"allOf": [
{

View File

@@ -4,7 +4,7 @@
"contact": {}
},
"paths": {
"/devices": {
"/api/v1/devices": {
"get": {
"description": "获取系统中所有设备的列表",
"produces": [
@@ -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,15 +63,27 @@
],
"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"
}
}
}
]
}
}
}
}
},
"/devices/{id}": {
"/api/v1/devices/{id}": {
"get": {
"description": "根据设备ID获取单个设备的详细信息",
"produces": [
@@ -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"
}
@@ -151,7 +202,7 @@
}
}
},
"/plans": {
"/api/v1/plans": {
"get": {
"description": "获取所有计划的列表",
"produces": [
@@ -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,15 +258,27 @@
],
"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"
}
}
}
]
}
}
}
}
},
"/plans/{id}": {
"/api/v1/plans/{id}": {
"get": {
"description": "根据计划ID获取单个计划的详细信息。",
"produces": [
@@ -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"
}
@@ -298,7 +397,7 @@
}
}
},
"/plans/{id}/start": {
"/api/v1/plans/{id}/start": {
"post": {
"description": "根据计划ID启动一个计划的执行。",
"produces": [
@@ -319,7 +418,7 @@
],
"responses": {
"200": {
"description": "业务失败具体错误码和信息见响应体例如400, 404, 500",
"description": "业务码为200代表成功启动计划",
"schema": {
"$ref": "#/definitions/controller.Response"
}
@@ -327,7 +426,7 @@
}
}
},
"/plans/{id}/stop": {
"/api/v1/plans/{id}/stop": {
"post": {
"description": "根据计划ID停止一个正在执行的计划。",
"produces": [
@@ -348,7 +447,7 @@
],
"responses": {
"200": {
"description": "业务失败具体错误码和信息见响应体例如400, 404, 500",
"description": "业务码为200代表成功停止计划",
"schema": {
"$ref": "#/definitions/controller.Response"
}
@@ -356,7 +455,7 @@
}
}
},
"/users": {
"/api/v1/users": {
"post": {
"description": "根据用户名和密码创建一个新的系统用户。",
"consumes": [
@@ -382,15 +481,27 @@
],
"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"
}
}
}
]
}
}
}
}
},
"/users/login": {
"/api/v1/users/login": {
"post": {
"description": "用户使用用户名和密码登录,成功后返回 JWT 令牌。",
"consumes": [
@@ -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"
}
}
}
]
}
}
}
@@ -426,9 +549,6 @@
}
},
"definitions": {
"controller.Properties": {
"type": "object"
},
"controller.Response": {
"type": "object",
"properties": {
@@ -446,17 +566,12 @@
}
},
"device.CreateDeviceRequest": {
"type": "object"
},
"device.DeviceResponse": {
"type": "object",
"required": [
"name",
"type"
],
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"location": {
"type": "string"
},
@@ -467,16 +582,14 @@
"type": "integer"
},
"properties": {
"$ref": "#/definitions/controller.Properties"
"type": "object",
"additionalProperties": true
},
"sub_type": {
"$ref": "#/definitions/models.DeviceSubType"
},
"type": {
"$ref": "#/definitions/models.DeviceType"
},
"updated_at": {
"type": "string"
}
}
},
@@ -497,7 +610,8 @@
"type": "integer"
},
"properties": {
"$ref": "#/definitions/controller.Properties"
"type": "object",
"additionalProperties": true
},
"sub_type": {
"$ref": "#/definitions/models.DeviceSubType"
@@ -507,6 +621,39 @@
}
}
},
"git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"location": {
"type": "string"
},
"name": {
"type": "string"
},
"parent_id": {
"type": "integer"
},
"properties": {
"type": "object",
"additionalProperties": true
},
"sub_type": {
"$ref": "#/definitions/models.DeviceSubType"
},
"type": {
"$ref": "#/definitions/models.DeviceType"
},
"updated_at": {
"type": "string"
}
}
},
"models.DeviceSubType": {
"type": "string",
"enum": [
@@ -577,18 +724,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"
]
},
@@ -616,6 +791,10 @@
"type": "string",
"example": "根据温度自动调节风扇和加热器"
},
"execute_num": {
"type": "integer",
"example": 10
},
"execution_type": {
"allOf": [
{
@@ -676,6 +855,14 @@
"type": "string",
"example": "根据温度自动调节风扇和加热器"
},
"execute_count": {
"type": "integer",
"example": 0
},
"execute_num": {
"type": "integer",
"example": 10
},
"execution_type": {
"allOf": [
{
@@ -692,6 +879,14 @@
"type": "string",
"example": "猪舍温度控制计划"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/models.PlanStatus"
}
],
"example": 0
},
"sub_plans": {
"type": "array",
"items": {
@@ -746,7 +941,8 @@
"example": "打开风扇"
},
"parameters": {
"$ref": "#/definitions/controller.Properties"
"type": "object",
"additionalProperties": true
},
"type": {
"allOf": [
@@ -778,7 +974,8 @@
"example": "打开风扇"
},
"parameters": {
"$ref": "#/definitions/controller.Properties"
"type": "object",
"additionalProperties": true
},
"plan_id": {
"type": "integer",
@@ -813,6 +1010,10 @@
"type": "string",
"example": "更新后的描述"
},
"execute_num": {
"type": "integer",
"example": 10
},
"execution_type": {
"allOf": [
{

View File

@@ -1,6 +1,4 @@
definitions:
controller.Properties:
type: object
controller.Response:
properties:
code:
@@ -13,8 +11,44 @@ definitions:
type: string
type: object
device.CreateDeviceRequest:
properties:
location:
type: string
name:
type: string
parent_id:
type: integer
properties:
additionalProperties: true
type: object
sub_type:
$ref: '#/definitions/models.DeviceSubType'
type:
$ref: '#/definitions/models.DeviceType'
required:
- name
- type
type: object
device.DeviceResponse:
device.UpdateDeviceRequest:
properties:
location:
type: string
name:
type: string
parent_id:
type: integer
properties:
additionalProperties: true
type: object
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
@@ -27,7 +61,8 @@ definitions:
parent_id:
type: integer
properties:
$ref: '#/definitions/controller.Properties'
additionalProperties: true
type: object
sub_type:
$ref: '#/definitions/models.DeviceSubType'
type:
@@ -35,24 +70,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 +124,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:
@@ -129,6 +169,9 @@ definitions:
description:
example: 根据温度自动调节风扇和加热器
type: string
execute_num:
example: 10
type: integer
execution_type:
allOf:
- $ref: '#/definitions/models.PlanExecutionType'
@@ -171,6 +214,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 +230,10 @@ definitions:
name:
example: 猪舍温度控制计划
type: string
status:
allOf:
- $ref: '#/definitions/models.PlanStatus'
example: 0
sub_plans:
items:
$ref: '#/definitions/plan.SubPlanResponse'
@@ -219,7 +272,8 @@ definitions:
example: 打开风扇
type: string
parameters:
$ref: '#/definitions/controller.Properties'
additionalProperties: true
type: object
type:
allOf:
- $ref: '#/definitions/models.TaskType'
@@ -240,7 +294,8 @@ definitions:
example: 打开风扇
type: string
parameters:
$ref: '#/definitions/controller.Properties'
additionalProperties: true
type: object
plan_id:
example: 1
type: integer
@@ -261,6 +316,9 @@ definitions:
description:
example: 更新后的描述
type: string
execute_num:
example: 10
type: integer
execution_type:
allOf:
- $ref: '#/definitions/models.PlanExecutionType'
@@ -326,16 +384,23 @@ definitions:
info:
contact: {}
paths:
/devices:
/api/v1/devices:
get:
description: 获取系统中所有设备的列表
produces:
- 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:
- 设备管理
@@ -354,13 +419,18 @@ 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:
- 设备管理
/devices/{id}:
/api/v1/devices/{id}:
delete:
description: 根据设备ID删除一个设备软删除
parameters:
@@ -373,7 +443,7 @@ paths:
- application/json
responses:
"200":
description: 业务失败,具体错误码和信息见响应体
description: OK
schema:
$ref: '#/definitions/controller.Response'
summary: 删除设备
@@ -391,9 +461,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:
- 设备管理
@@ -417,22 +492,32 @@ 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:
- 设备管理
/plans:
/api/v1/plans:
get:
description: 获取所有计划的列表
produces:
- 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:
- 计划管理
@@ -451,13 +536,18 @@ 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:
- 计划管理
/plans/{id}:
/api/v1/plans/{id}:
delete:
description: 根据计划ID删除计划。
parameters:
@@ -470,7 +560,7 @@ paths:
- application/json
responses:
"200":
description: 业务失败具体错误码和信息见响应体例如400, 404, 500
description: 业务码为200代表删除成功
schema:
$ref: '#/definitions/controller.Response'
summary: 删除计划
@@ -488,9 +578,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:
- 计划管理
@@ -514,13 +609,18 @@ 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:
- 计划管理
/plans/{id}/start:
/api/v1/plans/{id}/start:
post:
description: 根据计划ID启动一个计划的执行。
parameters:
@@ -533,13 +633,13 @@ paths:
- application/json
responses:
"200":
description: 业务失败具体错误码和信息见响应体例如400, 404, 500
description: 业务码为200代表成功启动计划
schema:
$ref: '#/definitions/controller.Response'
summary: 启动计划
tags:
- 计划管理
/plans/{id}/stop:
/api/v1/plans/{id}/stop:
post:
description: 根据计划ID停止一个正在执行的计划。
parameters:
@@ -552,13 +652,13 @@ paths:
- application/json
responses:
"200":
description: 业务失败具体错误码和信息见响应体例如400, 404, 500
description: 业务码为200代表成功停止计划
schema:
$ref: '#/definitions/controller.Response'
summary: 停止计划
tags:
- 计划管理
/users:
/api/v1/users:
post:
consumes:
- application/json
@@ -574,13 +674,18 @@ 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:
- 用户管理
/users/login:
/api/v1/users/login:
post:
consumes:
- application/json
@@ -596,9 +701,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:
- 用户管理

65
go.mod
View File

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

85
go.sum
View File

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

View File

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

View File

@@ -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 ---
@@ -104,9 +117,8 @@ 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 "业务失败,具体错误码和信息见响应体"
// @Router /devices [post]
// @Success 200 {object} controller.Response{data=DeviceResponse}
// @Router /api/v1/devices [post]
func (c *Controller) CreateDevice(ctx *gin.Context) {
var req CreateDeviceRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
@@ -115,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 {
@@ -130,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
@@ -139,9 +165,8 @@ 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 "业务失败,具体错误码和信息见响应体"
// @Router /devices/{id} [get]
// @Success 200 {object} controller.Response{data=DeviceResponse}
// @Router /api/v1/devices/{id} [get]
func (c *Controller) GetDevice(ctx *gin.Context) {
deviceID := ctx.Param("id")
@@ -160,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
@@ -168,9 +200,8 @@ func (c *Controller) GetDevice(ctx *gin.Context) {
// @Description 获取系统中所有设备的列表
// @Tags 设备管理
// @Produce json
// @Success 200 {object} controller.Response{data=[]DeviceResponse} "业务码为200代表获取成功"
// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体"
// @Router /devices [get]
// @Success 200 {object} controller.Response{data=[]DeviceResponse}
// @Router /api/v1/devices [get]
func (c *Controller) ListDevices(ctx *gin.Context) {
devices, err := c.repo.ListAll()
if err != nil {
@@ -179,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
@@ -190,9 +228,8 @@ 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 "业务失败,具体错误码和信息见响应体"
// @Router /devices/{id} [put]
// @Success 200 {object} controller.Response{data=DeviceResponse}
// @Router /api/v1/devices/{id} [put]
func (c *Controller) UpdateDevice(ctx *gin.Context) {
deviceID := ctx.Param("id")
@@ -220,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 {
@@ -235,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
@@ -244,9 +295,8 @@ 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 "业务失败,具体错误码和信息见响应体"
// @Router /devices/{id} [delete]
// @Success 200 {object} controller.Response
// @Router /api/v1/devices/{id} [delete]
func (c *Controller) DeleteDevice(ctx *gin.Context) {
deviceID := ctx.Param("id")

View File

@@ -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{
@@ -17,6 +18,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,
}
@@ -25,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
}
}
@@ -33,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模型并进行业务规则验证
@@ -50,6 +62,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,
}
@@ -69,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
}
}
@@ -96,6 +112,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,
}
@@ -115,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
}
}
@@ -133,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{
@@ -147,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, &params); err != nil {
return TaskResponse{}, fmt.Errorf("parsing task parameters failed (ID: %d): %w", task.ID, err)
}
}
return TaskResponse{
@@ -166,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{
@@ -181,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
}

View File

@@ -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"`
@@ -32,6 +33,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"`
@@ -49,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"`
@@ -66,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 定义 ---
@@ -112,8 +117,7 @@ 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 /plans [post]
// @Router /api/v1/plans [post]
func (c *Controller) CreatePlan(ctx *gin.Context) {
var req CreatePlanRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
@@ -134,14 +138,19 @@ 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)
}
// 使用已有的转换函数将创建后的模型转换为响应对象
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)
@@ -154,8 +163,7 @@ 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 /plans/{id} [get]
// @Router /api/v1/plans/{id} [get]
func (c *Controller) GetPlan(ctx *gin.Context) {
// 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id")
@@ -180,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)
@@ -192,8 +205,7 @@ 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 /plans [get]
// @Router /api/v1/plans [get]
func (c *Controller) ListPlans(ctx *gin.Context) {
// 1. 调用仓库层获取所有计划
plans, err := c.planRepo.ListBasicPlans()
@@ -206,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. 构造并发送成功响应
@@ -226,8 +244,7 @@ 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 /plans/{id} [put]
// @Router /api/v1/plans/{id} [put]
func (c *Controller) UpdatePlan(ctx *gin.Context) {
// 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id")
@@ -270,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. 获取更新后的完整计划用于响应
@@ -285,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)
@@ -298,8 +320,7 @@ 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 /plans/{id} [delete]
// @Router /api/v1/plans/{id} [delete]
func (c *Controller) DeletePlan(ctx *gin.Context) {
// 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id")
@@ -327,8 +348,7 @@ 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 /plans/{id}/start [post]
// @Router /api/v1/plans/{id}/start [post]
func (c *Controller) StartPlan(ctx *gin.Context) {
// 占位符:此处应调用服务层或仓库层来启动计划
c.logger.Infof("收到启动计划请求 (占位符)")
@@ -342,8 +362,7 @@ 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 /plans/{id}/stop [post]
// @Router /api/v1/plans/{id}/stop [post]
func (c *Controller) StopPlan(ctx *gin.Context) {
// 占位符:此处应调用服务层或仓库层来停止计划
c.logger.Infof("收到停止计划请求 (占位符)")

View File

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

View File

@@ -59,8 +59,7 @@ 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 /users [post]
// @Router /api/v1/users [post]
func (c *Controller) CreateUser(ctx *gin.Context) {
var req CreateUserRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
@@ -103,8 +102,7 @@ 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 /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 {

View File

@@ -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,281 @@ 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.addOrUpdateTriggers(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 {
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 从数据库获取刷新所需的所有数据。
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
}
// 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 {
pendingTriggersMap[pt.Task.PlanID] = pt
}
}
for _, plan := range runnablePlans {
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)
// 继续处理下一个,不因单点失败而中断
}
}
}
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 {
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
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
}

View File

@@ -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()
}
}
@@ -236,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)
@@ -256,10 +254,40 @@ 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 (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)
return
}
// 更新计划执行日志状态为完成
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)
}
}
@@ -301,8 +329,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, &params); 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 +350,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 +358,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 +375,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 +390,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
}

View File

@@ -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) {
// 创建存储实例

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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;"`

View File

@@ -8,11 +8,22 @@ 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
UpdateTaskExecutionLog(log *models.TaskExecutionLog) error
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 的具体实现。
@@ -26,6 +37,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 +69,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
}
@@ -63,3 +94,23 @@ 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
}
// 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
}

View File

@@ -1,6 +1,8 @@
package repository
import (
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
@@ -10,7 +12,18 @@ 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
// UpdatePendingTaskExecuteAt 更新指定待执行任务的执行时间
UpdatePendingTaskExecuteAt(id uint, executeAt time.Time) error
// ClearAllPendingTasks 清空所有待执行任务
ClearAllPendingTasks() error
// ClaimNextAvailableTask 原子地认领下一个可用的任务。
// 它会同时返回被认领任务对应的日志对象,以及被删除的待办任务对象的内存副本。
ClaimNextAvailableTask(excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error)
@@ -28,11 +41,56 @@ func NewGormPendingTaskRepository(db *gorm.DB) PendingTaskRepository {
return &gormPendingTaskRepository{db: db}
}
func (r *gormPendingTaskRepository) FindAllPendingTasks() ([]models.PendingTask, error) {
var tasks []models.PendingTask
// 预加载 Task 以便后续访问 Task.PlanID
// 预加载 TaskExecutionLog 以便后续访问 PlanExecutionLogID
err := r.db.Preload("Task").Preload("TaskExecutionLog").Find(&tasks).Error
return tasks, err
}
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.type = ? AND tasks.parameters->>'plan_id' = ?", models.TaskPlanAnalysis, fmt.Sprintf("%d", planID)).
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
}
// 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
}
// 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

View File

@@ -42,6 +42,18 @@ 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)
// CreatePlanAnalysisTask 创建一个 plan_analysis 类型的任务并返回它
CreatePlanAnalysisTask(plan *models.Plan) (*models.Task, error)
// FindPlansWithPendingTasks 查找所有正在执行的计划
FindPlansWithPendingTasks() ([]*models.Plan, error)
}
// gormPlanRepository 是 PlanRepository 的 GORM 实现
@@ -174,7 +186,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
@@ -275,7 +289,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
}
@@ -517,63 +531,115 @@ 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 使用简单粗暴的删除再创建方式实现更新, 以控制AnalysisPlanTask的定义全部在createPlanAnalysisTask方法中
// updatePlanAnalysisTask 使用更安全的方式更新触发器任务
func (r *gormPlanRepository) updatePlanAnalysisTask(tx *gorm.DB, plan *models.Plan) error {
task, err := r.findPlanAnalysisTaskByParamsPlanID(tx, plan.ID)
if err != nil {
task, err := r.findPlanAnalysisTask(tx, plan.ID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("查找现有计划分析任务失败: %w", err)
}
// 如果触发器任务不存在,则创建一个
if task == nil {
_, err := r.createPlanAnalysisTask(tx, plan)
return err
}
err = r.deleteTask(tx, task.ID)
if err != nil {
return err
}
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 是一个内部使用的、更高效的查找方法
// 关键修改:通过查询 parameters JSON 字段来查找
func (r *gormPlanRepository) findPlanAnalysisTask(tx *gorm.DB, planID uint) (*models.Task, error) {
var task models.Task
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
}
return &task, err
}
// 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
}
// 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
}