Compare commits
77 Commits
e66ee67cf7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d7c7b56b95 | |||
| 545a53bb68 | |||
| 9127eeaf31 | |||
| f0b71b47a0 | |||
| f569876225 | |||
| 8669dcd9b0 | |||
| 66554a1376 | |||
| b62a3d0e5d | |||
| 026dad9374 | |||
| 687c2f12ee | |||
| 4b0be88fca | |||
| bb42147974 | |||
| 8d7d9fc485 | |||
| 6cd566bc30 | |||
| 408df2f09c | |||
| 011658461e | |||
| 3ab2eb0535 | |||
| a29e15faba | |||
| 8e97922012 | |||
| 548d3eae00 | |||
| 6f7e462589 | |||
| cf9e43cdd8 | |||
| 426ae41f54 | |||
| 5b21dc0bd5 | |||
| 67d4fb097d | |||
| 0008141989 | |||
| c4ca0175dd | |||
| 193d77b5b7 | |||
| 0c88c76417 | |||
| 843bd8a814 | |||
| 348220bc7b | |||
| d6c18f0774 | |||
| e1c76fd8ec | |||
| bc6a960451 | |||
| 4e87436cc0 | |||
| 942ffa29a1 | |||
| b44e1a0e7c | |||
| d22ddac9cd | |||
| ccab7c98e4 | |||
| 3334537663 | |||
| 0c35e2ce7d | |||
| db11438f5c | |||
| 9f3e800e59 | |||
| 8d8310fd2c | |||
| 12c6dc515f | |||
| c2c2383305 | |||
| 4a92324774 | |||
| a4bd19f950 | |||
| f71d04f8af | |||
| 4b10efb13c | |||
| b4c70d4d9c | |||
| f624a8bf5e | |||
| 8ce553a9e4 | |||
| 5b064b4015 | |||
| 6228534155 | |||
| d235130d11 | |||
| f0982839e0 | |||
| ff8a8d2b97 | |||
| f2078ea54a | |||
| c463875fba | |||
| 7c5232e71b | |||
| 2c9b4777ae | |||
| 93f67812ae | |||
| e5b75e3879 | |||
| 67575c17bc | |||
| 7ac9e49212 | |||
| ff45c59946 | |||
| 8d48576305 | |||
| af8689d627 | |||
| 2910c9186a | |||
| b09d32b1d7 | |||
| 403d46b777 | |||
| 85bd5254c1 | |||
| 5050f76066 | |||
| 1ee3e638f7 | |||
| 94e8768424 | |||
| 675711cdcf |
18
AGENTS.md
Normal file
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Instructions
|
||||
|
||||
These instructions are for AI assistants working in this project.
|
||||
|
||||
Always open `@/openspec/AGENTS.md` when the request:
|
||||
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||
- Sounds ambiguous and you need the authoritative spec before coding
|
||||
|
||||
Use `@/openspec/AGENTS.md` to learn:
|
||||
- How to create and apply change proposals
|
||||
- Spec format and conventions
|
||||
- Project structure and guidelines
|
||||
|
||||
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
|
||||
<!-- OPENSPEC:END -->
|
||||
19
Makefile
19
Makefile
@@ -56,3 +56,22 @@ lint:
|
||||
.PHONY: dev
|
||||
dev:
|
||||
air
|
||||
|
||||
# 启用谷歌浏览器MCP服务器
|
||||
.PHONY: mcp-chrome
|
||||
mcp-chrome:
|
||||
node "C:\nvm4w\nodejs\node_modules\chrome-devtools-mcp\build\src\index.js"
|
||||
|
||||
# 生成文件目录树
|
||||
.PHONY: tree
|
||||
|
||||
# 定义要额外排除的生成代码目录
|
||||
EXCLUDE_CONTEXT_PREFIX = internal/infra/transport/lora/chirp_stack_proto/
|
||||
# 最终的文件清单会保存在这里
|
||||
OUTPUT_FILE = project_structure.txt
|
||||
|
||||
# 使用 PowerShell 脚本块执行 Git 命令和二次过滤
|
||||
tree:
|
||||
@powershell -Command "git ls-files --exclude-standard | Select-String -NotMatch '$(EXCLUDE_CONTEXT_PREFIX)' | Out-File -Encoding UTF8 $(OUTPUT_FILE)"
|
||||
@powershell -Command "Add-Content -Path $(OUTPUT_FILE) -Value '$(EXCLUDE_CONTEXT_PREFIX)' -Encoding UTF8"
|
||||
@echo "The project file list has been generated to project_structure.txt"
|
||||
|
||||
@@ -7,7 +7,7 @@ app:
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080 # 服务器监听端口
|
||||
mode: "debug" # 运行模式: debug, release, test
|
||||
mode: "debug" # 服务运行模式: debug, release, test
|
||||
|
||||
# 日志配置
|
||||
log:
|
||||
@@ -112,4 +112,4 @@ notify:
|
||||
|
||||
# 定时采集配置
|
||||
collection:
|
||||
interval: 300 # 采集间隔 (秒)
|
||||
interval: 1 # 采集间隔 (分钟)
|
||||
|
||||
@@ -8,11 +8,11 @@ app:
|
||||
# HTTP 服务配置
|
||||
server:
|
||||
port: 8086
|
||||
mode: "release" # Gin 运行模式: "debug", "release", "test"
|
||||
mode: "release" # 服务运行模式: "debug", "release", "test"
|
||||
|
||||
# 日志配置
|
||||
log:
|
||||
level: "debug" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal"
|
||||
level: "info" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal"
|
||||
format: "console" # 日志格式: "console" 或 "json"
|
||||
enable_file: true # 是否启用文件日志
|
||||
file_path: "./app_logs/app.log" # 日志文件路径
|
||||
@@ -90,4 +90,4 @@ lora_mesh:
|
||||
|
||||
# 定时采集配置
|
||||
collection:
|
||||
interval: 300 # 采集间隔 (秒)
|
||||
interval: 1 # 采集间隔 (分钟)
|
||||
@@ -0,0 +1,34 @@
|
||||
# 任务接口增加获取关联设备ID方法设计
|
||||
|
||||
## 1. 需求
|
||||
|
||||
为了在设备删除前进行验证,需要为任务接口增加一个方法,该方法能够直接返回指定任务配置中所有关联的设备ID列表。所有实现 `task` 接口的对象都必须实现此方法。
|
||||
|
||||
## 2. 新接口定义:`TaskDeviceIDResolver`
|
||||
|
||||
```go
|
||||
// TaskDeviceIDResolver 定义了从任务配置中解析设备ID的方法
|
||||
type TaskDeviceIDResolver interface {
|
||||
// ResolveDeviceIDs 从任务配置中解析并返回所有关联的设备ID列表
|
||||
// 返回值: uint数组,每个字符串代表一个设备ID
|
||||
ResolveDeviceIDs() ([]uint, error)
|
||||
}
|
||||
```
|
||||
|
||||
## 3. `task` 接口更新
|
||||
|
||||
`task` 接口将嵌入 `TaskDeviceIDResolver` 接口。
|
||||
|
||||
```go
|
||||
// Task 接口(示例,具体结构可能不同)
|
||||
type Task interface {
|
||||
// ... 其他现有方法 ...
|
||||
|
||||
// 嵌入 TaskDeviceIDResolver 接口
|
||||
TaskDeviceIDResolver
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 实现要求
|
||||
|
||||
所有当前及未来实现 `Task` 接口的类型,都必须实现 `TaskDeviceIDResolver` 接口中定义的所有方法,即 `ResolveDeviceIDs` 方法。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 方案:删除设备前的使用校验
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在删除设备前,检查该设备是否被任何任务关联。如果设备正在被使用,则禁止删除,并向用户返回明确的错误提示。
|
||||
|
||||
## 2. 核心思路
|
||||
|
||||
我们将遵循您项目清晰的分层架构,将“检查设备是否被任务使用”这一业务规则放在 **应用层** (`internal/app/service/`)
|
||||
中进行协调。当上层请求删除设备时,应用服务会先调用仓库层查询 `device_tasks` 关联表,如果发现设备仍被任务关联,则会拒绝删除并返回一个明确的业务错误。
|
||||
|
||||
## 3. 实施步骤
|
||||
|
||||
### 3.1. 仓库层 (`DeviceRepository`)
|
||||
|
||||
- **动作**: 在 `internal/infra/repository/device_repository.go` 的 `DeviceRepository` 接口中,增加一个新方法
|
||||
`IsDeviceInUse(deviceID uint) (bool, error)`。
|
||||
- **实现**: 在 `gormDeviceRepository` 中实现此方法。该方法将通过对 `models.DeviceTask` 模型执行 `Count`
|
||||
操作来高效地判断是否存在 `device_id` 匹配的记录。这比查询完整记录性能更好。
|
||||
|
||||
### 3.2. 应用层 (`DeviceService`)
|
||||
|
||||
- **动作**:
|
||||
1. 在 `internal/app/service/device_service.go` 文件顶部定义一个新的错误变量 `ErrDeviceInUse`,例如
|
||||
`var ErrDeviceInUse = errors.New("设备正在被一个或多个任务使用,无法删除")`。
|
||||
2. 修改该文件中的 `DeleteDevice` 方法。
|
||||
- **实现**: 在 `DeleteDevice` 方法中,在调用 `s.deviceRepo.Delete()` 之前,先调用我们刚刚创建的
|
||||
`s.deviceRepo.IsDeviceInUse()` 方法。如果返回 `true`,则立即返回 `ErrDeviceInUse` 错误,中断删除流程。
|
||||
|
||||
### 3.3. 表现层 (`DeviceController`)
|
||||
|
||||
- **动作**: 修改 `internal/app/controller/device/device_controller.go` 中的 `DeleteDevice` 方法。
|
||||
- **实现**: 在错误处理逻辑中,增加一个 `case` 来专门捕获从服务层返回的 `service.ErrDeviceInUse`
|
||||
错误。当捕获到此错误时,返回一个带有明确提示信息(如“设备正在被任务使用,无法删除”)和合适 HTTP 状态码(例如 `409 Conflict`)的错误响应。
|
||||
|
||||
## 4. 方案优势
|
||||
|
||||
- **职责清晰**: 业务流程的编排和校验逻辑被正确地放置在应用层,符合您项目清晰的分层架构。
|
||||
- **高效查询**: 通过 `COUNT` 查询代替 `Find`,避免了不必要的数据加载,性能更佳。
|
||||
- **代码内聚**: 与设备相关的数据库操作都统一封装在 `DeviceRepository` 中。
|
||||
- **用户友好**: 通过在控制器层处理特定业务错误,可以给前端返回明确、可操作的错误信息。
|
||||
@@ -0,0 +1,111 @@
|
||||
# 方案:维护设备与任务的关联关系
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在对计划(Plan)及其包含的任务(Task)进行创建、更新、删除(CRUD)操作时,同步维护 `device_tasks` 这张多对多关联表。
|
||||
|
||||
这是实现“删除设备前检查其是否被任务使用”这一需求的基础。
|
||||
|
||||
## 2. 核心挑战
|
||||
|
||||
1. **参数结构异构性**:不同类型的任务(`TaskType`),其设备 ID 存储在 `Parameters` (JSON) 字段中的 `key` 和数据结构(单个 ID
|
||||
或 ID 数组)各不相同。
|
||||
2. **分层架构原则**:解析 `Parameters` 以提取设备 ID 的逻辑属于 **业务规则**,需要找到一个合适的位置来封装它,以维持各层职责的清晰。
|
||||
|
||||
## 3. 方案设计
|
||||
|
||||
本方案旨在最大化地复用现有领域模型和逻辑,通过扩展 `TaskFactory` 来实现设备ID的解析,从而保持了各领域模块的高内聚和低耦合。
|
||||
|
||||
### 3.1. 核心思路:复用领域对象与工厂
|
||||
|
||||
我们不移动任何结构体,也不在 `plan` 包中引入任何具体任务的实现细节。取而代之,我们利用现有的 `TaskFactory`
|
||||
和各个任务领域对象自身的能力来解析参数。
|
||||
|
||||
每个具体的任务领域对象(如 `ReleaseFeedWeightTask`)最了解如何解析自己的 `Parameters`。因此,我们将解析设备ID的责任完全交还给它们。
|
||||
|
||||
### 3.2. 扩展 `TaskFactory`
|
||||
|
||||
- **动作**:在 `plan.TaskFactory` 接口中增加一个新方法 `CreateTaskFromModel(*models.Task) (TaskDeviceIDResolver, error)`。
|
||||
- **目的**:此方法允许我们在非任务执行的场景下(例如,在增删改查计划时),仅根据数据库模型 `models.Task` 来创建一个临时的、轻量级的任务领域对象。
|
||||
- **实现**:在 `internal/domain/task/task.go` 的 `taskFactory` 中实现此方法。它会根据传入的 `taskModel.Type`,`switch-case`
|
||||
来调用相应的构造函数(如 `NewReleaseFeedWeightTask`)创建实例。
|
||||
- **实现**:
|
||||
- **优势**:
|
||||
- **高内聚,低耦合**:`plan` 包保持通用,无需了解任何具体任务的参数细节。参数定义和解析逻辑都保留在各自的 `task` 包内。
|
||||
- **逻辑复用**:完美复用了您已在 `ReleaseFeedWeightTask` 中实现的 `ResolveDeviceIDs` 方法,避免了重复代码。
|
||||
|
||||
### 3.3. 调整领域服务层 (`PlanService`)
|
||||
|
||||
`PlanService` 将作为此业务用例的核心编排者。借助 `UnitOfWork` 模式,它可以在单个事务中协调多个仓库,完成数据准备和持久化。
|
||||
|
||||
- **职责**:在创建或更新计划的业务流程中,负责解析任务参数、准备设备关联数据,并调用仓库层完成持久化。
|
||||
- **实现**:
|
||||
- 向 `planServiceImpl` 注入 `repository.UnitOfWork` 和 `plan.TaskFactory`。
|
||||
- 在 `CreatePlan` 和 `UpdatePlan` 方法中,使用 `unitOfWork.ExecuteInTransaction` 来包裹整个操作。
|
||||
- 在事务闭包内,遍历计划中的所有任务 (`models.Task`):
|
||||
1. 调用 `taskFactory.CreateTaskFromModel(taskModel)` 创建一个临时的任务领域对象。
|
||||
2. 调用该领域对象的 `ResolveDeviceIDs()` 方法获取设备ID列表。
|
||||
3. 使用事务性的 `DeviceRepository` 查询出设备实体。
|
||||
4. 将查询到的设备实体列表填充到 `taskModel.Devices` 字段中。
|
||||
- 最后,将填充好关联数据的 `plan` 对象传递给事务性的 `PlanRepository` 进行创建或更新。
|
||||
- **优势**:
|
||||
- **职责清晰**:`PlanService` 完整地拥有了“创建/更新计划”的业务逻辑,而仓库层则回归到纯粹的数据访问职责。
|
||||
- **数据一致性**:`UnitOfWork` 确保了从准备数据(查询设备)到最终持久化(创建计划和关联)的所有数据库操作都在一个原子事务中完成。
|
||||
|
||||
### 3.4. 调整仓库层 (`PlanRepository`)
|
||||
|
||||
仓库层被简化,回归其作为数据持久化网关的纯粹角色。
|
||||
|
||||
- **职责**:负责 `Plan` 及其直接子对象(`Task`, `SubPlan`)的 CRUD 操作。
|
||||
- **实现**:
|
||||
- `CreatePlan` 和 `UpdatePlanMetadataAndStructure` 方法将被简化。它们不再需要任何特殊的关联处理逻辑(如 `Association().Replace()`)。
|
||||
- 只需接收一个由 `PlanService` 准备好的、`task.Devices` 字段已被填充的 `plan` 对象。
|
||||
- 在 `CreatePlan` 中,调用 `tx.Create(plan)` 时,GORM 会自动级联创建 `Plan`、`Task` 以及 `device_tasks` 中的关联记录。
|
||||
- 在 `UpdatePlanMetadataAndStructure` 的 `reconcileTasks` 逻辑中,对于新创建的任务,GORM 的 `tx.Create(task)` 同样会自动处理其设备关联。
|
||||
|
||||
### 3.5. 整体流程
|
||||
|
||||
以 **创建计划** 为例:
|
||||
|
||||
1. `PlanController` 调用 `PlanService.CreatePlan(plan)`。
|
||||
2. `PlanService` 调用 `unitOfWork.ExecuteInTransaction` 启动一个数据库事务。
|
||||
3. 在事务闭包内,`PlanService` 遍历 `plan` 对象中的所有 `task`。
|
||||
4. 对于每一个 `task` 模型,调用 `taskFactory.CreateTaskFromModel(task)` 创建一个临时的领域对象。
|
||||
5. 调用该领域对象的 `ResolveDeviceIDs()` 方法,获取其使用的设备 ID 列表。
|
||||
6. 如果返回了设备 ID 列表,则使用事务性的 `DeviceRepository` 查询出 `[]models.Device` 实体。
|
||||
7. 所有 `task` 的关联数据准备好后,调用事务性的 `PlanRepository.CreatePlan(plan)`。GORM 在创建 `plan` 和 `task` 的同时,会自动创建
|
||||
`device_tasks` 表中的关联记录。
|
||||
8. `UnitOfWork` 提交事务。
|
||||
|
||||
**更新计划** 的流程与创建类似,在 `UpdatePlanMetadataAndStructure` 方法中,由于会先删除旧任务再创建新任务,因此在创建新任务后执行相同的设备关联步骤。
|
||||
|
||||
**删除计划** 时,由于 `Task` 模型上配置了 `OnDelete:CASCADE`,GORM 会自动删除关联的 `Task` 记录。同时,GORM 的多对多删除逻辑会自动清理
|
||||
`device_tasks` 表中与被删除任务相关的记录。因此 `DeletePlan` 方法无需修改。
|
||||
|
||||
## 4. 实施步骤
|
||||
|
||||
1. **扩展 `TaskFactory` 接口**
|
||||
- 在 `internal/domain/plan/task.go` 文件中,为 `TaskFactory` 接口添加
|
||||
`CreateTaskFromModel(*models.Task) (TaskDeviceIDResolver, error)` 方法。
|
||||
|
||||
2. **实现 `TaskFactory` 新方法**
|
||||
- 在 `internal/domain/task/task.go` 文件中,为 `taskFactory` 结构体实现 `CreateTaskFromModel` 方法。
|
||||
|
||||
3. **修改 `PlanService`**
|
||||
- 在 `internal/domain/plan/plan_service.go` 中:
|
||||
- 修改 `planServiceImpl` 结构体,增加 `unitOfWork repository.UnitOfWork` 和 `taskFactory TaskFactory` 字段。
|
||||
- 修改 `NewPlanService` 构造函数,接收并注入这些新依赖。
|
||||
- 重构 `CreatePlan` 和 `UpdatePlan` 方法,使用 `UnitOfWork` 包裹事务,并在其中实现数据准备和关联逻辑。
|
||||
|
||||
4. **修改 `PlanRepository`**
|
||||
- 在 `internal/infra/repository/plan_repository.go` 中:
|
||||
- **简化 `CreatePlan` 和 `UpdatePlanMetadataAndStructure` 方法**。移除所有手动处理设备关联的代码(例如,如果之前有 `Association("Devices").Replace()` 等调用,则应删除)。
|
||||
- 确保这两个方法的核心逻辑就是调用 GORM 的 `Create` 或 `Updates`,信任 GORM 会根据传入模型中已填充的 `Devices` 字段来自动维护多对多关联。
|
||||
|
||||
5. **修改依赖注入**
|
||||
- 在 `internal/core/component_initializers.go` (或类似的依赖注入入口文件) 中:
|
||||
- 将 `unitOfWork` 和 `taskFactory` 实例传递给 `plan.NewPlanService` 的构造函数。
|
||||
|
||||
## 5. 结论
|
||||
|
||||
此方案通过复用现有的领域对象和工厂模式,优雅地解决了设备关联维护的问题。它保持了清晰的架构分层和模块职责,在实现功能的同时,为项目未来的扩展和维护奠定了坚实、可扩展的基础。
|
||||
@@ -0,0 +1,103 @@
|
||||
# 设备与任务多对多关联模型设计
|
||||
|
||||
## 需求背景
|
||||
|
||||
用户需要为系统中的“设备”和“任务”增加多对多关联,即一个设备可以执行多个任务,一个任务可以被多个设备执行。
|
||||
|
||||
## 现有模型分析
|
||||
|
||||
### `internal/infra/models/device.go`
|
||||
|
||||
`Device` 模型定义:
|
||||
|
||||
```go
|
||||
type Device struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
DeviceTemplateID uint `gorm:"not null;index" json:"device_template_id"`
|
||||
DeviceTemplate DeviceTemplate `json:"device_template"`
|
||||
AreaControllerID uint `gorm:"not null;index" json:"area_controller_id"`
|
||||
AreaController AreaController `json:"area_controller"`
|
||||
Location string `gorm:"index" json:"location"`
|
||||
Properties datatypes.JSON `json:"properties"`
|
||||
}
|
||||
```
|
||||
|
||||
### `internal/infra/models/plan.go`
|
||||
|
||||
`Task` 模型定义:
|
||||
|
||||
```go
|
||||
type Task struct {
|
||||
ID int `gorm:"primarykey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
|
||||
PlanID uint `gorm:"not null;index" json:"plan_id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Description string `json:"description"`
|
||||
ExecutionOrder int `gorm:"not null" json:"execution_order"`
|
||||
Type TaskType `gorm:"not null" json:"type"`
|
||||
Parameters datatypes.JSON `json:"parameters"`
|
||||
}
|
||||
```
|
||||
|
||||
## 方案设计
|
||||
|
||||
为了实现设备和任务的多对多关系,我们将引入一个中间关联模型 `DeviceTask`。考虑到 `Task` 模型定义在 `plan.go` 中,为了保持相关模型的内聚性,我们将 `DeviceTask` 模型也定义在 `internal/infra/models/plan.go` 文件中。
|
||||
|
||||
### 1. 在 `internal/infra/models/plan.go` 中新增 `DeviceTask` 关联模型
|
||||
|
||||
`DeviceTask` 模型将包含 `DeviceID` 和 `TaskID` 作为外键,以及 GORM 的标准模型字段。
|
||||
|
||||
```go
|
||||
// DeviceTask 是设备和任务之间的关联模型,表示一个设备可以执行多个任务,一个任务可以被多个设备执行。
|
||||
type DeviceTask struct {
|
||||
gorm.Model
|
||||
DeviceID uint `gorm:"not null;index"` // 设备ID
|
||||
TaskID uint `gorm:"not null;index"` // 任务ID
|
||||
|
||||
// 可选:如果需要存储关联的额外信息,可以在这里添加字段,例如:
|
||||
// Configuration datatypes.JSON `json:"configuration"` // 任务在特定设备上的配置
|
||||
}
|
||||
|
||||
// TableName 自定义 GORM 使用的数据库表名
|
||||
func (DeviceTask) TableName() string {
|
||||
return "device_tasks"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 修改 `internal/infra/models/device.go`
|
||||
|
||||
在 `Device` 结构体中添加 `Tasks` 字段,通过 `gorm:"many2many:device_tasks;"` 标签声明与 `Task` 的多对多关系,并指定中间表名为 `device_tasks`。
|
||||
|
||||
```go
|
||||
// Device 代表系统中的所有普通设备
|
||||
type Device struct {
|
||||
gorm.Model
|
||||
|
||||
// ... 其他现有字段 ...
|
||||
|
||||
// Tasks 是与此设备关联的任务列表,通过 DeviceTask 关联表实现多对多关系
|
||||
Tasks []Task `gorm:"many2many:device_tasks;" json:"tasks"`
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 修改 `internal/infra/models/plan.go`
|
||||
|
||||
在 `Task` 结构体中添加 `Devices` 字段,通过 `gorm:"many2many:device_tasks;"` 标签声明与 `Device` 的多对多关系,并指定中间表名为 `device_tasks`。
|
||||
|
||||
```go
|
||||
// Task 代表计划中的一个任务,具有执行顺序
|
||||
type Task struct {
|
||||
// ... 其他现有字段 ...
|
||||
|
||||
// Devices 是与此任务关联的设备列表,通过 DeviceTask 关联表实现多对多关系
|
||||
Devices []Device `gorm:"many2many:device_tasks;" json:"devices"`
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
通过上述修改,我们将在数据库中创建一个名为 `device_tasks` 的中间表,用于存储 `Device` 和 `Task` 之间的关联关系。在 Go 代码层面,`Device` 和 `Task` 模型将能够直接通过 `Tasks` 和 `Devices` 字段进行多对多关系的查询和操作。
|
||||
@@ -0,0 +1,24 @@
|
||||
# 需求
|
||||
|
||||
删除设备/设备模板/区域主控前进行校验
|
||||
|
||||
## issue
|
||||
|
||||
http://git.huangwc.com/pig/pig-farm-controller/issues/50
|
||||
|
||||
## 需求描述
|
||||
|
||||
1. 删除设备时检测是否被任务使用
|
||||
2. 删除设备模板时检测是否被设备使用
|
||||
3. 删除区域主控时检测是否被设备使用
|
||||
|
||||
# 实现
|
||||
|
||||
1. [重构计划领域](./plan_service_refactor.md)
|
||||
2. [让任务可以提供自身使用设备](./add_get_device_id_configs_to_task.md)
|
||||
3. [现有计划管理逻辑迁移](./plan_service_refactor_to_domain.md)
|
||||
4. [增加设备任务关联表](./device_task_many_to_many_design.md)
|
||||
5. [增加任务增删改查时对设备任务关联表的维护](./device_task_association_maintenance.md)
|
||||
6. [删除设备时检查](./check_before_device_deletion.md)
|
||||
7. [删除设备模板时检查和删除区域主控时检查](./refactor_deletion_check.md)
|
||||
8. [优化设备服务方法的入参](./refactor_id_conversion.md)
|
||||
@@ -0,0 +1,83 @@
|
||||
# 计划服务重构设计方案
|
||||
|
||||
## 1. 目标
|
||||
|
||||
将 `internal/domain/scheduler` 包重构为 `internal/domain/plan`,并创建一个新的 `Service` 对象,将原 `scheduler`
|
||||
包中的核心调度逻辑集成到 `Service` 中作为一个子服务,统一由 `Service`
|
||||
对外提供服务。此重构旨在提高代码的模块化、可维护性和可测试性,并为后续的“设备删除前校验”功能奠定基础。
|
||||
|
||||
## 2. 方案详情
|
||||
|
||||
### 2.1. 包重命名
|
||||
|
||||
* 将 `internal/domain/scheduler` 目录重命名为 `internal/domain/plan`。
|
||||
* 修改 `internal/domain/plan` 目录下所有 Go 文件中的 `package scheduler` 为 `package plan`。
|
||||
* 更新 `internal/domain/plan` 目录下所有 Go 文件中所有引用
|
||||
`git.huangwc.com/pig/pig-farm-controller/internal/domain/scheduler` 的导入路径为
|
||||
`git.huangwc.com/pig/pig-farm-controller/internal/domain/plan`。
|
||||
|
||||
### 2.2. `internal/domain/plan` 包内部结构调整
|
||||
|
||||
* **`internal/domain/plan/task.go`**:
|
||||
* 保持不变。它定义了任务的接口和工厂,是领域内的核心抽象。
|
||||
|
||||
* **`internal/domain/plan/plan_execution_manager.go`**:
|
||||
* 将 `Scheduler` 结构体更名为 `ExecutionManagerImpl`。这个名称更准确地反映了它作为计划任务执行的协调者和管理者的具体实现。
|
||||
* 将 `NewScheduler` 构造函数更名为 `NewExecutionManagerImpl`。
|
||||
* 文件内部所有对 `Scheduler` 的引用都将更新为 `ExecutionManagerImpl`。
|
||||
|
||||
* **`internal/domain/plan/analysis_plan_task_manager.go`**:
|
||||
* 将 `AnalysisPlanTaskManager` 结构体更名为 `AnalysisPlanTaskManagerImpl`。
|
||||
* 将 `NewAnalysisPlanTaskManager` 构造函数更名为 `NewAnalysisPlanTaskManagerImpl`。
|
||||
* 文件内部所有对 `AnalysisPlanTaskManager` 的引用都将更新为 `AnalysisPlanTaskManagerImpl`。
|
||||
|
||||
* **定义领域层接口**:
|
||||
* 在 `internal/domain/plan` 包中定义 `ExecutionManager` 接口,包含 `ExecutionManagerImpl` 对外暴露的所有公共方法。
|
||||
* 在 `internal/domain/plan` 包中定义 `AnalysisPlanTaskManager` 接口,包含 `AnalysisPlanTaskManagerImpl` 对外暴露的所有公共方法。
|
||||
* `ExecutionManagerImpl` 和 `AnalysisPlanTaskManagerImpl` 将分别实现对应的接口。
|
||||
|
||||
### 2.3. 创建 `internal/domain/plan/plan_service.go`
|
||||
|
||||
* 创建新文件 `internal/domain/plan/plan_service.go`。
|
||||
|
||||
* **定义领域服务接口**:
|
||||
* 在 `internal/domain/plan` 包中定义 `Service` 接口,该接口将聚合 `ExecutionManager` 和 `AnalysisPlanTaskManager`
|
||||
的所有公共方法,并由 `planServiceImpl` 实现这些方法的委托。
|
||||
|
||||
* **实现领域服务**:
|
||||
* 该文件将定义 `planServiceImpl` 结构体,并包含 `ExecutionManager` 接口和 `AnalysisPlanTaskManager` 接口的实例作为其依赖。
|
||||
* 实现 `NewService` 构造函数,负责接收 `ExecutionManager` 接口和 `AnalysisPlanTaskManager` 接口的实例,并将其注入到
|
||||
`planServiceImpl` 中。
|
||||
* `planServiceImpl` 将对外提供高层次的 API,这些 API 会协调调用其依赖的接口方法。例如:
|
||||
* `Service.Start()` 方法会调用 `ExecutionManager` 接口的 `Start()` 方法。
|
||||
* `Service.Stop()` 方法会调用 `ExecutionManager` 接口的 `Stop()` 方法。
|
||||
* `Service.RefreshPlanTriggers()` 方法会调用 `AnalysisPlanTaskManager` 接口的 `Refresh()` 方法。
|
||||
* `Service.CreateOrUpdateTrigger()` 方法会调用 `AnalysisPlanTaskManager` 接口的 `CreateOrUpdateTrigger()` 方法。
|
||||
* `Service.EnsureAnalysisTaskDefinition()` 方法会调用 `AnalysisPlanTaskManager` 接口的
|
||||
`EnsureAnalysisTaskDefinition()` 方法。
|
||||
* 未来所有与计划相关的领域操作,都将通过 `Service` 接口进行。
|
||||
|
||||
### 2.4. 调整依赖注入和引用
|
||||
|
||||
* **查找并替换导入路径:** 使用 `grep` 命令查找整个项目中所有引用
|
||||
`git.huangwc.com/pig/pig-farm-controller/internal/domain/scheduler` 的地方,并将其替换为
|
||||
`git.huangwc.com/pig/pig-farm-controller/internal/domain/plan`。
|
||||
* **更新 `internal/core/component_initializers.go`**:
|
||||
* 在初始化阶段,我们将创建 `plan.ExecutionManagerImpl` 和 `plan.AnalysisPlanTaskManagerImpl` 的具体实例。
|
||||
* 然后,将这些具体实例作为 `plan.ExecutionManager` 接口和 `plan.AnalysisPlanTaskManager` 接口类型传递给
|
||||
`plan.NewService` 构造函数,创建 `planServiceImpl` 实例。
|
||||
* 最终,`plan.NewService` 返回 `plan.Service` 接口类型。
|
||||
* 应用程序的其他部分将通过 `plan.Service` 接口来访问计划相关的逻辑,而不是直接访问底层的管理器或其具体实现。
|
||||
|
||||
## 3. 优势
|
||||
|
||||
* **职责分离清晰:** `internal/domain/plan` 包专注于计划领域的核心逻辑和管理,并提供统一的 `Service` 接口作为领域服务的入口。
|
||||
* **符合领域驱动设计:** 领域层包含核心业务逻辑和管理器,应用层(如果需要)作为领域层的协调者。
|
||||
* **与现有项目风格一致:** 借鉴 `domain/pig` 包的模式,提高了项目内部的一致性。
|
||||
* **可测试性增强:** `Service` 可以更容易地进行单元测试,因为其依赖的接口可以被模拟。
|
||||
* **可维护性提高:** 当计划相关的业务逻辑发生变化时,可以更精确地定位到需要修改的组件。
|
||||
* **松耦合:** `Service` 不依赖于具体的实现,而是依赖于接口,提高了系统的灵活性和可扩展性。
|
||||
|
||||
## 4. 验证和测试
|
||||
|
||||
在完成所有修改后,需要运行项目并进行测试,确保调度器功能正常,没有引入新的错误。
|
||||
@@ -0,0 +1,179 @@
|
||||
# 重构方案:将 `app/service/plan_service.go` 的核心逻辑迁移到 `domain/plan/plan_service.go`
|
||||
|
||||
## 目标:
|
||||
|
||||
* `app/service/plan_service.go` (应用服务层): 仅负责接收 DTO、将 DTO 转换为领域实体、调用 `domain/plan/plan_service` 的领域方法,并将领域方法返回的领域实体转换为 DTO 返回。
|
||||
* `domain/plan/plan_service.go` (领域层): 封装所有与计划相关的业务逻辑、验证规则、状态管理以及对领域实体的查询操作。
|
||||
|
||||
## 详细步骤:
|
||||
|
||||
### 第一步:修改 `domain/plan/plan_service.go` (领域层)
|
||||
|
||||
1. **引入必要的依赖**:
|
||||
* `repository.PlanRepository`:用于与计划数据存储交互。
|
||||
* `repository.DeviceRepository`:如果计划逻辑中需要设备信息。
|
||||
* `models.Plan`:领域实体。
|
||||
* `errors` 和 `gorm.ErrRecordNotFound`:用于错误处理。
|
||||
* `models.PlanTypeSystem`, `models.PlanStatusEnabled`, `models.PlanContentTypeSubPlans`, `models.PlanContentTypeTasks` 等常量。
|
||||
* `git.huangwc.com/pig/pig-farm-controller/internal/infra/models`
|
||||
* `git.huangwc.com/pig/pig-farm-controller/internal/infra/repository`
|
||||
* `errors`
|
||||
* `gorm.io/gorm`
|
||||
|
||||
2. **定义领域层错误**: 将 `app/service/plan_service.go` 中定义的错误(`ErrPlanNotFound`, `ErrPlanCannotBeModified` 等)迁移到 `domain/plan/plan_service.go`,并根据领域层的语义进行调整。
|
||||
|
||||
```go
|
||||
var (
|
||||
// ErrPlanNotFound 表示未找到计划
|
||||
ErrPlanNotFound = errors.New("计划不存在")
|
||||
// ErrPlanCannotBeModified 表示计划不允许修改
|
||||
ErrPlanCannotBeModified = errors.New("系统计划不允许修改")
|
||||
// ErrPlanCannotBeDeleted 表示计划不允许删除
|
||||
ErrPlanCannotBeDeleted = errors.New("系统计划不允许删除")
|
||||
// ErrPlanCannotBeStarted 表示计划不允许手动启动
|
||||
ErrPlanCannotBeStarted = errors.New("系统计划不允许手动启动")
|
||||
// ErrPlanAlreadyEnabled 表示计划已处于启动状态
|
||||
ErrPlanAlreadyEnabled = errors.New("计划已处于启动状态,无需重复操作")
|
||||
// ErrPlanNotEnabled 表示计划未处于启动状态
|
||||
ErrPlanNotEnabled = errors.New("计划当前不是启用状态")
|
||||
// ErrPlanCannotBeStopped 表示计划不允许停止
|
||||
ErrPlanCannotBeStopped = errors.New("系统计划不允许停止")
|
||||
)
|
||||
```
|
||||
|
||||
3. **扩展 `plan.Service` 接口**:
|
||||
* 将 `app/service/plan_service.go` 中 `PlanService` 接口的所有方法(`CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, `StartPlan`, `StopPlan`)添加到 `domain/plan/Service` 接口中。
|
||||
* 这些方法的参数和返回值将直接使用领域实体(`*models.Plan`)或基本类型,而不是 DTO。例如:
|
||||
* `CreatePlan(plan *models.Plan) (*models.Plan, error)`
|
||||
* `GetPlanByID(id uint) (*models.Plan, error)`
|
||||
* `ListPlans(opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)`
|
||||
* `UpdatePlan(plan *models.Plan) (*models.Plan, error)`
|
||||
* `DeletePlan(id uint) error`
|
||||
* `StartPlan(id uint) error`
|
||||
* `StopPlan(id uint) error`
|
||||
|
||||
4. **修改 `planServiceImpl` 结构体**:
|
||||
* 添加 `planRepo repository.PlanRepository` 字段。
|
||||
* 添加 `deviceRepo repository.DeviceRepository` 字段 (如果需要)。
|
||||
* `analysisPlanTaskManager plan.AnalysisPlanTaskManager` 字段保持不变。
|
||||
|
||||
```go
|
||||
type planServiceImpl struct {
|
||||
executionManager ExecutionManager
|
||||
taskManager AnalysisPlanTaskManager
|
||||
planRepo repository.PlanRepository // 新增
|
||||
// deviceRepo repository.DeviceRepository // 如果需要,新增
|
||||
logger *logs.Logger
|
||||
}
|
||||
```
|
||||
|
||||
5. **实现 `plan.Service` 接口中的新方法**:
|
||||
* 将 `app/service/plan_service.go` 中 `planService` 的所有业务逻辑方法(`CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, `StartPlan`, `StopPlan`)的实现,迁移到 `domain/plan/planServiceImpl` 中。
|
||||
* **关键修改点**:
|
||||
* **参数和返回值**: 确保这些方法现在接收和返回的是 `*models.Plan` 或其他领域实体,而不是 DTO。
|
||||
* **业务逻辑**: 保留所有的业务规则、验证和状态管理逻辑。
|
||||
* **依赖**: 这些方法将直接调用 `planRepo` 和 `analysisPlanTaskManager`。
|
||||
* **日志**: 日志记录保持不变,但可能需要调整日志信息以反映领域层的上下文。
|
||||
* **错误处理**: 错误处理逻辑保持不变,但现在将返回领域层定义的错误。
|
||||
* **ContentType 自动判断**: `CreatePlan` 和 `UpdatePlan` 中的 `ContentType` 自动判断逻辑应该保留在领域层。
|
||||
* **执行计数器重置**: `UpdatePlan` 和 `StartPlan` 中的执行计数器重置逻辑应该保留在领域层。
|
||||
* **系统计划限制**: 对系统计划的修改、删除、启动、停止限制逻辑应该保留在领域层。
|
||||
* **验证和重排顺序**: `models.Plan` 的 `ValidateExecutionOrder()` 和 `ReorderSteps()` 方法的调用应该在 `CreatePlan` 和 `UpdatePlan` 方法的领域层实现中进行,而不是在 DTO 转换函数中。
|
||||
|
||||
6. **修改 `NewPlanService` 函数**: 接收 `repository.PlanRepository` 和 `repository.DeviceRepository` (如果需要) 作为参数,并注入到 `planServiceImpl` 中。
|
||||
|
||||
```go
|
||||
func NewPlanService(
|
||||
executionManager ExecutionManager,
|
||||
taskManager AnalysisPlanTaskManager,
|
||||
planRepo repository.PlanRepository, // 新增
|
||||
// deviceRepo repository.DeviceRepository, // 如果需要,新增
|
||||
logger *logs.Logger,
|
||||
) Service {
|
||||
return &planServiceImpl{
|
||||
executionManager: executionManager,
|
||||
taskManager: taskManager,
|
||||
planRepo: planRepo, // 注入
|
||||
// deviceRepo: deviceRepo, // 注入
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步:修改 `app/service/plan_service.go` (应用服务层)
|
||||
|
||||
1. **修改 `planService` 结构体**:
|
||||
* 移除 `planRepo repository.PlanRepository` 字段。
|
||||
* 将 `analysisPlanTaskManager plan.AnalysisPlanTaskManager` 字段替换为 `domainPlanService plan.Service`。
|
||||
|
||||
```go
|
||||
type planService struct {
|
||||
logger *logs.Logger
|
||||
// planRepo repository.PlanRepository // 移除
|
||||
domainPlanService plan.Service // 替换为领域层的服务接口
|
||||
// analysisPlanTaskManager plan.AnalysisPlanTaskManager // 移除,由 domainPlanService 内部持有
|
||||
}
|
||||
```
|
||||
|
||||
2. **修改 `NewPlanService` 函数**:
|
||||
* 接收 `domainPlanService plan.Service` 作为参数。
|
||||
* 将 `planRepo` 和 `analysisPlanTaskManager` 的注入替换为 `domainPlanService`。
|
||||
|
||||
```go
|
||||
func NewPlanService(
|
||||
logger *logs.Logger,
|
||||
// planRepo repository.PlanRepository, // 移除
|
||||
domainPlanService plan.Service, // 接收领域层服务
|
||||
// analysisPlanTaskManager plan.AnalysisPlanTaskManager, // 移除
|
||||
) PlanService {
|
||||
return &planService{
|
||||
logger: logger,
|
||||
domainPlanService: domainPlanService, // 注入领域层服务
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **修改 `PlanService` 接口**:
|
||||
* 接口定义保持不变,仍然接收和返回 DTO。
|
||||
|
||||
4. **修改 `planService` 接口实现**:
|
||||
* **`CreatePlan`**:
|
||||
* 接收 `dto.CreatePlanRequest`。
|
||||
* 使用 `dto.NewPlanFromCreateRequest` 将 DTO 转换为 `*models.Plan`。**注意:此时 `NewPlanFromCreateRequest` 不再包含 `ValidateExecutionOrder()` 和 `ReorderSteps()` 的调用。**
|
||||
* 调用 `s.domainPlanService.CreatePlan(*models.Plan)`。
|
||||
* 将返回的 `*models.Plan` 转换为 `dto.PlanResponse`。
|
||||
* **`GetPlanByID`**:
|
||||
* 调用 `s.domainPlanService.GetPlanByID(id)`。
|
||||
* 将返回的 `*models.Plan` 转换为 `dto.PlanResponse`。
|
||||
* **`ListPlans`**:
|
||||
* 将 `dto.ListPlansQuery` 转换为 `repository.ListPlansOptions`。
|
||||
* 调用 `s.domainPlanService.ListPlans(...)`。
|
||||
* 将返回的 `[]models.Plan` 转换为 `[]dto.PlanResponse`。
|
||||
* **`UpdatePlan`**:
|
||||
* 使用 `dto.NewPlanFromUpdateRequest` 将 `dto.UpdatePlanRequest` 转换为 `*models.Plan`。**注意:此时 `NewPlanFromUpdateRequest` 不再包含 `ValidateExecutionOrder()` 和 `ReorderSteps()` 的调用。**
|
||||
* 设置 `plan.ID = id`。
|
||||
* 调用 `s.domainPlanService.UpdatePlan(*models.Plan)`。
|
||||
* 将返回的 `*models.Plan` 转换为 `dto.PlanResponse`。
|
||||
* **`DeletePlan`**:
|
||||
* 调用 `s.domainPlanService.DeletePlan(id)`。
|
||||
* **`StartPlan`**:
|
||||
* 调用 `s.domainPlanService.StartPlan(id)`。
|
||||
* **`StopPlan`**:
|
||||
* 调用 `s.domainPlanService.StopPlan(id)`。
|
||||
* **错误处理**: 应用服务层将捕获领域层返回的错误,并可能将其转换为更适合应用服务层或表示层的错误信息(例如,将领域层的 `ErrPlanNotFound` 转换为 `app/service` 层定义的 `ErrPlanNotFound`)。
|
||||
|
||||
### 第三步:修改 `internal/app/dto/plan_converter.go`
|
||||
|
||||
1. **移除 `NewPlanFromCreateRequest` 和 `NewPlanFromUpdateRequest` 中的领域逻辑**:
|
||||
* 从 `NewPlanFromCreateRequest` 和 `NewPlanFromUpdateRequest` 函数中移除 `plan.ValidateExecutionOrder()` 和 `plan.ReorderSteps()` 的调用。这些逻辑应该由领域服务来处理。
|
||||
|
||||
### 第四步:更新 `main.go` 或其他依赖注入点
|
||||
|
||||
* 调整 `NewPlanService` 的调用,确保正确注入 `domain/plan/Service` 的实现。
|
||||
|
||||
## 风险与注意事项:
|
||||
|
||||
* **事务管理**: 如果领域层的方法需要事务,确保事务在领域层内部或由应用服务层协调。
|
||||
* **错误映射**: 仔细处理领域层错误到应用服务层错误的映射,确保对外暴露的错误信息是恰当的。
|
||||
* **循环依赖**: 确保 `domain` 层不依赖 `app` 层,`app` 层可以依赖 `domain` 层。
|
||||
* **测试**: 重构后需要对所有相关功能进行全面的单元测试和集成测试。
|
||||
@@ -0,0 +1,196 @@
|
||||
# 重构方案:将删除前关联检查逻辑迁移至 Service 层
|
||||
|
||||
## 1. 目标
|
||||
|
||||
将删除**区域主控 (AreaController)** 和**设备模板 (DeviceTemplate)** 时的关联设备检查逻辑,从 Repository(数据访问)层重构至 Service(业务逻辑)层。
|
||||
|
||||
## 2. 动机
|
||||
|
||||
当前实现中,关联检查逻辑位于 Repository 层的 `Delete` 方法内。这违反了分层架构的最佳实践。Repository 层应仅负责单纯的数据持久化操作(增删改查),而不应包含业务规则。
|
||||
|
||||
通过本次重构,我们将实现:
|
||||
- **职责分离**: Service 层负责编排业务逻辑(如“删除前必须检查关联”),Repository 层回归其数据访问的单一职责。
|
||||
- **代码清晰**: 业务流程在 Service 层一目了然,便于理解和维护。
|
||||
- **可测试性增强**: 可以独立测试 Service 层的业务规则,而无需依赖数据库的事务或约束。
|
||||
|
||||
## 3. 详细实施步骤
|
||||
|
||||
### 第 1 步:在 Service 层定义业务错误
|
||||
|
||||
在 `internal/app/service/device_service.go` 文件中,导出两个新的错误变量,用于清晰地表达业务约束。
|
||||
|
||||
```go
|
||||
// ErrAreaControllerInUse 表示区域主控正在被设备使用,无法删除
|
||||
var ErrAreaControllerInUse = errors.New("区域主控正在被一个或多个设备使用,无法删除")
|
||||
|
||||
// ErrDeviceTemplateInUse 表示设备模板正在被设备使用,无法删除
|
||||
var ErrDeviceTemplateInUse = errors.New("设备模板正在被一个或多个设备使用,无法删除")
|
||||
```
|
||||
|
||||
### 第 2 步:调整 Repository 接口与实现
|
||||
|
||||
#### 2.1 `device_repository.go`
|
||||
在 `DeviceRepository` 接口中增加一个方法,用于检查区域主控是否被使用,并在 `gormDeviceRepository` 中实现它。
|
||||
|
||||
```go
|
||||
// internal/infra/repository/device_repository.go
|
||||
|
||||
type DeviceRepository interface {
|
||||
// ... 其他方法
|
||||
IsAreaControllerInUse(areaControllerID uint) (bool, error)
|
||||
}
|
||||
|
||||
func (r *gormDeviceRepository) IsAreaControllerInUse(areaControllerID uint) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil {
|
||||
return false, fmt.Errorf("检查区域主控使用情况失败: %w", err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 `area_controller_repository.go`
|
||||
简化 `Delete` 方法,移除所有业务逻辑,使其成为一个纯粹的数据库删除操作。
|
||||
|
||||
```go
|
||||
// internal/infra/repository/area_controller_repository.go
|
||||
|
||||
func (r *gormAreaControllerRepository) Delete(id uint) error {
|
||||
// 移除原有的事务和关联检查
|
||||
if err := r.db.Delete(&models.AreaController{}, id).Error; err != nil {
|
||||
return fmt.Errorf("删除区域主控失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 `device_template_repository.go`
|
||||
同样,简化 `Delete` 方法。`IsInUse` 方法保持不变,因为它仍然是一个有用的查询。
|
||||
|
||||
```go
|
||||
// internal/infra/repository/device_template_repository.go
|
||||
|
||||
func (r *gormDeviceTemplateRepository) Delete(id uint) error {
|
||||
// 移除原有的关联检查逻辑
|
||||
if err := r.db.Delete(&models.DeviceTemplate{}, id).Error; err != nil {
|
||||
return fmt.Errorf("删除设备模板失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 第 3 步:在 Service 层实现业务逻辑
|
||||
|
||||
#### 3.1 `device_service.go` - 删除区域主控
|
||||
修改 `DeleteAreaController` 方法,加入关联检查的业务逻辑。
|
||||
|
||||
```go
|
||||
// internal/app/service/device_service.go
|
||||
|
||||
func (s *deviceService) DeleteAreaController(id string) error {
|
||||
idUint, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无效的ID格式: %w", err)
|
||||
}
|
||||
acID := uint(idUint)
|
||||
|
||||
// 1. 检查是否存在
|
||||
_, err = s.areaControllerRepo.FindByID(acID)
|
||||
if err != nil {
|
||||
return err // 如果未找到,gorm会返回 ErrRecordNotFound
|
||||
}
|
||||
|
||||
// 2. 检查是否被使用(业务逻辑)
|
||||
inUse, err := s.deviceRepo.IsAreaControllerInUse(acID)
|
||||
if err != nil {
|
||||
return err // 返回数据库检查错误
|
||||
}
|
||||
if inUse {
|
||||
return ErrAreaControllerInUse // 返回业务错误
|
||||
}
|
||||
|
||||
// 3. 执行删除
|
||||
return s.areaControllerRepo.Delete(acID)
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 `device_service.go` - 删除设备模板
|
||||
修改 `DeleteDeviceTemplate` 方法,加入关联检查的业务逻辑。
|
||||
|
||||
```go
|
||||
// internal/app/service/device_service.go
|
||||
|
||||
func (s *deviceService) DeleteDeviceTemplate(id string) error {
|
||||
idUint, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无效的ID格式: %w", err)
|
||||
}
|
||||
dtID := uint(idUint)
|
||||
|
||||
// 1. 检查是否存在
|
||||
_, err = s.deviceTemplateRepo.FindByID(dtID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 检查是否被使用(业务逻辑)
|
||||
inUse, err := s.deviceTemplateRepo.IsInUse(dtID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inUse {
|
||||
return ErrDeviceTemplateInUse // 返回业务错误
|
||||
}
|
||||
|
||||
// 3. 执行删除
|
||||
return s.deviceTemplateRepo.Delete(dtID)
|
||||
}
|
||||
```
|
||||
|
||||
### 第 4 步:在 Controller 层处理新的业务错误
|
||||
|
||||
#### 4.1 `device_controller.go` - 删除区域主控
|
||||
在 `DeleteAreaController` 的错误处理中,增加对 `ErrAreaControllerInUse` 的捕获,并返回 `409 Conflict` 状态码。
|
||||
|
||||
```go
|
||||
// internal/app/controller/device/device_controller.go
|
||||
|
||||
func (c *Controller) DeleteAreaController(ctx echo.Context) error {
|
||||
// ...
|
||||
if err := c.deviceService.DeleteAreaController(acID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
// ...
|
||||
case errors.Is(err, service.ErrAreaControllerInUse): // 新增
|
||||
c.logger.Warnf("%s: 尝试删除正在被使用的主控, ID: %s", actionType, acID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "主控正在被使用", acID)
|
||||
default:
|
||||
// ...
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 `device_controller.go` - 删除设备模板
|
||||
在 `DeleteDeviceTemplate` 的错误处理中,增加对 `ErrDeviceTemplateInUse` 的捕获,并返回 `409 Conflict` 状态码。
|
||||
|
||||
```go
|
||||
// internal/app/controller/device/device_controller.go
|
||||
|
||||
func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error {
|
||||
// ...
|
||||
if err := c.deviceService.DeleteDeviceTemplate(dtID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
// ...
|
||||
case errors.Is(err, service.ErrDeviceTemplateInUse): // 新增
|
||||
c.logger.Warnf("%s: 尝试删除正在被使用的模板, ID: %s", actionType, dtID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "模板正在被使用", dtID)
|
||||
default:
|
||||
// ...
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
# 重构方案:将 ID 类型转换逻辑迁移至 Controller 层
|
||||
|
||||
## 1. 目标
|
||||
|
||||
将所有通过 URL 路径传入的 `id`(`string` 类型),其到 `uint` 类型的转换和验证逻辑,从 Service(业务逻辑)层统一迁移至 Controller(控制器)层。
|
||||
|
||||
## 2. 动机
|
||||
|
||||
当前实现中,Controller 将从 URL 获取的 `string` 类型的 ID 直接传递给 Service 层,由 Service 层负责使用 `strconv.ParseUint` 进行类型转换。
|
||||
|
||||
这种模式存在以下问题:
|
||||
- **职责不清**:Service 层被迫处理了本应属于输入验证和转换的逻辑,而这部分工作更贴近 Controller 的职责。
|
||||
- **Service 不纯粹**:业务核心逻辑与原始输入(字符串)耦合,降低了 Service 的可复用性。理想情况下,Service 的接口应该只处理内部定义的、类型安全的数据。
|
||||
- **延迟的错误处理**:对于一个无效的 ID(如 "abc"),请求会穿透到 Service 层才会失败,而这种格式错误在 Controller 层就应该被拦截。
|
||||
|
||||
通过本次重构,我们将实现:
|
||||
- **职责分离**:Controller 负责处理 HTTP 请求的原始数据(验证、转换),Service 负责处理核心业务。
|
||||
- **接口清晰**:Service 层的所有方法将只接受类型安全的 `uint` 作为 ID,使其意图更加明确。
|
||||
- **快速失败**:无效的 ID 将在 Controller 层被立即拒绝,并返回 `400 Bad Request`,提高了系统的健壮性。
|
||||
|
||||
## 3. 详细实施步骤
|
||||
|
||||
### 第 1 步:修改 `device_service.go`
|
||||
|
||||
#### 3.1 修改 `DeviceService` 接口
|
||||
所有接收 `id string` 参数的方法签名,全部修改为接收 `id uint`。
|
||||
|
||||
**受影响的方法列表:**
|
||||
- `GetDevice(id string)` -> `GetDevice(id uint)`
|
||||
- `UpdateDevice(id string, ...)` -> `UpdateDevice(id uint, ...)`
|
||||
- `DeleteDevice(id string)` -> `DeleteDevice(id uint)`
|
||||
- `ManualControl(id string, ...)` -> `ManualControl(id uint, ...)`
|
||||
- `GetAreaController(id string)` -> `GetAreaController(id uint)`
|
||||
- `UpdateAreaController(id string, ...)` -> `UpdateAreaController(id uint, ...)`
|
||||
- `DeleteAreaController(id string)` -> `DeleteAreaController(id uint)`
|
||||
- `GetDeviceTemplate(id string)` -> `GetDeviceTemplate(id uint)`
|
||||
- `UpdateDeviceTemplate(id string, ...)` -> `UpdateDeviceTemplate(id uint, ...)`
|
||||
- `DeleteDeviceTemplate(id string)` -> `DeleteDeviceTemplate(id uint)`
|
||||
|
||||
#### 3.2 修改 `deviceService` 实现
|
||||
在 `deviceService` 的方法实现中,移除所有 `strconv.ParseUint` 的调用,直接使用传入的 `uint` 类型的 ID。
|
||||
|
||||
**示例 (`DeleteDeviceTemplate`):**
|
||||
|
||||
**修改前:**
|
||||
```go
|
||||
func (s *deviceService) DeleteDeviceTemplate(id string) error {
|
||||
idUint, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无效的ID格式: %w", err)
|
||||
}
|
||||
dtID := uint(idUint)
|
||||
// ... 业务逻辑
|
||||
}
|
||||
```
|
||||
|
||||
**修改后:**
|
||||
```go
|
||||
func (s *deviceService) DeleteDeviceTemplate(id uint) error {
|
||||
// 直接使用 id
|
||||
// ... 业务逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 第 2 步:修改 `device_controller.go`
|
||||
|
||||
在所有调用受影响 Service 方法的 Controller 方法中,增加 ID 的转换和错误处理逻辑。
|
||||
|
||||
**示例 (`DeleteDeviceTemplate`):**
|
||||
|
||||
**修改前:**
|
||||
```go
|
||||
func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error {
|
||||
const actionType = "删除设备模板"
|
||||
dtID := ctx.Param("id") // dtID is a string
|
||||
|
||||
if err := c.deviceService.DeleteDeviceTemplate(dtID); err != nil {
|
||||
// ... 错误处理
|
||||
}
|
||||
// ... 成功处理
|
||||
}
|
||||
```
|
||||
|
||||
**修改后:**
|
||||
```go
|
||||
func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error {
|
||||
const actionType = "删除设备模板"
|
||||
idStr := ctx.Param("id")
|
||||
|
||||
// 在 Controller 层进行转换和验证
|
||||
idUint, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Warnf("%s: 无效的ID格式: %s", actionType, idStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", actionType, "ID格式错误", idStr)
|
||||
}
|
||||
dtID := uint(idUint)
|
||||
|
||||
// 调用 Service,传入 uint 类型的 ID
|
||||
if err := c.deviceService.DeleteDeviceTemplate(dtID); err != nil {
|
||||
// ... 错误处理 (保持不变)
|
||||
}
|
||||
|
||||
// ... 成功处理
|
||||
}
|
||||
```
|
||||
此模式将应用于所有受影响的 Controller 方法。
|
||||
420
docs/docs.go
420
docs/docs.go
@@ -776,7 +776,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -855,7 +855,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -934,7 +934,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -998,7 +998,6 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
7,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
@@ -1008,12 +1007,12 @@ const docTemplate = `{
|
||||
5,
|
||||
-1,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"x-enum-varnames": [
|
||||
"_numLevels",
|
||||
"DebugLevel",
|
||||
"InfoLevel",
|
||||
"WarnLevel",
|
||||
@@ -1023,7 +1022,8 @@ const docTemplate = `{
|
||||
"FatalLevel",
|
||||
"_minLevel",
|
||||
"_maxLevel",
|
||||
"InvalidLevel"
|
||||
"InvalidLevel",
|
||||
"_numLevels"
|
||||
],
|
||||
"name": "level",
|
||||
"in": "query"
|
||||
@@ -1057,7 +1057,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1156,7 +1156,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1235,7 +1235,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1309,7 +1309,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1393,7 +1393,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1467,7 +1467,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1561,7 +1561,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1640,7 +1640,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1714,7 +1714,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1788,7 +1788,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1872,7 +1872,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1941,7 +1941,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -2025,7 +2025,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -2104,7 +2104,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -2178,7 +2178,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -3415,7 +3415,7 @@ const docTemplate = `{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "创建一个新的猪舍",
|
||||
"description": "根据提供的信息创建一个新猪舍",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3600,7 +3600,7 @@ const docTemplate = `{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取所有计划的列表",
|
||||
"description": "获取所有计划的列表,支持按类型过滤和分页",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3608,6 +3608,36 @@ const docTemplate = `{
|
||||
"计划管理"
|
||||
],
|
||||
"summary": "获取计划列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "页码",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "每页大小",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"所有任务",
|
||||
"自定义任务",
|
||||
"系统任务"
|
||||
],
|
||||
"type": "string",
|
||||
"x-enum-varnames": [
|
||||
"PlanTypeFilterAll",
|
||||
"PlanTypeFilterCustom",
|
||||
"PlanTypeFilterSystem"
|
||||
],
|
||||
"description": "计划类型",
|
||||
"name": "plan_type",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表成功获取列表",
|
||||
@@ -3620,10 +3650,7 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.PlanResponse"
|
||||
}
|
||||
"$ref": "#/definitions/dto.ListPlansResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3733,7 +3760,7 @@ const docTemplate = `{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据计划ID更新计划的详细信息。",
|
||||
"description": "根据计划ID更新计划的详细信息。系统计划不允许修改。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3789,7 +3816,7 @@ const docTemplate = `{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据计划ID删除计划。(软删除)",
|
||||
"description": "根据计划ID删除计划。(软删除)系统计划不允许删除。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3823,7 +3850,7 @@ const docTemplate = `{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据计划ID启动一个计划的执行。",
|
||||
"description": "根据计划ID启动一个计划的执行。系统计划不允许手动启动。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3857,7 +3884,7 @@ const docTemplate = `{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据计划ID停止一个正在执行的计划。",
|
||||
"description": "根据计划ID停止一个正在执行的计划。系统计划不能被停止。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3976,97 +4003,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/users/{id}/history": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据用户ID,分页获取该用户的操作审计日志。支持与通用日志查询接口相同的过滤和排序参数。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户管理"
|
||||
],
|
||||
"summary": "获取指定用户的操作历史",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "用户ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "action_type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "end_time",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "order_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "start_time",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "username",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表成功获取",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.ListUserActionLogResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/users/{id}/notifications/test": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -4139,7 +4075,7 @@ const docTemplate = `{
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"description": "业务数据"
|
||||
"description": "业务数据, omitempty表示如果为空则不序列化"
|
||||
},
|
||||
"message": {
|
||||
"description": "提示信息",
|
||||
@@ -4154,6 +4090,7 @@ const docTemplate = `{
|
||||
2001,
|
||||
4000,
|
||||
4001,
|
||||
4003,
|
||||
4004,
|
||||
4009,
|
||||
5000,
|
||||
@@ -4163,6 +4100,7 @@ const docTemplate = `{
|
||||
"CodeBadRequest": "请求参数错误",
|
||||
"CodeConflict": "资源冲突",
|
||||
"CodeCreated": "创建成功",
|
||||
"CodeForbidden": "禁止访问",
|
||||
"CodeInternalError": "服务器内部错误",
|
||||
"CodeNotFound": "资源未找到",
|
||||
"CodeServiceUnavailable": "服务不可用",
|
||||
@@ -4174,6 +4112,7 @@ const docTemplate = `{
|
||||
"创建成功",
|
||||
"请求参数错误",
|
||||
"未授权",
|
||||
"禁止访问",
|
||||
"资源未找到",
|
||||
"资源冲突",
|
||||
"服务器内部错误",
|
||||
@@ -4184,6 +4123,7 @@ const docTemplate = `{
|
||||
"CodeCreated",
|
||||
"CodeBadRequest",
|
||||
"CodeUnauthorized",
|
||||
"CodeForbidden",
|
||||
"CodeNotFound",
|
||||
"CodeConflict",
|
||||
"CodeInternalError",
|
||||
@@ -4221,20 +4161,38 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"dto.AssignEmptyPensToBatchRequest": {
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pen_ids"
|
||||
],
|
||||
"properties": {
|
||||
"pen_ids": {
|
||||
"description": "待分配的猪栏ID列表",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.BuyPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"penID",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"totalPrice",
|
||||
"tradeDate",
|
||||
"traderName",
|
||||
"unitPrice"
|
||||
"total_price",
|
||||
"trade_date",
|
||||
"trader_name",
|
||||
"unit_price"
|
||||
],
|
||||
"properties": {
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -4247,20 +4205,20 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"totalPrice": {
|
||||
"total_price": {
|
||||
"description": "总价",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"tradeDate": {
|
||||
"trade_date": {
|
||||
"description": "交易日期",
|
||||
"type": "string"
|
||||
},
|
||||
"traderName": {
|
||||
"trader_name": {
|
||||
"description": "交易方名称",
|
||||
"type": "string"
|
||||
},
|
||||
"unitPrice": {
|
||||
"unit_price": {
|
||||
"description": "单价",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
@@ -4397,6 +4355,7 @@ const docTemplate = `{
|
||||
},
|
||||
"execute_num": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"example": 10
|
||||
},
|
||||
"execution_type": {
|
||||
@@ -4744,6 +4703,21 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ListPlansResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plans": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.PlanResponse"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"example": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ListRawMaterialPurchaseResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4936,7 +4910,7 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"quantity",
|
||||
"toPenID"
|
||||
"to_pen_id"
|
||||
],
|
||||
"properties": {
|
||||
"quantity": {
|
||||
@@ -4948,7 +4922,7 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"toPenID": {
|
||||
"to_pen_id": {
|
||||
"description": "目标猪栏ID",
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -5001,7 +4975,7 @@ const docTemplate = `{
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"page_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
@@ -5162,11 +5136,11 @@ const docTemplate = `{
|
||||
"description": "创建时间",
|
||||
"type": "string"
|
||||
},
|
||||
"currentTotalPigsInPens": {
|
||||
"current_total_pigs_in_pens": {
|
||||
"description": "当前存栏总数",
|
||||
"type": "integer"
|
||||
},
|
||||
"currentTotalQuantity": {
|
||||
"current_total_quantity": {
|
||||
"description": "当前总数",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5439,6 +5413,9 @@ const docTemplate = `{
|
||||
"plan_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"plan_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5493,6 +5470,14 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"example": "猪舍温度控制计划"
|
||||
},
|
||||
"plan_type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.PlanType"
|
||||
}
|
||||
],
|
||||
"example": "自定义任务"
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5587,11 +5572,11 @@ const docTemplate = `{
|
||||
"dto.ReclassifyPenToNewBatchRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"penID",
|
||||
"toBatchID"
|
||||
"pen_id",
|
||||
"to_batch_id"
|
||||
],
|
||||
"properties": {
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "待划拨的猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5599,7 +5584,7 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"toBatchID": {
|
||||
"to_batch_id": {
|
||||
"description": "目标猪批次ID",
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -5608,16 +5593,16 @@ const docTemplate = `{
|
||||
"dto.RecordCullRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5635,16 +5620,16 @@ const docTemplate = `{
|
||||
"dto.RecordDeathRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5662,17 +5647,17 @@ const docTemplate = `{
|
||||
"dto.RecordSickPigCullRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"treatmentLocation"
|
||||
"treatment_location"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5685,7 +5670,7 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"treatmentLocation": {
|
||||
"treatment_location": {
|
||||
"description": "治疗地点",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5698,17 +5683,17 @@ const docTemplate = `{
|
||||
"dto.RecordSickPigDeathRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"treatmentLocation"
|
||||
"treatment_location"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5721,7 +5706,7 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"treatmentLocation": {
|
||||
"treatment_location": {
|
||||
"description": "治疗地点",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5734,17 +5719,17 @@ const docTemplate = `{
|
||||
"dto.RecordSickPigRecoveryRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"treatmentLocation"
|
||||
"treatment_location"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5757,7 +5742,7 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"treatmentLocation": {
|
||||
"treatment_location": {
|
||||
"description": "治疗地点",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5770,17 +5755,17 @@ const docTemplate = `{
|
||||
"dto.RecordSickPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"treatmentLocation"
|
||||
"treatment_location"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5793,7 +5778,7 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"treatmentLocation": {
|
||||
"treatment_location": {
|
||||
"description": "治疗地点",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5806,15 +5791,15 @@ const docTemplate = `{
|
||||
"dto.SellPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"penID",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"totalPrice",
|
||||
"tradeDate",
|
||||
"traderName",
|
||||
"unitPrice"
|
||||
"total_price",
|
||||
"trade_date",
|
||||
"trader_name",
|
||||
"unit_price"
|
||||
],
|
||||
"properties": {
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5827,20 +5812,20 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"totalPrice": {
|
||||
"total_price": {
|
||||
"description": "总价",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"tradeDate": {
|
||||
"trade_date": {
|
||||
"description": "交易日期",
|
||||
"type": "string"
|
||||
},
|
||||
"traderName": {
|
||||
"trader_name": {
|
||||
"description": "交易方名称",
|
||||
"type": "string"
|
||||
},
|
||||
"unitPrice": {
|
||||
"unit_price": {
|
||||
"description": "单价",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
@@ -6033,17 +6018,17 @@ const docTemplate = `{
|
||||
"dto.TransferPigsAcrossBatchesRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"destBatchID",
|
||||
"fromPenID",
|
||||
"dest_batch_id",
|
||||
"from_pen_id",
|
||||
"quantity",
|
||||
"toPenID"
|
||||
"to_pen_id"
|
||||
],
|
||||
"properties": {
|
||||
"destBatchID": {
|
||||
"dest_batch_id": {
|
||||
"description": "目标猪批次ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"fromPenID": {
|
||||
"from_pen_id": {
|
||||
"description": "源猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -6056,7 +6041,7 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"toPenID": {
|
||||
"to_pen_id": {
|
||||
"description": "目标猪栏ID",
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -6065,12 +6050,12 @@ const docTemplate = `{
|
||||
"dto.TransferPigsWithinBatchRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"fromPenID",
|
||||
"from_pen_id",
|
||||
"quantity",
|
||||
"toPenID"
|
||||
"to_pen_id"
|
||||
],
|
||||
"properties": {
|
||||
"fromPenID": {
|
||||
"from_pen_id": {
|
||||
"description": "源猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -6083,7 +6068,7 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"toPenID": {
|
||||
"to_pen_id": {
|
||||
"description": "目标猪栏ID",
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -6259,6 +6244,7 @@ const docTemplate = `{
|
||||
},
|
||||
"execute_num": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"example": 10
|
||||
},
|
||||
"execution_type": {
|
||||
@@ -6737,6 +6723,17 @@ const docTemplate = `{
|
||||
"PlanStatusFailed"
|
||||
]
|
||||
},
|
||||
"models.PlanType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"自定义任务",
|
||||
"系统任务"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"PlanTypeCustom",
|
||||
"PlanTypeSystem"
|
||||
]
|
||||
},
|
||||
"models.SensorType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -6792,22 +6789,26 @@ const docTemplate = `{
|
||||
"enum": [
|
||||
"计划分析",
|
||||
"等待",
|
||||
"下料"
|
||||
"下料",
|
||||
"全量采集"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务",
|
||||
"TaskTypeFullCollection": "新增的全量采集任务",
|
||||
"TaskTypeReleaseFeedWeight": "下料口释放指定重量任务",
|
||||
"TaskTypeWaiting": "等待任务"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"解析Plan的Task列表并添加到待执行队列的特殊任务",
|
||||
"等待任务",
|
||||
"下料口释放指定重量任务"
|
||||
"下料口释放指定重量任务",
|
||||
"新增的全量采集任务"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TaskPlanAnalysis",
|
||||
"TaskTypeWaiting",
|
||||
"TaskTypeReleaseFeedWeight"
|
||||
"TaskTypeReleaseFeedWeight",
|
||||
"TaskTypeFullCollection"
|
||||
]
|
||||
},
|
||||
"models.ValueDescriptor": {
|
||||
@@ -6841,11 +6842,23 @@ const docTemplate = `{
|
||||
"NotifierTypeLog"
|
||||
]
|
||||
},
|
||||
"repository.PlanTypeFilter": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"所有任务",
|
||||
"自定义任务",
|
||||
"系统任务"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"PlanTypeFilterAll",
|
||||
"PlanTypeFilterCustom",
|
||||
"PlanTypeFilterSystem"
|
||||
]
|
||||
},
|
||||
"zapcore.Level": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"enum": [
|
||||
7,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
@@ -6855,10 +6868,10 @@ const docTemplate = `{
|
||||
5,
|
||||
-1,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"_numLevels",
|
||||
"DebugLevel",
|
||||
"InfoLevel",
|
||||
"WarnLevel",
|
||||
@@ -6868,7 +6881,8 @@ const docTemplate = `{
|
||||
"FatalLevel",
|
||||
"_minLevel",
|
||||
"_maxLevel",
|
||||
"InvalidLevel"
|
||||
"InvalidLevel",
|
||||
"_numLevels"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -768,7 +768,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -847,7 +847,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -926,7 +926,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -990,7 +990,6 @@
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
7,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
@@ -1000,12 +999,12 @@
|
||||
5,
|
||||
-1,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"x-enum-varnames": [
|
||||
"_numLevels",
|
||||
"DebugLevel",
|
||||
"InfoLevel",
|
||||
"WarnLevel",
|
||||
@@ -1015,7 +1014,8 @@
|
||||
"FatalLevel",
|
||||
"_minLevel",
|
||||
"_maxLevel",
|
||||
"InvalidLevel"
|
||||
"InvalidLevel",
|
||||
"_numLevels"
|
||||
],
|
||||
"name": "level",
|
||||
"in": "query"
|
||||
@@ -1049,7 +1049,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1148,7 +1148,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1227,7 +1227,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1301,7 +1301,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1385,7 +1385,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1459,7 +1459,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1553,7 +1553,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1632,7 +1632,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1706,7 +1706,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1780,7 +1780,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1864,7 +1864,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -1933,7 +1933,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -2017,7 +2017,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -2096,7 +2096,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -2170,7 +2170,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -3407,7 +3407,7 @@
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "创建一个新的猪舍",
|
||||
"description": "根据提供的信息创建一个新猪舍",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3592,7 +3592,7 @@
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取所有计划的列表",
|
||||
"description": "获取所有计划的列表,支持按类型过滤和分页",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3600,6 +3600,36 @@
|
||||
"计划管理"
|
||||
],
|
||||
"summary": "获取计划列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "页码",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "每页大小",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"所有任务",
|
||||
"自定义任务",
|
||||
"系统任务"
|
||||
],
|
||||
"type": "string",
|
||||
"x-enum-varnames": [
|
||||
"PlanTypeFilterAll",
|
||||
"PlanTypeFilterCustom",
|
||||
"PlanTypeFilterSystem"
|
||||
],
|
||||
"description": "计划类型",
|
||||
"name": "plan_type",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表成功获取列表",
|
||||
@@ -3612,10 +3642,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.PlanResponse"
|
||||
}
|
||||
"$ref": "#/definitions/dto.ListPlansResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3725,7 +3752,7 @@
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据计划ID更新计划的详细信息。",
|
||||
"description": "根据计划ID更新计划的详细信息。系统计划不允许修改。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3781,7 +3808,7 @@
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据计划ID删除计划。(软删除)",
|
||||
"description": "根据计划ID删除计划。(软删除)系统计划不允许删除。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3815,7 +3842,7 @@
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据计划ID启动一个计划的执行。",
|
||||
"description": "根据计划ID启动一个计划的执行。系统计划不允许手动启动。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3849,7 +3876,7 @@
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据计划ID停止一个正在执行的计划。",
|
||||
"description": "根据计划ID停止一个正在执行的计划。系统计划不能被停止。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -3968,97 +3995,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/users/{id}/history": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据用户ID,分页获取该用户的操作审计日志。支持与通用日志查询接口相同的过滤和排序参数。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户管理"
|
||||
],
|
||||
"summary": "获取指定用户的操作历史",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "用户ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "action_type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "end_time",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "order_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "pageSize",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "start_time",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "username",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表成功获取",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.ListUserActionLogResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/users/{id}/notifications/test": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -4131,7 +4067,7 @@
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"description": "业务数据"
|
||||
"description": "业务数据, omitempty表示如果为空则不序列化"
|
||||
},
|
||||
"message": {
|
||||
"description": "提示信息",
|
||||
@@ -4146,6 +4082,7 @@
|
||||
2001,
|
||||
4000,
|
||||
4001,
|
||||
4003,
|
||||
4004,
|
||||
4009,
|
||||
5000,
|
||||
@@ -4155,6 +4092,7 @@
|
||||
"CodeBadRequest": "请求参数错误",
|
||||
"CodeConflict": "资源冲突",
|
||||
"CodeCreated": "创建成功",
|
||||
"CodeForbidden": "禁止访问",
|
||||
"CodeInternalError": "服务器内部错误",
|
||||
"CodeNotFound": "资源未找到",
|
||||
"CodeServiceUnavailable": "服务不可用",
|
||||
@@ -4166,6 +4104,7 @@
|
||||
"创建成功",
|
||||
"请求参数错误",
|
||||
"未授权",
|
||||
"禁止访问",
|
||||
"资源未找到",
|
||||
"资源冲突",
|
||||
"服务器内部错误",
|
||||
@@ -4176,6 +4115,7 @@
|
||||
"CodeCreated",
|
||||
"CodeBadRequest",
|
||||
"CodeUnauthorized",
|
||||
"CodeForbidden",
|
||||
"CodeNotFound",
|
||||
"CodeConflict",
|
||||
"CodeInternalError",
|
||||
@@ -4213,20 +4153,38 @@
|
||||
}
|
||||
},
|
||||
"dto.AssignEmptyPensToBatchRequest": {
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pen_ids"
|
||||
],
|
||||
"properties": {
|
||||
"pen_ids": {
|
||||
"description": "待分配的猪栏ID列表",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.BuyPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"penID",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"totalPrice",
|
||||
"tradeDate",
|
||||
"traderName",
|
||||
"unitPrice"
|
||||
"total_price",
|
||||
"trade_date",
|
||||
"trader_name",
|
||||
"unit_price"
|
||||
],
|
||||
"properties": {
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -4239,20 +4197,20 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"totalPrice": {
|
||||
"total_price": {
|
||||
"description": "总价",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"tradeDate": {
|
||||
"trade_date": {
|
||||
"description": "交易日期",
|
||||
"type": "string"
|
||||
},
|
||||
"traderName": {
|
||||
"trader_name": {
|
||||
"description": "交易方名称",
|
||||
"type": "string"
|
||||
},
|
||||
"unitPrice": {
|
||||
"unit_price": {
|
||||
"description": "单价",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
@@ -4389,6 +4347,7 @@
|
||||
},
|
||||
"execute_num": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"example": 10
|
||||
},
|
||||
"execution_type": {
|
||||
@@ -4736,6 +4695,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ListPlansResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plans": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.PlanResponse"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"example": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ListRawMaterialPurchaseResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4928,7 +4902,7 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"quantity",
|
||||
"toPenID"
|
||||
"to_pen_id"
|
||||
],
|
||||
"properties": {
|
||||
"quantity": {
|
||||
@@ -4940,7 +4914,7 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"toPenID": {
|
||||
"to_pen_id": {
|
||||
"description": "目标猪栏ID",
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -4993,7 +4967,7 @@
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"page_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
@@ -5154,11 +5128,11 @@
|
||||
"description": "创建时间",
|
||||
"type": "string"
|
||||
},
|
||||
"currentTotalPigsInPens": {
|
||||
"current_total_pigs_in_pens": {
|
||||
"description": "当前存栏总数",
|
||||
"type": "integer"
|
||||
},
|
||||
"currentTotalQuantity": {
|
||||
"current_total_quantity": {
|
||||
"description": "当前总数",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5431,6 +5405,9 @@
|
||||
"plan_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"plan_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5485,6 +5462,14 @@
|
||||
"type": "string",
|
||||
"example": "猪舍温度控制计划"
|
||||
},
|
||||
"plan_type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.PlanType"
|
||||
}
|
||||
],
|
||||
"example": "自定义任务"
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5579,11 +5564,11 @@
|
||||
"dto.ReclassifyPenToNewBatchRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"penID",
|
||||
"toBatchID"
|
||||
"pen_id",
|
||||
"to_batch_id"
|
||||
],
|
||||
"properties": {
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "待划拨的猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5591,7 +5576,7 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"toBatchID": {
|
||||
"to_batch_id": {
|
||||
"description": "目标猪批次ID",
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -5600,16 +5585,16 @@
|
||||
"dto.RecordCullRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5627,16 +5612,16 @@
|
||||
"dto.RecordDeathRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5654,17 +5639,17 @@
|
||||
"dto.RecordSickPigCullRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"treatmentLocation"
|
||||
"treatment_location"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5677,7 +5662,7 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"treatmentLocation": {
|
||||
"treatment_location": {
|
||||
"description": "治疗地点",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5690,17 +5675,17 @@
|
||||
"dto.RecordSickPigDeathRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"treatmentLocation"
|
||||
"treatment_location"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5713,7 +5698,7 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"treatmentLocation": {
|
||||
"treatment_location": {
|
||||
"description": "治疗地点",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5726,17 +5711,17 @@
|
||||
"dto.RecordSickPigRecoveryRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"treatmentLocation"
|
||||
"treatment_location"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5749,7 +5734,7 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"treatmentLocation": {
|
||||
"treatment_location": {
|
||||
"description": "治疗地点",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5762,17 +5747,17 @@
|
||||
"dto.RecordSickPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"happenedAt",
|
||||
"penID",
|
||||
"happened_at",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"treatmentLocation"
|
||||
"treatment_location"
|
||||
],
|
||||
"properties": {
|
||||
"happenedAt": {
|
||||
"happened_at": {
|
||||
"description": "发生时间",
|
||||
"type": "string"
|
||||
},
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5785,7 +5770,7 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"treatmentLocation": {
|
||||
"treatment_location": {
|
||||
"description": "治疗地点",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5798,15 +5783,15 @@
|
||||
"dto.SellPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"penID",
|
||||
"pen_id",
|
||||
"quantity",
|
||||
"totalPrice",
|
||||
"tradeDate",
|
||||
"traderName",
|
||||
"unitPrice"
|
||||
"total_price",
|
||||
"trade_date",
|
||||
"trader_name",
|
||||
"unit_price"
|
||||
],
|
||||
"properties": {
|
||||
"penID": {
|
||||
"pen_id": {
|
||||
"description": "猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5819,20 +5804,20 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"totalPrice": {
|
||||
"total_price": {
|
||||
"description": "总价",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"tradeDate": {
|
||||
"trade_date": {
|
||||
"description": "交易日期",
|
||||
"type": "string"
|
||||
},
|
||||
"traderName": {
|
||||
"trader_name": {
|
||||
"description": "交易方名称",
|
||||
"type": "string"
|
||||
},
|
||||
"unitPrice": {
|
||||
"unit_price": {
|
||||
"description": "单价",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
@@ -6025,17 +6010,17 @@
|
||||
"dto.TransferPigsAcrossBatchesRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"destBatchID",
|
||||
"fromPenID",
|
||||
"dest_batch_id",
|
||||
"from_pen_id",
|
||||
"quantity",
|
||||
"toPenID"
|
||||
"to_pen_id"
|
||||
],
|
||||
"properties": {
|
||||
"destBatchID": {
|
||||
"dest_batch_id": {
|
||||
"description": "目标猪批次ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"fromPenID": {
|
||||
"from_pen_id": {
|
||||
"description": "源猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -6048,7 +6033,7 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"toPenID": {
|
||||
"to_pen_id": {
|
||||
"description": "目标猪栏ID",
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -6057,12 +6042,12 @@
|
||||
"dto.TransferPigsWithinBatchRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"fromPenID",
|
||||
"from_pen_id",
|
||||
"quantity",
|
||||
"toPenID"
|
||||
"to_pen_id"
|
||||
],
|
||||
"properties": {
|
||||
"fromPenID": {
|
||||
"from_pen_id": {
|
||||
"description": "源猪栏ID",
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -6075,7 +6060,7 @@
|
||||
"description": "备注",
|
||||
"type": "string"
|
||||
},
|
||||
"toPenID": {
|
||||
"to_pen_id": {
|
||||
"description": "目标猪栏ID",
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -6251,6 +6236,7 @@
|
||||
},
|
||||
"execute_num": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"example": 10
|
||||
},
|
||||
"execution_type": {
|
||||
@@ -6729,6 +6715,17 @@
|
||||
"PlanStatusFailed"
|
||||
]
|
||||
},
|
||||
"models.PlanType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"自定义任务",
|
||||
"系统任务"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"PlanTypeCustom",
|
||||
"PlanTypeSystem"
|
||||
]
|
||||
},
|
||||
"models.SensorType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -6784,22 +6781,26 @@
|
||||
"enum": [
|
||||
"计划分析",
|
||||
"等待",
|
||||
"下料"
|
||||
"下料",
|
||||
"全量采集"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务",
|
||||
"TaskTypeFullCollection": "新增的全量采集任务",
|
||||
"TaskTypeReleaseFeedWeight": "下料口释放指定重量任务",
|
||||
"TaskTypeWaiting": "等待任务"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"解析Plan的Task列表并添加到待执行队列的特殊任务",
|
||||
"等待任务",
|
||||
"下料口释放指定重量任务"
|
||||
"下料口释放指定重量任务",
|
||||
"新增的全量采集任务"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TaskPlanAnalysis",
|
||||
"TaskTypeWaiting",
|
||||
"TaskTypeReleaseFeedWeight"
|
||||
"TaskTypeReleaseFeedWeight",
|
||||
"TaskTypeFullCollection"
|
||||
]
|
||||
},
|
||||
"models.ValueDescriptor": {
|
||||
@@ -6833,11 +6834,23 @@
|
||||
"NotifierTypeLog"
|
||||
]
|
||||
},
|
||||
"repository.PlanTypeFilter": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"所有任务",
|
||||
"自定义任务",
|
||||
"系统任务"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"PlanTypeFilterAll",
|
||||
"PlanTypeFilterCustom",
|
||||
"PlanTypeFilterSystem"
|
||||
]
|
||||
},
|
||||
"zapcore.Level": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"enum": [
|
||||
7,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
@@ -6847,10 +6860,10 @@
|
||||
5,
|
||||
-1,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"_numLevels",
|
||||
"DebugLevel",
|
||||
"InfoLevel",
|
||||
"WarnLevel",
|
||||
@@ -6860,7 +6873,8 @@
|
||||
"FatalLevel",
|
||||
"_minLevel",
|
||||
"_maxLevel",
|
||||
"InvalidLevel"
|
||||
"InvalidLevel",
|
||||
"_numLevels"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ definitions:
|
||||
- $ref: '#/definitions/controller.ResponseCode'
|
||||
description: 业务状态码
|
||||
data:
|
||||
description: 业务数据
|
||||
description: 业务数据, omitempty表示如果为空则不序列化
|
||||
message:
|
||||
description: 提示信息
|
||||
type: string
|
||||
@@ -17,6 +17,7 @@ definitions:
|
||||
- 2001
|
||||
- 4000
|
||||
- 4001
|
||||
- 4003
|
||||
- 4004
|
||||
- 4009
|
||||
- 5000
|
||||
@@ -26,6 +27,7 @@ definitions:
|
||||
CodeBadRequest: 请求参数错误
|
||||
CodeConflict: 资源冲突
|
||||
CodeCreated: 创建成功
|
||||
CodeForbidden: 禁止访问
|
||||
CodeInternalError: 服务器内部错误
|
||||
CodeNotFound: 资源未找到
|
||||
CodeServiceUnavailable: 服务不可用
|
||||
@@ -36,6 +38,7 @@ definitions:
|
||||
- 创建成功
|
||||
- 请求参数错误
|
||||
- 未授权
|
||||
- 禁止访问
|
||||
- 资源未找到
|
||||
- 资源冲突
|
||||
- 服务器内部错误
|
||||
@@ -45,6 +48,7 @@ definitions:
|
||||
- CodeCreated
|
||||
- CodeBadRequest
|
||||
- CodeUnauthorized
|
||||
- CodeForbidden
|
||||
- CodeNotFound
|
||||
- CodeConflict
|
||||
- CodeInternalError
|
||||
@@ -70,10 +74,23 @@ definitions:
|
||||
type: string
|
||||
type: object
|
||||
dto.AssignEmptyPensToBatchRequest:
|
||||
properties:
|
||||
pen_ids:
|
||||
description: 待分配的猪栏ID列表
|
||||
example:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
items:
|
||||
type: integer
|
||||
minItems: 1
|
||||
type: array
|
||||
required:
|
||||
- pen_ids
|
||||
type: object
|
||||
dto.BuyPigsRequest:
|
||||
properties:
|
||||
penID:
|
||||
pen_id:
|
||||
description: 猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -83,27 +100,27 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
totalPrice:
|
||||
total_price:
|
||||
description: 总价
|
||||
minimum: 0
|
||||
type: number
|
||||
tradeDate:
|
||||
trade_date:
|
||||
description: 交易日期
|
||||
type: string
|
||||
traderName:
|
||||
trader_name:
|
||||
description: 交易方名称
|
||||
type: string
|
||||
unitPrice:
|
||||
unit_price:
|
||||
description: 单价
|
||||
minimum: 0
|
||||
type: number
|
||||
required:
|
||||
- penID
|
||||
- pen_id
|
||||
- quantity
|
||||
- totalPrice
|
||||
- tradeDate
|
||||
- traderName
|
||||
- unitPrice
|
||||
- total_price
|
||||
- trade_date
|
||||
- trader_name
|
||||
- unit_price
|
||||
type: object
|
||||
dto.CreateAreaControllerRequest:
|
||||
properties:
|
||||
@@ -192,6 +209,7 @@ definitions:
|
||||
type: string
|
||||
execute_num:
|
||||
example: 10
|
||||
minimum: 0
|
||||
type: integer
|
||||
execution_type:
|
||||
allOf:
|
||||
@@ -421,6 +439,16 @@ definitions:
|
||||
pagination:
|
||||
$ref: '#/definitions/dto.PaginationDTO'
|
||||
type: object
|
||||
dto.ListPlansResponse:
|
||||
properties:
|
||||
plans:
|
||||
items:
|
||||
$ref: '#/definitions/dto.PlanResponse'
|
||||
type: array
|
||||
total:
|
||||
example: 100
|
||||
type: integer
|
||||
type: object
|
||||
dto.ListRawMaterialPurchaseResponse:
|
||||
properties:
|
||||
list:
|
||||
@@ -554,12 +582,12 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
toPenID:
|
||||
to_pen_id:
|
||||
description: 目标猪栏ID
|
||||
type: integer
|
||||
required:
|
||||
- quantity
|
||||
- toPenID
|
||||
- to_pen_id
|
||||
type: object
|
||||
dto.NotificationDTO:
|
||||
properties:
|
||||
@@ -592,7 +620,7 @@ definitions:
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
pageSize:
|
||||
page_size:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
@@ -698,10 +726,10 @@ definitions:
|
||||
create_time:
|
||||
description: 创建时间
|
||||
type: string
|
||||
currentTotalPigsInPens:
|
||||
current_total_pigs_in_pens:
|
||||
description: 当前存栏总数
|
||||
type: integer
|
||||
currentTotalQuantity:
|
||||
current_total_quantity:
|
||||
description: 当前总数
|
||||
type: integer
|
||||
end_date:
|
||||
@@ -879,6 +907,8 @@ definitions:
|
||||
type: integer
|
||||
plan_id:
|
||||
type: integer
|
||||
plan_name:
|
||||
type: string
|
||||
started_at:
|
||||
type: string
|
||||
status:
|
||||
@@ -914,6 +944,10 @@ definitions:
|
||||
name:
|
||||
example: 猪舍温度控制计划
|
||||
type: string
|
||||
plan_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.PlanType'
|
||||
example: 自定义任务
|
||||
status:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.PlanStatus'
|
||||
@@ -974,25 +1008,25 @@ definitions:
|
||||
type: object
|
||||
dto.ReclassifyPenToNewBatchRequest:
|
||||
properties:
|
||||
penID:
|
||||
pen_id:
|
||||
description: 待划拨的猪栏ID
|
||||
type: integer
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
toBatchID:
|
||||
to_batch_id:
|
||||
description: 目标猪批次ID
|
||||
type: integer
|
||||
required:
|
||||
- penID
|
||||
- toBatchID
|
||||
- pen_id
|
||||
- to_batch_id
|
||||
type: object
|
||||
dto.RecordCullRequest:
|
||||
properties:
|
||||
happenedAt:
|
||||
happened_at:
|
||||
description: 发生时间
|
||||
type: string
|
||||
penID:
|
||||
pen_id:
|
||||
description: 猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1003,16 +1037,16 @@ definitions:
|
||||
description: 备注
|
||||
type: string
|
||||
required:
|
||||
- happenedAt
|
||||
- penID
|
||||
- happened_at
|
||||
- pen_id
|
||||
- quantity
|
||||
type: object
|
||||
dto.RecordDeathRequest:
|
||||
properties:
|
||||
happenedAt:
|
||||
happened_at:
|
||||
description: 发生时间
|
||||
type: string
|
||||
penID:
|
||||
pen_id:
|
||||
description: 猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1023,16 +1057,16 @@ definitions:
|
||||
description: 备注
|
||||
type: string
|
||||
required:
|
||||
- happenedAt
|
||||
- penID
|
||||
- happened_at
|
||||
- pen_id
|
||||
- quantity
|
||||
type: object
|
||||
dto.RecordSickPigCullRequest:
|
||||
properties:
|
||||
happenedAt:
|
||||
happened_at:
|
||||
description: 发生时间
|
||||
type: string
|
||||
penID:
|
||||
pen_id:
|
||||
description: 猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1042,22 +1076,22 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
treatmentLocation:
|
||||
treatment_location:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation'
|
||||
description: 治疗地点
|
||||
required:
|
||||
- happenedAt
|
||||
- penID
|
||||
- happened_at
|
||||
- pen_id
|
||||
- quantity
|
||||
- treatmentLocation
|
||||
- treatment_location
|
||||
type: object
|
||||
dto.RecordSickPigDeathRequest:
|
||||
properties:
|
||||
happenedAt:
|
||||
happened_at:
|
||||
description: 发生时间
|
||||
type: string
|
||||
penID:
|
||||
pen_id:
|
||||
description: 猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1067,22 +1101,22 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
treatmentLocation:
|
||||
treatment_location:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation'
|
||||
description: 治疗地点
|
||||
required:
|
||||
- happenedAt
|
||||
- penID
|
||||
- happened_at
|
||||
- pen_id
|
||||
- quantity
|
||||
- treatmentLocation
|
||||
- treatment_location
|
||||
type: object
|
||||
dto.RecordSickPigRecoveryRequest:
|
||||
properties:
|
||||
happenedAt:
|
||||
happened_at:
|
||||
description: 发生时间
|
||||
type: string
|
||||
penID:
|
||||
pen_id:
|
||||
description: 猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1092,22 +1126,22 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
treatmentLocation:
|
||||
treatment_location:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation'
|
||||
description: 治疗地点
|
||||
required:
|
||||
- happenedAt
|
||||
- penID
|
||||
- happened_at
|
||||
- pen_id
|
||||
- quantity
|
||||
- treatmentLocation
|
||||
- treatment_location
|
||||
type: object
|
||||
dto.RecordSickPigsRequest:
|
||||
properties:
|
||||
happenedAt:
|
||||
happened_at:
|
||||
description: 发生时间
|
||||
type: string
|
||||
penID:
|
||||
pen_id:
|
||||
description: 猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1117,19 +1151,19 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
treatmentLocation:
|
||||
treatment_location:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation'
|
||||
description: 治疗地点
|
||||
required:
|
||||
- happenedAt
|
||||
- penID
|
||||
- happened_at
|
||||
- pen_id
|
||||
- quantity
|
||||
- treatmentLocation
|
||||
- treatment_location
|
||||
type: object
|
||||
dto.SellPigsRequest:
|
||||
properties:
|
||||
penID:
|
||||
pen_id:
|
||||
description: 猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1139,27 +1173,27 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
totalPrice:
|
||||
total_price:
|
||||
description: 总价
|
||||
minimum: 0
|
||||
type: number
|
||||
tradeDate:
|
||||
trade_date:
|
||||
description: 交易日期
|
||||
type: string
|
||||
traderName:
|
||||
trader_name:
|
||||
description: 交易方名称
|
||||
type: string
|
||||
unitPrice:
|
||||
unit_price:
|
||||
description: 单价
|
||||
minimum: 0
|
||||
type: number
|
||||
required:
|
||||
- penID
|
||||
- pen_id
|
||||
- quantity
|
||||
- totalPrice
|
||||
- tradeDate
|
||||
- traderName
|
||||
- unitPrice
|
||||
- total_price
|
||||
- trade_date
|
||||
- trader_name
|
||||
- unit_price
|
||||
type: object
|
||||
dto.SendTestNotificationRequest:
|
||||
properties:
|
||||
@@ -1282,10 +1316,10 @@ definitions:
|
||||
type: object
|
||||
dto.TransferPigsAcrossBatchesRequest:
|
||||
properties:
|
||||
destBatchID:
|
||||
dest_batch_id:
|
||||
description: 目标猪批次ID
|
||||
type: integer
|
||||
fromPenID:
|
||||
from_pen_id:
|
||||
description: 源猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1295,18 +1329,18 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
toPenID:
|
||||
to_pen_id:
|
||||
description: 目标猪栏ID
|
||||
type: integer
|
||||
required:
|
||||
- destBatchID
|
||||
- fromPenID
|
||||
- dest_batch_id
|
||||
- from_pen_id
|
||||
- quantity
|
||||
- toPenID
|
||||
- to_pen_id
|
||||
type: object
|
||||
dto.TransferPigsWithinBatchRequest:
|
||||
properties:
|
||||
fromPenID:
|
||||
from_pen_id:
|
||||
description: 源猪栏ID
|
||||
type: integer
|
||||
quantity:
|
||||
@@ -1316,13 +1350,13 @@ definitions:
|
||||
remarks:
|
||||
description: 备注
|
||||
type: string
|
||||
toPenID:
|
||||
to_pen_id:
|
||||
description: 目标猪栏ID
|
||||
type: integer
|
||||
required:
|
||||
- fromPenID
|
||||
- from_pen_id
|
||||
- quantity
|
||||
- toPenID
|
||||
- to_pen_id
|
||||
type: object
|
||||
dto.UpdateAreaControllerRequest:
|
||||
properties:
|
||||
@@ -1439,6 +1473,7 @@ definitions:
|
||||
type: string
|
||||
execute_num:
|
||||
example: 10
|
||||
minimum: 0
|
||||
type: integer
|
||||
execution_type:
|
||||
allOf:
|
||||
@@ -1808,6 +1843,14 @@ definitions:
|
||||
- PlanStatusEnabled
|
||||
- PlanStatusStopped
|
||||
- PlanStatusFailed
|
||||
models.PlanType:
|
||||
enum:
|
||||
- 自定义任务
|
||||
- 系统任务
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- PlanTypeCustom
|
||||
- PlanTypeSystem
|
||||
models.SensorType:
|
||||
enum:
|
||||
- 信号强度
|
||||
@@ -1855,19 +1898,23 @@ definitions:
|
||||
- 计划分析
|
||||
- 等待
|
||||
- 下料
|
||||
- 全量采集
|
||||
type: string
|
||||
x-enum-comments:
|
||||
TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务
|
||||
TaskTypeFullCollection: 新增的全量采集任务
|
||||
TaskTypeReleaseFeedWeight: 下料口释放指定重量任务
|
||||
TaskTypeWaiting: 等待任务
|
||||
x-enum-descriptions:
|
||||
- 解析Plan的Task列表并添加到待执行队列的特殊任务
|
||||
- 等待任务
|
||||
- 下料口释放指定重量任务
|
||||
- 新增的全量采集任务
|
||||
x-enum-varnames:
|
||||
- TaskPlanAnalysis
|
||||
- TaskTypeWaiting
|
||||
- TaskTypeReleaseFeedWeight
|
||||
- TaskTypeFullCollection
|
||||
models.ValueDescriptor:
|
||||
properties:
|
||||
multiplier:
|
||||
@@ -1891,9 +1938,18 @@ definitions:
|
||||
- NotifierTypeWeChat
|
||||
- NotifierTypeLark
|
||||
- NotifierTypeLog
|
||||
repository.PlanTypeFilter:
|
||||
enum:
|
||||
- 所有任务
|
||||
- 自定义任务
|
||||
- 系统任务
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- PlanTypeFilterAll
|
||||
- PlanTypeFilterCustom
|
||||
- PlanTypeFilterSystem
|
||||
zapcore.Level:
|
||||
enum:
|
||||
- 7
|
||||
- -1
|
||||
- 0
|
||||
- 1
|
||||
@@ -1904,10 +1960,10 @@ definitions:
|
||||
- -1
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
format: int32
|
||||
type: integer
|
||||
x-enum-varnames:
|
||||
- _numLevels
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
@@ -1918,6 +1974,7 @@ definitions:
|
||||
- _minLevel
|
||||
- _maxLevel
|
||||
- InvalidLevel
|
||||
- _numLevels
|
||||
info:
|
||||
contact:
|
||||
email: divano@example.com
|
||||
@@ -2363,7 +2420,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: received_success
|
||||
@@ -2408,7 +2465,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pen_id
|
||||
@@ -2453,7 +2510,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pig_batch_id
|
||||
@@ -2489,7 +2546,6 @@ paths:
|
||||
name: end_time
|
||||
type: string
|
||||
- enum:
|
||||
- 7
|
||||
- -1
|
||||
- 0
|
||||
- 1
|
||||
@@ -2500,12 +2556,12 @@ paths:
|
||||
- -1
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
format: int32
|
||||
in: query
|
||||
name: level
|
||||
type: integer
|
||||
x-enum-varnames:
|
||||
- _numLevels
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
@@ -2516,6 +2572,7 @@ paths:
|
||||
- _minLevel
|
||||
- _maxLevel
|
||||
- InvalidLevel
|
||||
- _numLevels
|
||||
- enum:
|
||||
- 邮件
|
||||
- 企业微信
|
||||
@@ -2536,7 +2593,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: start_time
|
||||
@@ -2597,7 +2654,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: start_time
|
||||
@@ -2642,7 +2699,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pig_batch_id
|
||||
@@ -2684,7 +2741,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pig_batch_id
|
||||
@@ -2732,7 +2789,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pig_batch_id
|
||||
@@ -2774,7 +2831,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pen_id
|
||||
@@ -2828,7 +2885,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pen_id
|
||||
@@ -2873,7 +2930,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: plan_id
|
||||
@@ -2915,7 +2972,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: raw_material_id
|
||||
@@ -2957,7 +3014,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: raw_material_id
|
||||
@@ -3005,7 +3062,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: sensor_type
|
||||
@@ -3044,7 +3101,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: plan_execution_log_id
|
||||
@@ -3092,7 +3149,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: start_time
|
||||
@@ -3137,7 +3194,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pig_batch_id
|
||||
@@ -3179,7 +3236,7 @@ paths:
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
name: page_size
|
||||
type: integer
|
||||
- in: query
|
||||
name: pen_id
|
||||
@@ -3932,7 +3989,7 @@ paths:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 创建一个新的猪舍
|
||||
description: 根据提供的信息创建一个新猪舍
|
||||
parameters:
|
||||
- description: 猪舍信息
|
||||
in: body
|
||||
@@ -4038,7 +4095,28 @@ paths:
|
||||
- 猪场管理
|
||||
/api/v1/plans:
|
||||
get:
|
||||
description: 获取所有计划的列表
|
||||
description: 获取所有计划的列表,支持按类型过滤和分页
|
||||
parameters:
|
||||
- description: 页码
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- description: 每页大小
|
||||
in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
- description: 计划类型
|
||||
enum:
|
||||
- 所有任务
|
||||
- 自定义任务
|
||||
- 系统任务
|
||||
in: query
|
||||
name: plan_type
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- PlanTypeFilterAll
|
||||
- PlanTypeFilterCustom
|
||||
- PlanTypeFilterSystem
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -4049,9 +4127,7 @@ paths:
|
||||
- $ref: '#/definitions/controller.Response'
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/dto.PlanResponse'
|
||||
type: array
|
||||
$ref: '#/definitions/dto.ListPlansResponse'
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
@@ -4088,7 +4164,7 @@ paths:
|
||||
- 计划管理
|
||||
/api/v1/plans/{id}:
|
||||
delete:
|
||||
description: 根据计划ID删除计划。(软删除)
|
||||
description: 根据计划ID删除计划。(软删除)系统计划不允许删除。
|
||||
parameters:
|
||||
- description: 计划ID
|
||||
in: path
|
||||
@@ -4135,7 +4211,7 @@ paths:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据计划ID更新计划的详细信息。
|
||||
description: 根据计划ID更新计划的详细信息。系统计划不允许修改。
|
||||
parameters:
|
||||
- description: 计划ID
|
||||
in: path
|
||||
@@ -4167,7 +4243,7 @@ paths:
|
||||
- 计划管理
|
||||
/api/v1/plans/{id}/start:
|
||||
post:
|
||||
description: 根据计划ID启动一个计划的执行。
|
||||
description: 根据计划ID启动一个计划的执行。系统计划不允许手动启动。
|
||||
parameters:
|
||||
- description: 计划ID
|
||||
in: path
|
||||
@@ -4188,7 +4264,7 @@ paths:
|
||||
- 计划管理
|
||||
/api/v1/plans/{id}/stop:
|
||||
post:
|
||||
description: 根据计划ID停止一个正在执行的计划。
|
||||
description: 根据计划ID停止一个正在执行的计划。系统计划不能被停止。
|
||||
parameters:
|
||||
- description: 计划ID
|
||||
in: path
|
||||
@@ -4234,59 +4310,6 @@ paths:
|
||||
summary: 创建新用户
|
||||
tags:
|
||||
- 用户管理
|
||||
/api/v1/users/{id}/history:
|
||||
get:
|
||||
description: 根据用户ID,分页获取该用户的操作审计日志。支持与通用日志查询接口相同的过滤和排序参数。
|
||||
parameters:
|
||||
- description: 用户ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- in: query
|
||||
name: action_type
|
||||
type: string
|
||||
- in: query
|
||||
name: end_time
|
||||
type: string
|
||||
- in: query
|
||||
name: order_by
|
||||
type: string
|
||||
- in: query
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: pageSize
|
||||
type: integer
|
||||
- in: query
|
||||
name: start_time
|
||||
type: string
|
||||
- in: query
|
||||
name: status
|
||||
type: string
|
||||
- in: query
|
||||
name: user_id
|
||||
type: integer
|
||||
- in: query
|
||||
name: username
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 业务码为200代表成功获取
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/controller.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/dto.ListUserActionLogResponse'
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取指定用户的操作历史
|
||||
tags:
|
||||
- 用户管理
|
||||
/api/v1/users/{id}/notifications/test:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
55
go.mod
55
go.mod
@@ -3,23 +3,21 @@ module git.huangwc.com/pig/pig-farm-controller
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
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.24.1
|
||||
github.com/go-openapi/swag v0.25.1
|
||||
github.com/go-openapi/validate v0.24.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/panjf2000/ants/v2 v2.11.3
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
google.golang.org/protobuf v1.36.9
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
@@ -39,25 +37,26 @@ require (
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // 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.22.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.2 // 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-openapi/spec v0.22.0 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 // 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.27.0 // indirect
|
||||
@@ -72,8 +71,10 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // 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
|
||||
@@ -85,20 +86,26 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/swaggo/echo-swagger v1.4.1 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // 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
|
||||
go.yaml.in/yaml/v3 v3.0.4 // 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/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.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
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
)
|
||||
|
||||
64
go.sum
64
go.sum
@@ -17,6 +17,8 @@ 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.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
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 v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
@@ -34,40 +36,70 @@ 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.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
|
||||
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
|
||||
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
||||
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
||||
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/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||
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=
|
||||
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
|
||||
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
|
||||
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
||||
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||
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 v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8=
|
||||
github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo=
|
||||
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/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||
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/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
|
||||
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
|
||||
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/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU=
|
||||
github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M=
|
||||
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/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||
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/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
|
||||
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/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
|
||||
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
|
||||
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/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync=
|
||||
github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ=
|
||||
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/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4=
|
||||
github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE=
|
||||
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/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
|
||||
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/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
|
||||
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/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
|
||||
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=
|
||||
@@ -116,10 +148,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
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.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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=
|
||||
@@ -159,8 +197,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
|
||||
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
|
||||
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
@@ -171,6 +215,10 @@ 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.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
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=
|
||||
@@ -188,21 +236,29 @@ 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/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
@@ -216,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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.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=
|
||||
@@ -225,11 +283,17 @@ 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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
|
||||
@@ -27,20 +27,19 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
|
||||
domain_device "git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
|
||||
domain_plan "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
// API 结构体定义了 HTTP 服务器及其依赖
|
||||
type API struct {
|
||||
engine *gin.Engine // Gin 引擎实例,用于处理 HTTP 请求
|
||||
echo *echo.Echo // Echo 引擎实例,用于处理 HTTP 请求
|
||||
logger *logs.Logger // 日志记录器,用于输出日志信息
|
||||
userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作
|
||||
tokenService token.Service // Token 服务接口,用于 JWT token 的生成和解析
|
||||
@@ -54,42 +53,37 @@ type API struct {
|
||||
pigBatchController *management.PigBatchController // 猪群控制器实例
|
||||
monitorController *monitor.Controller // 数据监控控制器实例
|
||||
listenHandler webhook.ListenHandler // 设备上行事件监听器
|
||||
analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例
|
||||
analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例
|
||||
}
|
||||
|
||||
// NewAPI 创建并返回一个新的 API 实例
|
||||
// 负责初始化 Gin 引擎、设置全局中间件,并注入所有必要的依赖。
|
||||
// 负责初始化 Echo 引擎、设置全局中间件,并注入所有必要的依赖。
|
||||
func NewAPI(cfg config.ServerConfig,
|
||||
logger *logs.Logger,
|
||||
userRepo repository.UserRepository,
|
||||
deviceRepository repository.DeviceRepository,
|
||||
areaControllerRepository repository.AreaControllerRepository,
|
||||
deviceTemplateRepository repository.DeviceTemplateRepository,
|
||||
planRepository repository.PlanRepository,
|
||||
pigFarmService service.PigFarmService,
|
||||
pigBatchService service.PigBatchService,
|
||||
monitorService service.MonitorService,
|
||||
deviceService service.DeviceService,
|
||||
planService service.PlanService,
|
||||
userService service.UserService,
|
||||
tokenService token.Service,
|
||||
auditService audit.Service,
|
||||
notifyService domain_notify.Service,
|
||||
deviceService domain_device.Service,
|
||||
listenHandler webhook.ListenHandler,
|
||||
analysisTaskManager *task.AnalysisPlanTaskManager) *API {
|
||||
// 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式)
|
||||
// 从配置中获取 Gin 模式
|
||||
gin.SetMode(cfg.Mode)
|
||||
) *API {
|
||||
// 使用 echo.New() 创建一个 Echo 引擎实例
|
||||
e := echo.New()
|
||||
|
||||
// 使用 gin.New() 创建一个 Gin 引擎实例,而不是 gin.Default()
|
||||
// 这样可以手动添加所需的中间件,避免 gin.Default() 默认包含的 Logger 和 Recovery 中间件
|
||||
engine := gin.New()
|
||||
// 根据配置设置 Echo 的调试模式
|
||||
e.Debug = cfg.Mode == "debug"
|
||||
|
||||
// 添加 Gin Recovery 中间件,用于捕获 panic 并恢复,防止服务崩溃
|
||||
// gin.Logger() 已移除,因为我们使用自定义的 logger
|
||||
engine.Use(gin.Recovery())
|
||||
// 添加 Echo Recovery 中间件,用于捕获 panic 并恢复,防止服务崩溃
|
||||
// Echo 的 Logger 中间件默认会记录请求信息,如果需要自定义,可以替换
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
// 初始化 API 结构体
|
||||
api := &API{
|
||||
engine: engine,
|
||||
echo: e,
|
||||
logger: logger,
|
||||
userRepo: userRepo,
|
||||
tokenService: tokenService,
|
||||
@@ -97,11 +91,11 @@ func NewAPI(cfg config.ServerConfig,
|
||||
config: cfg,
|
||||
listenHandler: listenHandler,
|
||||
// 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员
|
||||
userController: user.NewController(userRepo, monitorService, logger, tokenService, notifyService),
|
||||
userController: user.NewController(userService, logger),
|
||||
// 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员
|
||||
deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, deviceService, logger),
|
||||
deviceController: device.NewController(deviceService, logger),
|
||||
// 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员
|
||||
planController: plan.NewController(logger, planRepository, analysisTaskManager),
|
||||
planController: plan.NewController(logger, planService),
|
||||
// 在 NewAPI 中初始化猪场管理控制器
|
||||
pigFarmController: management.NewPigFarmController(logger, pigFarmService),
|
||||
// 在 NewAPI 中初始化猪群控制器
|
||||
@@ -124,7 +118,7 @@ func (a *API) Start() {
|
||||
// 初始化标准库的 http.Server 实例
|
||||
a.httpServer = &http.Server{
|
||||
Addr: addr, // 服务器监听的地址从配置中获取
|
||||
Handler: a.engine, // 将 Gin 引擎作为 HTTP 请求的处理程序
|
||||
Handler: a.echo, // 将 Echo 引擎作为 HTTP 请求的处理程序
|
||||
}
|
||||
|
||||
// 在独立的 goroutine 中启动服务器
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/labstack/echo/v4"
|
||||
echoSwagger "github.com/swaggo/echo-swagger"
|
||||
)
|
||||
|
||||
// setupRoutes 设置所有 API 路由
|
||||
// 在此方法中,使用已初始化的控制器实例将其路由注册到 Gin 引擎中。
|
||||
// 在此方法中,使用已初始化的控制器实例将其路由注册到 Echo 引擎中。
|
||||
func (a *API) setupRoutes() {
|
||||
a.logger.Info("开始初始化所有 API 路由")
|
||||
|
||||
// --- Public Routes ---
|
||||
// 这些路由不需要身份验证
|
||||
|
||||
// 用户注册和登录
|
||||
a.engine.POST("/api/v1/users", a.userController.CreateUser) // 注册新用户
|
||||
a.engine.POST("/api/v1/users/login", a.userController.Login) // 用户登录
|
||||
a.logger.Info("公开接口注册成功:用户注册、登录")
|
||||
a.echo.POST("/api/v1/users", a.userController.CreateUser) // 注册新用户
|
||||
a.echo.POST("/api/v1/users/login", a.userController.Login) // 用户登录
|
||||
a.logger.Debug("公开接口注册成功:用户注册、登录")
|
||||
|
||||
// 注册 pprof 路由
|
||||
pprofGroup := a.engine.Group("/debug/pprof")
|
||||
pprofGroup := a.echo.Group("/debug/pprof")
|
||||
{
|
||||
pprofGroup.GET("/", gin.WrapF(pprof.Index)) // pprof 索引页
|
||||
pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline)) // pprof 命令行参数
|
||||
pprofGroup.GET("/profile", gin.WrapF(pprof.Profile)) // pprof CPU profile
|
||||
pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol)) // pprof 符号查找 (POST)
|
||||
pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol)) // pprof 符号查找 (GET)
|
||||
pprofGroup.GET("/trace", gin.WrapF(pprof.Trace)) // pprof 跟踪
|
||||
pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs"))) // pprof 内存分配
|
||||
pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block"))) // pprof 阻塞
|
||||
pprofGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
|
||||
pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap"))) // pprof 堆内存
|
||||
pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex"))) // pprof 互斥锁
|
||||
pprofGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
|
||||
pprofGroup.GET("/", echo.WrapHandler(http.HandlerFunc(pprof.Index))) // pprof 索引页
|
||||
pprofGroup.GET("/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline))) // pprof 命令行参数
|
||||
pprofGroup.GET("/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile))) // pprof CPU profile
|
||||
pprofGroup.POST("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol))) // pprof 符号查找 (POST)
|
||||
pprofGroup.GET("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol))) // pprof 符号查找 (GET)
|
||||
pprofGroup.GET("/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace))) // pprof 跟踪
|
||||
pprofGroup.GET("/allocs", echo.WrapHandler(pprof.Handler("allocs"))) // pprof 内存分配
|
||||
pprofGroup.GET("/block", echo.WrapHandler(pprof.Handler("block"))) // pprof 阻塞
|
||||
pprofGroup.GET("/goroutine", echo.WrapHandler(pprof.Handler("goroutine")))
|
||||
pprofGroup.GET("/heap", echo.WrapHandler(pprof.Handler("heap"))) // pprof 堆内存
|
||||
pprofGroup.GET("/mutex", echo.WrapHandler(pprof.Handler("mutex"))) // pprof 互斥锁
|
||||
pprofGroup.GET("/threadcreate", echo.WrapHandler(pprof.Handler("threadcreate")))
|
||||
}
|
||||
a.logger.Info("pprof 接口注册成功")
|
||||
a.logger.Debug("pprof 接口注册成功")
|
||||
|
||||
// 上行事件监听路由
|
||||
a.engine.POST("/upstream", gin.WrapH(a.listenHandler.Handler())) // 处理设备上行事件
|
||||
a.logger.Info("上行事件监听接口注册成功")
|
||||
a.echo.POST("/upstream", echo.WrapHandler(a.listenHandler.Handler())) // 处理设备上行事件
|
||||
a.logger.Debug("上行事件监听接口注册成功")
|
||||
|
||||
// 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到
|
||||
a.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // Swagger UI 接口
|
||||
a.logger.Info("Swagger UI 接口注册成功")
|
||||
a.echo.GET("/swagger/*any", echoSwagger.WrapHandler) // Swagger UI 接口
|
||||
a.logger.Debug("Swagger UI 接口注册成功")
|
||||
|
||||
// --- Authenticated Routes ---
|
||||
// 所有在此注册的路由都需要通过 JWT 身份验证
|
||||
authGroup := a.engine.Group("/api/v1")
|
||||
authGroup := a.echo.Group("/api/v1")
|
||||
authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证中间件
|
||||
authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志中间件
|
||||
{
|
||||
// 用户相关路由组
|
||||
userGroup := authGroup.Group("/users")
|
||||
{
|
||||
userGroup.GET("/:id/history", a.userController.ListUserHistory) // 获取用户操作历史
|
||||
userGroup.POST("/:id/notifications/test", a.userController.SendTestNotification)
|
||||
}
|
||||
a.logger.Info("用户相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("用户相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 设备相关路由组
|
||||
deviceGroup := authGroup.Group("/devices")
|
||||
@@ -71,7 +71,7 @@ func (a *API) setupRoutes() {
|
||||
deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) // 删除设备
|
||||
deviceGroup.POST("/manual-control/:id", a.deviceController.ManualControl) // 手动控制设备
|
||||
}
|
||||
a.logger.Info("设备相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("设备相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 区域主控相关路由组
|
||||
areaControllerGroup := authGroup.Group("/area-controllers")
|
||||
@@ -82,7 +82,7 @@ func (a *API) setupRoutes() {
|
||||
areaControllerGroup.PUT("/:id", a.deviceController.UpdateAreaController) // 更新区域主控
|
||||
areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController) // 删除区域主控
|
||||
}
|
||||
a.logger.Info("区域主控相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("区域主控相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 设备模板相关路由组
|
||||
deviceTemplateGroup := authGroup.Group("/device-templates")
|
||||
@@ -93,7 +93,7 @@ func (a *API) setupRoutes() {
|
||||
deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate) // 更新设备模板
|
||||
deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate) // 删除设备模板
|
||||
}
|
||||
a.logger.Info("设备模板相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("设备模板相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 计划相关路由组
|
||||
planGroup := authGroup.Group("/plans")
|
||||
@@ -106,7 +106,7 @@ func (a *API) setupRoutes() {
|
||||
planGroup.POST("/:id/start", a.planController.StartPlan) // 启动计划
|
||||
planGroup.POST("/:id/stop", a.planController.StopPlan) // 停止计划
|
||||
}
|
||||
a.logger.Info("计划相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("计划相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 猪舍相关路由组
|
||||
pigHouseGroup := authGroup.Group("/pig-houses")
|
||||
@@ -117,7 +117,7 @@ func (a *API) setupRoutes() {
|
||||
pigHouseGroup.PUT("/:id", a.pigFarmController.UpdatePigHouse) // 更新猪舍
|
||||
pigHouseGroup.DELETE("/:id", a.pigFarmController.DeletePigHouse) // 删除猪舍
|
||||
}
|
||||
a.logger.Info("猪舍相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("猪舍相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 猪圈相关路由组
|
||||
penGroup := authGroup.Group("/pens")
|
||||
@@ -129,7 +129,7 @@ func (a *API) setupRoutes() {
|
||||
penGroup.DELETE("/:id", a.pigFarmController.DeletePen) // 删除猪圈
|
||||
penGroup.PUT("/:id/status", a.pigFarmController.UpdatePenStatus) // 更新猪圈状态
|
||||
}
|
||||
a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("猪圈相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 猪群相关路由组
|
||||
pigBatchGroup := authGroup.Group("/pig-batches")
|
||||
@@ -154,7 +154,7 @@ func (a *API) setupRoutes() {
|
||||
pigBatchGroup.POST("/record-death/:id", a.pigBatchController.RecordDeath) // 记录正常猪只死亡事件
|
||||
pigBatchGroup.POST("/record-cull/:id", a.pigBatchController.RecordCull) // 记录正常猪只淘汰事件
|
||||
}
|
||||
a.logger.Info("猪群相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("猪群相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 数据监控相关路由组
|
||||
monitorGroup := authGroup.Group("/monitor")
|
||||
@@ -178,6 +178,8 @@ func (a *API) setupRoutes() {
|
||||
monitorGroup.GET("/pig-sales", a.monitorController.ListPigSales)
|
||||
monitorGroup.GET("/notifications", a.monitorController.ListNotifications)
|
||||
}
|
||||
a.logger.Info("数据监控相关接口注册成功 (需要认证和审计)")
|
||||
a.logger.Debug("数据监控相关接口注册成功 (需要认证和审计)")
|
||||
}
|
||||
|
||||
a.logger.Debug("所有接口注册成功")
|
||||
}
|
||||
|
||||
@@ -4,21 +4,21 @@ import (
|
||||
"errors"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUserNotFoundInContext 表示在 gin.Context 中未找到用户信息。
|
||||
// ErrUserNotFoundInContext 表示在 context 中未找到用户信息。
|
||||
ErrUserNotFoundInContext = errors.New("context中未找到用户信息")
|
||||
// ErrInvalidUserType 表示从 gin.Context 中获取的用户信息类型不正确。
|
||||
// ErrInvalidUserType 表示从 context 中获取的用户信息类型不正确。
|
||||
ErrInvalidUserType = errors.New("context中用户信息类型不正确")
|
||||
)
|
||||
|
||||
// GetOperatorIDFromContext 从 gin.Context 中提取操作者ID。
|
||||
// GetOperatorIDFromContext 从 echo.Context 中提取操作者ID。
|
||||
// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 ID 字段。
|
||||
func GetOperatorIDFromContext(c *gin.Context) (uint, error) {
|
||||
userVal, exists := c.Get(models.ContextUserKey.String())
|
||||
if !exists {
|
||||
func GetOperatorIDFromContext(c echo.Context) (uint, error) {
|
||||
userVal := c.Get(models.ContextUserKey.String())
|
||||
if userVal == nil {
|
||||
return 0, ErrUserNotFoundInContext
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ func GetOperatorIDFromContext(c *gin.Context) (uint, error) {
|
||||
return user.ID, nil
|
||||
}
|
||||
|
||||
// GetOperatorFromContext 从 gin.Context 中提取操作者。
|
||||
// GetOperatorFromContext 从 echo.Context 中提取操作者。
|
||||
// 假设操作者是由 AuthMiddleware 存储到 context 中的 *models.User 对象的字段。
|
||||
func GetOperatorFromContext(c *gin.Context) (*models.User, error) {
|
||||
userVal, exists := c.Get(models.ContextUserKey.String())
|
||||
if !exists {
|
||||
func GetOperatorFromContext(c echo.Context) (*models.User, error) {
|
||||
userVal := c.Get(models.ContextUserKey.String())
|
||||
if userVal == nil {
|
||||
return nil, ErrUserNotFoundInContext
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,29 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Controller 设备控制器,封装了所有与设备和区域主控相关的业务逻辑
|
||||
type Controller struct {
|
||||
deviceRepo repository.DeviceRepository
|
||||
areaControllerRepo repository.AreaControllerRepository
|
||||
deviceTemplateRepo repository.DeviceTemplateRepository
|
||||
deviceService device.Service
|
||||
deviceService service.DeviceService
|
||||
logger *logs.Logger
|
||||
}
|
||||
|
||||
// NewController 创建一个新的设备控制器实例
|
||||
func NewController(
|
||||
deviceRepo repository.DeviceRepository,
|
||||
areaControllerRepo repository.AreaControllerRepository,
|
||||
deviceTemplateRepo repository.DeviceTemplateRepository,
|
||||
deviceService device.Service,
|
||||
deviceService service.DeviceService,
|
||||
logger *logs.Logger,
|
||||
) *Controller {
|
||||
return &Controller{
|
||||
deviceRepo: deviceRepo,
|
||||
areaControllerRepo: areaControllerRepo,
|
||||
deviceTemplateRepo: deviceTemplateRepo,
|
||||
deviceService: deviceService,
|
||||
logger: logger,
|
||||
}
|
||||
@@ -54,58 +41,22 @@ func NewController(
|
||||
// @Param device body dto.CreateDeviceRequest true "设备信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
||||
// @Router /api/v1/devices [post]
|
||||
func (c *Controller) CreateDevice(ctx *gin.Context) {
|
||||
func (c *Controller) CreateDevice(ctx echo.Context) error {
|
||||
const actionType = "创建设备"
|
||||
var req dto.CreateDeviceRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
propertiesJSON, err := json.Marshal(req.Properties)
|
||||
resp, err := c.deviceService.CreateDevice(&req)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备失败: "+err.Error(), actionType, "服务层创建失败", req)
|
||||
}
|
||||
|
||||
device := &models.Device{
|
||||
Name: req.Name,
|
||||
DeviceTemplateID: req.DeviceTemplateID,
|
||||
AreaControllerID: req.AreaControllerID,
|
||||
Location: req.Location,
|
||||
Properties: propertiesJSON,
|
||||
}
|
||||
|
||||
if err := device.SelfCheck(); err != nil {
|
||||
c.logger.Errorf("%s: 设备属性自检失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备属性不符合要求: "+err.Error(), actionType, "设备属性自检失败", device)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.deviceRepo.Create(device); err != nil {
|
||||
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备失败: "+err.Error(), actionType, "数据库创建失败", device)
|
||||
return
|
||||
}
|
||||
|
||||
createdDevice, err := c.deviceRepo.FindByID(device.ID)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 重新加载创建的设备失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但重新加载设备失败", actionType, "重新加载设备失败", device)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := dto.NewDeviceResponse(createdDevice)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, createdDevice)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败", actionType, "响应序列化失败", createdDevice)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 设备创建成功, ID: %d", actionType, device.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备创建成功", resp, actionType, "设备创建成功", resp)
|
||||
c.logger.Infof("%s: 设备创建成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备创建成功", resp, actionType, "设备创建成功", resp)
|
||||
}
|
||||
|
||||
// GetDevice godoc
|
||||
@@ -117,42 +68,28 @@ func (c *Controller) CreateDevice(ctx *gin.Context) {
|
||||
// @Param id path string true "设备ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
||||
// @Router /api/v1/devices/{id} [get]
|
||||
func (c *Controller) GetDevice(ctx *gin.Context) {
|
||||
func (c *Controller) GetDevice(ctx echo.Context) error {
|
||||
const actionType = "获取设备"
|
||||
deviceID := ctx.Param("id")
|
||||
|
||||
if deviceID == "" {
|
||||
c.logger.Errorf("%s: 设备ID为空", actionType)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备ID不能为空", actionType, "设备ID为空", nil)
|
||||
return
|
||||
id, err := strconv.ParseUint(deviceID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
|
||||
}
|
||||
|
||||
device, err := c.deviceRepo.FindByIDString(deviceID)
|
||||
resp, err := c.deviceService.GetDevice(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
}
|
||||
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
||||
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
||||
return
|
||||
}
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: "+err.Error(), actionType, "服务层获取失败", deviceID)
|
||||
}
|
||||
|
||||
resp, err := dto.NewDeviceResponse(device)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, device)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误", actionType, "响应序列化失败", device)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 获取设备信息成功, ID: %d", actionType, device.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备信息成功", resp, actionType, "获取设备信息成功", resp)
|
||||
c.logger.Infof("%s: 获取设备信息成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备信息成功", resp, actionType, "获取设备信息成功", resp)
|
||||
}
|
||||
|
||||
// ListDevices godoc
|
||||
@@ -163,24 +100,16 @@ func (c *Controller) GetDevice(ctx *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} controller.Response{data=[]dto.DeviceResponse}
|
||||
// @Router /api/v1/devices [get]
|
||||
func (c *Controller) ListDevices(ctx *gin.Context) {
|
||||
func (c *Controller) ListDevices(ctx echo.Context) error {
|
||||
const actionType = "获取设备列表"
|
||||
devices, err := c.deviceRepo.ListAll()
|
||||
resp, err := c.deviceService.ListDevices()
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil)
|
||||
}
|
||||
|
||||
resp, err := dto.NewListDeviceResponse(devices)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, Devices: %+v", actionType, err, devices)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误", actionType, "响应序列化失败", devices)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 获取设备列表成功, 数量: %d", actionType, len(devices))
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备列表成功", resp, actionType, "获取设备列表成功", resp)
|
||||
c.logger.Infof("%s: 获取设备列表成功, 数量: %d", actionType, len(resp))
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备列表成功", resp, actionType, "获取设备列表成功", resp)
|
||||
}
|
||||
|
||||
// UpdateDevice godoc
|
||||
@@ -194,75 +123,34 @@ func (c *Controller) ListDevices(ctx *gin.Context) {
|
||||
// @Param device body dto.UpdateDeviceRequest true "要更新的设备信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
|
||||
// @Router /api/v1/devices/{id} [put]
|
||||
func (c *Controller) UpdateDevice(ctx *gin.Context) {
|
||||
func (c *Controller) UpdateDevice(ctx echo.Context) error {
|
||||
const actionType = "更新设备"
|
||||
deviceID := ctx.Param("id")
|
||||
|
||||
existingDevice, err := c.deviceRepo.FindByIDString(deviceID)
|
||||
var req dto.UpdateDeviceRequest
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(deviceID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
|
||||
}
|
||||
|
||||
resp, err := c.deviceService.UpdateDevice(uint(id), &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
}
|
||||
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
||||
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
||||
return
|
||||
}
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "服务层更新失败", deviceID)
|
||||
}
|
||||
|
||||
var req dto.UpdateDeviceRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
}
|
||||
|
||||
propertiesJSON, err := json.Marshal(req.Properties)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
||||
return
|
||||
}
|
||||
|
||||
existingDevice.Name = req.Name
|
||||
existingDevice.DeviceTemplateID = req.DeviceTemplateID
|
||||
existingDevice.AreaControllerID = req.AreaControllerID
|
||||
existingDevice.Location = req.Location
|
||||
existingDevice.Properties = propertiesJSON
|
||||
|
||||
if err := existingDevice.SelfCheck(); err != nil {
|
||||
c.logger.Errorf("%s: 设备属性自检失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备属性不符合要求: "+err.Error(), actionType, "设备属性自检失败", existingDevice)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.deviceRepo.Update(existingDevice); err != nil {
|
||||
c.logger.Errorf("%s: 数据库更新失败: %v, Device: %+v", actionType, err, existingDevice)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "数据库更新失败", existingDevice)
|
||||
return
|
||||
}
|
||||
|
||||
updatedDevice, err := c.deviceRepo.FindByID(existingDevice.ID)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 重新加载更新的设备失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但重新加载设备失败", actionType, "重新加载设备失败", existingDevice)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := dto.NewDeviceResponse(updatedDevice)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, updatedDevice)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败", actionType, "响应序列化失败", updatedDevice)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 设备更新成功, ID: %d", actionType, existingDevice.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备更新成功", resp, actionType, "设备更新成功", resp)
|
||||
c.logger.Infof("%s: 设备更新成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备更新成功", resp, actionType, "设备更新成功", resp)
|
||||
}
|
||||
|
||||
// DeleteDevice godoc
|
||||
@@ -274,37 +162,33 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) {
|
||||
// @Param id path string true "设备ID"
|
||||
// @Success 200 {object} controller.Response
|
||||
// @Router /api/v1/devices/{id} [delete]
|
||||
func (c *Controller) DeleteDevice(ctx *gin.Context) {
|
||||
func (c *Controller) DeleteDevice(ctx echo.Context) error {
|
||||
const actionType = "删除设备"
|
||||
deviceID := ctx.Param("id")
|
||||
|
||||
idUint, err := strconv.ParseUint(deviceID, 10, 64)
|
||||
id, err := strconv.ParseUint(deviceID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备ID格式", actionType, "设备ID格式错误", deviceID)
|
||||
return
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
|
||||
}
|
||||
|
||||
_, err = c.deviceRepo.FindByIDString(deviceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := c.deviceService.DeleteDevice(uint(id)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
case errors.Is(err, service.ErrDeviceInUse):
|
||||
c.logger.Warnf("%s: 尝试删除正在被使用的设备, ID: %s", actionType, deviceID)
|
||||
// 返回 409 Conflict 状态码,表示请求与服务器当前状态冲突
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "设备正在被使用", deviceID)
|
||||
default:
|
||||
c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "服务层删除失败", deviceID)
|
||||
}
|
||||
c.logger.Errorf("%s: 查找设备失败: %v, ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: 查找设备时发生内部错误", actionType, "数据库查询失败", deviceID)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.deviceRepo.Delete(uint(idUint)); err != nil {
|
||||
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "数据库删除失败", deviceID)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 设备删除成功, ID: %d", actionType, idUint)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备删除成功", nil, actionType, "设备删除成功", deviceID)
|
||||
c.logger.Infof("%s: 设备删除成功, ID: %s", actionType, deviceID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备删除成功", nil, actionType, "设备删除成功", deviceID)
|
||||
}
|
||||
|
||||
// ManualControl godoc
|
||||
@@ -318,60 +202,32 @@ func (c *Controller) DeleteDevice(ctx *gin.Context) {
|
||||
// @Param manualControl body dto.ManualControlDeviceRequest true "手动控制指令"
|
||||
// @Success 200 {object} controller.Response
|
||||
// @Router /api/v1/devices/manual-control/{id} [post]
|
||||
func (c *Controller) ManualControl(ctx *gin.Context) {
|
||||
func (c *Controller) ManualControl(ctx echo.Context) error {
|
||||
const actionType = "手动控制设备"
|
||||
deviceID := ctx.Param("id")
|
||||
|
||||
var req dto.ManualControlDeviceRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
dev, err := c.deviceRepo.FindByIDString(deviceID)
|
||||
id, err := strconv.ParseUint(deviceID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
|
||||
}
|
||||
|
||||
if err := c.deviceService.ManualControl(uint(id), &req); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
}
|
||||
if strings.Contains(err.Error(), "无效的设备ID格式") {
|
||||
c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID)
|
||||
return
|
||||
}
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "手动控制失败: "+err.Error(), actionType, "数据库查询失败", deviceID)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层手动控制失败: %v, ID: %s", actionType, err, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "手动控制失败: "+err.Error(), actionType, "服务层手动控制失败", deviceID)
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 接收到指令, 设备ID: %s, 动作: %s", actionType, deviceID, req.Action)
|
||||
if req.Action == nil {
|
||||
err = c.deviceService.Collect(dev.AreaControllerID, []*models.Device{dev})
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 获取设备状态失败: %v, 设备ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备状态失败: "+err.Error(), actionType, "获取设备状态失败", deviceID)
|
||||
}
|
||||
} else {
|
||||
action := device.DeviceActionStart
|
||||
switch *req.Action {
|
||||
case "off":
|
||||
action = device.DeviceActionStop
|
||||
case "on":
|
||||
default:
|
||||
c.logger.Errorf("%s: 无效的动作: %s, 设备ID: %s", actionType, *req.Action, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的动作: "+*req.Action, actionType, "无效的动作", req.Action)
|
||||
}
|
||||
err = c.deviceService.Switch(dev, action)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 设备控制失败: %v, 设备ID: %s", actionType, err, deviceID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备控制失败: "+err.Error(), actionType, "设备控制失败", deviceID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "指令已发送", map[string]interface{}{"device_id": deviceID}, actionType, "指令发送成功", gin.H{"device_id": deviceID, "action": req.Action})
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "指令已发送", nil, actionType, "指令发送成功", nil)
|
||||
}
|
||||
|
||||
// --- Controller Methods: Area Controllers ---
|
||||
@@ -386,50 +242,22 @@ func (c *Controller) ManualControl(ctx *gin.Context) {
|
||||
// @Param areaController body dto.CreateAreaControllerRequest true "区域主控信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
||||
// @Router /api/v1/area-controllers [post]
|
||||
func (c *Controller) CreateAreaController(ctx *gin.Context) {
|
||||
func (c *Controller) CreateAreaController(ctx echo.Context) error {
|
||||
const actionType = "创建区域主控"
|
||||
var req dto.CreateAreaControllerRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
propertiesJSON, err := json.Marshal(req.Properties)
|
||||
resp, err := c.deviceService.CreateAreaController(&req)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建区域主控失败: "+err.Error(), actionType, "服务层创建失败", req)
|
||||
}
|
||||
|
||||
ac := &models.AreaController{
|
||||
Name: req.Name,
|
||||
NetworkID: req.NetworkID,
|
||||
Location: req.Location,
|
||||
Properties: propertiesJSON,
|
||||
}
|
||||
|
||||
if err := ac.SelfCheck(); err != nil {
|
||||
c.logger.Errorf("%s: 区域主控自检失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "区域主控参数不符合要求: "+err.Error(), actionType, "区域主控自检失败", ac)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.areaControllerRepo.Create(ac); err != nil {
|
||||
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建区域主控失败: "+err.Error(), actionType, "数据库创建失败", ac)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := dto.NewAreaControllerResponse(ac)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控创建成功,但响应生成失败", actionType, "响应序列化失败", ac)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 区域主控创建成功, ID: %d", actionType, ac.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "区域主控创建成功", resp, actionType, "区域主控创建成功", resp)
|
||||
c.logger.Infof("%s: 区域主控创建成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "区域主控创建成功", resp, actionType, "区域主控创建成功", resp)
|
||||
}
|
||||
|
||||
// GetAreaController godoc
|
||||
@@ -441,38 +269,28 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) {
|
||||
// @Param id path string true "区域主控ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
||||
// @Router /api/v1/area-controllers/{id} [get]
|
||||
func (c *Controller) GetAreaController(ctx *gin.Context) {
|
||||
func (c *Controller) GetAreaController(ctx echo.Context) error {
|
||||
const actionType = "获取区域主控"
|
||||
acID := ctx.Param("id")
|
||||
|
||||
idUint, err := strconv.ParseUint(acID, 10, 64)
|
||||
id, err := strconv.ParseUint(acID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
||||
return
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, acID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
|
||||
}
|
||||
|
||||
ac, err := c.areaControllerRepo.FindByID(uint(idUint))
|
||||
resp, err := c.deviceService.GetAreaController(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||
}
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: "+err.Error(), actionType, "数据库查询失败", acID)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, acID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: "+err.Error(), actionType, "服务层获取失败", acID)
|
||||
}
|
||||
|
||||
resp, err := dto.NewAreaControllerResponse(ac)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, ac)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: 内部数据格式错误", actionType, "响应序列化失败", ac)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 获取区域主控信息成功, ID: %d", actionType, ac.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控信息成功", resp, actionType, "获取区域主控信息成功", resp)
|
||||
c.logger.Infof("%s: 获取区域主控信息成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控信息成功", resp, actionType, "获取区域主控信息成功", resp)
|
||||
}
|
||||
|
||||
// ListAreaControllers godoc
|
||||
@@ -483,24 +301,16 @@ func (c *Controller) GetAreaController(ctx *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} controller.Response{data=[]dto.AreaControllerResponse}
|
||||
// @Router /api/v1/area-controllers [get]
|
||||
func (c *Controller) ListAreaControllers(ctx *gin.Context) {
|
||||
func (c *Controller) ListAreaControllers(ctx echo.Context) error {
|
||||
const actionType = "获取区域主控列表"
|
||||
acs, err := c.areaControllerRepo.ListAll()
|
||||
resp, err := c.deviceService.ListAreaControllers()
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil)
|
||||
}
|
||||
|
||||
resp, err := dto.NewListAreaControllerResponse(acs)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, AreaControllers: %+v", actionType, err, acs)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: 内部数据格式错误", actionType, "响应序列化失败", acs)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 获取区域主控列表成功, 数量: %d", actionType, len(acs))
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控列表成功", resp, actionType, "获取区域主控列表成功", resp)
|
||||
c.logger.Infof("%s: 获取区域主控列表成功, 数量: %d", actionType, len(resp))
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控列表成功", resp, actionType, "获取区域主控列表成功", resp)
|
||||
}
|
||||
|
||||
// UpdateAreaController godoc
|
||||
@@ -514,69 +324,33 @@ func (c *Controller) ListAreaControllers(ctx *gin.Context) {
|
||||
// @Param areaController body dto.UpdateAreaControllerRequest true "要更新的区域主控信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
|
||||
// @Router /api/v1/area-controllers/{id} [put]
|
||||
func (c *Controller) UpdateAreaController(ctx *gin.Context) {
|
||||
func (c *Controller) UpdateAreaController(ctx echo.Context) error {
|
||||
const actionType = "更新区域主控"
|
||||
acID := ctx.Param("id")
|
||||
|
||||
idUint, err := strconv.ParseUint(acID, 10, 64)
|
||||
var req dto.UpdateAreaControllerRequest
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
id, err := strconv.ParseUint(acID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
||||
return
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, acID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
|
||||
}
|
||||
|
||||
existingAC, err := c.areaControllerRepo.FindByID(uint(idUint))
|
||||
resp, err := c.deviceService.UpdateAreaController(uint(id), &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||
}
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "数据库查询失败", acID)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, acID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "服务层更新失败", acID)
|
||||
}
|
||||
|
||||
var req dto.UpdateAreaControllerRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
}
|
||||
|
||||
propertiesJSON, err := json.Marshal(req.Properties)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties)
|
||||
return
|
||||
}
|
||||
|
||||
existingAC.Name = req.Name
|
||||
existingAC.NetworkID = req.NetworkID
|
||||
existingAC.Location = req.Location
|
||||
existingAC.Properties = propertiesJSON
|
||||
|
||||
if err := existingAC.SelfCheck(); err != nil {
|
||||
c.logger.Errorf("%s: 区域主控自检失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "区域主控参数不符合要求: "+err.Error(), actionType, "区域主控自检失败", existingAC)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.areaControllerRepo.Update(existingAC); err != nil {
|
||||
c.logger.Errorf("%s: 数据库更新失败: %v, AreaController: %+v", actionType, err, existingAC)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "数据库更新失败", existingAC)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := dto.NewAreaControllerResponse(existingAC)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, existingAC)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控更新成功,但响应生成失败", actionType, "响应序列化失败", existingAC)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 区域主控更新成功, ID: %d", actionType, existingAC.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控更新成功", resp, actionType, "区域主控更新成功", resp)
|
||||
c.logger.Infof("%s: 区域主控更新成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控更新成功", resp, actionType, "区域主控更新成功", resp)
|
||||
}
|
||||
|
||||
// DeleteAreaController godoc
|
||||
@@ -588,37 +362,32 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) {
|
||||
// @Param id path string true "区域主控ID"
|
||||
// @Success 200 {object} controller.Response
|
||||
// @Router /api/v1/area-controllers/{id} [delete]
|
||||
func (c *Controller) DeleteAreaController(ctx *gin.Context) {
|
||||
func (c *Controller) DeleteAreaController(ctx echo.Context) error {
|
||||
const actionType = "删除区域主控"
|
||||
acID := ctx.Param("id")
|
||||
|
||||
idUint, err := strconv.ParseUint(acID, 10, 64)
|
||||
id, err := strconv.ParseUint(acID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID)
|
||||
return
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, acID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
|
||||
}
|
||||
|
||||
_, err = c.areaControllerRepo.FindByID(uint(idUint))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := c.deviceService.DeleteAreaController(uint(id)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
|
||||
case errors.Is(err, service.ErrAreaControllerInUse):
|
||||
c.logger.Warnf("%s: 尝试删除正在被使用的主控, ID: %s", actionType, acID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "主控正在被使用", acID)
|
||||
default:
|
||||
c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, acID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "服务层删除失败", acID)
|
||||
}
|
||||
c.logger.Errorf("%s: 查找区域主控失败: %v, ID: %s", actionType, err, acID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: 查找时发生内部错误", actionType, "数据库查询失败", acID)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.areaControllerRepo.Delete(uint(idUint)); err != nil {
|
||||
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "数据库删除失败", acID)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 区域主控删除成功, ID: %d", actionType, idUint)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控删除成功", nil, actionType, "区域主控删除成功", acID)
|
||||
c.logger.Infof("%s: 区域主控删除成功, ID: %s", actionType, acID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控删除成功", nil, actionType, "区域主控删除成功", acID)
|
||||
}
|
||||
|
||||
// --- Controller Methods: Device Templates ---
|
||||
@@ -633,59 +402,22 @@ func (c *Controller) DeleteAreaController(ctx *gin.Context) {
|
||||
// @Param deviceTemplate body dto.CreateDeviceTemplateRequest true "设备模板信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
||||
// @Router /api/v1/device-templates [post]
|
||||
func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
|
||||
func (c *Controller) CreateDeviceTemplate(ctx echo.Context) error {
|
||||
const actionType = "创建设备模板"
|
||||
var req dto.CreateDeviceTemplateRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
commandsJSON, err := json.Marshal(req.Commands)
|
||||
resp, err := c.deviceService.CreateDeviceTemplate(&req)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备模板失败: "+err.Error(), actionType, "服务层创建失败", req)
|
||||
}
|
||||
|
||||
valuesJSON, err := json.Marshal(req.Values)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values)
|
||||
return
|
||||
}
|
||||
|
||||
deviceTemplate := &models.DeviceTemplate{
|
||||
Name: req.Name,
|
||||
Manufacturer: req.Manufacturer,
|
||||
Description: req.Description,
|
||||
Category: req.Category,
|
||||
Commands: commandsJSON,
|
||||
Values: valuesJSON,
|
||||
}
|
||||
|
||||
if err := deviceTemplate.SelfCheck(); err != nil {
|
||||
c.logger.Errorf("%s: 设备模板自检失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", deviceTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.deviceTemplateRepo.Create(deviceTemplate); err != nil {
|
||||
c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备模板失败: "+err.Error(), actionType, "数据库创建失败", deviceTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, deviceTemplate.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备模板创建成功", resp, actionType, "设备模板创建成功", resp)
|
||||
c.logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备模板创建成功", resp, actionType, "设备模板创建成功", resp)
|
||||
}
|
||||
|
||||
// GetDeviceTemplate godoc
|
||||
@@ -697,38 +429,28 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) {
|
||||
// @Param id path string true "设备模板ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
||||
// @Router /api/v1/device-templates/{id} [get]
|
||||
func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
|
||||
func (c *Controller) GetDeviceTemplate(ctx echo.Context) error {
|
||||
const actionType = "获取设备模板"
|
||||
dtID := ctx.Param("id")
|
||||
|
||||
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
||||
id, err := strconv.ParseUint(dtID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
||||
return
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
|
||||
}
|
||||
|
||||
deviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint))
|
||||
resp, err := c.deviceService.GetDeviceTemplate(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||
}
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: "+err.Error(), actionType, "数据库查询失败", dtID)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, dtID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: "+err.Error(), actionType, "服务层获取失败", dtID)
|
||||
}
|
||||
|
||||
resp, err := dto.NewDeviceTemplateResponse(deviceTemplate)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, deviceTemplate.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板信息成功", resp, actionType, "获取设备模板信息成功", resp)
|
||||
c.logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板信息成功", resp, actionType, "获取设备模板信息成功", resp)
|
||||
}
|
||||
|
||||
// ListDeviceTemplates godoc
|
||||
@@ -739,24 +461,16 @@ func (c *Controller) GetDeviceTemplate(ctx *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} controller.Response{data=[]dto.DeviceTemplateResponse}
|
||||
// @Router /api/v1/device-templates [get]
|
||||
func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
|
||||
func (c *Controller) ListDeviceTemplates(ctx echo.Context) error {
|
||||
const actionType = "获取设备模板列表"
|
||||
deviceTemplates, err := c.deviceTemplateRepo.ListAll()
|
||||
resp, err := c.deviceService.ListDeviceTemplates()
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: "+err.Error(), actionType, "数据库查询失败", nil)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil)
|
||||
}
|
||||
|
||||
resp, err := dto.NewListDeviceTemplateResponse(deviceTemplates)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(deviceTemplates))
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板列表成功", resp, actionType, "获取设备模板列表成功", resp)
|
||||
c.logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(resp))
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板列表成功", resp, actionType, "获取设备模板列表成功", resp)
|
||||
}
|
||||
|
||||
// UpdateDeviceTemplate godoc
|
||||
@@ -770,78 +484,34 @@ func (c *Controller) ListDeviceTemplates(ctx *gin.Context) {
|
||||
// @Param deviceTemplate body dto.UpdateDeviceTemplateRequest true "要更新的设备模板信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
|
||||
// @Router /api/v1/device-templates/{id} [put]
|
||||
func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
|
||||
func (c *Controller) UpdateDeviceTemplate(ctx echo.Context) error {
|
||||
const actionType = "更新设备模板"
|
||||
dtID := ctx.Param("id")
|
||||
|
||||
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
||||
return
|
||||
var req dto.UpdateDeviceTemplateRequest
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
existingDeviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint))
|
||||
id, err := strconv.ParseUint(dtID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
|
||||
}
|
||||
|
||||
resp, err := c.deviceService.UpdateDeviceTemplate(uint(id), &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||
}
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库查询失败", dtID)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, dtID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "服务层更新失败", dtID)
|
||||
}
|
||||
|
||||
var req dto.UpdateDeviceTemplateRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
}
|
||||
|
||||
commandsJSON, err := json.Marshal(req.Commands)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands)
|
||||
return
|
||||
}
|
||||
|
||||
valuesJSON, err := json.Marshal(req.Values)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values)
|
||||
return
|
||||
}
|
||||
|
||||
existingDeviceTemplate.Name = req.Name
|
||||
existingDeviceTemplate.Manufacturer = req.Manufacturer
|
||||
existingDeviceTemplate.Description = req.Description
|
||||
existingDeviceTemplate.Category = req.Category
|
||||
existingDeviceTemplate.Commands = commandsJSON
|
||||
existingDeviceTemplate.Values = valuesJSON
|
||||
|
||||
if err := existingDeviceTemplate.SelfCheck(); err != nil {
|
||||
c.logger.Errorf("%s: 设备模板自检失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", existingDeviceTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.deviceTemplateRepo.Update(existingDeviceTemplate); err != nil {
|
||||
c.logger.Errorf("%s: 数据库更新失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库更新失败", existingDeviceTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := dto.NewDeviceTemplateResponse(existingDeviceTemplate)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, existingDeviceTemplate.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板更新成功", resp, actionType, "设备模板更新成功", resp)
|
||||
c.logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板更新成功", resp, actionType, "设备模板更新成功", resp)
|
||||
}
|
||||
|
||||
// DeleteDeviceTemplate godoc
|
||||
@@ -853,43 +523,30 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) {
|
||||
// @Param id path string true "设备模板ID"
|
||||
// @Success 200 {object} controller.Response
|
||||
// @Router /api/v1/device-templates/{id} [delete]
|
||||
func (c *Controller) DeleteDeviceTemplate(ctx *gin.Context) {
|
||||
func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error {
|
||||
const actionType = "删除设备模板"
|
||||
dtID := ctx.Param("id")
|
||||
|
||||
idUint, err := strconv.ParseUint(dtID, 10, 64)
|
||||
id, err := strconv.ParseUint(dtID, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID)
|
||||
return
|
||||
c.logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
|
||||
}
|
||||
|
||||
// 在尝试删除之前,先检查设备模板是否存在
|
||||
_, err = c.deviceTemplateRepo.FindByID(uint(idUint))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := c.deviceService.DeleteDeviceTemplate(uint(id)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
|
||||
case errors.Is(err, service.ErrDeviceTemplateInUse):
|
||||
c.logger.Warnf("%s: 尝试删除正在被使用的模板, ID: %s", actionType, dtID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "模板正在被使用", dtID)
|
||||
default:
|
||||
c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, dtID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "服务层删除失败", dtID)
|
||||
}
|
||||
c.logger.Errorf("%s: 查找设备模板失败: %v, ID: %s", actionType, err, dtID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: 查找时发生内部错误", actionType, "数据库查询失败", dtID)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用仓库层的删除方法,该方法会检查模板是否被使用
|
||||
if err := c.deviceTemplateRepo.Delete(uint(idUint)); err != nil {
|
||||
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint)
|
||||
// 如果错误信息包含“设备模板正在被设备使用,无法删除”,则返回特定的错误码
|
||||
if strings.Contains(err.Error(), "设备模板正在被设备使用,无法删除") {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备模板正在使用", dtID)
|
||||
} else {
|
||||
// 其他数据库错误
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "数据库删除失败", dtID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Infof("%s: 设备模板删除成功, ID: %d", actionType, idUint)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板删除成功", nil, actionType, "设备模板删除成功", dtID)
|
||||
c.logger.Infof("%s: 设备模板删除成功, ID: %s", actionType, dtID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板删除成功", nil, actionType, "设备模板删除成功", dtID)
|
||||
}
|
||||
|
||||
@@ -1,741 +0,0 @@
|
||||
package device_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MockDeviceRepository 是 DeviceRepository 接口的模拟实现
|
||||
type MockDeviceRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreateTx 模拟 DeviceRepository 的 CreateTx 方法
|
||||
func (m *MockDeviceRepository) Create(device *models.Device) error {
|
||||
args := m.Called(device)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// FindByID 模拟 DeviceRepository 的 FindByID 方法
|
||||
func (m *MockDeviceRepository) FindByID(id uint) (*models.Device, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.Device), args.Error(1)
|
||||
}
|
||||
|
||||
// FindByIDString 模拟 DeviceRepository 的 FindByIDString 方法
|
||||
func (m *MockDeviceRepository) FindByIDString(id string) (*models.Device, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.Device), args.Error(1)
|
||||
}
|
||||
|
||||
// ListAll 模拟 DeviceRepository 的 ListAll 方法
|
||||
func (m *MockDeviceRepository) ListAll() ([]*models.Device, error) {
|
||||
args := m.Called()
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]*models.Device), args.Error(1)
|
||||
}
|
||||
|
||||
// ListByParentID 模拟 DeviceRepository 的 ListByParentID 方法
|
||||
func (m *MockDeviceRepository) ListByParentID(parentID *uint) ([]*models.Device, error) {
|
||||
args := m.Called(parentID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]*models.Device), args.Error(1)
|
||||
}
|
||||
|
||||
// Update 模拟 DeviceRepository 的 Update 方法
|
||||
func (m *MockDeviceRepository) Update(device *models.Device) error {
|
||||
args := m.Called(device)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Delete 模拟 DeviceRepository 的 Delete 方法
|
||||
func (m *MockDeviceRepository) Delete(id uint) error {
|
||||
args := m.Called(id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// testCase 结构体定义了所有测试用例的通用参数
|
||||
type testCase struct {
|
||||
name string
|
||||
httpMethod string // 新增字段:HTTP 方法
|
||||
requestBody interface{}
|
||||
paramID string // URL 中的 ID 参数
|
||||
mockRepoSetup func(*MockDeviceRepository)
|
||||
expectedStatus int // HTTP 状态码
|
||||
expectedCode int // 业务状态码
|
||||
expectedMessage string
|
||||
expectedDataFunc func(interface{}) bool // 用于验证 data 字段的函数
|
||||
}
|
||||
|
||||
// runTest 是一个辅助函数,用于执行单个测试用例
|
||||
func runTest(t *testing.T, tc testCase, controllerMethod func(*gin.Context, *MockDeviceRepository)) {
|
||||
// 初始化 Gin 上下文
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
|
||||
// 设置请求体和 HTTP 方法
|
||||
if tc.requestBody != nil {
|
||||
jsonBody, _ := json.Marshal(tc.requestBody)
|
||||
ctx.Request = httptest.NewRequest(tc.httpMethod, "/", io.NopCloser(bytes.NewBuffer(jsonBody)))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
// 对于没有请求体的请求 (GET, DELETE, 或没有 body 的 POST/PUT)
|
||||
ctx.Request = httptest.NewRequest(tc.httpMethod, "/", nil)
|
||||
}
|
||||
|
||||
// 设置 URL 参数
|
||||
if tc.paramID != "" {
|
||||
ctx.Params = append(ctx.Params, gin.Param{Key: "id", Value: tc.paramID})
|
||||
}
|
||||
|
||||
// 创建 Mock Repository
|
||||
mockRepo := new(MockDeviceRepository)
|
||||
// 设置 Mock 行为
|
||||
tc.mockRepoSetup(mockRepo)
|
||||
|
||||
// 调用被测试的方法,并传入 mockRepo
|
||||
controllerMethod(ctx, mockRepo)
|
||||
|
||||
// 解析响应体
|
||||
var responseBody controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 断言 HTTP 状态码始终为 200 OK
|
||||
assert.Equal(t, tc.expectedStatus, w.Code)
|
||||
|
||||
// 断言业务状态码和消息
|
||||
assert.Equal(t, tc.expectedCode, responseBody.Code)
|
||||
assert.Equal(t, tc.expectedMessage, responseBody.Message)
|
||||
|
||||
// 断言数据字段
|
||||
if tc.expectedDataFunc != nil {
|
||||
var data interface{}
|
||||
// 只有当 responseBody.Data 不为 nil 且其底层类型为 []byte 时才尝试 Unmarshal
|
||||
if responseBody.Data != nil {
|
||||
if byteData, ok := responseBody.Data.([]byte); ok {
|
||||
err = json.Unmarshal(byteData, &data)
|
||||
assert.NoError(t, err, "无法解析响应数据") // 增加对 Unmarshal 错误的断言
|
||||
} else {
|
||||
// 如果 Data 不为 nil 但也不是 []byte,这通常不应该发生
|
||||
// 但为了健壮性,直接将原始 interface{} 赋值给 data
|
||||
data = responseBody.Data
|
||||
}
|
||||
}
|
||||
assert.True(t, tc.expectedDataFunc(data), "数据字段验证失败")
|
||||
}
|
||||
|
||||
// 验证 Mock 期望是否都已满足
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestCreateDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功创建区域主控",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "主控A",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
Location: "猪舍1",
|
||||
Properties: controller.Properties(`{"lora_address":"0x1234"}`),
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("CreateTx", mock.MatchedBy(func(dev *models.Device) bool {
|
||||
// 检查 Name 字段
|
||||
nameMatch := dev.Name == "主控A"
|
||||
// 检查 Type 字段
|
||||
typeMatch := dev.Type == models.DeviceTypeAreaController
|
||||
// 检查 Location 字段
|
||||
locationMatch := dev.Location == "猪舍1"
|
||||
// 检查 Properties 字段的字节内容
|
||||
expectedProperties := controller.Properties(`{"lora_address":"0x1234"}`)
|
||||
propertiesMatch := bytes.Equal(dev.Properties, expectedProperties)
|
||||
|
||||
return nameMatch && typeMatch && locationMatch && propertiesMatch
|
||||
})).Return(nil).Run(func(args mock.Arguments) {
|
||||
// 模拟 GORM 自动填充 ID
|
||||
arg := args.Get(0).(*models.Device)
|
||||
arg.ID = 1
|
||||
arg.CreatedAt = time.Now()
|
||||
arg.UpdatedAt = time.Now()
|
||||
}).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeCreated,
|
||||
expectedMessage: "设备创建成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] != nil &&
|
||||
dataMap["name"] == "主控A" &&
|
||||
dataMap["type"] == string(models.DeviceTypeAreaController) &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "成功创建普通设备",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "温度传感器",
|
||||
Type: models.DeviceTypeDevice,
|
||||
SubType: models.SubTypeSensorTemp,
|
||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
||||
Location: "猪舍1-A区",
|
||||
Properties: controller.Properties(`{"bus_id":1,"bus_address":10}`),
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("CreateTx", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
|
||||
arg := args.Get(0).(*models.Device)
|
||||
arg.ID = 2
|
||||
arg.CreatedAt = time.Now()
|
||||
arg.UpdatedAt = time.Now()
|
||||
}).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeCreated,
|
||||
expectedMessage: "设备创建成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] != nil &&
|
||||
dataMap["name"] == "温度传感器" &&
|
||||
dataMap["type"] == string(models.DeviceTypeDevice) &&
|
||||
dataMap["sub_type"] == string(models.SubTypeSensorTemp) &&
|
||||
dataMap["parent_id"] != nil &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "", // 缺少必填字段 Name
|
||||
Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "Key: 'CreateDeviceRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "数据库创建失败",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "失败设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("CreateTx", mock.Anything).Return(errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "创建设备失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
// 新增:Properties字段JSON格式无效
|
||||
{
|
||||
name: "Properties字段JSON格式无效",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "无效JSON设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
Properties: controller.Properties(`{invalid json}`),
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 期望 CreateTx 方法被调用,并返回一个模拟的数据库错误
|
||||
// 这个错误模拟的是数据库层因为 Properties 字段的 JSON 格式无效而拒绝保存
|
||||
m.On("CreateTx", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
|
||||
dev := args.Get(0).(*models.Device)
|
||||
assert.Equal(t, "无效JSON设备", dev.Name)
|
||||
assert.Equal(t, models.DeviceTypeDevice, dev.Type)
|
||||
expectedProperties := controller.Properties(`{invalid json}`)
|
||||
assert.True(t, bytes.Equal(dev.Properties, expectedProperties), "Properties should match")
|
||||
}).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK, // HTTP status is 200 OK for business errors
|
||||
expectedCode: controller.CodeInternalError, // Business code for internal server error
|
||||
expectedMessage: "创建设备失败", // The message returned by the controller
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).CreateDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功获取设备",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "1").Return(&models.Device{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "测试设备",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
Location: "测试地点",
|
||||
Properties: datatypes.JSON(`{"key":"value"}`),
|
||||
}, nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "获取设备信息成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] == float64(1) &&
|
||||
dataMap["name"] == "测试设备" &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "设备未找到",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "999",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "999").Return(nil, gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeNotFound,
|
||||
expectedMessage: "设备未找到",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "ID格式无效",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "abc",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "abc").Return(nil, errors.New("无效的设备ID格式")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "无效的设备ID格式",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "数据库查询失败",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "1").Return(nil, errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "获取设备信息失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).GetDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDevices(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功获取空列表",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("ListAll").Return([]*models.Device{}, nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "获取设备列表成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
s, ok := data.([]interface{})
|
||||
return ok && len(s) == 0
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "成功获取包含设备的列表",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("ListAll").Return([]*models.Device{
|
||||
{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "设备1",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
{
|
||||
Model: gorm.Model{
|
||||
ID: 2,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "设备2",
|
||||
Type: models.DeviceTypeDevice,
|
||||
SubType: models.SubTypeFan,
|
||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
||||
},
|
||||
}, nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "获取设备列表成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataList, ok := data.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// 检查长度
|
||||
if len(dataList) != 2 {
|
||||
return false
|
||||
}
|
||||
// 检查第一个设备
|
||||
item1, ok1 := dataList[0].(map[string]interface{})
|
||||
if !ok1 || item1["id"] != float64(1) || item1["name"] != "设备1" {
|
||||
return false
|
||||
}
|
||||
// 检查第二个设备
|
||||
item2, ok2 := dataList[1].(map[string]interface{})
|
||||
if !ok2 || item2["id"] != float64(2) || item2["name"] != "设备2" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "数据库查询失败",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("ListAll").Return(nil, errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "获取设备列表失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).ListDevices(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功更新设备",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "更新后的主控",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
Location: "新地点",
|
||||
Properties: controller.Properties(`{"lora_address":"0x5678"}`),
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 模拟 FindByIDString 找到设备
|
||||
m.On("FindByIDString", "1").Return(&models.Device{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "旧主控",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
Location: "旧地点",
|
||||
Properties: datatypes.JSON(`{"lora_address":"0x1234"}`),
|
||||
}, nil).Once()
|
||||
// 模拟 Update 成功
|
||||
m.On("Update", mock.AnythingOfType("*models.Device")).Return(nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "设备更新成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] == float64(1) &&
|
||||
dataMap["name"] == "更新后的主控" &&
|
||||
dataMap["location"] == "新地点" &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "", // 缺少必填字段 Name
|
||||
Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 模拟 FindByIDString 找到设备,以便进入参数绑定阶段
|
||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "Key: 'UpdateDeviceRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "设备未找到",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "任意名称", Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
paramID: "999",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "999").Return(nil, gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeNotFound,
|
||||
expectedMessage: "设备未找到",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "ID格式无效",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "任意名称", Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
paramID: "abc",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "abc").Return(nil, errors.New("无效的设备ID格式")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "无效的设备ID格式",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "数据库更新失败",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "更新失败设备", Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
||||
m.On("Update", mock.AnythingOfType("*models.Device")).Return(errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "更新设备失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
// 新增:Properties字段JSON格式无效
|
||||
{
|
||||
name: "Properties字段JSON格式无效",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "无效JSON设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
Properties: controller.Properties(`{invalid json}`),
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 模拟 FindByIDString 找到设备,以便进入参数绑定阶段
|
||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
||||
// 期望 Update 方法被调用,并返回一个模拟的数据库错误
|
||||
m.On("Update", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
|
||||
dev := args.Get(0).(*models.Device)
|
||||
assert.Equal(t, "无效JSON设备", dev.Name)
|
||||
assert.Equal(t, models.DeviceTypeDevice, dev.Type)
|
||||
expectedProperties := controller.Properties(`{invalid json}`)
|
||||
assert.True(t, bytes.Equal(dev.Properties, expectedProperties), "Properties should match")
|
||||
}).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError, // Expected to be internal server error due to DB error
|
||||
expectedMessage: "更新设备失败", // The message returned by the controller
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
// 新增:成功更新设备的ParentID
|
||||
{
|
||||
name: "成功更新设备的ParentID",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "更新ParentID设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
ParentID: func() *uint { id := uint(10); return &id }(),
|
||||
Location: "新地点",
|
||||
Properties: controller.Properties(`{"key":"value"}`),
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 模拟 FindByIDString 找到设备
|
||||
m.On("FindByIDString", "1").Return(&models.Device{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "旧设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
||||
Location: "旧地点",
|
||||
Properties: datatypes.JSON(`{"old_key":"old_value"}`),
|
||||
}, nil).Once()
|
||||
// 模拟 Update 成功,并验证 ParentID 被更新
|
||||
m.On("Update", mock.MatchedBy(func(dev *models.Device) bool {
|
||||
return dev.ID == 1 && *dev.ParentID == 10
|
||||
})).Return(nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "设备更新成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] == float64(1) &&
|
||||
dataMap["parent_id"] == float64(10) &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).UpdateDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功删除设备",
|
||||
httpMethod: http.MethodDelete,
|
||||
requestBody: nil,
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("Delete", uint(1)).Return(nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "设备删除成功",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "ID格式无效",
|
||||
httpMethod: http.MethodDelete,
|
||||
requestBody: nil,
|
||||
paramID: "abc",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "无效的设备ID格式",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "数据库删除失败",
|
||||
httpMethod: http.MethodDelete,
|
||||
requestBody: nil,
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("Delete", uint(1)).Return(errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "删除设备失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
// 新增:删除设备未找到
|
||||
{
|
||||
name: "删除设备未找到",
|
||||
httpMethod: http.MethodDelete,
|
||||
requestBody: nil,
|
||||
paramID: "999",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("Delete", uint(999)).Return(gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError, // 当前控制器逻辑会将 ErrRecordNotFound 视为内部错误
|
||||
expectedMessage: "删除设备失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).DeleteDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,39 +7,39 @@ import (
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// mapAndSendError 统一映射服务层错误并发送响应。
|
||||
// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。
|
||||
func mapAndSendError(c *PigBatchController, ctx *gin.Context, action string, err error, id uint) {
|
||||
func mapAndSendError(c *PigBatchController, ctx echo.Context, action string, err error, id uint) error {
|
||||
if errors.Is(err, service.ErrPigBatchNotFound) ||
|
||||
errors.Is(err, service.ErrPenNotFound) ||
|
||||
errors.Is(err, service.ErrPenNotAssociatedWithBatch) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
||||
} else if errors.Is(err, service.ErrInvalidOperation) ||
|
||||
errors.Is(err, service.ErrPigBatchActive) ||
|
||||
errors.Is(err, service.ErrPigBatchNotActive) ||
|
||||
errors.Is(err, service.ErrPenOccupiedByOtherBatch) ||
|
||||
errors.Is(err, service.ErrPenStatusInvalidForAllocation) ||
|
||||
errors.Is(err, service.ErrPenNotEmpty) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||
} else {
|
||||
c.logger.Errorf("操作[%s]业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, fmt.Sprintf("操作失败: %v", err), action, err.Error(), id)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, fmt.Sprintf("操作失败: %v", err), action, err.Error(), id)
|
||||
}
|
||||
}
|
||||
|
||||
// idExtractorFunc 定义了一个函数类型,用于从gin.Context中提取主ID。
|
||||
type idExtractorFunc func(ctx *gin.Context) (uint, error)
|
||||
// idExtractorFunc 定义了一个函数类型,用于从echo.Context中提取主ID。
|
||||
type idExtractorFunc func(ctx echo.Context) (uint, error)
|
||||
|
||||
// extractOperatorAndPrimaryID 封装了从gin.Context中提取操作员ID和主ID的通用逻辑。
|
||||
// extractOperatorAndPrimaryID 封装了从echo.Context中提取操作员ID和主ID的通用逻辑。
|
||||
// 它负责处理ID提取过程中的错误,并发送相应的HTTP响应。
|
||||
//
|
||||
// 参数:
|
||||
//
|
||||
// c: *PigBatchController - 控制器实例,用于访问其日志。
|
||||
// ctx: *gin.Context - Gin上下文。
|
||||
// ctx: echo.Context - Echo上下文。
|
||||
// action: string - 当前操作的描述,用于日志和审计。
|
||||
// idExtractor: idExtractorFunc - 可选函数,用于从ctx中提取主ID。如果为nil,则尝试从":id"路径参数中提取。
|
||||
//
|
||||
@@ -47,26 +47,24 @@ type idExtractorFunc func(ctx *gin.Context) (uint, error)
|
||||
//
|
||||
// operatorID: uint - 提取到的操作员ID。
|
||||
// primaryID: uint - 提取到的主ID。
|
||||
// ok: bool - 如果ID提取成功且没有发送错误响应,则为true。
|
||||
// err: error - 如果ID提取失败或发送错误响应,则返回错误。
|
||||
func extractOperatorAndPrimaryID(
|
||||
c *PigBatchController,
|
||||
ctx *gin.Context,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
idExtractor idExtractorFunc,
|
||||
) (operatorID uint, primaryID uint, ok bool) {
|
||||
) (operatorID uint, primaryID uint, err error) {
|
||||
// 1. 获取操作员ID
|
||||
operatorID, err := controller.GetOperatorIDFromContext(ctx)
|
||||
operatorID, err = controller.GetOperatorIDFromContext(ctx)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
|
||||
return 0, 0, false
|
||||
return 0, 0, controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
|
||||
}
|
||||
|
||||
// 2. 提取主ID
|
||||
if idExtractor != nil {
|
||||
primaryID, err = idExtractor(ctx)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error())
|
||||
return 0, 0, false
|
||||
return 0, 0, controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error())
|
||||
}
|
||||
} else { // 默认从 ":id" 路径参数提取
|
||||
idParam := ctx.Param("id")
|
||||
@@ -75,165 +73,155 @@ func extractOperatorAndPrimaryID(
|
||||
} else {
|
||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam)
|
||||
return 0, 0, false
|
||||
return 0, 0, controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam)
|
||||
}
|
||||
primaryID = uint(parsedID)
|
||||
}
|
||||
}
|
||||
|
||||
return operatorID, primaryID, true
|
||||
return operatorID, primaryID, nil
|
||||
}
|
||||
|
||||
// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。
|
||||
// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。
|
||||
func handleAPIRequest[Req any](
|
||||
c *PigBatchController,
|
||||
ctx *gin.Context,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
reqDTO Req,
|
||||
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint, req Req) error,
|
||||
successMsg string,
|
||||
idExtractor idExtractorFunc,
|
||||
) {
|
||||
) error {
|
||||
// 1. 绑定请求体
|
||||
if err := ctx.ShouldBindJSON(&reqDTO); err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO)
|
||||
return
|
||||
if err := ctx.Bind(&reqDTO); err != nil {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO)
|
||||
}
|
||||
|
||||
// 2. 提取操作员ID和主ID
|
||||
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||
if !ok {
|
||||
return // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||
operatorID, primaryID, err := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||
if err != nil {
|
||||
return err // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||
}
|
||||
|
||||
// 3. 执行服务层逻辑
|
||||
err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
|
||||
err = serviceExecutor(ctx, operatorID, primaryID, reqDTO)
|
||||
if err != nil {
|
||||
mapAndSendError(c, ctx, action, err, primaryID)
|
||||
return
|
||||
return mapAndSendError(c, ctx, action, err, primaryID)
|
||||
}
|
||||
|
||||
// 4. 发送成功响应
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
|
||||
}
|
||||
|
||||
// handleNoBodyAPIRequest 封装了处理不带请求体,但有路径参数和操作员ID的API请求的通用逻辑。
|
||||
func handleNoBodyAPIRequest(
|
||||
c *PigBatchController,
|
||||
ctx *gin.Context,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) error,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint) error,
|
||||
successMsg string,
|
||||
idExtractor idExtractorFunc,
|
||||
) {
|
||||
) error {
|
||||
// 1. 提取操作员ID和主ID
|
||||
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||
if !ok {
|
||||
return // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||
operatorID, primaryID, err := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||
if err != nil {
|
||||
return err // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||
}
|
||||
|
||||
// 2. 执行服务层逻辑
|
||||
err := serviceExecutor(ctx, operatorID, primaryID)
|
||||
err = serviceExecutor(ctx, operatorID, primaryID)
|
||||
if err != nil {
|
||||
mapAndSendError(c, ctx, action, err, primaryID)
|
||||
return
|
||||
return mapAndSendError(c, ctx, action, err, primaryID)
|
||||
}
|
||||
|
||||
// 3. 发送成功响应
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID)
|
||||
}
|
||||
|
||||
// handleAPIRequestWithResponse 封装了控制器中处理带有请求体、路径参数并返回响应DTO的API请求的通用逻辑。
|
||||
func handleAPIRequestWithResponse[Req any, Resp any](
|
||||
c *PigBatchController,
|
||||
ctx *gin.Context,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
reqDTO Req,
|
||||
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) (Resp, error), // serviceExecutor现在返回Resp
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint, req Req) (Resp, error), // serviceExecutor现在返回Resp
|
||||
successMsg string,
|
||||
idExtractor idExtractorFunc,
|
||||
) {
|
||||
) error {
|
||||
// 1. 绑定请求体
|
||||
if err := ctx.ShouldBindJSON(&reqDTO); err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, fmt.Sprintf("无效的请求体: %v", err), action, fmt.Sprintf("请求体绑定失败: %v", err), reqDTO)
|
||||
return
|
||||
if err := ctx.Bind(&reqDTO); err != nil {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, fmt.Sprintf("无效的请求体: %v", err), action, fmt.Sprintf("请求体绑定失败: %v", err), reqDTO)
|
||||
}
|
||||
|
||||
// 2. 提取操作员ID和主ID
|
||||
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||
if !ok {
|
||||
return // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||
operatorID, primaryID, err := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||
if err != nil {
|
||||
return err // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||
}
|
||||
|
||||
// 3. 执行服务层逻辑
|
||||
respDTO, err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
|
||||
if err != nil {
|
||||
mapAndSendError(c, ctx, action, err, primaryID)
|
||||
return
|
||||
return mapAndSendError(c, ctx, action, err, primaryID)
|
||||
}
|
||||
|
||||
// 4. 发送成功响应
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
|
||||
}
|
||||
|
||||
// handleNoBodyAPIRequestWithResponse 封装了处理不带请求体,但有路径参数和操作员ID,并返回响应DTO的API请求的通用逻辑。
|
||||
func handleNoBodyAPIRequestWithResponse[Resp any](
|
||||
c *PigBatchController,
|
||||
ctx *gin.Context,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) (Resp, error), // serviceExecutor现在返回Resp
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint) (Resp, error), // serviceExecutor现在返回Resp
|
||||
successMsg string,
|
||||
idExtractor idExtractorFunc,
|
||||
) {
|
||||
) error {
|
||||
// 1. 提取操作员ID和主ID
|
||||
operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||
if !ok {
|
||||
return // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||
operatorID, primaryID, err := extractOperatorAndPrimaryID(c, ctx, action, idExtractor)
|
||||
if err != nil {
|
||||
return err // 错误已在 extractOperatorAndPrimaryID 中处理
|
||||
}
|
||||
|
||||
// 2. 执行服务层逻辑
|
||||
respDTO, err := serviceExecutor(ctx, operatorID, primaryID)
|
||||
if err != nil {
|
||||
mapAndSendError(c, ctx, action, err, primaryID)
|
||||
return
|
||||
return mapAndSendError(c, ctx, action, err, primaryID)
|
||||
}
|
||||
|
||||
// 3. 发送成功响应
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID)
|
||||
}
|
||||
|
||||
// handleQueryAPIRequestWithResponse 封装了处理带有查询参数并返回响应DTO的API请求的通用逻辑。
|
||||
func handleQueryAPIRequestWithResponse[Query any, Resp any](
|
||||
c *PigBatchController,
|
||||
ctx *gin.Context,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
queryDTO Query,
|
||||
serviceExecutor func(ctx *gin.Context, operatorID uint, query Query) (Resp, error), // serviceExecutor现在接收queryDTO
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, query Query) (Resp, error), // serviceExecutor现在接收queryDTO
|
||||
successMsg string,
|
||||
) {
|
||||
) error {
|
||||
// 1. 绑定查询参数
|
||||
if err := ctx.ShouldBindQuery(&queryDTO); err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", queryDTO)
|
||||
return
|
||||
if err := ctx.Bind(&queryDTO); err != nil {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", queryDTO)
|
||||
}
|
||||
|
||||
// 2. 获取操作员ID
|
||||
operatorID, err := controller.GetOperatorIDFromContext(ctx)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil)
|
||||
}
|
||||
|
||||
// 3. 执行服务层逻辑
|
||||
respDTO, err := serviceExecutor(ctx, operatorID, queryDTO)
|
||||
if err != nil {
|
||||
// 对于列表查询,通常没有primaryID,所以传递0
|
||||
mapAndSendError(c, ctx, action, err, 0)
|
||||
return
|
||||
return mapAndSendError(c, ctx, action, err, 0)
|
||||
}
|
||||
|
||||
// 4. 发送成功响应
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, nil)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, nil)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// PigBatchController 负责处理猪批次相关的API请求
|
||||
@@ -34,13 +34,13 @@ func NewPigBatchController(logger *logs.Logger, service service.PigBatchService)
|
||||
// @Param body body dto.PigBatchCreateDTO true "猪批次信息"
|
||||
// @Success 201 {object} controller.Response{data=dto.PigBatchResponseDTO} "创建成功"
|
||||
// @Router /api/v1/pig-batches [post]
|
||||
func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) {
|
||||
func (c *PigBatchController) CreatePigBatch(ctx echo.Context) error {
|
||||
const action = "创建猪批次"
|
||||
var req dto.PigBatchCreateDTO
|
||||
|
||||
handleAPIRequestWithResponse(
|
||||
return handleAPIRequestWithResponse(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
// 对于创建操作,primaryID通常不从路径中获取,而是由服务层生成
|
||||
return c.service.CreatePigBatch(operatorID, req)
|
||||
},
|
||||
@@ -58,12 +58,12 @@ func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) {
|
||||
// @Param id path int true "猪批次ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功"
|
||||
// @Router /api/v1/pig-batches/{id} [get]
|
||||
func (c *PigBatchController) GetPigBatch(ctx *gin.Context) {
|
||||
func (c *PigBatchController) GetPigBatch(ctx echo.Context) error {
|
||||
const action = "获取猪批次"
|
||||
|
||||
handleNoBodyAPIRequestWithResponse(
|
||||
return handleNoBodyAPIRequestWithResponse(
|
||||
c, ctx, action,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) {
|
||||
return c.service.GetPigBatch(primaryID)
|
||||
},
|
||||
"获取成功",
|
||||
@@ -82,13 +82,13 @@ func (c *PigBatchController) GetPigBatch(ctx *gin.Context) {
|
||||
// @Param body body dto.PigBatchUpdateDTO true "猪批次信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "更新成功"
|
||||
// @Router /api/v1/pig-batches/{id} [put]
|
||||
func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) {
|
||||
func (c *PigBatchController) UpdatePigBatch(ctx echo.Context) error {
|
||||
const action = "更新猪批次"
|
||||
var req dto.PigBatchUpdateDTO
|
||||
|
||||
handleAPIRequestWithResponse(
|
||||
return handleAPIRequestWithResponse(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
return c.service.UpdatePigBatch(primaryID, req)
|
||||
},
|
||||
"更新成功",
|
||||
@@ -105,12 +105,12 @@ func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) {
|
||||
// @Param id path int true "猪批次ID"
|
||||
// @Success 200 {object} controller.Response "删除成功"
|
||||
// @Router /api/v1/pig-batches/{id} [delete]
|
||||
func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) {
|
||||
func (c *PigBatchController) DeletePigBatch(ctx echo.Context) error {
|
||||
const action = "删除猪批次"
|
||||
|
||||
handleNoBodyAPIRequest(
|
||||
return handleNoBodyAPIRequest(
|
||||
c, ctx, action,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint) error {
|
||||
return c.service.DeletePigBatch(primaryID)
|
||||
},
|
||||
"删除成功",
|
||||
@@ -127,13 +127,13 @@ func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) {
|
||||
// @Param is_active query bool false "是否活跃 (true/false)"
|
||||
// @Success 200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功"
|
||||
// @Router /api/v1/pig-batches [get]
|
||||
func (c *PigBatchController) ListPigBatches(ctx *gin.Context) {
|
||||
func (c *PigBatchController) ListPigBatches(ctx echo.Context) error {
|
||||
const action = "获取猪批次列表"
|
||||
var query dto.PigBatchQueryDTO
|
||||
|
||||
handleQueryAPIRequestWithResponse(
|
||||
return handleQueryAPIRequestWithResponse(
|
||||
c, ctx, action, &query,
|
||||
func(ctx *gin.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
|
||||
func(ctx echo.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
|
||||
return c.service.ListPigBatches(query.IsActive)
|
||||
},
|
||||
"获取成功",
|
||||
@@ -151,13 +151,13 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) {
|
||||
// @Param body body dto.AssignEmptyPensToBatchRequest true "待分配的猪栏ID列表"
|
||||
// @Success 200 {object} controller.Response "分配成功"
|
||||
// @Router /api/v1/pig-batches/assign-pens/{id} [post]
|
||||
func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) {
|
||||
func (c *PigBatchController) AssignEmptyPensToBatch(ctx echo.Context) error {
|
||||
const action = "为猪批次分配空栏"
|
||||
var req dto.AssignEmptyPensToBatchRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error {
|
||||
return c.service.AssignEmptyPensToBatch(primaryID, req.PenIDs, operatorID)
|
||||
},
|
||||
"分配成功",
|
||||
@@ -176,18 +176,18 @@ func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) {
|
||||
// @Param body body dto.ReclassifyPenToNewBatchRequest true "划拨请求信息 (包含目标批次ID、猪栏ID和备注)"
|
||||
// @Success 200 {object} controller.Response "划拨成功"
|
||||
// @Router /api/v1/pig-batches/reclassify-pen/{fromBatchID} [post]
|
||||
func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) {
|
||||
func (c *PigBatchController) ReclassifyPenToNewBatch(ctx echo.Context) error {
|
||||
const action = "划拨猪栏到新批次"
|
||||
var req dto.ReclassifyPenToNewBatchRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error {
|
||||
// primaryID 在这里是 fromBatchID
|
||||
return c.service.ReclassifyPenToNewBatch(primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks)
|
||||
},
|
||||
"划拨成功",
|
||||
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":fromBatchID" 路径参数提取
|
||||
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":fromBatchID" 路径参数提取
|
||||
idParam := ctx.Param("fromBatchID")
|
||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||
if err != nil {
|
||||
@@ -208,22 +208,22 @@ func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) {
|
||||
// @Param penID path int true "待移除的猪栏ID"
|
||||
// @Success 200 {object} controller.Response "移除成功"
|
||||
// @Router /api/v1/pig-batches/remove-pen/{penID}/{batchID} [delete]
|
||||
func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) {
|
||||
func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx echo.Context) error {
|
||||
const action = "从猪批次移除空栏"
|
||||
|
||||
handleNoBodyAPIRequest(
|
||||
return handleNoBodyAPIRequest(
|
||||
c, ctx, action,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint) error {
|
||||
// primaryID 在这里是 batchID
|
||||
penIDParam := ctx.Param("penID")
|
||||
penID, err := strconv.ParseUint(penIDParam, 10, 32)
|
||||
parsedPenID, err := strconv.ParseUint(penIDParam, 10, 32)
|
||||
if err != nil {
|
||||
return err // 返回错误,因为 penID 格式无效
|
||||
}
|
||||
return c.service.RemoveEmptyPenFromBatch(primaryID, uint(penID))
|
||||
return c.service.RemoveEmptyPenFromBatch(primaryID, uint(parsedPenID))
|
||||
},
|
||||
"移除成功",
|
||||
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":batchID" 路径参数提取
|
||||
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":batchID" 路径参数提取
|
||||
idParam := ctx.Param("batchID")
|
||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||
if err != nil {
|
||||
@@ -245,13 +245,13 @@ func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) {
|
||||
// @Param body body dto.MovePigsIntoPenRequest true "移入猪只请求信息 (包含目标猪栏ID、数量和备注)"
|
||||
// @Success 200 {object} controller.Response "移入成功"
|
||||
// @Router /api/v1/pig-batches/move-pigs-into-pen/{id} [post]
|
||||
func (c *PigBatchController) MovePigsIntoPen(ctx *gin.Context) {
|
||||
func (c *PigBatchController) MovePigsIntoPen(ctx echo.Context) error {
|
||||
const action = "将猪只移入猪栏"
|
||||
var req dto.MovePigsIntoPenRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error {
|
||||
return c.service.MovePigsIntoPen(primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||
},
|
||||
"移入成功",
|
||||
|
||||
@@ -2,7 +2,7 @@ package management
|
||||
|
||||
import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// RecordSickPigs godoc
|
||||
@@ -16,13 +16,13 @@ import (
|
||||
// @Param body body dto.RecordSickPigsRequest true "记录病猪请求信息"
|
||||
// @Success 200 {object} controller.Response "记录成功"
|
||||
// @Router /api/v1/pig-batches/record-sick-pigs/{id} [post]
|
||||
func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) {
|
||||
func (c *PigBatchController) RecordSickPigs(ctx echo.Context) error {
|
||||
const action = "记录新增病猪事件"
|
||||
var req dto.RecordSickPigsRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
|
||||
return c.service.RecordSickPigs(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -41,13 +41,13 @@ func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) {
|
||||
// @Param body body dto.RecordSickPigRecoveryRequest true "记录病猪康复请求信息"
|
||||
// @Success 200 {object} controller.Response "记录成功"
|
||||
// @Router /api/v1/pig-batches/record-sick-pig-recovery/{id} [post]
|
||||
func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) {
|
||||
func (c *PigBatchController) RecordSickPigRecovery(ctx echo.Context) error {
|
||||
const action = "记录病猪康复事件"
|
||||
var req dto.RecordSickPigRecoveryRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
|
||||
return c.service.RecordSickPigRecovery(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -66,13 +66,13 @@ func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) {
|
||||
// @Param body body dto.RecordSickPigDeathRequest true "记录病猪死亡请求信息"
|
||||
// @Success 200 {object} controller.Response "记录成功"
|
||||
// @Router /api/v1/pig-batches/record-sick-pig-death/{id} [post]
|
||||
func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) {
|
||||
func (c *PigBatchController) RecordSickPigDeath(ctx echo.Context) error {
|
||||
const action = "记录病猪死亡事件"
|
||||
var req dto.RecordSickPigDeathRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
|
||||
return c.service.RecordSickPigDeath(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -91,13 +91,13 @@ func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) {
|
||||
// @Param body body dto.RecordSickPigCullRequest true "记录病猪淘汰请求信息"
|
||||
// @Success 200 {object} controller.Response "记录成功"
|
||||
// @Router /api/v1/pig-batches/record-sick-pig-cull/{id} [post]
|
||||
func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) {
|
||||
func (c *PigBatchController) RecordSickPigCull(ctx echo.Context) error {
|
||||
const action = "记录病猪淘汰事件"
|
||||
var req dto.RecordSickPigCullRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
|
||||
return c.service.RecordSickPigCull(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -116,13 +116,13 @@ func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) {
|
||||
// @Param body body dto.RecordDeathRequest true "记录正常猪只死亡请求信息"
|
||||
// @Success 200 {object} controller.Response "记录成功"
|
||||
// @Router /api/v1/pig-batches/record-death/{id} [post]
|
||||
func (c *PigBatchController) RecordDeath(ctx *gin.Context) {
|
||||
func (c *PigBatchController) RecordDeath(ctx echo.Context) error {
|
||||
const action = "记录正常猪只死亡事件"
|
||||
var req dto.RecordDeathRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
|
||||
return c.service.RecordDeath(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -141,13 +141,13 @@ func (c *PigBatchController) RecordDeath(ctx *gin.Context) {
|
||||
// @Param body body dto.RecordCullRequest true "记录正常猪只淘汰请求信息"
|
||||
// @Success 200 {object} controller.Response "记录成功"
|
||||
// @Router /api/v1/pig-batches/record-cull/{id} [post]
|
||||
func (c *PigBatchController) RecordCull(ctx *gin.Context) {
|
||||
func (c *PigBatchController) RecordCull(ctx echo.Context) error {
|
||||
const action = "记录正常猪只淘汰事件"
|
||||
var req dto.RecordCullRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
|
||||
return c.service.RecordCull(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
|
||||
@@ -2,7 +2,7 @@ package management
|
||||
|
||||
import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// SellPigs godoc
|
||||
@@ -16,13 +16,13 @@ import (
|
||||
// @Param body body dto.SellPigsRequest true "卖猪请求信息"
|
||||
// @Success 200 {object} controller.Response "卖猪成功"
|
||||
// @Router /api/v1/pig-batches/sell-pigs/{id} [post]
|
||||
func (c *PigBatchController) SellPigs(ctx *gin.Context) {
|
||||
func (c *PigBatchController) SellPigs(ctx echo.Context) error {
|
||||
const action = "卖猪"
|
||||
var req dto.SellPigsRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
|
||||
return c.service.SellPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
|
||||
},
|
||||
"卖猪成功",
|
||||
@@ -41,13 +41,13 @@ func (c *PigBatchController) SellPigs(ctx *gin.Context) {
|
||||
// @Param body body dto.BuyPigsRequest true "买猪请求信息"
|
||||
// @Success 200 {object} controller.Response "买猪成功"
|
||||
// @Router /api/v1/pig-batches/buy-pigs/{id} [post]
|
||||
func (c *PigBatchController) BuyPigs(ctx *gin.Context) {
|
||||
func (c *PigBatchController) BuyPigs(ctx echo.Context) error {
|
||||
const action = "买猪"
|
||||
var req dto.BuyPigsRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
|
||||
return c.service.BuyPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
|
||||
},
|
||||
"买猪成功",
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// TransferPigsAcrossBatches godoc
|
||||
@@ -18,18 +18,18 @@ import (
|
||||
// @Param body body dto.TransferPigsAcrossBatchesRequest true "跨群调栏请求信息"
|
||||
// @Success 200 {object} controller.Response "调栏成功"
|
||||
// @Router /api/v1/pig-batches/transfer-across-batches/{sourceBatchID} [post]
|
||||
func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) {
|
||||
func (c *PigBatchController) TransferPigsAcrossBatches(ctx echo.Context) error {
|
||||
const action = "跨猪群调栏"
|
||||
var req dto.TransferPigsAcrossBatchesRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
|
||||
// primaryID 在这里是 sourceBatchID
|
||||
return c.service.TransferPigsAcrossBatches(primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||
},
|
||||
"调栏成功",
|
||||
func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":sourceBatchID" 路径参数提取
|
||||
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":sourceBatchID" 路径参数提取
|
||||
idParam := ctx.Param("sourceBatchID")
|
||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||
if err != nil {
|
||||
@@ -51,13 +51,13 @@ func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) {
|
||||
// @Param body body dto.TransferPigsWithinBatchRequest true "群内调栏请求信息"
|
||||
// @Success 200 {object} controller.Response "调栏成功"
|
||||
// @Router /api/v1/pig-batches/transfer-within-batch/{id} [post]
|
||||
func (c *PigBatchController) TransferPigsWithinBatch(ctx *gin.Context) {
|
||||
func (c *PigBatchController) TransferPigsWithinBatch(ctx echo.Context) error {
|
||||
const action = "群内调栏"
|
||||
var req dto.TransferPigsWithinBatchRequest
|
||||
|
||||
handleAPIRequest(
|
||||
return handleAPIRequest(
|
||||
c, ctx, action, &req,
|
||||
func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
|
||||
// primaryID 在这里是 batchID
|
||||
return c.service.TransferPigsWithinBatch(primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// --- 控制器定义 ---
|
||||
@@ -31,7 +31,7 @@ func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *
|
||||
|
||||
// CreatePigHouse godoc
|
||||
// @Summary 创建猪舍
|
||||
// @Description 创建一个新的猪舍
|
||||
// @Description 根据提供的信息创建一个新猪舍
|
||||
// @Tags 猪场管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
@@ -39,27 +39,21 @@ func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *
|
||||
// @Param body body dto.CreatePigHouseRequest true "猪舍信息"
|
||||
// @Success 201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功"
|
||||
// @Router /api/v1/pig-houses [post]
|
||||
func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
|
||||
func (c *PigFarmController) CreatePigHouse(ctx echo.Context) error {
|
||||
const action = "创建猪舍"
|
||||
var req dto.CreatePigHouseRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
return
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", action, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
house, err := c.service.CreatePigHouse(req.Name, req.Description)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req)
|
||||
}
|
||||
|
||||
resp := dto.PigHouseResponse{
|
||||
ID: house.ID,
|
||||
Name: house.Name,
|
||||
Description: house.Description,
|
||||
}
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", house, action, "创建成功", house)
|
||||
}
|
||||
|
||||
// GetPigHouse godoc
|
||||
@@ -71,31 +65,23 @@ func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) {
|
||||
// @Param id path int true "猪舍ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功"
|
||||
// @Router /api/v1/pig-houses/{id} [get]
|
||||
func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
|
||||
func (c *PigFarmController) GetPigHouse(ctx echo.Context) error {
|
||||
const action = "获取猪舍"
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
house, err := c.service.GetPigHouseByID(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrHouseNotFound) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
}
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id)
|
||||
}
|
||||
|
||||
resp := dto.PigHouseResponse{
|
||||
ID: house.ID,
|
||||
Name: house.Name,
|
||||
Description: house.Description,
|
||||
}
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", house, action, "获取成功", house)
|
||||
}
|
||||
|
||||
// ListPigHouses godoc
|
||||
@@ -106,25 +92,15 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功"
|
||||
// @Router /api/v1/pig-houses [get]
|
||||
func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
|
||||
func (c *PigFarmController) ListPigHouses(ctx echo.Context) error {
|
||||
const action = "获取猪舍列表"
|
||||
houses, err := c.service.ListPigHouses()
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
|
||||
}
|
||||
|
||||
var resp []dto.PigHouseResponse
|
||||
for _, house := range houses {
|
||||
resp = append(resp, dto.PigHouseResponse{
|
||||
ID: house.ID,
|
||||
Name: house.Name,
|
||||
Description: house.Description,
|
||||
})
|
||||
}
|
||||
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", houses, action, "获取成功", houses)
|
||||
}
|
||||
|
||||
// UpdatePigHouse godoc
|
||||
@@ -138,37 +114,28 @@ func (c *PigFarmController) ListPigHouses(ctx *gin.Context) {
|
||||
// @Param body body dto.UpdatePigHouseRequest true "猪舍信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功"
|
||||
// @Router /api/v1/pig-houses/{id} [put]
|
||||
func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
|
||||
func (c *PigFarmController) UpdatePigHouse(ctx echo.Context) error {
|
||||
const action = "更新猪舍"
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
var req dto.UpdatePigHouseRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
return
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
house, err := c.service.UpdatePigHouse(uint(id), req.Name, req.Description)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrHouseNotFound) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
}
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
|
||||
}
|
||||
|
||||
resp := dto.PigHouseResponse{
|
||||
ID: house.ID,
|
||||
Name: house.Name,
|
||||
Description: house.Description,
|
||||
}
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", house, action, "更新成功", house)
|
||||
}
|
||||
|
||||
// DeletePigHouse godoc
|
||||
@@ -180,30 +147,26 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) {
|
||||
// @Param id path int true "猪舍ID"
|
||||
// @Success 200 {object} controller.Response "删除成功"
|
||||
// @Router /api/v1/pig-houses/{id} [delete]
|
||||
func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) {
|
||||
func (c *PigFarmController) DeletePigHouse(ctx echo.Context) error {
|
||||
const action = "删除猪舍"
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
if err := c.service.DeletePigHouse(uint(id)); err != nil {
|
||||
if errors.Is(err, service.ErrHouseNotFound) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
}
|
||||
// 检查是否是业务逻辑错误
|
||||
if errors.Is(err, service.ErrHouseContainsPens) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||
}
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
|
||||
}
|
||||
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
|
||||
}
|
||||
|
||||
// --- 猪栏 (Pen) API 实现 ---
|
||||
@@ -218,34 +181,24 @@ func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) {
|
||||
// @Param body body dto.CreatePenRequest true "猪栏信息"
|
||||
// @Success 201 {object} controller.Response{data=dto.PenResponse} "创建成功"
|
||||
// @Router /api/v1/pens [post]
|
||||
func (c *PigFarmController) CreatePen(ctx *gin.Context) {
|
||||
func (c *PigFarmController) CreatePen(ctx echo.Context) error {
|
||||
const action = "创建猪栏"
|
||||
var req dto.CreatePenRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
return
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity)
|
||||
if err != nil {
|
||||
// 检查是否是业务逻辑错误
|
||||
if errors.Is(err, service.ErrHouseNotFound) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), req)
|
||||
}
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req)
|
||||
}
|
||||
|
||||
resp := dto.PenResponse{
|
||||
ID: pen.ID,
|
||||
PenNumber: pen.PenNumber,
|
||||
HouseID: pen.HouseID,
|
||||
Capacity: pen.Capacity,
|
||||
Status: pen.Status,
|
||||
}
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", pen, action, "创建成功", pen)
|
||||
}
|
||||
|
||||
// GetPen godoc
|
||||
@@ -257,26 +210,23 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) {
|
||||
// @Param id path int true "猪栏ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.PenResponse} "获取成功"
|
||||
// @Router /api/v1/pens/{id} [get]
|
||||
func (c *PigFarmController) GetPen(ctx *gin.Context) {
|
||||
func (c *PigFarmController) GetPen(ctx echo.Context) error {
|
||||
const action = "获取猪栏"
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
pen, err := c.service.GetPenByID(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrPenNotFound) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
}
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id)
|
||||
}
|
||||
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", pen, action, "获取成功", pen)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", pen, action, "获取成功", pen)
|
||||
}
|
||||
|
||||
// ListPens godoc
|
||||
@@ -287,16 +237,15 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} controller.Response{data=[]dto.PenResponse} "获取成功"
|
||||
// @Router /api/v1/pens [get]
|
||||
func (c *PigFarmController) ListPens(ctx *gin.Context) {
|
||||
func (c *PigFarmController) ListPens(ctx echo.Context) error {
|
||||
const action = "获取猪栏列表"
|
||||
pens, err := c.service.ListPens()
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
|
||||
}
|
||||
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", pens, action, "获取成功", pens)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", pens, action, "获取成功", pens)
|
||||
}
|
||||
|
||||
// UpdatePen godoc
|
||||
@@ -310,41 +259,29 @@ func (c *PigFarmController) ListPens(ctx *gin.Context) {
|
||||
// @Param body body dto.UpdatePenRequest true "猪栏信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
|
||||
// @Router /api/v1/pens/{id} [put]
|
||||
func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
|
||||
func (c *PigFarmController) UpdatePen(ctx echo.Context) error {
|
||||
const action = "更新猪栏"
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
var req dto.UpdatePenRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
return
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
pen, err := c.service.UpdatePen(uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrPenNotFound) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
}
|
||||
// 其他业务逻辑错误可以在这里添加处理
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
|
||||
}
|
||||
|
||||
resp := dto.PenResponse{
|
||||
ID: pen.ID,
|
||||
PenNumber: pen.PenNumber,
|
||||
HouseID: pen.HouseID,
|
||||
Capacity: pen.Capacity,
|
||||
Status: pen.Status,
|
||||
PigBatchID: pen.PigBatchID,
|
||||
}
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", pen, action, "更新成功", pen)
|
||||
}
|
||||
|
||||
// DeletePen godoc
|
||||
@@ -356,30 +293,26 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) {
|
||||
// @Param id path int true "猪栏ID"
|
||||
// @Success 200 {object} controller.Response "删除成功"
|
||||
// @Router /api/v1/pens/{id} [delete]
|
||||
func (c *PigFarmController) DeletePen(ctx *gin.Context) {
|
||||
func (c *PigFarmController) DeletePen(ctx echo.Context) error {
|
||||
const action = "删除猪栏"
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
if err := c.service.DeletePen(uint(id)); err != nil {
|
||||
if errors.Is(err, service.ErrPenNotFound) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
}
|
||||
// 检查是否是业务逻辑错误
|
||||
if errors.Is(err, service.ErrPenInUse) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||
}
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
|
||||
}
|
||||
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id)
|
||||
}
|
||||
|
||||
// UpdatePenStatus godoc
|
||||
@@ -393,41 +326,28 @@ func (c *PigFarmController) DeletePen(ctx *gin.Context) {
|
||||
// @Param body body dto.UpdatePenStatusRequest true "新的猪栏状态"
|
||||
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
|
||||
// @Router /api/v1/pens/{id}/status [put]
|
||||
func (c *PigFarmController) UpdatePenStatus(ctx *gin.Context) {
|
||||
func (c *PigFarmController) UpdatePenStatus(ctx echo.Context) error {
|
||||
const action = "更新猪栏状态"
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
var req dto.UpdatePenStatusRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
return
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
pen, err := c.service.UpdatePenStatus(uint(id), req.Status)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrPenNotFound) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
||||
} else if errors.Is(err, service.ErrPenStatusInvalidForOccupiedPen) || errors.Is(err, service.ErrPenStatusInvalidForUnoccupiedPen) {
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
|
||||
}
|
||||
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id)
|
||||
}
|
||||
|
||||
resp := dto.PenResponse{
|
||||
ID: pen.ID,
|
||||
PenNumber: pen.PenNumber,
|
||||
HouseID: pen.HouseID,
|
||||
Capacity: pen.Capacity,
|
||||
Status: pen.Status,
|
||||
PigBatchID: pen.PigBatchID,
|
||||
}
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", pen, action, "更新成功", pen)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"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"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Controller 监控控制器,封装了所有与数据监控相关的业务逻辑
|
||||
@@ -35,43 +34,28 @@ func NewController(monitorService service.MonitorService, logger *logs.Logger) *
|
||||
// @Param query query dto.ListSensorDataRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListSensorDataResponse}
|
||||
// @Router /api/v1/monitor/sensor-data [get]
|
||||
func (c *Controller) ListSensorData(ctx *gin.Context) {
|
||||
func (c *Controller) ListSensorData(ctx echo.Context) error {
|
||||
const actionType = "获取传感器数据列表"
|
||||
|
||||
var req dto.ListSensorDataRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.SensorDataListOptions{
|
||||
DeviceID: req.DeviceID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.SensorType != nil {
|
||||
sensorType := models.SensorType(*req.SensorType)
|
||||
opts.SensorType = &sensorType
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListSensorData(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListSensorData(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取传感器数据失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取传感器数据失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListSensorDataResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取传感器数据成功", resp, actionType, "获取传感器数据成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取传感器数据成功", resp, actionType, "获取传感器数据成功", req)
|
||||
}
|
||||
|
||||
// ListDeviceCommandLogs godoc
|
||||
@@ -83,40 +67,28 @@ func (c *Controller) ListSensorData(ctx *gin.Context) {
|
||||
// @Param query query dto.ListDeviceCommandLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListDeviceCommandLogResponse}
|
||||
// @Router /api/v1/monitor/device-command-logs [get]
|
||||
func (c *Controller) ListDeviceCommandLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListDeviceCommandLogs(ctx echo.Context) error {
|
||||
const actionType = "获取设备命令日志列表"
|
||||
|
||||
var req dto.ListDeviceCommandLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.DeviceCommandLogListOptions{
|
||||
DeviceID: req.DeviceID,
|
||||
ReceivedSuccess: req.ReceivedSuccess,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListDeviceCommandLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListDeviceCommandLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备命令日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备命令日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListDeviceCommandLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备命令日志成功", resp, actionType, "获取设备命令日志成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备命令日志成功", resp, actionType, "获取设备命令日志成功", req)
|
||||
}
|
||||
|
||||
// ListPlanExecutionLogs godoc
|
||||
@@ -128,43 +100,28 @@ func (c *Controller) ListDeviceCommandLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListPlanExecutionLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListPlanExecutionLogResponse}
|
||||
// @Router /api/v1/monitor/plan-execution-logs [get]
|
||||
func (c *Controller) ListPlanExecutionLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListPlanExecutionLogs(ctx echo.Context) error {
|
||||
const actionType = "获取计划执行日志列表"
|
||||
|
||||
var req dto.ListPlanExecutionLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.PlanExecutionLogListOptions{
|
||||
PlanID: req.PlanID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.ExecutionStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListPlanExecutionLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListPlanExecutionLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListPlanExecutionLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划执行日志成功", resp, actionType, "获取计划执行日志成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划执行日志成功", resp, actionType, "获取计划执行日志成功", req)
|
||||
}
|
||||
|
||||
// ListTaskExecutionLogs godoc
|
||||
@@ -176,44 +133,28 @@ func (c *Controller) ListPlanExecutionLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListTaskExecutionLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListTaskExecutionLogResponse}
|
||||
// @Router /api/v1/monitor/task-execution-logs [get]
|
||||
func (c *Controller) ListTaskExecutionLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListTaskExecutionLogs(ctx echo.Context) error {
|
||||
const actionType = "获取任务执行日志列表"
|
||||
|
||||
var req dto.ListTaskExecutionLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.TaskExecutionLogListOptions{
|
||||
PlanExecutionLogID: req.PlanExecutionLogID,
|
||||
TaskID: req.TaskID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.ExecutionStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListTaskExecutionLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListTaskExecutionLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取任务执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取任务执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListTaskExecutionLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取任务执行日志成功", resp, actionType, "获取任务执行日志成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取任务执行日志成功", resp, actionType, "获取任务执行日志成功", req)
|
||||
}
|
||||
|
||||
// ListPendingCollections godoc
|
||||
@@ -225,43 +166,28 @@ func (c *Controller) ListTaskExecutionLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListPendingCollectionRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListPendingCollectionResponse}
|
||||
// @Router /api/v1/monitor/pending-collections [get]
|
||||
func (c *Controller) ListPendingCollections(ctx *gin.Context) {
|
||||
func (c *Controller) ListPendingCollections(ctx echo.Context) error {
|
||||
const actionType = "获取待采集请求列表"
|
||||
|
||||
var req dto.ListPendingCollectionRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.PendingCollectionListOptions{
|
||||
DeviceID: req.DeviceID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.PendingCollectionStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListPendingCollections(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListPendingCollections(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取待采集请求失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取待采集请求失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListPendingCollectionResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取待采集请求成功", resp, actionType, "获取待采集请求成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取待采集请求成功", resp, actionType, "获取待采集请求成功", req)
|
||||
}
|
||||
|
||||
// ListUserActionLogs godoc
|
||||
@@ -273,45 +199,28 @@ func (c *Controller) ListPendingCollections(ctx *gin.Context) {
|
||||
// @Param query query dto.ListUserActionLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListUserActionLogResponse}
|
||||
// @Router /api/v1/monitor/user-action-logs [get]
|
||||
func (c *Controller) ListUserActionLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListUserActionLogs(ctx echo.Context) error {
|
||||
const actionType = "获取用户操作日志列表"
|
||||
|
||||
var req dto.ListUserActionLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.UserActionLogListOptions{
|
||||
UserID: req.UserID,
|
||||
Username: req.Username,
|
||||
ActionType: req.ActionType,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.AuditStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListUserActionLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListUserActionLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户操作日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户操作日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作日志成功", resp, actionType, "获取用户操作日志成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作日志成功", resp, actionType, "获取用户操作日志成功", req)
|
||||
}
|
||||
|
||||
// ListRawMaterialPurchases godoc
|
||||
@@ -323,40 +232,28 @@ func (c *Controller) ListUserActionLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListRawMaterialPurchaseRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialPurchaseResponse}
|
||||
// @Router /api/v1/monitor/raw-material-purchases [get]
|
||||
func (c *Controller) ListRawMaterialPurchases(ctx *gin.Context) {
|
||||
func (c *Controller) ListRawMaterialPurchases(ctx echo.Context) error {
|
||||
const actionType = "获取原料采购记录列表"
|
||||
|
||||
var req dto.ListRawMaterialPurchaseRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.RawMaterialPurchaseListOptions{
|
||||
RawMaterialID: req.RawMaterialID,
|
||||
Supplier: req.Supplier,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListRawMaterialPurchases(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListRawMaterialPurchases(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListRawMaterialPurchaseResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料采购记录成功", resp, actionType, "获取原料采购记录成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料采购记录成功", resp, actionType, "获取原料采购记录成功", req)
|
||||
}
|
||||
|
||||
// ListRawMaterialStockLogs godoc
|
||||
@@ -368,44 +265,28 @@ func (c *Controller) ListRawMaterialPurchases(ctx *gin.Context) {
|
||||
// @Param query query dto.ListRawMaterialStockLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialStockLogResponse}
|
||||
// @Router /api/v1/monitor/raw-material-stock-logs [get]
|
||||
func (c *Controller) ListRawMaterialStockLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListRawMaterialStockLogs(ctx echo.Context) error {
|
||||
const actionType = "获取原料库存日志列表"
|
||||
|
||||
var req dto.ListRawMaterialStockLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.RawMaterialStockLogListOptions{
|
||||
RawMaterialID: req.RawMaterialID,
|
||||
SourceID: req.SourceID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.SourceType != nil {
|
||||
sourceType := models.StockLogSourceType(*req.SourceType)
|
||||
opts.SourceType = &sourceType
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListRawMaterialStockLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListRawMaterialStockLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料库存日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料库存日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListRawMaterialStockLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料库存日志成功", resp, actionType, "获取原料库存日志成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料库存日志成功", resp, actionType, "获取原料库存日志成功", req)
|
||||
}
|
||||
|
||||
// ListFeedUsageRecords godoc
|
||||
@@ -417,41 +298,28 @@ func (c *Controller) ListRawMaterialStockLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListFeedUsageRecordRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListFeedUsageRecordResponse}
|
||||
// @Router /api/v1/monitor/feed-usage-records [get]
|
||||
func (c *Controller) ListFeedUsageRecords(ctx *gin.Context) {
|
||||
func (c *Controller) ListFeedUsageRecords(ctx echo.Context) error {
|
||||
const actionType = "获取饲料使用记录列表"
|
||||
|
||||
var req dto.ListFeedUsageRecordRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.FeedUsageRecordListOptions{
|
||||
PenID: req.PenID,
|
||||
FeedFormulaID: req.FeedFormulaID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListFeedUsageRecords(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListFeedUsageRecords(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取饲料使用记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取饲料使用记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListFeedUsageRecordResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取饲料使用记录成功", resp, actionType, "获取饲料使用记录成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取饲料使用记录成功", resp, actionType, "获取饲料使用记录成功", req)
|
||||
}
|
||||
|
||||
// ListMedicationLogs godoc
|
||||
@@ -463,45 +331,28 @@ func (c *Controller) ListFeedUsageRecords(ctx *gin.Context) {
|
||||
// @Param query query dto.ListMedicationLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListMedicationLogResponse}
|
||||
// @Router /api/v1/monitor/medication-logs [get]
|
||||
func (c *Controller) ListMedicationLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListMedicationLogs(ctx echo.Context) error {
|
||||
const actionType = "获取用药记录列表"
|
||||
|
||||
var req dto.ListMedicationLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.MedicationLogListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
MedicationID: req.MedicationID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Reason != nil {
|
||||
reason := models.MedicationReasonType(*req.Reason)
|
||||
opts.Reason = &reason
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListMedicationLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListMedicationLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用药记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用药记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListMedicationLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用药记录成功", resp, actionType, "获取用药记录成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用药记录成功", resp, actionType, "获取用药记录成功", req)
|
||||
}
|
||||
|
||||
// ListPigBatchLogs godoc
|
||||
@@ -513,44 +364,28 @@ func (c *Controller) ListMedicationLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListPigBatchLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListPigBatchLogResponse}
|
||||
// @Router /api/v1/monitor/pig-batch-logs [get]
|
||||
func (c *Controller) ListPigBatchLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListPigBatchLogs(ctx echo.Context) error {
|
||||
const actionType = "获取猪批次日志列表"
|
||||
|
||||
var req dto.ListPigBatchLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.PigBatchLogListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.ChangeType != nil {
|
||||
changeType := models.LogChangeType(*req.ChangeType)
|
||||
opts.ChangeType = &changeType
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListPigBatchLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListPigBatchLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListPigBatchLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪批次日志成功", resp, actionType, "获取猪批次日志成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪批次日志成功", resp, actionType, "获取猪批次日志成功", req)
|
||||
}
|
||||
|
||||
// ListWeighingBatches godoc
|
||||
@@ -562,39 +397,28 @@ func (c *Controller) ListPigBatchLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListWeighingBatchRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListWeighingBatchResponse}
|
||||
// @Router /api/v1/monitor/weighing-batches [get]
|
||||
func (c *Controller) ListWeighingBatches(ctx *gin.Context) {
|
||||
func (c *Controller) ListWeighingBatches(ctx echo.Context) error {
|
||||
const actionType = "获取批次称重记录列表"
|
||||
|
||||
var req dto.ListWeighingBatchRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.WeighingBatchListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListWeighingBatches(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListWeighingBatches(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取批次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取批次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListWeighingBatchResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取批次称重记录成功", resp, actionType, "获取批次称重记录成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取批次称重记录成功", resp, actionType, "获取批次称重记录成功", req)
|
||||
}
|
||||
|
||||
// ListWeighingRecords godoc
|
||||
@@ -606,41 +430,28 @@ func (c *Controller) ListWeighingBatches(ctx *gin.Context) {
|
||||
// @Param query query dto.ListWeighingRecordRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListWeighingRecordResponse}
|
||||
// @Router /api/v1/monitor/weighing-records [get]
|
||||
func (c *Controller) ListWeighingRecords(ctx *gin.Context) {
|
||||
func (c *Controller) ListWeighingRecords(ctx echo.Context) error {
|
||||
const actionType = "获取单次称重记录列表"
|
||||
|
||||
var req dto.ListWeighingRecordRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.WeighingRecordListOptions{
|
||||
WeighingBatchID: req.WeighingBatchID,
|
||||
PenID: req.PenID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListWeighingRecords(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListWeighingRecords(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取单次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取单次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListWeighingRecordResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取单次称重记录成功", resp, actionType, "获取单次称重记录成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取单次称重记录成功", resp, actionType, "获取单次称重记录成功", req)
|
||||
}
|
||||
|
||||
// ListPigTransferLogs godoc
|
||||
@@ -652,46 +463,28 @@ func (c *Controller) ListWeighingRecords(ctx *gin.Context) {
|
||||
// @Param query query dto.ListPigTransferLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListPigTransferLogResponse}
|
||||
// @Router /api/v1/monitor/pig-transfer-logs [get]
|
||||
func (c *Controller) ListPigTransferLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListPigTransferLogs(ctx echo.Context) error {
|
||||
const actionType = "获取猪只迁移日志列表"
|
||||
|
||||
var req dto.ListPigTransferLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.PigTransferLogListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
PenID: req.PenID,
|
||||
OperatorID: req.OperatorID,
|
||||
CorrelationID: req.CorrelationID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.TransferType != nil {
|
||||
transferType := models.PigTransferType(*req.TransferType)
|
||||
opts.TransferType = &transferType
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListPigTransferLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListPigTransferLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只迁移日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只迁移日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListPigTransferLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只迁移日志成功", resp, actionType, "获取猪只迁移日志成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只迁移日志成功", resp, actionType, "获取猪只迁移日志成功", req)
|
||||
}
|
||||
|
||||
// ListPigSickLogs godoc
|
||||
@@ -703,49 +496,28 @@ func (c *Controller) ListPigTransferLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListPigSickLogRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListPigSickLogResponse}
|
||||
// @Router /api/v1/monitor/pig-sick-logs [get]
|
||||
func (c *Controller) ListPigSickLogs(ctx *gin.Context) {
|
||||
func (c *Controller) ListPigSickLogs(ctx echo.Context) error {
|
||||
const actionType = "获取病猪日志列表"
|
||||
|
||||
var req dto.ListPigSickLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.PigSickLogListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
PenID: req.PenID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Reason != nil {
|
||||
reason := models.PigBatchSickPigReasonType(*req.Reason)
|
||||
opts.Reason = &reason
|
||||
}
|
||||
if req.TreatmentLocation != nil {
|
||||
treatmentLocation := models.PigBatchSickPigTreatmentLocation(*req.TreatmentLocation)
|
||||
opts.TreatmentLocation = &treatmentLocation
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListPigSickLogs(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListPigSickLogs(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取病猪日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取病猪日志失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListPigSickLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取病猪日志成功", resp, actionType, "获取病猪日志成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取病猪日志成功", resp, actionType, "获取病猪日志成功", req)
|
||||
}
|
||||
|
||||
// ListPigPurchases godoc
|
||||
@@ -757,41 +529,28 @@ func (c *Controller) ListPigSickLogs(ctx *gin.Context) {
|
||||
// @Param query query dto.ListPigPurchaseRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListPigPurchaseResponse}
|
||||
// @Router /api/v1/monitor/pig-purchases [get]
|
||||
func (c *Controller) ListPigPurchases(ctx *gin.Context) {
|
||||
func (c *Controller) ListPigPurchases(ctx echo.Context) error {
|
||||
const actionType = "获取猪只采购记录列表"
|
||||
|
||||
var req dto.ListPigPurchaseRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.PigPurchaseListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
Supplier: req.Supplier,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListPigPurchases(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListPigPurchases(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListPigPurchaseResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只采购记录成功", resp, actionType, "获取猪只采购记录成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只采购记录成功", resp, actionType, "获取猪只采购记录成功", req)
|
||||
}
|
||||
|
||||
// ListPigSales godoc
|
||||
@@ -803,41 +562,28 @@ func (c *Controller) ListPigPurchases(ctx *gin.Context) {
|
||||
// @Param query query dto.ListPigSaleRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListPigSaleResponse}
|
||||
// @Router /api/v1/monitor/pig-sales [get]
|
||||
func (c *Controller) ListPigSales(ctx *gin.Context) {
|
||||
func (c *Controller) ListPigSales(ctx echo.Context) error {
|
||||
const actionType = "获取猪只售卖记录列表"
|
||||
|
||||
var req dto.ListPigSaleRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.PigSaleListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
Buyer: req.Buyer,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListPigSales(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListPigSales(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只售卖记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只售卖记录失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListPigSaleResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只售卖记录成功", resp, actionType, "获取猪只售卖记录成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只售卖记录成功", resp, actionType, "获取猪只售卖记录成功", req)
|
||||
}
|
||||
|
||||
// ListNotifications godoc
|
||||
@@ -849,40 +595,26 @@ func (c *Controller) ListPigSales(ctx *gin.Context) {
|
||||
// @Param query query dto.ListNotificationRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListNotificationResponse}
|
||||
// @Router /api/v1/monitor/notifications [get]
|
||||
func (c *Controller) ListNotifications(ctx *gin.Context) {
|
||||
func (c *Controller) ListNotifications(ctx echo.Context) error {
|
||||
const actionType = "批量查询通知"
|
||||
|
||||
var req dto.ListNotificationRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
}
|
||||
|
||||
opts := repository.NotificationListOptions{
|
||||
UserID: req.UserID,
|
||||
NotifierType: req.NotifierType,
|
||||
Level: req.Level,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
OrderBy: req.OrderBy,
|
||||
Status: req.Status,
|
||||
}
|
||||
|
||||
data, total, err := c.monitorService.ListNotifications(opts, req.Page, req.PageSize)
|
||||
resp, err := c.monitorService.ListNotifications(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "批量查询通知失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "批量查询通知失败: "+err.Error(), actionType, "服务层查询失败", req)
|
||||
}
|
||||
|
||||
resp := dto.NewListNotificationResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "批量查询通知成功", resp, actionType, "批量查询通知成功", req)
|
||||
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "批量查询通知成功", resp, actionType, "批量查询通知成功", req)
|
||||
}
|
||||
|
||||
@@ -6,29 +6,25 @@ import (
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// --- Controller 定义 ---
|
||||
// --- 控制器定义 ---
|
||||
|
||||
// Controller 定义了计划相关的控制器
|
||||
type Controller struct {
|
||||
logger *logs.Logger
|
||||
planRepo repository.PlanRepository
|
||||
analysisPlanTaskManager *task.AnalysisPlanTaskManager
|
||||
planService service.PlanService
|
||||
}
|
||||
|
||||
// NewController 创建一个新的 Controller 实例
|
||||
func NewController(logger *logs.Logger, planRepo repository.PlanRepository, analysisPlanTaskManager *task.AnalysisPlanTaskManager) *Controller {
|
||||
func NewController(logger *logs.Logger, planService service.PlanService) *Controller {
|
||||
return &Controller{
|
||||
logger: logger,
|
||||
planRepo: planRepo,
|
||||
analysisPlanTaskManager: analysisPlanTaskManager,
|
||||
planService: planService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,55 +40,28 @@ func NewController(logger *logs.Logger, planRepo repository.PlanRepository, anal
|
||||
// @Param plan body dto.CreatePlanRequest true "计划信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为201代表创建成功"
|
||||
// @Router /api/v1/plans [post]
|
||||
func (c *Controller) CreatePlan(ctx *gin.Context) {
|
||||
func (c *Controller) CreatePlan(ctx echo.Context) error {
|
||||
var req dto.CreatePlanRequest
|
||||
const actionType = "创建计划"
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
// 使用已有的转换函数,它已经包含了验证和重排逻辑
|
||||
planToCreate, err := dto.NewPlanFromCreateRequest(&req)
|
||||
// 调用服务层创建计划
|
||||
resp, err := c.planService.CreatePlan(&req)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层创建计划失败: %v", actionType, err)
|
||||
// 根据服务层返回的错误类型,转换为相应的HTTP状态码
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划数据校验失败或关联计划不存在", req)
|
||||
}
|
||||
|
||||
// --- 自动判断 ContentType ---
|
||||
if len(req.SubPlanIDs) > 0 {
|
||||
planToCreate.ContentType = models.PlanContentTypeSubPlans
|
||||
} else {
|
||||
// 如果 SubPlanIDs 未提供,则默认为 Tasks 类型(即使 Tasks 字段也未提供)
|
||||
planToCreate.ContentType = models.PlanContentTypeTasks
|
||||
}
|
||||
|
||||
// 调用仓库方法创建计划
|
||||
if err := c.planRepo.CreatePlan(planToCreate); err != nil {
|
||||
c.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "数据库创建计划失败", planToCreate)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
|
||||
if err := c.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil {
|
||||
// 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功
|
||||
c.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err)
|
||||
}
|
||||
|
||||
// 使用已有的转换函数将创建后的模型转换为响应对象
|
||||
resp, err := dto.NewPlanToResponse(planToCreate)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "服务层创建计划失败", req)
|
||||
}
|
||||
|
||||
// 使用统一的成功响应函数
|
||||
c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "计划创建成功", resp, actionType, "计划创建成功", resp)
|
||||
c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "计划创建成功", resp, actionType, "计划创建成功", resp)
|
||||
}
|
||||
|
||||
// GetPlan godoc
|
||||
@@ -104,87 +73,62 @@ func (c *Controller) CreatePlan(ctx *gin.Context) {
|
||||
// @Param id path int true "计划ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表成功获取"
|
||||
// @Router /api/v1/plans/{id} [get]
|
||||
func (c *Controller) GetPlan(ctx *gin.Context) {
|
||||
func (c *Controller) GetPlan(ctx echo.Context) error {
|
||||
const actionType = "获取计划详情"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 2. 调用仓库层获取计划详情
|
||||
plan, err := c.planRepo.GetPlanByID(uint(id))
|
||||
// 调用服务层获取计划详情
|
||||
resp, err := c.planService.GetPlanByID(uint(id))
|
||||
if err != nil {
|
||||
// 判断是否为“未找到”错误
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
|
||||
}
|
||||
// 其他数据库错误视为内部错误
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误", actionType, "数据库查询失败", id)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 将模型转换为响应 DTO
|
||||
resp, err := dto.NewPlanToResponse(plan)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: "+err.Error(), actionType, "服务层获取计划详情失败", id)
|
||||
}
|
||||
|
||||
// 4. 发送成功响应
|
||||
c.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划详情成功", resp, actionType, "获取计划详情成功", resp)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划详情成功", resp, actionType, "获取计划详情成功", resp)
|
||||
}
|
||||
|
||||
// ListPlans godoc
|
||||
// @Summary 获取计划列表
|
||||
// @Description 获取所有计划的列表
|
||||
// @Description 获取所有计划的列表,支持按类型过滤和分页
|
||||
// @Tags 计划管理
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} controller.Response{data=[]dto.PlanResponse} "业务码为200代表成功获取列表"
|
||||
// @Param query query dto.ListPlansQuery false "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListPlansResponse} "业务码为200代表成功获取列表"
|
||||
// @Router /api/v1/plans [get]
|
||||
func (c *Controller) ListPlans(ctx *gin.Context) {
|
||||
func (c *Controller) ListPlans(ctx echo.Context) error {
|
||||
const actionType = "获取计划列表"
|
||||
// 1. 调用仓库层获取所有计划
|
||||
plans, err := c.planRepo.ListBasicPlans()
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表时发生内部错误", actionType, "数据库查询失败", nil)
|
||||
return
|
||||
var query dto.ListPlansQuery
|
||||
if err := ctx.Bind(&query); err != nil {
|
||||
c.logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", query)
|
||||
}
|
||||
|
||||
// 2. 将模型转换为响应 DTO
|
||||
planResponses := make([]dto.PlanResponse, 0, len(plans))
|
||||
for _, p := range plans {
|
||||
resp, err := dto.NewPlanToResponse(&p)
|
||||
// 调用服务层获取计划列表
|
||||
resp, err := c.planService.ListPlans(&query)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p)
|
||||
return
|
||||
}
|
||||
planResponses = append(planResponses, *resp)
|
||||
c.logger.Errorf("%s: 服务层获取计划列表失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: "+err.Error(), actionType, "服务层获取计划列表失败", nil)
|
||||
}
|
||||
|
||||
// 3. 构造并发送成功响应
|
||||
resp := dto.ListPlansResponse{
|
||||
Plans: planResponses,
|
||||
Total: len(planResponses),
|
||||
}
|
||||
c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses))
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划列表成功", resp, actionType, "获取计划列表成功", resp)
|
||||
c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(resp.Plans))
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划列表成功", resp, actionType, "获取计划列表成功", resp)
|
||||
}
|
||||
|
||||
// UpdatePlan godoc
|
||||
// @Summary 更新计划
|
||||
// @Description 根据计划ID更新计划的详细信息。
|
||||
// @Description 根据计划ID更新计划的详细信息。系统计划不允许修改。
|
||||
// @Tags 计划管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
@@ -193,275 +137,148 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
|
||||
// @Param plan body dto.UpdatePlanRequest true "更新后的计划信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表更新成功"
|
||||
// @Router /api/v1/plans/{id} [put]
|
||||
func (c *Controller) UpdatePlan(ctx *gin.Context) {
|
||||
func (c *Controller) UpdatePlan(ctx echo.Context) error {
|
||||
const actionType = "更新计划"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 2. 绑定请求体
|
||||
var req dto.UpdatePlanRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
// 3. 将请求转换为模型(转换函数带校验)
|
||||
planToUpdate, err := dto.NewPlanFromUpdateRequest(&req)
|
||||
// 调用服务层更新计划
|
||||
resp, err := c.planService.UpdatePlan(uint(id), &req)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
|
||||
} else if errors.Is(err, plan.ErrPlanCannotBeModified) { // 修改为 plan.ErrPlanCannotBeModified
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许修改", id)
|
||||
}
|
||||
planToUpdate.ID = uint(id) // 确保ID被设置
|
||||
|
||||
// --- 自动判断 ContentType ---
|
||||
if len(req.SubPlanIDs) > 0 {
|
||||
planToUpdate.ContentType = models.PlanContentTypeSubPlans
|
||||
} else {
|
||||
// 如果 SubPlanIDs 未提供,则默认为 Tasks 类型(即使 Tasks 字段也未提供)
|
||||
planToUpdate.ContentType = models.PlanContentTypeTasks
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "服务层更新计划失败", req)
|
||||
}
|
||||
|
||||
// 4. 检查计划是否存在
|
||||
_, err = c.planRepo.GetBasicPlanByID(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||
return
|
||||
}
|
||||
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 调用仓库方法更新计划
|
||||
// 只要是更新任务,就重置执行计数器
|
||||
planToUpdate.ExecuteCount = 0 // 重置计数器
|
||||
c.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID)
|
||||
|
||||
if err := c.planRepo.UpdatePlan(planToUpdate); err != nil {
|
||||
c.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "数据库更新计划失败", planToUpdate)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新成功后,调用 manager 确保触发器任务定义存在
|
||||
if err := c.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil {
|
||||
// 这是一个非阻塞性错误,我们只记录日志
|
||||
c.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err)
|
||||
}
|
||||
|
||||
// 6. 获取更新后的完整计划用于响应
|
||||
updatedPlan, err := c.planRepo.GetPlanByID(uint(id))
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取更新后计划详情时发生内部错误", actionType, "获取更新后计划详情失败", id)
|
||||
return
|
||||
}
|
||||
|
||||
// 7. 将模型转换为响应 DTO
|
||||
resp, err := dto.NewPlanToResponse(updatedPlan)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan)
|
||||
return
|
||||
}
|
||||
|
||||
// 8. 发送成功响应
|
||||
c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划更新成功", resp, actionType, "计划更新成功", resp)
|
||||
// 9. 发送成功响应
|
||||
c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划更新成功", resp, actionType, "计划更新成功", resp)
|
||||
}
|
||||
|
||||
// DeletePlan godoc
|
||||
// @Summary 删除计划
|
||||
// @Description 根据计划ID删除计划。(软删除)
|
||||
// @Description 根据计划ID删除计划。(软删除)系统计划不允许删除。
|
||||
// @Tags 计划管理
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param id path int true "计划ID"
|
||||
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
|
||||
// @Router /api/v1/plans/{id} [delete]
|
||||
func (c *Controller) DeletePlan(ctx *gin.Context) {
|
||||
func (c *Controller) DeletePlan(ctx echo.Context) error {
|
||||
const actionType = "删除计划"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 2. 检查计划是否存在
|
||||
plan, err := c.planRepo.GetBasicPlanByID(uint(id))
|
||||
// 调用服务层删除计划
|
||||
err = c.planService.DeletePlan(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
|
||||
} else if errors.Is(err, plan.ErrPlanCannotBeDeleted) { // 修改为 plan.ErrPlanCannotBeDeleted
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许删除", id)
|
||||
}
|
||||
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划失败: "+err.Error(), actionType, "服务层删除计划失败", id)
|
||||
}
|
||||
|
||||
// 3. 停止这个计划
|
||||
if plan.Status == models.PlanStatusEnabled {
|
||||
if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil {
|
||||
c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 调用仓库层删除计划
|
||||
if err := c.planRepo.DeletePlan(uint(id)); err != nil {
|
||||
c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划时发生内部错误", actionType, "数据库删除失败", id)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 发送成功响应
|
||||
// 6. 发送成功响应
|
||||
c.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划删除成功", nil, actionType, "计划删除成功", id)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划删除成功", nil, actionType, "计划删除成功", id)
|
||||
}
|
||||
|
||||
// StartPlan godoc
|
||||
// @Summary 启动计划
|
||||
// @Description 根据计划ID启动一个计划的执行。
|
||||
// @Description 根据计划ID启动一个计划的执行。系统计划不允许手动启动。
|
||||
// @Tags 计划管理
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param id path int true "计划ID"
|
||||
// @Success 200 {object} controller.Response "业务码为200代表成功启动计划"
|
||||
// @Router /api/v1/plans/{id}/start [post]
|
||||
func (c *Controller) StartPlan(ctx *gin.Context) {
|
||||
func (c *Controller) StartPlan(ctx echo.Context) error {
|
||||
const actionType = "启动计划"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 2. 检查计划是否存在
|
||||
plan, err := c.planRepo.GetBasicPlanByID(uint(id))
|
||||
// 调用服务层启动计划
|
||||
err = c.planService.StartPlan(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
|
||||
} else if errors.Is(err, plan.ErrPlanCannotBeStarted) { // 修改为 plan.ErrPlanCannotBeStarted
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许手动启动", id)
|
||||
} else if errors.Is(err, plan.ErrPlanAlreadyEnabled) { // 修改为 plan.ErrPlanAlreadyEnabled
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "计划已处于启动状态", id)
|
||||
}
|
||||
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 检查计划当前状态
|
||||
if plan.Status == models.PlanStatusEnabled {
|
||||
c.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", id)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 检查并重置执行计数器,然后更新计划状态为“已启动”
|
||||
// 只有当计划是从非 Enabled 状态(如 Disabled, Stopeed, Failed)启动时,才需要重置计数器
|
||||
if plan.Status != models.PlanStatusEnabled {
|
||||
// 如果计划是从停止或失败状态重新启动,且计数器不为0,则重置执行计数
|
||||
if plan.ExecuteCount > 0 {
|
||||
if err := c.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil {
|
||||
c.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "重置计划执行计数失败", actionType, "重置执行计数失败", plan.ID)
|
||||
return
|
||||
}
|
||||
c.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID)
|
||||
}
|
||||
|
||||
// 更新计划状态为“已启动”
|
||||
if err := c.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil {
|
||||
c.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划状态失败", actionType, "更新计划状态失败", plan.ID)
|
||||
return
|
||||
}
|
||||
c.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID)
|
||||
} else {
|
||||
// 如果计划已经处于 Enabled 状态,则无需更新
|
||||
c.logger.Infof("计划 #%d 已处于启动状态,无需重复操作。", plan.ID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", plan.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 为计划创建或更新触发器
|
||||
if err := c.analysisPlanTaskManager.CreateOrUpdateTrigger(plan.ID); err != nil {
|
||||
// 此处错误不回滚状态,因为状态更新已成功,但需要明确告知用户触发器创建失败
|
||||
c.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划状态已更新,但创建执行触发器失败,请检查计划配置或稍后重试", actionType, "创建执行触发器失败", plan.ID)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "启动计划失败: "+err.Error(), actionType, "服务层启动计划失败", id)
|
||||
}
|
||||
|
||||
// 6. 发送成功响应
|
||||
c.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功启动", nil, actionType, "计划已成功启动", id)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功启动", nil, actionType, "计划已成功启动", id)
|
||||
}
|
||||
|
||||
// StopPlan godoc
|
||||
// @Summary 停止计划
|
||||
// @Description 根据计划ID停止一个正在执行的计划。
|
||||
// @Description 根据计划ID停止一个正在执行的计划。系统计划不能被停止。
|
||||
// @Tags 计划管理
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param id path int true "计划ID"
|
||||
// @Success 200 {object} controller.Response "业务码为200代表成功停止计划"
|
||||
// @Router /api/v1/plans/{id}/stop [post]
|
||||
func (c *Controller) StopPlan(ctx *gin.Context) {
|
||||
func (c *Controller) StopPlan(ctx echo.Context) error {
|
||||
const actionType = "停止计划"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 2. 检查计划是否存在
|
||||
plan, err := c.planRepo.GetBasicPlanByID(uint(id))
|
||||
// 调用服务层停止计划
|
||||
err = c.planService.StopPlan(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id)
|
||||
return
|
||||
c.logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
|
||||
} else if errors.Is(err, plan.ErrPlanCannotBeStopped) { // 修改为 plan.ErrPlanCannotBeStopped
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许停止", id)
|
||||
} else if errors.Is(err, plan.ErrPlanNotEnabled) { // 修改为 plan.ErrPlanNotEnabled
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "计划未启用", id)
|
||||
}
|
||||
c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划失败: "+err.Error(), actionType, "服务层停止计划失败", id)
|
||||
}
|
||||
|
||||
// 3. 检查计划当前状态
|
||||
if plan.Status != models.PlanStatusEnabled {
|
||||
c.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划当前不是启用状态", actionType, "计划未启用", id)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 调用仓库层方法,该方法内部处理事务
|
||||
if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil {
|
||||
c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 发送成功响应
|
||||
// 6. 发送成功响应
|
||||
c.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功停止", nil, actionType, "计划已成功停止", id)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功停止", nil, actionType, "计划已成功停止", id)
|
||||
}
|
||||
|
||||
@@ -1,827 +0,0 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MockPlanRepository 是 repository.PlanRepository 的一个模拟实现,用于测试
|
||||
type MockPlanRepository struct {
|
||||
// CreatePlanFunc 模拟 CreatePlan 方法的行为
|
||||
CreatePlanFunc func(plan *models.Plan) error
|
||||
// GetPlanByIDFunc 模拟 GetPlanByID 方法的行为
|
||||
GetPlanByIDFunc func(id uint) (*models.Plan, error)
|
||||
// GetBasicPlanByIDFunc 模拟 GetBasicPlanByID 方法的行为
|
||||
GetBasicPlanByIDFunc func(id uint) (*models.Plan, error)
|
||||
// ListBasicPlansFunc 模拟 ListBasicPlans 方法的行为
|
||||
ListBasicPlansFunc func() ([]models.Plan, error)
|
||||
// UpdatePlanFunc 模拟 UpdatePlan 方法的行为
|
||||
UpdatePlanFunc func(plan *models.Plan) error
|
||||
// DeletePlanFunc 模拟 DeletePlan 方法的行为
|
||||
DeletePlanFunc func(id uint) error
|
||||
}
|
||||
|
||||
// ListBasicPlans 实现了 MockPlanRepository 接口的 ListBasicPlans 方法
|
||||
func (m *MockPlanRepository) ListBasicPlans() ([]models.Plan, error) {
|
||||
return m.ListBasicPlansFunc()
|
||||
}
|
||||
|
||||
// GetBasicPlanByID 实现了 MockPlanRepository 接口的 GetBasicPlanByID 方法
|
||||
func (m *MockPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) {
|
||||
return m.GetBasicPlanByIDFunc(id)
|
||||
}
|
||||
|
||||
// GetPlanByID 实现了 MockPlanRepository 接口的 GetPlanByID 方法
|
||||
func (m *MockPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
|
||||
return m.GetPlanByIDFunc(id)
|
||||
}
|
||||
|
||||
// CreatePlan 实现了 MockPlanRepository 接口的 CreatePlan 方法
|
||||
func (m *MockPlanRepository) CreatePlan(plan *models.Plan) error {
|
||||
return m.CreatePlanFunc(plan)
|
||||
}
|
||||
|
||||
// UpdatePlan 实现了 MockPlanRepository 接口的 UpdatePlan 方法
|
||||
func (m *MockPlanRepository) UpdatePlan(plan *models.Plan) error {
|
||||
return m.UpdatePlanFunc(plan)
|
||||
}
|
||||
|
||||
// DeletePlan 实现了 MockPlanRepository 接口的 DeletePlan 方法
|
||||
func (m *MockPlanRepository) DeletePlan(id uint) error {
|
||||
return m.DeletePlanFunc(id)
|
||||
}
|
||||
|
||||
// setupTestRouter 创建一个用于测试的 gin 引擎和控制器实例
|
||||
func setupTestRouter(repo repository.PlanRepository) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
logger := logs.NewSilentLogger()
|
||||
planController := NewController(logger, repo)
|
||||
router.POST("/plans", planController.CreatePlan)
|
||||
router.GET("/plans/:id", planController.GetPlan)
|
||||
router.GET("/plans", planController.ListPlans)
|
||||
router.PUT("/plans/:id", planController.UpdatePlan)
|
||||
router.DELETE("/plans/:id", planController.DeletePlan)
|
||||
return router
|
||||
}
|
||||
|
||||
// TestController_CreatePlan 测试 CreatePlan 方法
|
||||
func TestController_CreatePlan(t *testing.T) {
|
||||
t.Run("成功-创建包含任务的计划", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库行为:CreatePlan 成功时,为计划和任务分配ID
|
||||
mockRepo := &MockPlanRepository{
|
||||
CreatePlanFunc: func(plan *models.Plan) error {
|
||||
plan.ID = 1
|
||||
for i := range plan.Tasks {
|
||||
plan.Tasks[i].ID = uint(i + 1)
|
||||
plan.Tasks[i].PlanID = plan.ID
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
// 设置 Gin 路由器,并注入模拟仓库
|
||||
router := setupTestRouter(mockRepo)
|
||||
|
||||
// 准备请求体
|
||||
reqBody := CreatePlanRequest{
|
||||
Name: "Test Plan with Tasks",
|
||||
ExecutionType: models.PlanExecutionTypeManual,
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []TaskRequest{
|
||||
{Name: "Task 1", ExecutionOrder: 1, Type: models.TaskTypeWaiting},
|
||||
},
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
// 创建 HTTP 请求
|
||||
req, _ := http.NewRequest(http.MethodPost, "/plans", bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Act (执行阶段)
|
||||
// 发送 HTTP 请求到路由器
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
// 验证 HTTP 状态码
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// 解析响应体
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证业务响应码和消息
|
||||
assert.Equal(t, controller.CodeCreated, resp.Code)
|
||||
assert.Equal(t, "计划创建成功", resp.Message)
|
||||
|
||||
// 验证返回数据中的计划ID
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, float64(1), dataMap["id"])
|
||||
})
|
||||
}
|
||||
|
||||
// TestController_GetPlan 是为 GetPlan 方法新增的单元测试函数
|
||||
func TestController_GetPlan(t *testing.T) {
|
||||
t.Run("成功-获取计划详情", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库行为:GetPlanByID 成功时返回一个计划
|
||||
mockRepo := &MockPlanRepository{
|
||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
assert.Equal(t, uint(1), id)
|
||||
return &models.Plan{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Name: "Test Plan",
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
// 设置 Gin 路由器
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
// 创建 HTTP 请求
|
||||
req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, float64(1), dataMap["id"])
|
||||
})
|
||||
|
||||
t.Run("成功-获取内容为空的计划详情", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库行为:GetPlanByID 成功时返回一个任务列表为空的计划
|
||||
mockRepo := &MockPlanRepository{
|
||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
assert.Equal(t, uint(3), id)
|
||||
return &models.Plan{
|
||||
Model: gorm.Model{ID: 3},
|
||||
Name: "Empty Plan",
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{}, // 任务列表为空
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/plans/3", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
||||
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, float64(3), dataMap["id"])
|
||||
assert.Equal(t, "Empty Plan", dataMap["name"])
|
||||
|
||||
// 关键断言:因为 omitempty 标签,当 tasks 列表为空时,该字段不应该出现在JSON中
|
||||
_, ok = dataMap["tasks"]
|
||||
assert.False(t, ok, "当任务列表为空时,'tasks' 字段因为 omitempty 标签,不应该出现在JSON响应中")
|
||||
})
|
||||
|
||||
t.Run("失败-计划不存在", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库行为:GetPlanByID 返回记录未找到错误
|
||||
mockRepo := &MockPlanRepository{
|
||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/plans/999", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeNotFound, resp.Code)
|
||||
assert.Equal(t, "计划不存在", resp.Message)
|
||||
})
|
||||
|
||||
t.Run("失败-无效的ID格式", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库为空,因为预期不会调用仓库方法
|
||||
mockRepo := &MockPlanRepository{}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
// 创建带有无效ID格式的 HTTP 请求
|
||||
req, _ := http.NewRequest(http.MethodGet, "/plans/abc", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
||||
assert.Equal(t, "无效的计划ID格式", resp.Message)
|
||||
})
|
||||
|
||||
t.Run("失败-仓库层内部错误", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
internalErr := errors.New("database connection lost")
|
||||
// 模拟仓库行为:GetPlanByID 返回内部错误
|
||||
mockRepo := &MockPlanRepository{
|
||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
return nil, internalErr
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
||||
assert.Equal(t, "获取计划详情时发生内部错误", resp.Message)
|
||||
})
|
||||
}
|
||||
|
||||
// TestController_ListPlans 测试 ListPlans 方法
|
||||
func TestController_ListPlans(t *testing.T) {
|
||||
t.Run("成功-获取计划列表", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟返回的计划列表
|
||||
mockPlans := []models.Plan{
|
||||
{Model: gorm.Model{ID: 1}, Name: "Plan 1", ContentType: models.PlanContentTypeTasks},
|
||||
{Model: gorm.Model{ID: 2}, Name: "Plan 2", ContentType: models.PlanContentTypeTasks},
|
||||
}
|
||||
// 模拟仓库行为:ListBasicPlans 成功时返回计划列表
|
||||
mockRepo := &MockPlanRepository{
|
||||
ListBasicPlansFunc: func() ([]models.Plan, error) {
|
||||
return mockPlans, nil
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/plans", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
||||
assert.Equal(t, "获取计划列表成功", resp.Message)
|
||||
|
||||
dataBytes, err := json.Marshal(resp.Data)
|
||||
assert.NoError(t, err)
|
||||
var listResp ListPlansResponse
|
||||
err = json.Unmarshal(dataBytes, &listResp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 2, listResp.Total)
|
||||
assert.Len(t, listResp.Plans, 2)
|
||||
assert.Equal(t, uint(1), listResp.Plans[0].ID)
|
||||
assert.Equal(t, "Plan 1", listResp.Plans[0].Name)
|
||||
})
|
||||
|
||||
t.Run("成功-返回空列表", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库行为:ListBasicPlans 返回空列表
|
||||
mockRepo := &MockPlanRepository{
|
||||
ListBasicPlansFunc: func() ([]models.Plan, error) {
|
||||
return []models.Plan{}, nil
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/plans", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
||||
|
||||
dataBytes, err := json.Marshal(resp.Data)
|
||||
assert.NoError(t, err)
|
||||
var listResp ListPlansResponse
|
||||
err = json.Unmarshal(dataBytes, &listResp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 0, listResp.Total)
|
||||
assert.Len(t, listResp.Plans, 0)
|
||||
})
|
||||
|
||||
t.Run("失败-仓库层返回错误", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
dbErr := errors.New("db error")
|
||||
// 模拟仓库行为:ListBasicPlans 返回数据库错误
|
||||
mockRepo := &MockPlanRepository{
|
||||
ListBasicPlansFunc: func() ([]models.Plan, error) {
|
||||
return nil, dbErr
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/plans", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
||||
assert.Equal(t, "获取计划列表时发生内部错误", resp.Message)
|
||||
})
|
||||
}
|
||||
|
||||
// TestController_UpdatePlan 是 UpdatePlan 的测试函数
|
||||
func TestController_UpdatePlan(t *testing.T) {
|
||||
t.Run("成功-更新计划", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
planID := uint(1)
|
||||
updatedName := "Updated Plan Name"
|
||||
// 模拟一个已存在的计划
|
||||
mockPlan := &models.Plan{
|
||||
Model: gorm.Model{ID: planID},
|
||||
Name: "Original Plan",
|
||||
Description: "Original Description",
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}
|
||||
// 配置模拟仓库的行为
|
||||
mockRepo := &MockPlanRepository{
|
||||
// 模拟 GetBasicPlanByID 成功返回现有计划
|
||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
assert.Equal(t, planID, id)
|
||||
return mockPlan, nil
|
||||
},
|
||||
// 模拟 UpdatePlan 成功更新计划,并更新 mockPlan 的名称
|
||||
UpdatePlanFunc: func(plan *models.Plan) error {
|
||||
assert.Equal(t, planID, plan.ID)
|
||||
assert.Equal(t, updatedName, plan.Name)
|
||||
mockPlan.Name = plan.Name // 模拟更新操作
|
||||
return nil
|
||||
},
|
||||
// 模拟 GetPlanByID 返回更新后的计划
|
||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
assert.Equal(t, planID, id)
|
||||
return mockPlan, nil // 返回已更新的 mockPlan
|
||||
},
|
||||
}
|
||||
// 设置 Gin 路由器,并注入模拟仓库
|
||||
router := setupTestRouter(mockRepo)
|
||||
|
||||
// 准备更新请求体
|
||||
reqBody := UpdatePlanRequest{
|
||||
Name: updatedName,
|
||||
Description: "Updated Description",
|
||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
// 创建 HTTP PUT 请求
|
||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Act (执行阶段)
|
||||
// 发送 HTTP 请求到路由器
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
// 验证 HTTP 状态码
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// 解析响应体
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证业务响应码、消息和返回数据
|
||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
||||
assert.Equal(t, "计划更新成功", resp.Message)
|
||||
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, float64(planID), dataMap["id"])
|
||||
assert.Equal(t, updatedName, dataMap["name"])
|
||||
})
|
||||
|
||||
t.Run("失败-无效的ID格式", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库为空,因为预期不会调用仓库方法
|
||||
mockRepo := &MockPlanRepository{}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
// 创建带有无效ID格式的 HTTP PUT 请求
|
||||
req, _ := http.NewRequest(http.MethodPut, "/plans/abc", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
||||
assert.Equal(t, "无效的计划ID格式", resp.Message)
|
||||
})
|
||||
|
||||
t.Run("失败-请求体绑定失败", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
planID := uint(1)
|
||||
// 模拟仓库为空,因为预期不会调用仓库方法(请求体绑定失败发生在控制器内部)
|
||||
mockRepo := &MockPlanRepository{}
|
||||
router := setupTestRouter(mockRepo)
|
||||
|
||||
// 准备一个无效的 JSON 请求体,例如 execution_type 类型错误
|
||||
reqBody := `{\"name\": \"Updated Plan Name\",}`
|
||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBufferString(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
||||
assert.Contains(t, resp.Message, "无效的请求体")
|
||||
})
|
||||
|
||||
t.Run("失败-计划不存在", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
planID := uint(999)
|
||||
// 模拟仓库行为:GetBasicPlanByID 返回记录未找到错误
|
||||
mockRepo := &MockPlanRepository{
|
||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
assert.Equal(t, planID, id)
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
|
||||
// 准备有效的请求体
|
||||
reqBody := UpdatePlanRequest{
|
||||
Name: "Updated Plan Name",
|
||||
Description: "Updated Description",
|
||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
// 创建 HTTP PUT 请求
|
||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeNotFound, resp.Code)
|
||||
assert.Equal(t, "计划不存在", resp.Message)
|
||||
})
|
||||
|
||||
t.Run("失败-计划数据校验失败", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
planID := uint(1)
|
||||
// 模拟一个已存在的计划
|
||||
mockPlan := &models.Plan{
|
||||
Model: gorm.Model{ID: planID},
|
||||
Name: "Original Plan",
|
||||
Description: "Original Description",
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}
|
||||
// 配置模拟仓库行为:GetBasicPlanByID 成功返回现有计划
|
||||
mockRepo := &MockPlanRepository{
|
||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
return mockPlan, nil
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
|
||||
// 准备一个会导致 PlanFromUpdateRequest 校验失败的请求体。
|
||||
// 这里通过提供重复的 ExecutionOrder 来触发 ValidateExecutionOrder 错误。
|
||||
reqBody := UpdatePlanRequest{
|
||||
Name: "Invalid Plan",
|
||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
||||
ContentType: models.PlanContentTypeTasks, // 设置为任务类型
|
||||
Tasks: []TaskRequest{
|
||||
{Name: "Task 1", ExecutionOrder: 1, Type: models.TaskTypeWaiting},
|
||||
{Name: "Task 2", ExecutionOrder: 1, Type: models.TaskTypeWaiting}, // 重复的执行顺序
|
||||
},
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
// 创建 HTTP PUT 请求
|
||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
||||
assert.Contains(t, resp.Message, "计划数据校验失败")
|
||||
})
|
||||
|
||||
t.Run("失败-仓库层更新失败", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
planID := uint(1)
|
||||
// 模拟一个已存在的计划
|
||||
mockPlan := &models.Plan{
|
||||
Model: gorm.Model{ID: planID},
|
||||
Name: "Original Plan",
|
||||
Description: "Original Description",
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}
|
||||
updateErr := errors.New("failed to update in repository")
|
||||
// 配置模拟仓库行为
|
||||
mockRepo := &MockPlanRepository{
|
||||
// 模拟 GetBasicPlanByID 成功返回现有计划
|
||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
return mockPlan, nil
|
||||
},
|
||||
// 模拟 UpdatePlan 返回更新失败错误
|
||||
UpdatePlanFunc: func(plan *models.Plan) error {
|
||||
return updateErr // 模拟更新失败
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
|
||||
// 准备有效的请求体
|
||||
reqBody := UpdatePlanRequest{
|
||||
Name: "Updated Plan Name",
|
||||
Description: "Updated Description",
|
||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
// 创建 HTTP PUT 请求
|
||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
||||
assert.Equal(t, "更新计划失败: "+updateErr.Error(), resp.Message)
|
||||
})
|
||||
|
||||
t.Run("失败-获取更新后计划失败", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
planID := uint(1)
|
||||
// 模拟一个已存在的计划
|
||||
mockPlan := &models.Plan{
|
||||
Model: gorm.Model{ID: planID},
|
||||
Name: "Original Plan",
|
||||
Description: "Original Description",
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}
|
||||
getUpdatedErr := errors.New("failed to get updated plan from repository")
|
||||
// 配置模拟仓库行为
|
||||
mockRepo := &MockPlanRepository{
|
||||
// 模拟 GetBasicPlanByID 成功返回现有计划
|
||||
GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
return mockPlan, nil
|
||||
},
|
||||
// 模拟 UpdatePlan 成功
|
||||
UpdatePlanFunc: func(plan *models.Plan) error {
|
||||
return nil // 模拟成功更新
|
||||
},
|
||||
// 模拟 GetPlanByID 返回获取失败错误
|
||||
GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
|
||||
return nil, getUpdatedErr // 模拟获取更新后计划失败
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
|
||||
// 准备有效的请求体
|
||||
reqBody := UpdatePlanRequest{
|
||||
Name: "Updated Plan Name",
|
||||
Description: "Updated Description",
|
||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
// 创建 HTTP PUT 请求
|
||||
req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
||||
assert.Equal(t, "获取更新后计划详情时发生内部错误", resp.Message)
|
||||
})
|
||||
}
|
||||
|
||||
// TestController_DeletePlan 是 DeletePlan 的单元测试
|
||||
func TestController_DeletePlan(t *testing.T) {
|
||||
t.Run("成功-删除计划", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库行为:DeletePlan 成功
|
||||
mockRepo := &MockPlanRepository{
|
||||
DeletePlanFunc: func(id uint) error {
|
||||
assert.Equal(t, uint(1), id)
|
||||
return nil // 模拟成功删除
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodDelete, "/plans/1", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeSuccess, resp.Code)
|
||||
assert.Equal(t, "计划删除成功", resp.Message)
|
||||
assert.Nil(t, resp.Data)
|
||||
})
|
||||
|
||||
t.Run("失败-计划不存在", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库行为:DeletePlan 返回记录未找到错误
|
||||
mockRepo := &MockPlanRepository{
|
||||
DeletePlanFunc: func(id uint) error {
|
||||
return gorm.ErrRecordNotFound // 模拟未找到记录
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodDelete, "/plans/999", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
||||
assert.Equal(t, "删除计划时发生内部错误", resp.Message)
|
||||
})
|
||||
|
||||
t.Run("失败-无效的ID格式", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
// 模拟仓库为空,因为预期不会调用仓库方法
|
||||
mockRepo := &MockPlanRepository{}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
// 创建带有无效ID格式的 HTTP DELETE 请求
|
||||
req, _ := http.NewRequest(http.MethodDelete, "/plans/abc", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeBadRequest, resp.Code)
|
||||
assert.Equal(t, "无效的计划ID格式", resp.Message)
|
||||
})
|
||||
|
||||
t.Run("失败-仓库层内部错误", func(t *testing.T) {
|
||||
// Arrange (准备阶段)
|
||||
internalErr := errors.New("something went wrong")
|
||||
// 模拟仓库行为:DeletePlan 返回内部错误
|
||||
mockRepo := &MockPlanRepository{
|
||||
|
||||
DeletePlanFunc: func(id uint) error {
|
||||
return internalErr // 模拟内部错误
|
||||
},
|
||||
}
|
||||
router := setupTestRouter(mockRepo)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodDelete, "/plans/1", nil)
|
||||
|
||||
// Act (执行阶段)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assert (断言阶段)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controller.CodeInternalError, resp.Code)
|
||||
assert.Equal(t, "删除计划时发生内部错误", resp.Message)
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// --- 业务状态码 ---
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
// 客户端错误状态码 (4000-4999)
|
||||
CodeBadRequest ResponseCode = 4000 // 请求参数错误
|
||||
CodeUnauthorized ResponseCode = 4001 // 未授权
|
||||
CodeForbidden ResponseCode = 4003 // 禁止访问
|
||||
CodeNotFound ResponseCode = 4004 // 资源未找到
|
||||
CodeConflict ResponseCode = 4009 // 资源冲突
|
||||
|
||||
@@ -32,12 +33,13 @@ const (
|
||||
type Response struct {
|
||||
Code ResponseCode `json:"code"` // 业务状态码
|
||||
Message string `json:"message"` // 提示信息
|
||||
Data interface{} `json:"data"` // 业务数据
|
||||
Data interface{} `json:"data,omitempty"` // 业务数据, omitempty表示如果为空则不序列化
|
||||
}
|
||||
|
||||
// SendResponse 发送统一格式的JSON响应 (基础函数,不带审计)
|
||||
func SendResponse(ctx *gin.Context, code ResponseCode, message string, data interface{}) {
|
||||
ctx.JSON(http.StatusOK, Response{
|
||||
// 所有的业务API都应该使用这个函数返回,以确保HTTP状态码始终为200 OK。
|
||||
func SendResponse(c echo.Context, code ResponseCode, message string, data interface{}) error {
|
||||
return c.JSON(http.StatusOK, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
@@ -45,51 +47,63 @@ func SendResponse(ctx *gin.Context, code ResponseCode, message string, data inte
|
||||
}
|
||||
|
||||
// SendErrorResponse 发送统一格式的错误响应 (基础函数,不带审计)
|
||||
func SendErrorResponse(ctx *gin.Context, code ResponseCode, message string) {
|
||||
SendResponse(ctx, code, message, nil)
|
||||
// HTTP状态码为200 OK,通过业务码表示错误。
|
||||
func SendErrorResponse(c echo.Context, code ResponseCode, message string) error {
|
||||
return SendResponse(c, code, message, nil)
|
||||
}
|
||||
|
||||
// SendErrorWithStatus 发送带有指定HTTP状态码的错误响应。
|
||||
// 这个函数主要用于中间件或特殊场景(如认证失败),在这些场景下需要返回非200的HTTP状态码。
|
||||
func SendErrorWithStatus(c echo.Context, httpStatus int, code ResponseCode, message string) error {
|
||||
return c.JSON(httpStatus, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// --- 带审计功能的响应函数 ---
|
||||
|
||||
// setAuditDetails 是一个内部辅助函数,用于在 gin.Context 中设置业务相关的审计信息。
|
||||
func setAuditDetails(c *gin.Context, actionType, description string, targetResource interface{}) {
|
||||
// setAuditDetails 是一个内部辅助函数,用于在 echo.Context 中统一设置所有业务相关的审计信息。
|
||||
func setAuditDetails(c echo.Context, actionType, description string, targetResource interface{}, status models.AuditStatus, resultDetails string) {
|
||||
// 只有当 actionType 不为空时,才设置审计信息,这作为触发审计的标志
|
||||
if actionType != "" {
|
||||
c.Set(models.ContextAuditActionType.String(), actionType)
|
||||
c.Set(models.ContextAuditDescription.String(), description)
|
||||
c.Set(models.ContextAuditTargetResource.String(), targetResource)
|
||||
c.Set(models.ContextAuditStatus.String(), status)
|
||||
c.Set(models.ContextAuditResultDetails.String(), resultDetails)
|
||||
}
|
||||
}
|
||||
|
||||
// SendSuccessWithAudit 发送成功的响应,并设置审计日志所需的信息。
|
||||
// 这是控制器中用于记录成功操作并返回响应的首选函数。
|
||||
func SendSuccessWithAudit(
|
||||
ctx *gin.Context, // Gin上下文,用于处理HTTP请求和响应
|
||||
c echo.Context, // Echo上下文,用于处理HTTP请求和响应
|
||||
code ResponseCode, // 业务状态码,表示操作结果
|
||||
message string, // 提示信息,向用户展示操作结果的文本描述
|
||||
data interface{}, // 业务数据,操作成功后返回的具体数据
|
||||
actionType string, // 审计操作类型,例如"创建用户", "更新配置"
|
||||
description string, // 审计描述,对操作的详细说明
|
||||
targetResource interface{}, // 审计目标资源,被操作的资源对象或其标识
|
||||
) {
|
||||
) error {
|
||||
// 1. 设置审计信息
|
||||
setAuditDetails(ctx, actionType, description, targetResource)
|
||||
setAuditDetails(c, actionType, description, targetResource, models.AuditStatusSuccess, "")
|
||||
// 2. 发送响应
|
||||
SendResponse(ctx, code, message, data)
|
||||
return SendResponse(c, code, message, data)
|
||||
}
|
||||
|
||||
// SendErrorWithAudit 发送失败的响应,并设置审计日志所需的信息。
|
||||
// 这是控制器中用于记录失败操作并返回响应的首选函数。
|
||||
func SendErrorWithAudit(
|
||||
ctx *gin.Context, // Gin上下文,用于处理HTTP请求和响应
|
||||
c echo.Context, // Echo上下文,用于处理HTTP请求和响应
|
||||
code ResponseCode, // 业务状态码,表示操作结果
|
||||
message string, // 提示信息,向用户展示操作结果的文本描述
|
||||
actionType string, // 审计操作类型,例如"登录失败", "删除失败"
|
||||
description string, // 审计描述,对操作的详细说明
|
||||
targetResource interface{}, // 审计目标资源,被操作的资源对象或其标识
|
||||
) {
|
||||
) error {
|
||||
// 1. 设置审计信息
|
||||
setAuditDetails(ctx, actionType, description, targetResource)
|
||||
setAuditDetails(c, actionType, description, targetResource, models.AuditStatusFailed, message)
|
||||
// 2. 发送响应
|
||||
SendErrorResponse(ctx, code, message)
|
||||
return SendErrorResponse(c, code, message)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,28 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Controller 用户控制器
|
||||
type Controller struct {
|
||||
userRepo repository.UserRepository
|
||||
monitorService service.MonitorService
|
||||
tokenService token.Service
|
||||
notifyService domain_notify.Service
|
||||
userService service.UserService
|
||||
logger *logs.Logger
|
||||
}
|
||||
|
||||
// NewController 创建用户控制器实例
|
||||
func NewController(
|
||||
userRepo repository.UserRepository,
|
||||
monitorService service.MonitorService,
|
||||
userService service.UserService,
|
||||
logger *logs.Logger,
|
||||
tokenService token.Service,
|
||||
notifyService domain_notify.Service,
|
||||
) *Controller {
|
||||
return &Controller{
|
||||
userRepo: userRepo,
|
||||
monitorService: monitorService,
|
||||
tokenService: tokenService,
|
||||
notifyService: notifyService,
|
||||
userService: userService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -53,38 +38,20 @@ func NewController(
|
||||
// @Param user body dto.CreateUserRequest true "用户信息"
|
||||
// @Success 200 {object} controller.Response{data=dto.CreateUserResponse} "业务码为201代表创建成功"
|
||||
// @Router /api/v1/users [post]
|
||||
func (c *Controller) CreateUser(ctx *gin.Context) {
|
||||
func (c *Controller) CreateUser(ctx echo.Context) error {
|
||||
var req dto.CreateUserRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("创建用户: 参数绑定失败: %v", err)
|
||||
controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
|
||||
return
|
||||
return controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Username: req.Username,
|
||||
Password: req.Password, // 密码会在 BeforeSave 钩子中哈希
|
||||
resp, err := c.userService.CreateUser(&req)
|
||||
if err != nil {
|
||||
c.logger.Errorf("创建用户: 服务层调用失败: %v", err)
|
||||
return controller.SendErrorResponse(ctx, controller.CodeInternalError, err.Error())
|
||||
}
|
||||
|
||||
if err := c.userRepo.Create(user); err != nil {
|
||||
c.logger.Errorf("创建用户: 创建用户失败: %v", err)
|
||||
|
||||
// 尝试查询用户,以判断是否是用户名重复导致的错误
|
||||
_, findErr := c.userRepo.FindByUsername(req.Username)
|
||||
if findErr == nil { // 如果能找到用户,说明是用户名重复
|
||||
controller.SendErrorResponse(ctx, controller.CodeConflict, "用户名已存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 其他创建失败的情况
|
||||
controller.SendErrorResponse(ctx, controller.CodeInternalError, "创建用户失败")
|
||||
return
|
||||
}
|
||||
|
||||
controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", dto.CreateUserResponse{
|
||||
Username: user.Username,
|
||||
ID: user.ID,
|
||||
})
|
||||
return controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", resp)
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
@@ -96,110 +63,20 @@ func (c *Controller) CreateUser(ctx *gin.Context) {
|
||||
// @Param credentials body dto.LoginRequest true "登录凭证"
|
||||
// @Success 200 {object} controller.Response{data=dto.LoginResponse} "业务码为200代表登录成功"
|
||||
// @Router /api/v1/users/login [post]
|
||||
func (c *Controller) Login(ctx *gin.Context) {
|
||||
func (c *Controller) Login(ctx echo.Context) error {
|
||||
var req dto.LoginRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("登录: 参数绑定失败: %v", err)
|
||||
controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
|
||||
return
|
||||
return controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
|
||||
}
|
||||
|
||||
// 使用新的方法,通过唯一标识符(用户名、邮箱等)查找用户
|
||||
user, err := c.userRepo.FindUserForLogin(req.Identifier)
|
||||
resp, err := c.userService.Login(&req)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
controller.SendErrorResponse(ctx, controller.CodeUnauthorized, "登录凭证不正确")
|
||||
return
|
||||
}
|
||||
c.logger.Errorf("登录: 查询用户失败: %v", err)
|
||||
controller.SendErrorResponse(ctx, controller.CodeInternalError, "登录失败")
|
||||
return
|
||||
c.logger.Errorf("登录: 服务层调用失败: %v", err)
|
||||
return controller.SendErrorResponse(ctx, controller.CodeUnauthorized, err.Error())
|
||||
}
|
||||
|
||||
if !user.CheckPassword(req.Password) {
|
||||
controller.SendErrorResponse(ctx, controller.CodeUnauthorized, "登录凭证不正确")
|
||||
return
|
||||
}
|
||||
|
||||
// 登录成功,生成 JWT token
|
||||
tokenString, err := c.tokenService.GenerateToken(user.ID)
|
||||
if err != nil {
|
||||
c.logger.Errorf("登录: 生成令牌失败: %v", err)
|
||||
controller.SendErrorResponse(ctx, controller.CodeInternalError, "登录失败,无法生成认证信息")
|
||||
return
|
||||
}
|
||||
|
||||
controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", dto.LoginResponse{
|
||||
Username: user.Username,
|
||||
ID: user.ID,
|
||||
Token: tokenString,
|
||||
})
|
||||
}
|
||||
|
||||
// ListUserHistory godoc
|
||||
// @Summary 获取指定用户的操作历史
|
||||
// @Description 根据用户ID,分页获取该用户的操作审计日志。支持与通用日志查询接口相同的过滤和排序参数。
|
||||
// @Tags 用户管理
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param id path int true "用户ID"
|
||||
// @Param query query dto.ListUserActionLogRequest false "查询参数 (除了 user_id,它被路径中的ID覆盖)"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListUserActionLogResponse} "业务码为200代表成功获取"
|
||||
// @Router /api/v1/users/{id}/history [get]
|
||||
func (c *Controller) ListUserHistory(ctx *gin.Context) {
|
||||
const actionType = "获取用户操作历史"
|
||||
|
||||
// 1. 解析路径中的用户ID,它的优先级最高
|
||||
userIDStr := ctx.Param("id")
|
||||
userID, err := strconv.ParseUint(userIDStr, 10, 64)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 无效的用户ID格式: %v, ID: %s", actionType, err, userIDStr)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", userIDStr)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 绑定通用的查询请求 DTO
|
||||
var req dto.ListUserActionLogRequest
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 准备 Service 调用参数,并强制使用路径中的 UserID
|
||||
uid := uint(userID)
|
||||
req.UserID = &uid // 强制覆盖
|
||||
|
||||
opts := repository.UserActionLogListOptions{
|
||||
UserID: req.UserID,
|
||||
Username: req.Username,
|
||||
ActionType: req.ActionType,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.AuditStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
// 4. 调用 monitorService,复用其业务逻辑
|
||||
data, total, err := c.monitorService.ListUserActionLogs(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", opts)
|
||||
return
|
||||
}
|
||||
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户历史记录失败", actionType, "服务层查询失败", opts)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 使用复用的 DTO 构建并发送成功响应
|
||||
resp := dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize)
|
||||
c.logger.Infof("%s: 成功获取用户 %d 的操作历史, 数量: %d", actionType, userID, len(data))
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作历史成功", resp, actionType, "获取用户操作历史成功", opts)
|
||||
return controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", resp)
|
||||
}
|
||||
|
||||
// SendTestNotification godoc
|
||||
@@ -213,34 +90,31 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) {
|
||||
// @Param body body dto.SendTestNotificationRequest true "请求体"
|
||||
// @Success 200 {object} controller.Response{data=string} "成功响应"
|
||||
// @Router /api/v1/users/{id}/notifications/test [post]
|
||||
func (c *Controller) SendTestNotification(ctx *gin.Context) {
|
||||
func (c *Controller) SendTestNotification(ctx echo.Context) error {
|
||||
const actionType = "发送测试通知"
|
||||
|
||||
// 1. 从 URL 中获取用户 ID
|
||||
userID, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", ctx.Param("id"))
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", ctx.Param("id"))
|
||||
}
|
||||
|
||||
// 2. 从请求体 (JSON Body) 中获取要测试的通知类型
|
||||
var req dto.SendTestNotificationRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "请求体格式错误或缺少 'type' 字段: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "请求体格式错误或缺少 'type' 字段: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
// 3. 调用领域服务
|
||||
err = c.notifyService.SendTestMessage(uint(userID), req.Type)
|
||||
// 3. 调用服务层
|
||||
err = c.userService.SendTestNotification(uint(userID), &req)
|
||||
if err != nil {
|
||||
c.logger.Errorf("%s: 服务层调用失败: %v", actionType, err)
|
||||
controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", gin.H{"userID": userID, "type": req.Type})
|
||||
return
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", map[string]interface{}{"userID": userID, "type": req.Type})
|
||||
}
|
||||
|
||||
// 4. 返回成功响应
|
||||
c.logger.Infof("%s: 成功为用户 %d 发送类型为 %s 的测试消息", actionType, userID, req.Type)
|
||||
controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "测试消息已发送,请检查您的接收端。", nil, actionType, "测试消息发送成功", gin.H{"userID": userID, "type": req.Type})
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "测试消息已发送,请检查您的接收端。", nil, actionType, "测试消息发送成功", map[string]interface{}{"userID": userID, "type": req.Type})
|
||||
}
|
||||
|
||||
@@ -1,450 +0,0 @@
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MockUserRepository 是 UserRepository 接口的模拟实现
|
||||
type MockUserRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreateTx 模拟 UserRepository 的 CreateTx 方法
|
||||
func (m *MockUserRepository) Create(user *models.User) error {
|
||||
args := m.Called(user)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// FindByUsername 模拟 UserRepository 的 FindByUsername 方法
|
||||
// 返回类型改回 *models.User
|
||||
func (m *MockUserRepository) FindByUsername(username string) (*models.User, error) {
|
||||
args := m.Called(username)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
// FindByID 模拟 UserRepository 的 FindByID 方法
|
||||
func (m *MockUserRepository) FindByID(id uint) (*models.User, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
// MockTokenService 是 token.TokenService 接口的模拟实现
|
||||
type MockTokenService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GenerateToken 模拟 TokenService 的 GenerateToken 方法
|
||||
func (m *MockTokenService) GenerateToken(userID uint) (string, error) {
|
||||
args := m.Called(userID)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
// ParseToken 模拟 TokenService 的 ParseToken 方法
|
||||
func (m *MockTokenService) ParseToken(tokenString string) (*token.Claims, error) {
|
||||
args := m.Called(tokenString)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*token.Claims), args.Error(1)
|
||||
}
|
||||
|
||||
// TestCreateUser 测试 CreateUser 方法
|
||||
func TestCreateUser(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode) // 设置 Gin 为测试模式
|
||||
|
||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
||||
silentLogger := logs.NewSilentLogger()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestBody user.CreateUserRequest
|
||||
mockRepoSetup func(*MockUserRepository)
|
||||
expectedResponse map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "成功创建用户",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Username: "testuser",
|
||||
Password: "password123",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 模拟 CreateTx 成功
|
||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) {
|
||||
// 模拟数据库自动填充 ID
|
||||
userArg := args.Get(0).(*models.User)
|
||||
userArg.ID = 1 // 设置一个非零的 ID
|
||||
}).Once()
|
||||
// 在成功创建用户的路径下,FindByUsername 不会被调用,因此这里不需要设置其期望
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeCreated), // 修改这里:使用自定义状态码
|
||||
"message": "用户创建成功",
|
||||
"data": map[string]interface{}{
|
||||
"username": "testuser",
|
||||
// "id": mock.Anything, // 移除这里的 id,在断言时单独检查
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败_密码过短",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Username: "testuser2",
|
||||
Password: "123", // 密码少于6位
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 不会调用 CreateTx 或 FindByUsername
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeBadRequest),
|
||||
"message": "Key: 'CreateUserRequest.Password' Error:Field validation for 'Password' failed on the 'min' tag",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败_缺少用户名",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Password: "password123",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 不会调用 CreateTx 或 FindByUsername
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeBadRequest),
|
||||
"message": "Key: 'CreateUserRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "用户名已存在",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Username: "existinguser",
|
||||
Password: "password123",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 模拟 CreateTx 失败,因为用户名已存在
|
||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once()
|
||||
// 模拟 FindByUsername 找到用户,确认是用户名重复
|
||||
m.On("FindByUsername", "existinguser").Return(&models.User{Username: "existinguser"}, nil).Once()
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeConflict),
|
||||
"message": "用户名已存在",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "创建用户失败_通用数据库错误",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Username: "db_error_user",
|
||||
Password: "password123",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 模拟 CreateTx 失败,通用数据库错误
|
||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once()
|
||||
// 模拟 FindByUsername 找不到用户,确认不是用户名重复
|
||||
m.On("FindByUsername", "db_error_user").Return(nil, gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeInternalError),
|
||||
"message": "创建用户失败",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 初始化 Gin 上下文和记录器
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/users", nil) // URL 路径不重要,因为我们不测试路由
|
||||
|
||||
// 设置请求体
|
||||
jsonBody, _ := json.Marshal(tt.requestBody)
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(jsonBody))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 创建 Mock UserRepository
|
||||
mockRepo := new(MockUserRepository)
|
||||
|
||||
// 设置 Mock UserRepository 行为
|
||||
tt.mockRepoSetup(mockRepo)
|
||||
|
||||
// 创建控制器实例,使用静默日志器
|
||||
userController := user.NewController(mockRepo, silentLogger, nil) // tokenService 在 CreateUser 中未使用,设为 nil
|
||||
|
||||
// 调用被测试的方法
|
||||
userController.CreateUser(ctx)
|
||||
|
||||
// 解析响应体
|
||||
var responseBody map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 断言响应体中的 code 字段
|
||||
assert.Equal(t, tt.expectedResponse["code"], responseBody["code"])
|
||||
|
||||
// 断言响应内容 (除了 code 字段)
|
||||
if tt.expectedResponse["code"] == float64(controller.CodeCreated) {
|
||||
// 确保 data 字段存在且是 map[string]interface{} 类型
|
||||
data, ok := responseBody["data"].(map[string]interface{})
|
||||
assert.True(t, ok, "响应体中的 data 字段应为 map[string]interface{}")
|
||||
// 确保 id 字段存在且不为零
|
||||
id, idOk := data["id"].(float64)
|
||||
assert.True(t, idOk, "响应体中的 data.id 字段应为 float64 类型")
|
||||
assert.NotEqual(t, float64(0), id, "响应体中的 data.id 不应为零")
|
||||
|
||||
// 移除 ID 字段以便进行通用断言
|
||||
delete(responseBody["data"].(map[string]interface{}), "id")
|
||||
// 移除 expectedResponse 中的 id 字段,因为我们已经单独验证了
|
||||
if expectedData, ok := tt.expectedResponse["data"].(map[string]interface{}); ok {
|
||||
delete(expectedData, "id")
|
||||
}
|
||||
}
|
||||
// 移除 code 字段以便进行通用断言
|
||||
delete(responseBody, "code")
|
||||
delete(tt.expectedResponse, "code")
|
||||
assert.Equal(t, tt.expectedResponse, responseBody)
|
||||
|
||||
// 验证 Mock 期望是否都已满足
|
||||
mockRepo.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLogin 测试 Login 方法
|
||||
func TestLogin(t *testing.T) {
|
||||
// 设置release模式阻止废话日志
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
||||
silentLogger := logs.NewSilentLogger()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestBody user.LoginRequest
|
||||
mockRepoSetup func(*MockUserRepository)
|
||||
mockTokenServiceSetup func(*MockTokenService)
|
||||
expectedResponse map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "成功登录",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
mockUser := &models.User{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
||||
}
|
||||
// 调用 BeforeCreate 钩子来哈希密码
|
||||
_ = mockUser.BeforeCreate(nil)
|
||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {
|
||||
m.On("GenerateToken", uint(1)).Return("mocked_token", nil).Once()
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeSuccess),
|
||||
"message": "登录成功",
|
||||
"data": map[string]interface{}{
|
||||
"username": "loginuser",
|
||||
"id": float64(1),
|
||||
"token": "mocked_token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败_缺少用户名",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "", // 缺少用户名
|
||||
Password: "password",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeBadRequest),
|
||||
"message": "Key: 'LoginRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败_缺少密码",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "", // 缺少密码
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeBadRequest),
|
||||
"message": "Key: 'LoginRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "用户不存在",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "nonexistent",
|
||||
Password: "anypassword",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
m.On("FindByUsername", "nonexistent").Return(nil, gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeUnauthorized),
|
||||
"message": "用户名或密码不正确",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "查询用户失败_通用数据库错误",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "dberroruser",
|
||||
Password: "password",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
m.On("FindByUsername", "dberroruser").Return(nil, errors.New("database connection error")).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {}, expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeInternalError),
|
||||
"message": "登录失败",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "密码不正确",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "loginuser",
|
||||
Password: "wrongpassword",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
mockUser := &models.User{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
||||
}
|
||||
// 调用 BeforeCreate 钩子来哈希密码
|
||||
_ = mockUser.BeforeCreate(nil)
|
||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeUnauthorized),
|
||||
"message": "用户名或密码不正确",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "生成Token失败",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
mockUser := &models.User{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
||||
}
|
||||
// 调用 BeforeCreate 钩子来哈希密码
|
||||
_ = mockUser.BeforeCreate(nil)
|
||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {
|
||||
m.On("GenerateToken", uint(1)).Return("", errors.New("jwt error")).Once()
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeInternalError),
|
||||
"message": "登录失败,无法生成认证信息",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 初始化 Gin 上下文和记录器
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/login", nil) // URL 路径不重要,因为我们不测试路由
|
||||
|
||||
// 设置请求体
|
||||
jsonBody, _ := json.Marshal(tt.requestBody)
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(jsonBody))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 创建 Mock
|
||||
mockRepo := new(MockUserRepository)
|
||||
mockTokenService := new(MockTokenService)
|
||||
|
||||
// 设置 Mock 行为
|
||||
tt.mockRepoSetup(mockRepo)
|
||||
tt.mockTokenServiceSetup(mockTokenService)
|
||||
|
||||
// 创建控制器实例
|
||||
userController := user.NewController(mockRepo, silentLogger, mockTokenService)
|
||||
|
||||
// 调用被测试的方法
|
||||
userController.Login(ctx)
|
||||
|
||||
// 解析响应体
|
||||
var responseBody map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 断言响应体中的 code 字段
|
||||
assert.Equal(t, tt.expectedResponse["code"], responseBody["code"])
|
||||
|
||||
// 断言响应内容 (除了 code 字段)
|
||||
if tt.expectedResponse["code"] == float64(controller.CodeSuccess) {
|
||||
// 确保 data 字段存在且是 map[string]interface{} 类型
|
||||
data, ok := responseBody["data"].(map[string]interface{})
|
||||
assert.True(t, ok, "响应体中的 data 字段应为 map[string]interface{}")
|
||||
|
||||
// 验证 id 和 token 存在
|
||||
assert.NotNil(t, data["id"])
|
||||
assert.NotNil(t, data["token"])
|
||||
|
||||
// 移除 ID 和 Token 字段以便进行通用断言
|
||||
delete(responseBody["data"].(map[string]interface{}), "id")
|
||||
delete(tt.expectedResponse["data"].(map[string]interface{}), "id")
|
||||
delete(responseBody["data"].(map[string]interface{}), "token")
|
||||
delete(tt.expectedResponse["data"].(map[string]interface{}), "token")
|
||||
}
|
||||
// 移除 code 字段以便进行通用断言
|
||||
delete(responseBody, "code")
|
||||
delete(tt.expectedResponse, "code")
|
||||
assert.Equal(t, tt.expectedResponse, responseBody)
|
||||
|
||||
// 验证 Mock 期望是否都已满足
|
||||
mockRepo.AssertExpectations(t)
|
||||
mockTokenService.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,20 @@ import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
|
||||
// CreateDeviceRequest 定义了创建设备时需要传入的参数
|
||||
type CreateDeviceRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
DeviceTemplateID uint `json:"device_template_id" binding:"required"`
|
||||
AreaControllerID uint `json:"area_controller_id" binding:"required"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
DeviceTemplateID uint `json:"device_template_id" validate:"required"`
|
||||
AreaControllerID uint `json:"area_controller_id" validate:"required"`
|
||||
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// UpdateDeviceRequest 定义了更新设备时需要传入的参数
|
||||
type UpdateDeviceRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
DeviceTemplateID uint `json:"device_template_id" binding:"required"`
|
||||
AreaControllerID uint `json:"area_controller_id" binding:"required"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
DeviceTemplateID uint `json:"device_template_id" validate:"required"`
|
||||
AreaControllerID uint `json:"area_controller_id" validate:"required"`
|
||||
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// ManualControlDeviceRequest 定义了手动控制设备时需要传入的参数
|
||||
@@ -28,38 +28,38 @@ type ManualControlDeviceRequest struct {
|
||||
|
||||
// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数
|
||||
type CreateAreaControllerRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
NetworkID string `json:"network_id" binding:"required"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
NetworkID string `json:"network_id" validate:"required"`
|
||||
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数
|
||||
type UpdateAreaControllerRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
NetworkID string `json:"network_id" binding:"required"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
NetworkID string `json:"network_id" validate:"required"`
|
||||
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数
|
||||
type CreateDeviceTemplateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Category models.DeviceCategory `json:"category" binding:"required"`
|
||||
Commands map[string]interface{} `json:"commands" binding:"required"`
|
||||
Values []models.ValueDescriptor `json:"values,omitempty"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Manufacturer string `json:"manufacturer,omitempty" validate:"omitempty"`
|
||||
Description string `json:"description,omitempty" validate:"omitempty"`
|
||||
Category models.DeviceCategory `json:"category" validate:"required"`
|
||||
Commands map[string]interface{} `json:"commands" validate:"required"`
|
||||
Values []models.ValueDescriptor `json:"values,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数
|
||||
type UpdateDeviceTemplateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Category models.DeviceCategory `json:"category" binding:"required"`
|
||||
Commands map[string]interface{} `json:"commands" binding:"required"`
|
||||
Values []models.ValueDescriptor `json:"values,omitempty"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Manufacturer string `json:"manufacturer,omitempty" validate:"omitempty"`
|
||||
Description string `json:"description,omitempty" validate:"omitempty"`
|
||||
Category models.DeviceCategory `json:"category" validate:"required"`
|
||||
Commands map[string]interface{} `json:"commands" validate:"required"`
|
||||
Values []models.ValueDescriptor `json:"values,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
// DeviceResponse 定义了返回给客户端的单个设备信息的结构
|
||||
|
||||
@@ -53,14 +53,20 @@ func NewListDeviceCommandLogResponse(data []models.DeviceCommandLog, total int64
|
||||
}
|
||||
|
||||
// NewListPlanExecutionLogResponse 从模型数据创建列表响应 DTO
|
||||
func NewListPlanExecutionLogResponse(data []models.PlanExecutionLog, total int64, page, pageSize int) *ListPlanExecutionLogResponse {
|
||||
dtos := make([]PlanExecutionLogDTO, len(data))
|
||||
for i, item := range data {
|
||||
func NewListPlanExecutionLogResponse(planLogs []models.PlanExecutionLog, plans []models.Plan, total int64, page, pageSize int) *ListPlanExecutionLogResponse {
|
||||
planId2Name := make(map[uint]string)
|
||||
for _, plan := range plans {
|
||||
planId2Name[plan.ID] = plan.Name
|
||||
}
|
||||
|
||||
dtos := make([]PlanExecutionLogDTO, len(planLogs))
|
||||
for i, item := range planLogs {
|
||||
dtos[i] = PlanExecutionLogDTO{
|
||||
ID: item.ID,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
PlanID: item.PlanID,
|
||||
PlanName: planId2Name[item.PlanID],
|
||||
Status: item.Status,
|
||||
StartedAt: item.StartedAt,
|
||||
EndedAt: item.EndedAt,
|
||||
|
||||
@@ -13,20 +13,20 @@ import (
|
||||
type PaginationDTO struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// --- SensorData ---
|
||||
|
||||
// ListSensorDataRequest 定义了获取传感器数据列表的请求参数
|
||||
type ListSensorDataRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
DeviceID *uint `form:"device_id"`
|
||||
SensorType *string `form:"sensor_type"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
DeviceID *uint `json:"device_id" query:"device_id"`
|
||||
SensorType *string `json:"sensor_type" query:"sensor_type"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// SensorDataDTO 是用于API响应的传感器数据结构
|
||||
@@ -48,13 +48,13 @@ type ListSensorDataResponse struct {
|
||||
|
||||
// ListDeviceCommandLogRequest 定义了获取设备命令日志列表的请求参数
|
||||
type ListDeviceCommandLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
DeviceID *uint `form:"device_id"`
|
||||
ReceivedSuccess *bool `form:"received_success"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
DeviceID *uint `json:"device_id" query:"device_id"`
|
||||
ReceivedSuccess *bool `json:"received_success" query:"received_success"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// DeviceCommandLogDTO 是用于API响应的设备命令日志结构
|
||||
@@ -76,13 +76,13 @@ type ListDeviceCommandLogResponse struct {
|
||||
|
||||
// ListPlanExecutionLogRequest 定义了获取计划执行日志列表的请求参数
|
||||
type ListPlanExecutionLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PlanID *uint `form:"plan_id"`
|
||||
Status *string `form:"status"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PlanID *uint `json:"plan_id" query:"plan_id"`
|
||||
Status *string `json:"status" query:"status"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// PlanExecutionLogDTO 是用于API响应的计划执行日志结构
|
||||
@@ -91,6 +91,7 @@ type PlanExecutionLogDTO struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PlanID uint `json:"plan_id"`
|
||||
PlanName string `json:"plan_name"`
|
||||
Status models.ExecutionStatus `json:"status"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
EndedAt time.Time `json:"ended_at"`
|
||||
@@ -107,14 +108,14 @@ type ListPlanExecutionLogResponse struct {
|
||||
|
||||
// ListTaskExecutionLogRequest 定义了获取任务执行日志列表的请求参数
|
||||
type ListTaskExecutionLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PlanExecutionLogID *uint `form:"plan_execution_log_id"`
|
||||
TaskID *int `form:"task_id"`
|
||||
Status *string `form:"status"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PlanExecutionLogID *uint `json:"plan_execution_log_id" query:"plan_execution_log_id"`
|
||||
TaskID *int `json:"task_id" query:"task_id"`
|
||||
Status *string `json:"status" query:"status"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// TaskDTO 是用于API响应的简化版任务结构
|
||||
@@ -148,13 +149,13 @@ type ListTaskExecutionLogResponse struct {
|
||||
|
||||
// ListPendingCollectionRequest 定义了获取待采集请求列表的请求参数
|
||||
type ListPendingCollectionRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
DeviceID *uint `form:"device_id"`
|
||||
Status *string `form:"status"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
DeviceID *uint `json:"device_id" query:"device_id"`
|
||||
Status *string `json:"status" query:"status"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// PendingCollectionDTO 是用于API响应的待采集请求结构
|
||||
@@ -177,15 +178,15 @@ type ListPendingCollectionResponse struct {
|
||||
|
||||
// ListUserActionLogRequest 定义了获取用户操作日志列表的请求参数
|
||||
type ListUserActionLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
UserID *uint `form:"user_id"`
|
||||
Username *string `form:"username"`
|
||||
ActionType *string `form:"action_type"`
|
||||
Status *string `form:"status"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
UserID *uint `json:"user_id" query:"user_id"`
|
||||
Username *string `json:"username" query:"username"`
|
||||
ActionType *string `json:"action_type" query:"action_type"`
|
||||
Status *string `json:"status" query:"status"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// UserActionLogDTO 是用于API响应的用户操作日志结构
|
||||
@@ -214,13 +215,13 @@ type ListUserActionLogResponse struct {
|
||||
|
||||
// ListRawMaterialPurchaseRequest 定义了获取原料采购列表的请求参数
|
||||
type ListRawMaterialPurchaseRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
RawMaterialID *uint `form:"raw_material_id"`
|
||||
Supplier *string `form:"supplier"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
RawMaterialID *uint `json:"raw_material_id" query:"raw_material_id"`
|
||||
Supplier *string `json:"supplier" query:"supplier"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// RawMaterialDTO 是用于API响应的简化版原料结构
|
||||
@@ -252,14 +253,14 @@ type ListRawMaterialPurchaseResponse struct {
|
||||
|
||||
// ListRawMaterialStockLogRequest 定义了获取原料库存日志列表的请求参数
|
||||
type ListRawMaterialStockLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
RawMaterialID *uint `form:"raw_material_id"`
|
||||
SourceType *string `form:"source_type"`
|
||||
SourceID *uint `form:"source_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
RawMaterialID *uint `json:"raw_material_id" query:"raw_material_id"`
|
||||
SourceType *string `json:"source_type" query:"source_type"`
|
||||
SourceID *uint `json:"source_id" query:"source_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// RawMaterialStockLogDTO 是用于API响应的原料库存日志结构
|
||||
@@ -283,14 +284,14 @@ type ListRawMaterialStockLogResponse struct {
|
||||
|
||||
// ListFeedUsageRecordRequest 定义了获取饲料使用记录列表的请求参数
|
||||
type ListFeedUsageRecordRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PenID *uint `form:"pen_id"`
|
||||
FeedFormulaID *uint `form:"feed_formula_id"`
|
||||
OperatorID *uint `form:"operator_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PenID *uint `json:"pen_id" query:"pen_id"`
|
||||
FeedFormulaID *uint `json:"feed_formula_id" query:"feed_formula_id"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// PenDTO 是用于API响应的简化版猪栏结构
|
||||
@@ -328,15 +329,15 @@ type ListFeedUsageRecordResponse struct {
|
||||
|
||||
// ListMedicationLogRequest 定义了获取用药记录列表的请求参数
|
||||
type ListMedicationLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PigBatchID *uint `form:"pig_batch_id"`
|
||||
MedicationID *uint `form:"medication_id"`
|
||||
Reason *string `form:"reason"`
|
||||
OperatorID *uint `form:"operator_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
MedicationID *uint `json:"medication_id" query:"medication_id"`
|
||||
Reason *string `json:"reason" query:"reason"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// MedicationDTO 是用于API响应的简化版药品结构
|
||||
@@ -369,14 +370,14 @@ type ListMedicationLogResponse struct {
|
||||
|
||||
// ListPigBatchLogRequest 定义了获取猪批次日志列表的请求参数
|
||||
type ListPigBatchLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PigBatchID *uint `form:"pig_batch_id"`
|
||||
ChangeType *string `form:"change_type"`
|
||||
OperatorID *uint `form:"operator_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
ChangeType *string `json:"change_type" query:"change_type"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// PigBatchLogDTO 是用于API响应的猪批次日志结构
|
||||
@@ -404,12 +405,12 @@ type ListPigBatchLogResponse struct {
|
||||
|
||||
// ListWeighingBatchRequest 定义了获取批次称重记录列表的请求参数
|
||||
type ListWeighingBatchRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PigBatchID *uint `form:"pig_batch_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// WeighingBatchDTO 是用于API响应的批次称重记录结构
|
||||
@@ -432,14 +433,14 @@ type ListWeighingBatchResponse struct {
|
||||
|
||||
// ListWeighingRecordRequest 定义了获取单次称重记录列表的请求参数
|
||||
type ListWeighingRecordRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
WeighingBatchID *uint `form:"weighing_batch_id"`
|
||||
PenID *uint `form:"pen_id"`
|
||||
OperatorID *uint `form:"operator_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
WeighingBatchID *uint `json:"weighing_batch_id" query:"weighing_batch_id"`
|
||||
PenID *uint `json:"pen_id" query:"pen_id"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// WeighingRecordDTO 是用于API响应的单次称重记录结构
|
||||
@@ -465,16 +466,16 @@ type ListWeighingRecordResponse struct {
|
||||
|
||||
// ListPigTransferLogRequest 定义了获取猪只迁移日志列表的请求参数
|
||||
type ListPigTransferLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PigBatchID *uint `form:"pig_batch_id"`
|
||||
PenID *uint `form:"pen_id"`
|
||||
TransferType *string `form:"transfer_type"`
|
||||
OperatorID *uint `form:"operator_id"`
|
||||
CorrelationID *string `form:"correlation_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
PenID *uint `json:"pen_id" query:"pen_id"`
|
||||
TransferType *string `json:"transfer_type" query:"transfer_type"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
CorrelationID *string `json:"correlation_id" query:"correlation_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// PigTransferLogDTO 是用于API响应的猪只迁移日志结构
|
||||
@@ -502,16 +503,16 @@ type ListPigTransferLogResponse struct {
|
||||
|
||||
// ListPigSickLogRequest 定义了获取病猪日志列表的请求参数
|
||||
type ListPigSickLogRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PigBatchID *uint `form:"pig_batch_id"`
|
||||
PenID *uint `form:"pen_id"`
|
||||
Reason *string `form:"reason"`
|
||||
TreatmentLocation *string `form:"treatment_location"`
|
||||
OperatorID *uint `form:"operator_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
PenID *uint `json:"pen_id" query:"pen_id"`
|
||||
Reason *string `json:"reason" query:"reason"`
|
||||
TreatmentLocation *string `json:"treatment_location" query:"treatment_location"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// PigSickLogDTO 是用于API响应的病猪日志结构
|
||||
@@ -541,14 +542,14 @@ type ListPigSickLogResponse struct {
|
||||
|
||||
// ListPigPurchaseRequest 定义了获取猪只采购记录列表的请求参数
|
||||
type ListPigPurchaseRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PigBatchID *uint `form:"pig_batch_id"`
|
||||
Supplier *string `form:"supplier"`
|
||||
OperatorID *uint `form:"operator_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
Supplier *string `json:"supplier" query:"supplier"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// PigPurchaseDTO 是用于API响应的猪只采购记录结构
|
||||
@@ -576,14 +577,14 @@ type ListPigPurchaseResponse struct {
|
||||
|
||||
// ListPigSaleRequest 定义了获取猪只销售记录列表的请求参数
|
||||
type ListPigSaleRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
PigBatchID *uint `form:"pig_batch_id"`
|
||||
Buyer *string `form:"buyer"`
|
||||
OperatorID *uint `form:"operator_id"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
Buyer *string `json:"buyer" query:"buyer"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// PigSaleDTO 是用于API响应的猪只销售记录结构
|
||||
|
||||
@@ -11,20 +11,20 @@ import (
|
||||
// SendTestNotificationRequest 定义了发送测试通知请求的 JSON 结构
|
||||
type SendTestNotificationRequest struct {
|
||||
// Type 指定要测试的通知渠道
|
||||
Type notify.NotifierType `json:"type" binding:"required"`
|
||||
Type notify.NotifierType `json:"type" validate:"required"`
|
||||
}
|
||||
|
||||
// ListNotificationRequest 定义了获取通知列表的请求参数
|
||||
type ListNotificationRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
UserID *uint `form:"user_id"`
|
||||
NotifierType *notify.NotifierType `form:"notifier_type"`
|
||||
Status *models.NotificationStatus `form:"status"`
|
||||
Level *zapcore.Level `form:"level"`
|
||||
StartTime *time.Time `form:"start_time"`
|
||||
EndTime *time.Time `form:"end_time"`
|
||||
OrderBy string `form:"order_by"`
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
UserID *uint `json:"user_id" query:"user_id"`
|
||||
NotifierType *notify.NotifierType `json:"notifier_type" query:"notifier_type"`
|
||||
Status *models.NotificationStatus `json:"status" query:"status"`
|
||||
Level *zapcore.Level `json:"level" query:"level"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"`
|
||||
OrderBy string `json:"order_by" query:"order_by"`
|
||||
}
|
||||
|
||||
// NotificationDTO 是用于API响应的通知结构
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
|
||||
// PigBatchCreateDTO 定义了创建猪批次的请求结构
|
||||
type PigBatchCreateDTO struct {
|
||||
BatchNumber string `json:"batch_number" binding:"required"` // 批次编号,必填
|
||||
OriginType models.PigBatchOriginType `json:"origin_type" binding:"required"` // 批次来源,必填
|
||||
StartDate time.Time `json:"start_date" binding:"required"` // 批次开始日期,必填
|
||||
InitialCount int `json:"initial_count" binding:"required,min=1"` // 初始数量,必填,最小为1
|
||||
Status models.PigBatchStatus `json:"status" binding:"required"` // 批次状态,必填
|
||||
BatchNumber string `json:"batch_number" validate:"required"` // 批次编号,必填
|
||||
OriginType models.PigBatchOriginType `json:"origin_type" validate:"required"` // 批次来源,必填
|
||||
StartDate time.Time `json:"start_date" validate:"required"` // 批次开始日期,必填
|
||||
InitialCount int `json:"initial_count" validate:"required,min=1"` // 初始数量,必填,最小为1
|
||||
Status models.PigBatchStatus `json:"status" validate:"required"` // 批次状态,必填
|
||||
}
|
||||
|
||||
// PigBatchUpdateDTO 定义了更新猪批次的请求结构
|
||||
@@ -27,7 +27,7 @@ type PigBatchUpdateDTO struct {
|
||||
|
||||
// PigBatchQueryDTO 定义了查询猪批次的请求结构
|
||||
type PigBatchQueryDTO struct {
|
||||
IsActive *bool `json:"is_active" form:"is_active"` // 是否活跃,可选,用于URL查询参数
|
||||
IsActive *bool `json:"is_active" query:"is_active"` // 是否活跃,可选,用于URL查询参数
|
||||
}
|
||||
|
||||
// PigBatchResponseDTO 定义了猪批次信息的响应结构
|
||||
@@ -40,123 +40,123 @@ type PigBatchResponseDTO struct {
|
||||
InitialCount int `json:"initial_count"` // 初始数量
|
||||
Status models.PigBatchStatus `json:"status"` // 批次状态
|
||||
IsActive bool `json:"is_active"` // 是否活跃
|
||||
CurrentTotalQuantity int `json:"currentTotalQuantity"` // 当前总数
|
||||
CurrentTotalPigsInPens int `json:"currentTotalPigsInPens"` // 当前存栏总数
|
||||
CurrentTotalQuantity int `json:"current_total_quantity"` // 当前总数
|
||||
CurrentTotalPigsInPens int `json:"current_total_pigs_in_pens"` // 当前存栏总数
|
||||
CreateTime time.Time `json:"create_time"` // 创建时间
|
||||
UpdateTime time.Time `json:"update_time"` // 更新时间
|
||||
}
|
||||
|
||||
// AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体
|
||||
type AssignEmptyPensToBatchRequest struct {
|
||||
PenIDs []uint `json:"penIDs" binding:"required,min=1" example:"[1,2,3]"` // 待分配的猪栏ID列表
|
||||
PenIDs []uint `json:"pen_ids" validate:"required,min=1,dive" example:"1,2,3"` // 待分配的猪栏ID列表
|
||||
}
|
||||
|
||||
// ReclassifyPenToNewBatchRequest 用于将猪栏划拨到新批次的请求体
|
||||
type ReclassifyPenToNewBatchRequest struct {
|
||||
ToBatchID uint `json:"toBatchID" binding:"required"` // 目标猪批次ID
|
||||
PenID uint `json:"penID" binding:"required"` // 待划拨的猪栏ID
|
||||
ToBatchID uint `json:"to_batch_id" validate:"required"` // 目标猪批次ID
|
||||
PenID uint `json:"pen_id" validate:"required"` // 待划拨的猪栏ID
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体
|
||||
type RemoveEmptyPenFromBatchRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 待移除的猪栏ID
|
||||
PenID uint `json:"pen_id" validate:"required"` // 待移除的猪栏ID
|
||||
}
|
||||
|
||||
// MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体
|
||||
type MovePigsIntoPenRequest struct {
|
||||
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 移入猪只数量
|
||||
ToPenID uint `json:"to_pen_id" validate:"required"` // 目标猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 移入猪只数量
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// SellPigsRequest 用于处理卖猪的请求体
|
||||
type SellPigsRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 卖出猪只数量
|
||||
UnitPrice float64 `json:"unitPrice" binding:"required,min=0"` // 单价
|
||||
TotalPrice float64 `json:"totalPrice" binding:"required,min=0"` // 总价
|
||||
TraderName string `json:"traderName" binding:"required"` // 交易方名称
|
||||
TradeDate time.Time `json:"tradeDate" binding:"required"` // 交易日期
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 卖出猪只数量
|
||||
UnitPrice float64 `json:"unit_price" validate:"required,min=0"` // 单价
|
||||
TotalPrice float64 `json:"total_price" validate:"required,min=0"` // 总价
|
||||
TraderName string `json:"trader_name" validate:"required"` // 交易方名称
|
||||
TradeDate time.Time `json:"trade_date" validate:"required"` // 交易日期
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// BuyPigsRequest 用于处理买猪的请求体
|
||||
type BuyPigsRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 买入猪只数量
|
||||
UnitPrice float64 `json:"unitPrice" binding:"required,min=0"` // 单价
|
||||
TotalPrice float64 `json:"totalPrice" binding:"required,min=0"` // 总价
|
||||
TraderName string `json:"traderName" binding:"required"` // 交易方名称
|
||||
TradeDate time.Time `json:"tradeDate" binding:"required"` // 交易日期
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 买入猪只数量
|
||||
UnitPrice float64 `json:"unit_price" validate:"required,min=0"` // 单价
|
||||
TotalPrice float64 `json:"total_price" validate:"required,min=0"` // 总价
|
||||
TraderName string `json:"trader_name" validate:"required"` // 交易方名称
|
||||
TradeDate time.Time `json:"trade_date" validate:"required"` // 交易日期
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体
|
||||
type TransferPigsAcrossBatchesRequest struct {
|
||||
DestBatchID uint `json:"destBatchID" binding:"required"` // 目标猪批次ID
|
||||
FromPenID uint `json:"fromPenID" binding:"required"` // 源猪栏ID
|
||||
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
|
||||
Quantity uint `json:"quantity" binding:"required,min=1"` // 调栏猪只数量
|
||||
DestBatchID uint `json:"dest_batch_id" validate:"required"` // 目标猪批次ID
|
||||
FromPenID uint `json:"from_pen_id" validate:"required"` // 源猪栏ID
|
||||
ToPenID uint `json:"to_pen_id" validate:"required"` // 目标猪栏ID
|
||||
Quantity uint `json:"quantity" validate:"required,min=1"` // 调栏猪只数量
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// TransferPigsWithinBatchRequest 用于群内调栏的请求体
|
||||
type TransferPigsWithinBatchRequest struct {
|
||||
FromPenID uint `json:"fromPenID" binding:"required"` // 源猪栏ID
|
||||
ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID
|
||||
Quantity uint `json:"quantity" binding:"required,min=1"` // 调栏猪只数量
|
||||
FromPenID uint `json:"from_pen_id" validate:"required"` // 源猪栏ID
|
||||
ToPenID uint `json:"to_pen_id" validate:"required"` // 目标猪栏ID
|
||||
Quantity uint `json:"quantity" validate:"required,min=1"` // 调栏猪只数量
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RecordSickPigsRequest 用于记录新增病猪事件的请求体
|
||||
type RecordSickPigsRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 病猪数量
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
|
||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 病猪数量
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点
|
||||
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体
|
||||
type RecordSickPigRecoveryRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 康复猪数量
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
|
||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 康复猪数量
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点
|
||||
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体
|
||||
type RecordSickPigDeathRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 死亡猪数量
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
|
||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 死亡猪数量
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点
|
||||
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体
|
||||
type RecordSickPigCullRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 淘汰猪数量
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点
|
||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 淘汰猪数量
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点
|
||||
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RecordDeathRequest 用于记录正常猪只死亡事件的请求体
|
||||
type RecordDeathRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 死亡猪数量
|
||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 死亡猪数量
|
||||
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RecordCullRequest 用于记录正常猪只淘汰事件的请求体
|
||||
type RecordCullRequest struct {
|
||||
PenID uint `json:"penID" binding:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" binding:"required,min=1"` // 淘汰猪数量
|
||||
HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
Quantity int `json:"quantity" validate:"required,min=1"` // 淘汰猪数量
|
||||
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
@@ -22,32 +22,32 @@ type PenResponse struct {
|
||||
|
||||
// CreatePigHouseRequest 定义了创建猪舍的请求结构
|
||||
type CreatePigHouseRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// UpdatePigHouseRequest 定义了更新猪舍的请求结构
|
||||
type UpdatePigHouseRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// CreatePenRequest 定义了创建猪栏的请求结构
|
||||
type CreatePenRequest struct {
|
||||
PenNumber string `json:"pen_number" binding:"required"`
|
||||
HouseID uint `json:"house_id" binding:"required"`
|
||||
Capacity int `json:"capacity" binding:"required"`
|
||||
PenNumber string `json:"pen_number" validate:"required"`
|
||||
HouseID uint `json:"house_id" validate:"required"`
|
||||
Capacity int `json:"capacity" validate:"required"`
|
||||
}
|
||||
|
||||
// UpdatePenRequest 定义了更新猪栏的请求结构
|
||||
type UpdatePenRequest struct {
|
||||
PenNumber string `json:"pen_number" binding:"required"`
|
||||
HouseID uint `json:"house_id" binding:"required"`
|
||||
Capacity int `json:"capacity" binding:"required"`
|
||||
Status models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验
|
||||
PenNumber string `json:"pen_number" validate:"required"`
|
||||
HouseID uint `json:"house_id" validate:"required"`
|
||||
Capacity int `json:"capacity" validate:"required"`
|
||||
Status models.PenStatus `json:"status" validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验
|
||||
}
|
||||
|
||||
// UpdatePenStatusRequest 定义了更新猪栏状态的请求结构
|
||||
type UpdatePenStatusRequest struct {
|
||||
Status models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"`
|
||||
Status models.PenStatus `json:"status" validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"`
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) {
|
||||
ID: plan.ID,
|
||||
Name: plan.Name,
|
||||
Description: plan.Description,
|
||||
PlanType: plan.PlanType,
|
||||
ExecutionType: plan.ExecutionType,
|
||||
Status: plan.Status,
|
||||
ExecuteNum: plan.ExecuteNum,
|
||||
@@ -52,7 +53,7 @@ func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// NewPlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证
|
||||
// NewPlanFromCreateRequest 将CreatePlanRequest转换为Plan模型
|
||||
func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
|
||||
if req == nil {
|
||||
return nil, nil
|
||||
@@ -64,7 +65,7 @@ func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
|
||||
ExecutionType: req.ExecutionType,
|
||||
ExecuteNum: req.ExecuteNum,
|
||||
CronExpression: req.CronExpression,
|
||||
// ContentType 在控制器中设置,此处不再处理
|
||||
// ContentType 和 PlanType 在控制器中设置,此处不再处理
|
||||
}
|
||||
|
||||
// 处理子计划 (通过ID引用)
|
||||
@@ -74,7 +75,7 @@ func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
|
||||
for i, childPlanID := range subPlanSlice {
|
||||
plan.SubPlans[i] = models.SubPlan{
|
||||
ChildPlanID: childPlanID,
|
||||
ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认
|
||||
ExecutionOrder: i, // 默认执行顺序
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,19 +93,10 @@ func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 首先,执行重复性验证
|
||||
if err := plan.ValidateExecutionOrder(); err != nil {
|
||||
// 如果检测到重复,立即返回错误
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 然后,调用方法来修复顺序断层
|
||||
plan.ReorderSteps()
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// NewPlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证
|
||||
// NewPlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型
|
||||
func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
|
||||
if req == nil {
|
||||
return nil, nil
|
||||
@@ -116,7 +108,7 @@ func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
|
||||
ExecutionType: req.ExecutionType,
|
||||
ExecuteNum: req.ExecuteNum,
|
||||
CronExpression: req.CronExpression,
|
||||
// ContentType 在控制器中设置,此处不再处理
|
||||
// ContentType 和 PlanType 在控制器中设置,此处不再处理
|
||||
}
|
||||
|
||||
// 处理子计划 (通过ID引用)
|
||||
@@ -126,7 +118,7 @@ func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
|
||||
for i, childPlanID := range subPlanSlice {
|
||||
plan.SubPlans[i] = models.SubPlan{
|
||||
ChildPlanID: childPlanID,
|
||||
ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认
|
||||
ExecutionOrder: i, // 默认执行顺序
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,15 +136,6 @@ func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 首先,执行重复性验证
|
||||
if err := plan.ValidateExecutionOrder(); err != nil {
|
||||
// 如果检测到重复,立即返回错误
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 然后,调用方法来修复顺序断层
|
||||
plan.ReorderSteps()
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
package dto
|
||||
|
||||
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// ListPlansQuery 定义了获取计划列表时的查询参数
|
||||
type ListPlansQuery struct {
|
||||
PlanType repository.PlanTypeFilter `json:"plan_type" query:"planType"` // 计划类型
|
||||
Page int `json:"page" query:"page"` // 页码
|
||||
PageSize int `json:"page_size" query:"page_size"` // 每页大小
|
||||
}
|
||||
|
||||
// CreatePlanRequest 定义创建计划请求的结构体
|
||||
type CreatePlanRequest struct {
|
||||
Name string `json:"name" binding:"required" example:"猪舍温度控制计划"`
|
||||
Name string `json:"name" validate:"required" example:"猪舍温度控制计划"`
|
||||
Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
|
||||
ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
|
||||
ExecuteNum uint `json:"execute_num,omitempty" example:"10"`
|
||||
CronExpression string `json:"cron_expression" example:"0 0 6 * * *"`
|
||||
SubPlanIDs []uint `json:"sub_plan_ids,omitempty"`
|
||||
Tasks []TaskRequest `json:"tasks,omitempty"`
|
||||
ExecutionType models.PlanExecutionType `json:"execution_type" validate:"required" example:"自动"`
|
||||
ExecuteNum uint `json:"execute_num,omitempty" validate:"omitempty,min=0" example:"10"`
|
||||
CronExpression string `json:"cron_expression" validate:"omitempty,cron" example:"0 0 6 * * *"`
|
||||
SubPlanIDs []uint `json:"sub_plan_ids,omitempty" validate:"omitempty,dive"`
|
||||
Tasks []TaskRequest `json:"tasks,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
// PlanResponse 定义计划详情响应的结构体
|
||||
@@ -18,6 +28,7 @@ type PlanResponse struct {
|
||||
ID uint `json:"id" example:"1"`
|
||||
Name string `json:"name" example:"猪舍温度控制计划"`
|
||||
Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
|
||||
PlanType models.PlanType `json:"plan_type" example:"自定义任务"`
|
||||
ExecutionType models.PlanExecutionType `json:"execution_type" example:"自动"`
|
||||
Status models.PlanStatus `json:"status" example:"已启用"`
|
||||
ExecuteNum uint `json:"execute_num" example:"10"`
|
||||
@@ -31,18 +42,18 @@ type PlanResponse struct {
|
||||
// ListPlansResponse 定义获取计划列表响应的结构体
|
||||
type ListPlansResponse struct {
|
||||
Plans []PlanResponse `json:"plans"`
|
||||
Total int `json:"total" example:"100"`
|
||||
Total int64 `json:"total" example:"100"`
|
||||
}
|
||||
|
||||
// UpdatePlanRequest 定义更新计划请求的结构体
|
||||
type UpdatePlanRequest struct {
|
||||
Name string `json:"name" example:"猪舍温度控制计划V2"`
|
||||
Description string `json:"description" example:"更新后的描述"`
|
||||
ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"`
|
||||
ExecuteNum uint `json:"execute_num,omitempty" example:"10"`
|
||||
CronExpression string `json:"cron_expression" example:"0 0 6 * * *"`
|
||||
SubPlanIDs []uint `json:"sub_plan_ids,omitempty"`
|
||||
Tasks []TaskRequest `json:"tasks,omitempty"`
|
||||
ExecutionType models.PlanExecutionType `json:"execution_type" validate:"required" example:"自动"`
|
||||
ExecuteNum uint `json:"execute_num,omitempty" validate:"omitempty,min=0" example:"10"`
|
||||
CronExpression string `json:"cron_expression" validate:"omitempty,cron" example:"0 0 6 * * *"`
|
||||
SubPlanIDs []uint `json:"sub_plan_ids,omitempty" validate:"omitempty,dive"`
|
||||
Tasks []TaskRequest `json:"tasks,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
// SubPlanResponse 定义子计划响应结构体
|
||||
|
||||
@@ -2,15 +2,15 @@ package dto
|
||||
|
||||
// CreateUserRequest 定义创建用户请求的结构体
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required" example:"newuser"`
|
||||
Password string `json:"password" binding:"required" example:"password123"`
|
||||
Username string `json:"username" validate:"required" example:"newuser"`
|
||||
Password string `json:"password" validate:"required" example:"password123"`
|
||||
}
|
||||
|
||||
// LoginRequest 定义登录请求的结构体
|
||||
type LoginRequest struct {
|
||||
// Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号
|
||||
Identifier string `json:"identifier" binding:"required" example:"testuser"`
|
||||
Password string `json:"password" binding:"required" example:"password123"`
|
||||
Identifier string `json:"identifier" validate:"required" example:"testuser"`
|
||||
Password string `json:"password" validate:"required" example:"password123"`
|
||||
}
|
||||
|
||||
// CreateUserResponse 定义创建用户成功响应的结构体
|
||||
|
||||
@@ -1,117 +1,59 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type auditResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// AuditLogMiddleware 创建一个Gin中间件,用于在请求结束后记录用户操作审计日志
|
||||
func AuditLogMiddleware(auditService audit.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 使用自定义的 response body writer 来捕获响应体
|
||||
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
|
||||
c.Writer = blw
|
||||
|
||||
// AuditLogMiddleware 创建一个Echo中间件,用于在请求结束后记录用户操作审计日志。
|
||||
// 它依赖于控制器通过调用 SendSuccessWithAudit 或 SendErrorWithAudit 在上下文中设置的审计信息。
|
||||
func AuditLogMiddleware(auditService audit.Service) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// 首先执行请求链中的后续处理程序(即业务控制器)
|
||||
c.Next()
|
||||
err := next(c)
|
||||
|
||||
// --- 在这里,请求已经处理完毕 ---
|
||||
|
||||
// 从上下文中尝试获取由控制器设置的业务审计信息
|
||||
actionType, exists := c.Get(models.ContextAuditActionType.String())
|
||||
if !exists {
|
||||
actionType, exists := c.Get(models.ContextAuditActionType.String()).(string)
|
||||
if !exists || actionType == "" {
|
||||
// 如果上下文中没有 actionType,说明此接口无需记录审计日志,直接返回
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// 从 Gin Context 中获取用户对象
|
||||
userCtx, userExists := c.Get(models.ContextUserKey.String())
|
||||
// 从 Context 中获取用户对象
|
||||
var user *models.User
|
||||
if userExists {
|
||||
if userCtx := c.Get(models.ContextUserKey.String()); userCtx != nil {
|
||||
user, _ = userCtx.(*models.User)
|
||||
}
|
||||
|
||||
// 构建 RequestContext
|
||||
reqCtx := audit.RequestContext{
|
||||
ClientIP: c.ClientIP(),
|
||||
HTTPPath: c.Request.URL.Path,
|
||||
HTTPMethod: c.Request.Method,
|
||||
ClientIP: c.RealIP(),
|
||||
HTTPPath: c.Request().URL.Path,
|
||||
HTTPMethod: c.Request().Method,
|
||||
}
|
||||
|
||||
// 获取其他审计信息
|
||||
description, _ := c.Get(models.ContextAuditDescription.String())
|
||||
targetResource, _ := c.Get(models.ContextAuditTargetResource.String())
|
||||
|
||||
// 默认操作状态为成功
|
||||
status := models.AuditStatusSuccess
|
||||
resultDetails := ""
|
||||
|
||||
// 尝试从捕获的响应体中解析平台响应
|
||||
var platformResponse auditResponse
|
||||
if err := json.Unmarshal(blw.body.Bytes(), &platformResponse); err == nil {
|
||||
// 如果解析成功,根据平台状态码判断操作是否失败
|
||||
// 成功状态码范围是 2000-2999
|
||||
if platformResponse.Code < 2000 || platformResponse.Code >= 3000 {
|
||||
status = models.AuditStatusFailed
|
||||
resultDetails = platformResponse.Message
|
||||
}
|
||||
} else {
|
||||
// 如果响应体不是预期的平台响应格式,或者解析失败,则记录原始HTTP状态码作为详情
|
||||
// 并且如果HTTP状态码不是2xx,则标记为失败
|
||||
if c.Writer.Status() < 200 || c.Writer.Status() >= 300 {
|
||||
status = models.AuditStatusFailed
|
||||
}
|
||||
resultDetails = "HTTP Status: " + strconv.Itoa(c.Writer.Status()) + ", Body Parse Error: " + err.Error()
|
||||
}
|
||||
// 直接从上下文中获取所有其他审计信息
|
||||
description, _ := c.Get(models.ContextAuditDescription.String()).(string)
|
||||
targetResource := c.Get(models.ContextAuditTargetResource.String())
|
||||
status, _ := c.Get(models.ContextAuditStatus.String()).(models.AuditStatus)
|
||||
resultDetails, _ := c.Get(models.ContextAuditResultDetails.String()).(string)
|
||||
|
||||
// 调用审计服务记录日志(异步)
|
||||
auditService.LogAction(
|
||||
user,
|
||||
reqCtx,
|
||||
actionType.(string),
|
||||
description.(string),
|
||||
actionType,
|
||||
description,
|
||||
targetResource,
|
||||
status,
|
||||
resultDetails,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// bodyLogWriter 是一个自定义的 gin.ResponseWriter,用于捕获响应体
|
||||
// 这对于在操作失败时记录详细的错误信息非常有用
|
||||
type bodyLogWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
return err
|
||||
}
|
||||
|
||||
func (w bodyLogWriter) Write(b []byte) (int, error) {
|
||||
w.body.Write(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (w bodyLogWriter) WriteString(s string) (int, error) {
|
||||
w.body.WriteString(s)
|
||||
return w.ResponseWriter.WriteString(s)
|
||||
}
|
||||
|
||||
// ReadBody 用于安全地读取请求体,并防止其被重复读取
|
||||
func ReadBody(c *gin.Context) ([]byte, error) {
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 将读取的内容放回 Body 中,以便后续的处理函数可以再次读取
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
return bodyBytes, nil
|
||||
}
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
// Package middleware 存放 gin 中间件
|
||||
// Package middleware 存放中间件
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthMiddleware 创建一个Gin中间件,用于JWT身份验证
|
||||
// AuthMiddleware 创建一个Echo中间件,用于JWT身份验证
|
||||
// 它依赖于 TokenService 来解析和验证 token,并使用 UserRepository 来获取完整的用户信息
|
||||
func AuthMiddleware(tokenService token.Service, userRepo repository.UserRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
func AuthMiddleware(tokenService token.Service, userRepo repository.UserRepository) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// 从 Authorization header 获取 token
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
authHeader := c.Request().Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "请求未包含授权标头"})
|
||||
return
|
||||
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "请求未包含授权标头")
|
||||
}
|
||||
|
||||
// 授权标头的格式应为 "Bearer <token>"
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "授权标头格式不正确"})
|
||||
return
|
||||
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "授权标头格式不正确")
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
@@ -35,27 +36,25 @@ func AuthMiddleware(tokenService token.Service, userRepo repository.UserReposito
|
||||
// 解析和验证 token
|
||||
claims, err := tokenService.ParseToken(tokenString)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "无效的Token"})
|
||||
return
|
||||
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "无效的Token")
|
||||
}
|
||||
|
||||
// 根据 token 中的用户ID,从数据库中获取完整的用户信息
|
||||
user, err := userRepo.FindByID(claims.UserID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Token有效,但对应的用户已不存在
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "授权用户不存在"})
|
||||
return
|
||||
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "授权用户不存在")
|
||||
}
|
||||
// 其他数据库查询错误
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "获取用户信息失败"})
|
||||
return
|
||||
return controller.SendErrorWithStatus(c, http.StatusInternalServerError, controller.CodeInternalError, "获取用户信息失败")
|
||||
}
|
||||
|
||||
// 将完整的用户对象存储在 context 中,以便后续的处理函数使用
|
||||
c.Set(models.ContextUserKey.String(), user)
|
||||
|
||||
// 继续处理请求链中的下一个处理程序
|
||||
c.Next()
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
389
internal/app/service/device_service.go
Normal file
389
internal/app/service/device_service.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrDeviceInUse 表示设备正在被任务使用,无法删除
|
||||
ErrDeviceInUse = errors.New("设备正在被一个或多个任务使用,无法删除")
|
||||
|
||||
// ErrAreaControllerInUse 表示区域主控正在被设备使用,无法删除
|
||||
ErrAreaControllerInUse = errors.New("区域主控正在被一个或多个设备使用,无法删除")
|
||||
|
||||
// ErrDeviceTemplateInUse 表示设备模板正在被设备使用,无法删除
|
||||
ErrDeviceTemplateInUse = errors.New("设备模板正在被一个或多个设备使用,无法删除")
|
||||
)
|
||||
|
||||
// DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。
|
||||
// DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。
|
||||
type DeviceService interface {
|
||||
CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error)
|
||||
GetDevice(id uint) (*dto.DeviceResponse, error)
|
||||
ListDevices() ([]*dto.DeviceResponse, error)
|
||||
UpdateDevice(id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error)
|
||||
DeleteDevice(id uint) error
|
||||
ManualControl(id uint, req *dto.ManualControlDeviceRequest) error
|
||||
|
||||
CreateAreaController(req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error)
|
||||
GetAreaController(id uint) (*dto.AreaControllerResponse, error)
|
||||
ListAreaControllers() ([]*dto.AreaControllerResponse, error)
|
||||
UpdateAreaController(id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error)
|
||||
DeleteAreaController(id uint) error
|
||||
|
||||
CreateDeviceTemplate(req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
|
||||
GetDeviceTemplate(id uint) (*dto.DeviceTemplateResponse, error)
|
||||
ListDeviceTemplates() ([]*dto.DeviceTemplateResponse, error)
|
||||
UpdateDeviceTemplate(id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
|
||||
DeleteDeviceTemplate(id uint) error
|
||||
}
|
||||
|
||||
// deviceService 是 DeviceService 接口的具体实现。
|
||||
type deviceService struct {
|
||||
deviceRepo repository.DeviceRepository
|
||||
areaControllerRepo repository.AreaControllerRepository
|
||||
deviceTemplateRepo repository.DeviceTemplateRepository
|
||||
deviceDomainSvc device.Service // 依赖领域服务
|
||||
}
|
||||
|
||||
// NewDeviceService 创建一个新的 DeviceService 实例。
|
||||
func NewDeviceService(
|
||||
deviceRepo repository.DeviceRepository,
|
||||
areaControllerRepo repository.AreaControllerRepository,
|
||||
deviceTemplateRepo repository.DeviceTemplateRepository,
|
||||
deviceDomainSvc device.Service,
|
||||
) DeviceService {
|
||||
return &deviceService{
|
||||
deviceRepo: deviceRepo,
|
||||
areaControllerRepo: areaControllerRepo,
|
||||
deviceTemplateRepo: deviceTemplateRepo,
|
||||
deviceDomainSvc: deviceDomainSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Devices ---
|
||||
|
||||
func (s *deviceService) CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) {
|
||||
propertiesJSON, err := json.Marshal(req.Properties)
|
||||
if err != nil {
|
||||
return nil, err // Consider wrapping this error for better context
|
||||
}
|
||||
|
||||
device := &models.Device{
|
||||
Name: req.Name,
|
||||
DeviceTemplateID: req.DeviceTemplateID,
|
||||
AreaControllerID: req.AreaControllerID,
|
||||
Location: req.Location,
|
||||
Properties: propertiesJSON,
|
||||
}
|
||||
|
||||
if err := device.SelfCheck(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.deviceRepo.Create(device); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createdDevice, err := s.deviceRepo.FindByID(device.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewDeviceResponse(createdDevice)
|
||||
}
|
||||
|
||||
func (s *deviceService) GetDevice(id uint) (*dto.DeviceResponse, error) {
|
||||
device, err := s.deviceRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.NewDeviceResponse(device)
|
||||
}
|
||||
|
||||
func (s *deviceService) ListDevices() ([]*dto.DeviceResponse, error) {
|
||||
devices, err := s.deviceRepo.ListAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.NewListDeviceResponse(devices)
|
||||
}
|
||||
|
||||
func (s *deviceService) UpdateDevice(id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) {
|
||||
existingDevice, err := s.deviceRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
propertiesJSON, err := json.Marshal(req.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingDevice.Name = req.Name
|
||||
existingDevice.DeviceTemplateID = req.DeviceTemplateID
|
||||
existingDevice.AreaControllerID = req.AreaControllerID
|
||||
existingDevice.Location = req.Location
|
||||
existingDevice.Properties = propertiesJSON
|
||||
|
||||
if err := existingDevice.SelfCheck(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.deviceRepo.Update(existingDevice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedDevice, err := s.deviceRepo.FindByID(existingDevice.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewDeviceResponse(updatedDevice)
|
||||
}
|
||||
|
||||
func (s *deviceService) DeleteDevice(id uint) error {
|
||||
|
||||
// 检查设备是否存在
|
||||
_, err := s.deviceRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return err // 如果未找到,会返回 gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// 在删除前检查设备是否被任务使用
|
||||
inUse, err := s.deviceRepo.IsDeviceInUse(id)
|
||||
if err != nil {
|
||||
// 如果检查过程中发生数据库错误,则返回错误
|
||||
return fmt.Errorf("检查设备使用情况失败: %w", err)
|
||||
}
|
||||
if inUse {
|
||||
// 如果设备正在被使用,则返回特定的业务错误
|
||||
return ErrDeviceInUse
|
||||
}
|
||||
|
||||
// 只有在未被使用时,才执行删除操作
|
||||
return s.deviceRepo.Delete(id)
|
||||
}
|
||||
|
||||
func (s *deviceService) ManualControl(id uint, req *dto.ManualControlDeviceRequest) error {
|
||||
dev, err := s.deviceRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Action == nil {
|
||||
return s.deviceDomainSvc.Collect(dev.AreaControllerID, []*models.Device{dev})
|
||||
} else {
|
||||
action := device.DeviceActionStart
|
||||
switch *req.Action {
|
||||
case "off":
|
||||
action = device.DeviceActionStop
|
||||
case "on":
|
||||
action = device.DeviceActionStart
|
||||
default:
|
||||
return errors.New("invalid action")
|
||||
}
|
||||
return s.deviceDomainSvc.Switch(dev, action)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Area Controllers ---
|
||||
|
||||
func (s *deviceService) CreateAreaController(req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error) {
|
||||
propertiesJSON, err := json.Marshal(req.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ac := &models.AreaController{
|
||||
Name: req.Name,
|
||||
NetworkID: req.NetworkID,
|
||||
Location: req.Location,
|
||||
Properties: propertiesJSON,
|
||||
}
|
||||
|
||||
if err := ac.SelfCheck(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.areaControllerRepo.Create(ac); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewAreaControllerResponse(ac)
|
||||
}
|
||||
|
||||
func (s *deviceService) GetAreaController(id uint) (*dto.AreaControllerResponse, error) {
|
||||
ac, err := s.areaControllerRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.NewAreaControllerResponse(ac)
|
||||
}
|
||||
|
||||
func (s *deviceService) ListAreaControllers() ([]*dto.AreaControllerResponse, error) {
|
||||
acs, err := s.areaControllerRepo.ListAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.NewListAreaControllerResponse(acs)
|
||||
}
|
||||
|
||||
func (s *deviceService) UpdateAreaController(id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) {
|
||||
existingAC, err := s.areaControllerRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
propertiesJSON, err := json.Marshal(req.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingAC.Name = req.Name
|
||||
existingAC.NetworkID = req.NetworkID
|
||||
existingAC.Location = req.Location
|
||||
existingAC.Properties = propertiesJSON
|
||||
|
||||
if err := existingAC.SelfCheck(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.areaControllerRepo.Update(existingAC); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewAreaControllerResponse(existingAC)
|
||||
}
|
||||
|
||||
func (s *deviceService) DeleteAreaController(id uint) error {
|
||||
|
||||
// 1. 检查是否存在
|
||||
_, err := s.areaControllerRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return err // 如果未找到,gorm会返回 ErrRecordNotFound
|
||||
}
|
||||
|
||||
// 2. 检查是否被使用(业务逻辑)
|
||||
inUse, err := s.deviceRepo.IsAreaControllerInUse(id)
|
||||
if err != nil {
|
||||
return err // 返回数据库检查错误
|
||||
}
|
||||
if inUse {
|
||||
return ErrAreaControllerInUse // 返回业务错误
|
||||
}
|
||||
|
||||
// 3. 执行删除
|
||||
return s.areaControllerRepo.Delete(id)
|
||||
}
|
||||
|
||||
// --- Device Templates ---
|
||||
|
||||
func (s *deviceService) CreateDeviceTemplate(req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) {
|
||||
commandsJSON, err := json.Marshal(req.Commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valuesJSON, err := json.Marshal(req.Values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deviceTemplate := &models.DeviceTemplate{
|
||||
Name: req.Name,
|
||||
Manufacturer: req.Manufacturer,
|
||||
Description: req.Description,
|
||||
Category: req.Category,
|
||||
Commands: commandsJSON,
|
||||
Values: valuesJSON,
|
||||
}
|
||||
|
||||
if err := deviceTemplate.SelfCheck(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.deviceTemplateRepo.Create(deviceTemplate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewDeviceTemplateResponse(deviceTemplate)
|
||||
}
|
||||
|
||||
func (s *deviceService) GetDeviceTemplate(id uint) (*dto.DeviceTemplateResponse, error) {
|
||||
deviceTemplate, err := s.deviceTemplateRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.NewDeviceTemplateResponse(deviceTemplate)
|
||||
}
|
||||
|
||||
func (s *deviceService) ListDeviceTemplates() ([]*dto.DeviceTemplateResponse, error) {
|
||||
deviceTemplates, err := s.deviceTemplateRepo.ListAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.NewListDeviceTemplateResponse(deviceTemplates)
|
||||
}
|
||||
|
||||
func (s *deviceService) UpdateDeviceTemplate(id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) {
|
||||
existingDeviceTemplate, err := s.deviceTemplateRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commandsJSON, err := json.Marshal(req.Commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valuesJSON, err := json.Marshal(req.Values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingDeviceTemplate.Name = req.Name
|
||||
existingDeviceTemplate.Manufacturer = req.Manufacturer
|
||||
existingDeviceTemplate.Description = req.Description
|
||||
existingDeviceTemplate.Category = req.Category
|
||||
existingDeviceTemplate.Commands = commandsJSON
|
||||
existingDeviceTemplate.Values = valuesJSON
|
||||
|
||||
if err := existingDeviceTemplate.SelfCheck(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.deviceTemplateRepo.Update(existingDeviceTemplate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewDeviceTemplateResponse(existingDeviceTemplate)
|
||||
}
|
||||
|
||||
func (s *deviceService) DeleteDeviceTemplate(id uint) error {
|
||||
|
||||
// 1. 检查是否存在
|
||||
_, err := s.deviceTemplateRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 检查是否被使用(业务逻辑)
|
||||
inUse, err := s.deviceTemplateRepo.IsInUse(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inUse {
|
||||
return ErrDeviceTemplateInUse // 返回业务错误
|
||||
}
|
||||
|
||||
// 3. 执行删除
|
||||
return s.deviceTemplateRepo.Delete(id)
|
||||
}
|
||||
@@ -1,30 +1,31 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// MonitorService 定义了监控相关的业务逻辑服务接口
|
||||
type MonitorService interface {
|
||||
ListSensorData(opts repository.SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error)
|
||||
ListDeviceCommandLogs(opts repository.DeviceCommandLogListOptions, page, pageSize int) ([]models.DeviceCommandLog, int64, error)
|
||||
ListPlanExecutionLogs(opts repository.PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, int64, error)
|
||||
ListTaskExecutionLogs(opts repository.TaskExecutionLogListOptions, page, pageSize int) ([]models.TaskExecutionLog, int64, error)
|
||||
ListPendingCollections(opts repository.PendingCollectionListOptions, page, pageSize int) ([]models.PendingCollection, int64, error)
|
||||
ListUserActionLogs(opts repository.UserActionLogListOptions, page, pageSize int) ([]models.UserActionLog, int64, error)
|
||||
ListRawMaterialPurchases(opts repository.RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error)
|
||||
ListRawMaterialStockLogs(opts repository.RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error)
|
||||
ListFeedUsageRecords(opts repository.FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error)
|
||||
ListMedicationLogs(opts repository.MedicationLogListOptions, page, pageSize int) ([]models.MedicationLog, int64, error)
|
||||
ListPigBatchLogs(opts repository.PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error)
|
||||
ListWeighingBatches(opts repository.WeighingBatchListOptions, page, pageSize int) ([]models.WeighingBatch, int64, error)
|
||||
ListWeighingRecords(opts repository.WeighingRecordListOptions, page, pageSize int) ([]models.WeighingRecord, int64, error)
|
||||
ListPigTransferLogs(opts repository.PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error)
|
||||
ListPigSickLogs(opts repository.PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error)
|
||||
ListPigPurchases(opts repository.PigPurchaseListOptions, page, pageSize int) ([]models.PigPurchase, int64, error)
|
||||
ListPigSales(opts repository.PigSaleListOptions, page, pageSize int) ([]models.PigSale, int64, error)
|
||||
ListNotifications(opts repository.NotificationListOptions, page, pageSize int) ([]models.Notification, int64, error)
|
||||
ListSensorData(req *dto.ListSensorDataRequest) (*dto.ListSensorDataResponse, error)
|
||||
ListDeviceCommandLogs(req *dto.ListDeviceCommandLogRequest) (*dto.ListDeviceCommandLogResponse, error)
|
||||
ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequest) (*dto.ListPlanExecutionLogResponse, error)
|
||||
ListTaskExecutionLogs(req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error)
|
||||
ListPendingCollections(req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error)
|
||||
ListUserActionLogs(req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error)
|
||||
ListRawMaterialPurchases(req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error)
|
||||
ListRawMaterialStockLogs(req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error)
|
||||
ListFeedUsageRecords(req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error)
|
||||
ListMedicationLogs(req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error)
|
||||
ListPigBatchLogs(req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error)
|
||||
ListWeighingBatches(req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error)
|
||||
ListWeighingRecords(req *dto.ListWeighingRecordRequest) (*dto.ListWeighingRecordResponse, error)
|
||||
ListPigTransferLogs(req *dto.ListPigTransferLogRequest) (*dto.ListPigTransferLogResponse, error)
|
||||
ListPigSickLogs(req *dto.ListPigSickLogRequest) (*dto.ListPigSickLogResponse, error)
|
||||
ListPigPurchases(req *dto.ListPigPurchaseRequest) (*dto.ListPigPurchaseResponse, error)
|
||||
ListPigSales(req *dto.ListPigSaleRequest) (*dto.ListPigSaleResponse, error)
|
||||
ListNotifications(req *dto.ListNotificationRequest) (*dto.ListNotificationResponse, error)
|
||||
}
|
||||
|
||||
// monitorService 是 MonitorService 接口的具体实现
|
||||
@@ -32,6 +33,7 @@ type monitorService struct {
|
||||
sensorDataRepo repository.SensorDataRepository
|
||||
deviceCommandLogRepo repository.DeviceCommandLogRepository
|
||||
executionLogRepo repository.ExecutionLogRepository
|
||||
planRepository repository.PlanRepository
|
||||
pendingCollectionRepo repository.PendingCollectionRepository
|
||||
userActionLogRepo repository.UserActionLogRepository
|
||||
rawMaterialRepo repository.RawMaterialRepository
|
||||
@@ -49,6 +51,7 @@ func NewMonitorService(
|
||||
sensorDataRepo repository.SensorDataRepository,
|
||||
deviceCommandLogRepo repository.DeviceCommandLogRepository,
|
||||
executionLogRepo repository.ExecutionLogRepository,
|
||||
planRepository repository.PlanRepository,
|
||||
pendingCollectionRepo repository.PendingCollectionRepository,
|
||||
userActionLogRepo repository.UserActionLogRepository,
|
||||
rawMaterialRepo repository.RawMaterialRepository,
|
||||
@@ -64,6 +67,7 @@ func NewMonitorService(
|
||||
sensorDataRepo: sensorDataRepo,
|
||||
deviceCommandLogRepo: deviceCommandLogRepo,
|
||||
executionLogRepo: executionLogRepo,
|
||||
planRepository: planRepository,
|
||||
pendingCollectionRepo: pendingCollectionRepo,
|
||||
userActionLogRepo: userActionLogRepo,
|
||||
rawMaterialRepo: rawMaterialRepo,
|
||||
@@ -78,91 +82,393 @@ func NewMonitorService(
|
||||
}
|
||||
|
||||
// ListSensorData 负责处理查询传感器数据列表的业务逻辑
|
||||
func (s *monitorService) ListSensorData(opts repository.SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) {
|
||||
return s.sensorDataRepo.List(opts, page, pageSize)
|
||||
func (s *monitorService) ListSensorData(req *dto.ListSensorDataRequest) (*dto.ListSensorDataResponse, error) {
|
||||
opts := repository.SensorDataListOptions{
|
||||
DeviceID: req.DeviceID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.SensorType != nil {
|
||||
sensorType := models.SensorType(*req.SensorType)
|
||||
opts.SensorType = &sensorType
|
||||
}
|
||||
|
||||
data, total, err := s.sensorDataRepo.List(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListSensorDataResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListDeviceCommandLogs 负责处理查询设备命令日志列表的业务逻辑
|
||||
func (s *monitorService) ListDeviceCommandLogs(opts repository.DeviceCommandLogListOptions, page, pageSize int) ([]models.DeviceCommandLog, int64, error) {
|
||||
return s.deviceCommandLogRepo.List(opts, page, pageSize)
|
||||
func (s *monitorService) ListDeviceCommandLogs(req *dto.ListDeviceCommandLogRequest) (*dto.ListDeviceCommandLogResponse, error) {
|
||||
opts := repository.DeviceCommandLogListOptions{
|
||||
DeviceID: req.DeviceID,
|
||||
ReceivedSuccess: req.ReceivedSuccess,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := s.deviceCommandLogRepo.List(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListDeviceCommandLogResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListPlanExecutionLogs 负责处理查询计划执行日志列表的业务逻辑
|
||||
func (s *monitorService) ListPlanExecutionLogs(opts repository.PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, int64, error) {
|
||||
return s.executionLogRepo.ListPlanExecutionLogs(opts, page, pageSize)
|
||||
func (s *monitorService) ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequest) (*dto.ListPlanExecutionLogResponse, error) {
|
||||
opts := repository.PlanExecutionLogListOptions{
|
||||
PlanID: req.PlanID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.ExecutionStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
planLogs, total, err := s.executionLogRepo.ListPlanExecutionLogs(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
planIds := make([]uint, 0, len(planLogs))
|
||||
for _, datum := range planLogs {
|
||||
has := false
|
||||
for _, id := range planIds {
|
||||
if id == datum.PlanID {
|
||||
has = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !has {
|
||||
planIds = append(planIds, datum.PlanID)
|
||||
}
|
||||
}
|
||||
plans, err := s.planRepository.GetPlansByIDs(planIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.NewListPlanExecutionLogResponse(planLogs, plans, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListTaskExecutionLogs 负责处理查询任务执行日志列表的业务逻辑
|
||||
func (s *monitorService) ListTaskExecutionLogs(opts repository.TaskExecutionLogListOptions, page, pageSize int) ([]models.TaskExecutionLog, int64, error) {
|
||||
return s.executionLogRepo.ListTaskExecutionLogs(opts, page, pageSize)
|
||||
func (s *monitorService) ListTaskExecutionLogs(req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error) {
|
||||
opts := repository.TaskExecutionLogListOptions{
|
||||
PlanExecutionLogID: req.PlanExecutionLogID,
|
||||
TaskID: req.TaskID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.ExecutionStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
data, total, err := s.executionLogRepo.ListTaskExecutionLogs(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListTaskExecutionLogResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListPendingCollections 负责处理查询待采集请求列表的业务逻辑
|
||||
func (s *monitorService) ListPendingCollections(opts repository.PendingCollectionListOptions, page, pageSize int) ([]models.PendingCollection, int64, error) {
|
||||
return s.pendingCollectionRepo.List(opts, page, pageSize)
|
||||
func (s *monitorService) ListPendingCollections(req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error) {
|
||||
opts := repository.PendingCollectionListOptions{
|
||||
DeviceID: req.DeviceID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.PendingCollectionStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
data, total, err := s.pendingCollectionRepo.List(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListPendingCollectionResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListUserActionLogs 负责处理查询用户操作日志列表的业务逻辑
|
||||
func (s *monitorService) ListUserActionLogs(opts repository.UserActionLogListOptions, page, pageSize int) ([]models.UserActionLog, int64, error) {
|
||||
return s.userActionLogRepo.List(opts, page, pageSize)
|
||||
func (s *monitorService) ListUserActionLogs(req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error) {
|
||||
opts := repository.UserActionLogListOptions{
|
||||
UserID: req.UserID,
|
||||
Username: req.Username,
|
||||
ActionType: req.ActionType,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Status != nil {
|
||||
status := models.AuditStatus(*req.Status)
|
||||
opts.Status = &status
|
||||
}
|
||||
|
||||
data, total, err := s.userActionLogRepo.List(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListRawMaterialPurchases 负责处理查询原料采购记录列表的业务逻辑
|
||||
func (s *monitorService) ListRawMaterialPurchases(opts repository.RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error) {
|
||||
return s.rawMaterialRepo.ListRawMaterialPurchases(opts, page, pageSize)
|
||||
func (s *monitorService) ListRawMaterialPurchases(req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error) {
|
||||
opts := repository.RawMaterialPurchaseListOptions{
|
||||
RawMaterialID: req.RawMaterialID,
|
||||
Supplier: req.Supplier,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := s.rawMaterialRepo.ListRawMaterialPurchases(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListRawMaterialPurchaseResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListRawMaterialStockLogs 负责处理查询原料库存日志列表的业务逻辑
|
||||
func (s *monitorService) ListRawMaterialStockLogs(opts repository.RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) {
|
||||
return s.rawMaterialRepo.ListRawMaterialStockLogs(opts, page, pageSize)
|
||||
func (s *monitorService) ListRawMaterialStockLogs(req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error) {
|
||||
opts := repository.RawMaterialStockLogListOptions{
|
||||
RawMaterialID: req.RawMaterialID,
|
||||
SourceID: req.SourceID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.SourceType != nil {
|
||||
sourceType := models.StockLogSourceType(*req.SourceType)
|
||||
opts.SourceType = &sourceType
|
||||
}
|
||||
|
||||
data, total, err := s.rawMaterialRepo.ListRawMaterialStockLogs(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListRawMaterialStockLogResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListFeedUsageRecords 负责处理查询饲料使用记录列表的业务逻辑
|
||||
func (s *monitorService) ListFeedUsageRecords(opts repository.FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error) {
|
||||
return s.rawMaterialRepo.ListFeedUsageRecords(opts, page, pageSize)
|
||||
func (s *monitorService) ListFeedUsageRecords(req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error) {
|
||||
opts := repository.FeedUsageRecordListOptions{
|
||||
PenID: req.PenID,
|
||||
FeedFormulaID: req.FeedFormulaID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := s.rawMaterialRepo.ListFeedUsageRecords(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListFeedUsageRecordResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListMedicationLogs 负责处理查询用药记录列表的业务逻辑
|
||||
func (s *monitorService) ListMedicationLogs(opts repository.MedicationLogListOptions, page, pageSize int) ([]models.MedicationLog, int64, error) {
|
||||
return s.medicationRepo.ListMedicationLogs(opts, page, pageSize)
|
||||
func (s *monitorService) ListMedicationLogs(req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error) {
|
||||
opts := repository.MedicationLogListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
MedicationID: req.MedicationID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Reason != nil {
|
||||
reason := models.MedicationReasonType(*req.Reason)
|
||||
opts.Reason = &reason
|
||||
}
|
||||
|
||||
data, total, err := s.medicationRepo.ListMedicationLogs(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListMedicationLogResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListPigBatchLogs 负责处理查询猪批次日志列表的业务逻辑
|
||||
func (s *monitorService) ListPigBatchLogs(opts repository.PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error) {
|
||||
return s.pigBatchLogRepo.List(opts, page, pageSize)
|
||||
func (s *monitorService) ListPigBatchLogs(req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error) {
|
||||
opts := repository.PigBatchLogListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.ChangeType != nil {
|
||||
changeType := models.LogChangeType(*req.ChangeType)
|
||||
opts.ChangeType = &changeType
|
||||
}
|
||||
|
||||
data, total, err := s.pigBatchLogRepo.List(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListPigBatchLogResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListWeighingBatches 负责处理查询批次称重记录列表的业务逻辑
|
||||
func (s *monitorService) ListWeighingBatches(opts repository.WeighingBatchListOptions, page, pageSize int) ([]models.WeighingBatch, int64, error) {
|
||||
return s.pigBatchRepo.ListWeighingBatches(opts, page, pageSize)
|
||||
func (s *monitorService) ListWeighingBatches(req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error) {
|
||||
opts := repository.WeighingBatchListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := s.pigBatchRepo.ListWeighingBatches(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListWeighingBatchResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListWeighingRecords 负责处理查询单次称重记录列表的业务逻辑
|
||||
func (s *monitorService) ListWeighingRecords(opts repository.WeighingRecordListOptions, page, pageSize int) ([]models.WeighingRecord, int64, error) {
|
||||
return s.pigBatchRepo.ListWeighingRecords(opts, page, pageSize)
|
||||
func (s *monitorService) ListWeighingRecords(req *dto.ListWeighingRecordRequest) (*dto.ListWeighingRecordResponse, error) {
|
||||
opts := repository.WeighingRecordListOptions{
|
||||
WeighingBatchID: req.WeighingBatchID,
|
||||
PenID: req.PenID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := s.pigBatchRepo.ListWeighingRecords(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListWeighingRecordResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListPigTransferLogs 负责处理查询猪只迁移日志列表的业务逻辑
|
||||
func (s *monitorService) ListPigTransferLogs(opts repository.PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error) {
|
||||
return s.pigTransferLogRepo.ListPigTransferLogs(opts, page, pageSize)
|
||||
func (s *monitorService) ListPigTransferLogs(req *dto.ListPigTransferLogRequest) (*dto.ListPigTransferLogResponse, error) {
|
||||
opts := repository.PigTransferLogListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
PenID: req.PenID,
|
||||
OperatorID: req.OperatorID,
|
||||
CorrelationID: req.CorrelationID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.TransferType != nil {
|
||||
transferType := models.PigTransferType(*req.TransferType)
|
||||
opts.TransferType = &transferType
|
||||
}
|
||||
|
||||
data, total, err := s.pigTransferLogRepo.ListPigTransferLogs(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListPigTransferLogResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListPigSickLogs 负责处理查询病猪日志列表的业务逻辑
|
||||
func (s *monitorService) ListPigSickLogs(opts repository.PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error) {
|
||||
return s.pigSickLogRepo.ListPigSickLogs(opts, page, pageSize)
|
||||
func (s *monitorService) ListPigSickLogs(req *dto.ListPigSickLogRequest) (*dto.ListPigSickLogResponse, error) {
|
||||
opts := repository.PigSickLogListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
PenID: req.PenID,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
if req.Reason != nil {
|
||||
reason := models.PigBatchSickPigReasonType(*req.Reason)
|
||||
opts.Reason = &reason
|
||||
}
|
||||
if req.TreatmentLocation != nil {
|
||||
treatmentLocation := models.PigBatchSickPigTreatmentLocation(*req.TreatmentLocation)
|
||||
opts.TreatmentLocation = &treatmentLocation
|
||||
}
|
||||
|
||||
data, total, err := s.pigSickLogRepo.ListPigSickLogs(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListPigSickLogResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListPigPurchases 负责处理查询猪只采购记录列表的业务逻辑
|
||||
func (s *monitorService) ListPigPurchases(opts repository.PigPurchaseListOptions, page, pageSize int) ([]models.PigPurchase, int64, error) {
|
||||
return s.pigTradeRepo.ListPigPurchases(opts, page, pageSize)
|
||||
func (s *monitorService) ListPigPurchases(req *dto.ListPigPurchaseRequest) (*dto.ListPigPurchaseResponse, error) {
|
||||
opts := repository.PigPurchaseListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
Supplier: req.Supplier,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := s.pigTradeRepo.ListPigPurchases(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListPigPurchaseResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListPigSales 负责处理查询猪只销售记录列表的业务逻辑
|
||||
func (s *monitorService) ListPigSales(opts repository.PigSaleListOptions, page, pageSize int) ([]models.PigSale, int64, error) {
|
||||
return s.pigTradeRepo.ListPigSales(opts, page, pageSize)
|
||||
func (s *monitorService) ListPigSales(req *dto.ListPigSaleRequest) (*dto.ListPigSaleResponse, error) {
|
||||
opts := repository.PigSaleListOptions{
|
||||
PigBatchID: req.PigBatchID,
|
||||
Buyer: req.Buyer,
|
||||
OperatorID: req.OperatorID,
|
||||
OrderBy: req.OrderBy,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
data, total, err := s.pigTradeRepo.ListPigSales(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListPigSaleResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListNotifications 负责处理查询通知列表的业务逻辑
|
||||
func (s *monitorService) ListNotifications(opts repository.NotificationListOptions, page, pageSize int) ([]models.Notification, int64, error) {
|
||||
return s.notificationRepo.List(opts, page, pageSize)
|
||||
func (s *monitorService) ListNotifications(req *dto.ListNotificationRequest) (*dto.ListNotificationResponse, error) {
|
||||
opts := repository.NotificationListOptions{
|
||||
UserID: req.UserID,
|
||||
NotifierType: req.NotifierType,
|
||||
Level: req.Level,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
OrderBy: req.OrderBy,
|
||||
Status: req.Status,
|
||||
}
|
||||
|
||||
data, total, err := s.notificationRepo.List(opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListNotificationResponse(data, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
@@ -16,20 +16,20 @@ import (
|
||||
// PigFarmService 提供了猪场资产管理的业务逻辑
|
||||
type PigFarmService interface {
|
||||
// PigHouse methods
|
||||
CreatePigHouse(name, description string) (*models.PigHouse, error)
|
||||
GetPigHouseByID(id uint) (*models.PigHouse, error)
|
||||
ListPigHouses() ([]models.PigHouse, error)
|
||||
UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error)
|
||||
CreatePigHouse(name, description string) (*dto.PigHouseResponse, error)
|
||||
GetPigHouseByID(id uint) (*dto.PigHouseResponse, error)
|
||||
ListPigHouses() ([]dto.PigHouseResponse, error)
|
||||
UpdatePigHouse(id uint, name, description string) (*dto.PigHouseResponse, error)
|
||||
DeletePigHouse(id uint) error
|
||||
|
||||
// Pen methods
|
||||
CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error)
|
||||
CreatePen(penNumber string, houseID uint, capacity int) (*dto.PenResponse, error)
|
||||
GetPenByID(id uint) (*dto.PenResponse, error)
|
||||
ListPens() ([]*dto.PenResponse, error)
|
||||
UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error)
|
||||
UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error)
|
||||
DeletePen(id uint) error
|
||||
// UpdatePenStatus 更新猪栏状态
|
||||
UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error)
|
||||
UpdatePenStatus(id uint, newStatus models.PenStatus) (*dto.PenResponse, error)
|
||||
}
|
||||
|
||||
type pigFarmService struct {
|
||||
@@ -60,24 +60,51 @@ func NewPigFarmService(farmRepository repository.PigFarmRepository,
|
||||
|
||||
// --- PigHouse Implementation ---
|
||||
|
||||
func (s *pigFarmService) CreatePigHouse(name, description string) (*models.PigHouse, error) {
|
||||
func (s *pigFarmService) CreatePigHouse(name, description string) (*dto.PigHouseResponse, error) {
|
||||
house := &models.PigHouse{
|
||||
Name: name,
|
||||
Description: description,
|
||||
}
|
||||
err := s.farmRepository.CreatePigHouse(house)
|
||||
return house, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.PigHouseResponse{
|
||||
ID: house.ID,
|
||||
Name: house.Name,
|
||||
Description: house.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) GetPigHouseByID(id uint) (*models.PigHouse, error) {
|
||||
return s.farmRepository.GetPigHouseByID(id)
|
||||
func (s *pigFarmService) GetPigHouseByID(id uint) (*dto.PigHouseResponse, error) {
|
||||
house, err := s.farmRepository.GetPigHouseByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.PigHouseResponse{
|
||||
ID: house.ID,
|
||||
Name: house.Name,
|
||||
Description: house.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) ListPigHouses() ([]models.PigHouse, error) {
|
||||
return s.farmRepository.ListPigHouses()
|
||||
func (s *pigFarmService) ListPigHouses() ([]dto.PigHouseResponse, error) {
|
||||
houses, err := s.farmRepository.ListPigHouses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp []dto.PigHouseResponse
|
||||
for _, house := range houses {
|
||||
resp = append(resp, dto.PigHouseResponse{
|
||||
ID: house.ID,
|
||||
Name: house.Name,
|
||||
Description: house.Description,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error) {
|
||||
func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*dto.PigHouseResponse, error) {
|
||||
house := &models.PigHouse{
|
||||
Model: gorm.Model{ID: id},
|
||||
Name: name,
|
||||
@@ -91,7 +118,15 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*mod
|
||||
return nil, ErrHouseNotFound
|
||||
}
|
||||
// 返回更新后的完整信息
|
||||
return s.farmRepository.GetPigHouseByID(id)
|
||||
updatedHouse, err := s.farmRepository.GetPigHouseByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.PigHouseResponse{
|
||||
ID: updatedHouse.ID,
|
||||
Name: updatedHouse.Name,
|
||||
Description: updatedHouse.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) DeletePigHouse(id uint) error {
|
||||
@@ -117,7 +152,7 @@ func (s *pigFarmService) DeletePigHouse(id uint) error {
|
||||
|
||||
// --- Pen Implementation ---
|
||||
|
||||
func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error) {
|
||||
func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*dto.PenResponse, error) {
|
||||
// 业务逻辑:验证所属猪舍是否存在
|
||||
_, err := s.farmRepository.GetPigHouseByID(houseID)
|
||||
if err != nil {
|
||||
@@ -134,7 +169,16 @@ func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int)
|
||||
Status: models.PenStatusEmpty,
|
||||
}
|
||||
err = s.penRepository.CreatePen(pen)
|
||||
return pen, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.PenResponse{
|
||||
ID: pen.ID,
|
||||
PenNumber: pen.PenNumber,
|
||||
HouseID: pen.HouseID,
|
||||
Capacity: pen.Capacity,
|
||||
Status: pen.Status,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) GetPenByID(id uint) (*dto.PenResponse, error) {
|
||||
@@ -197,7 +241,7 @@ func (s *pigFarmService) ListPens() ([]*dto.PenResponse, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) {
|
||||
func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error) {
|
||||
// 业务逻辑:验证所属猪舍是否存在
|
||||
_, err := s.farmRepository.GetPigHouseByID(houseID)
|
||||
if err != nil {
|
||||
@@ -222,7 +266,18 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa
|
||||
return nil, ErrPenNotFound
|
||||
}
|
||||
// 返回更新后的完整信息
|
||||
return s.penRepository.GetPenByID(id)
|
||||
updatedPen, err := s.penRepository.GetPenByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.PenResponse{
|
||||
ID: updatedPen.ID,
|
||||
PenNumber: updatedPen.PenNumber,
|
||||
HouseID: updatedPen.HouseID,
|
||||
Capacity: updatedPen.Capacity,
|
||||
Status: updatedPen.Status,
|
||||
PigBatchID: updatedPen.PigBatchID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) DeletePen(id uint) error {
|
||||
@@ -260,7 +315,7 @@ func (s *pigFarmService) DeletePen(id uint) error {
|
||||
}
|
||||
|
||||
// UpdatePenStatus 更新猪栏状态
|
||||
func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error) {
|
||||
func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*dto.PenResponse, error) {
|
||||
var updatedPen *models.Pen
|
||||
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
|
||||
pen, err := s.penRepository.GetPenByIDTx(tx, id)
|
||||
@@ -310,5 +365,12 @@ func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updatedPen, nil
|
||||
return &dto.PenResponse{
|
||||
ID: updatedPen.ID,
|
||||
PenNumber: updatedPen.PenNumber,
|
||||
HouseID: updatedPen.HouseID,
|
||||
Capacity: updatedPen.Capacity,
|
||||
Status: updatedPen.Status,
|
||||
PigBatchID: updatedPen.PigBatchID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
205
internal/app/service/plan_service.go
Normal file
205
internal/app/service/plan_service.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// PlanService 定义了计划相关的应用服务接口
|
||||
type PlanService interface {
|
||||
// CreatePlan 创建一个新的计划
|
||||
CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error)
|
||||
// GetPlanByID 根据ID获取计划详情
|
||||
GetPlanByID(id uint) (*dto.PlanResponse, error)
|
||||
// ListPlans 获取计划列表,支持过滤和分页
|
||||
ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error)
|
||||
// UpdatePlan 更新计划
|
||||
UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error)
|
||||
// DeletePlan 删除计划(软删除)
|
||||
DeletePlan(id uint) error
|
||||
// StartPlan 启动计划
|
||||
StartPlan(id uint) error
|
||||
// StopPlan 停止计划
|
||||
StopPlan(id uint) error
|
||||
}
|
||||
|
||||
// planService 是 PlanService 接口的实现
|
||||
type planService struct {
|
||||
logger *logs.Logger
|
||||
domainPlanService plan.Service // 替换为领域层的服务接口
|
||||
}
|
||||
|
||||
// NewPlanService 创建一个新的 PlanService 实例
|
||||
func NewPlanService(
|
||||
logger *logs.Logger,
|
||||
domainPlanService plan.Service, // 接收领域层服务
|
||||
) PlanService {
|
||||
return &planService{
|
||||
logger: logger,
|
||||
domainPlanService: domainPlanService, // 注入领域层服务
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePlan 创建一个新的计划
|
||||
func (s *planService) CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error) {
|
||||
const actionType = "应用服务层:创建计划"
|
||||
|
||||
// 使用 DTO 转换函数将请求转换为领域实体
|
||||
planToCreate, err := dto.NewPlanFromCreateRequest(req)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 调用领域服务创建计划
|
||||
createdPlan, err := s.domainPlanService.CreatePlan(planToCreate)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 领域服务创建计划失败: %v", actionType, err)
|
||||
return nil, err // 直接返回领域层错误
|
||||
}
|
||||
|
||||
// 将领域实体转换为响应 DTO
|
||||
resp, err := dto.NewPlanToResponse(createdPlan)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, createdPlan)
|
||||
return nil, errors.New("计划创建成功,但响应生成失败")
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, createdPlan.ID)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetPlanByID 根据ID获取计划详情
|
||||
func (s *planService) GetPlanByID(id uint) (*dto.PlanResponse, error) {
|
||||
const actionType = "应用服务层:获取计划详情"
|
||||
|
||||
// 调用领域服务获取计划
|
||||
plan, err := s.domainPlanService.GetPlanByID(id)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 领域服务获取计划详情失败: %v, ID: %d", actionType, err, id)
|
||||
return nil, err // 直接返回领域层错误
|
||||
}
|
||||
|
||||
// 将领域实体转换为响应 DTO
|
||||
resp, err := dto.NewPlanToResponse(plan)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
|
||||
return nil, errors.New("获取计划详情失败: 内部数据格式错误")
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListPlans 获取计划列表,支持过滤和分页
|
||||
func (s *planService) ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error) {
|
||||
const actionType = "应用服务层:获取计划列表"
|
||||
|
||||
// 将 DTO 查询参数转换为领域层可接受的选项
|
||||
opts := repository.ListPlansOptions{PlanType: query.PlanType}
|
||||
|
||||
// 调用领域服务获取计划列表
|
||||
plans, total, err := s.domainPlanService.ListPlans(opts, query.Page, query.PageSize)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 领域服务获取计划列表失败: %v", actionType, err)
|
||||
return nil, err // 直接返回领域层错误
|
||||
}
|
||||
|
||||
// 将领域实体列表转换为响应 DTO 列表
|
||||
planResponses := make([]dto.PlanResponse, 0, len(plans))
|
||||
for _, p := range plans {
|
||||
resp, err := dto.NewPlanToResponse(&p)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 序列化单个计划响应失败: %v, Plan: %+v", actionType, err, p)
|
||||
// 这里选择跳过有问题的计划,并记录错误,而不是中断整个列表的返回
|
||||
continue
|
||||
}
|
||||
planResponses = append(planResponses, *resp)
|
||||
}
|
||||
|
||||
resp := &dto.ListPlansResponse{
|
||||
Plans: planResponses,
|
||||
Total: total,
|
||||
}
|
||||
s.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses))
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UpdatePlan 更新计划
|
||||
func (s *planService) UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error) {
|
||||
const actionType = "应用服务层:更新计划"
|
||||
|
||||
// 使用 DTO 转换函数将请求转换为领域实体
|
||||
planToUpdate, err := dto.NewPlanFromUpdateRequest(req)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
|
||||
return nil, err
|
||||
}
|
||||
planToUpdate.ID = id // 确保ID被设置
|
||||
|
||||
// 调用领域服务更新计划
|
||||
updatedPlan, err := s.domainPlanService.UpdatePlan(planToUpdate)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 领域服务更新计划失败: %v, ID: %d", actionType, err, id)
|
||||
return nil, err // 直接返回领域层错误
|
||||
}
|
||||
|
||||
// 将领域实体转换为响应 DTO
|
||||
resp, err := dto.NewPlanToResponse(updatedPlan)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
|
||||
return nil, errors.New("计划更新成功,但响应生成失败")
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeletePlan 删除计划(软删除)
|
||||
func (s *planService) DeletePlan(id uint) error {
|
||||
const actionType = "应用服务层:删除计划"
|
||||
|
||||
// 调用领域服务删除计划
|
||||
err := s.domainPlanService.DeletePlan(id)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 领域服务删除计划失败: %v, ID: %d", actionType, err, id)
|
||||
return err // 直接返回领域层错误
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPlan 启动计划
|
||||
func (s *planService) StartPlan(id uint) error {
|
||||
const actionType = "应用服务层:启动计划"
|
||||
|
||||
// 调用领域服务启动计划
|
||||
err := s.domainPlanService.StartPlan(id)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 领域服务启动计划失败: %v, ID: %d", actionType, err, id)
|
||||
return err // 直接返回领域层错误
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopPlan 停止计划
|
||||
func (s *planService) StopPlan(id uint) error {
|
||||
const actionType = "应用服务层:停止计划"
|
||||
|
||||
// 调用领域服务停止计划
|
||||
err := s.domainPlanService.StopPlan(id)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 领域服务停止计划失败: %v, ID: %d", actionType, err, id)
|
||||
return err // 直接返回领域层错误
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
|
||||
return nil
|
||||
}
|
||||
110
internal/app/service/user_service.go
Normal file
110
internal/app/service/user_service.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserService 定义用户服务接口
|
||||
type UserService interface {
|
||||
CreateUser(req *dto.CreateUserRequest) (*dto.CreateUserResponse, error)
|
||||
Login(req *dto.LoginRequest) (*dto.LoginResponse, error)
|
||||
SendTestNotification(userID uint, req *dto.SendTestNotificationRequest) error
|
||||
}
|
||||
|
||||
// userService 实现了 UserService 接口
|
||||
type userService struct {
|
||||
userRepo repository.UserRepository
|
||||
tokenService token.Service
|
||||
notifyService domain_notify.Service
|
||||
logger *logs.Logger
|
||||
}
|
||||
|
||||
// NewUserService 创建并返回一个新的 UserService 实例
|
||||
func NewUserService(
|
||||
userRepo repository.UserRepository,
|
||||
tokenService token.Service,
|
||||
notifyService domain_notify.Service,
|
||||
logger *logs.Logger,
|
||||
) UserService {
|
||||
return &userService{
|
||||
userRepo: userRepo,
|
||||
tokenService: tokenService,
|
||||
notifyService: notifyService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser 创建新用户
|
||||
func (s *userService) CreateUser(req *dto.CreateUserRequest) (*dto.CreateUserResponse, error) {
|
||||
user := &models.User{
|
||||
Username: req.Username,
|
||||
Password: req.Password, // 密码会在 BeforeSave 钩子中哈希
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
s.logger.Errorf("创建用户: 创建用户失败: %v", err)
|
||||
|
||||
// 尝试查询用户,以判断是否是用户名重复导致的错误
|
||||
_, findErr := s.userRepo.FindByUsername(req.Username)
|
||||
if findErr == nil { // 如果能找到用户,说明是用户名重复
|
||||
return nil, errors.New("用户名已存在")
|
||||
}
|
||||
|
||||
// 其他创建失败的情况
|
||||
return nil, errors.New("创建用户失败")
|
||||
}
|
||||
|
||||
return &dto.CreateUserResponse{
|
||||
Username: user.Username,
|
||||
ID: user.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *userService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
|
||||
// 使用新的方法,通过唯一标识符(用户名、邮箱等)查找用户
|
||||
user, err := s.userRepo.FindUserForLogin(req.Identifier)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("登录凭证不正确")
|
||||
}
|
||||
s.logger.Errorf("登录: 查询用户失败: %v", err)
|
||||
return nil, errors.New("登录失败")
|
||||
}
|
||||
|
||||
if !user.CheckPassword(req.Password) {
|
||||
return nil, errors.New("登录凭证不正确")
|
||||
}
|
||||
|
||||
// 登录成功,生成 JWT token
|
||||
tokenString, err := s.tokenService.GenerateToken(user.ID)
|
||||
if err != nil {
|
||||
s.logger.Errorf("登录: 生成令牌失败: %v", err)
|
||||
return nil, errors.New("登录失败,无法生成认证信息")
|
||||
}
|
||||
|
||||
return &dto.LoginResponse{
|
||||
Username: user.Username,
|
||||
ID: user.ID,
|
||||
Token: tokenString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendTestNotification 发送测试通知
|
||||
func (s *userService) SendTestNotification(userID uint, req *dto.SendTestNotificationRequest) error {
|
||||
err := s.notifyService.SendTestMessage(userID, req.Type)
|
||||
if err != nil {
|
||||
s.logger.Errorf("发送测试通知: 服务层调用失败: %v", err)
|
||||
return errors.New("发送测试消息失败: " + err.Error())
|
||||
}
|
||||
s.logger.Infof("发送测试通知: 成功为用户 %d 发送类型为 %s 的测试消息", userID, req.Type)
|
||||
return nil
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/api"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
// Application 是整个应用的核心,封装了所有组件和生命周期。
|
||||
@@ -45,20 +44,16 @@ func NewApplication(configPath string) (*Application, error) {
|
||||
apiServer := api.NewAPI(
|
||||
cfg.Server,
|
||||
logger,
|
||||
infra.Repos.UserRepo,
|
||||
infra.Repos.DeviceRepo,
|
||||
infra.Repos.AreaControllerRepo,
|
||||
infra.Repos.DeviceTemplateRepo,
|
||||
infra.Repos.PlanRepo,
|
||||
appServices.PigFarmService,
|
||||
appServices.PigBatchService,
|
||||
appServices.MonitorService,
|
||||
infra.TokenService,
|
||||
appServices.AuditService,
|
||||
infra.NotifyService,
|
||||
domain.GeneralDeviceService,
|
||||
infra.Lora.ListenHandler,
|
||||
domain.AnalysisPlanTaskManager,
|
||||
infra.repos.userRepo,
|
||||
appServices.pigFarmService,
|
||||
appServices.pigBatchService,
|
||||
appServices.monitorService,
|
||||
appServices.deviceService,
|
||||
appServices.planService,
|
||||
appServices.userService,
|
||||
infra.tokenService,
|
||||
appServices.auditService,
|
||||
infra.lora.listenHandler,
|
||||
)
|
||||
|
||||
// 4. 组装 Application 对象
|
||||
@@ -79,7 +74,7 @@ func (app *Application) Start() error {
|
||||
app.Logger.Info("应用启动中...")
|
||||
|
||||
// 1. 启动底层监听器
|
||||
if err := app.Infra.Lora.LoraListener.Listen(); err != nil {
|
||||
if err := app.Infra.lora.loraListener.Listen(); err != nil {
|
||||
return fmt.Errorf("启动 LoRa Mesh 监听器失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -89,8 +84,7 @@ func (app *Application) Start() error {
|
||||
}
|
||||
|
||||
// 3. 启动后台工作协程
|
||||
app.Domain.Scheduler.Start()
|
||||
app.Domain.TimedCollector.Start()
|
||||
app.Domain.planService.Start()
|
||||
|
||||
// 4. 启动 API 服务器
|
||||
app.API.Start()
|
||||
@@ -112,18 +106,15 @@ func (app *Application) Stop() error {
|
||||
app.API.Stop()
|
||||
|
||||
// 关闭任务执行器
|
||||
app.Domain.Scheduler.Stop()
|
||||
|
||||
// 关闭定时采集器
|
||||
app.Domain.TimedCollector.Stop()
|
||||
app.Domain.planService.Stop()
|
||||
|
||||
// 断开数据库连接
|
||||
if err := app.Infra.Storage.Disconnect(); err != nil {
|
||||
if err := app.Infra.storage.Disconnect(); err != nil {
|
||||
app.Logger.Errorw("数据库连接断开失败", "error", err)
|
||||
}
|
||||
|
||||
// 关闭 LoRa Mesh 监听器
|
||||
if err := app.Infra.Lora.LoraListener.Stop(); err != nil {
|
||||
if err := app.Infra.lora.loraListener.Stop(); err != nil {
|
||||
app.Logger.Errorw("LoRa Mesh 监听器关闭失败", "error", err)
|
||||
}
|
||||
|
||||
@@ -133,133 +124,3 @@ func (app *Application) Stop() error {
|
||||
app.Logger.Info("应用已成功关闭")
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeState 在应用启动时准备其初始数据状态。
|
||||
// 这包括清理任何因上次异常关闭而留下的悬空任务或请求。
|
||||
func (app *Application) initializeState() error {
|
||||
// 清理待采集任务 (非致命错误)
|
||||
if err := app.initializePendingCollections(); err != nil {
|
||||
app.Logger.Errorw("清理待采集任务时发生非致命错误", "error", err)
|
||||
}
|
||||
|
||||
// 初始化待执行任务列表 (致命错误)
|
||||
if err := app.initializePendingTasks(); err != nil {
|
||||
return fmt.Errorf("初始化待执行任务列表失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializePendingCollections 在应用启动时处理所有未完成的采集请求。
|
||||
// 我们的策略是:任何在程序重启前仍处于“待处理”状态的请求,都应被视为已失败。
|
||||
// 这保证了系统在每次启动时都处于一个干净、确定的状态。
|
||||
func (app *Application) initializePendingCollections() error {
|
||||
app.Logger.Info("开始清理所有未完成的采集请求...")
|
||||
|
||||
// 直接将所有 'pending' 状态的请求更新为 'timed_out'。
|
||||
count, err := app.Infra.Repos.PendingCollectionRepo.MarkAllPendingAsTimedOut()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理未完成的采集请求失败: %v", err)
|
||||
} else if count > 0 {
|
||||
app.Logger.Infof("成功将 %d 个未完成的采集请求标记为超时。", count)
|
||||
} else {
|
||||
app.Logger.Info("没有需要清理的采集请求。")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializePendingTasks 在应用启动时清理并刷新待执行任务列表。
|
||||
func (app *Application) initializePendingTasks() error {
|
||||
logger := app.Logger
|
||||
planRepo := app.Infra.Repos.PlanRepo
|
||||
pendingTaskRepo := app.Infra.Repos.PendingTaskRepo
|
||||
executionLogRepo := app.Infra.Repos.ExecutionLogRepo
|
||||
analysisPlanTaskManager := app.Domain.AnalysisPlanTaskManager
|
||||
|
||||
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.PlanStatusStopped
|
||||
logger.Infof("计划 #%d 状态已更新为 '执行完毕'。", plan.ID)
|
||||
|
||||
}
|
||||
// 保存更新后的计划
|
||||
if err := planRepo.UpdatePlan(plan); err != nil {
|
||||
logger.Errorf("修正计划 #%d 状态失败: %v", plan.ID, err)
|
||||
// 这是一个非阻塞性错误,继续处理其他计划
|
||||
}
|
||||
}
|
||||
logger.Info("阶段一:固定次数计划修正完成。")
|
||||
|
||||
// 阶段二:清理所有待执行任务和相关日志
|
||||
logger.Info("阶段二:开始清理所有待执行任务和相关日志...")
|
||||
|
||||
// --- 新增逻辑:处理因崩溃导致状态不一致的计划主表状态 ---
|
||||
// 1. 查找所有未完成的计划执行日志 (状态为 Started 或 Waiting)
|
||||
incompletePlanLogs, err := executionLogRepo.FindIncompletePlanExecutionLogs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找未完成的计划执行日志失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 收集所有受影响的唯一 PlanID
|
||||
affectedPlanIDs := make(map[uint]struct{})
|
||||
for _, log := range incompletePlanLogs {
|
||||
affectedPlanIDs[log.PlanID] = struct{}{}
|
||||
}
|
||||
|
||||
// 3. 对于每个受影响的 PlanID,重置其 execute_count 并将其状态设置为 Failed
|
||||
for planID := range affectedPlanIDs {
|
||||
logger.Warnf("检测到计划 #%d 在应用崩溃前处于未完成状态,将重置其计数并标记为失败。", planID)
|
||||
// 使用 UpdatePlanStateAfterExecution 来更新主表状态,避免影响关联数据
|
||||
if err := planRepo.UpdatePlanStateAfterExecution(planID, 0, models.PlanStatusFailed); err != nil {
|
||||
logger.Errorf("重置计划 #%d 计数并标记为失败时出错: %v", planID, err)
|
||||
// 这是一个非阻塞性错误,继续处理其他计划
|
||||
}
|
||||
}
|
||||
logger.Info("阶段二:计划主表状态修正完成。")
|
||||
|
||||
// 直接调用新的方法来更新计划执行日志状态为失败
|
||||
if err := executionLogRepo.FailAllIncompletePlanExecutionLogs(); err != nil {
|
||||
logger.Errorf("更新所有未完成计划执行日志状态为失败失败: %v", err)
|
||||
// 这是一个非阻塞性错误,继续执行
|
||||
}
|
||||
|
||||
// 直接调用新的方法来更新任务执行日志状态为取消
|
||||
if err := executionLogRepo.CancelAllIncompleteTaskExecutionLogs(); 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
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/collection"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
|
||||
@@ -26,11 +26,11 @@ import (
|
||||
|
||||
// Infrastructure 聚合了所有基础设施层的组件。
|
||||
type Infrastructure struct {
|
||||
Storage database.Storage
|
||||
Repos *Repositories
|
||||
Lora *LoraComponents
|
||||
NotifyService domain_notify.Service
|
||||
TokenService token.Service
|
||||
storage database.Storage
|
||||
repos *Repositories
|
||||
lora *LoraComponents
|
||||
notifyService domain_notify.Service
|
||||
tokenService token.Service
|
||||
}
|
||||
|
||||
// initInfrastructure 初始化所有基础设施层组件。
|
||||
@@ -47,7 +47,7 @@ func initInfrastructure(cfg *config.Config, logger *logs.Logger) (*Infrastructur
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyService, err := initNotifyService(cfg.Notify, logger, repos.UserRepo, repos.NotificationRepo)
|
||||
notifyService, err := initNotifyService(cfg.Notify, logger, repos.userRepo, repos.notificationRepo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化通知服务失败: %w", err)
|
||||
}
|
||||
@@ -55,177 +55,200 @@ func initInfrastructure(cfg *config.Config, logger *logs.Logger) (*Infrastructur
|
||||
tokenService := token.NewTokenService([]byte(cfg.App.JWTSecret))
|
||||
|
||||
return &Infrastructure{
|
||||
Storage: storage,
|
||||
Repos: repos,
|
||||
Lora: lora,
|
||||
NotifyService: notifyService,
|
||||
TokenService: tokenService,
|
||||
storage: storage,
|
||||
repos: repos,
|
||||
lora: lora,
|
||||
notifyService: notifyService,
|
||||
tokenService: tokenService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Repositories 聚合了所有的仓库实例。
|
||||
type Repositories struct {
|
||||
UserRepo repository.UserRepository
|
||||
DeviceRepo repository.DeviceRepository
|
||||
AreaControllerRepo repository.AreaControllerRepository
|
||||
DeviceTemplateRepo repository.DeviceTemplateRepository
|
||||
PlanRepo repository.PlanRepository
|
||||
PendingTaskRepo repository.PendingTaskRepository
|
||||
ExecutionLogRepo repository.ExecutionLogRepository
|
||||
SensorDataRepo repository.SensorDataRepository
|
||||
DeviceCommandLogRepo repository.DeviceCommandLogRepository
|
||||
PendingCollectionRepo repository.PendingCollectionRepository
|
||||
UserActionLogRepo repository.UserActionLogRepository
|
||||
PigBatchRepo repository.PigBatchRepository
|
||||
PigBatchLogRepo repository.PigBatchLogRepository
|
||||
PigFarmRepo repository.PigFarmRepository
|
||||
PigPenRepo repository.PigPenRepository
|
||||
PigTransferLogRepo repository.PigTransferLogRepository
|
||||
PigTradeRepo repository.PigTradeRepository
|
||||
PigSickPigLogRepo repository.PigSickLogRepository
|
||||
MedicationLogRepo repository.MedicationLogRepository
|
||||
RawMaterialRepo repository.RawMaterialRepository
|
||||
NotificationRepo repository.NotificationRepository
|
||||
UnitOfWork repository.UnitOfWork
|
||||
userRepo repository.UserRepository
|
||||
deviceRepo repository.DeviceRepository
|
||||
areaControllerRepo repository.AreaControllerRepository
|
||||
deviceTemplateRepo repository.DeviceTemplateRepository
|
||||
planRepo repository.PlanRepository
|
||||
pendingTaskRepo repository.PendingTaskRepository
|
||||
executionLogRepo repository.ExecutionLogRepository
|
||||
sensorDataRepo repository.SensorDataRepository
|
||||
deviceCommandLogRepo repository.DeviceCommandLogRepository
|
||||
pendingCollectionRepo repository.PendingCollectionRepository
|
||||
userActionLogRepo repository.UserActionLogRepository
|
||||
pigBatchRepo repository.PigBatchRepository
|
||||
pigBatchLogRepo repository.PigBatchLogRepository
|
||||
pigFarmRepo repository.PigFarmRepository
|
||||
pigPenRepo repository.PigPenRepository
|
||||
pigTransferLogRepo repository.PigTransferLogRepository
|
||||
pigTradeRepo repository.PigTradeRepository
|
||||
pigSickPigLogRepo repository.PigSickLogRepository
|
||||
medicationLogRepo repository.MedicationLogRepository
|
||||
rawMaterialRepo repository.RawMaterialRepository
|
||||
notificationRepo repository.NotificationRepository
|
||||
unitOfWork repository.UnitOfWork
|
||||
}
|
||||
|
||||
// initRepositories 初始化所有的仓库。
|
||||
func initRepositories(db *gorm.DB, logger *logs.Logger) *Repositories {
|
||||
return &Repositories{
|
||||
UserRepo: repository.NewGormUserRepository(db),
|
||||
DeviceRepo: repository.NewGormDeviceRepository(db),
|
||||
AreaControllerRepo: repository.NewGormAreaControllerRepository(db),
|
||||
DeviceTemplateRepo: repository.NewGormDeviceTemplateRepository(db),
|
||||
PlanRepo: repository.NewGormPlanRepository(db),
|
||||
PendingTaskRepo: repository.NewGormPendingTaskRepository(db),
|
||||
ExecutionLogRepo: repository.NewGormExecutionLogRepository(db),
|
||||
SensorDataRepo: repository.NewGormSensorDataRepository(db),
|
||||
DeviceCommandLogRepo: repository.NewGormDeviceCommandLogRepository(db),
|
||||
PendingCollectionRepo: repository.NewGormPendingCollectionRepository(db),
|
||||
UserActionLogRepo: repository.NewGormUserActionLogRepository(db),
|
||||
PigBatchRepo: repository.NewGormPigBatchRepository(db),
|
||||
PigBatchLogRepo: repository.NewGormPigBatchLogRepository(db),
|
||||
PigFarmRepo: repository.NewGormPigFarmRepository(db),
|
||||
PigPenRepo: repository.NewGormPigPenRepository(db),
|
||||
PigTransferLogRepo: repository.NewGormPigTransferLogRepository(db),
|
||||
PigTradeRepo: repository.NewGormPigTradeRepository(db),
|
||||
PigSickPigLogRepo: repository.NewGormPigSickLogRepository(db),
|
||||
MedicationLogRepo: repository.NewGormMedicationLogRepository(db),
|
||||
RawMaterialRepo: repository.NewGormRawMaterialRepository(db),
|
||||
NotificationRepo: repository.NewGormNotificationRepository(db),
|
||||
UnitOfWork: repository.NewGormUnitOfWork(db, logger),
|
||||
userRepo: repository.NewGormUserRepository(db),
|
||||
deviceRepo: repository.NewGormDeviceRepository(db),
|
||||
areaControllerRepo: repository.NewGormAreaControllerRepository(db),
|
||||
deviceTemplateRepo: repository.NewGormDeviceTemplateRepository(db),
|
||||
planRepo: repository.NewGormPlanRepository(db),
|
||||
pendingTaskRepo: repository.NewGormPendingTaskRepository(db),
|
||||
executionLogRepo: repository.NewGormExecutionLogRepository(db),
|
||||
sensorDataRepo: repository.NewGormSensorDataRepository(db),
|
||||
deviceCommandLogRepo: repository.NewGormDeviceCommandLogRepository(db),
|
||||
pendingCollectionRepo: repository.NewGormPendingCollectionRepository(db),
|
||||
userActionLogRepo: repository.NewGormUserActionLogRepository(db),
|
||||
pigBatchRepo: repository.NewGormPigBatchRepository(db),
|
||||
pigBatchLogRepo: repository.NewGormPigBatchLogRepository(db),
|
||||
pigFarmRepo: repository.NewGormPigFarmRepository(db),
|
||||
pigPenRepo: repository.NewGormPigPenRepository(db),
|
||||
pigTransferLogRepo: repository.NewGormPigTransferLogRepository(db),
|
||||
pigTradeRepo: repository.NewGormPigTradeRepository(db),
|
||||
pigSickPigLogRepo: repository.NewGormPigSickLogRepository(db),
|
||||
medicationLogRepo: repository.NewGormMedicationLogRepository(db),
|
||||
rawMaterialRepo: repository.NewGormRawMaterialRepository(db),
|
||||
notificationRepo: repository.NewGormNotificationRepository(db),
|
||||
unitOfWork: repository.NewGormUnitOfWork(db, logger),
|
||||
}
|
||||
}
|
||||
|
||||
// DomainServices 聚合了所有的领域服务实例。
|
||||
type DomainServices struct {
|
||||
PigPenTransferManager pig.PigPenTransferManager
|
||||
PigTradeManager pig.PigTradeManager
|
||||
PigSickManager pig.SickPigManager
|
||||
PigBatchDomain pig.PigBatchService
|
||||
TimedCollector collection.Collector
|
||||
GeneralDeviceService device.Service
|
||||
AnalysisPlanTaskManager *task.AnalysisPlanTaskManager
|
||||
Scheduler *task.Scheduler
|
||||
pigPenTransferManager pig.PigPenTransferManager
|
||||
pigTradeManager pig.PigTradeManager
|
||||
pigSickManager pig.SickPigManager
|
||||
pigBatchDomain pig.PigBatchService
|
||||
generalDeviceService device.Service
|
||||
taskFactory plan.TaskFactory
|
||||
planExecutionManager plan.ExecutionManager
|
||||
analysisPlanTaskManager plan.AnalysisPlanTaskManager
|
||||
planService plan.Service
|
||||
}
|
||||
|
||||
// initDomainServices 初始化所有的领域服务。
|
||||
func initDomainServices(cfg *config.Config, infra *Infrastructure, logger *logs.Logger) *DomainServices {
|
||||
// 猪群管理相关
|
||||
pigPenTransferManager := pig.NewPigPenTransferManager(infra.Repos.PigPenRepo, infra.Repos.PigTransferLogRepo, infra.Repos.PigBatchRepo)
|
||||
pigTradeManager := pig.NewPigTradeManager(infra.Repos.PigTradeRepo)
|
||||
pigSickManager := pig.NewSickPigManager(infra.Repos.PigSickPigLogRepo, infra.Repos.MedicationLogRepo)
|
||||
pigBatchDomain := pig.NewPigBatchService(infra.Repos.PigBatchRepo, infra.Repos.PigBatchLogRepo, infra.Repos.UnitOfWork,
|
||||
pigPenTransferManager := pig.NewPigPenTransferManager(infra.repos.pigPenRepo, infra.repos.pigTransferLogRepo, infra.repos.pigBatchRepo)
|
||||
pigTradeManager := pig.NewPigTradeManager(infra.repos.pigTradeRepo)
|
||||
pigSickManager := pig.NewSickPigManager(infra.repos.pigSickPigLogRepo, infra.repos.medicationLogRepo)
|
||||
pigBatchDomain := pig.NewPigBatchService(infra.repos.pigBatchRepo, infra.repos.pigBatchLogRepo, infra.repos.unitOfWork,
|
||||
pigPenTransferManager, pigTradeManager, pigSickManager)
|
||||
|
||||
// 通用设备服务
|
||||
generalDeviceService := device.NewGeneralDeviceService(
|
||||
infra.Repos.DeviceRepo,
|
||||
infra.Repos.DeviceCommandLogRepo,
|
||||
infra.Repos.PendingCollectionRepo,
|
||||
infra.repos.deviceRepo,
|
||||
infra.repos.deviceCommandLogRepo,
|
||||
infra.repos.pendingCollectionRepo,
|
||||
logger,
|
||||
infra.Lora.Comm,
|
||||
infra.lora.comm,
|
||||
)
|
||||
|
||||
// 任务工厂
|
||||
taskFactory := task.NewTaskFactory(logger, infra.repos.sensorDataRepo, infra.repos.deviceRepo, generalDeviceService)
|
||||
|
||||
// 计划任务管理器
|
||||
analysisPlanTaskManager := task.NewAnalysisPlanTaskManager(infra.Repos.PlanRepo, infra.Repos.PendingTaskRepo, infra.Repos.ExecutionLogRepo, logger)
|
||||
analysisPlanTaskManager := plan.NewAnalysisPlanTaskManager(infra.repos.planRepo, infra.repos.pendingTaskRepo, infra.repos.executionLogRepo, logger)
|
||||
|
||||
// 任务执行器
|
||||
scheduler := task.NewScheduler(
|
||||
infra.Repos.PendingTaskRepo,
|
||||
infra.Repos.ExecutionLogRepo,
|
||||
infra.Repos.DeviceRepo,
|
||||
infra.Repos.SensorDataRepo,
|
||||
infra.Repos.PlanRepo,
|
||||
planExecutionManager := plan.NewPlanExecutionManager(
|
||||
infra.repos.pendingTaskRepo,
|
||||
infra.repos.executionLogRepo,
|
||||
infra.repos.deviceRepo,
|
||||
infra.repos.sensorDataRepo,
|
||||
infra.repos.planRepo,
|
||||
analysisPlanTaskManager,
|
||||
taskFactory,
|
||||
logger,
|
||||
generalDeviceService,
|
||||
time.Duration(cfg.Task.Interval)*time.Second,
|
||||
cfg.Task.NumWorkers,
|
||||
)
|
||||
|
||||
// 定时采集器
|
||||
timedCollector := collection.NewTimedCollector(
|
||||
infra.Repos.DeviceRepo,
|
||||
generalDeviceService,
|
||||
logger,
|
||||
time.Duration(cfg.Collection.Interval)*time.Second,
|
||||
)
|
||||
// 计划管理器
|
||||
planService := plan.NewPlanService(
|
||||
planExecutionManager,
|
||||
analysisPlanTaskManager,
|
||||
infra.repos.planRepo,
|
||||
infra.repos.deviceRepo,
|
||||
infra.repos.unitOfWork,
|
||||
taskFactory,
|
||||
logger)
|
||||
|
||||
return &DomainServices{
|
||||
PigPenTransferManager: pigPenTransferManager,
|
||||
PigTradeManager: pigTradeManager,
|
||||
PigSickManager: pigSickManager,
|
||||
PigBatchDomain: pigBatchDomain,
|
||||
GeneralDeviceService: generalDeviceService,
|
||||
AnalysisPlanTaskManager: analysisPlanTaskManager,
|
||||
Scheduler: scheduler,
|
||||
TimedCollector: timedCollector,
|
||||
pigPenTransferManager: pigPenTransferManager,
|
||||
pigTradeManager: pigTradeManager,
|
||||
pigSickManager: pigSickManager,
|
||||
pigBatchDomain: pigBatchDomain,
|
||||
generalDeviceService: generalDeviceService,
|
||||
analysisPlanTaskManager: analysisPlanTaskManager,
|
||||
taskFactory: taskFactory,
|
||||
planExecutionManager: planExecutionManager,
|
||||
planService: planService,
|
||||
}
|
||||
}
|
||||
|
||||
// AppServices 聚合了所有的应用服务实例。
|
||||
type AppServices struct {
|
||||
PigFarmService service.PigFarmService
|
||||
PigBatchService service.PigBatchService
|
||||
MonitorService service.MonitorService
|
||||
AuditService audit.Service
|
||||
pigFarmService service.PigFarmService
|
||||
pigBatchService service.PigBatchService
|
||||
monitorService service.MonitorService
|
||||
deviceService service.DeviceService
|
||||
planService service.PlanService
|
||||
userService service.UserService
|
||||
auditService audit.Service
|
||||
}
|
||||
|
||||
// initAppServices 初始化所有的应用服务。
|
||||
func initAppServices(infra *Infrastructure, domainServices *DomainServices, logger *logs.Logger) *AppServices {
|
||||
pigFarmService := service.NewPigFarmService(infra.Repos.PigFarmRepo, infra.Repos.PigPenRepo, infra.Repos.PigBatchRepo, domainServices.PigBatchDomain, infra.Repos.UnitOfWork, logger)
|
||||
pigBatchService := service.NewPigBatchService(domainServices.PigBatchDomain, logger)
|
||||
pigFarmService := service.NewPigFarmService(infra.repos.pigFarmRepo, infra.repos.pigPenRepo, infra.repos.pigBatchRepo, domainServices.pigBatchDomain, infra.repos.unitOfWork, logger)
|
||||
pigBatchService := service.NewPigBatchService(domainServices.pigBatchDomain, logger)
|
||||
monitorService := service.NewMonitorService(
|
||||
infra.Repos.SensorDataRepo,
|
||||
infra.Repos.DeviceCommandLogRepo,
|
||||
infra.Repos.ExecutionLogRepo,
|
||||
infra.Repos.PendingCollectionRepo,
|
||||
infra.Repos.UserActionLogRepo,
|
||||
infra.Repos.RawMaterialRepo,
|
||||
infra.Repos.MedicationLogRepo,
|
||||
infra.Repos.PigBatchRepo,
|
||||
infra.Repos.PigBatchLogRepo,
|
||||
infra.Repos.PigTransferLogRepo,
|
||||
infra.Repos.PigSickPigLogRepo,
|
||||
infra.Repos.PigTradeRepo,
|
||||
infra.Repos.NotificationRepo,
|
||||
infra.repos.sensorDataRepo,
|
||||
infra.repos.deviceCommandLogRepo,
|
||||
infra.repos.executionLogRepo,
|
||||
infra.repos.planRepo,
|
||||
infra.repos.pendingCollectionRepo,
|
||||
infra.repos.userActionLogRepo,
|
||||
infra.repos.rawMaterialRepo,
|
||||
infra.repos.medicationLogRepo,
|
||||
infra.repos.pigBatchRepo,
|
||||
infra.repos.pigBatchLogRepo,
|
||||
infra.repos.pigTransferLogRepo,
|
||||
infra.repos.pigSickPigLogRepo,
|
||||
infra.repos.pigTradeRepo,
|
||||
infra.repos.notificationRepo,
|
||||
)
|
||||
auditService := audit.NewService(infra.Repos.UserActionLogRepo, logger)
|
||||
deviceService := service.NewDeviceService(
|
||||
infra.repos.deviceRepo,
|
||||
infra.repos.areaControllerRepo,
|
||||
infra.repos.deviceTemplateRepo,
|
||||
domainServices.generalDeviceService,
|
||||
)
|
||||
auditService := audit.NewService(infra.repos.userActionLogRepo, logger)
|
||||
planService := service.NewPlanService(logger, domainServices.planService)
|
||||
userService := service.NewUserService(infra.repos.userRepo, infra.tokenService, infra.notifyService, logger)
|
||||
|
||||
return &AppServices{
|
||||
PigFarmService: pigFarmService,
|
||||
PigBatchService: pigBatchService,
|
||||
MonitorService: monitorService,
|
||||
AuditService: auditService,
|
||||
pigFarmService: pigFarmService,
|
||||
pigBatchService: pigBatchService,
|
||||
monitorService: monitorService,
|
||||
deviceService: deviceService,
|
||||
auditService: auditService,
|
||||
planService: planService,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// LoraComponents 聚合了所有 LoRa 相关组件。
|
||||
type LoraComponents struct {
|
||||
ListenHandler webhook.ListenHandler
|
||||
Comm transport.Communicator
|
||||
LoraListener transport.Listener
|
||||
listenHandler webhook.ListenHandler
|
||||
comm transport.Communicator
|
||||
loraListener transport.Listener
|
||||
}
|
||||
|
||||
// initLora 根据配置初始化 LoRa 相关组件。
|
||||
@@ -240,13 +263,13 @@ func initLora(
|
||||
|
||||
if cfg.Lora.Mode == config.LoraMode_LoRaWAN {
|
||||
logger.Info("当前运行模式: lora_wan。初始化 ChirpStack 监听器和传输层。")
|
||||
listenHandler = webhook.NewChirpStackListener(logger, repos.SensorDataRepo, repos.DeviceRepo, repos.AreaControllerRepo, repos.DeviceCommandLogRepo, repos.PendingCollectionRepo)
|
||||
listenHandler = webhook.NewChirpStackListener(logger, repos.sensorDataRepo, repos.deviceRepo, repos.areaControllerRepo, repos.deviceCommandLogRepo, repos.pendingCollectionRepo)
|
||||
comm = lora.NewChirpStackTransport(cfg.ChirpStack, logger)
|
||||
loraListener = lora.NewPlaceholderTransport(logger)
|
||||
} else {
|
||||
logger.Info("当前运行模式: lora_mesh。初始化 LoRa Mesh 传输层和占位符监听器。")
|
||||
listenHandler = webhook.NewPlaceholderListener(logger)
|
||||
tp, err := lora.NewLoRaMeshUartPassthroughTransport(cfg.LoraMesh, logger, repos.AreaControllerRepo, repos.PendingCollectionRepo, repos.DeviceRepo, repos.SensorDataRepo)
|
||||
tp, err := lora.NewLoRaMeshUartPassthroughTransport(cfg.LoraMesh, logger, repos.areaControllerRepo, repos.pendingCollectionRepo, repos.deviceRepo, repos.sensorDataRepo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法初始化 LoRa Mesh 模块: %w", err)
|
||||
}
|
||||
@@ -255,9 +278,9 @@ func initLora(
|
||||
}
|
||||
|
||||
return &LoraComponents{
|
||||
ListenHandler: listenHandler,
|
||||
Comm: comm,
|
||||
LoraListener: loraListener,
|
||||
listenHandler: listenHandler,
|
||||
comm: comm,
|
||||
loraListener: loraListener,
|
||||
}, nil
|
||||
}
|
||||
|
||||
245
internal/core/data_initializer.go
Normal file
245
internal/core/data_initializer.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
// PlanNameTimedFullDataCollection 是定时全量数据采集计划的名称
|
||||
PlanNameTimedFullDataCollection = "定时全量数据采集"
|
||||
)
|
||||
|
||||
// initializeState 在应用启动时准备其初始数据状态。
|
||||
// 这包括清理任何因上次异常关闭而留下的悬空任务或请求。
|
||||
func (app *Application) initializeState() error {
|
||||
// 初始化预定义系统计划 (致命错误)
|
||||
if err := app.initializeSystemPlans(); err != nil {
|
||||
return fmt.Errorf("初始化预定义系统计划失败: %w", err)
|
||||
}
|
||||
|
||||
// 清理待采集任务 (非致命错误)
|
||||
if err := app.initializePendingCollections(); err != nil {
|
||||
app.Logger.Errorw("清理待采集任务时发生非致命错误", "error", err)
|
||||
}
|
||||
|
||||
// 初始化待执行任务列表 (致命错误)
|
||||
if err := app.initializePendingTasks(); err != nil {
|
||||
return fmt.Errorf("初始化待执行任务列表失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeSystemPlans 确保预定义的系统计划在数据库中存在并保持最新。
|
||||
func (app *Application) initializeSystemPlans() error {
|
||||
app.Logger.Info("开始检查并更新预定义的系统计划...")
|
||||
|
||||
// 动态构建预定义计划列表
|
||||
predefinedSystemPlans := app.getPredefinedSystemPlans()
|
||||
|
||||
// 1. 获取所有已存在的系统计划
|
||||
existingPlans, _, err := app.Infra.repos.planRepo.ListPlans(repository.ListPlansOptions{
|
||||
PlanType: repository.PlanTypeFilterSystem,
|
||||
}, 1, 99999) // 使用一个较大的 pageSize 来获取所有系统计划
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取现有系统计划失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 为了方便查找, 将现有计划名放入一个 map
|
||||
existingPlanMap := make(map[string]*models.Plan)
|
||||
for i := range existingPlans {
|
||||
existingPlanMap[existingPlans[i].Name] = &existingPlans[i]
|
||||
}
|
||||
|
||||
// 3. 遍历预定义的计划列表
|
||||
for i := range predefinedSystemPlans {
|
||||
predefinedPlan := &predefinedSystemPlans[i] // 获取可修改的指针
|
||||
|
||||
if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok {
|
||||
// 如果计划存在,则进行无差别更新
|
||||
app.Logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name)
|
||||
|
||||
// 将数据库中已存在的计划的ID和运行时状态字段赋值给预定义计划
|
||||
predefinedPlan.ID = foundExistingPlan.ID
|
||||
predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount
|
||||
|
||||
if err := app.Infra.repos.planRepo.UpdatePlan(predefinedPlan); err != nil {
|
||||
return fmt.Errorf("更新预定义计划 '%s' 失败: %w", predefinedPlan.Name, err)
|
||||
} else {
|
||||
app.Logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name)
|
||||
}
|
||||
} else {
|
||||
// 如果计划不存在, 则创建
|
||||
app.Logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name)
|
||||
if err := app.Infra.repos.planRepo.CreatePlan(predefinedPlan); err != nil {
|
||||
return fmt.Errorf("创建预定义计划 '%s' 失败: %w", predefinedPlan.Name, err)
|
||||
} else {
|
||||
app.Logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.Logger.Info("预定义系统计划检查完成。")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPredefinedSystemPlans 返回一个基于当前配置的预定义系统计划列表。
|
||||
func (app *Application) getPredefinedSystemPlans() []models.Plan {
|
||||
|
||||
// 根据配置创建定时全量采集计划
|
||||
interval := app.Config.Collection.Interval
|
||||
if interval <= 0 {
|
||||
interval = 1 // 确保间隔至少为1分钟
|
||||
}
|
||||
cronExpression := fmt.Sprintf("*/%d * * * *", interval)
|
||||
timedCollectionPlan := models.Plan{
|
||||
Name: PlanNameTimedFullDataCollection,
|
||||
Description: fmt.Sprintf("这是一个系统预定义的计划, 每 %d 分钟自动触发一次全量数据采集。", app.Config.Collection.Interval),
|
||||
PlanType: models.PlanTypeSystem,
|
||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
||||
CronExpression: cronExpression,
|
||||
Status: models.PlanStatusEnabled,
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{
|
||||
{
|
||||
Name: "全量采集",
|
||||
Description: "触发一次全量数据采集",
|
||||
ExecutionOrder: 1,
|
||||
Type: models.TaskTypeFullCollection,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return []models.Plan{timedCollectionPlan}
|
||||
}
|
||||
|
||||
// initializePendingCollections 在应用启动时处理所有未完成的采集请求。
|
||||
// 我们的策略是:任何在程序重启前仍处于“待处理”状态的请求,都应被视为已失败。
|
||||
// 这保证了系统在每次启动时都处于一个干净、确定的状态。
|
||||
func (app *Application) initializePendingCollections() error {
|
||||
app.Logger.Info("开始清理所有未完成的采集请求...")
|
||||
|
||||
// 直接将所有 'pending' 状态的请求更新为 'timed_out'。
|
||||
count, err := app.Infra.repos.pendingCollectionRepo.MarkAllPendingAsTimedOut()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理未完成的采集请求失败: %v", err)
|
||||
} else if count > 0 {
|
||||
app.Logger.Infof("成功将 %d 个未完成的采集请求标记为超时。", count)
|
||||
} else {
|
||||
app.Logger.Info("没有需要清理的采集请求。")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializePendingTasks 在应用启动时清理并刷新待执行任务列表。
|
||||
func (app *Application) initializePendingTasks() error {
|
||||
logger := app.Logger
|
||||
planRepo := app.Infra.repos.planRepo
|
||||
pendingTaskRepo := app.Infra.repos.pendingTaskRepo
|
||||
executionLogRepo := app.Infra.repos.executionLogRepo
|
||||
planService := app.Domain.planService
|
||||
|
||||
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.PlanStatusStopped
|
||||
logger.Infof("计划 #%d 状态已更新为 '执行完毕'。", plan.ID)
|
||||
|
||||
}
|
||||
// 保存更新后的计划
|
||||
if err := planRepo.UpdatePlan(plan); err != nil {
|
||||
logger.Errorf("修正计划 #%d 状态失败: %v", plan.ID, err)
|
||||
// 这是一个非阻塞性错误,继续处理其他计划
|
||||
}
|
||||
}
|
||||
logger.Info("阶段一:固定次数计划修正完成。")
|
||||
|
||||
// 阶段二:清理所有待执行任务和相关日志
|
||||
logger.Info("阶段二:开始清理所有待执行任务和相关日志...")
|
||||
|
||||
// --- 新增逻辑:处理因崩溃导致状态不一致的计划主表状态 ---
|
||||
// 1. 查找所有未完成的计划执行日志 (状态为 Started 或 Waiting)
|
||||
incompletePlanLogs, err := executionLogRepo.FindIncompletePlanExecutionLogs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找未完成的计划执行日志失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 收集所有受影响的唯一 PlanID
|
||||
affectedPlanIDs := make(map[uint]struct{})
|
||||
for _, log := range incompletePlanLogs {
|
||||
affectedPlanIDs[log.PlanID] = struct{}{}
|
||||
}
|
||||
|
||||
// 3. 对于每个受影响的 PlanID,重置其 execute_count 并将其状态设置为 Failed, 系统计划不受此影响
|
||||
for planID := range affectedPlanIDs {
|
||||
// 首先,获取计划的详细信息以判断其类型
|
||||
plan, err := planRepo.GetBasicPlanByID(planID)
|
||||
if err != nil {
|
||||
logger.Errorf("在尝试修正计划状态时,获取计划 #%d 的基本信息失败: %v", planID, err)
|
||||
continue // 获取失败,跳过此计划
|
||||
}
|
||||
|
||||
// 如果是系统计划,则不应标记为失败,仅记录日志
|
||||
if plan.PlanType == models.PlanTypeSystem {
|
||||
logger.Warnf("检测到系统计划 #%d 在应用崩溃前处于未完成状态,但根据策略,将保持其原有状态不标记为失败。", planID)
|
||||
continue // 跳过,不处理
|
||||
}
|
||||
|
||||
// 对于非系统计划,执行原有的失败标记逻辑
|
||||
logger.Warnf("检测到计划 #%d 在应用崩溃前处于未完成状态,将重置其计数并标记为失败。", planID)
|
||||
// 使用 UpdatePlanStateAfterExecution 来更新主表状态,避免影响关联数据
|
||||
if err := planRepo.UpdatePlanStateAfterExecution(planID, 0, models.PlanStatusFailed); err != nil {
|
||||
logger.Errorf("重置计划 #%d 计数并标记为失败时出错: %v", planID, err)
|
||||
// 这是一个非阻塞性错误,继续处理其他计划
|
||||
}
|
||||
}
|
||||
logger.Info("阶段二:计划主表状态修正完成。")
|
||||
|
||||
// 直接调用新的方法来更新计划执行日志状态为失败
|
||||
if err := executionLogRepo.FailAllIncompletePlanExecutionLogs(); err != nil {
|
||||
logger.Errorf("更新所有未完成计划执行日志状态为失败失败: %v", err)
|
||||
// 这是一个非阻塞性错误,继续执行
|
||||
}
|
||||
|
||||
// 直接调用新的方法来更新任务执行日志状态为取消
|
||||
if err := executionLogRepo.CancelAllIncompleteTaskExecutionLogs(); err != nil {
|
||||
logger.Errorf("更新所有未完成任务执行日志状态为取消失败: %v", err)
|
||||
// 这是一个非阻塞性错误,继续执行
|
||||
}
|
||||
|
||||
// 清空待执行列表
|
||||
if err := pendingTaskRepo.ClearAllPendingTasks(); err != nil {
|
||||
return fmt.Errorf("清空待执行任务列表失败: %w", err)
|
||||
}
|
||||
logger.Info("阶段二:待执行任务和相关日志清理完成。")
|
||||
|
||||
// 阶段三:初始刷新
|
||||
logger.Info("阶段三:开始刷新待执行列表...")
|
||||
if err := planService.RefreshPlanTriggers(); err != nil {
|
||||
return fmt.Errorf("刷新待执行任务列表失败: %w", err)
|
||||
}
|
||||
logger.Info("阶段三:待执行任务列表初始化完成。")
|
||||
|
||||
logger.Info("待执行任务列表初始化完成。")
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package collection
|
||||
|
||||
type Collector interface {
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package collection
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// TimedCollector 实现了 Collector 接口,用于定时从数据库获取设备信息并下发采集指令
|
||||
type TimedCollector struct {
|
||||
deviceRepo repository.DeviceRepository
|
||||
deviceService device.Service
|
||||
logger *logs.Logger
|
||||
interval time.Duration
|
||||
ticker *time.Ticker
|
||||
done chan bool
|
||||
}
|
||||
|
||||
// NewTimedCollector 创建一个定时采集器实例
|
||||
func NewTimedCollector(
|
||||
deviceRepo repository.DeviceRepository,
|
||||
deviceService device.Service,
|
||||
logger *logs.Logger,
|
||||
interval time.Duration,
|
||||
) Collector {
|
||||
return &TimedCollector{
|
||||
deviceRepo: deviceRepo,
|
||||
deviceService: deviceService,
|
||||
logger: logger,
|
||||
interval: interval,
|
||||
done: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 开始定时采集
|
||||
func (c *TimedCollector) Start() {
|
||||
c.logger.Infof("定时采集器启动,采集间隔: %s", c.interval)
|
||||
c.ticker = time.NewTicker(c.interval)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c.done:
|
||||
return
|
||||
case <-c.ticker.C:
|
||||
c.collect()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop 停止定时采集
|
||||
func (c *TimedCollector) Stop() {
|
||||
c.logger.Info("定时采集器停止")
|
||||
c.ticker.Stop()
|
||||
c.done <- true
|
||||
}
|
||||
|
||||
// collect 是核心的采集逻辑
|
||||
func (c *TimedCollector) collect() {
|
||||
c.logger.Info("开始新一轮的设备数据采集")
|
||||
|
||||
sensors, err := c.deviceRepo.ListAllSensors()
|
||||
if err != nil {
|
||||
c.logger.Errorf("采集周期: 从数据库获取所有传感器失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(sensors) == 0 {
|
||||
c.logger.Info("采集周期: 未发现任何传感器设备,跳过本次采集")
|
||||
return
|
||||
}
|
||||
|
||||
sensorsByController := make(map[uint][]*models.Device)
|
||||
for _, sensor := range sensors {
|
||||
sensorsByController[sensor.AreaControllerID] = append(sensorsByController[sensor.AreaControllerID], sensor)
|
||||
}
|
||||
|
||||
for controllerID, controllerSensors := range sensorsByController {
|
||||
c.logger.Infof("采集周期: 准备为区域主控 %d 下的 %d 个传感器下发采集指令", controllerID, len(controllerSensors))
|
||||
if err := c.deviceService.Collect(controllerID, controllerSensors); err != nil {
|
||||
c.logger.Errorf("采集周期: 为区域主控 %d 下发采集指令失败: %v", controllerID, err)
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Info("本轮设备数据采集完成")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package task
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -11,10 +11,22 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils"
|
||||
)
|
||||
|
||||
// AnalysisPlanTaskManager 负责管理分析计划的触发器任务。
|
||||
// AnalysisPlanTaskManager 定义了分析计划任务管理器的接口。
|
||||
type AnalysisPlanTaskManager interface {
|
||||
// Refresh 同步数据库中的计划状态和待执行队列中的触发器任务。
|
||||
Refresh() error
|
||||
// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。
|
||||
// 如果触发器已存在,会根据计划类型更新其执行时间。
|
||||
CreateOrUpdateTrigger(planID uint) error
|
||||
// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。
|
||||
// 如果不存在,则会自动创建。此方法不涉及待执行队列。
|
||||
EnsureAnalysisTaskDefinition(planID uint) error
|
||||
}
|
||||
|
||||
// analysisPlanTaskManagerImpl 负责管理分析计划的触发器任务。
|
||||
// 它确保数据库中可执行的计划在待执行队列中有对应的触发器,并移除无效的触发器。
|
||||
// 这是一个有状态的组件,包含一个互斥锁以确保并发安全。
|
||||
type AnalysisPlanTaskManager struct {
|
||||
type analysisPlanTaskManagerImpl struct {
|
||||
planRepo repository.PlanRepository
|
||||
pendingTaskRepo repository.PendingTaskRepository
|
||||
executionLogRepo repository.ExecutionLogRepository
|
||||
@@ -22,14 +34,14 @@ type AnalysisPlanTaskManager struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewAnalysisPlanTaskManager 是 AnalysisPlanTaskManager 的构造函数。
|
||||
// NewAnalysisPlanTaskManager 是 analysisPlanTaskManagerImpl 的构造函数。
|
||||
func NewAnalysisPlanTaskManager(
|
||||
planRepo repository.PlanRepository,
|
||||
pendingTaskRepo repository.PendingTaskRepository,
|
||||
executionLogRepo repository.ExecutionLogRepository,
|
||||
logger *logs.Logger,
|
||||
) *AnalysisPlanTaskManager {
|
||||
return &AnalysisPlanTaskManager{
|
||||
) AnalysisPlanTaskManager {
|
||||
return &analysisPlanTaskManagerImpl{
|
||||
planRepo: planRepo,
|
||||
pendingTaskRepo: pendingTaskRepo,
|
||||
executionLogRepo: executionLogRepo,
|
||||
@@ -39,7 +51,7 @@ func NewAnalysisPlanTaskManager(
|
||||
|
||||
// Refresh 同步数据库中的计划状态和待执行队列中的触发器任务。
|
||||
// 这是一个编排方法,将复杂的逻辑分解到多个内部方法中。
|
||||
func (m *AnalysisPlanTaskManager) Refresh() error {
|
||||
func (m *analysisPlanTaskManagerImpl) Refresh() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -68,7 +80,7 @@ func (m *AnalysisPlanTaskManager) Refresh() error {
|
||||
|
||||
// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。
|
||||
// 如果触发器已存在,会根据计划类型更新其执行时间。
|
||||
func (m *AnalysisPlanTaskManager) CreateOrUpdateTrigger(planID uint) error {
|
||||
func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(planID uint) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -123,7 +135,7 @@ func (m *AnalysisPlanTaskManager) CreateOrUpdateTrigger(planID uint) error {
|
||||
|
||||
// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。
|
||||
// 如果不存在,则会自动创建。此方法不涉及待执行队列。
|
||||
func (m *AnalysisPlanTaskManager) EnsureAnalysisTaskDefinition(planID uint) error {
|
||||
func (m *analysisPlanTaskManagerImpl) EnsureAnalysisTaskDefinition(planID uint) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -154,7 +166,7 @@ func (m *AnalysisPlanTaskManager) EnsureAnalysisTaskDefinition(planID uint) erro
|
||||
// --- 内部私有方法 ---
|
||||
|
||||
// getRefreshData 从数据库获取刷新所需的所有数据。
|
||||
func (m *AnalysisPlanTaskManager) getRefreshData() (runnablePlans []*models.Plan, invalidPlanIDs []uint, pendingTasks []models.PendingTask, err error) {
|
||||
func (m *analysisPlanTaskManagerImpl) getRefreshData() (runnablePlans []*models.Plan, invalidPlanIDs []uint, pendingTasks []models.PendingTask, err error) {
|
||||
runnablePlans, err = m.planRepo.FindRunnablePlans()
|
||||
if err != nil {
|
||||
m.logger.Errorf("获取可执行计划列表失败: %v", err)
|
||||
@@ -180,7 +192,7 @@ func (m *AnalysisPlanTaskManager) getRefreshData() (runnablePlans []*models.Plan
|
||||
}
|
||||
|
||||
// cleanupInvalidTasks 清理所有与失效计划相关的待执行任务。
|
||||
func (m *AnalysisPlanTaskManager) cleanupInvalidTasks(invalidPlanIDs []uint, allPendingTasks []models.PendingTask) error {
|
||||
func (m *analysisPlanTaskManagerImpl) cleanupInvalidTasks(invalidPlanIDs []uint, allPendingTasks []models.PendingTask) error {
|
||||
if len(invalidPlanIDs) == 0 {
|
||||
return nil // 没有需要清理的计划
|
||||
}
|
||||
@@ -224,7 +236,7 @@ func (m *AnalysisPlanTaskManager) cleanupInvalidTasks(invalidPlanIDs []uint, all
|
||||
}
|
||||
|
||||
// addOrUpdateTriggers 检查、更新或创建触发器。
|
||||
func (m *AnalysisPlanTaskManager) addOrUpdateTriggers(runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error {
|
||||
func (m *analysisPlanTaskManagerImpl) addOrUpdateTriggers(runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error {
|
||||
// 创建一个映射,存放所有已在队列中的计划触发器
|
||||
pendingTriggersMap := make(map[uint]models.PendingTask)
|
||||
for _, pt := range allPendingTasks {
|
||||
@@ -266,7 +278,7 @@ func (m *AnalysisPlanTaskManager) addOrUpdateTriggers(runnablePlans []*models.Pl
|
||||
}
|
||||
|
||||
// createTriggerTask 是创建触发器任务的内部核心逻辑。
|
||||
func (m *AnalysisPlanTaskManager) createTriggerTask(plan *models.Plan) error {
|
||||
func (m *analysisPlanTaskManagerImpl) createTriggerTask(plan *models.Plan) error {
|
||||
analysisTask, err := m.planRepo.FindPlanAnalysisTaskByPlanID(plan.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找计划分析任务失败: %w", err)
|
||||
1
internal/domain/plan/device_id_extractor.go
Normal file
1
internal/domain/plan/device_id_extractor.go
Normal file
@@ -0,0 +1 @@
|
||||
package plan
|
||||
@@ -1,4 +1,4 @@
|
||||
package task
|
||||
package plan
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -13,6 +13,14 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ExecutionManager 定义了计划执行管理器的接口。
|
||||
type ExecutionManager interface {
|
||||
// Start 启动计划执行管理器。
|
||||
Start()
|
||||
// Stop 优雅地停止计划执行管理器。
|
||||
Stop()
|
||||
}
|
||||
|
||||
// ProgressTracker 仅用于在内存中提供计划执行的并发锁
|
||||
type ProgressTracker struct {
|
||||
mu sync.Mutex
|
||||
@@ -73,8 +81,8 @@ func (t *ProgressTracker) GetRunningPlanIDs() []uint {
|
||||
return ids
|
||||
}
|
||||
|
||||
// Scheduler 是核心的、持久化的任务调度器
|
||||
type Scheduler struct {
|
||||
// planExecutionManagerImpl 是核心的、持久化的任务调度器
|
||||
type planExecutionManagerImpl struct {
|
||||
logger *logs.Logger
|
||||
pollingInterval time.Duration
|
||||
workers int
|
||||
@@ -83,7 +91,8 @@ type Scheduler struct {
|
||||
deviceRepo repository.DeviceRepository
|
||||
sensorDataRepo repository.SensorDataRepository
|
||||
planRepo repository.PlanRepository
|
||||
analysisPlanTaskManager *AnalysisPlanTaskManager
|
||||
taskFactory TaskFactory
|
||||
analysisPlanTaskManager AnalysisPlanTaskManager
|
||||
progressTracker *ProgressTracker
|
||||
deviceService device.Service
|
||||
|
||||
@@ -92,26 +101,28 @@ type Scheduler struct {
|
||||
stopChan chan struct{} // 用于停止主循环的信号通道
|
||||
}
|
||||
|
||||
// NewScheduler 创建一个新的调度器实例
|
||||
func NewScheduler(
|
||||
// NewPlanExecutionManager 创建一个新的调度器实例
|
||||
func NewPlanExecutionManager(
|
||||
pendingTaskRepo repository.PendingTaskRepository,
|
||||
executionLogRepo repository.ExecutionLogRepository,
|
||||
deviceRepo repository.DeviceRepository,
|
||||
sensorDataRepo repository.SensorDataRepository,
|
||||
planRepo repository.PlanRepository,
|
||||
analysisPlanTaskManager *AnalysisPlanTaskManager,
|
||||
analysisPlanTaskManager AnalysisPlanTaskManager,
|
||||
taskFactory TaskFactory,
|
||||
logger *logs.Logger,
|
||||
deviceService device.Service,
|
||||
interval time.Duration,
|
||||
numWorkers int,
|
||||
) *Scheduler {
|
||||
return &Scheduler{
|
||||
) ExecutionManager {
|
||||
return &planExecutionManagerImpl{
|
||||
pendingTaskRepo: pendingTaskRepo,
|
||||
executionLogRepo: executionLogRepo,
|
||||
deviceRepo: deviceRepo,
|
||||
sensorDataRepo: sensorDataRepo,
|
||||
planRepo: planRepo,
|
||||
analysisPlanTaskManager: analysisPlanTaskManager,
|
||||
taskFactory: taskFactory,
|
||||
logger: logger,
|
||||
deviceService: deviceService,
|
||||
pollingInterval: interval,
|
||||
@@ -122,7 +133,7 @@ func NewScheduler(
|
||||
}
|
||||
|
||||
// Start 启动调度器,包括初始化协程池和启动主轮询循环
|
||||
func (s *Scheduler) Start() {
|
||||
func (s *planExecutionManagerImpl) Start() {
|
||||
s.logger.Warnf("任务调度器正在启动,工作协程数: %d...", s.workers)
|
||||
pool, err := ants.NewPool(s.workers, ants.WithPanicHandler(func(err interface{}) {
|
||||
s.logger.Errorf("[严重] 任务执行时发生 panic: %v", err)
|
||||
@@ -138,7 +149,7 @@ func (s *Scheduler) Start() {
|
||||
}
|
||||
|
||||
// Stop 优雅地停止调度器
|
||||
func (s *Scheduler) Stop() {
|
||||
func (s *planExecutionManagerImpl) Stop() {
|
||||
s.logger.Warnf("正在停止任务调度器...")
|
||||
close(s.stopChan) // 1. 发出停止信号,停止主循环
|
||||
s.wg.Wait() // 2. 等待主循环完成
|
||||
@@ -147,7 +158,7 @@ func (s *Scheduler) Stop() {
|
||||
}
|
||||
|
||||
// run 是主轮询循环,负责从数据库认领任务并提交到协程池
|
||||
func (s *Scheduler) run() {
|
||||
func (s *planExecutionManagerImpl) run() {
|
||||
defer s.wg.Done()
|
||||
ticker := time.NewTicker(s.pollingInterval)
|
||||
defer ticker.Stop()
|
||||
@@ -165,7 +176,7 @@ func (s *Scheduler) run() {
|
||||
}
|
||||
|
||||
// claimAndSubmit 实现了最终的“认领-锁定-执行 或 等待-放回”的健壮逻辑
|
||||
func (s *Scheduler) claimAndSubmit() {
|
||||
func (s *planExecutionManagerImpl) claimAndSubmit() {
|
||||
runningPlanIDs := s.progressTracker.GetRunningPlanIDs()
|
||||
|
||||
claimedLog, pendingTask, err := s.pendingTaskRepo.ClaimNextAvailableTask(runningPlanIDs)
|
||||
@@ -198,7 +209,7 @@ func (s *Scheduler) claimAndSubmit() {
|
||||
}
|
||||
|
||||
// handleRequeue 同步地、安全地将一个无法立即执行的任务放回队列。
|
||||
func (s *Scheduler) handleRequeue(planExecutionLogID uint, taskToRequeue *models.PendingTask) {
|
||||
func (s *planExecutionManagerImpl) handleRequeue(planExecutionLogID uint, taskToRequeue *models.PendingTask) {
|
||||
s.logger.Warnf("计划 %d 正在执行,任务 %d (TaskID: %d) 将等待并重新入队...", planExecutionLogID, taskToRequeue.ID, taskToRequeue.TaskID)
|
||||
|
||||
// 1. 阻塞式地等待,直到可以获取到该计划的锁。
|
||||
@@ -215,7 +226,7 @@ func (s *Scheduler) handleRequeue(planExecutionLogID uint, taskToRequeue *models
|
||||
}
|
||||
|
||||
// processTask 处理单个任务的逻辑
|
||||
func (s *Scheduler) processTask(claimedLog *models.TaskExecutionLog) {
|
||||
func (s *planExecutionManagerImpl) processTask(claimedLog *models.TaskExecutionLog) {
|
||||
s.logger.Warnf("开始处理任务, 日志ID: %d, 任务ID: %d, 任务名称: %s, 描述: %s",
|
||||
claimedLog.ID, claimedLog.TaskID, claimedLog.Task.Name, claimedLog.Task.Description)
|
||||
|
||||
@@ -258,7 +269,7 @@ func (s *Scheduler) processTask(claimedLog *models.TaskExecutionLog) {
|
||||
}
|
||||
|
||||
// runTask 用于执行具体任务
|
||||
func (s *Scheduler) runTask(claimedLog *models.TaskExecutionLog) error {
|
||||
func (s *planExecutionManagerImpl) runTask(claimedLog *models.TaskExecutionLog) error {
|
||||
// 这是个特殊任务, 用于解析Plan并将解析出的任务队列添加到待执行队列中
|
||||
if claimedLog.Task.Type == models.TaskPlanAnalysis {
|
||||
// 解析plan
|
||||
@@ -271,7 +282,7 @@ func (s *Scheduler) runTask(claimedLog *models.TaskExecutionLog) error {
|
||||
|
||||
} else {
|
||||
// 执行普通任务
|
||||
task := s.taskFactory(claimedLog)
|
||||
task := s.taskFactory.Production(claimedLog)
|
||||
|
||||
if err := task.Execute(); err != nil {
|
||||
s.logger.Errorf("[严重] 任务执行失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
|
||||
@@ -283,22 +294,8 @@ func (s *Scheduler) runTask(claimedLog *models.TaskExecutionLog) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// taskFactory 会根据任务类型初始化对应任务
|
||||
func (s *Scheduler) taskFactory(claimedLog *models.TaskExecutionLog) Task {
|
||||
switch claimedLog.Task.Type {
|
||||
case models.TaskTypeWaiting:
|
||||
return NewDelayTask(s.logger, claimedLog)
|
||||
case models.TaskTypeReleaseFeedWeight:
|
||||
return NewReleaseFeedWeightTask(claimedLog, s.sensorDataRepo, s.deviceRepo, s.deviceService, s.logger)
|
||||
|
||||
default:
|
||||
// TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型
|
||||
panic("不支持的任务类型")
|
||||
}
|
||||
}
|
||||
|
||||
// analysisPlan 解析Plan并将解析出的Task列表插入待执行队列中
|
||||
func (s *Scheduler) analysisPlan(claimedLog *models.TaskExecutionLog) error {
|
||||
func (s *planExecutionManagerImpl) analysisPlan(claimedLog *models.TaskExecutionLog) error {
|
||||
// 创建Plan执行记录
|
||||
// 从任务的 Parameters 中解析出真实的 PlanID
|
||||
var params struct {
|
||||
@@ -371,7 +368,7 @@ func (s *Scheduler) analysisPlan(claimedLog *models.TaskExecutionLog) error {
|
||||
}
|
||||
|
||||
// updateTaskExecutionLogStatus 修改任务历史中的执行状态
|
||||
func (s *Scheduler) updateTaskExecutionLogStatus(claimedLog *models.TaskExecutionLog) error {
|
||||
func (s *planExecutionManagerImpl) updateTaskExecutionLogStatus(claimedLog *models.TaskExecutionLog) error {
|
||||
claimedLog.EndedAt = time.Now()
|
||||
|
||||
if err := s.executionLogRepo.UpdateTaskExecutionLog(claimedLog); err != nil {
|
||||
@@ -383,7 +380,7 @@ func (s *Scheduler) updateTaskExecutionLogStatus(claimedLog *models.TaskExecutio
|
||||
}
|
||||
|
||||
// handlePlanTermination 集中处理计划的终止逻辑(失败或取消)
|
||||
func (s *Scheduler) handlePlanTermination(planLogID uint, reason string) {
|
||||
func (s *planExecutionManagerImpl) handlePlanTermination(planLogID uint, reason string) {
|
||||
// 1. 从待执行队列中删除所有相关的子任务
|
||||
if err := s.pendingTaskRepo.DeletePendingTasksByPlanLogID(planLogID); err != nil {
|
||||
s.logger.Errorf("从待执行队列中删除计划 %d 的后续任务时出错: %v", planLogID, err)
|
||||
@@ -399,19 +396,34 @@ func (s *Scheduler) handlePlanTermination(planLogID uint, reason string) {
|
||||
s.logger.Errorf("取消计划 %d 的后续任务日志时出错: %v", planLogID, err)
|
||||
}
|
||||
|
||||
// 4. 将计划本身的状态更新为失败
|
||||
// 4. 获取计划执行日志以获取顶层 PlanID
|
||||
planLog, err := s.executionLogRepo.FindPlanExecutionLogByID(planLogID)
|
||||
if err != nil {
|
||||
s.logger.Errorf("无法找到计划执行日志 %d 以更新父计划状态: %v", planLogID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 获取顶层计划的详细信息,以检查其类型
|
||||
topLevelPlan, err := s.planRepo.GetBasicPlanByID(planLog.PlanID)
|
||||
if err != nil {
|
||||
s.logger.Errorf("获取顶层计划 %d 的基本信息失败: %v", planLog.PlanID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 6. 如果是系统任务,则不修改计划状态
|
||||
if topLevelPlan.PlanType == models.PlanTypeSystem {
|
||||
s.logger.Warnf("系统任务 %d (日志ID: %d) 执行失败,但根据策略不修改其计划状态。", topLevelPlan.ID, planLogID)
|
||||
return
|
||||
}
|
||||
|
||||
// 7. 将计划本身的状态更新为失败 (仅对非系统任务执行)
|
||||
if err := s.planRepo.UpdatePlanStatus(planLog.PlanID, models.PlanStatusFailed); err != nil {
|
||||
s.logger.Errorf("更新计划 %d 状态为 '失败' 时出错: %v", planLog.PlanID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePlanCompletion 集中处理计划成功完成后的所有逻辑
|
||||
func (s *Scheduler) handlePlanCompletion(planLogID uint) {
|
||||
func (s *planExecutionManagerImpl) handlePlanCompletion(planLogID uint) {
|
||||
s.logger.Infof("计划执行 %d 的所有任务已完成,开始处理计划完成逻辑...", planLogID)
|
||||
|
||||
// 1. 通过 PlanExecutionLog 反查正确的顶层 PlanID
|
||||
409
internal/domain/plan/plan_service.go
Normal file
409
internal/domain/plan/plan_service.go
Normal file
@@ -0,0 +1,409 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPlanNotFound 表示未找到计划
|
||||
ErrPlanNotFound = errors.New("计划不存在")
|
||||
// ErrPlanCannotBeModified 表示计划不允许修改
|
||||
ErrPlanCannotBeModified = errors.New("系统计划不允许修改")
|
||||
// ErrPlanCannotBeDeleted 表示计划不允许删除
|
||||
ErrPlanCannotBeDeleted = errors.New("系统计划不允许删除")
|
||||
// ErrPlanCannotBeStarted 表示计划不允许手动启动
|
||||
ErrPlanCannotBeStarted = errors.New("系统计划不允许手动启动")
|
||||
// ErrPlanAlreadyEnabled 表示计划已处于启动状态
|
||||
ErrPlanAlreadyEnabled = errors.New("计划已处于启动状态,无需重复操作")
|
||||
// ErrPlanNotEnabled 表示计划未处于启动状态
|
||||
ErrPlanNotEnabled = errors.New("计划当前不是启用状态")
|
||||
// ErrPlanCannotBeStopped 表示计划不允许停止
|
||||
ErrPlanCannotBeStopped = errors.New("系统计划不允许停止")
|
||||
)
|
||||
|
||||
// Service 定义了计划领域服务的接口。
|
||||
type Service interface {
|
||||
// Start 启动计划相关的后台服务,例如计划执行管理器。
|
||||
Start()
|
||||
// Stop 停止计划相关的后台服务,例如计划执行管理器。
|
||||
Stop()
|
||||
// RefreshPlanTriggers 刷新计划触发器,同步数据库中的计划状态和待执行队列中的触发器任务。
|
||||
RefreshPlanTriggers() error
|
||||
|
||||
// CreatePlan 创建一个新的计划
|
||||
CreatePlan(plan *models.Plan) (*models.Plan, error)
|
||||
// GetPlanByID 根据ID获取计划详情
|
||||
GetPlanByID(id uint) (*models.Plan, error)
|
||||
// ListPlans 获取计划列表,支持过滤和分页
|
||||
ListPlans(opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)
|
||||
// UpdatePlan 更新计划
|
||||
UpdatePlan(plan *models.Plan) (*models.Plan, error)
|
||||
// DeletePlan 删除计划(软删除)
|
||||
DeletePlan(id uint) error
|
||||
// StartPlan 启动计划
|
||||
StartPlan(id uint) error
|
||||
// StopPlan 停止计划
|
||||
StopPlan(id uint) error
|
||||
}
|
||||
|
||||
// planServiceImpl 是 Service 接口的具体实现。
|
||||
type planServiceImpl struct {
|
||||
executionManager ExecutionManager
|
||||
taskManager AnalysisPlanTaskManager
|
||||
planRepo repository.PlanRepository
|
||||
deviceRepo repository.DeviceRepository
|
||||
unitOfWork repository.UnitOfWork
|
||||
taskFactory TaskFactory
|
||||
logger *logs.Logger
|
||||
}
|
||||
|
||||
// NewPlanService 创建一个新的 Service 实例。
|
||||
func NewPlanService(
|
||||
executionManager ExecutionManager,
|
||||
taskManager AnalysisPlanTaskManager,
|
||||
planRepo repository.PlanRepository,
|
||||
deviceRepo repository.DeviceRepository,
|
||||
unitOfWork repository.UnitOfWork,
|
||||
taskFactory TaskFactory,
|
||||
logger *logs.Logger,
|
||||
) Service {
|
||||
return &planServiceImpl{
|
||||
executionManager: executionManager,
|
||||
taskManager: taskManager,
|
||||
planRepo: planRepo,
|
||||
deviceRepo: deviceRepo,
|
||||
unitOfWork: unitOfWork,
|
||||
taskFactory: taskFactory,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动计划相关的后台服务。
|
||||
func (s *planServiceImpl) Start() {
|
||||
s.logger.Infof("PlanService 正在启动...")
|
||||
s.executionManager.Start()
|
||||
}
|
||||
|
||||
// Stop 停止计划相关的后台服务。
|
||||
func (s *planServiceImpl) Stop() {
|
||||
s.logger.Infof("PlanService 正在停止...")
|
||||
s.executionManager.Stop()
|
||||
}
|
||||
|
||||
// RefreshPlanTriggers 刷新计划触发器。
|
||||
func (s *planServiceImpl) RefreshPlanTriggers() error {
|
||||
s.logger.Infof("PlanService 正在刷新计划触发器...")
|
||||
return s.taskManager.Refresh()
|
||||
}
|
||||
|
||||
// CreatePlan 创建一个新的计划
|
||||
func (s *planServiceImpl) CreatePlan(planToCreate *models.Plan) (*models.Plan, error) {
|
||||
const actionType = "领域层:创建计划"
|
||||
|
||||
// 1. 业务规则处理
|
||||
// 用户创建的计划永远是自定义计划
|
||||
planToCreate.PlanType = models.PlanTypeCustom
|
||||
|
||||
// 自动判断 ContentType
|
||||
if len(planToCreate.SubPlans) > 0 {
|
||||
planToCreate.ContentType = models.PlanContentTypeSubPlans
|
||||
} else {
|
||||
planToCreate.ContentType = models.PlanContentTypeTasks
|
||||
}
|
||||
|
||||
// 2. 验证和重排顺序 (领域逻辑)
|
||||
if err := planToCreate.ValidateExecutionOrder(); err != nil {
|
||||
s.logger.Errorf("%s: 计划 (ID: %d) 的执行顺序无效: %v", actionType, planToCreate.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
planToCreate.ReorderSteps()
|
||||
|
||||
// 3. 在调用仓库前,准备好所有数据,包括设备关联
|
||||
for i := range planToCreate.Tasks {
|
||||
taskModel := &planToCreate.Tasks[i]
|
||||
// 使用工厂创建临时领域对象
|
||||
taskResolver, err := s.taskFactory.CreateTaskFromModel(taskModel)
|
||||
if err != nil {
|
||||
// 如果一个任务类型不支持,我们可以选择跳过或报错
|
||||
s.logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
|
||||
continue
|
||||
}
|
||||
|
||||
deviceIDs, err := taskResolver.ResolveDeviceIDs()
|
||||
if err != nil {
|
||||
// 在事务外解析失败,直接返回错误
|
||||
return nil, fmt.Errorf("为任务 '%s' 提取设备ID失败: %w", taskModel.Name, err)
|
||||
}
|
||||
if len(deviceIDs) > 0 {
|
||||
// 优化:无需查询完整的设备对象,只需构建包含ID的结构体即可建立关联
|
||||
devices := make([]models.Device, len(deviceIDs))
|
||||
for i, id := range deviceIDs {
|
||||
devices[i] = models.Device{Model: gorm.Model{ID: id}}
|
||||
}
|
||||
taskModel.Devices = devices
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 调用仓库方法创建计划,该方法内部会处理事务
|
||||
err := s.planRepo.CreatePlan(planToCreate)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
|
||||
if err := s.taskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil {
|
||||
// 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功
|
||||
s.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err)
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID)
|
||||
return planToCreate, nil
|
||||
}
|
||||
|
||||
// GetPlanByID 根据ID获取计划详情
|
||||
func (s *planServiceImpl) GetPlanByID(id uint) (*models.Plan, error) {
|
||||
const actionType = "领域层:获取计划详情"
|
||||
|
||||
plan, err := s.planRepo.GetPlanByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
return nil, ErrPlanNotFound
|
||||
}
|
||||
s.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// ListPlans 获取计划列表,支持过滤和分页
|
||||
func (s *planServiceImpl) ListPlans(opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) {
|
||||
const actionType = "领域层:获取计划列表"
|
||||
|
||||
plans, total, err := s.planRepo.ListPlans(opts, page, pageSize)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(plans))
|
||||
return plans, total, nil
|
||||
}
|
||||
|
||||
// UpdatePlan 更新计划
|
||||
func (s *planServiceImpl) UpdatePlan(planToUpdate *models.Plan) (*models.Plan, error) {
|
||||
const actionType = "领域层:更新计划"
|
||||
|
||||
existingPlan, err := s.planRepo.GetBasicPlanByID(planToUpdate.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, planToUpdate.ID)
|
||||
return nil, ErrPlanNotFound
|
||||
}
|
||||
s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, planToUpdate.ID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 系统计划不允许修改
|
||||
if existingPlan.PlanType == models.PlanTypeSystem {
|
||||
s.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, planToUpdate.ID)
|
||||
return nil, ErrPlanCannotBeModified
|
||||
}
|
||||
|
||||
// 自动判断 ContentType
|
||||
if len(planToUpdate.SubPlans) > 0 {
|
||||
planToUpdate.ContentType = models.PlanContentTypeSubPlans
|
||||
} else {
|
||||
planToUpdate.ContentType = models.PlanContentTypeTasks
|
||||
}
|
||||
|
||||
// 验证和重排顺序 (领域逻辑)
|
||||
if err := planToUpdate.ValidateExecutionOrder(); err != nil {
|
||||
s.logger.Errorf("%s: 计划 (ID: %d) 的执行顺序无效: %v", actionType, planToUpdate.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
planToUpdate.ReorderSteps()
|
||||
|
||||
// 只要是更新任务,就重置执行计数器
|
||||
planToUpdate.ExecuteCount = 0
|
||||
s.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID)
|
||||
|
||||
// 在调用仓库前,准备好所有数据,包括设备关联
|
||||
for i := range planToUpdate.Tasks {
|
||||
taskModel := &planToUpdate.Tasks[i]
|
||||
taskResolver, err := s.taskFactory.CreateTaskFromModel(taskModel)
|
||||
if err != nil {
|
||||
s.logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
|
||||
continue
|
||||
}
|
||||
|
||||
deviceIDs, err := taskResolver.ResolveDeviceIDs()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("为任务 '%s' 提取设备ID失败: %w", taskModel.Name, err)
|
||||
}
|
||||
if len(deviceIDs) > 0 {
|
||||
// 优化:无需查询完整的设备对象,只需构建包含ID的结构体即可建立关联
|
||||
devices := make([]models.Device, len(deviceIDs))
|
||||
for i, id := range deviceIDs {
|
||||
devices[i] = models.Device{Model: gorm.Model{ID: id}}
|
||||
}
|
||||
taskModel.Devices = devices
|
||||
}
|
||||
}
|
||||
|
||||
// 调用仓库方法更新计划,该方法内部会处理事务
|
||||
err = s.planRepo.UpdatePlanMetadataAndStructure(planToUpdate)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.taskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil {
|
||||
s.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err)
|
||||
}
|
||||
|
||||
updatedPlan, err := s.planRepo.GetPlanByID(planToUpdate.ID)
|
||||
if err != nil {
|
||||
s.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, planToUpdate.ID)
|
||||
return nil, errors.New("获取更新后计划详情时发生内部错误")
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
|
||||
return updatedPlan, nil
|
||||
}
|
||||
|
||||
// DeletePlan 删除计划(软删除)
|
||||
func (s *planServiceImpl) DeletePlan(id uint) error {
|
||||
const actionType = "领域层:删除计划"
|
||||
|
||||
plan, err := s.planRepo.GetBasicPlanByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
return ErrPlanNotFound
|
||||
}
|
||||
s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// 系统计划不允许删除
|
||||
if plan.PlanType == models.PlanTypeSystem {
|
||||
s.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id)
|
||||
return ErrPlanCannotBeDeleted
|
||||
}
|
||||
|
||||
// 如果计划处于启用状态,先停止它
|
||||
if plan.Status == models.PlanStatusEnabled {
|
||||
if err := s.planRepo.StopPlanTransactionally(id); err != nil {
|
||||
s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.planRepo.DeletePlan(id); err != nil {
|
||||
s.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPlan 启动计划
|
||||
func (s *planServiceImpl) StartPlan(id uint) error {
|
||||
const actionType = "领域层:启动计划"
|
||||
|
||||
plan, err := s.planRepo.GetBasicPlanByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
return ErrPlanNotFound
|
||||
}
|
||||
s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// 系统计划不允许手动启动
|
||||
if plan.PlanType == models.PlanTypeSystem {
|
||||
s.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id)
|
||||
return ErrPlanCannotBeStarted
|
||||
}
|
||||
// 计划已处于启动状态,无需重复操作
|
||||
if plan.Status == models.PlanStatusEnabled {
|
||||
s.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id)
|
||||
return ErrPlanAlreadyEnabled
|
||||
}
|
||||
|
||||
// 如果计划未处于启用状态
|
||||
if plan.Status != models.PlanStatusEnabled {
|
||||
// 如果执行计数器大于0,重置为0
|
||||
if plan.ExecuteCount > 0 {
|
||||
if err := s.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil {
|
||||
s.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID)
|
||||
return err
|
||||
}
|
||||
s.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID)
|
||||
}
|
||||
|
||||
// 更新计划状态为启用
|
||||
if err := s.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil {
|
||||
s.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID)
|
||||
return err
|
||||
}
|
||||
s.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID)
|
||||
}
|
||||
|
||||
// 创建或更新触发器
|
||||
if err := s.taskManager.CreateOrUpdateTrigger(plan.ID); err != nil {
|
||||
s.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopPlan 停止计划
|
||||
func (s *planServiceImpl) StopPlan(id uint) error {
|
||||
const actionType = "领域层:停止计划"
|
||||
|
||||
plan, err := s.planRepo.GetBasicPlanByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
|
||||
return ErrPlanNotFound
|
||||
}
|
||||
s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// 系统计划不允许停止
|
||||
if plan.PlanType == models.PlanTypeSystem {
|
||||
s.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id)
|
||||
return ErrPlanCannotBeStopped
|
||||
}
|
||||
|
||||
// 计划当前不是启用状态
|
||||
if plan.Status != models.PlanStatusEnabled {
|
||||
s.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status)
|
||||
return ErrPlanNotEnabled
|
||||
}
|
||||
|
||||
// 停止计划事务性操作
|
||||
if err := s.planRepo.StopPlanTransactionally(id); err != nil {
|
||||
s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
|
||||
return nil
|
||||
}
|
||||
34
internal/domain/plan/task.go
Normal file
34
internal/domain/plan/task.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package plan
|
||||
|
||||
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
|
||||
// Task 定义了所有可被调度器执行的任务必须实现的接口。
|
||||
type Task interface {
|
||||
// Execute 是任务的核心执行逻辑。
|
||||
// ctx: 用于控制任务的超时或取消。
|
||||
// log: 包含了当前任务执行的完整上下文信息,包括从数据库中加载的任务参数等。
|
||||
// 返回的 error 表示任务是否执行成功。调度器会根据返回的 error 是否为 nil 来决定任务状态。
|
||||
Execute() error
|
||||
|
||||
// OnFailure 定义了当 Execute 方法返回错误时,需要执行的回滚或清理逻辑。
|
||||
// log: 任务执行的上下文。
|
||||
// executeErr: 从 Execute 方法返回的原始错误。
|
||||
OnFailure(executeErr error)
|
||||
|
||||
TaskDeviceIDResolver
|
||||
}
|
||||
|
||||
// TaskDeviceIDResolver 定义了从任务配置中解析设备ID的方法
|
||||
type TaskDeviceIDResolver interface {
|
||||
// ResolveDeviceIDs 从任务配置中解析并返回所有关联的设备ID列表
|
||||
// 返回值: uint数组,每个字符串代表一个设备ID
|
||||
ResolveDeviceIDs() ([]uint, error)
|
||||
}
|
||||
|
||||
// TaskFactory 是一个工厂接口,用于根据任务执行日志创建任务实例。
|
||||
type TaskFactory interface {
|
||||
// Production 根据指定的任务执行日志创建一个任务实例。
|
||||
Production(claimedLog *models.TaskExecutionLog) Task
|
||||
// CreateTaskFromModel 仅根据任务模型创建一个任务实例,用于非执行场景(如参数解析)。
|
||||
CreateTaskFromModel(taskModel *models.Task) (TaskDeviceIDResolver, error)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
@@ -19,7 +20,7 @@ type DelayTask struct {
|
||||
logger *logs.Logger
|
||||
}
|
||||
|
||||
func NewDelayTask(logger *logs.Logger, executionTask *models.TaskExecutionLog) Task {
|
||||
func NewDelayTask(logger *logs.Logger, executionTask *models.TaskExecutionLog) plan.Task {
|
||||
return &DelayTask{
|
||||
executionTask: executionTask,
|
||||
logger: logger,
|
||||
@@ -64,3 +65,7 @@ func (d *DelayTask) parseParameters() error {
|
||||
func (d *DelayTask) OnFailure(executeErr error) {
|
||||
d.logger.Errorf("任务 %v: 执行失败: %v", d.executionTask.TaskID, executeErr)
|
||||
}
|
||||
|
||||
func (d *DelayTask) ResolveDeviceIDs() ([]uint, error) {
|
||||
return []uint{}, nil
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task"
|
||||
)
|
||||
|
||||
func TestNewDelayTask(t *testing.T) {
|
||||
id := "test-delay-task-1"
|
||||
duration := 100 * time.Millisecond
|
||||
priority := 1
|
||||
|
||||
dt := task.NewDelayTask(id, duration, priority)
|
||||
|
||||
if dt.GetID() != id {
|
||||
t.Errorf("期望任务ID为 %s, 实际为 %s", id, dt.GetID())
|
||||
}
|
||||
if dt.GetPriority() != priority {
|
||||
t.Errorf("期望任务优先级为 %d, 实际为 %d", priority, dt.GetPriority())
|
||||
}
|
||||
if dt.IsDone() != false {
|
||||
t.Error("任务初始状态不应为已完成")
|
||||
}
|
||||
// 动态生成的描述,需要匹配 GetDescription 的实现
|
||||
expectedDesc := fmt.Sprintf("延迟任务,ID: %s,延迟时间: %s", id, duration)
|
||||
if dt.GetDescription() != expectedDesc {
|
||||
t.Errorf("期望任务描述为 %s, 实际为 %s", expectedDesc, dt.GetDescription())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelayTaskExecute(t *testing.T) {
|
||||
id := "test-delay-task-execute"
|
||||
duration := 50 * time.Millisecond // 使用较短的延迟以加快测试速度
|
||||
priority := 1
|
||||
|
||||
dt := task.NewDelayTask(id, duration, priority)
|
||||
|
||||
if dt.IsDone() {
|
||||
t.Error("任务执行前不应为已完成状态")
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
err := dt.Execute()
|
||||
endTime := time.Now()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Execute 方法返回错误: %v", err)
|
||||
}
|
||||
if !dt.IsDone() {
|
||||
t.Error("任务执行后应为已完成状态")
|
||||
}
|
||||
|
||||
// 验证延迟时间大致正确,允许一些误差
|
||||
elapsed := endTime.Sub(startTime)
|
||||
if elapsed < duration || elapsed > duration*2 {
|
||||
t.Errorf("期望执行时间在 %v 左右, 但实际耗时 %v", duration, elapsed)
|
||||
}
|
||||
}
|
||||
100
internal/domain/task/full_collection_task.go
Normal file
100
internal/domain/task/full_collection_task.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// FullCollectionTask 实现了 plan.Task 接口,用于执行一次全量的设备数据采集
|
||||
type FullCollectionTask struct {
|
||||
log *models.TaskExecutionLog
|
||||
deviceRepo repository.DeviceRepository
|
||||
deviceService device.Service
|
||||
logger *logs.Logger
|
||||
}
|
||||
|
||||
// NewFullCollectionTask 创建一个全量采集任务实例
|
||||
func NewFullCollectionTask(
|
||||
log *models.TaskExecutionLog,
|
||||
deviceRepo repository.DeviceRepository,
|
||||
deviceService device.Service,
|
||||
logger *logs.Logger,
|
||||
) plan.Task {
|
||||
return &FullCollectionTask{
|
||||
log: log,
|
||||
deviceRepo: deviceRepo,
|
||||
deviceService: deviceService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 是任务的核心执行逻辑
|
||||
func (t *FullCollectionTask) Execute() error {
|
||||
t.logger.Infow("开始执行全量采集任务", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
|
||||
|
||||
sensors, err := t.deviceRepo.ListAllSensors()
|
||||
if err != nil {
|
||||
return fmt.Errorf("全量采集任务: 从数据库获取所有传感器失败: %w", err)
|
||||
}
|
||||
|
||||
if len(sensors) == 0 {
|
||||
t.logger.Infow("全量采集任务: 未发现任何传感器设备,跳过本次采集", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
sensorsByController := make(map[uint][]*models.Device)
|
||||
for _, sensor := range sensors {
|
||||
sensorsByController[sensor.AreaControllerID] = append(sensorsByController[sensor.AreaControllerID], sensor)
|
||||
}
|
||||
|
||||
var firstError error
|
||||
for controllerID, controllerSensors := range sensorsByController {
|
||||
t.logger.Infow("全量采集任务: 准备为区域主控下的传感器下发采集指令",
|
||||
"task_id", t.log.TaskID,
|
||||
"task_type", t.log.Task.Type,
|
||||
"log_id", t.log.ID,
|
||||
"controller_id", controllerID,
|
||||
"sensor_count", len(controllerSensors),
|
||||
)
|
||||
if err := t.deviceService.Collect(controllerID, controllerSensors); err != nil {
|
||||
t.logger.Errorw("全量采集任务: 为区域主控下发采集指令失败",
|
||||
"task_id", t.log.TaskID,
|
||||
"task_type", t.log.Task.Type,
|
||||
"log_id", t.log.ID,
|
||||
"controller_id", controllerID,
|
||||
"error", err,
|
||||
)
|
||||
if firstError == nil {
|
||||
firstError = err // 保存第一个错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if firstError != nil {
|
||||
return fmt.Errorf("全量采集任务执行期间发生错误: %w", firstError)
|
||||
}
|
||||
|
||||
t.logger.Infow("全量采集任务执行完成", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnFailure 定义了当 Execute 方法返回错误时,需要执行的回滚或清理逻辑
|
||||
func (t *FullCollectionTask) OnFailure(executeErr error) {
|
||||
t.logger.Errorw("全量采集任务执行失败",
|
||||
"task_id", t.log.TaskID,
|
||||
"task_type", t.log.Task.Type,
|
||||
"log_id", t.log.ID,
|
||||
"error", executeErr,
|
||||
)
|
||||
}
|
||||
|
||||
// ResolveDeviceIDs 获取当前任务需要使用的设备ID列表
|
||||
func (t *FullCollectionTask) ResolveDeviceIDs() ([]uint, error) {
|
||||
// 全量采集任务不和任何设备绑定, 每轮采集都会重新获取全量传感器
|
||||
return []uint{}, nil
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package task
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
@@ -30,6 +32,9 @@ type ReleaseFeedWeightTask struct {
|
||||
|
||||
feedPort device.Service
|
||||
|
||||
// onceParse 保证解析参数只执行一次
|
||||
onceParse sync.Once
|
||||
|
||||
logger *logs.Logger
|
||||
}
|
||||
|
||||
@@ -40,12 +45,12 @@ func NewReleaseFeedWeightTask(
|
||||
deviceRepo repository.DeviceRepository,
|
||||
deviceService device.Service,
|
||||
logger *logs.Logger,
|
||||
) Task {
|
||||
) plan.Task {
|
||||
return &ReleaseFeedWeightTask{
|
||||
claimedLog: claimedLog,
|
||||
deviceRepo: deviceRepo,
|
||||
sensorDataRepo: sensorDataRepo,
|
||||
feedPort: deviceService, // 直接注入
|
||||
feedPort: deviceService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -116,30 +121,32 @@ func (r *ReleaseFeedWeightTask) getNowWeight() (float64, error) {
|
||||
}
|
||||
|
||||
func (r *ReleaseFeedWeightTask) parseParameters() error {
|
||||
var err error
|
||||
r.onceParse.Do(func() {
|
||||
if r.claimedLog.Task.Parameters == nil {
|
||||
r.logger.Errorf("任务 %v: 缺少参数", r.claimedLog.TaskID)
|
||||
return fmt.Errorf("任务 %v: 参数不全", r.claimedLog.TaskID)
|
||||
err = fmt.Errorf("任务 %v: 参数不全", r.claimedLog.TaskID)
|
||||
}
|
||||
|
||||
var params ReleaseFeedWeightTaskParams
|
||||
err := r.claimedLog.Task.ParseParameters(¶ms)
|
||||
if err != nil {
|
||||
r.logger.Errorf("任务 %v: 解析参数失败: %v", r.claimedLog.TaskID, err)
|
||||
return fmt.Errorf("任务 %v: 解析参数失败: %v", r.claimedLog.TaskID, err)
|
||||
err = fmt.Errorf("任务 %v: 解析参数失败: %v", r.claimedLog.TaskID, err)
|
||||
}
|
||||
|
||||
// 校验参数是否存在
|
||||
if params.ReleaseWeight == 0 {
|
||||
r.logger.Errorf("任务 %v: 参数 release_weight 缺失或无效", r.claimedLog.TaskID)
|
||||
return fmt.Errorf("任务 %v: 参数 release_weight 缺失或无效", r.claimedLog.TaskID)
|
||||
err = fmt.Errorf("任务 %v: 参数 release_weight 缺失或无效", r.claimedLog.TaskID)
|
||||
}
|
||||
if params.FeedPortDeviceID == 0 {
|
||||
r.logger.Errorf("任务 %v: 参数 feed_port_device_id 缺失或无效", r.claimedLog.TaskID)
|
||||
return fmt.Errorf("任务 %v: 参数 feed_port_device_id 缺失或无效", r.claimedLog.TaskID)
|
||||
err = fmt.Errorf("任务 %v: 参数 feed_port_device_id 缺失或无效", r.claimedLog.TaskID)
|
||||
}
|
||||
if params.MixingTankDeviceID == 0 {
|
||||
r.logger.Errorf("任务 %v: 参数 mixing_tank_device_id 缺失或无效", r.claimedLog.TaskID)
|
||||
return fmt.Errorf("任务 %v: 参数 mixing_tank_device_id 缺失或无效", r.claimedLog.TaskID)
|
||||
err = fmt.Errorf("任务 %v: 参数 mixing_tank_device_id 缺失或无效", r.claimedLog.TaskID)
|
||||
}
|
||||
|
||||
r.releaseWeight = params.ReleaseWeight
|
||||
@@ -147,14 +154,15 @@ func (r *ReleaseFeedWeightTask) parseParameters() error {
|
||||
r.feedPortDevice, err = r.deviceRepo.FindByID(params.FeedPortDeviceID)
|
||||
if err != nil {
|
||||
r.logger.Errorf("任务 %v: 获取设备信息失败: %v", r.claimedLog.TaskID, err)
|
||||
return fmt.Errorf("任务 %v: 获取设备信息失败: %v", r.claimedLog.TaskID, err)
|
||||
err = fmt.Errorf("任务 %v: 获取设备信息失败: %v", r.claimedLog.TaskID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ReleaseFeedWeightTask) OnFailure(executeErr error) {
|
||||
r.logger.Errorf("开始善后处理, 日志ID:%v", r.claimedLog.ID)
|
||||
r.logger.Errorf("开始善后处理, 日志ID:%v; 错误信息: %v", r.claimedLog.ID, executeErr)
|
||||
if r.feedPort != nil {
|
||||
err := r.feedPort.Switch(r.feedPortDevice, device.DeviceActionStop)
|
||||
if err != nil {
|
||||
@@ -165,3 +173,10 @@ func (r *ReleaseFeedWeightTask) OnFailure(executeErr error) {
|
||||
}
|
||||
r.logger.Errorf("善后处理完成, 日志ID:%v", r.claimedLog.ID)
|
||||
}
|
||||
|
||||
func (r *ReleaseFeedWeightTask) ResolveDeviceIDs() ([]uint, error) {
|
||||
if err := r.parseParameters(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []uint{r.feedPortDevice.ID, r.mixingTankDeviceID}, nil
|
||||
}
|
||||
|
||||
@@ -1,30 +1,71 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// Task 定义了所有可被调度器执行的任务必须实现的接口。
|
||||
type Task interface {
|
||||
// Execute 是任务的核心执行逻辑。
|
||||
// ctx: 用于控制任务的超时或取消。
|
||||
// log: 包含了当前任务执行的完整上下文信息,包括从数据库中加载的任务参数等。
|
||||
// 返回的 error 表示任务是否执行成功。调度器会根据返回的 error 是否为 nil 来决定任务状态。
|
||||
Execute() error
|
||||
|
||||
// OnFailure 定义了当 Execute 方法返回错误时,需要执行的回滚或清理逻辑。
|
||||
// log: 任务执行的上下文。
|
||||
// executeErr: 从 Execute 方法返回的原始错误。
|
||||
OnFailure(executeErr error)
|
||||
type taskFactory struct {
|
||||
logger *logs.Logger
|
||||
sensorDataRepo repository.SensorDataRepository
|
||||
deviceRepo repository.DeviceRepository
|
||||
deviceService device.Service
|
||||
}
|
||||
|
||||
// TaskFactory 是一个任务组装工厂, 可以根据Task类型获取到对应的初始化函数
|
||||
var TaskFactory = func(tt models.TaskType) Task {
|
||||
switch tt {
|
||||
func NewTaskFactory(
|
||||
logger *logs.Logger,
|
||||
sensorDataRepo repository.SensorDataRepository,
|
||||
deviceRepo repository.DeviceRepository,
|
||||
deviceService device.Service,
|
||||
) plan.TaskFactory {
|
||||
return &taskFactory{
|
||||
logger: logger,
|
||||
sensorDataRepo: sensorDataRepo,
|
||||
deviceRepo: deviceRepo,
|
||||
deviceService: deviceService,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *taskFactory) Production(claimedLog *models.TaskExecutionLog) plan.Task {
|
||||
switch claimedLog.Task.Type {
|
||||
case models.TaskTypeWaiting:
|
||||
return &DelayTask{}
|
||||
return NewDelayTask(t.logger, claimedLog)
|
||||
case models.TaskTypeReleaseFeedWeight:
|
||||
return NewReleaseFeedWeightTask(claimedLog, t.sensorDataRepo, t.deviceRepo, t.deviceService, t.logger)
|
||||
case models.TaskTypeFullCollection:
|
||||
return NewFullCollectionTask(claimedLog, t.deviceRepo, t.deviceService, t.logger)
|
||||
default:
|
||||
// 出现位置任务类型说明业务逻辑出现重大问题, 一个异常任务被创建了出来
|
||||
panic("发现未知任务类型")
|
||||
// TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型
|
||||
t.logger.Panicf("不支持的任务类型: %s", claimedLog.Task.Type)
|
||||
panic("不支持的任务类型") // 显式panic防编译器报错
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTaskFromModel 实现了 TaskFactory 接口,用于从模型创建任务实例。
|
||||
func (t *taskFactory) CreateTaskFromModel(taskModel *models.Task) (plan.TaskDeviceIDResolver, error) {
|
||||
// 这个方法不关心 claimedLog 的其他字段,所以可以构造一个临时的
|
||||
// 它只用于访问那些不依赖于执行日志的方法,比如 ResolveDeviceIDs
|
||||
tempLog := &models.TaskExecutionLog{Task: *taskModel}
|
||||
|
||||
switch taskModel.Type {
|
||||
case models.TaskTypeWaiting:
|
||||
return NewDelayTask(t.logger, tempLog), nil
|
||||
case models.TaskTypeReleaseFeedWeight:
|
||||
return NewReleaseFeedWeightTask(
|
||||
tempLog,
|
||||
t.sensorDataRepo,
|
||||
t.deviceRepo,
|
||||
t.deviceService,
|
||||
t.logger,
|
||||
), nil
|
||||
case models.TaskTypeFullCollection:
|
||||
return NewFullCollectionTask(tempLog, t.deviceRepo, t.deviceService, t.logger), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func TestGenerateToken(t *testing.T) {
|
||||
// 使用一个测试密钥初始化 TokenService
|
||||
testSecret := []byte("test_secret_key")
|
||||
service := token.NewTokenService(testSecret)
|
||||
|
||||
userID := uint(123)
|
||||
tokenString, err := service.GenerateToken(userID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("生成令牌失败: %v", err)
|
||||
}
|
||||
|
||||
if tokenString == "" {
|
||||
t.Fatal("生成的令牌字符串为空")
|
||||
}
|
||||
|
||||
// 解析 token 以确保其有效性及声明
|
||||
claims, err := service.ParseToken(tokenString)
|
||||
if err != nil {
|
||||
t.Fatalf("生成后解析令牌失败: %v", err)
|
||||
}
|
||||
|
||||
if claims.UserID != userID {
|
||||
t.Errorf("期望用户ID %d, 实际为 %d", userID, claims.UserID)
|
||||
}
|
||||
|
||||
// 检查 token 是否未过期 (在合理范围内)
|
||||
if claims.ExpiresAt == nil || claims.ExpiresAt.Time.Before(time.Now().Add(-time.Minute)) {
|
||||
t.Errorf("令牌过期时间无效或已过期")
|
||||
}
|
||||
|
||||
if claims.Issuer != "pig-farm-controller" {
|
||||
t.Errorf("期望签发者 \"pig-farm-controller\", 实际为 \"%s\"", claims.Issuer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToken(t *testing.T) {
|
||||
// 使用两个不同的测试密钥
|
||||
correctSecret := []byte("the_correct_secret")
|
||||
wrongSecret := []byte("a_very_wrong_secret")
|
||||
|
||||
serviceWithCorrectKey := token.NewTokenService(correctSecret)
|
||||
serviceWithWrongKey := token.NewTokenService(wrongSecret)
|
||||
|
||||
userID := uint(456)
|
||||
|
||||
// 1. 生成一个有效的 token
|
||||
validToken, err := serviceWithCorrectKey.GenerateToken(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("为解析测试生成有效令牌失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试用例 1: 使用正确的密钥成功解析
|
||||
claims, err := serviceWithCorrectKey.ParseToken(validToken)
|
||||
if err != nil {
|
||||
t.Errorf("使用正确密钥解析有效令牌失败: %v", err)
|
||||
}
|
||||
if claims.UserID != userID {
|
||||
t.Errorf("解析有效令牌时期望用户ID %d, 实际为 %d", userID, claims.UserID)
|
||||
}
|
||||
|
||||
// 测试用例 2: 无效 token (例如, 格式错误的字符串)
|
||||
invalidTokenString := "this.is.not.a.valid.jwt"
|
||||
_, err = serviceWithCorrectKey.ParseToken(invalidTokenString)
|
||||
if err == nil {
|
||||
t.Error("解析格式错误的令牌意外成功")
|
||||
}
|
||||
|
||||
// 测试用C:\Users\divano\Desktop\work\AA-Pig\pig-farm-controller\internal\infra\repository\plan_repository_test.go例 3: 过期 token
|
||||
expiredClaims := token.Claims{
|
||||
UserID: userID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), // 1 小时前
|
||||
Issuer: "pig-farm-controller",
|
||||
},
|
||||
}
|
||||
expiredTokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, expiredClaims)
|
||||
expiredTokenString, err := expiredTokenClaims.SignedString(correctSecret)
|
||||
if err != nil {
|
||||
t.Fatalf("生成过期令牌失败: %v", err)
|
||||
}
|
||||
_, err = serviceWithCorrectKey.ParseToken(expiredTokenString)
|
||||
if err == nil {
|
||||
t.Error("解析过期令牌意外成功")
|
||||
}
|
||||
|
||||
// 新增测试用例 4: 使用错误的密钥解析
|
||||
_, err = serviceWithWrongKey.ParseToken(validToken)
|
||||
if err == nil {
|
||||
t.Error("使用错误密钥解析令牌意外成功")
|
||||
}
|
||||
// 我们可以更精确地检查错误类型,以确保它是签名错误
|
||||
if !errors.Is(err, jwt.ErrTokenSignatureInvalid) {
|
||||
t.Errorf("期望得到签名无效错误 (ErrTokenSignatureInvalid),但得到了: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -200,13 +200,18 @@ type LarkConfig struct {
|
||||
|
||||
// CollectionConfig 代表定时采集配置
|
||||
type CollectionConfig struct {
|
||||
// Interval 采集间隔(分钟), 默认 1
|
||||
Interval int `yaml:"interval"`
|
||||
}
|
||||
|
||||
// NewConfig 创建并返回一个新的配置实例
|
||||
func NewConfig() *Config {
|
||||
// 默认值可以在这里设置,但我们优先使用配置文件中的值
|
||||
return &Config{}
|
||||
return &Config{
|
||||
Collection: CollectionConfig{
|
||||
Interval: 1, // 默认为1分钟
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Load 从指定路径加载配置文件
|
||||
|
||||
@@ -177,13 +177,13 @@ func (ps *PostgresStorage) creatingHyperTable() error {
|
||||
for _, table := range tablesToConvert {
|
||||
tableName := table.model.TableName()
|
||||
chunkInterval := "1 days" // 统一设置为1天
|
||||
ps.logger.Infow("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval)
|
||||
ps.logger.Debugw("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval)
|
||||
sql := fmt.Sprintf("SELECT create_hypertable('%s', '%s', chunk_time_interval => INTERVAL '%s', if_not_exists => TRUE);", tableName, table.timeColumn, chunkInterval)
|
||||
if err := ps.db.Exec(sql).Error; err != nil {
|
||||
ps.logger.Errorw("转换为超表失败", "table", tableName, "error", err)
|
||||
return fmt.Errorf("将 %s 转换为超表失败: %w", tableName, err)
|
||||
}
|
||||
ps.logger.Infow("成功将表转换为超表 (或已转换)", "table", tableName)
|
||||
ps.logger.Debugw("成功将表转换为超表 (或已转换)", "table", tableName)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -220,22 +220,23 @@ func (ps *PostgresStorage) applyCompressionPolicies() error {
|
||||
compressAfter := "3 days" // 统一设置为2天后(即进入第3天)开始压缩
|
||||
|
||||
// 1. 开启表的压缩设置,并指定分段列
|
||||
ps.logger.Infow("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn)
|
||||
ps.logger.Debugw("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn)
|
||||
// 使用 + 而非Sprintf以规避goland静态检查报错
|
||||
alterSQL := "ALTER TABLE" + " " + tableName + " SET (timescaledb.compress, timescaledb.compress_segmentby = '" + policy.segmentColumn + "');"
|
||||
if err := ps.db.Exec(alterSQL).Error; err != nil {
|
||||
// 忽略错误,因为这个设置可能是不可变的,重复执行会报错
|
||||
ps.logger.Warnw("启用压缩设置时遇到问题 (可能已设置,可忽略)", "table", tableName, "error", err)
|
||||
}
|
||||
ps.logger.Debugw("成功为表启用压缩设置 (或已启用)", "table", tableName)
|
||||
|
||||
// 2. 添加压缩策略
|
||||
ps.logger.Infow("为表添加压缩策略", "table", tableName, "compress_after", compressAfter)
|
||||
ps.logger.Debugw("为表添加压缩策略", "table", tableName, "compress_after", compressAfter)
|
||||
policySQL := fmt.Sprintf("SELECT add_compression_policy('%s', INTERVAL '%s', if_not_exists => TRUE);", tableName, compressAfter)
|
||||
if err := ps.db.Exec(policySQL).Error; err != nil {
|
||||
ps.logger.Errorw("添加压缩策略失败", "table", tableName, "error", err)
|
||||
return fmt.Errorf("为 %s 添加压缩策略失败: %w", tableName, err)
|
||||
}
|
||||
ps.logger.Infow("成功为表添加压缩策略 (或已存在)", "table", tableName)
|
||||
ps.logger.Debugw("成功为表添加压缩策略 (或已存在)", "table", tableName)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -247,22 +248,22 @@ func (ps *PostgresStorage) creatingIndex() error {
|
||||
// 如果索引已存在,此命令不会报错
|
||||
|
||||
// 为 sensor_data 表的 data 字段创建 GIN 索引
|
||||
ps.logger.Info("正在为 sensor_data 表的 data 字段创建 GIN 索引")
|
||||
ps.logger.Debug("正在为 sensor_data 表的 data 字段创建 GIN 索引")
|
||||
ginSensorDataIndexSQL := "CREATE INDEX IF NOT EXISTS idx_sensor_data_data_gin ON sensor_data USING GIN (data);"
|
||||
if err := ps.db.Exec(ginSensorDataIndexSQL).Error; err != nil {
|
||||
ps.logger.Errorw("为 sensor_data 的 data 字段创建 GIN 索引失败", "error", err)
|
||||
return fmt.Errorf("为 sensor_data 的 data 字段创建 GIN 索引失败: %w", err)
|
||||
}
|
||||
ps.logger.Info("成功为 sensor_data 的 data 字段创建 GIN 索引 (或已存在)")
|
||||
ps.logger.Debug("成功为 sensor_data 的 data 字段创建 GIN 索引 (或已存在)")
|
||||
|
||||
// 为 tasks.parameters 创建 GIN 索引
|
||||
ps.logger.Info("正在为 tasks 表的 parameters 字段创建 GIN 索引")
|
||||
ps.logger.Debug("正在为 tasks 表的 parameters 字段创建 GIN 索引")
|
||||
taskGinIndexSQL := "CREATE INDEX IF NOT EXISTS idx_tasks_parameters_gin ON tasks USING GIN (parameters);"
|
||||
if err := ps.db.Exec(taskGinIndexSQL).Error; err != nil {
|
||||
ps.logger.Errorw("为 tasks 的 parameters 字段创建 GIN 索引失败", "error", err)
|
||||
return fmt.Errorf("为 tasks 的 parameters 字段创建 GIN 索引失败: %w", err)
|
||||
}
|
||||
ps.logger.Info("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
|
||||
ps.logger.Debug("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
package logs_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// captureOutput 是一个辅助函数,用于捕获 logger 的输出到内存缓冲区
|
||||
func captureOutput(cfg config.LogConfig) (*logs.Logger, *bytes.Buffer) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
encoder := logs.GetEncoder(cfg.Format)
|
||||
|
||||
writer := zapcore.AddSync(&buf)
|
||||
|
||||
level := zap.NewAtomicLevel()
|
||||
_ = level.UnmarshalText([]byte(cfg.Level))
|
||||
|
||||
core := zapcore.NewCore(encoder, writer, level)
|
||||
// 匹配 logs.go 中 NewLogger 的行为,添加调用者信息
|
||||
zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
||||
|
||||
logger := &logs.Logger{SugaredLogger: zapLogger.Sugar()}
|
||||
return logger, &buf
|
||||
}
|
||||
func TestNewLogger(t *testing.T) {
|
||||
t.Run("日志级别应生效", func(t *testing.T) {
|
||||
// 1. 创建一个级别为 WARN 的 logger
|
||||
logger, buf := captureOutput(config.LogConfig{Level: "warn", Format: "console"})
|
||||
|
||||
// 2. 调用不同级别的日志方法
|
||||
logger.Info("这条 info 日志不应被打印")
|
||||
logger.Warn("这条 warn 日志应该被打印")
|
||||
|
||||
// 3. 断言输出
|
||||
output := buf.String()
|
||||
assert.NotContains(t, output, "这条 info 日志不应被打印")
|
||||
assert.Contains(t, output, "这条 warn 日志应该被打印")
|
||||
})
|
||||
|
||||
t.Run("JSON 格式应生效", func(t *testing.T) {
|
||||
// 1. 创建一个格式为 JSON 的 logger
|
||||
logger, buf := captureOutput(config.LogConfig{Level: "info", Format: "json"})
|
||||
|
||||
// 2. 打印一条日志
|
||||
logger.Info("测试json输出")
|
||||
|
||||
// 3. 断言输出
|
||||
output := buf.String()
|
||||
// 验证它是否是合法的 JSON,并且包含预期的键值对
|
||||
var logEntry map[string]interface{}
|
||||
// 注意:由于日志库可能会在行尾添加换行符,我们先 trim space
|
||||
err := json.Unmarshal([]byte(strings.TrimSpace(output)), &logEntry)
|
||||
assert.NoError(t, err, "日志输出应为合法的JSON")
|
||||
assert.Equal(t, "INFO", logEntry["level"])
|
||||
assert.Equal(t, "测试json输出", logEntry["msg"])
|
||||
})
|
||||
|
||||
t.Run("文件日志构造函数不应 panic", func(t *testing.T) {
|
||||
// 这个测试保持原样,只验证构造函数在启用文件时不会崩溃
|
||||
// 注意:我们不在单元测试中实际写入文件
|
||||
cfgFile := config.LogConfig{
|
||||
Level: "info",
|
||||
EnableFile: true,
|
||||
FilePath: "test.log", // 在测试环境中,这个文件不会被真正创建
|
||||
}
|
||||
assert.NotPanics(t, func() { logs.NewLogger(cfgFile) })
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogger_Write_ForGin(t *testing.T) {
|
||||
logger, buf := captureOutput(config.LogConfig{Level: "info"})
|
||||
|
||||
ginLog := "[GIN-debug] Listening and serving HTTP on :8080\n"
|
||||
_, err := logger.Write([]byte(ginLog))
|
||||
|
||||
assert.NoError(t, err)
|
||||
output := buf.String()
|
||||
// logger.Write 会将 gin 的日志转为 info 级别
|
||||
assert.Contains(t, output, "INFO")
|
||||
assert.Contains(t, output, strings.TrimSpace(ginLog))
|
||||
}
|
||||
|
||||
func TestGormLogger(t *testing.T) {
|
||||
logger, buf := captureOutput(config.LogConfig{Level: "debug"}) // 设置为 debug 以捕获所有级别
|
||||
gormLogger := logs.NewGormLogger(logger)
|
||||
|
||||
// 模拟 GORM 的 Trace 调用参数
|
||||
ctx := context.Background()
|
||||
sql := "SELECT * FROM users WHERE id = 1"
|
||||
rows := int64(1)
|
||||
fc := func() (string, int64) {
|
||||
return sql, rows
|
||||
}
|
||||
|
||||
t.Run("慢查询应记录为警告", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
// 模拟一个耗时超过 200ms 的查询
|
||||
begin := time.Now().Add(-300 * time.Millisecond)
|
||||
gormLogger.Trace(ctx, begin, fc, nil)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "WARN", "应包含 WARN 级别")
|
||||
assert.Contains(t, output, "[GORM] slow query", "应包含慢查询信息")
|
||||
assert.Contains(t, output, "SELECT * FROM users WHERE id = 1", "应包含 SQL 语句")
|
||||
})
|
||||
|
||||
t.Run("普通错误应记录为Error", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
queryError := errors.New("syntax error")
|
||||
gormLogger.Trace(ctx, time.Now(), fc, queryError)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "ERROR")
|
||||
assert.Contains(t, output, "[GORM] error: syntax error")
|
||||
})
|
||||
|
||||
t.Run("当SkipErrRecordNotFound为true时应跳过RecordNotFound错误", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
// 确保默认设置是 true
|
||||
gormLogger.SkipErrRecordNotFound = true
|
||||
// 错误必须包含 "record not found" 字符串以匹配 logs.go 中的判断逻辑
|
||||
queryError := errors.New("record not found")
|
||||
gormLogger.Trace(ctx, time.Now(), fc, queryError)
|
||||
|
||||
assert.Empty(t, buf.String(), "开启 SkipErrRecordNotFound 后,record not found 错误不应产生任何日志")
|
||||
})
|
||||
|
||||
t.Run("当SkipErrRecordNotFound为false时应记录RecordNotFound错误", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
// 手动将 SkipErrRecordNotFound 设置为 false
|
||||
gormLogger.SkipErrRecordNotFound = false
|
||||
|
||||
queryError := errors.New("record not found")
|
||||
gormLogger.Trace(ctx, time.Now(), fc, queryError)
|
||||
|
||||
// 恢复设置,避免影响其他测试
|
||||
gormLogger.SkipErrRecordNotFound = true
|
||||
|
||||
output := buf.String()
|
||||
assert.NotEmpty(t, output, "关闭 SkipErrRecordNotFound 后,record not found 错误应该产生日志")
|
||||
assert.Contains(t, output, "ERROR")
|
||||
assert.Contains(t, output, "[GORM] error: record not found")
|
||||
})
|
||||
|
||||
t.Run("正常查询应记录为Debug", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
// 模拟一个快速查询
|
||||
gormLogger.Trace(ctx, time.Now(), fc, nil)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "DEBUG") // 正常查询是 Debug 级别
|
||||
assert.Contains(t, output, "[GORM] trace")
|
||||
})
|
||||
}
|
||||
@@ -77,6 +77,9 @@ type Device struct {
|
||||
// Properties 用于存储特定类型设备的独有属性,采用JSON格式。
|
||||
// 建议在应用层为不同子类型的设备定义专用的属性结构体,以保证数据一致性。
|
||||
Properties datatypes.JSON `json:"properties"`
|
||||
|
||||
// Tasks 是与此设备关联的任务列表,通过 DeviceTask 关联表实现多对多关系
|
||||
Tasks []Task `gorm:"many2many:device_tasks;" json:"tasks"`
|
||||
}
|
||||
|
||||
// SelfCheck 对 Device 的关键字段和属性进行业务逻辑验证。
|
||||
|
||||
@@ -171,6 +171,9 @@ const (
|
||||
ContextAuditActionType AuditContextKey = "auditActionType"
|
||||
ContextAuditTargetResource AuditContextKey = "auditTargetResource"
|
||||
ContextAuditDescription AuditContextKey = "auditDescription"
|
||||
ContextAuditStatus AuditContextKey = "auditStatus"
|
||||
ContextAuditResultDetails AuditContextKey = "auditResultDetails"
|
||||
|
||||
ContextUserKey AuditContextKey = "user"
|
||||
)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ func GetAllModels() []interface{} {
|
||||
&DeviceTemplate{},
|
||||
&SensorData{},
|
||||
&DeviceCommandLog{},
|
||||
&DeviceTask{},
|
||||
|
||||
// Plan & Task Models
|
||||
&Plan{},
|
||||
|
||||
@@ -34,6 +34,7 @@ const (
|
||||
TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务
|
||||
TaskTypeWaiting TaskType = "等待" // 等待任务
|
||||
TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务
|
||||
TaskTypeFullCollection TaskType = "全量采集" // 新增的全量采集任务
|
||||
)
|
||||
|
||||
// -- Task Parameters --
|
||||
@@ -52,12 +53,20 @@ const (
|
||||
PlanStatusFailed PlanStatus = "执行失败" // 执行失败
|
||||
)
|
||||
|
||||
type PlanType string
|
||||
|
||||
const (
|
||||
PlanTypeCustom PlanType = "自定义任务"
|
||||
PlanTypeSystem PlanType = "系统任务"
|
||||
)
|
||||
|
||||
// Plan 代表系统中的一个计划,可以包含子计划或任务
|
||||
type Plan struct {
|
||||
gorm.Model
|
||||
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Description string `json:"description"`
|
||||
PlanType PlanType `gorm:"not null;index" json:"plan_type"` // 任务类型, 包括系统任务和用户自定义任务
|
||||
ExecutionType PlanExecutionType `gorm:"not null;index" json:"execution_type"`
|
||||
Status PlanStatus `gorm:"default:'已禁用';index" json:"status"` // 计划是否被启动
|
||||
ExecuteNum uint `gorm:"default:0" json:"execute_num"` // 计划预期执行次数
|
||||
@@ -169,6 +178,9 @@ type Task struct {
|
||||
ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在计划中的执行顺序
|
||||
Type TaskType `gorm:"not null" json:"type"` // 任务的类型,对应 task 包中的具体动作
|
||||
Parameters datatypes.JSON `json:"parameters"` // 任务特定参数的JSON (例如: 设备ID, 值)
|
||||
|
||||
// Devices 是与此任务关联的设备列表,通过 DeviceTask 关联表实现多对多关系
|
||||
Devices []Device `gorm:"many2many:device_tasks;" json:"devices"`
|
||||
}
|
||||
|
||||
// TableName 自定义 GORM 使用的数据库表名
|
||||
@@ -188,3 +200,18 @@ func (t Task) ParseParameters(v interface{}) error {
|
||||
}
|
||||
return json.Unmarshal(t.Parameters, v)
|
||||
}
|
||||
|
||||
// DeviceTask 是设备和任务之间的关联模型,表示一个设备可以执行多个任务,一个任务可以被多个设备执行。
|
||||
type DeviceTask struct {
|
||||
gorm.Model
|
||||
DeviceID uint `gorm:"not null;index"` // 设备ID
|
||||
TaskID uint `gorm:"not null;index"` // 任务ID
|
||||
|
||||
// 可选:如果需要存储关联的额外信息,可以在这里添加字段,例如:
|
||||
// Configuration datatypes.JSON `json:"configuration"` // 任务在特定设备上的配置
|
||||
}
|
||||
|
||||
// TableName 自定义 GORM 使用的数据库表名
|
||||
func (DeviceTask) TableName() string {
|
||||
return "device_tasks"
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPlan_ReorderSteps(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
initialPlan *models.Plan
|
||||
expectedOrders []int
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
// --- Test Cases for Tasks ---
|
||||
{
|
||||
name: "Tasks: 完美顺序",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{
|
||||
{ExecutionOrder: 1},
|
||||
{ExecutionOrder: 2},
|
||||
{ExecutionOrder: 3},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "Tasks: 有间断",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{
|
||||
{ExecutionOrder: 1},
|
||||
{ExecutionOrder: 3},
|
||||
{ExecutionOrder: 5},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "Tasks: 从0开始",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{
|
||||
{ExecutionOrder: 0},
|
||||
{ExecutionOrder: 1},
|
||||
{ExecutionOrder: 2},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "Tasks: 完全无序",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{
|
||||
{ExecutionOrder: 8},
|
||||
{ExecutionOrder: 2},
|
||||
{ExecutionOrder: 4},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "Tasks: 包含负数",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{
|
||||
{ExecutionOrder: -5},
|
||||
{ExecutionOrder: 10},
|
||||
{ExecutionOrder: 2},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "Tasks: 空切片",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{},
|
||||
},
|
||||
expectedOrders: []int{},
|
||||
},
|
||||
{
|
||||
name: "Tasks: 单个元素",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{
|
||||
{ExecutionOrder: 100},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1},
|
||||
},
|
||||
// --- Test Cases for SubPlans ---
|
||||
{
|
||||
name: "SubPlans: 完美顺序",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeSubPlans,
|
||||
SubPlans: []models.SubPlan{
|
||||
{ExecutionOrder: 1},
|
||||
{ExecutionOrder: 2},
|
||||
{ExecutionOrder: 3},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "SubPlans: 有间断",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeSubPlans,
|
||||
SubPlans: []models.SubPlan{
|
||||
{ExecutionOrder: 1},
|
||||
{ExecutionOrder: 3},
|
||||
{ExecutionOrder: 5},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "SubPlans: 从0开始",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeSubPlans,
|
||||
SubPlans: []models.SubPlan{
|
||||
{ExecutionOrder: 0},
|
||||
{ExecutionOrder: 1},
|
||||
{ExecutionOrder: 2},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "SubPlans: 完全无序",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeSubPlans,
|
||||
SubPlans: []models.SubPlan{
|
||||
{ExecutionOrder: 8},
|
||||
{ExecutionOrder: 2},
|
||||
{ExecutionOrder: 4},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "SubPlans: 包含负数",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeSubPlans,
|
||||
SubPlans: []models.SubPlan{
|
||||
{ExecutionOrder: -5},
|
||||
{ExecutionOrder: 10},
|
||||
{ExecutionOrder: 2},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "SubPlans: 空切片",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeSubPlans,
|
||||
SubPlans: []models.SubPlan{},
|
||||
},
|
||||
expectedOrders: []int{},
|
||||
},
|
||||
{
|
||||
name: "SubPlans: 单个元素",
|
||||
initialPlan: &models.Plan{
|
||||
ContentType: models.PlanContentTypeSubPlans,
|
||||
SubPlans: []models.SubPlan{
|
||||
{ExecutionOrder: 100},
|
||||
},
|
||||
},
|
||||
expectedOrders: []int{1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// 调用被测试的方法
|
||||
tc.initialPlan.ReorderSteps()
|
||||
|
||||
// 提取并验证最终的顺序
|
||||
finalOrders := make([]int, 0)
|
||||
if tc.initialPlan.ContentType == models.PlanContentTypeTasks {
|
||||
for _, task := range tc.initialPlan.Tasks {
|
||||
finalOrders = append(finalOrders, task.ExecutionOrder)
|
||||
}
|
||||
} else if tc.initialPlan.ContentType == models.PlanContentTypeSubPlans {
|
||||
for _, subPlan := range tc.initialPlan.SubPlans {
|
||||
finalOrders = append(finalOrders, subPlan.ExecutionOrder)
|
||||
}
|
||||
}
|
||||
|
||||
// 对 finalOrders 进行排序,以确保比较的一致性,因为 ReorderSteps 后的顺序是固定的
|
||||
sort.Ints(finalOrders)
|
||||
|
||||
assert.Equal(t, tc.expectedOrders, finalOrders, "The final execution orders should be a continuous sequence starting from 1.")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Package models_test 包含对 models 包的单元测试
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestUser_CheckPassword(t *testing.T) {
|
||||
plainPassword := "my-secret-password"
|
||||
|
||||
// 1. 生成一个密码哈希用于测试
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost)
|
||||
assert.NoError(t, err, "生成密码哈希不应出错")
|
||||
|
||||
user := &models.User{
|
||||
Password: string(hashedPassword),
|
||||
}
|
||||
|
||||
t.Run("密码正确", func(t *testing.T) {
|
||||
// 2. 使用正确的明文密码进行校验
|
||||
match := user.CheckPassword(plainPassword)
|
||||
assert.True(t, match, "正确的密码应该校验通过")
|
||||
})
|
||||
|
||||
t.Run("密码错误", func(t *testing.T) {
|
||||
// 3. 使用错误的明文密码进行校验
|
||||
match := user.CheckPassword("wrong-password")
|
||||
assert.False(t, match, "错误的密码应该校验失败")
|
||||
})
|
||||
|
||||
t.Run("空密码", func(t *testing.T) {
|
||||
// 4. 使用空字符串作为密码进行校验
|
||||
match := user.CheckPassword("")
|
||||
assert.False(t, match, "空密码应该校验失败")
|
||||
})
|
||||
}
|
||||
func TestUser_BeforeCreate(t *testing.T) {
|
||||
t.Run("密码应被成功哈希", func(t *testing.T) {
|
||||
plainPassword := "securepassword123"
|
||||
user := &models.User{
|
||||
Username: "testuser",
|
||||
Password: plainPassword,
|
||||
}
|
||||
|
||||
// 模拟 GORM 钩子调用
|
||||
err := user.BeforeCreate(nil) // GORM 钩子通常接收 *gorm.DB,这里我们传入 nil,因为 BeforeCreate 不依赖 DB
|
||||
assert.NoError(t, err, "BeforeCreate 不应返回错误")
|
||||
|
||||
// 验证密码是否已被哈希(不再是明文)
|
||||
assert.NotEqual(t, plainPassword, user.Password, "密码应已被哈希")
|
||||
|
||||
// 验证哈希后的密码是否能被正确校验
|
||||
assert.True(t, user.CheckPassword(plainPassword), "哈希后的密码应能通过校验")
|
||||
})
|
||||
|
||||
t.Run("空密码不应被哈希", func(t *testing.T) {
|
||||
plainPassword := ""
|
||||
user := &models.User{
|
||||
Username: "empty_pass_user",
|
||||
Password: plainPassword,
|
||||
}
|
||||
|
||||
// 模拟 GORM 钩子调用
|
||||
err := user.BeforeCreate(nil)
|
||||
assert.NoError(t, err, "BeforeCreate 不应返回错误")
|
||||
|
||||
// 验证密码仍然是空字符串
|
||||
assert.Equal(t, plainPassword, user.Password, "空密码不应被哈希")
|
||||
})
|
||||
}
|
||||
@@ -47,26 +47,11 @@ func (r *gormAreaControllerRepository) Update(ac *models.AreaController) error {
|
||||
}
|
||||
|
||||
// Delete 删除一个 AreaController 记录。
|
||||
// 在删除前会检查是否有设备关联到该主控,如果有,则不允许删除。
|
||||
func (r *gormAreaControllerRepository) Delete(id uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 检查是否有设备关联到这个区域主控
|
||||
var count int64
|
||||
if err := tx.Model(&models.Device{}).Where("area_controller_id = ?", id).Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("检查关联设备失败: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return fmt.Errorf("无法删除区域主控,因为仍有 %d 个设备关联到它", count)
|
||||
}
|
||||
|
||||
// 如果没有关联设备,则执行删除操作
|
||||
if err := tx.Delete(&models.AreaController{}, id).Error; err != nil {
|
||||
if err := r.db.Delete(&models.AreaController{}, id).Error; err != nil {
|
||||
return fmt.Errorf("删除区域主控失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// FindByID 通过 ID 查找一个 AreaController。
|
||||
|
||||
@@ -17,7 +17,7 @@ type DeviceRepository interface {
|
||||
// FindByID 根据主键 ID 查找设备
|
||||
FindByID(id uint) (*models.Device, error)
|
||||
|
||||
// FindByIDString 根据字符串形式的主键 ID 查找设备,方便控制器调用
|
||||
// FindByIDString 根据字符串形式的主键 ID 查找设备
|
||||
FindByIDString(id string) (*models.Device, error)
|
||||
|
||||
// ListAll 获取所有设备的列表
|
||||
@@ -26,7 +26,7 @@ type DeviceRepository interface {
|
||||
// ListAllSensors 获取所有传感器类型的设备列表
|
||||
ListAllSensors() ([]*models.Device, error)
|
||||
|
||||
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备。
|
||||
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备
|
||||
ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error)
|
||||
|
||||
// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备
|
||||
@@ -40,6 +40,15 @@ type DeviceRepository interface {
|
||||
|
||||
// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备
|
||||
FindByAreaControllerAndPhysicalAddress(areaControllerID uint, busNumber int, busAddress int) (*models.Device, error)
|
||||
|
||||
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
|
||||
GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error)
|
||||
|
||||
// IsDeviceInUse 检查设备是否被任何任务使用
|
||||
IsDeviceInUse(deviceID uint) (bool, error)
|
||||
|
||||
// IsAreaControllerInUse 检查区域主控是否被任何设备使用
|
||||
IsAreaControllerInUse(areaControllerID uint) (bool, error)
|
||||
}
|
||||
|
||||
// gormDeviceRepository 是 DeviceRepository 的 GORM 实现
|
||||
@@ -66,6 +75,18 @@ func (r *gormDeviceRepository) FindByID(id uint) (*models.Device, error) {
|
||||
return &device, nil
|
||||
}
|
||||
|
||||
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
|
||||
func (r *gormDeviceRepository) GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error) {
|
||||
var devices []models.Device
|
||||
if len(ids) == 0 {
|
||||
return devices, nil
|
||||
}
|
||||
if err := tx.Where("id IN ?", ids).Find(&devices).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
// FindByIDString 根据字符串形式的主键 ID 查找设备
|
||||
func (r *gormDeviceRepository) FindByIDString(id string) (*models.Device, error) {
|
||||
// 将字符串ID转换为uint64
|
||||
@@ -146,3 +167,23 @@ func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(areaContro
|
||||
}
|
||||
return &device, nil
|
||||
}
|
||||
|
||||
// IsDeviceInUse 检查设备是否被任何任务使用
|
||||
func (r *gormDeviceRepository) IsDeviceInUse(deviceID uint) (bool, error) {
|
||||
var count int64
|
||||
// 直接对 device_tasks 关联表进行 COUNT 操作,性能最高
|
||||
err := r.db.Model(&models.DeviceTask{}).Where("device_id = ?", deviceID).Count(&count).Error
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("查询设备任务关联失败: %w", err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// IsAreaControllerInUse 检查区域主控是否被任何设备使用
|
||||
func (r *gormDeviceRepository) IsAreaControllerInUse(areaControllerID uint) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil {
|
||||
return false, fmt.Errorf("检查区域主控使用情况失败: %w", err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
@@ -83,12 +83,8 @@ func (r *gormDeviceTemplateRepository) IsInUse(id uint) (bool, error) {
|
||||
|
||||
// Delete 软删除数据库中的设备模板
|
||||
func (r *gormDeviceTemplateRepository) Delete(id uint) error {
|
||||
inUse, err := r.IsInUse(id)
|
||||
if err != nil {
|
||||
return err
|
||||
if err := r.db.Delete(&models.DeviceTemplate{}, id).Error; err != nil {
|
||||
return fmt.Errorf("删除设备模板失败: %w", err)
|
||||
}
|
||||
if inUse {
|
||||
return errors.New("设备模板正在被设备使用,无法删除")
|
||||
}
|
||||
return r.db.Delete(&models.DeviceTemplate{}, id).Error
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// setupTestDB 是一个共享的辅助函数,用于为集成测试创建一个干净的、内存中的 SQLite 数据库实例。
|
||||
func setupTestDB(t *testing.T) *gorm.DB {
|
||||
// "file::memory:?cache=shared" 是 GORM 连接内存 SQLite 的标准方式,确保在同一测试中的不同连接可以访问相同的数据,而我们显然不需要这个
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
assert.NoError(t, err, "连接内存数据库时发生错误")
|
||||
|
||||
// 自动迁移所有需要的表结构
|
||||
err = db.AutoMigrate(models.GetAllModels()...)
|
||||
assert.NoError(t, err, "数据库迁移时发生错误")
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// TestMain 是一个特殊的函数,它会在包内的所有测试运行之前被调用。
|
||||
// 我们可以在这里进行一些全局的设置和清理工作。
|
||||
func TestMain(m *testing.M) {
|
||||
// 在所有测试运行前可以执行一些设置代码
|
||||
|
||||
// 运行包中的所有测试
|
||||
code := m.Run()
|
||||
|
||||
// 在所有测试运行后可以执行一些清理代码
|
||||
|
||||
// 退出测试
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -21,18 +21,38 @@ var (
|
||||
ErrDeleteWithReferencedPlan = errors.New("禁止删除正在被引用的计划")
|
||||
)
|
||||
|
||||
// PlanTypeFilter 定义计划类型的过滤器
|
||||
type PlanTypeFilter string
|
||||
|
||||
const (
|
||||
PlanTypeFilterAll PlanTypeFilter = "所有任务"
|
||||
PlanTypeFilterCustom PlanTypeFilter = "自定义任务"
|
||||
PlanTypeFilterSystem PlanTypeFilter = "系统任务"
|
||||
)
|
||||
|
||||
// ListPlansOptions 定义了查询计划时的可选参数
|
||||
type ListPlansOptions struct {
|
||||
PlanType PlanTypeFilter
|
||||
}
|
||||
|
||||
// PlanRepository 定义了与计划模型相关的数据库操作接口
|
||||
// 这是为了让业务逻辑层依赖于抽象,而不是具体的数据库实现
|
||||
type PlanRepository interface {
|
||||
// ListBasicPlans 获取所有计划的基本信息,不包含子计划和任务详情
|
||||
ListBasicPlans() ([]models.Plan, error)
|
||||
// ListPlans 获取计划列表,支持过滤和分页
|
||||
ListPlans(opts ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)
|
||||
// GetBasicPlanByID 根据ID获取计划的基本信息,不包含子计划和任务详情
|
||||
GetBasicPlanByID(id uint) (*models.Plan, error)
|
||||
// GetPlanByID 根据ID获取计划,包含子计划和任务详情
|
||||
GetPlanByID(id uint) (*models.Plan, error)
|
||||
// GetPlansByIDs 根据ID列表获取计划,不包含子计划和任务详情
|
||||
GetPlansByIDs(ids []uint) ([]models.Plan, error)
|
||||
// CreatePlan 创建一个新的计划
|
||||
CreatePlan(plan *models.Plan) error
|
||||
// UpdatePlan 更新计划,包括子计划和任务
|
||||
// CreatePlanTx 在指定事务中创建一个新的计划
|
||||
CreatePlanTx(tx *gorm.DB, plan *models.Plan) error
|
||||
// UpdatePlanMetadataAndStructure 更新计划的元数据和结构,但不包括状态等运行时信息
|
||||
UpdatePlanMetadataAndStructure(plan *models.Plan) error
|
||||
// UpdatePlan 更新计划的所有字段
|
||||
UpdatePlan(plan *models.Plan) error
|
||||
// UpdatePlanStatus 更新指定计划的状态
|
||||
UpdatePlanStatus(id uint, status models.PlanStatus) error
|
||||
@@ -59,9 +79,6 @@ type PlanRepository interface {
|
||||
// FindPlansWithPendingTasks 查找所有正在执行的计划
|
||||
FindPlansWithPendingTasks() ([]*models.Plan, error)
|
||||
|
||||
// DB 返回底层的数据库连接实例,用于服务层事务
|
||||
DB() *gorm.DB
|
||||
|
||||
// StopPlanTransactionally 停止一个计划的执行,包括更新状态、移除待执行任务和更新执行日志
|
||||
StopPlanTransactionally(planID uint) error
|
||||
|
||||
@@ -81,15 +98,37 @@ func NewGormPlanRepository(db *gorm.DB) PlanRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// ListBasicPlans 获取所有计划的基本信息,不包含子计划和任务详情
|
||||
func (r *gormPlanRepository) ListBasicPlans() ([]models.Plan, error) {
|
||||
var plans []models.Plan
|
||||
// GORM 默认不会加载关联,除非使用 Preload,所以直接 Find 即可满足要求
|
||||
result := r.db.Find(&plans)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
// ListPlans 获取计划列表,支持过滤和分页
|
||||
func (r *gormPlanRepository) ListPlans(opts ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) {
|
||||
if page <= 0 || pageSize <= 0 {
|
||||
return nil, 0, ErrInvalidPagination
|
||||
}
|
||||
return plans, nil
|
||||
|
||||
var plans []models.Plan
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.Plan{})
|
||||
|
||||
switch opts.PlanType {
|
||||
case PlanTypeFilterCustom:
|
||||
query = query.Where("plan_type = ?", models.PlanTypeCustom)
|
||||
case PlanTypeFilterSystem:
|
||||
query = query.Where("plan_type = ?", models.PlanTypeSystem)
|
||||
case PlanTypeFilterAll:
|
||||
// 不添加 plan_type 的过滤条件
|
||||
default:
|
||||
// 默认查询自定义
|
||||
query = query.Where("plan_type = ?", models.PlanTypeCustom)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Limit(pageSize).Offset(offset).Order("id DESC").Find(&plans).Error
|
||||
|
||||
return plans, total, err
|
||||
}
|
||||
|
||||
// GetBasicPlanByID 根据ID获取计划的基本信息,不包含子计划和任务详情
|
||||
@@ -103,6 +142,19 @@ func (r *gormPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) {
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
// GetPlansByIDs 根据ID列表获取计划,不包含子计划和任务详情
|
||||
func (r *gormPlanRepository) GetPlansByIDs(ids []uint) ([]models.Plan, error) {
|
||||
var plans []models.Plan
|
||||
if len(ids) == 0 {
|
||||
return plans, nil
|
||||
}
|
||||
err := r.db.Where("id IN ?", ids).Find(&plans).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
// GetPlanByID 根据ID获取计划,包含子计划和任务详情
|
||||
func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
|
||||
var plan models.Plan
|
||||
@@ -150,7 +202,11 @@ func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
|
||||
|
||||
// CreatePlan 创建一个新的计划
|
||||
func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
return r.CreatePlanTx(r.db, plan)
|
||||
}
|
||||
|
||||
// CreatePlanTx 在指定事务中创建一个新的计划
|
||||
func (r *gormPlanRepository) CreatePlanTx(tx *gorm.DB, plan *models.Plan) error {
|
||||
// 1. 前置校验
|
||||
if plan.ID != 0 {
|
||||
return ErrCreateWithNonZeroID
|
||||
@@ -193,7 +249,8 @@ func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error {
|
||||
}
|
||||
|
||||
// 2. 创建根计划
|
||||
// GORM 会自动处理关联的 Tasks (如果 ContentType 是 tasks 且 Task.ID 为 0)
|
||||
// GORM 会自动处理关联的 Tasks (如果 ContentType 是 tasks 且 Task.ID 为 0),
|
||||
// 以及 Tasks 内部已经填充好的 Devices 关联。
|
||||
if err := tx.Create(plan).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,18 +262,22 @@ func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdatePlan 是更新计划的公共入口点
|
||||
// UpdatePlan 更新计划
|
||||
func (r *gormPlanRepository) UpdatePlan(plan *models.Plan) error {
|
||||
return r.db.Save(plan).Error
|
||||
}
|
||||
|
||||
// UpdatePlanMetadataAndStructure 是更新计划元数据和结构的公共入口点
|
||||
func (r *gormPlanRepository) UpdatePlanMetadataAndStructure(plan *models.Plan) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
return r.updatePlanTx(tx, plan)
|
||||
return r.updatePlanMetadataAndStructureTx(tx, plan)
|
||||
})
|
||||
}
|
||||
|
||||
// updatePlanTx 在事务中协调整个更新过程
|
||||
func (r *gormPlanRepository) updatePlanTx(tx *gorm.DB, plan *models.Plan) error {
|
||||
// updatePlanMetadataAndStructureTx 在事务中协调整个更新过程
|
||||
func (r *gormPlanRepository) updatePlanMetadataAndStructureTx(tx *gorm.DB, plan *models.Plan) error {
|
||||
if err := r.validatePlanTree(tx, plan); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -359,9 +420,7 @@ func (r *gormPlanRepository) reconcileTasks(tx *gorm.DB, plan *models.Plan) erro
|
||||
}
|
||||
|
||||
if len(tasksToDelete) > 0 {
|
||||
if err := tx.Delete(&models.Task{}, tasksToDelete).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.deleteTasksTx(tx, tasksToDelete)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -500,42 +559,43 @@ func (r *gormPlanRepository) flattenPlanTasksRecursive(plan *models.Plan) ([]mod
|
||||
func (r *gormPlanRepository) DeleteTask(id int) error {
|
||||
// 使用事务确保操作的原子性
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
return r.deleteTask(tx, id)
|
||||
return r.deleteTasksTx(tx, []int{id})
|
||||
})
|
||||
}
|
||||
|
||||
// deleteTask 根据ID删除任务
|
||||
func (r *gormPlanRepository) deleteTask(tx *gorm.DB, id int) error {
|
||||
// 1. 检查是否有待执行任务引用了这个任务
|
||||
// deleteTasksTx 在事务中批量软删除任务,并物理删除其在关联表中的记录
|
||||
func (r *gormPlanRepository) deleteTasksTx(tx *gorm.DB, ids []int) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否有待执行任务引用了这些任务
|
||||
var pendingTaskCount int64
|
||||
if err := tx.Model(&models.PendingTask{}).Where("task_id = ?", id).Count(&pendingTaskCount).Error; err != nil {
|
||||
if err := tx.Model(&models.PendingTask{}).Where("task_id IN ?", ids).Count(&pendingTaskCount).Error; err != nil {
|
||||
return fmt.Errorf("检查待执行任务时出错: %w", err)
|
||||
}
|
||||
|
||||
// 如果有待执行任务引用该任务,不能删除
|
||||
if pendingTaskCount > 0 {
|
||||
return fmt.Errorf("无法删除任务(ID: %d),因为存在 %d 条待执行任务引用该任务", id, pendingTaskCount)
|
||||
return fmt.Errorf("无法删除任务,因为存在 %d 条待执行任务引用这些任务", pendingTaskCount)
|
||||
}
|
||||
|
||||
// 2. 检查是否有计划仍在使用这个任务
|
||||
var planCount int64
|
||||
if err := tx.Model(&models.Plan{}).Joins("JOIN tasks ON plans.id = tasks.plan_id").Where("tasks.id = ?", id).Count(&planCount).Error; err != nil {
|
||||
return fmt.Errorf("检查计划引用任务时出错: %w", err)
|
||||
// 因为钩子函数在批量删除中不会被触发, 所以手动删除关联表, 通过批量删除语句优化性能
|
||||
|
||||
// 1. 直接、高效地从关联表中物理删除所有相关记录
|
||||
// 这是最关键的优化,避免了不必要的查询和循环
|
||||
if err := tx.Where("task_id IN ?", ids).Delete(&models.DeviceTask{}).Error; err != nil {
|
||||
return fmt.Errorf("清理任务的设备关联失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果有计划在使用该任务,不能删除
|
||||
if planCount > 0 {
|
||||
return fmt.Errorf("无法删除任务(ID: %d),因为存在 %d 个计划仍在使用该任务", id, planCount)
|
||||
}
|
||||
|
||||
// 3. 执行删除操作
|
||||
result := tx.Delete(&models.Task{}, id)
|
||||
// 2. 对任务本身进行软删除
|
||||
result := tx.Delete(&models.Task{}, ids)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("删除任务失败: %w", result.Error)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// 检查是否实际删除了记录
|
||||
if result.RowsAffected == 0 {
|
||||
// 3. 如果是单个删除且未找到记录,则返回错误
|
||||
if len(ids) == 1 && result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
@@ -657,11 +717,6 @@ func (r *gormPlanRepository) FindPlansWithPendingTasks() ([]*models.Plan, error)
|
||||
return plans, err
|
||||
}
|
||||
|
||||
// DB 返回底层的数据库连接实例
|
||||
func (r *gormPlanRepository) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
|
||||
// StopPlanTransactionally 停止一个计划的执行,包括更新状态、移除待执行任务和更新执行日志。
|
||||
func (r *gormPlanRepository) StopPlanTransactionally(planID uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
@@ -3,4 +3,4 @@ package repository
|
||||
import "errors"
|
||||
|
||||
// ErrInvalidPagination 表示分页参数无效
|
||||
var ErrInvalidPagination = errors.New("无效的分页参数:page和pageSize必须为大于0")
|
||||
var ErrInvalidPagination = errors.New("无效的分页参数:page和page_size必须为大于0")
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
// Package repository_test 包含对 repository 包的集成测试
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestGormUserRepository(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := repository.NewGormUserRepository(db)
|
||||
|
||||
plainPassword := "my-secret-password"
|
||||
userToCreate := &models.User{
|
||||
Username: "testuser",
|
||||
Password: plainPassword, // 我们提供的是明文密码
|
||||
}
|
||||
|
||||
t.Run("创建 - 成功创建并验证密码哈希", func(t *testing.T) {
|
||||
err := repo.Create(userToCreate)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证用户已被创建
|
||||
assert.NotZero(t, userToCreate.ID)
|
||||
|
||||
// 从数据库中直接取回记录,以验证 BeforeSave 钩子是否生效
|
||||
var savedUser models.User
|
||||
db.First(&savedUser, userToCreate.ID)
|
||||
|
||||
// 验证密码字段存储的不是明文
|
||||
assert.NotEqual(t, plainPassword, savedUser.Password, "数据库中存储的密码不应是明文")
|
||||
|
||||
// 验证存储的哈希是正确的
|
||||
assert.True(t, savedUser.CheckPassword(plainPassword), "存储的密码哈希应该能与原明文匹配")
|
||||
})
|
||||
|
||||
t.Run("创建 - 用户名冲突", func(t *testing.T) {
|
||||
// 尝试创建一个同名用户
|
||||
duplicateUser := &models.User{Username: "testuser", Password: "anypassword"}
|
||||
err := repo.Create(duplicateUser)
|
||||
|
||||
// 我们期望一个错误,因为用户名是唯一的
|
||||
assert.Error(t, err, "创建同名用户应该返回错误")
|
||||
// 更精确地,可以检查是否是唯一键冲突错误
|
||||
assert.Contains(t, err.Error(), "UNIQUE constraint failed: users.username", "错误信息应包含唯一键冲突")
|
||||
})
|
||||
|
||||
t.Run("按用户名查找 - 找到用户", func(t *testing.T) {
|
||||
foundUser, err := repo.FindByUsername("testuser")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, foundUser)
|
||||
assert.Equal(t, userToCreate.ID, foundUser.ID)
|
||||
assert.Equal(t, "testuser", foundUser.Username)
|
||||
})
|
||||
|
||||
t.Run("按用户名查找 - 未找到用户", func(t *testing.T) {
|
||||
_, err := repo.FindByUsername("nonexistent")
|
||||
assert.Error(t, err, "查找不存在的用户应该返回错误")
|
||||
assert.ErrorIs(t, err, gorm.ErrRecordNotFound, "错误类型应为 gorm.ErrRecordNotFound")
|
||||
})
|
||||
|
||||
t.Run("按ID查找 - 找到用户", func(t *testing.T) {
|
||||
foundUser, err := repo.FindByID(userToCreate.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, foundUser)
|
||||
assert.Equal(t, userToCreate.ID, foundUser.ID)
|
||||
})
|
||||
|
||||
t.Run("按ID查找 - 未找到用户", func(t *testing.T) {
|
||||
_, err := repo.FindByID(99999)
|
||||
assert.Error(t, err, "查找不存在的ID应该返回错误")
|
||||
assert.ErrorIs(t, err, gorm.ErrRecordNotFound, "错误类型应为 gorm.ErrRecordNotFound")
|
||||
})
|
||||
}
|
||||
454
openspec/AGENTS.md
Normal file
454
openspec/AGENTS.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# OpenSpec Instructions
|
||||
|
||||
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||
|
||||
## TL;DR Quick Checklist
|
||||
|
||||
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
|
||||
- Decide scope: new capability vs modify existing capability
|
||||
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
|
||||
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
|
||||
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement
|
||||
- Validate: `openspec validate [change-id] --strict` and fix issues
|
||||
- Request approval: Do not start implementation until proposal is approved
|
||||
|
||||
## Three-Stage Workflow
|
||||
|
||||
### Stage 1: Creating Changes
|
||||
Create proposal when you need to:
|
||||
- Add features or functionality
|
||||
- Make breaking changes (API, schema)
|
||||
- Change architecture or patterns
|
||||
- Optimize performance (changes behavior)
|
||||
- Update security patterns
|
||||
|
||||
Triggers (examples):
|
||||
- "Help me create a change proposal"
|
||||
- "Help me plan a change"
|
||||
- "Help me create a proposal"
|
||||
- "I want to create a spec proposal"
|
||||
- "I want to create a spec"
|
||||
|
||||
Loose matching guidance:
|
||||
- Contains one of: `proposal`, `change`, `spec`
|
||||
- With one of: `create`, `plan`, `make`, `start`, `help`
|
||||
|
||||
Skip proposal for:
|
||||
- Bug fixes (restore intended behavior)
|
||||
- Typos, formatting, comments
|
||||
- Dependency updates (non-breaking)
|
||||
- Configuration changes
|
||||
- Tests for existing behavior
|
||||
|
||||
**Workflow**
|
||||
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
|
||||
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||
|
||||
### Stage 2: Implementing Changes
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. **Read proposal.md** - Understand what's being built
|
||||
2. **Read design.md** (if exists) - Review technical decisions
|
||||
3. **Read tasks.md** - Get implementation checklist
|
||||
4. **Implement tasks sequentially** - Complete in order
|
||||
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
|
||||
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
|
||||
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
|
||||
### Stage 3: Archiving Changes
|
||||
After deployment, create separate PR to:
|
||||
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||
- Update `specs/` if capabilities changed
|
||||
- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)
|
||||
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||
|
||||
## Before Any Task
|
||||
|
||||
**Context Checklist:**
|
||||
- [ ] Read relevant specs in `specs/[capability]/spec.md`
|
||||
- [ ] Check pending changes in `changes/` for conflicts
|
||||
- [ ] Read `openspec/project.md` for conventions
|
||||
- [ ] Run `openspec list` to see active changes
|
||||
- [ ] Run `openspec list --specs` to see existing capabilities
|
||||
|
||||
**Before Creating Specs:**
|
||||
- Always check if capability already exists
|
||||
- Prefer modifying existing specs over creating duplicates
|
||||
- Use `openspec show [spec]` to review current state
|
||||
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
|
||||
|
||||
### Search Guidance
|
||||
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
|
||||
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
|
||||
- Show details:
|
||||
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
|
||||
- Change: `openspec show <change-id> --json --deltas-only`
|
||||
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
# Essential commands
|
||||
openspec list # List active changes
|
||||
openspec list --specs # List specifications
|
||||
openspec show [item] # Display change or spec
|
||||
openspec validate [item] # Validate changes or specs
|
||||
openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
|
||||
|
||||
# Project management
|
||||
openspec init [path] # Initialize OpenSpec
|
||||
openspec update [path] # Update instruction files
|
||||
|
||||
# Interactive mode
|
||||
openspec show # Prompts for selection
|
||||
openspec validate # Bulk validation mode
|
||||
|
||||
# Debugging
|
||||
openspec show [change] --json --deltas-only
|
||||
openspec validate [change] --strict
|
||||
```
|
||||
|
||||
### Command Flags
|
||||
|
||||
- `--json` - Machine-readable output
|
||||
- `--type change|spec` - Disambiguate items
|
||||
- `--strict` - Comprehensive validation
|
||||
- `--no-interactive` - Disable prompts
|
||||
- `--skip-specs` - Archive without spec updates
|
||||
- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project conventions
|
||||
├── specs/ # Current truth - what IS built
|
||||
│ └── [capability]/ # Single focused capability
|
||||
│ ├── spec.md # Requirements and scenarios
|
||||
│ └── design.md # Technical patterns
|
||||
├── changes/ # Proposals - what SHOULD change
|
||||
│ ├── [change-name]/
|
||||
│ │ ├── proposal.md # Why, what, impact
|
||||
│ │ ├── tasks.md # Implementation checklist
|
||||
│ │ ├── design.md # Technical decisions (optional; see criteria)
|
||||
│ │ └── specs/ # Delta changes
|
||||
│ │ └── [capability]/
|
||||
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
|
||||
│ └── archive/ # Completed changes
|
||||
```
|
||||
|
||||
## Creating Change Proposals
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
New request?
|
||||
├─ Bug fix restoring spec behavior? → Fix directly
|
||||
├─ Typo/format/comment? → Fix directly
|
||||
├─ New feature/capability? → Create proposal
|
||||
├─ Breaking change? → Create proposal
|
||||
├─ Architecture change? → Create proposal
|
||||
└─ Unclear? → Create proposal (safer)
|
||||
```
|
||||
|
||||
### Proposal Structure
|
||||
|
||||
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
|
||||
|
||||
2. **Write proposal.md:**
|
||||
```markdown
|
||||
## Why
|
||||
[1-2 sentences on problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
- [Bullet list of changes]
|
||||
- [Mark breaking changes with **BREAKING**]
|
||||
|
||||
## Impact
|
||||
- Affected specs: [list capabilities]
|
||||
- Affected code: [key files/systems]
|
||||
```
|
||||
|
||||
3. **Create spec deltas:** `specs/[capability]/spec.md`
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: New Feature
|
||||
The system SHALL provide...
|
||||
|
||||
#### Scenario: Success case
|
||||
- **WHEN** user performs action
|
||||
- **THEN** expected result
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Existing Feature
|
||||
[Complete modified requirement]
|
||||
|
||||
## REMOVED Requirements
|
||||
### Requirement: Old Feature
|
||||
**Reason**: [Why removing]
|
||||
**Migration**: [How to handle]
|
||||
```
|
||||
If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.
|
||||
|
||||
4. **Create tasks.md:**
|
||||
```markdown
|
||||
## 1. Implementation
|
||||
- [ ] 1.1 Create database schema
|
||||
- [ ] 1.2 Implement API endpoint
|
||||
- [ ] 1.3 Add frontend component
|
||||
- [ ] 1.4 Write tests
|
||||
```
|
||||
|
||||
5. **Create design.md when needed:**
|
||||
Create `design.md` if any of the following apply; otherwise omit it:
|
||||
- Cross-cutting change (multiple services/modules) or a new architectural pattern
|
||||
- New external dependency or significant data model changes
|
||||
- Security, performance, or migration complexity
|
||||
- Ambiguity that benefits from technical decisions before coding
|
||||
|
||||
Minimal `design.md` skeleton:
|
||||
```markdown
|
||||
## Context
|
||||
[Background, constraints, stakeholders]
|
||||
|
||||
## Goals / Non-Goals
|
||||
- Goals: [...]
|
||||
- Non-Goals: [...]
|
||||
|
||||
## Decisions
|
||||
- Decision: [What and why]
|
||||
- Alternatives considered: [Options + rationale]
|
||||
|
||||
## Risks / Trade-offs
|
||||
- [Risk] → Mitigation
|
||||
|
||||
## Migration Plan
|
||||
[Steps, rollback]
|
||||
|
||||
## Open Questions
|
||||
- [...]
|
||||
```
|
||||
|
||||
## Spec File Format
|
||||
|
||||
### Critical: Scenario Formatting
|
||||
|
||||
**CORRECT** (use #### headers):
|
||||
```markdown
|
||||
#### Scenario: User login success
|
||||
- **WHEN** valid credentials provided
|
||||
- **THEN** return JWT token
|
||||
```
|
||||
|
||||
**WRONG** (don't use bullets or bold):
|
||||
```markdown
|
||||
- **Scenario: User login** ❌
|
||||
**Scenario**: User login ❌
|
||||
### Scenario: User login ❌
|
||||
```
|
||||
|
||||
Every requirement MUST have at least one scenario.
|
||||
|
||||
### Requirement Wording
|
||||
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
|
||||
|
||||
### Delta Operations
|
||||
|
||||
- `## ADDED Requirements` - New capabilities
|
||||
- `## MODIFIED Requirements` - Changed behavior
|
||||
- `## REMOVED Requirements` - Deprecated features
|
||||
- `## RENAMED Requirements` - Name changes
|
||||
|
||||
Headers matched with `trim(header)` - whitespace ignored.
|
||||
|
||||
#### When to use ADDED vs MODIFIED
|
||||
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
|
||||
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
|
||||
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
|
||||
|
||||
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
|
||||
|
||||
Authoring a MODIFIED requirement correctly:
|
||||
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
|
||||
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
|
||||
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
|
||||
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
|
||||
|
||||
Example for RENAMED:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Login`
|
||||
- TO: `### Requirement: User Authentication`
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
**"Change must have at least one delta"**
|
||||
- Check `changes/[name]/specs/` exists with .md files
|
||||
- Verify files have operation prefixes (## ADDED Requirements)
|
||||
|
||||
**"Requirement must have at least one scenario"**
|
||||
- Check scenarios use `#### Scenario:` format (4 hashtags)
|
||||
- Don't use bullet points or bold for scenario headers
|
||||
|
||||
**Silent scenario parsing failures**
|
||||
- Exact format required: `#### Scenario: Name`
|
||||
- Debug with: `openspec show [change] --json --deltas-only`
|
||||
|
||||
### Validation Tips
|
||||
|
||||
```bash
|
||||
# Always use strict mode for comprehensive checks
|
||||
openspec validate [change] --strict
|
||||
|
||||
# Debug delta parsing
|
||||
openspec show [change] --json | jq '.deltas'
|
||||
|
||||
# Check specific requirement
|
||||
openspec show [spec] --json -r 1
|
||||
```
|
||||
|
||||
## Happy Path Script
|
||||
|
||||
```bash
|
||||
# 1) Explore current state
|
||||
openspec spec list --long
|
||||
openspec list
|
||||
# Optional full-text search:
|
||||
# rg -n "Requirement:|Scenario:" openspec/specs
|
||||
# rg -n "^#|Requirement:" openspec/changes
|
||||
|
||||
# 2) Choose change id and scaffold
|
||||
CHANGE=add-two-factor-auth
|
||||
mkdir -p openspec/changes/$CHANGE/{specs/auth}
|
||||
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
|
||||
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
|
||||
|
||||
# 3) Add deltas (example)
|
||||
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
Users MUST provide a second factor during login.
|
||||
|
||||
#### Scenario: OTP required
|
||||
- **WHEN** valid credentials are provided
|
||||
- **THEN** an OTP challenge is required
|
||||
EOF
|
||||
|
||||
# 4) Validate
|
||||
openspec validate $CHANGE --strict
|
||||
```
|
||||
|
||||
## Multi-Capability Example
|
||||
|
||||
```
|
||||
openspec/changes/add-2fa-notify/
|
||||
├── proposal.md
|
||||
├── tasks.md
|
||||
└── specs/
|
||||
├── auth/
|
||||
│ └── spec.md # ADDED: Two-Factor Authentication
|
||||
└── notifications/
|
||||
└── spec.md # ADDED: OTP email notification
|
||||
```
|
||||
|
||||
auth/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
...
|
||||
```
|
||||
|
||||
notifications/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: OTP Email Notification
|
||||
...
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Simplicity First
|
||||
- Default to <100 lines of new code
|
||||
- Single-file implementations until proven insufficient
|
||||
- Avoid frameworks without clear justification
|
||||
- Choose boring, proven patterns
|
||||
|
||||
### Complexity Triggers
|
||||
Only add complexity with:
|
||||
- Performance data showing current solution too slow
|
||||
- Concrete scale requirements (>1000 users, >100MB data)
|
||||
- Multiple proven use cases requiring abstraction
|
||||
|
||||
### Clear References
|
||||
- Use `file.ts:42` format for code locations
|
||||
- Reference specs as `specs/auth/spec.md`
|
||||
- Link related changes and PRs
|
||||
|
||||
### Capability Naming
|
||||
- Use verb-noun: `user-auth`, `payment-capture`
|
||||
- Single purpose per capability
|
||||
- 10-minute understandability rule
|
||||
- Split if description needs "AND"
|
||||
|
||||
### Change ID Naming
|
||||
- Use kebab-case, short and descriptive: `add-two-factor-auth`
|
||||
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
|
||||
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
|
||||
|
||||
## Tool Selection Guide
|
||||
|
||||
| Task | Tool | Why |
|
||||
|------|------|-----|
|
||||
| Find files by pattern | Glob | Fast pattern matching |
|
||||
| Search code content | Grep | Optimized regex search |
|
||||
| Read specific files | Read | Direct file access |
|
||||
| Explore unknown scope | Task | Multi-step investigation |
|
||||
|
||||
## Error Recovery
|
||||
|
||||
### Change Conflicts
|
||||
1. Run `openspec list` to see active changes
|
||||
2. Check for overlapping specs
|
||||
3. Coordinate with change owners
|
||||
4. Consider combining proposals
|
||||
|
||||
### Validation Failures
|
||||
1. Run with `--strict` flag
|
||||
2. Check JSON output for details
|
||||
3. Verify spec file format
|
||||
4. Ensure scenarios properly formatted
|
||||
|
||||
### Missing Context
|
||||
1. Read project.md first
|
||||
2. Check related specs
|
||||
3. Review recent archives
|
||||
4. Ask for clarification
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Stage Indicators
|
||||
- `changes/` - Proposed, not yet built
|
||||
- `specs/` - Built and deployed
|
||||
- `archive/` - Completed changes
|
||||
|
||||
### File Purposes
|
||||
- `proposal.md` - Why and what
|
||||
- `tasks.md` - Implementation steps
|
||||
- `design.md` - Technical decisions
|
||||
- `spec.md` - Requirements and behavior
|
||||
|
||||
### CLI Essentials
|
||||
```bash
|
||||
openspec list # What's in progress?
|
||||
openspec show [item] # View details
|
||||
openspec validate --strict # Is it correct?
|
||||
openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation)
|
||||
```
|
||||
|
||||
Remember: Specs are truth. Changes are proposals. Keep them in sync.
|
||||
@@ -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` 只需从上下文中读取这些信息即可,**完全消除了捕获和解析响应体的需要**,使得设计更加清晰和高效。
|
||||
@@ -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`: 所有控制器及其辅助函数。
|
||||
@@ -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 框架时完全相同的响应(状态码、头部和正文格式)。
|
||||
@@ -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 路由是否正常。
|
||||
@@ -0,0 +1,414 @@
|
||||
# `monitor` 模块重构设计
|
||||
|
||||
## Context
|
||||
|
||||
当前, `monitor` 模块的数据转换逻辑(例如, 将 `repository` 层返回的 `models` 实体转换为 `dto` 对象)主要存在于
|
||||
`internal/app/controller/monitor/monitor_controller.go` 文件中。
|
||||
|
||||
这种设计导致了以下问题:
|
||||
|
||||
- **职责不清**:控制器层承担了过多的数据处理任务, 违反了“关注点分离”原则。控制器应主要负责处理 HTTP 请求、参数绑定和调用服务,
|
||||
而非执行业务或数据转换逻辑。
|
||||
- **代码重复**:如果未来有其他服务需要类似的数据转换, 可能会导致代码重复。
|
||||
- **可测试性差**:由于转换逻辑与 `echo.Context` 紧密耦合, 对其进行单元测试变得更加复杂。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
### Goals
|
||||
|
||||
- **迁移数据转换逻辑**:将 `monitor` 模块中所有的数据转换逻辑从控制器层 (`monitor_controller.go`) 迁移到服务层 (
|
||||
`monitor_service.go`)。
|
||||
- **统一服务层接口**:使服务层的方法直接接收请求 DTO, 并返回响应 DTO, 从而使服务本身成为一个完整的、自包含的业务逻辑单元。
|
||||
- **简化控制器**:精简控制器中的代码, 使其只关注其核心职责:请求处理和响应发送。
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- **不修改业务逻辑**:本次重构不涉及任何已有业务规则的变更。例如, `ListPlanExecutionLogs` 中获取关联计划信息的逻辑必须保持不变。
|
||||
- **不改变 API 契约**:API 的请求参数和响应结构对最终用户保持不变。
|
||||
- **不引入新的依赖**:仅在现有框架和依赖下进行代码调整。
|
||||
|
||||
## Decisions
|
||||
|
||||
- **决策:在服务层完成 DTO 转换**
|
||||
- **理由**:服务层是封装业务逻辑的核心, 将数据从领域模型 (`models`) 转换为外部表示 (`dto`)
|
||||
是业务服务的一部分。这样做可以确保任何调用该服务的客户端(无论是控制器、gRPC 服务还是其他服务)都能获得一致的、随时可用的数据结构。
|
||||
- **替代方案**:曾考虑在 `dto` 包中创建一个独立的转换层。但最终认为, 将转换逻辑内聚到服务层更能体现其业务属性,
|
||||
因为服务层最清楚需要暴露哪些数据以及如何组织这些数据。
|
||||
|
||||
- **决策:修改服务层接口以直接处理 DTO**
|
||||
- **具体实现**:计划将 `MonitorService` 接口中的所有 `List...` 方法签名从
|
||||
`ListSomething(opts repository.ListOptions, page, pageSize int) ([]models.Something, int64, error)` 修改为
|
||||
`ListSomething(req *dto.ListSomethingRequest) (*dto.ListSomethingResponse, error)`。
|
||||
- **理由**:这种设计将极大地简化控制器与服务之间的交互。控制器将不再需要手动构建 `repository.ListOptions`
|
||||
或在调用服务后手动组装响应 DTO。它只需传递请求 DTO, 然后直接使用服务返回的响应 DTO, 从而实现彻底的解耦。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **风险:意外修改或丢失现有业务逻辑**
|
||||
- **描述**:在移动代码的过程中, 尤其是像 `ListPlanExecutionLogs` 这样包含特定业务逻辑(获取关联 `plans`)的方法,
|
||||
存在逻辑被无意中删除或修改的风险。
|
||||
- **缓解措施**:
|
||||
1. **代码审查**:在重构前后仔细比对原有逻辑, 确保其被完整地迁移到了新的服务层方法中。
|
||||
2. **保留原有实现**:在新的服务层方法中, 将严格按照控制器中原有的顺序——先构建查询选项, 再调用仓库,
|
||||
最后进行数据转换——来组织代码, 确保逻辑的等效性。
|
||||
3. **测试**:在完成重构后, 必须进行完整的回归测试, 确保所有受影响的 API 端点的行为与重构前完全一致。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
本次重构将按以下步骤进行:
|
||||
|
||||
1. **修改服务层 (`internal/app/service/monitor_service.go`)**
|
||||
- **更新接口**:修改 `MonitorService` 接口中所有 `List...` 方法的签名, 使其接收请求 DTO 并返回响应 DTO。
|
||||
- **实现数据转换**:在每个 `List...` 方法的实现中, 添加从请求 DTO 到 `repository.ListOptions` 的转换逻辑, 以及从业仓库返回的
|
||||
`models` 到响应 DTO 的转换逻辑。对于 `ListPlanExecutionLogs` 等方法, 确保原有的附加业务逻辑(如查询关联 `Plan`
|
||||
信息)被完整保留。
|
||||
|
||||
2. **修改控制器层 (`internal/app/controller/monitor/monitor_controller.go`)**
|
||||
- **移除转换逻辑**:删除所有手动构建 `repository.ListOptions` 和调用 `dto.NewList...Response` 的代码。
|
||||
- **更新服务调用**:修改对 `monitorService` 的调用, 使其传递完整的请求 DTO, 并直接处理返回的响应 DTO。
|
||||
- **简化日志**:调整日志记录, 以便从服务层返回的 DTO 中获取列表长度和总记录数。
|
||||
|
||||
3. **验证**
|
||||
- 通过静态代码分析和审查, 确认代码风格和逻辑的正确性。
|
||||
- 进行完整的单元测试和集成测试, 以确保重构没有引入任何回归问题。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 暂无。
|
||||
|
||||
---
|
||||
|
||||
## `device` 模块重构设计
|
||||
|
||||
### Context
|
||||
|
||||
`device_controller.go` 当前直接依赖多个 `repository` 和 `domain.Service`,并在其方法内部执行了大量本应属于应用服务层的逻辑,包括:
|
||||
|
||||
- **直接的数据库操作**:调用 `repository` 的 `Create`, `Update`, `Delete`, `Find` 等方法。
|
||||
- **领域模型实例化**:通过 `&models.Device{...}` 直接创建数据库模型。
|
||||
- **内部字段序列化**:对 `Properties`, `Commands`, `Values` 等字段执行 `json.Marshal`。
|
||||
- **业务规则验证**:调用 `model.SelfCheck()`。
|
||||
- **复杂的错误处理**:通过 `errors.Is` 和 `strings.Contains` 解析底层数据库错误。
|
||||
- **DTO 转换**:在方法末尾调用 `dto.New...Response`。
|
||||
|
||||
这种设计导致控制器与基础设施层和领域层紧密耦合,违反了分层架构的原则。
|
||||
|
||||
### Goals / Non-Goals
|
||||
|
||||
#### Goals
|
||||
|
||||
- **创建应用服务层**:引入一个新的 `internal/app/service/device_service.go` 来封装业务逻辑。
|
||||
- **迁移业务逻辑**:将上述所有在控制器中识别出的业务逻辑和数据处理任务,全部迁移到新的 `DeviceService` 中。
|
||||
- **简化控制器**:使 `device_controller.go` 只负责 HTTP 请求处理和对新 `DeviceService` 的调用。
|
||||
- **保持领域服务纯粹**:确保 `internal/domain/device/device_service.go` 继续专注于核心领域逻辑,不与 DTO 发生耦合。
|
||||
|
||||
#### Non-Goals
|
||||
|
||||
- **不改变领域服务**:不对 `domain.device.Service` 的接口和实现进行任何修改。
|
||||
- **不改变 API 契约**:对外暴露的 API 接口、请求和响应格式保持不变。
|
||||
|
||||
### Decisions
|
||||
|
||||
- **决策:引入新的应用服务 `DeviceService`**
|
||||
- **理由**:这是解决控制器职责过重和分层不清问题的标准做法。该服务将作为应用层门面,协调 `repository` 和
|
||||
`domain.Service`,并为控制器提供一个清晰、稳定的接口。
|
||||
- **结构**:`DeviceService` 将依赖于 `DeviceRepository`, `AreaControllerRepository`, `DeviceTemplateRepository` 和
|
||||
`domain.device.Service`。
|
||||
|
||||
- **决策:`DeviceService` 接口全面采用 DTO**
|
||||
- **具体实现**:接口方法将接收 `dto.Create...Request` 等请求 DTO,并返回 `*dto....Response` 响应 DTO。
|
||||
- **理由**:这与 `monitor` 模块的重构决策一致,可以确保应用服务层的接口统一、清晰,并与上层(控制器)和下层(领域/仓库)完全解耦。
|
||||
|
||||
### Migration Plan
|
||||
|
||||
1. **创建 `internal/app/service/device_service.go` 文件**
|
||||
- 定义 `DeviceService` 接口,为控制器中的每个处理器方法(`CreateDevice`, `UpdateDevice`, `GetDevice`, `ListDevices`,
|
||||
`DeleteDevice`, `ManualControl` 等)创建相应的方法。
|
||||
- 定义 `deviceService` 结构体,并实现 `DeviceService` 接口。
|
||||
- **`Create/Update` 方法实现**:
|
||||
1. 接收请求 DTO。
|
||||
2. 执行 `json.Marshal` 转换 `Properties` 等字段。
|
||||
3. 创建 `models.Xxx` 实例。
|
||||
4. 调用 `model.SelfCheck()`。
|
||||
5. 调用 `repository.Create/Update`。
|
||||
6. 调用 `repository.FindByID` 重新加载模型(确保关联数据完整)。
|
||||
7. 调用 `dto.New...Response` 将模型转换为响应 DTO 并返回。
|
||||
- **`Get/List` 方法实现**:
|
||||
1. 调用 `repository.Find/List`。
|
||||
2. 调用 `dto.New...Response` 转换并返回。
|
||||
- **`Delete` 方法实现**:
|
||||
1. 调用 `repository.Delete`。
|
||||
2. 捕获并转换特定的“资源被使用”错误。
|
||||
- **`ManualControl` 方法实现**:
|
||||
1. 调用 `repository.FindByIDString` 加载模型。
|
||||
2. 实现 `action` 字符串到 `device.DeviceAction` 的映射。
|
||||
3. 调用 `domain.device.Service.Switch/Collect`。
|
||||
|
||||
2. **修改 `internal/app/controller/device/device_controller.go`**
|
||||
- **更新依赖**:将 `Controller` 的依赖从多个 `repository` 和 `domain.Service` 替换为唯一的
|
||||
`app/service.DeviceService`。
|
||||
- **简化所有处理器方法**:
|
||||
1. 移除所有业务逻辑(`json.Marshal`, `SelfCheck`, `repository` 调用, `dto` 转换等)。
|
||||
2. 每个方法仅保留:参数绑定、调用 `c.deviceService.Method(req)`、错误处理和成功响应。
|
||||
|
||||
3. **修改 `internal/core/component_initializers.go`**
|
||||
- 在 `AppServices` 结构体中增加 `DeviceService service.DeviceService` 字段。
|
||||
- 在 `initAppServices` 函数中,调用 `service.NewDeviceService` 创建实例,并将其注入到 `AppServices` 中。
|
||||
|
||||
4. **修改 `internal/app/api/api.go`**
|
||||
- 更新 `NewAPI` 函数的参数,使其接收新的 `app/service.DeviceService`。
|
||||
- 更新 `device.NewController` 的调用,将多个仓库和领域服务的依赖替换为单一的 `DeviceService` 依赖。
|
||||
|
||||
### Open Questions
|
||||
|
||||
- 暂无。
|
||||
|
||||
---
|
||||
|
||||
## `pig-farm` 模块重构设计
|
||||
|
||||
### Context
|
||||
|
||||
与 `monitor` 模块类似, `pig_farm_controller.go` 当前包含了将 `service` 层返回的 `models.PigHouse` 和 `models.Pen`
|
||||
实体手动转换为 `dto.PigHouseResponse` 和 `dto.PenResponse` 的逻辑。此外,
|
||||
控制器还处理了部分本应由服务层处理的业务错误判断 (例如 `service.ErrHouseNotFound`)。
|
||||
|
||||
这种模式导致了与 `monitor` 模块相同的职责不清、代码重复和可测试性差的问题。
|
||||
|
||||
### Goals / Non-Goals
|
||||
|
||||
#### Goals
|
||||
|
||||
- **迁移数据转换逻辑**: 将 `pig-farm` 模块中所有的数据转换逻辑从控制器层 (`pig_farm_controller.go`) 迁移到服务层 (
|
||||
`pig_farm_service.go`)。
|
||||
- **统一服务层接口**: 修改 `PigFarmService` 接口, 使其直接返回响应 DTO (`dto.XxxResponse`)。
|
||||
- **简化控制器**: 精简 `PigFarmController` 中的代码, 移除所有 `models` 到 `dto` 的转换代码, 使其直接使用服务层返回的
|
||||
DTO。
|
||||
|
||||
#### Non-Goals
|
||||
|
||||
- **不修改业务逻辑**: 本次重构严格保证业务逻辑不变。服务层将精确复制控制器层现有的转换逻辑, 不增加或减少任何字段。
|
||||
- **不改变 API 契约**: API 的请求和响应对最终用户保持完全一致。
|
||||
|
||||
### Decisions
|
||||
|
||||
- **决策:在服务层完成 `models` 到 `dto` 的转换**
|
||||
- **理由**: 与其他模块保持一致, 将数据转换视为服务层业务逻辑的一部分。这确保了服务接口的稳定性和调用方的便利性。
|
||||
- **具体实现**: `pig_farm_service.go` 中的方法在从 `repository` 获取 `models` 实体后, 将其转换为对应的 `dto` 再返回。
|
||||
|
||||
### Migration Plan
|
||||
|
||||
1. **修改 `internal/app/service/pig_farm_service.go`**
|
||||
- **更新 `PigFarmService` 接口**:
|
||||
- `CreatePigHouse(...) (*models.PigHouse, error)` -> `CreatePigHouse(...) (*dto.PigHouseResponse, error)`
|
||||
- `GetPigHouseByID(...) (*models.PigHouse, error)` -> `GetPigHouseByID(...) (*dto.PigHouseResponse, error)`
|
||||
- `ListPigHouses(...) ([]models.PigHouse, error)` -> `ListPigHouses(...) ([]dto.PigHouseResponse, error)`
|
||||
- `UpdatePigHouse(...) (*models.PigHouse, error)` -> `UpdatePigHouse(...) (*dto.PigHouseResponse, error)`
|
||||
- `CreatePen(...) (*models.Pen, error)` -> `CreatePen(...) (*dto.PenResponse, error)`
|
||||
- `UpdatePen(...) (*models.Pen, error)` -> `UpdatePen(...) (*dto.PenResponse, error)`
|
||||
- `UpdatePenStatus(...) (*models.Pen, error)` -> `UpdatePenStatus(...) (*dto.PenResponse, error)`
|
||||
- **实现数据转换**:
|
||||
- 在上述每个方法的实现中, 在从 `repository` 获得 `models` 对象后, 添加代码将其转换为对应的 `dto.XxxResponse` 对象。
|
||||
- 转换逻辑将严格按照 `pig_farm_controller.go` 中现有的实现, 确保字段一一对应, 无任何增删。
|
||||
- 例如, 在 `UpdatePigHouse` 中:
|
||||
|
||||
|
||||
2. **修改 `internal/app/controller/management/pig_farm_controller.go`**
|
||||
- **移除 DTO 转换代码**:
|
||||
- 在 `CreatePigHouse`, `GetPigHouse`, `UpdatePigHouse` 方法中, 删除手动创建 `dto.PigHouseResponse` 的代码。
|
||||
- 在 `ListPigHouses` 方法中, 删除用于遍历 `houses` 并创建 `[]dto.PigHouseResponse` 的 `for` 循环。
|
||||
- 在 `CreatePen`, `UpdatePen`, `UpdatePenStatus` 方法中, 删除手动创建 `dto.PenResponse` 的代码。
|
||||
- **更新服务调用**:
|
||||
- 将服务层返回的 DTO 对象直接传递给 `controller.SendSuccessWithAudit`。
|
||||
|
||||
|
||||
3. **验证**
|
||||
- 通过代码审查确认转换逻辑被精确迁移。
|
||||
- 运行相关测试, 并通过手动 API 测试验证端点行为与重构前完全一致。
|
||||
|
||||
### Open Questions
|
||||
|
||||
- 暂无。
|
||||
|
||||
---
|
||||
|
||||
## `plan` 模块重构设计
|
||||
|
||||
### Context
|
||||
|
||||
`plan_controller.go` 当前包含了大量的业务逻辑,这违反了控制器层应只负责请求处理和响应发送的原则。具体问题包括:
|
||||
|
||||
- **业务规则判断**:控制器中直接判断计划类型(如 `models.PlanTypeSystem`)、计划状态(如 `models.PlanStatusEnabled`)以及
|
||||
`ContentType` 的自动判断。
|
||||
- **领域对象创建与转换**:控制器直接使用 `dto.NewPlanFromCreateRequest` 和 `dto.NewPlanFromUpdateRequest` 将请求 DTO 转换为
|
||||
`models.Plan`,并在响应前将 `models.Plan` 转换为 `dto.PlanResponse`。
|
||||
- **直接调用仓库层**:控制器直接调用 `planRepo` 的 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`,
|
||||
`GetBasicPlanByID`, `UpdatePlanStatus`, `UpdateExecuteCount`, `StopPlanTransactionally` 等方法。
|
||||
- **协调领域服务**:控制器直接协调 `analysisPlanTaskManager` 的 `EnsureAnalysisTaskDefinition` 和 `CreateOrUpdateTrigger`
|
||||
方法。
|
||||
- **错误处理**:控制器直接通过 `errors.Is(err, gorm.ErrRecordNotFound)` 判断仓库层错误,并根据错误类型返回不同的 HTTP 状态码。
|
||||
- **执行计数器重置**:在 `UpdatePlan` 和 `StartPlan` 中,控制器直接处理 `ExecuteCount` 的重置逻辑。
|
||||
|
||||
这种设计导致控制器层职责过重,业务逻辑分散,难以维护和测试。
|
||||
|
||||
### Goals / Non-Goals
|
||||
|
||||
#### Goals
|
||||
|
||||
- **创建应用服务层**:引入一个新的 `internal/app/service/plan_service.go` 来封装 `plan` 模块的所有业务逻辑。
|
||||
- **迁移业务逻辑**:将 `plan_controller.go` 中识别出的所有业务规则判断、领域对象创建与转换、对仓库层的直接调用、对
|
||||
`analysisPlanTaskManager` 的协调以及错误处理逻辑,全部迁移到新的 `PlanService` 中。
|
||||
- **简化控制器**:使 `plan_controller.go` 只负责 HTTP 请求处理、参数绑定、调用新的 `PlanService` 方法,并处理服务层返回的
|
||||
DTO。
|
||||
- **统一服务层接口**:`PlanService` 的方法将接收 DTO 作为输入,并返回 DTO 作为输出,实现服务层接口的标准化。
|
||||
|
||||
#### Non-Goals
|
||||
|
||||
- **不修改业务逻辑**:本次重构不涉及任何已有业务规则的变更。所有业务逻辑将原封不动地从控制器迁移到服务层。
|
||||
- **不改变 API 契约**:对外暴露的 API 接口、请求参数和响应结构对最终用户保持不变。
|
||||
- **不改变领域服务**:不对 `internal/domain/scheduler/analysis_plan_task_manager.go` 的接口和实现进行任何修改。
|
||||
- **不改变仓库层接口**:不对 `internal/infra/repository/plan_repository.go` 的接口进行任何修改。
|
||||
|
||||
### Decisions
|
||||
|
||||
- **决策:引入新的应用服务 `PlanService`**
|
||||
- **理由**:这是解决控制器职责过重和分层不清问题的标准做法。`PlanService` 将作为应用层门面,协调 `PlanRepository` 和
|
||||
`AnalysisPlanTaskManager`,并为控制器提供一个清晰、稳定的接口。
|
||||
- **结构**:`PlanService` 将依赖于 `PlanRepository` 和 `AnalysisPlanTaskManager`。
|
||||
|
||||
- **决策:`PlanService` 接口全面采用 DTO**
|
||||
- **具体实现**:接口方法将接收 `dto.CreatePlanRequest`, `dto.UpdatePlanRequest`, `dto.ListPlansQuery` 等请求 DTO,并返回
|
||||
`*dto.PlanResponse`, `*dto.ListPlansResponse` 等响应 DTO。
|
||||
- **理由**:这与 `monitor`、`device` 和 `pig-farm` 模块的重构决策一致,可以确保应用服务层的接口统一、清晰,并与上层(控制器)和下层(领域/仓库)完全解耦。服务层内部将负责
|
||||
`DTO` 到 `models` 的转换以及 `models` 到 `DTO` 的转换。
|
||||
|
||||
- **决策:将控制器中的业务规则判断和错误处理下沉到服务层**
|
||||
- **理由**:控制器应专注于 HTTP 协议相关的职责。所有业务规则的判断(如计划类型、状态检查、ContentType
|
||||
自动判断、执行计数器重置)以及对底层错误的具体判断(如 `gorm.ErrRecordNotFound`
|
||||
)都属于业务逻辑范畴,应由服务层处理。服务层将返回更抽象的业务错误,控制器只需根据这些抽象错误进行统一的 HTTP 响应处理。
|
||||
|
||||
### Risks / Trade-offs
|
||||
|
||||
- **风险:意外修改或丢失现有业务逻辑**
|
||||
- **描述**:在将控制器中分散的业务逻辑迁移到服务层时,存在逻辑被无意中删除、修改或遗漏的风险,尤其是在处理计划状态转换、执行计数器重置和
|
||||
`ContentType` 自动判断等复杂逻辑时。
|
||||
- **缓解措施**:
|
||||
1. **逐行迁移与比对**:在迁移过程中,将控制器中的每一段业务逻辑代码逐行复制到服务层,并仔细比对,确保逻辑的等效性。
|
||||
2. **详细注释**:在服务层中对迁移过来的业务逻辑添加详细注释,解释其来源和作用。
|
||||
3. **回归测试**:在完成重构后,必须进行完整的回归测试,确保所有受影响的 API 端点的行为与重构前完全一致。
|
||||
|
||||
### Migration Plan
|
||||
|
||||
1. **创建 `internal/app/service/plan_service.go` 文件**:
|
||||
- 定义 `PlanService` 接口,包含 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, `StartPlan`,
|
||||
`StopPlan` 等方法。
|
||||
- 定义 `planService` 结构体,并实现 `PlanService` 接口。
|
||||
- 在 `planService` 的实现中,将 `plan_controller.go` 中所有相关的业务逻辑(包括 DTO 转换、业务规则判断、对 `planRepo` 和
|
||||
`analysisPlanTaskManager` 的调用、错误处理)精确迁移到对应的方法中。
|
||||
|
||||
2. **修改 `internal/app/controller/plan/plan_controller.go`**:
|
||||
- 更新 `Controller` 结构体,将 `planRepo` 和 `analysisPlanTaskManager` 替换为 `service.PlanService`。
|
||||
- 修改 `NewController` 函数,注入 `service.PlanService`。
|
||||
- 简化所有处理器方法,移除所有业务逻辑,只保留请求参数绑定、调用 `service.PlanService` 方法、错误处理和响应构建。
|
||||
|
||||
3. **修改 `internal/core/component_initializers.go`**:
|
||||
- 在 `AppServices` 结构体中添加 `PlanService service.PlanService` 字段。
|
||||
- 在 `initAppServices` 函数中,初始化 `PlanService` 实例,并将其注入到 `AppServices` 中。
|
||||
|
||||
4. **修改 `internal/app/api/api.go`**:
|
||||
- 更新 `NewAPI` 函数的参数,移除 `planRepository` 和 `analysisTaskManager`,添加 `service.PlanService`。
|
||||
- 更新 `plan.NewController` 的调用,传入新的 `service.PlanService` 依赖。
|
||||
|
||||
### Open Questions
|
||||
|
||||
- 暂无。
|
||||
|
||||
---
|
||||
|
||||
## `user` 模块重构设计
|
||||
|
||||
### Context
|
||||
|
||||
`user_controller.go` 当前直接依赖 `repository.UserRepository`、`token.Service` 和 `domain_notify.Service`
|
||||
,并在其方法内部执行了大量本应属于应用服务层的逻辑,包括:
|
||||
|
||||
- **直接的数据库操作**:调用 `userRepo` 的 `Create`, `FindByUsername`, `FindUserForLogin` 等方法。
|
||||
- **领域模型实例化**:通过 `&models.User{...}` 直接创建数据库模型。
|
||||
- **业务规则验证**:例如在 `CreateUser` 中判断用户名是否重复,在 `Login` 中进行密码验证。
|
||||
- **协调领域服务**:在 `Login` 中协调 `tokenService` 生成 JWT,在 `SendTestNotification` 中协调 `domain_notify.Service`
|
||||
发送测试消息。
|
||||
- **复杂的错误处理**:通过 `errors.Is` 和 `gorm.ErrRecordNotFound` 解析底层错误。
|
||||
- **DTO 转换**:在方法末尾将 `models.User` 转换为 `dto.CreateUserResponse` 或 `dto.LoginResponse`。
|
||||
|
||||
这种设计导致控制器与基础设施层和领域层紧密耦合,违反了分层架构的原则。
|
||||
|
||||
### Goals / Non-Goals
|
||||
|
||||
#### Goals
|
||||
|
||||
- **创建应用服务层**:引入一个新的 `internal/app/service/user_service.go` 来封装业务逻辑。
|
||||
- **迁移业务逻辑**:将上述所有在控制器中识别出的业务逻辑和数据处理任务,全部迁移到新的 `UserService` 中。
|
||||
- **简化控制器**:使 `user_controller.go` 只负责 HTTP 请求处理和对新 `UserService` 的调用。
|
||||
- **保持领域服务纯粹**:确保 `internal/domain/token.Service` 和 `internal/domain/notify.Service` 继续专注于核心领域逻辑,不与
|
||||
DTO 发生耦合。
|
||||
|
||||
#### Non-Goals
|
||||
|
||||
- **不修改业务逻辑**:本次重构不涉及任何已有业务规则的变更。所有业务逻辑将原封不动地从控制器迁移到服务层。
|
||||
- **不改变 API 契约**:对外暴露的 API 接口、请求和响应格式保持不变。
|
||||
- **不改变领域服务**:不对 `domain.token.Service` 和 `domain.notify.Service` 的接口和实现进行任何修改。
|
||||
- **不改变仓库层接口**:不对 `internal/infra/repository/user_repository.go` 的接口进行任何修改。
|
||||
- **不涉及 `ListUserHistory` 方法**:该方法已从重构范围中移除。
|
||||
|
||||
### Decisions
|
||||
|
||||
- **决策:引入新的应用服务 `UserService`**
|
||||
- **理由**:这是解决控制器职责过重和分层不清问题的标准做法。该服务将作为应用层门面,协调 `UserRepository`、
|
||||
`token.Service` 和 `domain_notify.Service`,并为控制器提供一个清晰、稳定的接口。
|
||||
- **结构**:`UserService` 将依赖于 `repository.UserRepository`, `token.Service`, `domain_notify.Service` 和
|
||||
`logs.Logger`。
|
||||
|
||||
- **决策:`UserService` 接口全面采用 DTO**
|
||||
- **具体实现**:接口方法将接收 `dto.CreateUserRequest`, `dto.LoginRequest`, `dto.SendTestNotificationRequest` 等请求
|
||||
DTO,并返回 `*dto.CreateUserResponse`, `*dto.LoginResponse` 等响应 DTO。
|
||||
- **理由**:这与 `monitor`、`device`、`pig-farm` 和 `plan` 模块的重构决策一致,可以确保应用服务层的接口统一、清晰,并与上层(控制器)和下层(领域/仓库)完全解耦。服务层内部将负责
|
||||
DTO 到 `models` 的转换以及 `models` 到 DTO 的转换。
|
||||
|
||||
- **决策:将控制器中的业务规则判断和错误处理下沉到服务层**
|
||||
- **理由**:控制器应专注于 HTTP 协议相关的职责。所有业务规则的判断(如用户名重复检查、密码验证)以及对底层错误的具体判断(如
|
||||
`gorm.ErrRecordNotFound`)都属于业务逻辑范畴,应由服务层处理。服务层将返回更抽象的业务错误,控制器只需根据这些抽象错误进行统一的
|
||||
HTTP 响应处理。
|
||||
|
||||
### Risks / Trade-offs
|
||||
|
||||
- **风险:意外修改或丢失现有业务逻辑**
|
||||
- **描述**:在将控制器中分散的业务逻辑迁移到服务层时,存在逻辑被无意中删除、修改或遗漏的风险,尤其是在处理用户创建、登录和通知发送等复杂逻辑时。
|
||||
- **缓解措施**:
|
||||
1. **逐行迁移与比对**:在迁移过程中,将控制器中的每一段业务逻辑代码逐行复制到服务层,并仔细比对,确保逻辑的等效性。
|
||||
2. **详细注释**:在服务层中对迁移过来的业务逻辑添加详细注释,解释其来源和作用。
|
||||
3. **回归测试**:在完成重构后,必须进行完整的回归测试,确保所有受影响的 API 端点的行为与重构前完全一致。
|
||||
|
||||
### Migration Plan
|
||||
|
||||
1. **创建 `internal/app/service/user_service.go` 文件**:
|
||||
- 定义 `UserService` 接口,包含 `CreateUser`, `Login`, `SendTestNotification` 等方法。
|
||||
- 定义 `userService` 结构体,并实现 `UserService` 接口。
|
||||
- 在 `userService` 的实现中,将 `user_controller.go` 中所有相关的业务逻辑(包括 DTO 转换、业务规则判断、对 `userRepo`、
|
||||
`tokenService` 和 `notifyService` 的调用、错误处理)精确迁移到对应的方法中。
|
||||
|
||||
2. **修改 `internal/app/controller/user/user_controller.go`**:
|
||||
- 更新 `Controller` 结构体,将 `userRepo`, `tokenService`, `notifyService` 替换为 `service.UserService`。
|
||||
- 修改 `NewController` 函数,注入 `service.UserService`。
|
||||
- 简化所有处理器方法,移除所有业务逻辑,只保留请求参数绑定、调用 `service.UserService` 方法、错误处理和响应构建。
|
||||
|
||||
3. **修改 `internal/core/component_initializers.go`**:
|
||||
- 在 `AppServices` 结构体中添加 `UserService service.UserService` 字段。
|
||||
- 在 `initAppServices` 函数中,初始化 `UserService` 实例,并将其注入到 `AppServices` 中。
|
||||
|
||||
4. **修改 `internal/app/api/api.go`**:
|
||||
- 更新 `NewAPI` 函数的参数,移除 `userRepo`, `tokenService`, `notifyService`,添加 `service.UserService`。
|
||||
- 更新 `user.NewController` 的调用,传入新的 `service.UserService` 依赖。
|
||||
|
||||
### Open Questions
|
||||
|
||||
- 暂无。
|
||||
@@ -0,0 +1,46 @@
|
||||
## Why
|
||||
当前项目中,控制器层与服务层、仓库层之间存在严重的领域侵入问题。具体表现为:
|
||||
1. **服务层直接吐出数据库模型:** 导致控制器层直接感知并操作领域模型,增加了控制器与数据持久化细节的耦合。
|
||||
2. **服务层接收数据库对象或仓库层特定结构:** 控制器层直接构建数据库模型或仓库层查询选项并传递给服务层/仓库层,使得服务层接口不够抽象,且控制器承担了不应有的数据转换职责。
|
||||
3. **业务逻辑散落在控制器层:** 控制器层包含了大量的业务规则判断、领域对象的创建与验证、以及对仓库层和领域服务的直接协调,这违反了控制器层应只做数据校验、绑定解析和调用服务层方法的原则,导致业务逻辑分散、难以维护和测试。
|
||||
* **控制器直接进行领域模型内部字段的序列化/反序列化:** 例如,控制器直接对 `req.Properties` 进行 `json.Marshal` 操作,将领域模型的内部结构(如 JSON 字符串存储)暴露给控制器。
|
||||
* **控制器直接实例化领域模型对象:** 控制器直接通过 `&models.Xxx{...}` 实例化领域模型对象,而非通过服务层进行创建。
|
||||
* **控制器通过检查底层(仓库层或服务层)的特定错误类型或错误信息来执行业务判断:** 例如,通过 `strings.Contains(err.Error(), "...")` 或 `errors.Is(err, service.ErrXxx)` 来判断具体的业务错误类型,使得控制器与底层实现细节紧密耦合。
|
||||
|
||||
这些问题导致了代码的紧密耦合、可维护性差、测试困难,并且不利于后续的业务扩展和架构演进。
|
||||
|
||||
## What Changes
|
||||
本次重构旨在解决上述领域侵入问题,明确各层的职责,提升代码质量。主要变更包括:
|
||||
- **服务层接口标准化:** 确保服务层方法只接收 DTO 或基本参数,并只返回 DTO 或业务领域对象(而非数据库模型)。
|
||||
- **控制器层职责收敛:** 控制器层将仅负责请求参数的绑定与校验、调用服务层方法,并将服务层返回的 DTO 转换为 HTTP 响应。所有业务逻辑、领域对象的创建与验证、以及与仓库层的直接交互都将从控制器层移除并下沉到服务层。
|
||||
* **移除控制器中的领域模型内部字段序列化/反序列化逻辑:** 将此类操作下沉到服务层或专门的转换器中。
|
||||
* **移除控制器中直接实例化领域模型对象的逻辑:** 领域模型的创建应通过服务层完成。
|
||||
* **优化控制器中的业务错误处理:** 服务层将返回更抽象的业务错误,控制器层根据这些抽象错误进行统一的 HTTP 响应处理,避免直接依赖仓库层或服务层内部的具体错误类型或错误信息。
|
||||
- **DTO 转换逻辑下沉:** 将数据库模型与 DTO 之间的转换逻辑从控制器层移动到服务层内部或专门的转换器中。
|
||||
- **业务错误处理优化:** 服务层将返回更抽象的业务错误,控制器层根据这些抽象错误进行统一的 HTTP 响应处理,避免直接依赖仓库层或服务层内部的具体错误类型。
|
||||
|
||||
**BREAKING**:本次变更将涉及服务层接口的修改,以及控制器层对服务层调用的调整,可能对依赖这些接口的代码造成影响。
|
||||
|
||||
## Impact
|
||||
- **Affected specs:**
|
||||
- `specs/monitor/spec.md` (如果存在,需要更新服务层返回 DTO 的要求)
|
||||
- `specs/device/spec.md` (如果存在,需要更新服务层返回 DTO 的要求)
|
||||
- `specs/pig-farm/spec.md` (如果存在,需要更新服务层返回 DTO 的要求)
|
||||
- `specs/plan/spec.md` (如果存在,需要更新服务层返回 DTO 的要求)
|
||||
- `specs/user/spec.md` (如果存在,需要更新服务层返回 DTO 的要求)
|
||||
- **Affected code:**
|
||||
- `internal/app/controller/monitor/monitor_controller.go`
|
||||
- `internal/app/controller/device/device_controller.go`
|
||||
- `internal/app/controller/management/pig_farm_controller.go`
|
||||
- `internal/app/controller/plan/plan_controller.go`
|
||||
- `internal/app/controller/user/user_controller.go`
|
||||
- `internal/app/service/monitor_service.go` (及其实现)
|
||||
- `internal/app/service/device_service.go` (及其实现)
|
||||
- `internal/app/service/pig_farm_service.go` (及其实现)
|
||||
- `internal/app/service/plan_service.go` (及其实现)
|
||||
- `internal/app/service/user_service.go` (及其实现)
|
||||
- `internal/infra/repository/*.go` (可能需要调整接口,以适应服务层接收 DTO 的变化)
|
||||
- `internal/infra/models/*.go` (可能需要添加或修改 DTO 转换方法)
|
||||
- `internal/app/dto/*.go` (可能需要添加新的 DTO 或修改现有 DTO 的构造函数)
|
||||
- `internal/core/component_initializers.go`
|
||||
- `internal/app/api/api.go`
|
||||
@@ -0,0 +1,70 @@
|
||||
# 业务逻辑分层重构规范
|
||||
|
||||
## Purpose
|
||||
本规范旨在明确业务逻辑分层重构的目标、变更内容和预期行为,以解决控制器层职责过重、代码耦合严重、可维护性差的问题。通过本次重构,我们将实现各层职责的清晰划分,提升代码质量和可测试性。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 服务层接口标准化
|
||||
- **说明**: 服务层方法现在 **MUST** 只接收数据传输对象 (DTO) 或基本参数,并 **MUST** 只返回 DTO 或业务领域对象,不再直接暴露数据库模型。
|
||||
- **理由**: 减少服务层与持久化细节的耦合,提高接口的抽象性和稳定性。
|
||||
- **影响**: 高。所有调用服务层的方法都需要调整。
|
||||
- **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。
|
||||
|
||||
#### Scenario: 服务层方法接收 DTO 作为输入
|
||||
- **假如**: `UserService` 的 `CreateUser` 方法被调用。
|
||||
- **当**: `CreateUser` 方法接收 `dto.CreateUserRequest` 作为参数。
|
||||
- **那么**: `UserService` 内部负责将 `dto.CreateUserRequest` 转换为 `models.User` 进行处理。
|
||||
|
||||
#### Scenario: 服务层方法返回 DTO 作为输出
|
||||
- **假如**: `UserService` 的 `CreateUser` 方法执行成功。
|
||||
- **当**: `CreateUser` 方法返回 `*dto.CreateUserResponse`。
|
||||
- **那么**: 调用方可以直接使用 `dto.CreateUserResponse`,无需进行额外的模型转换。
|
||||
|
||||
### Requirement: 控制器层职责收敛
|
||||
- **说明**: 控制器层现在 **MUST** 仅负责 HTTP 请求的参数绑定与校验、调用服务层方法,并将服务层返回的 DTO 转换为 HTTP 响应。所有业务逻辑、领域对象的创建与验证、以及与仓库层的直接交互都 **MUST** 从控制器层移除并下沉到服务层。
|
||||
- **理由**: 遵循“关注点分离”原则,使控制器层专注于 HTTP 协议处理,提高代码的可维护性和可测试性。
|
||||
- **影响**: 高。所有控制器方法都需要大幅简化。
|
||||
- **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。
|
||||
|
||||
#### Scenario: 控制器不再直接进行领域模型内部字段的序列化/反序列化
|
||||
- **假如**: `DeviceController` 的 `CreateDevice` 方法被调用。
|
||||
- **当**: `CreateDevice` 方法不再包含 `json.Marshal` 或 `json.Unmarshal` 等操作来处理 `Properties` 等字段。
|
||||
- **那么**: 这些序列化/反序列化逻辑已下沉到 `DeviceService` 中。
|
||||
|
||||
#### Scenario: 控制器不再直接实例化领域模型对象
|
||||
- **假如**: `UserController` 的 `CreateUser` 方法被调用。
|
||||
- **当**: `CreateUser` 方法不再包含 `&models.User{...}` 这样的代码。
|
||||
- **那么**: 领域模型的创建已通过 `UserService` 完成。
|
||||
|
||||
#### Scenario: 控制器不再直接调用仓库层方法
|
||||
- **假如**: `PlanController` 的 `ListPlans` 方法被调用。
|
||||
- **当**: `ListPlans` 方法不再直接调用 `planRepo.ListPlans`。
|
||||
- **那么**: `PlanService` 负责协调 `PlanRepository`。
|
||||
|
||||
#### Scenario: 控制器不再直接进行业务规则判断
|
||||
- **假如**: `PlanController` 的 `UpdatePlan` 方法被调用。
|
||||
- **当**: `UpdatePlan` 方法不再包含对计划类型、状态或 `ContentType` 的直接判断逻辑。
|
||||
- **那么**: 这些业务规则判断已下沉到 `PlanService` 中。
|
||||
|
||||
### Requirement: DTO 转换逻辑下沉
|
||||
- **说明**: 数据库模型与 DTO 之间的转换逻辑 **MUST** 从控制器层移动到服务层内部或专门的转换器中。
|
||||
- **理由**: 确保数据转换逻辑与业务逻辑紧密结合,避免控制器层承担不必要的职责。
|
||||
- **影响**: 中。主要影响数据流转和转换点。
|
||||
- **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。
|
||||
|
||||
#### Scenario: 服务层负责将数据库模型转换为响应 DTO
|
||||
- **假如**: `PigFarmService` 的 `GetPigHouseByID` 方法从 `repository` 获取到 `models.PigHouse`。
|
||||
- **当**: `GetPigHouseByID` 方法在返回前将 `models.PigHouse` 转换为 `dto.PigHouseResponse`。
|
||||
- **那么**: 控制器直接接收 `dto.PigHouseResponse`。
|
||||
|
||||
### Requirement: 业务错误处理优化
|
||||
- **说明**: 服务层现在 **MUST** 返回更抽象的业务错误,控制器层 **MUST** 根据这些抽象错误进行统一的 HTTP 响应处理,避免直接依赖仓库层或服务层内部的具体错误类型或错误信息。
|
||||
- **理由**: 提高错误处理的一致性和可维护性,解耦控制器与底层错误实现。
|
||||
- **影响**: 中。影响错误处理流程。
|
||||
- **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。
|
||||
|
||||
#### Scenario: 服务层返回抽象业务错误
|
||||
- **假如**: `UserService` 的 `CreateUser` 方法因用户名重复而失败。
|
||||
- **当**: `UserService` 返回一个表示“用户名已存在”的抽象错误(例如自定义错误类型或包装后的错误)。
|
||||
- **那么**: `UserController` 接收到此抽象错误后,可以统一转换为相应的 HTTP 状态码和错误信息,而无需解析底层 `gorm.ErrDuplicatedKey` 等具体错误。
|
||||
@@ -0,0 +1,123 @@
|
||||
## 1. 准备工作
|
||||
|
||||
- [x] 1.1 阅读并理解 `openspec/changes/refactor-business-logic-layering/proposal.md`。
|
||||
- [x] 1.2 阅读并理解 `openspec/changes/refactor-business-logic-layering/design.md`。
|
||||
- [x] 1.3 阅读并理解 'AGENTS.md'
|
||||
|
||||
## 2. 统一服务层接口输入输出为 DTO
|
||||
|
||||
### 2.1 `monitor` 模块
|
||||
|
||||
- [x] 2.1.1 **修改 `internal/app/service/monitor_service.go`:**
|
||||
- [x] 将所有 `List...` 方法的 `opts repository.ListOptions` 参数替换为服务层自定义的查询 DTO 或一系列基本参数。
|
||||
- [x] 将所有 `List...` 方法的返回值 `[]models.Xxx` 替换为 `[]dto.XxxResponse`。
|
||||
- [x] 调整 `List...` 方法的实现,在服务层内部将服务层查询 DTO 转换为 `repository.ListOptions`。
|
||||
- [x] 调整 `List...` 方法的实现,在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`。
|
||||
- [x] 2.1.2 **修改 `internal/app/controller/monitor/monitor_controller.go`:**
|
||||
- [x] 移除控制器中构建 `repository.ListOptions` 的逻辑。
|
||||
- [x] 移除控制器中将 `models` 转换为 `dto.NewList...Response` 的逻辑。
|
||||
- [x] 移除控制器中直接使用 `models` 进行枚举类型转换的逻辑,将其下沉到服务层或 DTO 转换逻辑中。
|
||||
- [x] 调整服务层方法的调用,使其接收新的服务层查询 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`。
|
||||
|
||||
### 2.2 `device` 模块
|
||||
|
||||
- [x] 2.2.1 **创建并修改 `internal/app/service/device_service.go`:**
|
||||
- [x] 定义 `DeviceService` 接口,包含 `CreateDevice`, `UpdateDevice`, `CreateAreaController`, `UpdateAreaController`,
|
||||
`CreateDeviceTemplate`, `UpdateDeviceTemplate`, `GetDevice`, `ListDevices`, `GetAreaController`,
|
||||
`ListAreaControllers`, `GetDeviceTemplate`, `ListDeviceTemplates`, `ManualControl` 等方法。
|
||||
- [x] 为 `CreateDevice`, `UpdateDevice`, `CreateAreaController`, `UpdateAreaController`, `CreateDeviceTemplate`,
|
||||
`UpdateDeviceTemplate`, `ManualControl` 方法定义并接收 DTO 作为输入。
|
||||
- [x] 将 `GetDevice`, `ListDevices`, `GetAreaController`, `ListAreaControllers`, `GetDeviceTemplate`,
|
||||
`ListDeviceTemplates` 方法的返回值 `models.Xxx` 或 `[]models.Xxx` 替换为 `dto.XxxResponse` 或 `[]dto.XxxResponse`。
|
||||
- [x] 实现 `DeviceService` 接口。
|
||||
- [x] 在此服务层内部将输入 DTO 转换为 `models` 对象。
|
||||
- [x] 在此服务层内部将 `repository` 或 `domain` 层返回的 `models` 对象转换为 `dto.XxxResponse`。
|
||||
- [x] 将控制器中 `SelfCheck()` 验证逻辑移入此服务层。
|
||||
- [x] 将控制器中 `Properties`, `Commands`, `Values` 的 JSON 序列化逻辑移入此服务层。
|
||||
- [x] 将控制器中 `ManualControl` 的业务逻辑(如动作映射)移入此服务层。
|
||||
- [x] 将控制器中直接调用 `repository` 方法的逻辑移入此服务层。
|
||||
- [x] 将控制器中通过检查 `repository` 错误信息处理业务规则的逻辑移入此服务层。
|
||||
- [x] 调整此服务层对 `internal/domain/device.Service` 的调用,确保传递的是 `models` 或领域对象,而不是 DTO。
|
||||
- [x] 2.2.2 **修改 `internal/app/controller/device/device_controller.go`:**
|
||||
- [x] 引入并使用新创建的 `internal/app/service.DeviceService`。
|
||||
- [x] 移除控制器中直接创建 `models.Device`, `models.AreaController`, `models.DeviceTemplate` 对象的逻辑。
|
||||
- [x] 移除控制器中直接调用 `SelfCheck()` 的逻辑。
|
||||
- [x] 移除控制器中直接调用 `repository` 方法的逻辑。
|
||||
- [x] 移除控制器中通过检查 `repository` 错误信息处理业务规则的逻辑。
|
||||
- [x] 移除控制器中 `Properties`, `Commands`, `Values` 的 JSON 序列化逻辑。
|
||||
- [x] 调整服务层方法的调用,使其接收新的服务层输入 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`。
|
||||
- [x] 2.2.3 **修改 `internal/core/component_initializers.go`**:创建并提供新的 `DeviceService`。
|
||||
- [x] 2.2.4 **修改 `internal/app/api/api.go`**:更新 `DeviceController` 的依赖注入。
|
||||
|
||||
### 2.3 `pig-farm` 模块
|
||||
|
||||
- [x] 2.3.1 **修改 `internal/app/service/pig_farm_service.go`:**
|
||||
- [x] 将 `CreatePigHouse`, `GetPigHouseByID`, `ListPigHouses`, `UpdatePigHouse`, `CreatePen`, `GetPenByID`,
|
||||
`ListPens`, `UpdatePen`, `UpdatePenStatus` 方法的返回值 `models.Xxx` 或 `[]models.Xxx` 替换为 `dto.XxxResponse` 或
|
||||
`[]dto.XxxResponse`。
|
||||
- [x] 在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`。
|
||||
- [x] 将控制器中处理服务层特定业务错误(如 `service.ErrHouseNotFound`)的逻辑移入服务层,服务层应返回更抽象的错误或直接返回
|
||||
DTO。
|
||||
- [x] 2.3.2 **修改 `internal/app/controller/management/pig_farm_controller.go`:**
|
||||
- [x] 移除控制器中手动将领域实体转换为 DTO 的逻辑。
|
||||
- [x] 移除控制器中直接处理服务层特定业务错误类型的逻辑。
|
||||
- [x] 调整服务层方法的调用,使其直接处理服务层返回的 `dto.XxxResponse`。
|
||||
|
||||
### 2.4 `plan` 模块
|
||||
|
||||
- [x] 2.4.1 **创建并修改 `internal/app/service/plan_service.go`:**
|
||||
- [x] 定义 `PlanService` 接口,包含 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`,
|
||||
`StartPlan`, `StopPlan` 等方法。
|
||||
- [x] 为 `CreatePlan`, `UpdatePlan` 方法定义并接收 DTO 作为输入。
|
||||
- [x] 将 `GetPlanByID`, `ListPlans` 方法的返回值 `models.Plan` 或 `[]models.Plan` 替换为 `dto.PlanResponse` 或
|
||||
`[]dto.PlanResponse`。
|
||||
- [x] 调整 `ListPlans` 方法的 `opts repository.ListPlansOptions` 参数替换为服务层自定义的查询 DTO 或一系列基本参数。
|
||||
- [x] 调整 `DeletePlan`, `StartPlan`, `StopPlan` 方法,使其接收 DTO 或基本参数,并封装所有业务逻辑。
|
||||
- [x] 实现 `PlanService` 接口。
|
||||
- [x] 在服务层内部将输入 DTO 转换为 `models` 对象。
|
||||
- [x] 在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`。
|
||||
- [x] 将 `internal/app/controller/plan/plan_controller.go` 中所有的业务规则判断(计划类型检查、状态检查、执行计数器重置、ContentType
|
||||
自动判断)移入服务层。
|
||||
- [x] 将 `internal/app/controller/plan/plan_controller.go` 中对 `repository` 方法的直接调用移入服务层。
|
||||
- [x] 将 `internal/app/controller/plan/plan_controller.go` 中对 `analysisPlanTaskManager` 的协调移入服务层。
|
||||
- [x] 将 `internal/app/controller/plan/plan_controller.go` 中处理仓库层特有错误(`gorm.ErrRecordNotFound`)的逻辑移入服务层。
|
||||
- [x] 2.4.2 **修改 `internal/app/controller/plan/plan_controller.go`:**
|
||||
- [x] 引入并使用新创建的 `plan_service`。
|
||||
- [x] 移除控制器中直接创建 `models.Plan` 对象和 `repository.ListPlansOptions` 的逻辑。
|
||||
- [x] 移除控制器中所有的业务规则判断。
|
||||
- [x] 移除控制器中直接调用 `repository` 方法的逻辑。
|
||||
- [x] 移除控制器中直接协调 `analysisPlanTaskManager` 的逻辑。
|
||||
- [x] 移除控制器中直接处理仓库层特有错误的逻辑。
|
||||
- [x] 调整服务层方法的调用,使其接收新的服务层输入 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`。
|
||||
- [x] 2.4.3 **修改 `internal/core/component_initializers.go`**:创建并提供新的 `PlanService`。
|
||||
- [x] 2.4.4 **修改 `internal/app/api/api.go`**:更新 `PlanController` 的依赖注入。
|
||||
|
||||
### 2.5 `user` 模块
|
||||
|
||||
- [x] 2.5.1 **创建并修改 `internal/app/service/user_service.go`:**
|
||||
- [x] 定义 `UserService` 接口,包含 `CreateUser`, `Login`, `SendTestNotification` 等方法。
|
||||
- [x] 为 `CreateUser`, `Login` 方法定义并接收 DTO 作为输入。
|
||||
- [x] 将 `CreateUser`, `Login` 方法的返回值 `models.User` 替换为 `dto.CreateUserResponse` 或 `dto.LoginResponse`。
|
||||
- [x] 调整 `SendTestNotification` 方法,使其接收 DTO 或基本参数,并封装所有业务逻辑。
|
||||
- [x] 实现 `UserService` 接口。
|
||||
- [x] 在服务层内部将输入 DTO 转换为 `models` 对象。
|
||||
- [x] 在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`。
|
||||
- [x] 将 `CreateUser` 中处理用户名重复的业务逻辑从控制器移入服务层。
|
||||
- [x] 将 `Login` 中进行密码验证的业务逻辑和协调 `tokenService` 的逻辑从控制器移入服务层。
|
||||
- [x] 将 `SendTestNotification` 中调用 `domain_notify.Service` 的逻辑移入服务层。
|
||||
- [x] 将控制器中通过检查底层(仓库层或服务层)的特定错误类型或错误信息来执行业务判断的逻辑移入服务层。
|
||||
- [x] 2.5.2 **修改 `internal/app/controller/user/user_controller.go`:**
|
||||
- [x] 引入并使用新创建的 `user_service`。
|
||||
- [x] 移除控制器中直接创建 `models.User` 对象的逻辑。
|
||||
- [x] 移除控制器中处理用户名重复的业务逻辑。
|
||||
- [x] 移除控制器中进行密码验证的业务逻辑和协调 `tokenService` 的逻辑。
|
||||
- [x] 移除控制器中通过检查底层(仓库层或服务层)的特定错误类型或错误信息来执行业务判断的逻辑。
|
||||
- [x] 调整服务层方法的调用,使其接收新的服务层输入 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`。
|
||||
- [x] 2.5.2 **修改 `internal/core/component_initializers.go`**:创建并提供新的 `UserService`。
|
||||
- [x] 2.5.3 **修改 `internal/app/api/api.go`**:更新 `UserController` 的依赖注入。
|
||||
|
||||
## 3. 验证与测试
|
||||
|
||||
- [x] 3.1 运行所有单元测试和集成测试,确保重构没有引入新的问题。
|
||||
- [x] 3.2 针对受影响的 API 接口进行手动测试,验证功能是否正常。
|
||||
- [x] 3.3 确保日志输出和审计记录仍然准确无误.
|
||||
@@ -0,0 +1,93 @@
|
||||
## Why
|
||||
|
||||
关键的系统预定义计划(如定时数据采集)应该具备高度的韧性。无论它们之前因为何种原因(例如,执行失败、手动停止、应用异常崩溃)而处于非活动状态,在应用下一次启动时,都应该被自动恢复为“启用”状态,以确保核心功能的持续、可靠运行。
|
||||
|
||||
当前的启动逻辑无法将在“失败”或“停止”状态的系统计划自动拉起,需要手动干预,这降低了系统的鲁棒性。
|
||||
|
||||
## What Changes
|
||||
|
||||
本次变更的核心是对 `PlanRepository` 进行一次重要的重构,以消除 `UpdatePlan` 函数的设计缺陷,并在此基础上,实现系统任务的启动自愈功能。
|
||||
|
||||
1. **重构 `PlanRepository`**:
|
||||
- 将行为不符合名称预期的 `UpdatePlan` 函数重命名为 `UpdatePlanMetadataAndStructure`。
|
||||
- 创建一个全新的、行为正确的 `UpdatePlan` 函数,确保全字段更新。
|
||||
|
||||
2. **修正调用点**:
|
||||
- 更新 `data_initializer.go` 中的调用,使其调用重命名后的 `UpdatePlanMetadataAndStructure`,并辅以 `UpdatePlanStatus` 来实现安全的自愈逻辑。
|
||||
- 确认 `plan_service.go` 中的调用会自然地指向新的、正确的 `UpdatePlan` 函数。
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected Specs**: `design.md` (已合并入本文档) 详细描述了重构的设计与决策。
|
||||
- **Affected Code**:
|
||||
- `internal/infra/repository/plan_repository.go`: 接口和实现将被重构。
|
||||
- `internal/core/data_initializer.go`: 调用点将被修正。
|
||||
- `internal/app/service/plan_service.go`: 调用点将隐式地指向新实现。
|
||||
|
||||
---
|
||||
|
||||
## Design Document: `UpdatePlan` Refactoring
|
||||
|
||||
### 1. 问题背景与根源分析
|
||||
|
||||
在实现“系统任务启动自愈”功能时,我们发现了一个深层次的存储库(Repository)层设计问题。
|
||||
|
||||
最初的尝试(在 `data_initializer.go` 中移除状态同步代码)失败了。经过深入调查,根源在于 `plan_repository.go` 中的 `UpdatePlan` 函数实现存在歧义,其行为与函数名不符。
|
||||
|
||||
#### `UpdatePlan` 的问题
|
||||
|
||||
该函数的内部实现 `reconcilePlanNode` 使用了 GORM 的 `Select(...)` 方法来指定要更新的数据库字段。然而,这个字段列表中**只包含了计划的元数据**(如 `Name`, `Description`, `CronExpression` 等),**并未包含 `Status` 字段**。
|
||||
|
||||
```go
|
||||
// reconcilePlanNode in plan_repository.go
|
||||
if err := tx.Model(plan).Select("Name", "Description", "ExecutionType", "ExecuteNum", "CronExpression", "ContentType").Updates(plan).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
这导致了以下问题:
|
||||
|
||||
1. **名不副实**:函数名 `UpdatePlan` 暗示了一个完整的、全字段的更新操作,但其行为却是一个不包含状态(`Status`)和执行计数(`ExecuteCount`)等运行时信息的“部分更新”。
|
||||
2. **行为不符合预期**:任何调用此函数并期望更新 `Status` 字段的上层业务逻辑,都会静默失败(即代码不报错,但数据未更新),这极易引发难以追踪的 Bug。
|
||||
3. **潜在风险**:直接修改这个函数以包含 `Status` 字段是危险的。因为我们无法确定项目中其他调用方是否依赖于它“不更新状态”这一隐性行为。任何草率的修改都可能破坏现有功能。
|
||||
|
||||
### 2. 重构方案:明确职责,消除歧义
|
||||
|
||||
为了从根本上解决此问题,并保证系统的健壮性和可维护性,我们决定对 `PlanRepository` 接口及其实现进行重构。
|
||||
|
||||
核心思想是**让函数的名称和行为完全匹配**。
|
||||
|
||||
#### 2.1. 接口与实现变更
|
||||
|
||||
我们将对 `plan_repository.go` 进行以下修改:
|
||||
|
||||
1. **重命名旧函数**:将现有的、有问题的 `UpdatePlan` 函数重命名为 `UpdatePlanMetadataAndStructure`。这个新名字精确地描述了它的实际行为:只更新计划的元数据和结构(任务或子计划),而不触及运行时状态。
|
||||
|
||||
2. **创建新函数**:创建一个新的、名为 `UpdatePlan` 的函数。这个新函数将提供一个符合开发者直觉的、真正的“全字段更新”功能。它将使用 GORM 的 `Save` 方法来实现,确保模型对象中的所有字段都被持久化到数据库。
|
||||
|
||||
```go
|
||||
// 新的、正确的 UpdatePlan 实现
|
||||
func (r *gormPlanRepository) UpdatePlan(plan *models.Plan) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
return tx.Save(plan).Error
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2. 调用点修正
|
||||
|
||||
在完成上述重构后,我们需要审查并修正所有原 `UpdatePlan` 的调用点,根据其业务意图,将其指向正确的函数:
|
||||
|
||||
1. **`internal/core/data_initializer.go`**:
|
||||
* **业务意图**:在应用启动时,用预定义模板更新系统计划的元数据,并强制重置其状态为“启用”。
|
||||
* **修正方案**:此处的调用应该指向 `UpdatePlanMetadataAndStructure`,以确保只更新元数据。后续再通过调用专用的 `UpdatePlanStatus` 方法来安全地重置状态。这完美地分离了两个不同的关注点。
|
||||
|
||||
2. **`internal/app/service/plan_service.go`**:
|
||||
* **业务意图**:提供一个供 API 使用的公共服务,允许用户完整地更新一个计划的所有信息。
|
||||
* **修正方案**:此处的调用应该指向新的 `UpdatePlan` 函数。由于函数名没有改变,此文件中的代码**无需修改**,但在逻辑上它已经从调用一个有问题的旧函数,变成了调用一个行为正确的新函数。
|
||||
|
||||
### 3. 结论
|
||||
|
||||
这个重构方案不仅解决了最初的“系统任务自愈”功能实现障碍,更重要的是,它消除了一处严重的设计隐患,提升了代码库的整体质量和可维护性。
|
||||
|
||||
通过让函数名副其实,我们为未来的开发和维护工作扫清了障碍,降低了出现类似 Bug 的风险。
|
||||
@@ -0,0 +1,12 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 系统计划在启动时应具备恢复能力
|
||||
|
||||
系统 SHALL 确保类型为“系统计划” (`PlanTypeSystem`) 的计划在应用启动时,即使因为上次异常关闭而处于未完成状态,也不会被标记为“失败” (`执行失败`)。
|
||||
|
||||
#### Scenario: 系统计划在执行中遭遇应用重启
|
||||
- **GIVEN** 一个状态为“已启用” (`已启用`) 的系统计划正在执行
|
||||
- **WHEN** 应用意外崩溃或重启
|
||||
- **AND** 应用完成启动初始化流程
|
||||
- **THEN** 该系统计划的状态应依然为“已启用” (`已启用`)
|
||||
- **AND** 该系统计划应能够在下一个调度周期被正常触发执行
|
||||
@@ -0,0 +1,29 @@
|
||||
1. **分析与方案制定**
|
||||
- [x] 发现“系统任务自愈”功能无法按预期工作。
|
||||
- [x] 深入分析发现根源在于 `PlanRepository.UpdatePlan` 函数的设计缺陷(名不副实,部分更新)。
|
||||
- [x] 制定了包含代码重构的最终方案,以从根本上解决问题。
|
||||
|
||||
2. **文档更新**
|
||||
- [x] 新建 `design.md` (已合并入 `proposal.md`),详细记录问题分析、设计决策和重构方案。
|
||||
- [x] 更新 `proposal.md` 以反映最终的、包含重构的变更范围。
|
||||
- [x] 更新 `tasks.md` 任务列表。
|
||||
|
||||
3. **代码重构与修改**
|
||||
- [x] **重构 `plan_repository.go`**:
|
||||
- [x] 将 `UpdatePlan` 接口和实现重命名为 `UpdatePlanMetadataAndStructure`。
|
||||
- [x] 创建一个新的、使用 `Save` 实现全字段更新的 `UpdatePlan` 函数。
|
||||
- [x] **修正 `data_initializer.go`**:
|
||||
- [x] 将对旧 `UpdatePlan` 的调用修改为 `UpdatePlanMetadataAndStructure`。
|
||||
- [x] 保留并确认后续调用 `UpdatePlanStatus` 的逻辑,以完成状态恢复。
|
||||
|
||||
4. **测试与验证**
|
||||
- [x] **场景一:系统任务失败**
|
||||
- [x] 手动将 `PlanNameTimedFullDataCollection` 计划在数据库中的状态修改为 `failed`。
|
||||
- [x] 重启应用。
|
||||
- [x] 验证该计划的状态是否已自动恢复为 `enabled`。
|
||||
- [x] **场景二:系统任务被停止**
|
||||
- [x] 手动将 `PlanNameTimedFullDataCollection` 计划在数据库中的状态修改为 `stopped`。
|
||||
- [x] 重启应用。
|
||||
- [x] 验证该计划的状态是否已自动恢复为 `enabled`。
|
||||
- [x] **场景三:回归测试 `plan_service`**
|
||||
- [x] (可选) 通过 API 调用更新一个普通计划,确认其所有字段(包括状态)都能被正确更新。
|
||||
31
openspec/project.md
Normal file
31
openspec/project.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Project Context
|
||||
|
||||
## Purpose
|
||||
[Describe your project's purpose and goals]
|
||||
|
||||
## Tech Stack
|
||||
- [List your primary technologies]
|
||||
- [e.g., TypeScript, React, Node.js]
|
||||
|
||||
## Project Conventions
|
||||
|
||||
### Code Style
|
||||
[Describe your code style preferences, formatting rules, and naming conventions]
|
||||
|
||||
### Architecture Patterns
|
||||
[Document your architectural decisions and patterns]
|
||||
|
||||
### Testing Strategy
|
||||
[Explain your testing approach and requirements]
|
||||
|
||||
### Git Workflow
|
||||
[Describe your branching strategy and commit conventions]
|
||||
|
||||
## Domain Context
|
||||
[Add domain-specific knowledge that AI assistants need to understand]
|
||||
|
||||
## Important Constraints
|
||||
[List any technical, business, or regulatory constraints]
|
||||
|
||||
## External Dependencies
|
||||
[Document key external services, APIs, or systems]
|
||||
69
openspec/specs/business-logic-layering/spec.md
Normal file
69
openspec/specs/business-logic-layering/spec.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# business-logic-layering Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change refactor-business-logic-layering. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 服务层接口标准化
|
||||
- **说明**: 服务层方法现在 **MUST** 只接收数据传输对象 (DTO) 或基本参数,并 **MUST** 只返回 DTO 或业务领域对象,不再直接暴露数据库模型。
|
||||
- **理由**: 减少服务层与持久化细节的耦合,提高接口的抽象性和稳定性。
|
||||
- **影响**: 高。所有调用服务层的方法都需要调整。
|
||||
- **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。
|
||||
|
||||
#### Scenario: 服务层方法接收 DTO 作为输入
|
||||
- **假如**: `UserService` 的 `CreateUser` 方法被调用。
|
||||
- **当**: `CreateUser` 方法接收 `dto.CreateUserRequest` 作为参数。
|
||||
- **那么**: `UserService` 内部负责将 `dto.CreateUserRequest` 转换为 `models.User` 进行处理。
|
||||
|
||||
#### Scenario: 服务层方法返回 DTO 作为输出
|
||||
- **假如**: `UserService` 的 `CreateUser` 方法执行成功。
|
||||
- **当**: `CreateUser` 方法返回 `*dto.CreateUserResponse`。
|
||||
- **那么**: 调用方可以直接使用 `dto.CreateUserResponse`,无需进行额外的模型转换。
|
||||
|
||||
### Requirement: 控制器层职责收敛
|
||||
- **说明**: 控制器层现在 **MUST** 仅负责 HTTP 请求的参数绑定与校验、调用服务层方法,并将服务层返回的 DTO 转换为 HTTP 响应。所有业务逻辑、领域对象的创建与验证、以及与仓库层的直接交互都 **MUST** 从控制器层移除并下沉到服务层。
|
||||
- **理由**: 遵循“关注点分离”原则,使控制器层专注于 HTTP 协议处理,提高代码的可维护性和可测试性。
|
||||
- **影响**: 高。所有控制器方法都需要大幅简化。
|
||||
- **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。
|
||||
|
||||
#### Scenario: 控制器不再直接进行领域模型内部字段的序列化/反序列化
|
||||
- **假如**: `DeviceController` 的 `CreateDevice` 方法被调用。
|
||||
- **当**: `CreateDevice` 方法不再包含 `json.Marshal` 或 `json.Unmarshal` 等操作来处理 `Properties` 等字段。
|
||||
- **那么**: 这些序列化/反序列化逻辑已下沉到 `DeviceService` 中。
|
||||
|
||||
#### Scenario: 控制器不再直接实例化领域模型对象
|
||||
- **假如**: `UserController` 的 `CreateUser` 方法被调用。
|
||||
- **当**: `CreateUser` 方法不再包含 `&models.User{...}` 这样的代码。
|
||||
- **那么**: 领域模型的创建已通过 `UserService` 完成。
|
||||
|
||||
#### Scenario: 控制器不再直接调用仓库层方法
|
||||
- **假如**: `PlanController` 的 `ListPlans` 方法被调用。
|
||||
- **当**: `ListPlans` 方法不再直接调用 `planRepo.ListPlans`。
|
||||
- **那么**: `PlanService` 负责协调 `PlanRepository`。
|
||||
|
||||
#### Scenario: 控制器不再直接进行业务规则判断
|
||||
- **假如**: `PlanController` 的 `UpdatePlan` 方法被调用。
|
||||
- **当**: `UpdatePlan` 方法不再包含对计划类型、状态或 `ContentType` 的直接判断逻辑。
|
||||
- **那么**: 这些业务规则判断已下沉到 `PlanService` 中。
|
||||
|
||||
### Requirement: DTO 转换逻辑下沉
|
||||
- **说明**: 数据库模型与 DTO 之间的转换逻辑 **MUST** 从控制器层移动到服务层内部或专门的转换器中。
|
||||
- **理由**: 确保数据转换逻辑与业务逻辑紧密结合,避免控制器层承担不必要的职责。
|
||||
- **影响**: 中。主要影响数据流转和转换点。
|
||||
- **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。
|
||||
|
||||
#### Scenario: 服务层负责将数据库模型转换为响应 DTO
|
||||
- **假如**: `PigFarmService` 的 `GetPigHouseByID` 方法从 `repository` 获取到 `models.PigHouse`。
|
||||
- **当**: `GetPigHouseByID` 方法在返回前将 `models.PigHouse` 转换为 `dto.PigHouseResponse`。
|
||||
- **那么**: 控制器直接接收 `dto.PigHouseResponse`。
|
||||
|
||||
### Requirement: 业务错误处理优化
|
||||
- **说明**: 服务层现在 **MUST** 返回更抽象的业务错误,控制器层 **MUST** 根据这些抽象错误进行统一的 HTTP 响应处理,避免直接依赖仓库层或服务层内部的具体错误类型或错误信息。
|
||||
- **理由**: 提高错误处理的一致性和可维护性,解耦控制器与底层错误实现。
|
||||
- **影响**: 中。影响错误处理流程。
|
||||
- **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。
|
||||
|
||||
#### Scenario: 服务层返回抽象业务错误
|
||||
- **假如**: `UserService` 的 `CreateUser` 方法因用户名重复而失败。
|
||||
- **当**: `UserService` 返回一个表示“用户名已存在”的抽象错误(例如自定义错误类型或包装后的错误)。
|
||||
- **那么**: `UserController` 接收到此抽象错误后,可以统一转换为相应的 HTTP 状态码和错误信息,而无需解析底层 `gorm.ErrDuplicatedKey` 等具体错误。
|
||||
|
||||
17
openspec/specs/http-server/spec.md
Normal file
17
openspec/specs/http-server/spec.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# HTTP Server Capability Specification
|
||||
|
||||
## Purpose
|
||||
该规范描述了本项目中 HTTP 服务器的功能和设计目标。它确保了 API 的可靠性和可维护性。
|
||||
## Requirements
|
||||
### Requirement: API 服务器框架已更新
|
||||
|
||||
- **说明**: 底层 Web 框架从 Gin 迁移到 Echo。所有现有的 API 端点 **MUST** 保持功能齐全和向后兼容。
|
||||
- **理由**: 为了提高路由灵活性并使技术栈现代化。这是一次技术重构,不会改变任何外部 API 行为。
|
||||
- **影响**: 高。影响核心请求处理、路由和中间件。
|
||||
- **受影响的端点**: 全部。
|
||||
|
||||
#### Scenario: 所有现有的 API 端点保持功能齐全和向后兼容
|
||||
- **假如**: API 服务器在迁移到 Echo 后正在运行。
|
||||
- **当**: 客户端向任何现有的 API 端点(例如, `POST /api/v1/users/login`)发送请求。
|
||||
- **那么**: 服务器处理该请求并返回与使用 Gin 框架时完全相同的响应(状态码、头部和正文格式)。
|
||||
|
||||
16
openspec/specs/plan-lifecycle/spec.md
Normal file
16
openspec/specs/plan-lifecycle/spec.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# plan-lifecycle Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-plan-recovery-option. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 系统计划在启动时应具备恢复能力
|
||||
|
||||
系统 SHALL 确保类型为“系统计划” (`PlanTypeSystem`) 的计划在应用启动时,即使因为上次异常关闭而处于未完成状态,也不会被标记为“失败” (`执行失败`)。
|
||||
|
||||
#### Scenario: 系统计划在执行中遭遇应用重启
|
||||
- **GIVEN** 一个状态为“已启用” (`已启用`) 的系统计划正在执行
|
||||
- **WHEN** 应用意外崩溃或重启
|
||||
- **AND** 应用完成启动初始化流程
|
||||
- **THEN** 该系统计划的状态应依然为“已启用” (`已启用`)
|
||||
- **AND** 该系统计划应能够在下一个调度周期被正常触发执行
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user