openspec归档

This commit is contained in:
2025-10-30 18:25:25 +08:00
parent b4c70d4d9c
commit 4b10efb13c
5 changed files with 17 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
## Context
当前 API 服务基于 Gin 构建。本次任务的目标是将其完整迁移到 Echo 框架同时保持功能和接口的完全向后兼容。这包括路由、请求处理、中间件、Swagger 文档和 pprof 分析工具。
## Goals / Non-Goals
- **Goals**:
- 成功将 Web 框架从 Gin 迁移到 Echo v4。
- 保持所有现有 API 端点的路径、方法和行为不变。
- 确保所有自定义中间件(认证、审计日志)功能正常。
- 确保 Swagger UI 可以在 `/swagger/index.html` 正常访问。
- 确保 pprof 调试端点在 `/debug/pprof/*` 路径下正常工作。
- **Non-Goals**:
- 增加任何新的 API 端点或功能。
- 修改任何现有的 API 请求/响应模型。
- 在本次变更中引入新的业务逻辑。
## Decisions
以下是从 Gin 到 Echo 的关键组件映射决策:
1. **框架实例**:
- **From**: `gin.SetMode(cfg.Mode)`, `engine := gin.New()`, `engine.Use(gin.Recovery())`
- **To**: `e := echo.New()`, `e.Debug = (cfg.Mode == "debug")`, `e.Use(middleware.Recover())`
- **Rationale**: `echo.New()` 提供了干净的实例。Echo 的 `Debug` 属性控制调试模式可以根据配置设置。Echo 提供了内置的 `middleware.Recover()` 来替代 Gin 的 Recovery 中间件。
2. **上下文对象 (Context) 与处理器签名**:
- **From**: `func(c *gin.Context)`
- **To**: `func(c echo.Context) error`
- **Rationale**: 这是两个框架的核心区别。所有控制器处理函数签名都需要更新。常见方法映射如下:
- `ctx.ShouldBindJSON(&req)` -> `c.Bind(&req)` (Echo 的 `Bind` 更通用)
- `ctx.Param("id")` -> `c.Param("id")`
- `ctx.GetHeader("Authorization")` -> `c.Request().Header.Get("Authorization")`
- `ctx.Set/Get("key", value)` -> `c.Set/Get("key")`
- `ctx.ClientIP()` -> `c.RealIP()`
- `controller.SendResponse(ctx, ...)` -> `return controller.SendResponse(c, ...)`
- `ctx.AbortWithStatusJSON(...)` -> 对于需要返回特定HTTP状态码的场景如认证中间件将使用一个专门的辅助函数 `return controller.SendErrorWithStatus(c, http.StatusUnauthorized, ...)`
3. **中间件 (Middleware)**:
- **From**: `func AuthMiddleware(...) gin.HandlerFunc { return func(c *gin.Context) { ... } }`
- **To**: `func AuthMiddleware(...) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { ...; return next(c) } } }`
- **Rationale**: Echo 的中间件是一个包装器模式。我们需要将现有的 `AuthMiddleware``AuditLogMiddleware` 逻辑迁移到这个新的结构中。
4. **Swagger 集成**:
- **From**: `github.com/swaggo/gin-swagger`
- **To**: `github.com/swaggo/echo-swagger`
- **Rationale**: 这是 `swaggo` 官方为 Echo 提供的适配库,可以无缝替换。
5. **Pprof 与其他 `net/http` 处理器集成**:
- **From**: `gin.WrapH``gin.WrapF`
- **To**: `echo.WrapHandler``echo.WrapFunc`
- **Rationale**: Echo 提供了类似的 `net/http` 处理器包装函数。
6. **控制器辅助函数与审计逻辑重构**:
- **Affected Files**: `response.go`, `auth_utils.go`, `controller_helpers.go`
- **Change**:
- 所有辅助函数中的 `*gin.Context` 都将替换为 `echo.Context`
- **`response.go` 将被重构**`setAuditDetails` 函数将成为设置所有审计信息(包括操作状态和失败详情)的唯一入口。`SendSuccessWithAudit``SendErrorWithAudit` 会调用它来将最终结果存入 `echo.Context`
- `controller_helpers.go` 中的泛型辅助函数将修改为返回 `error`,以适配 Echo 的错误处理链。
- **Rationale**: 这种重构使得审计逻辑更加清晰和内聚,避免了在中间件中进行复杂的响应体捕获。
7. **DTO 注解 (Annotations)**:
- **From**: Gin 相关的注解,主要包括 `binding:"..."``form:"..."`
- **To**: Echo 兼容的注解,主要包括 `validate:"..."``query:"..."`
- **Rationale**: Gin 使用 `binding` 标签进行请求参数绑定和验证,`form` 标签用于表单或查询参数绑定。Echo 框架通常结合 `go-playground/validator` 库进行验证,其对应的标签为 `validate`。对于查询参数Echo 默认使用 `query` 标签。
- **通用修改规则**
- `json:"..."` 标签保持不变。
- `example:"..."` 标签保持不变。
-`binding:"required"` 替换为 `validate:"required"`
-`form:"field,default=value"` 替换为 `query:"field"``default` 行为需在代码中手动实现(如在 DTO 构造函数中设置默认值),标签中不再需要。
-`form:"field"` 替换为 `query:"field"`
- 对于 `json:"...,omitempty"` 的字段,在 `validate` 标签中也添加 `omitempty`
- 对于结构体切片或数组字段,在 `validate` 标签中添加 `dive` 以递归验证切片元素。
- 根据字段的业务含义,添加更具体的 `validate` 规则(例如 `min=0`, `cron` 等)。
## Risks / Trade-offs
- **Risk**: 迁移工作量大,可能遗漏某些 Gin 特有的功能或上下文用法,导致运行时错误。
- **Mitigation**: 采用逐个文件、逐个控制器修改的方式,每修改完一部分就进行编译检查。在完成所有编码后,进行全面的手动 API 测试。
- **Risk (Resolved)**: `AuditLogMiddleware` 中间件最初的设计依赖于捕获响应体,这在 Echo 中难以实现。
- **Resolution**: 我们通过重构 `response.go` 解决了这个问题。现在,控制器在调用响应函数时,会将最终的操作状态(成功/失败)和结果详情直接存入 `echo.Context``AuditLogMiddleware` 只需从上下文中读取这些信息即可,**完全消除了捕获和解析响应体的需要**,使得设计更加清晰和高效。

View File

@@ -0,0 +1,26 @@
## Why
本项目当前使用 Gin 作为核心 Web 框架。Gin 的路由系统存在一些限制,例如无法优雅地支持类似 `/:id/action``/:other_id/other-action` 这种在同一层级使用不同动态参数的路由模式。为了解决此问题并利用更现代、灵活的路由和中间件系统,我们计划将框架迁移到 Echo (v4)。本次变更仅进行框架替换,暂不修改现有路由结构。
## What Changes
- **核心框架替换**: 将 `github.com/gin-gonic/gin` 的所有引用替换为 `github.com/labstack/echo/v4`
- **API 路由重写**: 更新 `internal/app/api/router.go` 以使用 Echo 的路由注册方式。
- **上下文对象适配**: 在所有 Controller 和 Middleware 中,将 `*gin.Context` 替换为 `echo.Context`,并调整相关方法调用。
- **中间件迁移**: 将现有的 Gin 中间件 (`AuthMiddleware`, `AuditLogMiddleware`) 适配为 Echo 的中间件格式。
- **Swagger 文档适配**: 将 `gin-swagger` 替换为 Echo 兼容的 `echo-swagger`,确保 API 文档能够正常生成和访问。
- **Pprof 路由适配**: 确保性能分析工具 pprof 的路由在 Echo 框架下正常工作。
**BREAKING**: 这是一项纯粹的技术栈重构,**不应该**对外部 API 消费者产生任何破坏性影响。所有 API 端点、请求/响应格式将保持完全兼容。
## Impact
- **Affected specs**: 无。此变更是技术实现层面的重构,不改变任何已定义的功能规约。
- **Affected code**:
- `go.mod` / `go.sum`: 依赖项变更。
- `config.yml` / `config.example.yml`: 更新 `mode` 配置项的注释。
- `internal/app/api/api.go`
- `internal/app/api/router.go`
- `internal/app/middleware/auth.go`
- `internal/app/middleware/audit.go`
- `internal/app/controller/**/*.go`: 所有控制器及其辅助函数。

View File

@@ -0,0 +1,17 @@
# HTTP Server Specification
本文档概述了 HTTP 服务器的需求。
## MODIFIED Requirements
### Requirement: API 服务器框架已更新
- **说明**: 底层 Web 框架从 Gin 迁移到 Echo。所有现有的 API 端点 **MUST** 保持功能齐全和向后兼容。
- **理由**: 为了提高路由灵活性并使技术栈现代化。这是一次技术重构,不会改变任何外部 API 行为。
- **影响**: 高。影响核心请求处理、路由和中间件。
- **受影响的端点**: 全部。
#### Scenario: 所有现有的 API 端点保持功能齐全和向后兼容
- **假如**: API 服务器在迁移到 Echo 后正在运行。
- **当**: 客户端向任何现有的 API 端点(例如, `POST /api/v1/users/login`)发送请求。
- **那么**: 服务器处理该请求并返回与使用 Gin 框架时完全相同的响应(状态码、头部和正文格式)。

View File

@@ -0,0 +1,355 @@
## 任务清单Gin 到 Echo 迁移
- [x] **1. 配置文件 (无代码依赖)**
- [x] 修改 `config.yml``mode` 配置项的注释,将 "Gin 运行模式" 改为 "服务运行模式"。
- [x] 修改 `config.example.yml``mode` 配置项的注释,保持与 `config.yml` 一致。
- [x] **2. 控制器辅助函数 (最基础的依赖)**
- [x] **`internal/infra/models/execution.go`**
- [x] 添加 `ContextAuditStatus``ContextAuditResultDetails` 常量。
- [x] **`internal/app/controller/response.go`**
- [x]`*gin.Context` 参数全部替换为 `echo.Context`
- [x] 修改响应函数,使其返回 `error`
- [x] **新增 `SendErrorWithStatus` 函数**用于在中间件等场景下发送带有特定HTTP状态码的错误响应。
- [x] **重构 `setAuditDetails` 函数**,使其成为统一设置所有审计信息(包括操作状态和失败详情)的唯一入口。
- [x] 更新 `SendSuccessWithAudit``SendErrorWithAudit` 以调用重构后的 `setAuditDetails`
- [x] **`internal/app/controller/auth_utils.go`**
- [x]`*gin.Context` 参数全部替换为 `echo.Context`
- [x] 适配 `Get...FromContext` 系列函数,使用 `c.Get("key")` 提取数据。
- [x] **3. 中间件 (`internal/app/middleware`)**
- [x] **`auth.go`**
- [x] 迁移到 Echo 中间件格式。
- [x] **使用 `controller.SendErrorWithStatus`** 在认证失败时返回 `401``500` HTTP状态码。
- [x] **`audit.go`**
- [x] **极大简化并迁移到 Echo 中间件格式**
- [x] **移除所有响应体捕获和解析的逻辑** (`bodyLogWriter`, `auditResponse` 等)。
- [x]`next(c)` 调用后,**直接从 `echo.Context` 中获取**由 `response.go` 设置好的最终审计状态和结果详情。
- [x] **4. 控制器 (`internal/app/controller/...`)**
- [x] **通用修改**:对所有控制器文件执行以下操作:
- [x]`import "github.com/gin-gonic/gin"` 替换为 `import "github.com/labstack/echo/v4"`
- [x] 将所有处理函数签名从 `func(c *gin.Context)` 修改为 `func(c echo.Context) error`
- [x]`c.ShouldBindJSON(&req)``c.ShouldBindQuery(&req)` 替换为
`if err := c.Bind(&req); err != nil { ... }`
- [x]`c.Param("id")` 替换为 `c.Param("id")` (用法相同,检查返回值即可)。
- [x]`controller.SendResponse(c, ...)``controller.SendErrorResponse(c, ...)` 调用修改为
`return controller.SendResponse(c, ...)``return controller.SendErrorResponse(c, ...)`
- [x] **文件清单** (按依赖顺序建议):
- [x] `internal/app/controller/management/controller_helpers.go` (注意:其中的泛型辅助函数也需要修改为返回
`error`)
- [x] `internal/app/controller/device/device_controller.go`
- [x] `internal/app/controller/management/pig_farm_controller.go`
- [x] `internal/app/controller/management/pig_batch_controller.go`
- [x] `internal/app/controller/management/pig_batch_health_controller.go`
- [x] `internal/app/controller/management/pig_batch_trade_controller.go`
- [x] `internal/app/controller/management/pig_batch_transfer_controller.go`
- [x] `internal/app/controller/monitor/monitor_controller.go`
- [x] `internal/app/controller/plan/plan_controller.go`
- [x] `internal/app/controller/user/user_controller.go`
- [x] **5. DTO 结构体注解**
- [x] **通用修改规则**
- [x] `json:"..."` 标签保持不变。
- [x] `example:"..."` 标签保持不变。
- [x]`binding:"required"` 替换为 `validate:"required"`
- [x]`form:"field,default=value"` 替换为 `query:"field"``default` 行为需在代码中手动实现(如在 DTO 构造函数中设置默认值),标签中不再需要。
- [x]`form:"field"` 替换为 `query:"field"`
- [x] 对于 `json:"...,omitempty"` 的字段,在 `validate` 标签中也添加 `omitempty`
- [x] 对于结构体切片或数组字段,在 `validate` 标签中添加 `dive` 以递归验证切片元素。
- [x] 根据字段的业务含义,添加更具体的 `validate` 规则(例如 `min=0`, `cron` 等)。
- [x] **文件清单** (按 `internal/app/dto` 目录下的文件顺序)
- [x] `internal/app/dto/plan_dto.go`
- [x] `ListPlansQuery.PlanType`: `form:"planType,default=自定义任务"` -> `query:"planType"`
- [x] `ListPlansQuery.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListPlansQuery.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `CreatePlanRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `CreatePlanRequest.ExecutionType`: `binding:"required"` -> `validate:"required"`
- [x] `CreatePlanRequest.ExecuteNum`: 添加 `validate:"omitempty,min=0"`
- [x] `CreatePlanRequest.CronExpression`: 添加 `validate:"omitempty,cron"`
- [x] `CreatePlanRequest.SubPlanIDs`: 添加 `validate:"omitempty,dive"`
- [x] `CreatePlanRequest.Tasks`: 添加 `validate:"omitempty,dive"`
- [x] `UpdatePlanRequest.ExecutionType`: `binding:"required"` -> `validate:"required"`
- [x] `UpdatePlanRequest.ExecuteNum`: 添加 `validate:"omitempty,min=0"`
- [x] `UpdatePlanRequest.CronExpression`: 添加 `validate:"omitempty,cron"`
- [x] `UpdatePlanRequest.SubPlanIDs`: 添加 `validate:"omitempty,dive"`
- [x] `UpdatePlanRequest.Tasks`: 添加 `validate:"omitempty,dive"`
- [x] `internal/app/dto/user_dto.go`
- [x] `CreateUserRequest.Username`: `binding:"required"` -> `validate:"required"`
- [x] `CreateUserRequest.Password`: `binding:"required"` -> `validate:"required"`
- [x] `LoginRequest.Identifier`: `binding:"required"` -> `validate:"required"`
- [x] `LoginRequest.Password`: `binding:"required"` -> `validate:"required"`
- [x] `internal/app/dto/device_dto.go`
- [x] `CreateDeviceRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `CreateDeviceRequest.DeviceTemplateID`: `binding:"required"` -> `validate:"required"`
- [x] `CreateDeviceRequest.AreaControllerID`: `binding:"required"` -> `validate:"required"`
- [x] `CreateDeviceRequest.Location`: `json:"location,omitempty"` -> `validate:"omitempty"`
- [x] `CreateDeviceRequest.Properties`: `json:"properties,omitempty"` -> `validate:"omitempty"`
- [x] `UpdateDeviceRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `UpdateDeviceRequest.DeviceTemplateID`: `binding:"required"` -> `validate:"required"`
- [x] `UpdateDeviceRequest.AreaControllerID`: `binding:"required"` -> `validate:"required"`
- [x] `UpdateDeviceRequest.Location`: `json:"location,omitempty"` -> `validate:"omitempty"`
- [x] `UpdateDeviceRequest.Properties`: `json:"properties,omitempty"` -> `validate:"omitempty"`
- [x] `CreateAreaControllerRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `CreateAreaControllerRequest.NetworkID`: `binding:"required"` -> `validate:"required"`
- [x] `CreateAreaControllerRequest.Location`: `json:"location,omitempty"` -> `validate:"omitempty"`
- [x] `CreateAreaControllerRequest.Properties`: `json:"properties,omitempty"` -> `validate:"omitempty"`
- [x] `UpdateAreaControllerRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `UpdateAreaControllerRequest.NetworkID`: `binding:"required"` -> `validate:"required"`
- [x] `UpdateAreaControllerRequest.Location`: `json:"location,omitempty"` -> `validate:"omitempty"`
- [x] `UpdateAreaControllerRequest.Properties`: `json:"properties,omitempty"` -> `validate:"omitempty"`
- [x] `CreateDeviceTemplateRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `CreateDeviceTemplateRequest.Manufacturer`: `json:"manufacturer,omitempty"` -> `validate:"omitempty"`
- [x] `CreateDeviceTemplateRequest.Description`: `json:"description,omitempty"` -> `validate:"omitempty"`
- [x] `CreateDeviceTemplateRequest.Category`: `binding:"required"` -> `validate:"required"`
- [x] `CreateDeviceTemplateRequest.Commands`: `binding:"required"` -> `validate:"required"`
- [x] `CreateDeviceTemplateRequest.Values`: `json:"values,omitempty"` -> `validate:"omitempty,dive"`
- [x] `UpdateDeviceTemplateRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `UpdateDeviceTemplateRequest.Manufacturer`: `json:"manufacturer,omitempty"` -> `validate:"omitempty"`
- [x] `UpdateDeviceTemplateRequest.Description`: `json:"description,omitempty"` -> `validate:"omitempty"`
- [x] `UpdateDeviceTemplateRequest.Category`: `binding:"required"` -> `validate:"required"`
- [x] `UpdateDeviceTemplateRequest.Commands`: `binding:"required"` -> `validate:"required"`
- [x] `UpdateDeviceTemplateRequest.Values`: `json:"values,omitempty"` -> `validate:"omitempty,dive"`
- [x] `internal/app/dto/monitor_dto.go`
- [x] `ListSensorDataRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListSensorDataRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListSensorDataRequest.DeviceID`: `form:"device_id"` -> `query:"device_id"`
- [x] `ListSensorDataRequest.SensorType`: `form:"sensor_type"` -> `query:"sensor_type"`
- [x] `ListSensorDataRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListSensorDataRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListSensorDataRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListDeviceCommandLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListDeviceCommandLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListDeviceCommandLogRequest.DeviceID`: `form:"device_id"` -> `query:"device_id"`
- [x] `ListDeviceCommandLogRequest.ReceivedSuccess`: `form:"received_success"` -> `query:"received_success"`
- [x] `ListDeviceCommandLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListDeviceCommandLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListDeviceCommandLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListPlanExecutionLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListPlanExecutionLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListPlanExecutionLogRequest.PlanID`: `form:"plan_id"` -> `query:"plan_id"`
- [x] `ListPlanExecutionLogRequest.Status`: `form:"status"` -> `query:"status"`
- [x] `ListPlanExecutionLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListPlanExecutionLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListPlanExecutionLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListTaskExecutionLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListTaskExecutionLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListTaskExecutionLogRequest.PlanExecutionLogID`: `form:"plan_execution_log_id"` -> `query:"plan_execution_log_id"`
- [x] `ListTaskExecutionLogRequest.TaskID`: `form:"task_id"` -> `query:"task_id"`
- [x] `ListTaskExecutionLogRequest.Status`: `form:"status"` -> `query:"status"`
- [x] `ListTaskExecutionLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListTaskExecutionLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListTaskExecutionLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListPendingCollectionRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListPendingCollectionRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListPendingCollectionRequest.DeviceID`: `form:"device_id"` -> `query:"device_id"`
- [x] `ListPendingCollectionRequest.Status`: `form:"status"` -> `query:"status"`
- [x] `ListPendingCollectionRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListPendingCollectionRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListPendingCollectionRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListUserActionLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListUserActionLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListUserActionLogRequest.UserID`: `form:"user_id"` -> `query:"user_id"`
- [x] `ListUserActionLogRequest.Username`: `form:"username"` -> `query:"username"`
- [x] `ListUserActionLogRequest.ActionType`: `form:"action_type"` -> `query:"action_type"`
- [x] `ListUserActionLogRequest.Status`: `form:"status"` -> `query:"status"`
- [x] `ListUserActionLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListUserActionLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListUserActionLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListRawMaterialPurchaseRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListRawMaterialPurchaseRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListRawMaterialPurchaseRequest.RawMaterialID`: `form:"raw_material_id"` -> `query:"raw_material_id"`
- [x] `ListRawMaterialPurchaseRequest.Supplier`: `form:"supplier"` -> `query:"supplier"`
- [x] `ListRawMaterialPurchaseRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListRawMaterialPurchaseRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListRawMaterialPurchaseRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListRawMaterialStockLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListRawMaterialStockLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListRawMaterialStockLogRequest.RawMaterialID`: `form:"raw_material_id"` -> `query:"raw_material_id"`
- [x] `ListRawMaterialStockLogRequest.SourceType`: `form:"source_type"` -> `query:"source_type"`
- [x] `ListRawMaterialStockLogRequest.SourceID`: `form:"source_id"` -> `query:"source_id"`
- [x] `ListRawMaterialStockLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListRawMaterialStockLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListRawMaterialStockLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListFeedUsageRecordRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListFeedUsageRecordRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListFeedUsageRecordRequest.PenID`: `form:"pen_id"` -> `query:"pen_id"`
- [x] `ListFeedUsageRecordRequest.FeedFormulaID`: `form:"feed_formula_id"` -> `query:"feed_formula_id"`
- [x] `ListFeedUsageRecordRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
- [x] `ListFeedUsageRecordRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListFeedUsageRecordRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListFeedUsageRecordRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListMedicationLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListMedicationLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListMedicationLogRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
- [x] `ListMedicationLogRequest.MedicationID`: `form:"medication_id"` -> `query:"medication_id"`
- [x] `ListMedicationLogRequest.Reason`: `form:"reason"` -> `query:"reason"`
- [x] `ListMedicationLogRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
- [x] `ListMedicationLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListMedicationLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListMedicationLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListPigBatchLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListPigBatchLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListPigBatchLogRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
- [x] `ListPigBatchLogRequest.ChangeType`: `form:"change_type"` -> `query:"change_type"`
- [x] `ListPigBatchLogRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
- [x] `ListPigBatchLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListPigBatchLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListPigBatchLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListWeighingBatchRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListWeighingBatchRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListWeighingBatchRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
- [x] `ListWeighingBatchRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListWeighingBatchRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListWeighingBatchRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListWeighingRecordRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListWeighingRecordRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListWeighingRecordRequest.WeighingBatchID`: `form:"weighing_batch_id"` -> `query:"weighing_batch_id"`
- [x] `ListWeighingRecordRequest.PenID`: `form:"pen_id"` -> `query:"pen_id"`
- [x] `ListWeighingRecordRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
- [x] `ListWeighingRecordRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListWeighingRecordRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListWeighingRecordRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListPigTransferLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListPigTransferLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListPigTransferLogRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
- [x] `ListPigTransferLogRequest.PenID`: `form:"pen_id"` -> `query:"pen_id"`
- [x] `ListPigTransferLogRequest.TransferType`: `form:"transfer_type"` -> `query:"transfer_type"`
- [x] `ListPigTransferLogRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
- [x] `ListPigTransferLogRequest.CorrelationID`: `form:"correlation_id"` -> `query:"correlation_id"`
- [x] `ListPigTransferLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListPigTransferLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListPigTransferLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListPigSickLogRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListPigSickLogRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListPigSickLogRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
- [x] `ListPigSickLogRequest.PenID`: `form:"pen_id"` -> `query:"pen_id"`
- [x] `ListPigSickLogRequest.Reason`: `form:"reason"` -> `query:"reason"`
- [x] `ListPigSickLogRequest.TreatmentLocation`: `form:"treatment_location"` -> `query:"treatment_location"`
- [x] `ListPigSickLogRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
- [x] `ListPigSickLogRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListPigSickLogRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListPigSickLogRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListPigPurchaseRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListPigPurchaseRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListPigPurchaseRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
- [x] `ListPigPurchaseRequest.Supplier`: `form:"supplier"` -> `query:"supplier"`
- [x] `ListPigPurchaseRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
- [x] `ListPigPurchaseRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListPigPurchaseRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListPigPurchaseRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `ListPigSaleRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListPigSaleRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListPigSaleRequest.PigBatchID`: `form:"pig_batch_id"` -> `query:"pig_batch_id"`
- [x] `ListPigSaleRequest.Buyer`: `form:"buyer"` -> `query:"buyer"`
- [x] `ListPigSaleRequest.OperatorID`: `form:"operator_id"` -> `query:"operator_id"`
- [x] `ListPigSaleRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListPigSaleRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListPigSaleRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `internal/app/dto/pig_farm_dto.go`
- [x] `CreatePigHouseRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `UpdatePigHouseRequest.Name`: `binding:"required"` -> `validate:"required"`
- [x] `CreatePenRequest.PenNumber`: `binding:"required"` -> `validate:"required"`
- [x] `CreatePenRequest.HouseID`: `binding:"required"` -> `validate:"required"`
- [x] `CreatePenRequest.Capacity`: `binding:"required"` -> `validate:"required"`
- [x] `UpdatePenRequest.PenNumber`: `binding:"required"` -> `validate:"required"`
- [x] `UpdatePenRequest.HouseID`: `binding:"required"` -> `validate:"required"`
- [x] `UpdatePenRequest.Capacity`: `binding:"required"` -> `validate:"required"`
- [x] `UpdatePenRequest.Status`: `binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` -> `validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"`
- [x] `UpdatePenStatusRequest.Status`: `binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` -> `validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"`
- [x] `internal/app/dto/pig_batch_dto.go`
- [x] `PigBatchCreateDTO.BatchNumber`: `binding:"required"` -> `validate:"required"`
- [x] `PigBatchCreateDTO.OriginType`: `binding:"required"` -> `validate:"required"`
- [x] `PigBatchCreateDTO.StartDate`: `binding:"required"` -> `validate:"required"`
- [x] `PigBatchCreateDTO.InitialCount`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `PigBatchCreateDTO.Status`: `binding:"required"` -> `validate:"required"`
- [x] `PigBatchQueryDTO.IsActive`: `form:"is_active"` -> `query:"is_active"`
- [x] `AssignEmptyPensToBatchRequest.PenIDs`: `binding:"required,min=1"` -> `validate:"required,min=1,dive"`
- [x] `ReclassifyPenToNewBatchRequest.ToBatchID`: `binding:"required"` -> `validate:"required"`
- [x] `ReclassifyPenToNewBatchRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `RemoveEmptyPenFromBatchRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `MovePigsIntoPenRequest.ToPenID`: `binding:"required"` -> `validate:"required"`
- [x] `MovePigsIntoPenRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `SellPigsRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `SellPigsRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `SellPigsRequest.UnitPrice`: `binding:"required,min=0"` -> `validate:"required,min=0"`
- [x] `SellPigsRequest.TotalPrice`: `binding:"required,min=0"` -> `validate:"required,min=0"`
- [x] `SellPigsRequest.TraderName`: `binding:"required"` -> `validate:"required"`
- [x] `SellPigsRequest.TradeDate`: `binding:"required"` -> `validate:"required"`
- [x] `BuyPigsRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `BuyPigsRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `BuyPigsRequest.UnitPrice`: `binding:"required,min=0"` -> `validate:"required,min=0"`
- [x] `BuyPigsRequest.TotalPrice`: `binding:"required,min=0"` -> `validate:"required,min=0"`
- [x] `BuyPigsRequest.TraderName`: `binding:"required"` -> `validate:"required"`
- [x] `BuyPigsRequest.TradeDate`: `binding:"required"` -> `validate:"required"`
- [x] `TransferPigsAcrossBatchesRequest.DestBatchID`: `binding:"required"` -> `validate:"required"`
- [x] `TransferPigsAcrossBatchesRequest.FromPenID`: `binding:"required"` -> `validate:"required"`
- [x] `TransferPigsAcrossBatchesRequest.ToPenID`: `binding:"required"` -> `validate:"required"`
- [x] `TransferPigsAcrossBatchesRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `TransferPigsWithinBatchRequest.FromPenID`: `binding:"required"` -> `validate:"required"`
- [x] `TransferPigsWithinBatchRequest.ToPenID`: `binding:"required"` -> `validate:"required"`
- [x] `TransferPigsWithinBatchRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `RecordSickPigsRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigsRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `RecordSickPigsRequest.TreatmentLocation`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigsRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigRecoveryRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigRecoveryRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `RecordSickPigRecoveryRequest.TreatmentLocation`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigRecoveryRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigDeathRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigDeathRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `RecordSickPigDeathRequest.TreatmentLocation`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigDeathRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigCullRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigCullRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `RecordSickPigCullRequest.TreatmentLocation`: `binding:"required"` -> `validate:"required"`
- [x] `RecordSickPigCullRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
- [x] `RecordDeathRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `RecordDeathRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `RecordDeathRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
- [x] `RecordCullRequest.PenID`: `binding:"required"` -> `validate:"required"`
- [x] `RecordCullRequest.Quantity`: `binding:"required,min=1"` -> `validate:"required,min=1"`
- [x] `RecordCullRequest.HappenedAt`: `binding:"required"` -> `validate:"required"`
- [x] `internal/app/dto/notification_dto.go`
- [x] `SendTestNotificationRequest.Type`: `binding:"required"` -> `validate:"required"`
- [x] `ListNotificationRequest.Page`: `form:"page,default=1"` -> `query:"page"`
- [x] `ListNotificationRequest.PageSize`: `form:"pageSize,default=10"` -> `query:"pageSize"`
- [x] `ListNotificationRequest.UserID`: `form:"user_id"` -> `query:"user_id"`
- [x] `ListNotificationRequest.NotifierType`: `form:"notifier_type"` -> `query:"notifier_type"`
- [x] `ListNotificationRequest.Status`: `form:"status"` -> `query:"status"`
- [x] `ListNotificationRequest.Level`: `form:"level"` -> `query:"level"`
- [x] `ListNotificationRequest.StartTime`: `form:"start_time"` -> `query:"start_time"`
- [x] `ListNotificationRequest.EndTime`: `form:"end_time"` -> `query:"end_time"`
- [x] `ListNotificationRequest.OrderBy`: `form:"order_by"` -> `query:"order_by"`
- [x] `internal/app/dto/plan_converter.go` (跳过,非 DTO 结构体)
- [x] `internal/app/dto/device_converter.go` (跳过,非 DTO 结构体)
- [x] `internal/app/dto/monitor_converter.go` (跳过,非 DTO 结构体)
- [x] `internal/app/dto/notification_converter.go` (跳过,非 DTO 结构体)
- [x] **6. 核心 API 层 (`internal/app/api`)**
- [x] **`router.go`**
- [x] 将所有 `router.GET`, `router.POST` 等 Gin 路由注册方法替换为 Echo 的 `e.GET`, `e.POST` 等方法。
- [x] 将 Swagger 路由 `router.GET("/swagger/*", ginSwagger.WrapHandler(swaggerFiles.Handler))` 替换为
`e.GET("/swagger/*", echoSwagger.WrapHandler)`
- [x] 将 pprof 路由的 `gin.WrapH``gin.WrapF` 调用替换为 `echo.WrapHandler``echo.WrapFunc`
- [x] **`api.go`**
- [x]`engine *gin.Engine` 替换为 `engine *echo.Echo`
- [x] 更新 `NewAPI` 函数:
- [x]`gin.SetMode(cfg.Mode)` 替换为 `e.Debug = (cfg.Mode == "debug")`
- [x]`gin.New()` 替换为 `echo.New()`
- [x]`engine.Use(middleware.Recover())` 替换为 `e.Use(middleware.Recover())`
- [x] **7. 依赖管理**
- [x]`go.mod` 中移除 `github.com/gin-gonic/gin`
- [x]`go.mod` 中移除 `github.com/swaggo/gin-swagger`
- [x]`go.mod` 中添加 `github.com/labstack/echo/v4`
- [x]`go.mod` 中添加 `github.com/swaggo/echo-swagger`
- [x] 执行 `go mod tidy` 清理依赖项。
- [x] **8. 验证**
- [x] 运行 `go build ./...` 确保项目能够成功编译。
- [x] 启动服务,手动测试所有 API 端点,验证功能是否与迁移前一致。
- [x] 访问 `/swagger/index.html`,确认 Swagger UI 是否正常工作。
- [x] (可选) 访问 `/debug/pprof/`,确认 pprof 路由是否正常。