Merge pull request 'issue_62' (#65) from issue_62 into main
Reviewed-on: #65
This commit is contained in:
@@ -113,3 +113,14 @@ notify:
|
||||
# 定时采集配置
|
||||
collection:
|
||||
interval: 1 # 采集间隔 (分钟)
|
||||
|
||||
# 告警通知配置
|
||||
alarm_notification:
|
||||
notification_intervals: # 告警通知间隔(分钟)
|
||||
debug: 1
|
||||
info: 1
|
||||
warn: 1
|
||||
error: 1
|
||||
dpanic: 1
|
||||
panic: 1
|
||||
fatal: 1
|
||||
|
||||
13
config.yml
13
config.yml
@@ -90,4 +90,15 @@ lora_mesh:
|
||||
|
||||
# 定时采集配置
|
||||
collection:
|
||||
interval: 1 # 采集间隔 (分钟)
|
||||
interval: 1 # 采集间隔 (分钟)
|
||||
|
||||
# 告警通知配置
|
||||
alarm_notification:
|
||||
notification_intervals: # 告警通知间隔 (分钟)
|
||||
debug: 1
|
||||
info: 1
|
||||
warn: 1
|
||||
error: 1
|
||||
dpanic: 1
|
||||
panic: 1
|
||||
fatal: 1
|
||||
|
||||
149
design/exceeding-threshold-alarm/index.md
Normal file
149
design/exceeding-threshold-alarm/index.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# 需求
|
||||
|
||||
实现采集数据超过阈值报警
|
||||
|
||||
## issue
|
||||
|
||||
[实现采集数据超过阈值报警](http://git.huangwc.com/pig/pig-farm-controller/issues/62)
|
||||
|
||||
# 方案
|
||||
|
||||
1. **架构核心**: 新增一个 **告警领域服务**,作为告警系统的核心大脑,负责告警事件的生命周期管理。
|
||||
2. **任务分离**:
|
||||
* 新增 **阈值告警任务** (分为区域主控和普通设备两种),仅负责检测数据并将结果报告给领域服务。
|
||||
* 新增 **告警通知发送任务**,作为一个独立的、系统预定义的定时任务,负责调用领域服务,获取并发送所有待处理的通知。
|
||||
3. **计划调度**:
|
||||
* 修改现有 "定时全量数据采集" 计划, 更名为 "周期性系统健康检查"。此计划包含固定的 **全量采集任务** 和由用户动态配置的
|
||||
**阈值告警任务**。
|
||||
* 新增一个独立的 "告警通知发送" 计划,用于定时执行固定的 **告警通知发送任务**。
|
||||
4. **数据与接口**:
|
||||
* 新增独立的告警记录表(建议采用“活跃告警表 + 历史告警超表”的模式)。
|
||||
* 新增相应的告警配置管理接口。
|
||||
|
||||
## 方案细节
|
||||
|
||||
### 架构与职责划分
|
||||
|
||||
1. **告警领域服务 (`internal/domain/alarm/`) - 管理器**
|
||||
* **职责**: 作为告警系统的核心大脑,负责处理告警事件的完整生命周期。
|
||||
* **功能**:
|
||||
* 接收来自检测任务的状态报告(包含设备ID、传感器类型、当前是否异常等信息)。
|
||||
* 根据报告和数据库中的告警记录,决策是创建新告警、更新为已解决、还是因被忽略而跳过。
|
||||
* 管理“手动忽略” (`Ignored`) 状态和忽略到期时间 (`ignored_until`)。
|
||||
* 实现可配置的“重复通知”策略(`re_notification_interval`),决定何时对持续存在的告警再次发送通知。
|
||||
* 提供接口供 `告警通知发送任务` 调用,以获取所有待处理的通知。
|
||||
|
||||
2. **阈值告警任务 (`internal/domain/task/`) - 检测器**
|
||||
* **职责**: 职责纯粹,仅负责执行检测并将结果报告给告警领域服务。
|
||||
* **逻辑**: 从传感器数据表读取最新数据 -> 与自身配置的阈值进行比对 -> 无论结果如何,都调用 `告警领域服务.ReportStatus()`
|
||||
报告当前状态(正常或异常)。
|
||||
* **无状态**: 任务本身不关心告警是否已存在或被忽略,它只负责“状态同步”。
|
||||
|
||||
3. **告警通知发送任务 (`internal/domain/task/`) - 发送器**
|
||||
* **职责**: 作为一个独立的定时任务,解耦通知发送与告警检测。
|
||||
* **逻辑**: 调用 `告警领域服务.GetAndProcessPendingNotifications()` -> 获取待发送通知列表 -> 调用 `通知领域服务`
|
||||
逐一发送。
|
||||
* **优势**: 统一管理定时任务,实现资源控制,提高系统稳定性和可扩展性。
|
||||
|
||||
### 计划与任务调度
|
||||
|
||||
1. **"周期性系统健康检查" 计划**
|
||||
* **任务构成**:
|
||||
* **全量数据采集任务 (ExecutionOrder: 1)**: 系统预定义,必须是第一个执行的任务,为后续的告警检测提供最新的数据基础。
|
||||
* **阈值告警任务 (ExecutionOrder: 2, 3...)**: 由用户通过API动态配置和管理,`告警配置服务` 负责将其增删改到此计划中。
|
||||
|
||||
2. **"告警通知发送" 计划**
|
||||
* **任务构成**: 包含一个系统预定义的 `告警通知发送任务`。
|
||||
* **调度**: 可配置独立的执行频率(如每分钟一次),与健康检查计划解耦。
|
||||
|
||||
3. **系统初始化 (`data_initializer.go`)**
|
||||
* **职责**: 只负责创建和维护系统预定义的、固定的计划和任务。
|
||||
* **操作**:
|
||||
* 确保 "周期性系统健康检查" 计划存在,并包含 `全量数据采集任务`。
|
||||
* 确保 "告警通知发送" 计划存在,并包含 `告警通知发送任务`。
|
||||
* **注意**: 初始化逻辑 **不会** 也 **不应该** 触及用户动态配置的阈值告警任务。
|
||||
|
||||
### 阈值告警任务 (用户可配置的任务类型)
|
||||
|
||||
1. **任务类型**: 提供两种可供用户配置的阈值告警任务类型,分别对应 **区域主控** 和 **普通设备** 告警。
|
||||
2. **参数结构**:
|
||||
* **通用参数**: 任务参数将包含 `Thresholds` (阈值) 和 `Operator` (操作符,如 `>` 或 `<`) 字段。
|
||||
* **普通设备任务**: 配置包含 `DeviceID`。
|
||||
* **区域主控任务**: 配置包含 `AreaControllerID`, `SensorType`, 以及一个 `ExcludeDeviceIDs` (需要排除的设备ID列表)。
|
||||
|
||||
### 告警事件与生命周期
|
||||
|
||||
1. **告警事件定义**:
|
||||
* 区分 **告警规则** (配置的策略) 和 **告警事件** (规则被具体设备触发的实例)。
|
||||
* 区域主控下不同设备触发的告警,即使基于同一规则,也应视为独立的 **告警事件**,以便于精确追溯和独立操作。
|
||||
|
||||
2. **生命周期管理**:
|
||||
* **自动闭环**: 当阈值告警任务报告数据恢复正常时,告警领域服务会自动将对应的 `Active` 告警事件状态更新为 `Resolved`。
|
||||
* **手动忽略 (Snooze)**: 用户可通过接口将告警事件状态置为 `Ignored` 并设置 `ignored_until`
|
||||
。在此期间,即使数据持续异常,也不会发送通知。忽略到期后若问题仍存在,告警将重新变为 `Active` 并发送通知。
|
||||
* **持续告警与重复通知**: 对持续未解决的 `Active` 告警,只保留一条记录。告警领域服务会根据 `re_notification_interval`
|
||||
配置的重复通知间隔,决定是否需要再次发送通知。
|
||||
|
||||
### 数据库设计考量
|
||||
|
||||
1. **冷热分离方案 (推荐)**:
|
||||
* **`active_alarms` (活跃告警表)**:
|
||||
* **类型**: 标准 PostgreSQL 表。
|
||||
* **内容**: 只存放 `Active` 和 `Ignored` 状态的告警。
|
||||
* **优势**: 保证高频读写的性能,避免在被压缩的数据上执行更新操作。
|
||||
* **`historical_alarms` (历史告警表)**:
|
||||
* **类型**: 改造为 **TimescaleDB 超表**。
|
||||
* **内容**: 存放 `Resolved` 状态的告警。当告警在 `active_alarms` 中被解决后,记录将移至此表。
|
||||
* **优势**: 适合存储海量历史数据,便于分析、统计,并可利用 TimescaleDB 的压缩和数据生命周期管理功能。
|
||||
|
||||
2. **表结构字段**:
|
||||
* `status`: 枚举类型,包含 `Active`, `Resolved`, `Ignored`。
|
||||
* `ignored_until`: `timestamp` 类型,记录忽略截止时间。
|
||||
* `last_notified_at`: `timestamp` 类型,记录上次发送通知的时间。
|
||||
|
||||
### 阈值告警服务 (领域层)
|
||||
|
||||
1. **服务职责**:
|
||||
* 负责管理阈值告警 **任务配置** 的增删改查。这些任务配置包含了具体的阈值规则。
|
||||
* 负责将用户创建的阈值告警任务动态更新到 "周期性系统健康检查" 计划中。
|
||||
* **任务配置引用检查**: 提供自检方法,用于在删除设备或设备模板前,检查它们是否被任何阈值告警任务配置所引用,以防止产生悬空引用。
|
||||
|
||||
2. **排除列表计算与联动**:
|
||||
* **删除独立任务配置后归属**: 当一个普通设备的独立告警任务配置被删除时,它将自动从其所属区域主控的 `ExcludeDeviceIDs`
|
||||
列表中移除,从而回归到区域统一告警策略的管理之下。
|
||||
* **设备生命周期管理**: 在对设备进行修改(特别是更换区域主控)或删除时,以及在删除区域主控时,必须同步更新相关的
|
||||
`ExcludeDeviceIDs` 列表,同时解决相关告警(当删除时), 以保证数据一致性。
|
||||
* **实现**: `DeviceService` 中负责处理设备更新和删除的方法,需要调用本服务提供的“任务配置引用检查”和刷新接口。
|
||||
|
||||
### 阈值告警控制器
|
||||
|
||||
1. **独立接口**: 提供两组独立的 Web 接口,分别用于管理区域主控和普通设备的阈值告警配置。
|
||||
* 区域主控告警配置接口: `/api/v1/alarm/region-config`
|
||||
* 普通设备告警配置接口: `/api/v1/alarm/device-config`
|
||||
2. **接口职责**: 接口负责接收前端请求,调用应用服务层的阈值告警服务来完成实际的业务逻辑。
|
||||
|
||||
### TODO
|
||||
|
||||
1. 是否要加一个延时操作, 因为采集是异步的, 采集任务结束时不一定能拿到最新数据, 所以需要一个延时操作等待区域主控上传
|
||||
2. 统一一下区域主控的命名, 目前有AreaController和RegionalController, 不排除还有别的
|
||||
3. 将数据类型转为float32, 节约空间, float64精度有些浪费, float32小数点后6-7位足够了
|
||||
|
||||
# 实现记录
|
||||
|
||||
1. 定义告警表和告警历史表
|
||||
2. 重构部分枚举, 让models包不依赖其他项目中的包
|
||||
3. 创建仓库层对象(不包含方法)
|
||||
4. 实现告警发送任务
|
||||
5. 实现告警通知发送计划/全量采集计划改名
|
||||
6. 实现设备阈值检查任务
|
||||
7. 实现忽略告警和取消忽略告警接口及功能
|
||||
8. 实现列表查询活跃告警和历史告警
|
||||
9. 系统初始化时健康计划调整(包括增加延时任务)
|
||||
10. 实现区域阈值告警任务
|
||||
11. 实现区域阈值告警和设备阈值告警的增删改查
|
||||
12. 实现任务11应的八个web接口
|
||||
13. 实现根据区域ID或设备ID清空对应阈值告警任务
|
||||
14. 设备和区域主控删除时清除对应区域阈值告警或设备阈值告警任务
|
||||
15. 将所有Regional更改为Area
|
||||
16. float64全部改float32
|
||||
17. uint/uint64全部改为uint32
|
||||
1146
docs/docs.go
1146
docs/docs.go
File diff suppressed because it is too large
Load Diff
1146
docs/swagger.json
1146
docs/swagger.json
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,35 @@ definitions:
|
||||
- CodeConflict
|
||||
- CodeInternalError
|
||||
- CodeServiceUnavailable
|
||||
dto.ActiveAlarmDTO:
|
||||
properties:
|
||||
alarm_code:
|
||||
$ref: '#/definitions/models.AlarmCode'
|
||||
alarm_details:
|
||||
type: string
|
||||
alarm_summary:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
ignored_until:
|
||||
type: string
|
||||
is_ignored:
|
||||
type: boolean
|
||||
last_notified_at:
|
||||
type: string
|
||||
level:
|
||||
$ref: '#/definitions/models.SeverityLevel'
|
||||
source_id:
|
||||
type: integer
|
||||
source_type:
|
||||
$ref: '#/definitions/models.AlarmSourceType'
|
||||
trigger_time:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
dto.AreaControllerResponse:
|
||||
properties:
|
||||
created_at:
|
||||
@@ -73,6 +102,21 @@ definitions:
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
dto.AreaThresholdAlarmDTO:
|
||||
properties:
|
||||
area_controller_id:
|
||||
type: integer
|
||||
id:
|
||||
type: integer
|
||||
level:
|
||||
$ref: '#/definitions/models.SeverityLevel'
|
||||
operator:
|
||||
$ref: '#/definitions/models.Operator'
|
||||
sensor_type:
|
||||
$ref: '#/definitions/models.SensorType'
|
||||
thresholds:
|
||||
type: number
|
||||
type: object
|
||||
dto.AssignEmptyPensToBatchRequest:
|
||||
properties:
|
||||
pen_ids:
|
||||
@@ -137,6 +181,32 @@ definitions:
|
||||
- name
|
||||
- network_id
|
||||
type: object
|
||||
dto.CreateAreaThresholdAlarmDTO:
|
||||
properties:
|
||||
area_controller_id:
|
||||
description: 区域主控ID
|
||||
type: integer
|
||||
level:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.SeverityLevel'
|
||||
description: 告警等级,可选
|
||||
operator:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.Operator'
|
||||
description: 操作符
|
||||
sensor_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.SensorType'
|
||||
description: 传感器类型
|
||||
thresholds:
|
||||
description: 阈值
|
||||
type: number
|
||||
required:
|
||||
- area_controller_id
|
||||
- operator
|
||||
- sensor_type
|
||||
- thresholds
|
||||
type: object
|
||||
dto.CreateDeviceRequest:
|
||||
properties:
|
||||
area_controller_id:
|
||||
@@ -177,6 +247,32 @@ definitions:
|
||||
- commands
|
||||
- name
|
||||
type: object
|
||||
dto.CreateDeviceThresholdAlarmDTO:
|
||||
properties:
|
||||
device_id:
|
||||
description: 设备ID
|
||||
type: integer
|
||||
level:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.SeverityLevel'
|
||||
description: 告警等级,可选,如果未提供则使用默认值
|
||||
operator:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.Operator'
|
||||
description: 操作符 (使用string类型,与前端交互更通用)
|
||||
sensor_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.SensorType'
|
||||
description: 传感器类型
|
||||
thresholds:
|
||||
description: 阈值
|
||||
type: number
|
||||
required:
|
||||
- device_id
|
||||
- operator
|
||||
- sensor_type
|
||||
- thresholds
|
||||
type: object
|
||||
dto.CreatePenRequest:
|
||||
properties:
|
||||
capacity:
|
||||
@@ -251,6 +347,15 @@ definitions:
|
||||
example: newuser
|
||||
type: string
|
||||
type: object
|
||||
dto.DeleteDeviceThresholdAlarmDTO:
|
||||
properties:
|
||||
sensor_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.SensorType'
|
||||
description: 传感器类型
|
||||
required:
|
||||
- sensor_type
|
||||
type: object
|
||||
dto.DeviceCommandLogDTO:
|
||||
properties:
|
||||
acknowledged_at:
|
||||
@@ -312,6 +417,21 @@ definitions:
|
||||
$ref: '#/definitions/models.ValueDescriptor'
|
||||
type: array
|
||||
type: object
|
||||
dto.DeviceThresholdAlarmDTO:
|
||||
properties:
|
||||
device_id:
|
||||
type: integer
|
||||
id:
|
||||
type: integer
|
||||
level:
|
||||
$ref: '#/definitions/models.SeverityLevel'
|
||||
operator:
|
||||
$ref: '#/definitions/models.Operator'
|
||||
sensor_type:
|
||||
$ref: '#/definitions/models.SensorType'
|
||||
thresholds:
|
||||
type: number
|
||||
type: object
|
||||
dto.FeedFormulaDTO:
|
||||
properties:
|
||||
id:
|
||||
@@ -340,6 +460,40 @@ definitions:
|
||||
remarks:
|
||||
type: string
|
||||
type: object
|
||||
dto.HistoricalAlarmDTO:
|
||||
properties:
|
||||
alarm_code:
|
||||
$ref: '#/definitions/models.AlarmCode'
|
||||
alarm_details:
|
||||
type: string
|
||||
alarm_summary:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
level:
|
||||
$ref: '#/definitions/models.SeverityLevel'
|
||||
resolve_method:
|
||||
type: string
|
||||
resolve_time:
|
||||
type: string
|
||||
resolved_by:
|
||||
type: integer
|
||||
source_id:
|
||||
type: integer
|
||||
source_type:
|
||||
$ref: '#/definitions/models.AlarmSourceType'
|
||||
trigger_time:
|
||||
type: string
|
||||
type: object
|
||||
dto.ListActiveAlarmResponse:
|
||||
properties:
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/dto.ActiveAlarmDTO'
|
||||
type: array
|
||||
pagination:
|
||||
$ref: '#/definitions/dto.PaginationDTO'
|
||||
type: object
|
||||
dto.ListDeviceCommandLogResponse:
|
||||
properties:
|
||||
list:
|
||||
@@ -358,6 +512,15 @@ definitions:
|
||||
pagination:
|
||||
$ref: '#/definitions/dto.PaginationDTO'
|
||||
type: object
|
||||
dto.ListHistoricalAlarmResponse:
|
||||
properties:
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/dto.HistoricalAlarmDTO'
|
||||
type: array
|
||||
pagination:
|
||||
$ref: '#/definitions/dto.PaginationDTO'
|
||||
type: object
|
||||
dto.ListMedicationLogResponse:
|
||||
properties:
|
||||
list:
|
||||
@@ -600,11 +763,11 @@ definitions:
|
||||
id:
|
||||
type: integer
|
||||
level:
|
||||
$ref: '#/definitions/zapcore.Level'
|
||||
$ref: '#/definitions/models.SeverityLevel'
|
||||
message:
|
||||
type: string
|
||||
notifier_type:
|
||||
$ref: '#/definitions/notify.NotifierType'
|
||||
$ref: '#/definitions/models.NotifierType'
|
||||
status:
|
||||
$ref: '#/definitions/models.NotificationStatus'
|
||||
title:
|
||||
@@ -1199,26 +1362,35 @@ definitions:
|
||||
properties:
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/notify.NotifierType'
|
||||
- $ref: '#/definitions/models.NotifierType'
|
||||
description: Type 指定要测试的通知渠道
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
dto.SensorDataDTO:
|
||||
properties:
|
||||
area_controller_id:
|
||||
type: integer
|
||||
data:
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
device_id:
|
||||
type: integer
|
||||
regional_controller_id:
|
||||
type: integer
|
||||
sensor_type:
|
||||
$ref: '#/definitions/models.SensorType'
|
||||
time:
|
||||
type: string
|
||||
type: object
|
||||
dto.SnoozeAlarmRequest:
|
||||
properties:
|
||||
duration_minutes:
|
||||
description: 忽略时长,单位分钟
|
||||
minimum: 1
|
||||
type: integer
|
||||
required:
|
||||
- duration_minutes
|
||||
type: object
|
||||
dto.SubPlanResponse:
|
||||
properties:
|
||||
child_plan:
|
||||
@@ -1373,6 +1545,23 @@ definitions:
|
||||
- name
|
||||
- network_id
|
||||
type: object
|
||||
dto.UpdateAreaThresholdAlarmDTO:
|
||||
properties:
|
||||
level:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.SeverityLevel'
|
||||
description: 新的告警等级,可选
|
||||
operator:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.Operator'
|
||||
description: 新的操作符
|
||||
thresholds:
|
||||
description: 新的阈值
|
||||
type: number
|
||||
required:
|
||||
- operator
|
||||
- thresholds
|
||||
type: object
|
||||
dto.UpdateDeviceRequest:
|
||||
properties:
|
||||
area_controller_id:
|
||||
@@ -1413,6 +1602,23 @@ definitions:
|
||||
- commands
|
||||
- name
|
||||
type: object
|
||||
dto.UpdateDeviceThresholdAlarmDTO:
|
||||
properties:
|
||||
level:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.SeverityLevel'
|
||||
description: 新的告警等级,可选
|
||||
operator:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.Operator'
|
||||
description: 新的操作符
|
||||
thresholds:
|
||||
description: 新的阈值
|
||||
type: number
|
||||
required:
|
||||
- operator
|
||||
- thresholds
|
||||
type: object
|
||||
dto.UpdatePenRequest:
|
||||
properties:
|
||||
capacity:
|
||||
@@ -1558,6 +1764,34 @@ definitions:
|
||||
weight:
|
||||
type: number
|
||||
type: object
|
||||
models.AlarmCode:
|
||||
enum:
|
||||
- 温度阈值
|
||||
- 湿度阈值
|
||||
- 重量阈值
|
||||
- 电池电量阈值
|
||||
- 信号强度阈值
|
||||
- 设备离线
|
||||
- 区域主控离线
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- AlarmCodeTemperature
|
||||
- AlarmCodeHumidity
|
||||
- AlarmCodeWeight
|
||||
- AlarmCodeBatteryLevel
|
||||
- AlarmCodeSignalMetrics
|
||||
- AlarmCodeDeviceOffline
|
||||
- AlarmCodeAreaControllerOffline
|
||||
models.AlarmSourceType:
|
||||
enum:
|
||||
- 普通设备
|
||||
- 区域主控
|
||||
- 系统
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- AlarmSourceTypeDevice
|
||||
- AlarmSourceTypeAreaController
|
||||
- AlarmSourceTypeSystem
|
||||
models.AuditStatus:
|
||||
enum:
|
||||
- 成功
|
||||
@@ -1646,6 +1880,34 @@ definitions:
|
||||
- NotificationStatusSuccess
|
||||
- NotificationStatusFailed
|
||||
- NotificationStatusSkipped
|
||||
models.NotifierType:
|
||||
enum:
|
||||
- 邮件
|
||||
- 企业微信
|
||||
- 飞书
|
||||
- 日志
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- NotifierTypeSMTP
|
||||
- NotifierTypeWeChat
|
||||
- NotifierTypeLark
|
||||
- NotifierTypeLog
|
||||
models.Operator:
|
||||
enum:
|
||||
- <
|
||||
- <=
|
||||
- '>'
|
||||
- '>='
|
||||
- =
|
||||
- '!='
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- OperatorLessThan
|
||||
- OperatorLessThanOrEqualTo
|
||||
- OperatorGreaterThan
|
||||
- OperatorGreaterThanOrEqualTo
|
||||
- OperatorEqualTo
|
||||
- OperatorNotEqualTo
|
||||
models.PenStatus:
|
||||
enum:
|
||||
- 空闲
|
||||
@@ -1877,6 +2139,24 @@ definitions:
|
||||
- SensorTypeTemperature
|
||||
- SensorTypeHumidity
|
||||
- SensorTypeWeight
|
||||
models.SeverityLevel:
|
||||
enum:
|
||||
- Debug
|
||||
- Info
|
||||
- Warn
|
||||
- Error
|
||||
- DPanic
|
||||
- Panic
|
||||
- Fatal
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
- ErrorLevel
|
||||
- DPanicLevel
|
||||
- PanicLevel
|
||||
- FatalLevel
|
||||
models.StockLogSourceType:
|
||||
enum:
|
||||
- 采购入库
|
||||
@@ -1899,9 +2179,15 @@ definitions:
|
||||
- 等待
|
||||
- 下料
|
||||
- 全量采集
|
||||
- 告警通知
|
||||
- 设备阈值检查
|
||||
- 区域阈值检查
|
||||
type: string
|
||||
x-enum-comments:
|
||||
TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务
|
||||
TaskTypeAlarmNotification: 告警通知任务
|
||||
TaskTypeAreaCollectorThresholdCheck: 区域阈值检查任务
|
||||
TaskTypeDeviceThresholdCheck: 设备阈值检查任务
|
||||
TaskTypeFullCollection: 新增的全量采集任务
|
||||
TaskTypeReleaseFeedWeight: 下料口释放指定重量任务
|
||||
TaskTypeWaiting: 等待任务
|
||||
@@ -1910,11 +2196,17 @@ definitions:
|
||||
- 等待任务
|
||||
- 下料口释放指定重量任务
|
||||
- 新增的全量采集任务
|
||||
- 告警通知任务
|
||||
- 设备阈值检查任务
|
||||
- 区域阈值检查任务
|
||||
x-enum-varnames:
|
||||
- TaskPlanAnalysis
|
||||
- TaskTypeWaiting
|
||||
- TaskTypeReleaseFeedWeight
|
||||
- TaskTypeFullCollection
|
||||
- TaskTypeAlarmNotification
|
||||
- TaskTypeDeviceThresholdCheck
|
||||
- TaskTypeAreaCollectorThresholdCheck
|
||||
models.ValueDescriptor:
|
||||
properties:
|
||||
multiplier:
|
||||
@@ -1926,18 +2218,6 @@ definitions:
|
||||
type:
|
||||
$ref: '#/definitions/models.SensorType'
|
||||
type: object
|
||||
notify.NotifierType:
|
||||
enum:
|
||||
- 邮件
|
||||
- 企业微信
|
||||
- 飞书
|
||||
- 日志
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- NotifierTypeSMTP
|
||||
- NotifierTypeWeChat
|
||||
- NotifierTypeLark
|
||||
- NotifierTypeLog
|
||||
repository.PlanTypeFilter:
|
||||
enum:
|
||||
- 所有任务
|
||||
@@ -1950,7 +2230,6 @@ definitions:
|
||||
- PlanTypeFilterSystem
|
||||
zapcore.Level:
|
||||
enum:
|
||||
- 7
|
||||
- -1
|
||||
- 0
|
||||
- 1
|
||||
@@ -1961,10 +2240,10 @@ definitions:
|
||||
- -1
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
format: int32
|
||||
type: integer
|
||||
x-enum-varnames:
|
||||
- _numLevels
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
@@ -1975,6 +2254,7 @@ definitions:
|
||||
- _minLevel
|
||||
- _maxLevel
|
||||
- InvalidLevel
|
||||
- _numLevels
|
||||
info:
|
||||
contact:
|
||||
email: divano@example.com
|
||||
@@ -1987,6 +2267,428 @@ info:
|
||||
title: 猪场管理系统 API
|
||||
version: "1.0"
|
||||
paths:
|
||||
/api/v1/alarm/threshold/{id}/cancel-snooze:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据告警ID取消对一个阈值告警的忽略状态
|
||||
parameters:
|
||||
- description: 告警ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功取消忽略告警
|
||||
schema:
|
||||
$ref: '#/definitions/controller.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 取消忽略阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
/api/v1/alarm/threshold/{id}/snooze:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据告警ID忽略一个活跃的阈值告警,或更新其忽略时间
|
||||
parameters:
|
||||
- description: 告警ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: 忽略告警请求体
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SnoozeAlarmRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功忽略告警
|
||||
schema:
|
||||
$ref: '#/definitions/controller.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 忽略阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
/api/v1/alarm/threshold/active-alarms:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据过滤条件和分页参数查询活跃告警列表
|
||||
parameters:
|
||||
- description: 告警触发时间范围 - 结束时间
|
||||
in: query
|
||||
name: end_time
|
||||
type: string
|
||||
- description: 按是否被忽略过滤
|
||||
in: query
|
||||
name: is_ignored
|
||||
type: boolean
|
||||
- description: 按告警严重性等级过滤
|
||||
enum:
|
||||
- Debug
|
||||
- Info
|
||||
- Warn
|
||||
- Error
|
||||
- DPanic
|
||||
- Panic
|
||||
- Fatal
|
||||
in: query
|
||||
name: level
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
- ErrorLevel
|
||||
- DPanicLevel
|
||||
- PanicLevel
|
||||
- FatalLevel
|
||||
- description: 排序字段,例如 "trigger_time DESC"
|
||||
in: query
|
||||
name: order_by
|
||||
type: string
|
||||
- in: query
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
- description: 按告警来源ID过滤
|
||||
in: query
|
||||
name: source_id
|
||||
type: integer
|
||||
- description: 按告警来源类型过滤
|
||||
enum:
|
||||
- 普通设备
|
||||
- 区域主控
|
||||
- 系统
|
||||
in: query
|
||||
name: source_type
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- AlarmSourceTypeDevice
|
||||
- AlarmSourceTypeAreaController
|
||||
- AlarmSourceTypeSystem
|
||||
- description: 告警触发时间范围 - 开始时间
|
||||
in: query
|
||||
name: trigger_time
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功获取活跃告警列表
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/controller.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/dto.ListActiveAlarmResponse'
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 批量查询活跃告警
|
||||
tags:
|
||||
- 告警管理
|
||||
/api/v1/alarm/threshold/area:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 为指定的区域主控创建一个新的阈值告警规则
|
||||
parameters:
|
||||
- description: 创建区域阈值告警请求体
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.CreateAreaThresholdAlarmDTO'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功创建区域阈值告警
|
||||
schema:
|
||||
$ref: '#/definitions/controller.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 创建区域阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
/api/v1/alarm/threshold/area/{task_id}:
|
||||
delete:
|
||||
description: 根据任务ID删除区域阈值告警规则
|
||||
parameters:
|
||||
- description: 任务ID
|
||||
in: path
|
||||
name: task_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功删除区域阈值告警
|
||||
schema:
|
||||
$ref: '#/definitions/controller.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 删除区域阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
get:
|
||||
description: 根据任务ID获取单个区域阈值告警规则的详细信息
|
||||
parameters:
|
||||
- description: 任务ID
|
||||
in: path
|
||||
name: task_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功获取区域阈值告警
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/controller.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/dto.AreaThresholdAlarmDTO'
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取区域阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据任务ID更新已存在的区域阈值告警规则
|
||||
parameters:
|
||||
- description: 任务ID
|
||||
in: path
|
||||
name: task_id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 更新区域阈值告警请求体
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.UpdateAreaThresholdAlarmDTO'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功更新区域阈值告警
|
||||
schema:
|
||||
$ref: '#/definitions/controller.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 更新区域阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
/api/v1/alarm/threshold/device:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 为单个设备创建一条新的阈值告警规则
|
||||
parameters:
|
||||
- description: 创建设备阈值告警请求体
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.CreateDeviceThresholdAlarmDTO'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功创建设备阈值告警
|
||||
schema:
|
||||
$ref: '#/definitions/controller.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 创建设备阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
/api/v1/alarm/threshold/device/{task_id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据任务ID删除设备阈值告警规则
|
||||
parameters:
|
||||
- description: 任务ID
|
||||
in: path
|
||||
name: task_id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 删除设备阈值告警请求体
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.DeleteDeviceThresholdAlarmDTO'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功删除设备阈值告警
|
||||
schema:
|
||||
$ref: '#/definitions/controller.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 删除设备阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
get:
|
||||
description: 根据任务ID获取单个设备阈值告警规则的详细信息
|
||||
parameters:
|
||||
- description: 任务ID
|
||||
in: path
|
||||
name: task_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功获取设备阈值告警
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/controller.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/dto.DeviceThresholdAlarmDTO'
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取设备阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据任务ID更新已存在的设备阈值告警规则
|
||||
parameters:
|
||||
- description: 任务ID
|
||||
in: path
|
||||
name: task_id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 更新设备阈值告警请求体
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.UpdateDeviceThresholdAlarmDTO'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功更新设备阈值告警
|
||||
schema:
|
||||
$ref: '#/definitions/controller.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 更新设备阈值告警
|
||||
tags:
|
||||
- 告警管理
|
||||
/api/v1/alarm/threshold/historical-alarms:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据过滤条件和分页参数查询历史告警列表
|
||||
parameters:
|
||||
- description: 按告警严重性等级过滤
|
||||
enum:
|
||||
- Debug
|
||||
- Info
|
||||
- Warn
|
||||
- Error
|
||||
- DPanic
|
||||
- Panic
|
||||
- Fatal
|
||||
in: query
|
||||
name: level
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
- ErrorLevel
|
||||
- DPanicLevel
|
||||
- PanicLevel
|
||||
- FatalLevel
|
||||
- description: 排序字段,例如 "trigger_time DESC"
|
||||
in: query
|
||||
name: order_by
|
||||
type: string
|
||||
- in: query
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
- description: 告警解决时间范围 - 结束时间
|
||||
in: query
|
||||
name: resolve_time_end
|
||||
type: string
|
||||
- description: 告警解决时间范围 - 开始时间
|
||||
in: query
|
||||
name: resolve_time_start
|
||||
type: string
|
||||
- description: 按告警来源ID过滤
|
||||
in: query
|
||||
name: source_id
|
||||
type: integer
|
||||
- description: 按告警来源类型过滤
|
||||
enum:
|
||||
- 普通设备
|
||||
- 区域主控
|
||||
- 系统
|
||||
in: query
|
||||
name: source_type
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- AlarmSourceTypeDevice
|
||||
- AlarmSourceTypeAreaController
|
||||
- AlarmSourceTypeSystem
|
||||
- description: 告警触发时间范围 - 结束时间
|
||||
in: query
|
||||
name: trigger_time_end
|
||||
type: string
|
||||
- description: 告警触发时间范围 - 开始时间
|
||||
in: query
|
||||
name: trigger_time_start
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功获取历史告警列表
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/controller.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/dto.ListHistoricalAlarmResponse'
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 批量查询历史告警
|
||||
tags:
|
||||
- 告警管理
|
||||
/api/v1/area-controllers:
|
||||
get:
|
||||
description: 获取系统中所有区域主控的列表
|
||||
@@ -2546,7 +3248,6 @@ paths:
|
||||
name: end_time
|
||||
type: string
|
||||
- enum:
|
||||
- 7
|
||||
- -1
|
||||
- 0
|
||||
- 1
|
||||
@@ -2557,12 +3258,12 @@ paths:
|
||||
- -1
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
format: int32
|
||||
in: query
|
||||
name: level
|
||||
type: integer
|
||||
x-enum-varnames:
|
||||
- _numLevels
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
@@ -2573,6 +3274,7 @@ paths:
|
||||
- _minLevel
|
||||
- _maxLevel
|
||||
- InvalidLevel
|
||||
- _numLevels
|
||||
- enum:
|
||||
- 邮件
|
||||
- 企业微信
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
_ "git.huangwc.com/pig/pig-farm-controller/docs" // 引入 swag 生成的 docs
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/alarm"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/health"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/management"
|
||||
@@ -53,6 +54,7 @@ type API struct {
|
||||
pigBatchController *management.PigBatchController // 猪群控制器实例
|
||||
monitorController *monitor.Controller // 数据监控控制器实例
|
||||
healthController *health.Controller // 健康检查控制器实例
|
||||
alarmController *alarm.ThresholdAlarmController // 阈值告警控制器
|
||||
listenHandler webhook.ListenHandler // 设备上行事件监听器
|
||||
analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例
|
||||
}
|
||||
@@ -69,6 +71,7 @@ func NewAPI(cfg config.ServerConfig,
|
||||
planService service.PlanService,
|
||||
userService service.UserService,
|
||||
auditService service.AuditService,
|
||||
alarmService service.ThresholdAlarmService,
|
||||
tokenGenerator token.Generator,
|
||||
listenHandler webhook.ListenHandler,
|
||||
) *API {
|
||||
@@ -106,6 +109,8 @@ func NewAPI(cfg config.ServerConfig,
|
||||
monitorController: monitor.NewController(logs.AddCompName(baseCtx, "MonitorController"), monitorService),
|
||||
// 在 NewAPI 中初始化健康检查控制器
|
||||
healthController: health.NewController(logs.AddCompName(baseCtx, "HealthController")),
|
||||
// 在 NewAPI 中初始化阈
|
||||
alarmController: alarm.NewThresholdAlarmController(logs.AddCompName(baseCtx, "ThresholdAlarmController"), alarmService),
|
||||
}
|
||||
|
||||
api.setupRoutes() // 设置所有路由
|
||||
|
||||
@@ -187,6 +187,32 @@ func (a *API) setupRoutes() {
|
||||
monitorGroup.GET("/notifications", a.monitorController.ListNotifications)
|
||||
}
|
||||
logger.Debug("数据监控相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
// 告警相关路由组
|
||||
alarmGroup := authGroup.Group("/alarm")
|
||||
{
|
||||
thresholdGroup := alarmGroup.Group("/thresholds")
|
||||
{
|
||||
thresholdGroup.POST("/:id/snooze", a.alarmController.SnoozeThresholdAlarm) // 忽略阈值告警
|
||||
thresholdGroup.POST("/:id/cancel-snooze", a.alarmController.CancelSnoozeThresholdAlarm) // 取消忽略阈值告警
|
||||
thresholdGroup.GET("/active-alarms", a.alarmController.ListActiveAlarms) // 获取活跃告警
|
||||
thresholdGroup.GET("/historical-alarms", a.alarmController.ListHistoricalAlarms) // 获取历史告警
|
||||
|
||||
// 设备阈值告警配置
|
||||
thresholdGroup.POST("/device", a.alarmController.CreateDeviceThresholdAlarm)
|
||||
thresholdGroup.GET("/device/:task_id", a.alarmController.GetDeviceThresholdAlarm)
|
||||
thresholdGroup.PUT("/device/:task_id", a.alarmController.UpdateDeviceThresholdAlarm)
|
||||
thresholdGroup.DELETE("/device/:task_id", a.alarmController.DeleteDeviceThresholdAlarm)
|
||||
|
||||
// 区域阈值告警配置
|
||||
thresholdGroup.POST("/area", a.alarmController.CreateAreaThresholdAlarm)
|
||||
thresholdGroup.GET("/area/:task_id", a.alarmController.GetAreaThresholdAlarm)
|
||||
thresholdGroup.PUT("/area/:task_id", a.alarmController.UpdateAreaThresholdAlarm)
|
||||
thresholdGroup.DELETE("/area/:task_id", a.alarmController.DeleteAreaThresholdAlarm)
|
||||
|
||||
}
|
||||
}
|
||||
logger.Debug("告警相关接口注册成功 (需要认证和审计)")
|
||||
}
|
||||
|
||||
logger.Debug("所有接口注册成功")
|
||||
|
||||
456
internal/app/controller/alarm/threshold_alarm_controller.go
Normal file
456
internal/app/controller/alarm/threshold_alarm_controller.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package alarm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ThresholdAlarmController 阈值告警控制器,封装了所有与阈值告警配置相关的业务逻辑
|
||||
type ThresholdAlarmController struct {
|
||||
ctx context.Context
|
||||
thresholdAlarmService service.ThresholdAlarmService
|
||||
}
|
||||
|
||||
// NewThresholdAlarmController 创建一个新的阈值告警控制器实例
|
||||
func NewThresholdAlarmController(
|
||||
ctx context.Context,
|
||||
thresholdAlarmService service.ThresholdAlarmService,
|
||||
) *ThresholdAlarmController {
|
||||
return &ThresholdAlarmController{
|
||||
ctx: ctx,
|
||||
thresholdAlarmService: thresholdAlarmService,
|
||||
}
|
||||
}
|
||||
|
||||
// SnoozeThresholdAlarm godoc
|
||||
// @Summary 忽略阈值告警
|
||||
// @Description 根据告警ID忽略一个活跃的阈值告警,或更新其忽略时间
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "告警ID"
|
||||
// @Param request body dto.SnoozeAlarmRequest true "忽略告警请求体"
|
||||
// @Success 200 {object} controller.Response "成功忽略告警"
|
||||
// @Router /api/v1/alarm/threshold/{id}/snooze [post]
|
||||
func (t *ThresholdAlarmController) SnoozeThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "SnoozeThresholdAlarm")
|
||||
|
||||
const actionType = "忽略阈值告警"
|
||||
alarmIDStr := ctx.Param("id")
|
||||
|
||||
alarmID, err := strconv.ParseUint(alarmIDStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的告警ID: %s", actionType, alarmIDStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的告警ID: "+alarmIDStr, actionType, "无效的告警ID", alarmIDStr)
|
||||
}
|
||||
|
||||
var req dto.SnoozeAlarmRequest
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
if err := t.thresholdAlarmService.SnoozeThresholdAlarm(reqCtx, uint32(alarmID), req.DurationMinutes); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 告警不存在, ID: %d", actionType, alarmID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "告警未找到", actionType, "告警不存在", alarmID)
|
||||
}
|
||||
logger.Errorf("%s: 服务层忽略告警失败: %v, ID: %d", actionType, err, alarmID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "忽略告警失败: "+err.Error(), actionType, "服务层忽略告警失败", alarmID)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 告警已成功忽略, ID: %d", actionType, alarmID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "告警已成功忽略", nil, actionType, "告警已成功忽略", alarmID)
|
||||
}
|
||||
|
||||
// CancelSnoozeThresholdAlarm godoc
|
||||
// @Summary 取消忽略阈值告警
|
||||
// @Description 根据告警ID取消对一个阈值告警的忽略状态
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "告警ID"
|
||||
// @Success 200 {object} controller.Response "成功取消忽略告警"
|
||||
// @Router /api/v1/alarm/threshold/{id}/cancel-snooze [post]
|
||||
func (t *ThresholdAlarmController) CancelSnoozeThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "CancelSnoozeThresholdAlarm")
|
||||
|
||||
const actionType = "取消忽略阈值告警"
|
||||
alarmIDStr := ctx.Param("id")
|
||||
|
||||
alarmID, err := strconv.ParseUint(alarmIDStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的告警ID: %s", actionType, alarmIDStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的告警ID: "+alarmIDStr, actionType, "无效的告警ID", alarmIDStr)
|
||||
}
|
||||
|
||||
if err := t.thresholdAlarmService.CancelSnoozeThresholdAlarm(reqCtx, uint32(alarmID)); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 告警不存在, ID: %d", actionType, alarmID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "告警未找到", actionType, "告警不存在", alarmID)
|
||||
}
|
||||
logger.Errorf("%s: 服务层取消忽略告警失败: %v, ID: %d", actionType, err, alarmID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "取消忽略告警失败: "+err.Error(), actionType, "服务层取消忽略告警失败", alarmID)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 告警忽略状态已成功取消, ID: %d", actionType, alarmID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "告警忽略状态已成功取消", nil, actionType, "告警忽略状态已成功取消", alarmID)
|
||||
}
|
||||
|
||||
// ListActiveAlarms godoc
|
||||
// @Summary 批量查询活跃告警
|
||||
// @Description 根据过滤条件和分页参数查询活跃告警列表
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query dto.ListActiveAlarmRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListActiveAlarmResponse} "成功获取活跃告警列表"
|
||||
// @Router /api/v1/alarm/threshold/active-alarms [get]
|
||||
func (t *ThresholdAlarmController) ListActiveAlarms(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "ListActiveAlarms")
|
||||
|
||||
const actionType = "批量查询活跃告警"
|
||||
var req dto.ListActiveAlarmRequest
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求参数: "+err.Error(), actionType, "请求参数绑定失败", nil)
|
||||
}
|
||||
|
||||
resp, err := t.thresholdAlarmService.ListActiveAlarms(reqCtx, &req)
|
||||
if err != nil {
|
||||
// 捕获 ErrInvalidPagination 错误,并返回 Bad Request
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
logger.Errorf("%s: 服务层查询活跃告警失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "查询活跃告警失败: "+err.Error(), actionType, "服务层查询活跃告警失败", req)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "成功获取活跃告警列表", resp, actionType, "成功获取活跃告警列表", req)
|
||||
}
|
||||
|
||||
// ListHistoricalAlarms godoc
|
||||
// @Summary 批量查询历史告警
|
||||
// @Description 根据过滤条件和分页参数查询历史告警列表
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query dto.ListHistoricalAlarmRequest true "查询参数"
|
||||
// @Success 200 {object} controller.Response{data=dto.ListHistoricalAlarmResponse} "成功获取历史告警列表"
|
||||
// @Router /api/v1/alarm/threshold/historical-alarms [get]
|
||||
func (t *ThresholdAlarmController) ListHistoricalAlarms(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "ListHistoricalAlarms")
|
||||
|
||||
const actionType = "批量查询历史告警"
|
||||
var req dto.ListHistoricalAlarmRequest
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求参数: "+err.Error(), actionType, "请求参数绑定失败", nil)
|
||||
}
|
||||
|
||||
resp, err := t.thresholdAlarmService.ListHistoricalAlarms(reqCtx, &req)
|
||||
if err != nil {
|
||||
// 捕获 ErrInvalidPagination 错误,并返回 Bad Request
|
||||
if errors.Is(err, repository.ErrInvalidPagination) {
|
||||
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
|
||||
}
|
||||
logger.Errorf("%s: 服务层查询历史告警失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "查询历史告警失败: "+err.Error(), actionType, "服务层查询历史告警失败", req)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "成功获取历史告警列表", resp, actionType, "成功获取历史告警列表", req)
|
||||
}
|
||||
|
||||
// CreateDeviceThresholdAlarm godoc
|
||||
// @Summary 创建设备阈值告警
|
||||
// @Description 为单个设备创建一条新的阈值告警规则
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CreateDeviceThresholdAlarmDTO true "创建设备阈值告警请求体"
|
||||
// @Success 200 {object} controller.Response "成功创建设备阈值告警"
|
||||
// @Router /api/v1/alarm/threshold/device [post]
|
||||
func (t *ThresholdAlarmController) CreateDeviceThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "CreateDeviceThresholdAlarm")
|
||||
const actionType = "创建设备阈值告警"
|
||||
|
||||
var req dto.CreateDeviceThresholdAlarmDTO
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
if err := t.thresholdAlarmService.CreateDeviceThresholdAlarm(reqCtx, &req); err != nil {
|
||||
logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建失败: "+err.Error(), actionType, "服务层创建失败", req)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, DeviceID: %d, SensorType: %s", actionType, req.DeviceID, req.SensorType)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "创建成功", nil, actionType, "创建成功", req)
|
||||
}
|
||||
|
||||
// GetDeviceThresholdAlarm godoc
|
||||
// @Summary 获取设备阈值告警
|
||||
// @Description 根据任务ID获取单个设备阈值告警规则的详细信息
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param task_id path int true "任务ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.DeviceThresholdAlarmDTO} "成功获取设备阈值告警"
|
||||
// @Router /api/v1/alarm/threshold/device/{task_id} [get]
|
||||
func (t *ThresholdAlarmController) GetDeviceThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "GetDeviceThresholdAlarm")
|
||||
const actionType = "获取设备阈值告警"
|
||||
|
||||
taskID, err := strconv.Atoi(ctx.Param("task_id"))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的任务ID: %s", actionType, ctx.Param("task_id"))
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的任务ID", actionType, "无效的任务ID", ctx.Param("task_id"))
|
||||
}
|
||||
|
||||
resp, err := t.thresholdAlarmService.GetDeviceThresholdAlarm(reqCtx, taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 任务不存在, ID: %d", actionType, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "任务未找到", actionType, "任务不存在", taskID)
|
||||
}
|
||||
logger.Errorf("%s: 服务层获取失败: %v, ID: %d", actionType, err, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取失败: "+err.Error(), actionType, "服务层获取失败", taskID)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, ID: %d", actionType, taskID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, actionType, "获取成功", taskID)
|
||||
}
|
||||
|
||||
// UpdateDeviceThresholdAlarm godoc
|
||||
// @Summary 更新设备阈值告警
|
||||
// @Description 根据任务ID更新已存在的设备阈值告警规则
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param task_id path int true "任务ID"
|
||||
// @Param request body dto.UpdateDeviceThresholdAlarmDTO true "更新设备阈值告警请求体"
|
||||
// @Success 200 {object} controller.Response "成功更新设备阈值告警"
|
||||
// @Router /api/v1/alarm/threshold/device/{task_id} [put]
|
||||
func (t *ThresholdAlarmController) UpdateDeviceThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "UpdateDeviceThresholdAlarm")
|
||||
const actionType = "更新设备阈值告警"
|
||||
|
||||
taskID, err := strconv.Atoi(ctx.Param("task_id"))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的任务ID: %s", actionType, ctx.Param("task_id"))
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的任务ID", actionType, "无效的任务ID", ctx.Param("task_id"))
|
||||
}
|
||||
|
||||
var req dto.UpdateDeviceThresholdAlarmDTO
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
if err := t.thresholdAlarmService.UpdateDeviceThresholdAlarm(reqCtx, taskID, &req); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 任务不存在, ID: %d", actionType, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "任务未找到", actionType, "任务不存在", taskID)
|
||||
}
|
||||
logger.Errorf("%s: 服务层更新失败: %v, ID: %d", actionType, err, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败: "+err.Error(), actionType, "服务层更新失败", taskID)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, ID: %d", actionType, taskID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", nil, actionType, "更新成功", taskID)
|
||||
}
|
||||
|
||||
// DeleteDeviceThresholdAlarm godoc
|
||||
// @Summary 删除设备阈值告警
|
||||
// @Description 根据任务ID删除设备阈值告警规则
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param task_id path int true "任务ID"
|
||||
// @Param request body dto.DeleteDeviceThresholdAlarmDTO true "删除设备阈值告警请求体"
|
||||
// @Success 200 {object} controller.Response "成功删除设备阈值告警"
|
||||
// @Router /api/v1/alarm/threshold/device/{task_id} [delete]
|
||||
func (t *ThresholdAlarmController) DeleteDeviceThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "DeleteDeviceThresholdAlarm")
|
||||
const actionType = "删除设备阈值告警"
|
||||
|
||||
taskID, err := strconv.Atoi(ctx.Param("task_id"))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的任务ID: %s", actionType, ctx.Param("task_id"))
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的任务ID", actionType, "无效的任务ID", ctx.Param("task_id"))
|
||||
}
|
||||
|
||||
var req dto.DeleteDeviceThresholdAlarmDTO
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
if err := t.thresholdAlarmService.DeleteDeviceThresholdAlarm(reqCtx, taskID, &req); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 任务不存在, ID: %d", actionType, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "任务未找到", actionType, "任务不存在", taskID)
|
||||
}
|
||||
logger.Errorf("%s: 服务层删除失败: %v, ID: %d", actionType, err, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败: "+err.Error(), actionType, "服务层删除失败", taskID)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, ID: %d", actionType, taskID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, actionType, "删除成功", taskID)
|
||||
}
|
||||
|
||||
// CreateAreaThresholdAlarm godoc
|
||||
// @Summary 创建区域阈值告警
|
||||
// @Description 为指定的区域主控创建一个新的阈值告警规则
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CreateAreaThresholdAlarmDTO true "创建区域阈值告警请求体"
|
||||
// @Success 200 {object} controller.Response "成功创建区域阈值告警"
|
||||
// @Router /api/v1/alarm/threshold/area [post]
|
||||
func (t *ThresholdAlarmController) CreateAreaThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "CreateAreaThresholdAlarm")
|
||||
const actionType = "创建区域阈值告警"
|
||||
|
||||
var req dto.CreateAreaThresholdAlarmDTO
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
if err := t.thresholdAlarmService.CreateAreaThresholdAlarm(reqCtx, &req); err != nil {
|
||||
logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建失败: "+err.Error(), actionType, "服务层创建失败", req)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, AreaControllerID: %d, SensorType: %s", actionType, req.AreaControllerID, req.SensorType)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "创建成功", nil, actionType, "创建成功", req)
|
||||
}
|
||||
|
||||
// GetAreaThresholdAlarm godoc
|
||||
// @Summary 获取区域阈值告警
|
||||
// @Description 根据任务ID获取单个区域阈值告警规则的详细信息
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param task_id path int true "任务ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.AreaThresholdAlarmDTO} "成功获取区域阈值告警"
|
||||
// @Router /api/v1/alarm/threshold/area/{task_id} [get]
|
||||
func (t *ThresholdAlarmController) GetAreaThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "GetAreaThresholdAlarm")
|
||||
const actionType = "获取区域阈值告警"
|
||||
|
||||
taskID, err := strconv.Atoi(ctx.Param("task_id"))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的任务ID: %s", actionType, ctx.Param("task_id"))
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的任务ID", actionType, "无效的任务ID", ctx.Param("task_id"))
|
||||
}
|
||||
|
||||
resp, err := t.thresholdAlarmService.GetAreaThresholdAlarm(reqCtx, taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 任务不存在, ID: %d", actionType, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "任务未找到", actionType, "任务不存在", taskID)
|
||||
}
|
||||
logger.Errorf("%s: 服务层获取失败: %v, ID: %d", actionType, err, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取失败: "+err.Error(), actionType, "服务层获取失败", taskID)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, ID: %d", actionType, taskID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, actionType, "获取成功", taskID)
|
||||
}
|
||||
|
||||
// UpdateAreaThresholdAlarm godoc
|
||||
// @Summary 更新区域阈值告警
|
||||
// @Description 根据任务ID更新已存在的区域阈值告警规则
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param task_id path int true "任务ID"
|
||||
// @Param request body dto.UpdateAreaThresholdAlarmDTO true "更新区域阈值告警请求体"
|
||||
// @Success 200 {object} controller.Response "成功更新区域阈值告警"
|
||||
// @Router /api/v1/alarm/threshold/area/{task_id} [put]
|
||||
func (t *ThresholdAlarmController) UpdateAreaThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "UpdateAreaThresholdAlarm")
|
||||
const actionType = "更新区域阈值告警"
|
||||
|
||||
taskID, err := strconv.Atoi(ctx.Param("task_id"))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的任务ID: %s", actionType, ctx.Param("task_id"))
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的任务ID", actionType, "无效的任务ID", ctx.Param("task_id"))
|
||||
}
|
||||
|
||||
var req dto.UpdateAreaThresholdAlarmDTO
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
if err := t.thresholdAlarmService.UpdateAreaThresholdAlarm(reqCtx, taskID, &req); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 任务不存在, ID: %d", actionType, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "任务未找到", actionType, "任务不存在", taskID)
|
||||
}
|
||||
logger.Errorf("%s: 服务层更新失败: %v, ID: %d", actionType, err, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败: "+err.Error(), actionType, "服务层更新失败", taskID)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, ID: %d", actionType, taskID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", nil, actionType, "更新成功", taskID)
|
||||
}
|
||||
|
||||
// DeleteAreaThresholdAlarm godoc
|
||||
// @Summary 删除区域阈值告警
|
||||
// @Description 根据任务ID删除区域阈值告警规则
|
||||
// @Tags 告警管理
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param task_id path int true "任务ID"
|
||||
// @Success 200 {object} controller.Response "成功删除区域阈值告警"
|
||||
// @Router /api/v1/alarm/threshold/area/{task_id} [delete]
|
||||
func (t *ThresholdAlarmController) DeleteAreaThresholdAlarm(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), t.ctx, "DeleteAreaThresholdAlarm")
|
||||
const actionType = "删除区域阈值告警"
|
||||
|
||||
taskID, err := strconv.Atoi(ctx.Param("task_id"))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的任务ID: %s", actionType, ctx.Param("task_id"))
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的任务ID", actionType, "无效的任务ID", ctx.Param("task_id"))
|
||||
}
|
||||
|
||||
if err := t.thresholdAlarmService.DeleteAreaThresholdAlarm(reqCtx, taskID); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 任务不存在, ID: %d", actionType, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "任务未找到", actionType, "任务不存在", taskID)
|
||||
}
|
||||
logger.Errorf("%s: 服务层删除失败: %v, ID: %d", actionType, err, taskID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败: "+err.Error(), actionType, "服务层删除失败", taskID)
|
||||
}
|
||||
|
||||
logger.Infof("%s: 成功, ID: %d", actionType, taskID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, actionType, "删除成功", taskID)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ var (
|
||||
|
||||
// GetOperatorIDFromContext 从 echo.Context 中提取操作者ID。
|
||||
// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 ID 字段。
|
||||
func GetOperatorIDFromContext(c echo.Context) (uint, error) {
|
||||
func GetOperatorIDFromContext(c echo.Context) (uint32, error) {
|
||||
userVal := c.Get(models.ContextUserKey.String())
|
||||
if userVal == nil {
|
||||
return 0, ErrUserNotFoundInContext
|
||||
|
||||
@@ -84,7 +84,7 @@ func (c *Controller) GetDevice(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
|
||||
}
|
||||
|
||||
resp, err := c.deviceService.GetDevice(reqCtx, uint(id))
|
||||
resp, err := c.deviceService.GetDevice(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||
@@ -149,7 +149,7 @@ func (c *Controller) UpdateDevice(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
|
||||
}
|
||||
|
||||
resp, err := c.deviceService.UpdateDevice(reqCtx, uint(id), &req)
|
||||
resp, err := c.deviceService.UpdateDevice(reqCtx, uint32(id), &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||
@@ -184,7 +184,7 @@ func (c *Controller) DeleteDevice(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
|
||||
}
|
||||
|
||||
if err := c.deviceService.DeleteDevice(reqCtx, uint(id)); err != nil {
|
||||
if err := c.deviceService.DeleteDevice(reqCtx, uint32(id)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||
@@ -232,7 +232,7 @@ func (c *Controller) ManualControl(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
|
||||
}
|
||||
|
||||
if err := c.deviceService.ManualControl(reqCtx, uint(id), &req); err != nil {
|
||||
if err := c.deviceService.ManualControl(reqCtx, uint32(id), &req); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
|
||||
@@ -297,7 +297,7 @@ func (c *Controller) GetAreaController(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
|
||||
}
|
||||
|
||||
resp, err := c.deviceService.GetAreaController(reqCtx, uint(id))
|
||||
resp, err := c.deviceService.GetAreaController(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||
@@ -361,7 +361,7 @@ func (c *Controller) UpdateAreaController(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
|
||||
}
|
||||
|
||||
resp, err := c.deviceService.UpdateAreaController(reqCtx, uint(id), &req)
|
||||
resp, err := c.deviceService.UpdateAreaController(reqCtx, uint32(id), &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||
@@ -396,7 +396,7 @@ func (c *Controller) DeleteAreaController(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
|
||||
}
|
||||
|
||||
if err := c.deviceService.DeleteAreaController(reqCtx, uint(id)); err != nil {
|
||||
if err := c.deviceService.DeleteAreaController(reqCtx, uint32(id)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
|
||||
@@ -467,7 +467,7 @@ func (c *Controller) GetDeviceTemplate(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
|
||||
}
|
||||
|
||||
resp, err := c.deviceService.GetDeviceTemplate(reqCtx, uint(id))
|
||||
resp, err := c.deviceService.GetDeviceTemplate(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||
@@ -532,7 +532,7 @@ func (c *Controller) UpdateDeviceTemplate(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
|
||||
}
|
||||
|
||||
resp, err := c.deviceService.UpdateDeviceTemplate(reqCtx, uint(id), &req)
|
||||
resp, err := c.deviceService.UpdateDeviceTemplate(reqCtx, uint32(id), &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||
@@ -567,7 +567,7 @@ func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
|
||||
}
|
||||
|
||||
if err := c.deviceService.DeleteDeviceTemplate(reqCtx, uint(id)); err != nil {
|
||||
if err := c.deviceService.DeleteDeviceTemplate(reqCtx, uint32(id)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
// mapAndSendError 统一映射服务层错误并发送响应。
|
||||
// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。
|
||||
func mapAndSendError(reqContext context.Context, c *PigBatchController, ctx echo.Context, action string, err error, id uint) error {
|
||||
func mapAndSendError(reqContext context.Context, c *PigBatchController, ctx echo.Context, action string, err error, id uint32) error {
|
||||
if errors.Is(err, service.ErrPigBatchNotFound) ||
|
||||
errors.Is(err, service.ErrPenNotFound) ||
|
||||
errors.Is(err, service.ErrPenNotAssociatedWithBatch) {
|
||||
@@ -34,7 +34,7 @@ func mapAndSendError(reqContext context.Context, c *PigBatchController, ctx echo
|
||||
}
|
||||
|
||||
// idExtractorFunc 定义了一个函数类型,用于从echo.Context中提取主ID。
|
||||
type idExtractorFunc func(ctx echo.Context) (uint, error)
|
||||
type idExtractorFunc func(ctx echo.Context) (uint32, error)
|
||||
|
||||
// extractOperatorAndPrimaryID 封装了从echo.Context中提取操作员ID和主ID的通用逻辑。
|
||||
// 它负责处理ID提取过程中的错误,并发送相应的HTTP响应。
|
||||
@@ -48,15 +48,15 @@ type idExtractorFunc func(ctx echo.Context) (uint, error)
|
||||
//
|
||||
// 返回值:
|
||||
//
|
||||
// operatorID: uint - 提取到的操作员ID。
|
||||
// primaryID: uint - 提取到的主ID。
|
||||
// operatorID: uint32 - 提取到的操作员ID。
|
||||
// primaryID: uint32 - 提取到的主ID。
|
||||
// err: error - 如果ID提取失败或发送错误响应,则返回错误。
|
||||
func extractOperatorAndPrimaryID(
|
||||
c *PigBatchController,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
idExtractor idExtractorFunc,
|
||||
) (operatorID uint, primaryID uint, err error) {
|
||||
) (operatorID uint32, primaryID uint32, err error) {
|
||||
// 1. 获取操作员ID
|
||||
operatorID, err = controller.GetOperatorIDFromContext(ctx)
|
||||
if err != nil {
|
||||
@@ -78,7 +78,7 @@ func extractOperatorAndPrimaryID(
|
||||
if err != nil {
|
||||
return 0, 0, controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam)
|
||||
}
|
||||
primaryID = uint(parsedID)
|
||||
primaryID = uint32(parsedID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func handleAPIRequest[Req any](
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
reqDTO Req,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint, req Req) error,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint32, primaryID uint32, req Req) error,
|
||||
successMsg string,
|
||||
idExtractor idExtractorFunc,
|
||||
) error {
|
||||
@@ -124,7 +124,7 @@ func handleNoBodyAPIRequest(
|
||||
c *PigBatchController,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint) error,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint32, primaryID uint32) error,
|
||||
successMsg string,
|
||||
idExtractor idExtractorFunc,
|
||||
) error {
|
||||
@@ -151,7 +151,7 @@ func handleAPIRequestWithResponse[Req any, Resp any](
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
reqDTO Req,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint, req Req) (Resp, error), // serviceExecutor现在返回Resp
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint32, primaryID uint32, req Req) (Resp, error), // serviceExecutor现在返回Resp
|
||||
successMsg string,
|
||||
idExtractor idExtractorFunc,
|
||||
) error {
|
||||
@@ -182,7 +182,7 @@ func handleNoBodyAPIRequestWithResponse[Resp any](
|
||||
c *PigBatchController,
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint) (Resp, error), // serviceExecutor现在返回Resp
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint32, primaryID uint32) (Resp, error), // serviceExecutor现在返回Resp
|
||||
successMsg string,
|
||||
idExtractor idExtractorFunc,
|
||||
) error {
|
||||
@@ -209,7 +209,7 @@ func handleQueryAPIRequestWithResponse[Query any, Resp any](
|
||||
ctx echo.Context,
|
||||
action string,
|
||||
queryDTO Query,
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint, query Query) (Resp, error), // serviceExecutor现在接收queryDTO
|
||||
serviceExecutor func(ctx echo.Context, operatorID uint32, query Query) (Resp, error), // serviceExecutor现在接收queryDTO
|
||||
successMsg string,
|
||||
) error {
|
||||
// 1. 绑定查询参数
|
||||
|
||||
@@ -43,7 +43,7 @@ func (c *PigBatchController) CreatePigBatch(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequestWithResponse(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
// 对于创建操作,primaryID通常不从路径中获取,而是由服务层生成
|
||||
return c.service.CreatePigBatch(reqCtx, operatorID, req)
|
||||
},
|
||||
@@ -68,7 +68,7 @@ func (c *PigBatchController) GetPigBatch(ctx echo.Context) error {
|
||||
|
||||
return handleNoBodyAPIRequestWithResponse(
|
||||
reqCtx, c, ctx, action,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32) (*dto.PigBatchResponseDTO, error) {
|
||||
return c.service.GetPigBatch(reqCtx, primaryID)
|
||||
},
|
||||
"获取成功",
|
||||
@@ -95,7 +95,7 @@ func (c *PigBatchController) UpdatePigBatch(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequestWithResponse(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
return c.service.UpdatePigBatch(reqCtx, primaryID, req)
|
||||
},
|
||||
"更新成功",
|
||||
@@ -119,7 +119,7 @@ func (c *PigBatchController) DeletePigBatch(ctx echo.Context) error {
|
||||
|
||||
return handleNoBodyAPIRequest(
|
||||
reqCtx, c, ctx, action,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32) error {
|
||||
return c.service.DeletePigBatch(reqCtx, primaryID)
|
||||
},
|
||||
"删除成功",
|
||||
@@ -144,7 +144,7 @@ func (c *PigBatchController) ListPigBatches(ctx echo.Context) error {
|
||||
|
||||
return handleQueryAPIRequestWithResponse(
|
||||
reqCtx, c, ctx, action, &query,
|
||||
func(ctx echo.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
|
||||
func(ctx echo.Context, operatorID uint32, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
|
||||
return c.service.ListPigBatches(reqCtx, query.IsActive)
|
||||
},
|
||||
"获取成功",
|
||||
@@ -170,7 +170,7 @@ func (c *PigBatchController) AssignEmptyPensToBatch(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.AssignEmptyPensToBatchRequest) error {
|
||||
return c.service.AssignEmptyPensToBatch(reqCtx, primaryID, req.PenIDs, operatorID)
|
||||
},
|
||||
"分配成功",
|
||||
@@ -197,18 +197,18 @@ func (c *PigBatchController) ReclassifyPenToNewBatch(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.ReclassifyPenToNewBatchRequest) error {
|
||||
// primaryID 在这里是 fromBatchID
|
||||
return c.service.ReclassifyPenToNewBatch(reqCtx, primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks)
|
||||
},
|
||||
"划拨成功",
|
||||
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":fromBatchID" 路径参数提取
|
||||
func(ctx echo.Context) (uint32, error) { // 自定义ID提取器,从 ":fromBatchID" 路径参数提取
|
||||
idParam := ctx.Param("fromBatchID")
|
||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(parsedID), nil
|
||||
return uint32(parsedID), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -230,23 +230,23 @@ func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx echo.Context) error {
|
||||
|
||||
return handleNoBodyAPIRequest(
|
||||
reqCtx, c, ctx, action,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32) error {
|
||||
// primaryID 在这里是 batchID
|
||||
penIDParam := ctx.Param("penID")
|
||||
parsedPenID, err := strconv.ParseUint(penIDParam, 10, 32)
|
||||
if err != nil {
|
||||
return err // 返回错误,因为 penID 格式无效
|
||||
}
|
||||
return c.service.RemoveEmptyPenFromBatch(reqCtx, primaryID, uint(parsedPenID))
|
||||
return c.service.RemoveEmptyPenFromBatch(reqCtx, primaryID, uint32(parsedPenID))
|
||||
},
|
||||
"移除成功",
|
||||
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":batchID" 路径参数提取
|
||||
func(ctx echo.Context) (uint32, error) { // 自定义ID提取器,从 ":batchID" 路径参数提取
|
||||
idParam := ctx.Param("batchID")
|
||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(parsedID), nil
|
||||
return uint32(parsedID), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -270,7 +270,7 @@ func (c *PigBatchController) MovePigsIntoPen(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.MovePigsIntoPenRequest) error {
|
||||
return c.service.MovePigsIntoPen(reqCtx, primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||
},
|
||||
"移入成功",
|
||||
|
||||
@@ -26,7 +26,7 @@ func (c *PigBatchController) RecordSickPigs(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordSickPigsRequest) error {
|
||||
return c.service.RecordSickPigs(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -53,7 +53,7 @@ func (c *PigBatchController) RecordSickPigRecovery(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordSickPigRecoveryRequest) error {
|
||||
return c.service.RecordSickPigRecovery(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -80,7 +80,7 @@ func (c *PigBatchController) RecordSickPigDeath(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordSickPigDeathRequest) error {
|
||||
return c.service.RecordSickPigDeath(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -107,7 +107,7 @@ func (c *PigBatchController) RecordSickPigCull(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordSickPigCullRequest) error {
|
||||
return c.service.RecordSickPigCull(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -134,7 +134,7 @@ func (c *PigBatchController) RecordDeath(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordDeathRequest) error {
|
||||
return c.service.RecordDeath(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
@@ -161,7 +161,7 @@ func (c *PigBatchController) RecordCull(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordCullRequest) error {
|
||||
return c.service.RecordCull(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
|
||||
},
|
||||
"记录成功",
|
||||
|
||||
@@ -25,7 +25,7 @@ func (c *PigBatchController) SellPigs(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.SellPigsRequest) error {
|
||||
return c.service.SellPigs(reqCtx, primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
|
||||
},
|
||||
"卖猪成功",
|
||||
@@ -51,7 +51,7 @@ func (c *PigBatchController) BuyPigs(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.BuyPigsRequest) error {
|
||||
return c.service.BuyPigs(reqCtx, primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
|
||||
},
|
||||
"买猪成功",
|
||||
|
||||
@@ -28,18 +28,18 @@ func (c *PigBatchController) TransferPigsAcrossBatches(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.TransferPigsAcrossBatchesRequest) error {
|
||||
// primaryID 在这里是 sourceBatchID
|
||||
return c.service.TransferPigsAcrossBatches(reqCtx, primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||
},
|
||||
"调栏成功",
|
||||
func(ctx echo.Context) (uint, error) { // 自定义ID提取器,从 ":sourceBatchID" 路径参数提取
|
||||
func(ctx echo.Context) (uint32, error) { // 自定义ID提取器,从 ":sourceBatchID" 路径参数提取
|
||||
idParam := ctx.Param("sourceBatchID")
|
||||
parsedID, err := strconv.ParseUint(idParam, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(parsedID), nil
|
||||
return uint32(parsedID), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func (c *PigBatchController) TransferPigsWithinBatch(ctx echo.Context) error {
|
||||
|
||||
return handleAPIRequest(
|
||||
reqCtx, c, ctx, action, &req,
|
||||
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
|
||||
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.TransferPigsWithinBatchRequest) error {
|
||||
// primaryID 在这里是 batchID
|
||||
return c.service.TransferPigsWithinBatch(reqCtx, primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
|
||||
},
|
||||
|
||||
@@ -76,7 +76,7 @@ func (c *PigFarmController) GetPigHouse(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
house, err := c.service.GetPigHouseByID(reqCtx, uint(id))
|
||||
house, err := c.service.GetPigHouseByID(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrHouseNotFound) {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
@@ -132,7 +132,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
house, err := c.service.UpdatePigHouse(reqCtx, uint(id), req.Name, req.Description)
|
||||
house, err := c.service.UpdatePigHouse(reqCtx, uint32(id), req.Name, req.Description)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrHouseNotFound) {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
@@ -161,7 +161,7 @@ func (c *PigFarmController) DeletePigHouse(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
if err := c.service.DeletePigHouse(reqCtx, uint(id)); err != nil {
|
||||
if err := c.service.DeletePigHouse(reqCtx, uint32(id)); err != nil {
|
||||
if errors.Is(err, service.ErrHouseNotFound) {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func (c *PigFarmController) GetPen(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
pen, err := c.service.GetPenByID(reqCtx, uint(id))
|
||||
pen, err := c.service.GetPenByID(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrPenNotFound) {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
@@ -282,7 +282,7 @@ func (c *PigFarmController) UpdatePen(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
pen, err := c.service.UpdatePen(reqCtx, uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
|
||||
pen, err := c.service.UpdatePen(reqCtx, uint32(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrPenNotFound) {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
@@ -312,7 +312,7 @@ func (c *PigFarmController) DeletePen(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
|
||||
}
|
||||
|
||||
if err := c.service.DeletePen(reqCtx, uint(id)); err != nil {
|
||||
if err := c.service.DeletePen(reqCtx, uint32(id)); err != nil {
|
||||
if errors.Is(err, service.ErrPenNotFound) {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
|
||||
}
|
||||
@@ -351,7 +351,7 @@ func (c *PigFarmController) UpdatePenStatus(ctx echo.Context) error {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
|
||||
}
|
||||
|
||||
pen, err := c.service.UpdatePenStatus(reqCtx, uint(id), req.Status)
|
||||
pen, err := c.service.UpdatePenStatus(reqCtx, uint32(id), req.Status)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrPenNotFound) {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
|
||||
|
||||
@@ -81,14 +81,14 @@ func (c *Controller) GetPlan(ctx echo.Context) error {
|
||||
const actionType = "获取计划详情"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 调用服务层获取计划详情
|
||||
resp, err := c.planService.GetPlanByID(reqCtx, uint(id))
|
||||
resp, err := c.planService.GetPlanByID(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
@@ -147,7 +147,7 @@ func (c *Controller) UpdatePlan(ctx echo.Context) error {
|
||||
const actionType = "更新计划"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
@@ -161,7 +161,7 @@ func (c *Controller) UpdatePlan(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
// 调用服务层更新计划
|
||||
resp, err := c.planService.UpdatePlan(reqCtx, uint(id), &req)
|
||||
resp, err := c.planService.UpdatePlan(reqCtx, uint32(id), &req)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
@@ -191,14 +191,14 @@ func (c *Controller) DeletePlan(ctx echo.Context) error {
|
||||
const actionType = "删除计划"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 调用服务层删除计划
|
||||
err = c.planService.DeletePlan(reqCtx, uint(id))
|
||||
err = c.planService.DeletePlan(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
@@ -228,14 +228,14 @@ func (c *Controller) StartPlan(ctx echo.Context) error {
|
||||
const actionType = "启动计划"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 调用服务层启动计划
|
||||
err = c.planService.StartPlan(reqCtx, uint(id))
|
||||
err = c.planService.StartPlan(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
@@ -267,14 +267,14 @@ func (c *Controller) StopPlan(ctx echo.Context) error {
|
||||
const actionType = "停止计划"
|
||||
// 1. 从 URL 路径中获取 ID
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
|
||||
}
|
||||
|
||||
// 调用服务层停止计划
|
||||
err = c.planService.StopPlan(reqCtx, uint(id))
|
||||
err = c.planService.StopPlan(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id)
|
||||
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
|
||||
|
||||
@@ -101,7 +101,7 @@ func (c *Controller) SendTestNotification(ctx echo.Context) error {
|
||||
const actionType = "发送测试通知"
|
||||
|
||||
// 1. 从 URL 中获取用户 ID
|
||||
userID, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
userID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", ctx.Param("id"))
|
||||
@@ -115,7 +115,7 @@ func (c *Controller) SendTestNotification(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
// 3. 调用服务层
|
||||
err = c.userService.SendTestNotification(reqCtx, uint(userID), &req)
|
||||
err = c.userService.SendTestNotification(reqCtx, uint32(userID), &req)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 服务层调用失败: %v", actionType, err)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", map[string]interface{}{"userID": userID, "type": req.Type})
|
||||
|
||||
65
internal/app/dto/alarm_converter.go
Normal file
65
internal/app/dto/alarm_converter.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
// NewListActiveAlarmResponse 从模型数据创建活跃告警列表响应 DTO
|
||||
func NewListActiveAlarmResponse(data []models.ActiveAlarm, total int64, page, pageSize int) *ListActiveAlarmResponse {
|
||||
dtos := make([]ActiveAlarmDTO, len(data))
|
||||
for i, item := range data {
|
||||
dtos[i] = ActiveAlarmDTO{
|
||||
ID: item.ID,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
SourceType: item.SourceType,
|
||||
SourceID: item.SourceID,
|
||||
AlarmCode: item.AlarmCode,
|
||||
AlarmSummary: item.AlarmSummary,
|
||||
Level: item.Level,
|
||||
AlarmDetails: item.AlarmDetails,
|
||||
TriggerTime: item.TriggerTime,
|
||||
IsIgnored: item.IsIgnored,
|
||||
IgnoredUntil: item.IgnoredUntil,
|
||||
LastNotifiedAt: item.LastNotifiedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return &ListActiveAlarmResponse{
|
||||
List: dtos,
|
||||
Pagination: PaginationDTO{
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewListHistoricalAlarmResponse 从模型数据创建历史告警列表响应 DTO
|
||||
func NewListHistoricalAlarmResponse(data []models.HistoricalAlarm, total int64, page, pageSize int) *ListHistoricalAlarmResponse {
|
||||
dtos := make([]HistoricalAlarmDTO, len(data))
|
||||
for i, item := range data {
|
||||
dtos[i] = HistoricalAlarmDTO{
|
||||
ID: item.ID,
|
||||
SourceType: item.SourceType,
|
||||
SourceID: item.SourceID,
|
||||
AlarmCode: item.AlarmCode,
|
||||
AlarmSummary: item.AlarmSummary,
|
||||
Level: item.Level,
|
||||
AlarmDetails: item.AlarmDetails,
|
||||
TriggerTime: item.TriggerTime,
|
||||
ResolveTime: item.ResolveTime,
|
||||
ResolveMethod: item.ResolveMethod,
|
||||
ResolvedBy: item.ResolvedBy,
|
||||
}
|
||||
}
|
||||
|
||||
return &ListHistoricalAlarmResponse{
|
||||
List: dtos,
|
||||
Pagination: PaginationDTO{
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
140
internal/app/dto/alarm_dto.go
Normal file
140
internal/app/dto/alarm_dto.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
// SnoozeAlarmRequest 定义了忽略告警的请求体
|
||||
type SnoozeAlarmRequest struct {
|
||||
DurationMinutes uint32 `json:"duration_minutes" validate:"required,min=1"` // 忽略时长,单位分钟
|
||||
}
|
||||
|
||||
// ListActiveAlarmRequest 定义了获取活跃告警列表的请求参数
|
||||
type ListActiveAlarmRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
SourceType *models.AlarmSourceType `json:"source_type" query:"source_type"` // 按告警来源类型过滤
|
||||
SourceID *uint32 `json:"source_id" query:"source_id"` // 按告警来源ID过滤
|
||||
Level *models.SeverityLevel `json:"level" query:"level"` // 按告警严重性等级过滤
|
||||
IsIgnored *bool `json:"is_ignored" query:"is_ignored"` // 按是否被忽略过滤
|
||||
TriggerTime *time.Time `json:"trigger_time" query:"trigger_time"` // 告警触发时间范围 - 开始时间
|
||||
EndTime *time.Time `json:"end_time" query:"end_time"` // 告警触发时间范围 - 结束时间
|
||||
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "trigger_time DESC"
|
||||
}
|
||||
|
||||
// ActiveAlarmDTO 是用于API响应的活跃告警结构
|
||||
type ActiveAlarmDTO struct {
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
SourceType models.AlarmSourceType `json:"source_type"`
|
||||
SourceID uint32 `json:"source_id"`
|
||||
AlarmCode models.AlarmCode `json:"alarm_code"`
|
||||
AlarmSummary string `json:"alarm_summary"`
|
||||
Level models.SeverityLevel `json:"level"`
|
||||
AlarmDetails string `json:"alarm_details"`
|
||||
TriggerTime time.Time `json:"trigger_time"`
|
||||
IsIgnored bool `json:"is_ignored"`
|
||||
IgnoredUntil *time.Time `json:"ignored_until"`
|
||||
LastNotifiedAt *time.Time `json:"last_notified_at"`
|
||||
}
|
||||
|
||||
// ListActiveAlarmResponse 是获取活跃告警列表的响应结构
|
||||
type ListActiveAlarmResponse struct {
|
||||
List []ActiveAlarmDTO `json:"list"`
|
||||
Pagination PaginationDTO `json:"pagination"`
|
||||
}
|
||||
|
||||
// ListHistoricalAlarmRequest 定义了获取历史告警列表的请求参数
|
||||
type ListHistoricalAlarmRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
SourceType *models.AlarmSourceType `json:"source_type" query:"source_type"` // 按告警来源类型过滤
|
||||
SourceID *uint32 `json:"source_id" query:"source_id"` // 按告警来源ID过滤
|
||||
Level *models.SeverityLevel `json:"level" query:"level"` // 按告警严重性等级过滤
|
||||
TriggerTimeStart *time.Time `json:"trigger_time_start" query:"trigger_time_start"` // 告警触发时间范围 - 开始时间
|
||||
TriggerTimeEnd *time.Time `json:"trigger_time_end" query:"trigger_time_end"` // 告警触发时间范围 - 结束时间
|
||||
ResolveTimeStart *time.Time `json:"resolve_time_start" query:"resolve_time_start"` // 告警解决时间范围 - 开始时间
|
||||
ResolveTimeEnd *time.Time `json:"resolve_time_end" query:"resolve_time_end"` // 告警解决时间范围 - 结束时间
|
||||
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "trigger_time DESC"
|
||||
}
|
||||
|
||||
// HistoricalAlarmDTO 是用于API响应的历史告警结构
|
||||
type HistoricalAlarmDTO struct {
|
||||
ID uint32 `json:"id"`
|
||||
SourceType models.AlarmSourceType `json:"source_type"`
|
||||
SourceID uint32 `json:"source_id"`
|
||||
AlarmCode models.AlarmCode `json:"alarm_code"`
|
||||
AlarmSummary string `json:"alarm_summary"`
|
||||
Level models.SeverityLevel `json:"level"`
|
||||
AlarmDetails string `json:"alarm_details"`
|
||||
TriggerTime time.Time `json:"trigger_time"`
|
||||
ResolveTime time.Time `json:"resolve_time"`
|
||||
ResolveMethod string `json:"resolve_method"`
|
||||
ResolvedBy *uint32 `json:"resolved_by"`
|
||||
}
|
||||
|
||||
// ListHistoricalAlarmResponse 是获取历史告警列表的响应结构
|
||||
type ListHistoricalAlarmResponse struct {
|
||||
List []HistoricalAlarmDTO `json:"list"`
|
||||
Pagination PaginationDTO `json:"pagination"`
|
||||
}
|
||||
|
||||
// CreateDeviceThresholdAlarmDTO 创建设备阈值告警的请求DTO
|
||||
type CreateDeviceThresholdAlarmDTO struct {
|
||||
DeviceID uint32 `json:"device_id" binding:"required"` // 设备ID
|
||||
SensorType models.SensorType `json:"sensor_type" binding:"required"` // 传感器类型
|
||||
Thresholds float32 `json:"thresholds" binding:"required"` // 阈值
|
||||
Operator models.Operator `json:"operator" binding:"required"` // 操作符 (使用string类型,与前端交互更通用)
|
||||
Level models.SeverityLevel `json:"level,omitempty"` // 告警等级,可选,如果未提供则使用默认值
|
||||
}
|
||||
|
||||
// UpdateDeviceThresholdAlarmDTO 更新设备阈值告警的请求DTO
|
||||
type UpdateDeviceThresholdAlarmDTO struct {
|
||||
Thresholds float32 `json:"thresholds" binding:"required"` // 新的阈值
|
||||
Operator models.Operator `json:"operator" binding:"required"` // 新的操作符
|
||||
Level models.SeverityLevel `json:"level,omitempty"` // 新的告警等级,可选
|
||||
}
|
||||
|
||||
// CreateAreaThresholdAlarmDTO 创建区域阈值告警的请求DTO
|
||||
type CreateAreaThresholdAlarmDTO struct {
|
||||
AreaControllerID uint32 `json:"area_controller_id" binding:"required"` // 区域主控ID
|
||||
SensorType models.SensorType `json:"sensor_type" binding:"required"` // 传感器类型
|
||||
Thresholds float32 `json:"thresholds" binding:"required"` // 阈值
|
||||
Operator models.Operator `json:"operator" binding:"required"` // 操作符
|
||||
Level models.SeverityLevel `json:"level,omitempty"` // 告警等级,可选
|
||||
}
|
||||
|
||||
// UpdateAreaThresholdAlarmDTO 更新区域阈值告警的请求DTO
|
||||
type UpdateAreaThresholdAlarmDTO struct {
|
||||
Thresholds float32 `json:"thresholds" binding:"required"` // 新的阈值
|
||||
Operator models.Operator `json:"operator" binding:"required"` // 新的操作符
|
||||
Level models.SeverityLevel `json:"level,omitempty"` // 新的告警等级,可选
|
||||
}
|
||||
|
||||
// DeleteDeviceThresholdAlarmDTO 删除设备阈值告警的请求DTO
|
||||
type DeleteDeviceThresholdAlarmDTO struct {
|
||||
SensorType models.SensorType `json:"sensor_type" binding:"required"` // 传感器类型
|
||||
}
|
||||
|
||||
// AreaThresholdAlarmDTO 用于表示一个区域阈值告警任务的详细信息
|
||||
type AreaThresholdAlarmDTO struct {
|
||||
ID int `json:"id"`
|
||||
AreaControllerID uint32 `json:"area_controller_id"`
|
||||
SensorType models.SensorType `json:"sensor_type"`
|
||||
Thresholds float32 `json:"thresholds"`
|
||||
Operator models.Operator `json:"operator"`
|
||||
Level models.SeverityLevel `json:"level"`
|
||||
}
|
||||
|
||||
// DeviceThresholdAlarmDTO 用于表示一个设备阈值告警任务的详细信息
|
||||
type DeviceThresholdAlarmDTO struct {
|
||||
ID int `json:"id"`
|
||||
DeviceID uint32 `json:"device_id"`
|
||||
SensorType models.SensorType `json:"sensor_type"`
|
||||
Thresholds float32 `json:"thresholds"`
|
||||
Operator models.Operator `json:"operator"`
|
||||
Level models.SeverityLevel `json:"level"`
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
// CreateDeviceRequest 定义了创建设备时需要传入的参数
|
||||
type CreateDeviceRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
DeviceTemplateID uint `json:"device_template_id" validate:"required"`
|
||||
AreaControllerID uint `json:"area_controller_id" validate:"required"`
|
||||
DeviceTemplateID uint32 `json:"device_template_id" validate:"required"`
|
||||
AreaControllerID uint32 `json:"area_controller_id" validate:"required"`
|
||||
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||
}
|
||||
@@ -14,8 +14,8 @@ type CreateDeviceRequest struct {
|
||||
// UpdateDeviceRequest 定义了更新设备时需要传入的参数
|
||||
type UpdateDeviceRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
DeviceTemplateID uint `json:"device_template_id" validate:"required"`
|
||||
AreaControllerID uint `json:"area_controller_id" validate:"required"`
|
||||
DeviceTemplateID uint32 `json:"device_template_id" validate:"required"`
|
||||
AreaControllerID uint32 `json:"area_controller_id" validate:"required"`
|
||||
Location string `json:"location,omitempty" validate:"omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
|
||||
}
|
||||
@@ -64,11 +64,11 @@ type UpdateDeviceTemplateRequest struct {
|
||||
|
||||
// DeviceResponse 定义了返回给客户端的单个设备信息的结构
|
||||
type DeviceResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DeviceTemplateID uint `json:"device_template_id"`
|
||||
DeviceTemplateID uint32 `json:"device_template_id"`
|
||||
DeviceTemplateName string `json:"device_template_name"`
|
||||
AreaControllerID uint `json:"area_controller_id"`
|
||||
AreaControllerID uint32 `json:"area_controller_id"`
|
||||
AreaControllerName string `json:"area_controller_name"`
|
||||
Location string `json:"location"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
@@ -78,7 +78,7 @@ type DeviceResponse struct {
|
||||
|
||||
// AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构
|
||||
type AreaControllerResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
NetworkID string `json:"network_id"`
|
||||
Location string `json:"location"`
|
||||
@@ -90,7 +90,7 @@ type AreaControllerResponse struct {
|
||||
|
||||
// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构
|
||||
type DeviceTemplateResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
Description string `json:"description"`
|
||||
|
||||
10
internal/app/dto/dto.go
Normal file
10
internal/app/dto/dto.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package dto
|
||||
|
||||
// --- General ---
|
||||
|
||||
// PaginationDTO 定义了分页信息的标准结构
|
||||
type PaginationDTO struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
@@ -11,11 +11,11 @@ func NewListSensorDataResponse(data []models.SensorData, total int64, page, page
|
||||
dtos := make([]SensorDataDTO, len(data))
|
||||
for i, item := range data {
|
||||
dtos[i] = SensorDataDTO{
|
||||
Time: item.Time,
|
||||
DeviceID: item.DeviceID,
|
||||
RegionalControllerID: item.RegionalControllerID,
|
||||
SensorType: item.SensorType,
|
||||
Data: json.RawMessage(item.Data),
|
||||
Time: item.Time,
|
||||
DeviceID: item.DeviceID,
|
||||
AreaControllerID: item.AreaControllerID,
|
||||
SensorType: item.SensorType,
|
||||
Data: json.RawMessage(item.Data),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ func NewListDeviceCommandLogResponse(data []models.DeviceCommandLog, total int64
|
||||
|
||||
// NewListPlanExecutionLogResponse 从模型数据创建列表响应 DTO
|
||||
func NewListPlanExecutionLogResponse(planLogs []models.PlanExecutionLog, plans []models.Plan, total int64, page, pageSize int) *ListPlanExecutionLogResponse {
|
||||
planId2Name := make(map[uint]string)
|
||||
planId2Name := make(map[uint32]string)
|
||||
for _, plan := range plans {
|
||||
planId2Name[plan.ID] = plan.Name
|
||||
planId2Name[plan.ID] = string(plan.Name)
|
||||
}
|
||||
|
||||
dtos := make([]PlanExecutionLogDTO, len(planLogs))
|
||||
@@ -95,7 +95,7 @@ func NewListTaskExecutionLogResponse(data []models.TaskExecutionLog, total int64
|
||||
PlanExecutionLogID: item.PlanExecutionLogID,
|
||||
TaskID: item.TaskID,
|
||||
Task: TaskDTO{
|
||||
ID: uint(item.Task.ID),
|
||||
ID: uint32(item.Task.ID),
|
||||
Name: item.Task.Name,
|
||||
Description: item.Task.Description,
|
||||
},
|
||||
@@ -373,7 +373,7 @@ func NewListWeighingRecordResponse(data []models.WeighingRecord, total int64, pa
|
||||
func NewListPigTransferLogResponse(data []models.PigTransferLog, total int64, page, pageSize int) *ListPigTransferLogResponse {
|
||||
dtos := make([]PigTransferLogDTO, len(data))
|
||||
for i, item := range data {
|
||||
// 注意:PigTransferLog 的 ID, CreatedAt, UpdatedAt 字段是 gorm.Model 嵌入的
|
||||
// 注意:PigTransferLog 的 ID, CreatedAt, UpdatedAt 字段是 Model 嵌入的
|
||||
dtos[i] = PigTransferLogDTO{
|
||||
ID: item.ID,
|
||||
CreatedAt: item.CreatedAt,
|
||||
|
||||
@@ -7,22 +7,13 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
// --- General ---
|
||||
|
||||
// PaginationDTO 定义了分页信息的标准结构
|
||||
type PaginationDTO struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// --- SensorData ---
|
||||
|
||||
// ListSensorDataRequest 定义了获取传感器数据列表的请求参数
|
||||
type ListSensorDataRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
DeviceID *uint `json:"device_id" query:"device_id"`
|
||||
DeviceID *uint32 `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"`
|
||||
@@ -31,11 +22,11 @@ type ListSensorDataRequest struct {
|
||||
|
||||
// SensorDataDTO 是用于API响应的传感器数据结构
|
||||
type SensorDataDTO struct {
|
||||
Time time.Time `json:"time"`
|
||||
DeviceID uint `json:"device_id"`
|
||||
RegionalControllerID uint `json:"regional_controller_id"`
|
||||
SensorType models.SensorType `json:"sensor_type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Time time.Time `json:"time"`
|
||||
DeviceID uint32 `json:"device_id"`
|
||||
AreaControllerID uint32 `json:"area_controller_id"`
|
||||
SensorType models.SensorType `json:"sensor_type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// ListSensorDataResponse 是获取传感器数据列表的响应结构
|
||||
@@ -50,7 +41,7 @@ type ListSensorDataResponse struct {
|
||||
type ListDeviceCommandLogRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
DeviceID *uint `json:"device_id" query:"device_id"`
|
||||
DeviceID *uint32 `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"`
|
||||
@@ -60,7 +51,7 @@ type ListDeviceCommandLogRequest struct {
|
||||
// DeviceCommandLogDTO 是用于API响应的设备命令日志结构
|
||||
type DeviceCommandLogDTO struct {
|
||||
MessageID string `json:"message_id"`
|
||||
DeviceID uint `json:"device_id"`
|
||||
DeviceID uint32 `json:"device_id"`
|
||||
SentAt time.Time `json:"sent_at"`
|
||||
AcknowledgedAt *time.Time `json:"acknowledged_at"`
|
||||
ReceivedSuccess bool `json:"received_success"`
|
||||
@@ -78,7 +69,7 @@ type ListDeviceCommandLogResponse struct {
|
||||
type ListPlanExecutionLogRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PlanID *uint `json:"plan_id" query:"plan_id"`
|
||||
PlanID *uint32 `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"`
|
||||
@@ -87,10 +78,10 @@ type ListPlanExecutionLogRequest struct {
|
||||
|
||||
// PlanExecutionLogDTO 是用于API响应的计划执行日志结构
|
||||
type PlanExecutionLogDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PlanID uint `json:"plan_id"`
|
||||
PlanID uint32 `json:"plan_id"`
|
||||
PlanName string `json:"plan_name"`
|
||||
Status models.ExecutionStatus `json:"status"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
@@ -110,7 +101,7 @@ type ListPlanExecutionLogResponse struct {
|
||||
type ListTaskExecutionLogRequest struct {
|
||||
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"`
|
||||
PlanExecutionLogID *uint32 `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"`
|
||||
@@ -120,17 +111,17 @@ type ListTaskExecutionLogRequest struct {
|
||||
|
||||
// TaskDTO 是用于API响应的简化版任务结构
|
||||
type TaskDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// TaskExecutionLogDTO 是用于API响应的任务执行日志结构
|
||||
type TaskExecutionLogDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PlanExecutionLogID uint `json:"plan_execution_log_id"`
|
||||
PlanExecutionLogID uint32 `json:"plan_execution_log_id"`
|
||||
TaskID int `json:"task_id"`
|
||||
Task TaskDTO `json:"task"` // 嵌套的任务信息
|
||||
Status models.ExecutionStatus `json:"status"`
|
||||
@@ -151,7 +142,7 @@ type ListTaskExecutionLogResponse struct {
|
||||
type ListPendingCollectionRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
DeviceID *uint `json:"device_id" query:"device_id"`
|
||||
DeviceID *uint32 `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"`
|
||||
@@ -161,7 +152,7 @@ type ListPendingCollectionRequest struct {
|
||||
// PendingCollectionDTO 是用于API响应的待采集请求结构
|
||||
type PendingCollectionDTO struct {
|
||||
CorrelationID string `json:"correlation_id"`
|
||||
DeviceID uint `json:"device_id"`
|
||||
DeviceID uint32 `json:"device_id"`
|
||||
CommandMetadata models.UintArray `json:"command_metadata"`
|
||||
Status models.PendingCollectionStatus `json:"status"`
|
||||
FulfilledAt *time.Time `json:"fulfilled_at"`
|
||||
@@ -180,7 +171,7 @@ type ListPendingCollectionResponse struct {
|
||||
type ListUserActionLogRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
UserID *uint `json:"user_id" query:"user_id"`
|
||||
UserID *uint32 `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"`
|
||||
@@ -191,9 +182,9 @@ type ListUserActionLogRequest struct {
|
||||
|
||||
// UserActionLogDTO 是用于API响应的用户操作日志结构
|
||||
type UserActionLogDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Time time.Time `json:"time"`
|
||||
UserID uint `json:"user_id"`
|
||||
UserID uint32 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
SourceIP string `json:"source_ip"`
|
||||
ActionType string `json:"action_type"`
|
||||
@@ -217,7 +208,7 @@ type ListUserActionLogResponse struct {
|
||||
type ListRawMaterialPurchaseRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
RawMaterialID *uint `json:"raw_material_id" query:"raw_material_id"`
|
||||
RawMaterialID *uint32 `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"`
|
||||
@@ -226,19 +217,19 @@ type ListRawMaterialPurchaseRequest struct {
|
||||
|
||||
// RawMaterialDTO 是用于API响应的简化版原料结构
|
||||
type RawMaterialDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// RawMaterialPurchaseDTO 是用于API响应的原料采购结构
|
||||
type RawMaterialPurchaseDTO struct {
|
||||
ID uint `json:"id"`
|
||||
RawMaterialID uint `json:"raw_material_id"`
|
||||
ID uint32 `json:"id"`
|
||||
RawMaterialID uint32 `json:"raw_material_id"`
|
||||
RawMaterial RawMaterialDTO `json:"raw_material"`
|
||||
Supplier string `json:"supplier"`
|
||||
Amount float64 `json:"amount"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
Amount float32 `json:"amount"`
|
||||
UnitPrice float32 `json:"unit_price"`
|
||||
TotalPrice float32 `json:"total_price"`
|
||||
PurchaseDate time.Time `json:"purchase_date"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
@@ -255,9 +246,9 @@ type ListRawMaterialPurchaseResponse struct {
|
||||
type ListRawMaterialStockLogRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
RawMaterialID *uint `json:"raw_material_id" query:"raw_material_id"`
|
||||
RawMaterialID *uint32 `json:"raw_material_id" query:"raw_material_id"`
|
||||
SourceType *string `json:"source_type" query:"source_type"`
|
||||
SourceID *uint `json:"source_id" query:"source_id"`
|
||||
SourceID *uint32 `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"`
|
||||
@@ -265,11 +256,11 @@ type ListRawMaterialStockLogRequest struct {
|
||||
|
||||
// RawMaterialStockLogDTO 是用于API响应的原料库存日志结构
|
||||
type RawMaterialStockLogDTO struct {
|
||||
ID uint `json:"id"`
|
||||
RawMaterialID uint `json:"raw_material_id"`
|
||||
ChangeAmount float64 `json:"change_amount"`
|
||||
ID uint32 `json:"id"`
|
||||
RawMaterialID uint32 `json:"raw_material_id"`
|
||||
ChangeAmount float32 `json:"change_amount"`
|
||||
SourceType models.StockLogSourceType `json:"source_type"`
|
||||
SourceID uint `json:"source_id"`
|
||||
SourceID uint32 `json:"source_id"`
|
||||
HappenedAt time.Time `json:"happened_at"`
|
||||
Remarks string `json:"remarks"`
|
||||
}
|
||||
@@ -286,9 +277,9 @@ type ListRawMaterialStockLogResponse struct {
|
||||
type ListFeedUsageRecordRequest struct {
|
||||
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"`
|
||||
PenID *uint32 `json:"pen_id" query:"pen_id"`
|
||||
FeedFormulaID *uint32 `json:"feed_formula_id" query:"feed_formula_id"`
|
||||
OperatorID *uint32 `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"`
|
||||
@@ -296,26 +287,26 @@ type ListFeedUsageRecordRequest struct {
|
||||
|
||||
// PenDTO 是用于API响应的简化版猪栏结构
|
||||
type PenDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// FeedFormulaDTO 是用于API响应的简化版饲料配方结构
|
||||
type FeedFormulaDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// FeedUsageRecordDTO 是用于API响应的饲料使用记录结构
|
||||
type FeedUsageRecordDTO struct {
|
||||
ID uint `json:"id"`
|
||||
PenID uint `json:"pen_id"`
|
||||
ID uint32 `json:"id"`
|
||||
PenID uint32 `json:"pen_id"`
|
||||
Pen PenDTO `json:"pen"`
|
||||
FeedFormulaID uint `json:"feed_formula_id"`
|
||||
FeedFormulaID uint32 `json:"feed_formula_id"`
|
||||
FeedFormula FeedFormulaDTO `json:"feed_formula"`
|
||||
Amount float64 `json:"amount"`
|
||||
Amount float32 `json:"amount"`
|
||||
RecordedAt time.Time `json:"recorded_at"`
|
||||
OperatorID uint `json:"operator_id"`
|
||||
OperatorID uint32 `json:"operator_id"`
|
||||
Remarks string `json:"remarks"`
|
||||
}
|
||||
|
||||
@@ -331,10 +322,10 @@ type ListFeedUsageRecordResponse struct {
|
||||
type ListMedicationLogRequest struct {
|
||||
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"`
|
||||
PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
MedicationID *uint32 `json:"medication_id" query:"medication_id"`
|
||||
Reason *string `json:"reason" query:"reason"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
OperatorID *uint32 `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"`
|
||||
@@ -342,21 +333,21 @@ type ListMedicationLogRequest struct {
|
||||
|
||||
// MedicationDTO 是用于API响应的简化版药品结构
|
||||
type MedicationDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// MedicationLogDTO 是用于API响应的用药记录结构
|
||||
type MedicationLogDTO struct {
|
||||
ID uint `json:"id"`
|
||||
PigBatchID uint `json:"pig_batch_id"`
|
||||
MedicationID uint `json:"medication_id"`
|
||||
ID uint32 `json:"id"`
|
||||
PigBatchID uint32 `json:"pig_batch_id"`
|
||||
MedicationID uint32 `json:"medication_id"`
|
||||
Medication MedicationDTO `json:"medication"`
|
||||
DosageUsed float64 `json:"dosage_used"`
|
||||
DosageUsed float32 `json:"dosage_used"`
|
||||
TargetCount int `json:"target_count"`
|
||||
Reason models.MedicationReasonType `json:"reason"`
|
||||
Description string `json:"description"`
|
||||
OperatorID uint `json:"operator_id"`
|
||||
OperatorID uint32 `json:"operator_id"`
|
||||
HappenedAt time.Time `json:"happened_at"`
|
||||
}
|
||||
|
||||
@@ -372,9 +363,9 @@ type ListMedicationLogResponse struct {
|
||||
type ListPigBatchLogRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
ChangeType *string `json:"change_type" query:"change_type"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
OperatorID *uint32 `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"`
|
||||
@@ -382,16 +373,16 @@ type ListPigBatchLogRequest struct {
|
||||
|
||||
// PigBatchLogDTO 是用于API响应的猪批次日志结构
|
||||
type PigBatchLogDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PigBatchID uint `json:"pig_batch_id"`
|
||||
PigBatchID uint32 `json:"pig_batch_id"`
|
||||
ChangeType models.LogChangeType `json:"change_type"`
|
||||
ChangeCount int `json:"change_count"`
|
||||
Reason string `json:"reason"`
|
||||
BeforeCount int `json:"before_count"`
|
||||
AfterCount int `json:"after_count"`
|
||||
OperatorID uint `json:"operator_id"`
|
||||
OperatorID uint32 `json:"operator_id"`
|
||||
HappenedAt time.Time `json:"happened_at"`
|
||||
}
|
||||
|
||||
@@ -407,7 +398,7 @@ type ListPigBatchLogResponse struct {
|
||||
type ListWeighingBatchRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
PigBatchID *uint32 `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"`
|
||||
@@ -415,12 +406,12 @@ type ListWeighingBatchRequest struct {
|
||||
|
||||
// WeighingBatchDTO 是用于API响应的批次称重记录结构
|
||||
type WeighingBatchDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
WeighingTime time.Time `json:"weighing_time"`
|
||||
Description string `json:"description"`
|
||||
PigBatchID uint `json:"pig_batch_id"`
|
||||
PigBatchID uint32 `json:"pig_batch_id"`
|
||||
}
|
||||
|
||||
// ListWeighingBatchResponse 是获取批次称重记录列表的响应结构
|
||||
@@ -435,9 +426,9 @@ type ListWeighingBatchResponse struct {
|
||||
type ListWeighingRecordRequest struct {
|
||||
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"`
|
||||
WeighingBatchID *uint32 `json:"weighing_batch_id" query:"weighing_batch_id"`
|
||||
PenID *uint32 `json:"pen_id" query:"pen_id"`
|
||||
OperatorID *uint32 `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"`
|
||||
@@ -445,13 +436,13 @@ type ListWeighingRecordRequest struct {
|
||||
|
||||
// WeighingRecordDTO 是用于API响应的单次称重记录结构
|
||||
type WeighingRecordDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Weight float64 `json:"weight"`
|
||||
WeighingBatchID uint `json:"weighing_batch_id"`
|
||||
PenID uint `json:"pen_id"`
|
||||
OperatorID uint `json:"operator_id"`
|
||||
Weight float32 `json:"weight"`
|
||||
WeighingBatchID uint32 `json:"weighing_batch_id"`
|
||||
PenID uint32 `json:"pen_id"`
|
||||
OperatorID uint32 `json:"operator_id"`
|
||||
Remark string `json:"remark"`
|
||||
WeighingTime time.Time `json:"weighing_time"`
|
||||
}
|
||||
@@ -468,10 +459,10 @@ type ListWeighingRecordResponse struct {
|
||||
type ListPigTransferLogRequest struct {
|
||||
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"`
|
||||
PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
PenID *uint32 `json:"pen_id" query:"pen_id"`
|
||||
TransferType *string `json:"transfer_type" query:"transfer_type"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
OperatorID *uint32 `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"`
|
||||
@@ -480,16 +471,16 @@ type ListPigTransferLogRequest struct {
|
||||
|
||||
// PigTransferLogDTO 是用于API响应的猪只迁移日志结构
|
||||
type PigTransferLogDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
TransferTime time.Time `json:"transfer_time"`
|
||||
PigBatchID uint `json:"pig_batch_id"`
|
||||
PenID uint `json:"pen_id"`
|
||||
PigBatchID uint32 `json:"pig_batch_id"`
|
||||
PenID uint32 `json:"pen_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
Type models.PigTransferType `json:"type"`
|
||||
CorrelationID string `json:"correlation_id"`
|
||||
OperatorID uint `json:"operator_id"`
|
||||
OperatorID uint32 `json:"operator_id"`
|
||||
Remarks string `json:"remarks"`
|
||||
}
|
||||
|
||||
@@ -505,11 +496,11 @@ type ListPigTransferLogResponse struct {
|
||||
type ListPigSickLogRequest struct {
|
||||
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"`
|
||||
PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
PenID *uint32 `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"`
|
||||
OperatorID *uint32 `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"`
|
||||
@@ -517,18 +508,18 @@ type ListPigSickLogRequest struct {
|
||||
|
||||
// PigSickLogDTO 是用于API响应的病猪日志结构
|
||||
type PigSickLogDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PigBatchID uint `json:"pig_batch_id"`
|
||||
PenID uint `json:"pen_id"`
|
||||
PigBatchID uint32 `json:"pig_batch_id"`
|
||||
PenID uint32 `json:"pen_id"`
|
||||
ChangeCount int `json:"change_count"`
|
||||
Reason models.PigBatchSickPigReasonType `json:"reason"`
|
||||
BeforeCount int `json:"before_count"`
|
||||
AfterCount int `json:"after_count"`
|
||||
Remarks string `json:"remarks"`
|
||||
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location"`
|
||||
OperatorID uint `json:"operator_id"`
|
||||
OperatorID uint32 `json:"operator_id"`
|
||||
HappenedAt time.Time `json:"happened_at"`
|
||||
}
|
||||
|
||||
@@ -544,9 +535,9 @@ type ListPigSickLogResponse struct {
|
||||
type ListPigPurchaseRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
Supplier *string `json:"supplier" query:"supplier"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
OperatorID *uint32 `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"`
|
||||
@@ -554,17 +545,17 @@ type ListPigPurchaseRequest struct {
|
||||
|
||||
// PigPurchaseDTO 是用于API响应的猪只采购记录结构
|
||||
type PigPurchaseDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PigBatchID uint `json:"pig_batch_id"`
|
||||
PigBatchID uint32 `json:"pig_batch_id"`
|
||||
PurchaseDate time.Time `json:"purchase_date"`
|
||||
Supplier string `json:"supplier"`
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
UnitPrice float32 `json:"unit_price"`
|
||||
TotalPrice float32 `json:"total_price"`
|
||||
Remarks string `json:"remarks"`
|
||||
OperatorID uint `json:"operator_id"`
|
||||
OperatorID uint32 `json:"operator_id"`
|
||||
}
|
||||
|
||||
// ListPigPurchaseResponse 是获取猪只采购记录列表的响应结构
|
||||
@@ -579,9 +570,9 @@ type ListPigPurchaseResponse struct {
|
||||
type ListPigSaleRequest struct {
|
||||
Page int `json:"page" query:"page"`
|
||||
PageSize int `json:"page_size" query:"page_size"`
|
||||
PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"`
|
||||
Buyer *string `json:"buyer" query:"buyer"`
|
||||
OperatorID *uint `json:"operator_id" query:"operator_id"`
|
||||
OperatorID *uint32 `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"`
|
||||
@@ -589,17 +580,17 @@ type ListPigSaleRequest struct {
|
||||
|
||||
// PigSaleDTO 是用于API响应的猪只销售记录结构
|
||||
type PigSaleDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PigBatchID uint `json:"pig_batch_id"`
|
||||
PigBatchID uint32 `json:"pig_batch_id"`
|
||||
SaleDate time.Time `json:"sale_date"`
|
||||
Buyer string `json:"buyer"`
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
UnitPrice float32 `json:"unit_price"`
|
||||
TotalPrice float32 `json:"total_price"`
|
||||
Remarks string `json:"remarks"`
|
||||
OperatorID uint `json:"operator_id"`
|
||||
OperatorID uint32 `json:"operator_id"`
|
||||
}
|
||||
|
||||
// ListPigSaleResponse 是获取猪只销售记录列表的响应结构
|
||||
|
||||
@@ -2,8 +2,6 @@ package dto
|
||||
|
||||
import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// NewListNotificationResponse 从模型数据创建通知列表响应 DTO
|
||||
@@ -18,7 +16,7 @@ func NewListNotificationResponse(data []models.Notification, total int64, page,
|
||||
UserID: item.UserID,
|
||||
Title: item.Title,
|
||||
Message: item.Message,
|
||||
Level: zapcore.Level(item.Level),
|
||||
Level: item.Level,
|
||||
AlarmTimestamp: item.AlarmTimestamp,
|
||||
ToAddress: item.ToAddress,
|
||||
Status: item.Status,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/notify"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
@@ -12,15 +11,15 @@ import (
|
||||
// SendTestNotificationRequest 定义了发送测试通知请求的 JSON 结构
|
||||
type SendTestNotificationRequest struct {
|
||||
// Type 指定要测试的通知渠道
|
||||
Type notify.NotifierType `json:"type" validate:"required"`
|
||||
Type models.NotifierType `json:"type" validate:"required"`
|
||||
}
|
||||
|
||||
// ListNotificationRequest 定义了获取通知列表的请求参数
|
||||
type ListNotificationRequest struct {
|
||||
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"`
|
||||
UserID *uint32 `json:"user_id" query:"user_id"`
|
||||
NotifierType *models.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"`
|
||||
@@ -30,14 +29,14 @@ type ListNotificationRequest struct {
|
||||
|
||||
// NotificationDTO 是用于API响应的通知结构
|
||||
type NotificationDTO struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
NotifierType notify.NotifierType `json:"notifier_type"`
|
||||
UserID uint `json:"user_id"`
|
||||
NotifierType models.NotifierType `json:"notifier_type"`
|
||||
UserID uint32 `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Level zapcore.Level `json:"level"`
|
||||
Level models.SeverityLevel `json:"level"`
|
||||
AlarmTimestamp time.Time `json:"alarm_timestamp"`
|
||||
ToAddress string `json:"to_address"`
|
||||
Status models.NotificationStatus `json:"status"`
|
||||
|
||||
@@ -32,7 +32,7 @@ type PigBatchQueryDTO struct {
|
||||
|
||||
// PigBatchResponseDTO 定义了猪批次信息的响应结构
|
||||
type PigBatchResponseDTO struct {
|
||||
ID uint `json:"id"` // 批次ID
|
||||
ID uint32 `json:"id"` // 批次ID
|
||||
BatchNumber string `json:"batch_number"` // 批次编号
|
||||
OriginType models.PigBatchOriginType `json:"origin_type"` // 批次来源
|
||||
StartDate time.Time `json:"start_date"` // 批次开始日期
|
||||
@@ -48,34 +48,34 @@ type PigBatchResponseDTO struct {
|
||||
|
||||
// AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体
|
||||
type AssignEmptyPensToBatchRequest struct {
|
||||
PenIDs []uint `json:"pen_ids" validate:"required,min=1,dive" example:"1,2,3"` // 待分配的猪栏ID列表
|
||||
PenIDs []uint32 `json:"pen_ids" validate:"required,min=1,dive" example:"1,2,3"` // 待分配的猪栏ID列表
|
||||
}
|
||||
|
||||
// ReclassifyPenToNewBatchRequest 用于将猪栏划拨到新批次的请求体
|
||||
type ReclassifyPenToNewBatchRequest struct {
|
||||
ToBatchID uint `json:"to_batch_id" validate:"required"` // 目标猪批次ID
|
||||
PenID uint `json:"pen_id" validate:"required"` // 待划拨的猪栏ID
|
||||
ToBatchID uint32 `json:"to_batch_id" validate:"required"` // 目标猪批次ID
|
||||
PenID uint32 `json:"pen_id" validate:"required"` // 待划拨的猪栏ID
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体
|
||||
type RemoveEmptyPenFromBatchRequest struct {
|
||||
PenID uint `json:"pen_id" validate:"required"` // 待移除的猪栏ID
|
||||
PenID uint32 `json:"pen_id" validate:"required"` // 待移除的猪栏ID
|
||||
}
|
||||
|
||||
// MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体
|
||||
type MovePigsIntoPenRequest struct {
|
||||
ToPenID uint `json:"to_pen_id" validate:"required"` // 目标猪栏ID
|
||||
ToPenID uint32 `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:"pen_id" validate:"required"` // 猪栏ID
|
||||
PenID uint32 `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"` // 总价
|
||||
UnitPrice float32 `json:"unit_price" validate:"required,min=0"` // 单价
|
||||
TotalPrice float32 `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"` // 备注
|
||||
@@ -83,10 +83,10 @@ type SellPigsRequest struct {
|
||||
|
||||
// BuyPigsRequest 用于处理买猪的请求体
|
||||
type BuyPigsRequest struct {
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
PenID uint32 `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"` // 总价
|
||||
UnitPrice float32 `json:"unit_price" validate:"required,min=0"` // 单价
|
||||
TotalPrice float32 `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"` // 备注
|
||||
@@ -94,24 +94,24 @@ type BuyPigsRequest struct {
|
||||
|
||||
// TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体
|
||||
type TransferPigsAcrossBatchesRequest struct {
|
||||
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"` // 调栏猪只数量
|
||||
DestBatchID uint32 `json:"dest_batch_id" validate:"required"` // 目标猪批次ID
|
||||
FromPenID uint32 `json:"from_pen_id" validate:"required"` // 源猪栏ID
|
||||
ToPenID uint32 `json:"to_pen_id" validate:"required"` // 目标猪栏ID
|
||||
Quantity uint32 `json:"quantity" validate:"required,min=1"` // 调栏猪只数量
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// TransferPigsWithinBatchRequest 用于群内调栏的请求体
|
||||
type TransferPigsWithinBatchRequest struct {
|
||||
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"` // 调栏猪只数量
|
||||
FromPenID uint32 `json:"from_pen_id" validate:"required"` // 源猪栏ID
|
||||
ToPenID uint32 `json:"to_pen_id" validate:"required"` // 目标猪栏ID
|
||||
Quantity uint32 `json:"quantity" validate:"required,min=1"` // 调栏猪只数量
|
||||
Remarks string `json:"remarks"` // 备注
|
||||
}
|
||||
|
||||
// RecordSickPigsRequest 用于记录新增病猪事件的请求体
|
||||
type RecordSickPigsRequest struct {
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
PenID uint32 `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"` // 发生时间
|
||||
@@ -120,7 +120,7 @@ type RecordSickPigsRequest struct {
|
||||
|
||||
// RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体
|
||||
type RecordSickPigRecoveryRequest struct {
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
PenID uint32 `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"` // 发生时间
|
||||
@@ -129,7 +129,7 @@ type RecordSickPigRecoveryRequest struct {
|
||||
|
||||
// RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体
|
||||
type RecordSickPigDeathRequest struct {
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
PenID uint32 `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"` // 发生时间
|
||||
@@ -138,7 +138,7 @@ type RecordSickPigDeathRequest struct {
|
||||
|
||||
// RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体
|
||||
type RecordSickPigCullRequest struct {
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
PenID uint32 `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"` // 发生时间
|
||||
@@ -147,7 +147,7 @@ type RecordSickPigCullRequest struct {
|
||||
|
||||
// RecordDeathRequest 用于记录正常猪只死亡事件的请求体
|
||||
type RecordDeathRequest struct {
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
PenID uint32 `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"` // 备注
|
||||
@@ -155,7 +155,7 @@ type RecordDeathRequest struct {
|
||||
|
||||
// RecordCullRequest 用于记录正常猪只淘汰事件的请求体
|
||||
type RecordCullRequest struct {
|
||||
PenID uint `json:"pen_id" validate:"required"` // 猪栏ID
|
||||
PenID uint32 `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"` // 备注
|
||||
|
||||
@@ -4,19 +4,19 @@ import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
|
||||
// PigHouseResponse 定义了猪舍信息的响应结构
|
||||
type PigHouseResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// PenResponse 定义了猪栏信息的响应结构
|
||||
type PenResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ID uint32 `json:"id"`
|
||||
PenNumber string `json:"pen_number"`
|
||||
HouseID uint `json:"house_id"`
|
||||
HouseID uint32 `json:"house_id"`
|
||||
Capacity int `json:"capacity"`
|
||||
Status models.PenStatus `json:"status"`
|
||||
PigBatchID *uint `json:"pig_batch_id,omitempty"`
|
||||
PigBatchID *uint32 `json:"pig_batch_id,omitempty"`
|
||||
CurrentPigCount int `json:"current_pig_count"`
|
||||
}
|
||||
|
||||
@@ -35,14 +35,14 @@ type UpdatePigHouseRequest struct {
|
||||
// CreatePenRequest 定义了创建猪栏的请求结构
|
||||
type CreatePenRequest struct {
|
||||
PenNumber string `json:"pen_number" validate:"required"`
|
||||
HouseID uint `json:"house_id" validate:"required"`
|
||||
HouseID uint32 `json:"house_id" validate:"required"`
|
||||
Capacity int `json:"capacity" validate:"required"`
|
||||
}
|
||||
|
||||
// UpdatePenRequest 定义了更新猪栏的请求结构
|
||||
type UpdatePenRequest struct {
|
||||
PenNumber string `json:"pen_number" validate:"required"`
|
||||
HouseID uint `json:"house_id" validate:"required"`
|
||||
HouseID uint32 `json:"house_id" validate:"required"`
|
||||
Capacity int `json:"capacity" validate:"required"`
|
||||
Status models.PenStatus `json:"status" validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) {
|
||||
|
||||
response := &PlanResponse{
|
||||
ID: plan.ID,
|
||||
Name: plan.Name,
|
||||
Name: string(plan.Name),
|
||||
Description: plan.Description,
|
||||
PlanType: plan.PlanType,
|
||||
ExecutionType: plan.ExecutionType,
|
||||
@@ -60,7 +60,7 @@ func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
|
||||
}
|
||||
|
||||
plan := &models.Plan{
|
||||
Name: req.Name,
|
||||
Name: models.PlanName(req.Name),
|
||||
Description: req.Description,
|
||||
ExecutionType: req.ExecutionType,
|
||||
ExecuteNum: req.ExecuteNum,
|
||||
@@ -103,7 +103,7 @@ func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
|
||||
}
|
||||
|
||||
plan := &models.Plan{
|
||||
Name: req.Name,
|
||||
Name: models.PlanName(req.Name),
|
||||
Description: req.Description,
|
||||
ExecutionType: req.ExecutionType,
|
||||
ExecuteNum: req.ExecuteNum,
|
||||
|
||||
@@ -17,22 +17,22 @@ type CreatePlanRequest struct {
|
||||
Name string `json:"name" validate:"required" example:"猪舍温度控制计划"`
|
||||
Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
|
||||
ExecutionType models.PlanExecutionType `json:"execution_type" validate:"required" example:"自动"`
|
||||
ExecuteNum uint `json:"execute_num,omitempty" validate:"omitempty,min=0" example:"10"`
|
||||
ExecuteNum uint32 `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"`
|
||||
SubPlanIDs []uint32 `json:"sub_plan_ids,omitempty" validate:"omitempty,dive"`
|
||||
Tasks []TaskRequest `json:"tasks,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
// PlanResponse 定义计划详情响应的结构体
|
||||
type PlanResponse struct {
|
||||
ID uint `json:"id" example:"1"`
|
||||
ID uint32 `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"`
|
||||
ExecuteCount uint `json:"execute_count" example:"0"`
|
||||
ExecuteNum uint32 `json:"execute_num" example:"10"`
|
||||
ExecuteCount uint32 `json:"execute_count" example:"0"`
|
||||
CronExpression string `json:"cron_expression" example:"0 0 6 * * *"`
|
||||
ContentType models.PlanContentType `json:"content_type" example:"任务"`
|
||||
SubPlans []SubPlanResponse `json:"sub_plans,omitempty"`
|
||||
@@ -50,17 +50,17 @@ type UpdatePlanRequest struct {
|
||||
Name string `json:"name" example:"猪舍温度控制计划V2"`
|
||||
Description string `json:"description" example:"更新后的描述"`
|
||||
ExecutionType models.PlanExecutionType `json:"execution_type" validate:"required" example:"自动"`
|
||||
ExecuteNum uint `json:"execute_num,omitempty" validate:"omitempty,min=0" example:"10"`
|
||||
ExecuteNum uint32 `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"`
|
||||
SubPlanIDs []uint32 `json:"sub_plan_ids,omitempty" validate:"omitempty,dive"`
|
||||
Tasks []TaskRequest `json:"tasks,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
// SubPlanResponse 定义子计划响应结构体
|
||||
type SubPlanResponse struct {
|
||||
ID uint `json:"id" example:"1"`
|
||||
ParentPlanID uint `json:"parent_plan_id" example:"1"`
|
||||
ChildPlanID uint `json:"child_plan_id" example:"2"`
|
||||
ID uint32 `json:"id" example:"1"`
|
||||
ParentPlanID uint32 `json:"parent_plan_id" example:"1"`
|
||||
ChildPlanID uint32 `json:"child_plan_id" example:"2"`
|
||||
ExecutionOrder int `json:"execution_order" example:"1"`
|
||||
ChildPlan *PlanResponse `json:"child_plan,omitempty"`
|
||||
}
|
||||
@@ -77,7 +77,7 @@ type TaskRequest struct {
|
||||
// TaskResponse 定义任务响应结构体
|
||||
type TaskResponse struct {
|
||||
ID int `json:"id" example:"1"`
|
||||
PlanID uint `json:"plan_id" example:"1"`
|
||||
PlanID uint32 `json:"plan_id" example:"1"`
|
||||
Name string `json:"name" example:"打开风扇"`
|
||||
Description string `json:"description" example:"打开1号风扇"`
|
||||
ExecutionOrder int `json:"execution_order" example:"1"`
|
||||
|
||||
@@ -16,19 +16,19 @@ type LoginRequest struct {
|
||||
// CreateUserResponse 定义创建用户成功响应的结构体
|
||||
type CreateUserResponse struct {
|
||||
Username string `json:"username" example:"newuser"`
|
||||
ID uint `json:"id" example:"1"`
|
||||
ID uint32 `json:"id" example:"1"`
|
||||
}
|
||||
|
||||
// LoginResponse 定义登录成功响应的结构体
|
||||
type LoginResponse struct {
|
||||
Username string `json:"username" example:"testuser"`
|
||||
ID uint `json:"id" example:"1"`
|
||||
ID uint32 `json:"id" example:"1"`
|
||||
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
|
||||
}
|
||||
|
||||
// HistoryResponse 定义单条操作历史的响应结构体
|
||||
type HistoryResponse struct {
|
||||
UserID uint `json:"user_id" example:"101"`
|
||||
UserID uint32 `json:"user_id" example:"101"`
|
||||
Username string `json:"username" example:"testuser"`
|
||||
ActionType string `json:"action_type" example:"更新设备"`
|
||||
Description string `json:"description" example:"设备更新成功"`
|
||||
|
||||
@@ -28,32 +28,33 @@ var (
|
||||
// DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。
|
||||
type DeviceService interface {
|
||||
CreateDevice(ctx context.Context, req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error)
|
||||
GetDevice(ctx context.Context, id uint) (*dto.DeviceResponse, error)
|
||||
GetDevice(ctx context.Context, id uint32) (*dto.DeviceResponse, error)
|
||||
ListDevices(ctx context.Context) ([]*dto.DeviceResponse, error)
|
||||
UpdateDevice(ctx context.Context, id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error)
|
||||
DeleteDevice(ctx context.Context, id uint) error
|
||||
ManualControl(ctx context.Context, id uint, req *dto.ManualControlDeviceRequest) error
|
||||
UpdateDevice(ctx context.Context, id uint32, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error)
|
||||
DeleteDevice(ctx context.Context, id uint32) error
|
||||
ManualControl(ctx context.Context, id uint32, req *dto.ManualControlDeviceRequest) error
|
||||
|
||||
CreateAreaController(ctx context.Context, req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error)
|
||||
GetAreaController(ctx context.Context, id uint) (*dto.AreaControllerResponse, error)
|
||||
GetAreaController(ctx context.Context, id uint32) (*dto.AreaControllerResponse, error)
|
||||
ListAreaControllers(ctx context.Context) ([]*dto.AreaControllerResponse, error)
|
||||
UpdateAreaController(ctx context.Context, id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error)
|
||||
DeleteAreaController(ctx context.Context, id uint) error
|
||||
UpdateAreaController(ctx context.Context, id uint32, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error)
|
||||
DeleteAreaController(ctx context.Context, id uint32) error
|
||||
|
||||
CreateDeviceTemplate(ctx context.Context, req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
|
||||
GetDeviceTemplate(ctx context.Context, id uint) (*dto.DeviceTemplateResponse, error)
|
||||
GetDeviceTemplate(ctx context.Context, id uint32) (*dto.DeviceTemplateResponse, error)
|
||||
ListDeviceTemplates(ctx context.Context) ([]*dto.DeviceTemplateResponse, error)
|
||||
UpdateDeviceTemplate(ctx context.Context, id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
|
||||
DeleteDeviceTemplate(ctx context.Context, id uint) error
|
||||
UpdateDeviceTemplate(ctx context.Context, id uint32, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
|
||||
DeleteDeviceTemplate(ctx context.Context, id uint32) error
|
||||
}
|
||||
|
||||
// deviceService 是 DeviceService 接口的具体实现。
|
||||
type deviceService struct {
|
||||
ctx context.Context
|
||||
deviceRepo repository.DeviceRepository
|
||||
areaControllerRepo repository.AreaControllerRepository
|
||||
deviceTemplateRepo repository.DeviceTemplateRepository
|
||||
deviceDomainSvc device.Service
|
||||
ctx context.Context
|
||||
deviceRepo repository.DeviceRepository
|
||||
areaControllerRepo repository.AreaControllerRepository
|
||||
deviceTemplateRepo repository.DeviceTemplateRepository
|
||||
deviceDomainSvc device.Service
|
||||
thresholdAlarmService ThresholdAlarmService
|
||||
}
|
||||
|
||||
// NewDeviceService 创建一个新的 DeviceService 实例。
|
||||
@@ -63,13 +64,15 @@ func NewDeviceService(
|
||||
areaControllerRepo repository.AreaControllerRepository,
|
||||
deviceTemplateRepo repository.DeviceTemplateRepository,
|
||||
deviceDomainSvc device.Service,
|
||||
thresholdAlarmService ThresholdAlarmService,
|
||||
) DeviceService {
|
||||
return &deviceService{
|
||||
ctx: ctx,
|
||||
deviceRepo: deviceRepo,
|
||||
areaControllerRepo: areaControllerRepo,
|
||||
deviceTemplateRepo: deviceTemplateRepo,
|
||||
deviceDomainSvc: deviceDomainSvc,
|
||||
ctx: ctx,
|
||||
deviceRepo: deviceRepo,
|
||||
areaControllerRepo: areaControllerRepo,
|
||||
deviceTemplateRepo: deviceTemplateRepo,
|
||||
deviceDomainSvc: deviceDomainSvc,
|
||||
thresholdAlarmService: thresholdAlarmService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +109,7 @@ func (s *deviceService) CreateDevice(ctx context.Context, req *dto.CreateDeviceR
|
||||
return dto.NewDeviceResponse(createdDevice)
|
||||
}
|
||||
|
||||
func (s *deviceService) GetDevice(ctx context.Context, id uint) (*dto.DeviceResponse, error) {
|
||||
func (s *deviceService) GetDevice(ctx context.Context, id uint32) (*dto.DeviceResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDevice")
|
||||
device, err := s.deviceRepo.FindByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -124,7 +127,7 @@ func (s *deviceService) ListDevices(ctx context.Context) ([]*dto.DeviceResponse,
|
||||
return dto.NewListDeviceResponse(devices)
|
||||
}
|
||||
|
||||
func (s *deviceService) UpdateDevice(ctx context.Context, id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) {
|
||||
func (s *deviceService) UpdateDevice(ctx context.Context, id uint32, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateDevice")
|
||||
existingDevice, err := s.deviceRepo.FindByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -158,7 +161,7 @@ func (s *deviceService) UpdateDevice(ctx context.Context, id uint, req *dto.Upda
|
||||
return dto.NewDeviceResponse(updatedDevice)
|
||||
}
|
||||
|
||||
func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error {
|
||||
func (s *deviceService) DeleteDevice(ctx context.Context, id uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteDevice")
|
||||
|
||||
// 检查设备是否存在
|
||||
@@ -168,7 +171,7 @@ func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
// 在删除前检查设备是否被任务使用
|
||||
inUse, err := s.deviceRepo.IsDeviceInUse(serviceCtx, id)
|
||||
inUse, err := s.deviceRepo.IsDeviceInUse(serviceCtx, id, []models.TaskType{models.TaskTypeDeviceThresholdCheck})
|
||||
if err != nil {
|
||||
// 如果检查过程中发生数据库错误,则返回错误
|
||||
return fmt.Errorf("检查设备使用情况失败: %w", err)
|
||||
@@ -178,11 +181,17 @@ func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error {
|
||||
return ErrDeviceInUse
|
||||
}
|
||||
|
||||
// TODO 这个应该用事务处理
|
||||
err = s.thresholdAlarmService.DeleteDeviceThresholdAlarmByDeviceID(serviceCtx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除设备阈值告警失败: %w", err)
|
||||
}
|
||||
|
||||
// 只有在未被使用时,才执行删除操作
|
||||
return s.deviceRepo.Delete(serviceCtx, id)
|
||||
}
|
||||
|
||||
func (s *deviceService) ManualControl(ctx context.Context, id uint, req *dto.ManualControlDeviceRequest) error {
|
||||
func (s *deviceService) ManualControl(ctx context.Context, id uint32, req *dto.ManualControlDeviceRequest) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ManualControl")
|
||||
dev, err := s.deviceRepo.FindByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -232,7 +241,7 @@ func (s *deviceService) CreateAreaController(ctx context.Context, req *dto.Creat
|
||||
return dto.NewAreaControllerResponse(ac)
|
||||
}
|
||||
|
||||
func (s *deviceService) GetAreaController(ctx context.Context, id uint) (*dto.AreaControllerResponse, error) {
|
||||
func (s *deviceService) GetAreaController(ctx context.Context, id uint32) (*dto.AreaControllerResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetAreaController")
|
||||
ac, err := s.areaControllerRepo.FindByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -250,7 +259,7 @@ func (s *deviceService) ListAreaControllers(ctx context.Context) ([]*dto.AreaCon
|
||||
return dto.NewListAreaControllerResponse(acs)
|
||||
}
|
||||
|
||||
func (s *deviceService) UpdateAreaController(ctx context.Context, id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) {
|
||||
func (s *deviceService) UpdateAreaController(ctx context.Context, id uint32, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateAreaController")
|
||||
existingAC, err := s.areaControllerRepo.FindByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -278,7 +287,7 @@ func (s *deviceService) UpdateAreaController(ctx context.Context, id uint, req *
|
||||
return dto.NewAreaControllerResponse(existingAC)
|
||||
}
|
||||
|
||||
func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error {
|
||||
func (s *deviceService) DeleteAreaController(ctx context.Context, id uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteAreaController")
|
||||
|
||||
// 1. 检查是否存在
|
||||
@@ -288,7 +297,7 @@ func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error
|
||||
}
|
||||
|
||||
// 2. 检查是否被使用(业务逻辑)
|
||||
inUse, err := s.deviceRepo.IsAreaControllerInUse(serviceCtx, id)
|
||||
inUse, err := s.areaControllerRepo.IsAreaControllerUsedByTasks(serviceCtx, id, []models.TaskType{models.TaskTypeAreaCollectorThresholdCheck})
|
||||
if err != nil {
|
||||
return err // 返回数据库检查错误
|
||||
}
|
||||
@@ -296,6 +305,12 @@ func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error
|
||||
return ErrAreaControllerInUse // 返回业务错误
|
||||
}
|
||||
|
||||
// TODO 这个应该用事务处理
|
||||
err = s.thresholdAlarmService.DeleteAreaThresholdAlarmByAreaControllerID(serviceCtx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除区域阈值告警失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 执行删除
|
||||
return s.areaControllerRepo.Delete(serviceCtx, id)
|
||||
}
|
||||
@@ -334,7 +349,7 @@ func (s *deviceService) CreateDeviceTemplate(ctx context.Context, req *dto.Creat
|
||||
return dto.NewDeviceTemplateResponse(deviceTemplate)
|
||||
}
|
||||
|
||||
func (s *deviceService) GetDeviceTemplate(ctx context.Context, id uint) (*dto.DeviceTemplateResponse, error) {
|
||||
func (s *deviceService) GetDeviceTemplate(ctx context.Context, id uint32) (*dto.DeviceTemplateResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDeviceTemplate")
|
||||
deviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -352,7 +367,7 @@ func (s *deviceService) ListDeviceTemplates(ctx context.Context) ([]*dto.DeviceT
|
||||
return dto.NewListDeviceTemplateResponse(deviceTemplates)
|
||||
}
|
||||
|
||||
func (s *deviceService) UpdateDeviceTemplate(ctx context.Context, id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) {
|
||||
func (s *deviceService) UpdateDeviceTemplate(ctx context.Context, id uint32, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateDeviceTemplate")
|
||||
existingDeviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -387,7 +402,7 @@ func (s *deviceService) UpdateDeviceTemplate(ctx context.Context, id uint, req *
|
||||
return dto.NewDeviceTemplateResponse(existingDeviceTemplate)
|
||||
}
|
||||
|
||||
func (s *deviceService) DeleteDeviceTemplate(ctx context.Context, id uint) error {
|
||||
func (s *deviceService) DeleteDeviceTemplate(ctx context.Context, id uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteDeviceTemplate")
|
||||
|
||||
// 1. 检查是否存在
|
||||
|
||||
@@ -147,7 +147,7 @@ func (s *monitorService) ListPlanExecutionLogs(ctx context.Context, req *dto.Lis
|
||||
return nil, err
|
||||
}
|
||||
|
||||
planIds := make([]uint, 0, len(planLogs))
|
||||
planIds := make([]uint32, 0, len(planLogs))
|
||||
for _, datum := range planLogs {
|
||||
has := false
|
||||
for _, id := range planIds {
|
||||
|
||||
@@ -12,35 +12,35 @@ import (
|
||||
|
||||
// PigBatchService 接口定义保持不变,继续作为应用层对外的契约。
|
||||
type PigBatchService interface {
|
||||
CreatePigBatch(ctx context.Context, operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error)
|
||||
GetPigBatch(ctx context.Context, id uint) (*dto.PigBatchResponseDTO, error)
|
||||
UpdatePigBatch(ctx context.Context, id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error)
|
||||
DeletePigBatch(ctx context.Context, id uint) error
|
||||
CreatePigBatch(ctx context.Context, operatorID uint32, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error)
|
||||
GetPigBatch(ctx context.Context, id uint32) (*dto.PigBatchResponseDTO, error)
|
||||
UpdatePigBatch(ctx context.Context, id uint32, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error)
|
||||
DeletePigBatch(ctx context.Context, id uint32) error
|
||||
ListPigBatches(ctx context.Context, isActive *bool) ([]*dto.PigBatchResponseDTO, error)
|
||||
|
||||
// Pig Pen Management
|
||||
AssignEmptyPensToBatch(ctx context.Context, batchID uint, penIDs []uint, operatorID uint) error
|
||||
ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
|
||||
RemoveEmptyPenFromBatch(ctx context.Context, batchID uint, penID uint) error
|
||||
MovePigsIntoPen(ctx context.Context, batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
|
||||
AssignEmptyPensToBatch(ctx context.Context, batchID uint32, penIDs []uint32, operatorID uint32) error
|
||||
ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint32, toBatchID uint32, penID uint32, operatorID uint32, remarks string) error
|
||||
RemoveEmptyPenFromBatch(ctx context.Context, batchID uint32, penID uint32) error
|
||||
MovePigsIntoPen(ctx context.Context, batchID uint32, toPenID uint32, quantity int, operatorID uint32, remarks string) error
|
||||
|
||||
// Trade Sub-service
|
||||
SellPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
|
||||
BuyPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
|
||||
SellPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, tatalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error
|
||||
BuyPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, tatalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error
|
||||
|
||||
// Transfer Sub-service
|
||||
TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
|
||||
TransferPigsWithinBatch(ctx context.Context, batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
|
||||
TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint32, destBatchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error
|
||||
TransferPigsWithinBatch(ctx context.Context, batchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error
|
||||
|
||||
// Sick Pig Management
|
||||
RecordSickPigs(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigRecovery(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigs(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigRecovery(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigDeath(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigCull(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
|
||||
// Normal Pig Management
|
||||
RecordDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
|
||||
RecordCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
|
||||
RecordDeath(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, happenedAt time.Time, remarks string) error
|
||||
RecordCull(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, happenedAt time.Time, remarks string) error
|
||||
}
|
||||
|
||||
// pigBatchService 的实现现在依赖于领域服务接口。
|
||||
@@ -79,7 +79,7 @@ func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch, currentT
|
||||
}
|
||||
|
||||
// CreatePigBatch 现在将请求委托给领域服务处理。
|
||||
func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint32, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreatePigBatch")
|
||||
// 1. DTO -> 领域模型
|
||||
batch := &models.PigBatch{
|
||||
@@ -102,7 +102,7 @@ func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint, d
|
||||
}
|
||||
|
||||
// GetPigBatch 从领域服务获取数据并转换为DTO,同时处理错误转换。
|
||||
func (s *pigBatchService) GetPigBatch(ctx context.Context, id uint) (*dto.PigBatchResponseDTO, error) {
|
||||
func (s *pigBatchService) GetPigBatch(ctx context.Context, id uint32) (*dto.PigBatchResponseDTO, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPigBatch")
|
||||
batch, err := s.domainService.GetPigBatch(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -123,7 +123,7 @@ func (s *pigBatchService) GetPigBatch(ctx context.Context, id uint) (*dto.PigBat
|
||||
}
|
||||
|
||||
// UpdatePigBatch 协调获取、更新和保存的流程,并处理错误转换。
|
||||
func (s *pigBatchService) UpdatePigBatch(ctx context.Context, id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
func (s *pigBatchService) UpdatePigBatch(ctx context.Context, id uint32, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePigBatch")
|
||||
// 1. 先获取最新的领域模型
|
||||
existingBatch, err := s.domainService.GetPigBatch(serviceCtx, id)
|
||||
@@ -176,7 +176,7 @@ func (s *pigBatchService) UpdatePigBatch(ctx context.Context, id uint, dto *dto.
|
||||
}
|
||||
|
||||
// DeletePigBatch 将删除操作委托给领域服务,并转换领域错误为应用层错误。
|
||||
func (s *pigBatchService) DeletePigBatch(ctx context.Context, id uint) error {
|
||||
func (s *pigBatchService) DeletePigBatch(ctx context.Context, id uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeletePigBatch")
|
||||
err := s.domainService.DeletePigBatch(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -214,7 +214,7 @@ func (s *pigBatchService) ListPigBatches(ctx context.Context, isActive *bool) ([
|
||||
}
|
||||
|
||||
// AssignEmptyPensToBatch 委托给领域服务
|
||||
func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID uint, penIDs []uint, operatorID uint) error {
|
||||
func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID uint32, penIDs []uint32, operatorID uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "AssignEmptyPensToBatch")
|
||||
err := s.domainService.AssignEmptyPensToBatch(serviceCtx, batchID, penIDs, operatorID)
|
||||
if err != nil {
|
||||
@@ -225,7 +225,7 @@ func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID ui
|
||||
}
|
||||
|
||||
// ReclassifyPenToNewBatch 委托给领域服务
|
||||
func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint32, toBatchID uint32, penID uint32, operatorID uint32, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "ReclassifyPenToNewBatch")
|
||||
err := s.domainService.ReclassifyPenToNewBatch(serviceCtx, fromBatchID, toBatchID, penID, operatorID, remarks)
|
||||
if err != nil {
|
||||
@@ -236,7 +236,7 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatch
|
||||
}
|
||||
|
||||
// RemoveEmptyPenFromBatch 委托给领域服务
|
||||
func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID uint, penID uint) error {
|
||||
func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID uint32, penID uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "RemoveEmptyPenFromBatch")
|
||||
err := s.domainService.RemoveEmptyPenFromBatch(serviceCtx, batchID, penID)
|
||||
if err != nil {
|
||||
@@ -247,7 +247,7 @@ func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID u
|
||||
}
|
||||
|
||||
// MovePigsIntoPen 委托给领域服务
|
||||
func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint32, toPenID uint32, quantity int, operatorID uint32, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "MovePigsIntoPen")
|
||||
err := s.domainService.MovePigsIntoPen(serviceCtx, batchID, toPenID, quantity, operatorID, remarks)
|
||||
if err != nil {
|
||||
@@ -258,7 +258,7 @@ func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint, toP
|
||||
}
|
||||
|
||||
// SellPigs 委托给领域服务
|
||||
func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
|
||||
func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, tatalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "SellPigs")
|
||||
err := s.domainService.SellPigs(serviceCtx, batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
|
||||
if err != nil {
|
||||
@@ -269,7 +269,7 @@ func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint, penID uint
|
||||
}
|
||||
|
||||
// BuyPigs 委托给领域服务
|
||||
func (s *pigBatchService) BuyPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
|
||||
func (s *pigBatchService) BuyPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, tatalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "BuyPigs")
|
||||
err := s.domainService.BuyPigs(serviceCtx, batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
|
||||
if err != nil {
|
||||
@@ -280,7 +280,7 @@ func (s *pigBatchService) BuyPigs(ctx context.Context, batchID uint, penID uint,
|
||||
}
|
||||
|
||||
// TransferPigsAcrossBatches 委托给领域服务
|
||||
func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint32, destBatchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "TransferPigsAcrossBatches")
|
||||
err := s.domainService.TransferPigsAcrossBatches(serviceCtx, sourceBatchID, destBatchID, fromPenID, toPenID, quantity, operatorID, remarks)
|
||||
if err != nil {
|
||||
@@ -291,7 +291,7 @@ func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceB
|
||||
}
|
||||
|
||||
// TransferPigsWithinBatch 委托给领域服务
|
||||
func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "TransferPigsWithinBatch")
|
||||
err := s.domainService.TransferPigsWithinBatch(serviceCtx, batchID, fromPenID, toPenID, quantity, operatorID, remarks)
|
||||
if err != nil {
|
||||
@@ -302,7 +302,7 @@ func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID u
|
||||
}
|
||||
|
||||
// RecordSickPigs 委托给领域服务
|
||||
func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordSickPigs")
|
||||
err := s.domainService.RecordSickPigs(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
|
||||
if err != nil {
|
||||
@@ -313,7 +313,7 @@ func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint, b
|
||||
}
|
||||
|
||||
// RecordSickPigRecovery 委托给领域服务
|
||||
func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordSickPigRecovery")
|
||||
err := s.domainService.RecordSickPigRecovery(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
|
||||
if err != nil {
|
||||
@@ -324,7 +324,7 @@ func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID
|
||||
}
|
||||
|
||||
// RecordSickPigDeath 委托给领域服务
|
||||
func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordSickPigDeath")
|
||||
err := s.domainService.RecordSickPigDeath(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
|
||||
if err != nil {
|
||||
@@ -335,7 +335,7 @@ func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uin
|
||||
}
|
||||
|
||||
// RecordSickPigCull 委托给领域服务
|
||||
func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordSickPigCull")
|
||||
err := s.domainService.RecordSickPigCull(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
|
||||
if err != nil {
|
||||
@@ -346,7 +346,7 @@ func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint
|
||||
}
|
||||
|
||||
// RecordDeath 委托给领域服务
|
||||
func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordDeath")
|
||||
err := s.domainService.RecordDeath(serviceCtx, operatorID, batchID, penID, quantity, happenedAt, remarks)
|
||||
if err != nil {
|
||||
@@ -357,7 +357,7 @@ func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint, batc
|
||||
}
|
||||
|
||||
// RecordCull 委托给领域服务
|
||||
func (s *pigBatchService) RecordCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordCull(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordCull")
|
||||
err := s.domainService.RecordCull(serviceCtx, operatorID, batchID, penID, quantity, happenedAt, remarks)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,19 +18,19 @@ import (
|
||||
type PigFarmService interface {
|
||||
// PigHouse methods
|
||||
CreatePigHouse(ctx context.Context, name, description string) (*dto.PigHouseResponse, error)
|
||||
GetPigHouseByID(ctx context.Context, id uint) (*dto.PigHouseResponse, error)
|
||||
GetPigHouseByID(ctx context.Context, id uint32) (*dto.PigHouseResponse, error)
|
||||
ListPigHouses(ctx context.Context) ([]dto.PigHouseResponse, error)
|
||||
UpdatePigHouse(ctx context.Context, id uint, name, description string) (*dto.PigHouseResponse, error)
|
||||
DeletePigHouse(ctx context.Context, id uint) error
|
||||
UpdatePigHouse(ctx context.Context, id uint32, name, description string) (*dto.PigHouseResponse, error)
|
||||
DeletePigHouse(ctx context.Context, id uint32) error
|
||||
|
||||
// Pen methods
|
||||
CreatePen(ctx context.Context, penNumber string, houseID uint, capacity int) (*dto.PenResponse, error)
|
||||
GetPenByID(ctx context.Context, id uint) (*dto.PenResponse, error)
|
||||
CreatePen(ctx context.Context, penNumber string, houseID uint32, capacity int) (*dto.PenResponse, error)
|
||||
GetPenByID(ctx context.Context, id uint32) (*dto.PenResponse, error)
|
||||
ListPens(ctx context.Context) ([]*dto.PenResponse, error)
|
||||
UpdatePen(ctx context.Context, id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error)
|
||||
DeletePen(ctx context.Context, id uint) error
|
||||
UpdatePen(ctx context.Context, id uint32, penNumber string, houseID uint32, capacity int, status models.PenStatus) (*dto.PenResponse, error)
|
||||
DeletePen(ctx context.Context, id uint32) error
|
||||
// UpdatePenStatus 更新猪栏状态
|
||||
UpdatePenStatus(ctx context.Context, id uint, newStatus models.PenStatus) (*dto.PenResponse, error)
|
||||
UpdatePenStatus(ctx context.Context, id uint32, newStatus models.PenStatus) (*dto.PenResponse, error)
|
||||
}
|
||||
|
||||
type pigFarmService struct {
|
||||
@@ -79,7 +79,7 @@ func (s *pigFarmService) CreatePigHouse(ctx context.Context, name, description s
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) GetPigHouseByID(ctx context.Context, id uint) (*dto.PigHouseResponse, error) {
|
||||
func (s *pigFarmService) GetPigHouseByID(ctx context.Context, id uint32) (*dto.PigHouseResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigHouseByID")
|
||||
house, err := s.farmRepository.GetPigHouseByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -109,10 +109,10 @@ func (s *pigFarmService) ListPigHouses(ctx context.Context) ([]dto.PigHouseRespo
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) UpdatePigHouse(ctx context.Context, id uint, name, description string) (*dto.PigHouseResponse, error) {
|
||||
func (s *pigFarmService) UpdatePigHouse(ctx context.Context, id uint32, name, description string) (*dto.PigHouseResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigHouse")
|
||||
house := &models.PigHouse{
|
||||
Model: gorm.Model{ID: id},
|
||||
Model: models.Model{ID: id},
|
||||
Name: name,
|
||||
Description: description,
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func (s *pigFarmService) UpdatePigHouse(ctx context.Context, id uint, name, desc
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) DeletePigHouse(ctx context.Context, id uint) error {
|
||||
func (s *pigFarmService) DeletePigHouse(ctx context.Context, id uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigHouse")
|
||||
// 业务逻辑:检查猪舍是否包含猪栏
|
||||
penCount, err := s.farmRepository.CountPensInHouse(serviceCtx, id)
|
||||
@@ -159,7 +159,7 @@ func (s *pigFarmService) DeletePigHouse(ctx context.Context, id uint) error {
|
||||
|
||||
// --- Pen Implementation ---
|
||||
|
||||
func (s *pigFarmService) CreatePen(ctx context.Context, penNumber string, houseID uint, capacity int) (*dto.PenResponse, error) {
|
||||
func (s *pigFarmService) CreatePen(ctx context.Context, penNumber string, houseID uint32, capacity int) (*dto.PenResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePen")
|
||||
// 业务逻辑:验证所属猪舍是否存在
|
||||
_, err := s.farmRepository.GetPigHouseByID(serviceCtx, houseID)
|
||||
@@ -189,7 +189,7 @@ func (s *pigFarmService) CreatePen(ctx context.Context, penNumber string, houseI
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) GetPenByID(ctx context.Context, id uint) (*dto.PenResponse, error) {
|
||||
func (s *pigFarmService) GetPenByID(ctx context.Context, id uint32) (*dto.PenResponse, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPenByID")
|
||||
pen, err := s.penRepository.GetPenByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -251,7 +251,7 @@ func (s *pigFarmService) ListPens(ctx context.Context) ([]*dto.PenResponse, erro
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) UpdatePen(ctx context.Context, id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error) {
|
||||
func (s *pigFarmService) UpdatePen(ctx context.Context, id uint32, penNumber string, houseID uint32, capacity int, status models.PenStatus) (*dto.PenResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePen")
|
||||
// 业务逻辑:验证所属猪舍是否存在
|
||||
_, err := s.farmRepository.GetPigHouseByID(serviceCtx, houseID)
|
||||
@@ -263,7 +263,7 @@ func (s *pigFarmService) UpdatePen(ctx context.Context, id uint, penNumber strin
|
||||
}
|
||||
|
||||
pen := &models.Pen{
|
||||
Model: gorm.Model{ID: id},
|
||||
Model: models.Model{ID: id},
|
||||
PenNumber: penNumber,
|
||||
HouseID: houseID,
|
||||
Capacity: capacity,
|
||||
@@ -291,7 +291,7 @@ func (s *pigFarmService) UpdatePen(ctx context.Context, id uint, penNumber strin
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pigFarmService) DeletePen(ctx context.Context, id uint) error {
|
||||
func (s *pigFarmService) DeletePen(ctx context.Context, id uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePen")
|
||||
// 业务逻辑:检查猪栏是否被活跃批次使用
|
||||
pen, err := s.penRepository.GetPenByID(serviceCtx, id)
|
||||
@@ -327,7 +327,7 @@ func (s *pigFarmService) DeletePen(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
// UpdatePenStatus 更新猪栏状态
|
||||
func (s *pigFarmService) UpdatePenStatus(ctx context.Context, id uint, newStatus models.PenStatus) (*dto.PenResponse, error) {
|
||||
func (s *pigFarmService) UpdatePenStatus(ctx context.Context, id uint32, newStatus models.PenStatus) (*dto.PenResponse, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePenStatus")
|
||||
var updatedPen *models.Pen
|
||||
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"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/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
@@ -15,17 +16,17 @@ type PlanService interface {
|
||||
// CreatePlan 创建一个新的计划
|
||||
CreatePlan(ctx context.Context, req *dto.CreatePlanRequest) (*dto.PlanResponse, error)
|
||||
// GetPlanByID 根据ID获取计划详情
|
||||
GetPlanByID(ctx context.Context, id uint) (*dto.PlanResponse, error)
|
||||
GetPlanByID(ctx context.Context, id uint32) (*dto.PlanResponse, error)
|
||||
// ListPlans 获取计划列表,支持过滤和分页
|
||||
ListPlans(ctx context.Context, query *dto.ListPlansQuery) (*dto.ListPlansResponse, error)
|
||||
// UpdatePlan 更新计划
|
||||
UpdatePlan(ctx context.Context, id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error)
|
||||
UpdatePlan(ctx context.Context, id uint32, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error)
|
||||
// DeletePlan 删除计划(软删除)
|
||||
DeletePlan(ctx context.Context, id uint) error
|
||||
DeletePlan(ctx context.Context, id uint32) error
|
||||
// StartPlan 启动计划
|
||||
StartPlan(ctx context.Context, id uint) error
|
||||
StartPlan(ctx context.Context, id uint32) error
|
||||
// StopPlan 停止计划
|
||||
StopPlan(ctx context.Context, id uint) error
|
||||
StopPlan(ctx context.Context, id uint32) error
|
||||
}
|
||||
|
||||
// planService 是 PlanService 接口的实现
|
||||
@@ -76,7 +77,7 @@ func (s *planService) CreatePlan(ctx context.Context, req *dto.CreatePlanRequest
|
||||
}
|
||||
|
||||
// GetPlanByID 根据ID获取计划详情
|
||||
func (s *planService) GetPlanByID(ctx context.Context, id uint) (*dto.PlanResponse, error) {
|
||||
func (s *planService) GetPlanByID(ctx context.Context, id uint32) (*dto.PlanResponse, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPlanByID")
|
||||
const actionType = "应用服务层:获取计划详情"
|
||||
|
||||
@@ -134,7 +135,7 @@ func (s *planService) ListPlans(ctx context.Context, query *dto.ListPlansQuery)
|
||||
}
|
||||
|
||||
// UpdatePlan 更新计划
|
||||
func (s *planService) UpdatePlan(ctx context.Context, id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error) {
|
||||
func (s *planService) UpdatePlan(ctx context.Context, id uint32, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePlan")
|
||||
const actionType = "应用服务层:更新计划"
|
||||
|
||||
@@ -147,7 +148,7 @@ func (s *planService) UpdatePlan(ctx context.Context, id uint, req *dto.UpdatePl
|
||||
planToUpdate.ID = id // 确保ID被设置
|
||||
|
||||
// 调用领域服务更新计划
|
||||
updatedPlan, err := s.domainPlanService.UpdatePlan(serviceCtx, planToUpdate)
|
||||
updatedPlan, err := s.domainPlanService.UpdatePlan(serviceCtx, planToUpdate, models.PlanTypeCustom)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 领域服务更新计划失败: %v, ID: %d", actionType, err, id)
|
||||
return nil, err // 直接返回领域层错误
|
||||
@@ -165,7 +166,7 @@ func (s *planService) UpdatePlan(ctx context.Context, id uint, req *dto.UpdatePl
|
||||
}
|
||||
|
||||
// DeletePlan 删除计划(软删除)
|
||||
func (s *planService) DeletePlan(ctx context.Context, id uint) error {
|
||||
func (s *planService) DeletePlan(ctx context.Context, id uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeletePlan")
|
||||
const actionType = "应用服务层:删除计划"
|
||||
|
||||
@@ -181,7 +182,7 @@ func (s *planService) DeletePlan(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
// StartPlan 启动计划
|
||||
func (s *planService) StartPlan(ctx context.Context, id uint) error {
|
||||
func (s *planService) StartPlan(ctx context.Context, id uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "StartPlan")
|
||||
const actionType = "应用服务层:启动计划"
|
||||
|
||||
@@ -197,7 +198,7 @@ func (s *planService) StartPlan(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
// StopPlan 停止计划
|
||||
func (s *planService) StopPlan(ctx context.Context, id uint) error {
|
||||
func (s *planService) StopPlan(ctx context.Context, id uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "StopPlan")
|
||||
const actionType = "应用服务层:停止计划"
|
||||
|
||||
|
||||
688
internal/app/service/threshold_alarm_service.go
Normal file
688
internal/app/service/threshold_alarm_service.go
Normal file
@@ -0,0 +1,688 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
|
||||
domainAlarm "git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm"
|
||||
"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/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// ThresholdAlarmService 定义了阈值告警配置服务的接口。
|
||||
// 该服务负责管理阈值告警任务的配置,并将其与计划进行联动。
|
||||
type ThresholdAlarmService interface {
|
||||
// SnoozeThresholdAlarm 忽略一个阈值告警,或更新其忽略时间。
|
||||
SnoozeThresholdAlarm(ctx context.Context, alarmID uint32, durationMinutes uint32) error
|
||||
// CancelSnoozeThresholdAlarm 取消对一个阈值告警的忽略状态。
|
||||
CancelSnoozeThresholdAlarm(ctx context.Context, alarmID uint32) error
|
||||
// ListActiveAlarms 批量查询活跃告警。
|
||||
ListActiveAlarms(ctx context.Context, req *dto.ListActiveAlarmRequest) (*dto.ListActiveAlarmResponse, error)
|
||||
// ListHistoricalAlarms 批量查询历史告警。
|
||||
ListHistoricalAlarms(ctx context.Context, req *dto.ListHistoricalAlarmRequest) (*dto.ListHistoricalAlarmResponse, error)
|
||||
|
||||
// CreateDeviceThresholdAlarm 创建一个设备阈值告警。
|
||||
CreateDeviceThresholdAlarm(ctx context.Context, req *dto.CreateDeviceThresholdAlarmDTO) error
|
||||
// UpdateDeviceThresholdAlarm 更新一个设备阈值告警。
|
||||
UpdateDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.UpdateDeviceThresholdAlarmDTO) error
|
||||
// GetDeviceThresholdAlarm 根据ID获取一个设备阈值告警任务。
|
||||
GetDeviceThresholdAlarm(ctx context.Context, taskID int) (*dto.DeviceThresholdAlarmDTO, error)
|
||||
// DeleteDeviceThresholdAlarm 删除一个设备阈值告警。
|
||||
DeleteDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.DeleteDeviceThresholdAlarmDTO) error
|
||||
// DeleteDeviceThresholdAlarmByDeviceID 实现了根据设备ID删除一个设备下所有设备阈值告警的逻辑。
|
||||
DeleteDeviceThresholdAlarmByDeviceID(ctx context.Context, deviceID uint32) error
|
||||
|
||||
// CreateAreaThresholdAlarm 创建一个区域阈值告警。
|
||||
CreateAreaThresholdAlarm(ctx context.Context, req *dto.CreateAreaThresholdAlarmDTO) error
|
||||
// UpdateAreaThresholdAlarm 更新一个区域阈值告警。
|
||||
UpdateAreaThresholdAlarm(ctx context.Context, taskID int, req *dto.UpdateAreaThresholdAlarmDTO) error
|
||||
// GetAreaThresholdAlarm 根据ID获取一个区域阈值告警任务。
|
||||
GetAreaThresholdAlarm(ctx context.Context, taskID int) (*dto.AreaThresholdAlarmDTO, error)
|
||||
// DeleteAreaThresholdAlarm 实现了删除一个区域阈值告警的逻辑。
|
||||
DeleteAreaThresholdAlarm(ctx context.Context, taskID int) error
|
||||
// DeleteAreaThresholdAlarmByAreaControllerID 实现了根据区域ID删除一个区域下所有区域阈值告警的逻辑。
|
||||
DeleteAreaThresholdAlarmByAreaControllerID(ctx context.Context, areaControllerID uint32) error
|
||||
}
|
||||
|
||||
// thresholdAlarmService 是 ThresholdAlarmService 接口的具体实现。
|
||||
type thresholdAlarmService struct {
|
||||
ctx context.Context
|
||||
alarmService domainAlarm.AlarmService
|
||||
planService plan.Service
|
||||
|
||||
alarmRepo repository.AlarmRepository
|
||||
planRepo repository.PlanRepository
|
||||
areaRepo repository.AreaControllerRepository
|
||||
deviceRepo repository.DeviceRepository
|
||||
}
|
||||
|
||||
// NewThresholdAlarmService 创建一个新的 ThresholdAlarmService 实例。
|
||||
func NewThresholdAlarmService(ctx context.Context,
|
||||
alarmService domainAlarm.AlarmService,
|
||||
planService plan.Service,
|
||||
alarmRepo repository.AlarmRepository,
|
||||
planRepo repository.PlanRepository,
|
||||
areaRepo repository.AreaControllerRepository,
|
||||
deviceRepo repository.DeviceRepository,
|
||||
) ThresholdAlarmService {
|
||||
return &thresholdAlarmService{
|
||||
ctx: ctx,
|
||||
alarmService: alarmService,
|
||||
planService: planService,
|
||||
alarmRepo: alarmRepo,
|
||||
planRepo: planRepo,
|
||||
areaRepo: areaRepo,
|
||||
deviceRepo: deviceRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// SnoozeThresholdAlarm 实现了忽略阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) SnoozeThresholdAlarm(ctx context.Context, alarmID uint32, durationMinutes uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "SnoozeThresholdAlarm")
|
||||
return s.alarmService.SnoozeAlarm(serviceCtx, alarmID, time.Duration(durationMinutes)*time.Minute)
|
||||
}
|
||||
|
||||
// CancelSnoozeThresholdAlarm 实现了取消忽略阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) CancelSnoozeThresholdAlarm(ctx context.Context, alarmID uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CancelSnoozeThresholdAlarm")
|
||||
return s.alarmService.CancelAlarmSnooze(serviceCtx, alarmID)
|
||||
}
|
||||
|
||||
// ListActiveAlarms 实现了批量查询活跃告警的逻辑。
|
||||
func (s *thresholdAlarmService) ListActiveAlarms(ctx context.Context, req *dto.ListActiveAlarmRequest) (*dto.ListActiveAlarmResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListActiveAlarms")
|
||||
|
||||
opts := repository.ActiveAlarmListOptions{
|
||||
SourceType: req.SourceType,
|
||||
SourceID: req.SourceID,
|
||||
Level: req.Level,
|
||||
IsIgnored: req.IsIgnored,
|
||||
TriggerTime: req.TriggerTime,
|
||||
EndTime: req.EndTime,
|
||||
OrderBy: req.OrderBy,
|
||||
}
|
||||
|
||||
alarms, total, err := s.alarmRepo.ListActiveAlarms(serviceCtx, opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListActiveAlarmResponse(alarms, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// ListHistoricalAlarms 实现了批量查询历史告警的逻辑。
|
||||
func (s *thresholdAlarmService) ListHistoricalAlarms(ctx context.Context, req *dto.ListHistoricalAlarmRequest) (*dto.ListHistoricalAlarmResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListHistoricalAlarms")
|
||||
|
||||
opts := repository.HistoricalAlarmListOptions{
|
||||
SourceType: req.SourceType,
|
||||
SourceID: req.SourceID,
|
||||
Level: req.Level,
|
||||
TriggerTimeStart: req.TriggerTimeStart,
|
||||
TriggerTimeEnd: req.TriggerTimeEnd,
|
||||
ResolveTimeStart: req.ResolveTimeStart,
|
||||
ResolveTimeEnd: req.ResolveTimeEnd,
|
||||
OrderBy: req.OrderBy,
|
||||
}
|
||||
|
||||
alarms, total, err := s.alarmRepo.ListHistoricalAlarms(serviceCtx, opts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewListHistoricalAlarmResponse(alarms, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// CreateDeviceThresholdAlarm 实现了创建一个设备阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) CreateDeviceThresholdAlarm(ctx context.Context, req *dto.CreateDeviceThresholdAlarmDTO) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreateThresholdAlarm")
|
||||
|
||||
device, err := s.deviceRepo.FindByID(serviceCtx, req.DeviceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取设备 %v 失败: %v", req.DeviceID, err)
|
||||
}
|
||||
|
||||
plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取系统计划 %v 失败: %v", models.PlanNamePeriodicSystemHealthCheck, err)
|
||||
}
|
||||
if plan == nil {
|
||||
logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck)
|
||||
}
|
||||
|
||||
// 系统计划肯定是子任务
|
||||
for i, t := range plan.Tasks {
|
||||
switch t.Type {
|
||||
case models.TaskTypeDeviceThresholdCheck: // 检查任务是否存在
|
||||
var params task.DeviceThresholdCheckParams
|
||||
err = t.ParseParameters(¶ms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("任务 %v: 解析设备阈值检查任务参数失败: %v", t.ID, err)
|
||||
}
|
||||
if params.DeviceID == req.DeviceID && params.SensorType == req.SensorType {
|
||||
return fmt.Errorf("设备 %v: 该设备已存在阈值检查任务", req.DeviceID)
|
||||
}
|
||||
case models.TaskTypeAreaCollectorThresholdCheck: // 向区域阈值检查任务过滤列表中添加该设备
|
||||
params := task.AreaThresholdCheckParams{
|
||||
ExcludeDeviceIDs: []uint32{},
|
||||
}
|
||||
err = t.ParseParameters(¶ms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("任务 %v: 解析区域阈值检查任务参数失败: %v", t.ID, err)
|
||||
}
|
||||
if params.AreaControllerID == device.AreaControllerID {
|
||||
has := false
|
||||
for _, d := range params.ExcludeDeviceIDs {
|
||||
if d == req.DeviceID {
|
||||
has = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !has {
|
||||
params.ExcludeDeviceIDs = append(params.ExcludeDeviceIDs, req.DeviceID)
|
||||
err = plan.Tasks[i].SaveParameters(params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("任务 %v: 保存任务参数失败: %v", t.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
t := models.Task{
|
||||
PlanID: plan.ID,
|
||||
Name: fmt.Sprintf("设备 %v 的阈值检测任务", req.DeviceID),
|
||||
Description: fmt.Sprintf("检测该设备 %v 是否 %v %v", req.SensorType, req.Operator, req.Thresholds),
|
||||
ExecutionOrder: len(plan.Tasks) + 1,
|
||||
Type: models.TaskTypeDeviceThresholdCheck,
|
||||
}
|
||||
err = t.SaveParameters(task.DeviceThresholdCheckParams{
|
||||
DeviceID: req.DeviceID,
|
||||
SensorType: req.SensorType,
|
||||
Thresholds: req.Thresholds,
|
||||
Level: req.Level,
|
||||
Operator: req.Operator,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存任务参数失败: %v", err)
|
||||
}
|
||||
|
||||
plan.Tasks = append(plan.Tasks, t)
|
||||
plan.ReorderSteps()
|
||||
_, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新计划失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDeviceThresholdAlarm 实现了更新一个设备阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) UpdateDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.UpdateDeviceThresholdAlarmDTO) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdateDeviceThresholdAlarm")
|
||||
|
||||
// 1. 获取系统健康检查计划
|
||||
plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取系统健康检查计划失败: %w", err)
|
||||
}
|
||||
if plan == nil {
|
||||
// 这个系统计划必须存在
|
||||
logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck)
|
||||
}
|
||||
|
||||
// 2. 遍历任务列表,查找并更新目标任务
|
||||
taskFound := false
|
||||
for i, t := range plan.Tasks {
|
||||
if t.ID == taskID && t.Type == models.TaskTypeDeviceThresholdCheck {
|
||||
taskFound = true
|
||||
|
||||
var params task.DeviceThresholdCheckParams
|
||||
if err = t.ParseParameters(¶ms); err != nil {
|
||||
return fmt.Errorf("任务 %d: 解析现有参数失败: %w", taskID, err)
|
||||
}
|
||||
params.Thresholds = req.Thresholds
|
||||
params.Operator = req.Operator
|
||||
params.Level = req.Level
|
||||
// 刷新任务说明
|
||||
plan.Tasks[i].Description = fmt.Sprintf("检测该设备 %v 是否 %v %v", params.SensorType, params.Operator, params.Thresholds)
|
||||
|
||||
err = plan.Tasks[i].SaveParameters(params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("任务 %d: 保存参数失败: %w", taskID, err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !taskFound {
|
||||
return fmt.Errorf("任务 %d: 不存在", taskID)
|
||||
}
|
||||
|
||||
// 全量更新计划
|
||||
_, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDeviceThresholdAlarm 实现了根据ID获取一个设备阈值告警任务的逻辑。
|
||||
func (s *thresholdAlarmService) GetDeviceThresholdAlarm(ctx context.Context, taskID int) (*dto.DeviceThresholdAlarmDTO, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDeviceThresholdAlarm")
|
||||
|
||||
// 1. 使用 planRepo 查询任务
|
||||
t, err := s.planRepo.FindTaskByID(serviceCtx, taskID)
|
||||
if err != nil {
|
||||
return nil, err // 如果未找到或发生其他错误,直接返回
|
||||
}
|
||||
|
||||
// 2. 验证任务类型是否正确
|
||||
if t.Type != models.TaskTypeDeviceThresholdCheck {
|
||||
return nil, fmt.Errorf("任务 %d 不是一个设备阈值检查任务", taskID)
|
||||
}
|
||||
var params task.DeviceThresholdCheckParams
|
||||
err = t.ParseParameters(¶ms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("任务 %d: 解析参数失败: %w", taskID, err)
|
||||
}
|
||||
|
||||
resp := &dto.DeviceThresholdAlarmDTO{
|
||||
ID: t.ID,
|
||||
DeviceID: params.DeviceID,
|
||||
SensorType: params.SensorType,
|
||||
Thresholds: params.Thresholds,
|
||||
Operator: params.Operator,
|
||||
Level: params.Level,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteDeviceThresholdAlarm 实现了删除一个设备阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) DeleteDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.DeleteDeviceThresholdAlarmDTO) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteDeviceThresholdAlarm")
|
||||
|
||||
// 获取待删除任务并校验
|
||||
deleteTask, err := s.planRepo.FindTaskByID(serviceCtx, taskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取任务失败: %w", err)
|
||||
}
|
||||
if deleteTask.Type != models.TaskTypeDeviceThresholdCheck {
|
||||
return fmt.Errorf("任务 %d 不是一个设备阈值检查任务", taskID)
|
||||
}
|
||||
var deviceParams task.DeviceThresholdCheckParams
|
||||
if err := deleteTask.ParseParameters(&deviceParams); err != nil {
|
||||
return fmt.Errorf("任务 %d: 解析参数失败: %w", taskID, err)
|
||||
}
|
||||
|
||||
// 获得任务对应设备对应区域主控
|
||||
device, err := s.deviceRepo.FindByID(serviceCtx, deviceParams.DeviceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取设备 %d 失败: %w", deviceParams.DeviceID, err)
|
||||
}
|
||||
area, err := s.areaRepo.FindByID(serviceCtx, device.AreaControllerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取区域 %d 失败: %w", device.AreaControllerID, err)
|
||||
}
|
||||
|
||||
// 获取健康检查计划任务列表
|
||||
plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取系统健康检查计划失败: %w", err)
|
||||
}
|
||||
if plan == nil {
|
||||
logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck)
|
||||
}
|
||||
|
||||
taskIndexToDelete := -1
|
||||
for i, t := range plan.Tasks {
|
||||
if t.ID == taskID {
|
||||
taskIndexToDelete = i
|
||||
}
|
||||
if t.Type == models.TaskTypeAreaCollectorThresholdCheck {
|
||||
var areaParams task.AreaThresholdCheckParams
|
||||
if err := t.ParseParameters(&areaParams); err != nil {
|
||||
return fmt.Errorf("任务 %d: 解析参数失败: %w", t.ID, err)
|
||||
}
|
||||
if areaParams.AreaControllerID == area.ID && areaParams.SensorType == deviceParams.SensorType {
|
||||
for ia, e := range areaParams.ExcludeDeviceIDs {
|
||||
if e == deviceParams.DeviceID {
|
||||
areaParams.ExcludeDeviceIDs = append(areaParams.ExcludeDeviceIDs[:ia], areaParams.ExcludeDeviceIDs[ia+1:]...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
err = plan.Tasks[i].SaveParameters(areaParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("任务 %d: 保存参数失败: %w", t.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if taskIndexToDelete == -1 {
|
||||
return fmt.Errorf("任务 %d 在系统计划中未找到", taskID)
|
||||
}
|
||||
plan.Tasks = append(plan.Tasks[:taskIndexToDelete], plan.Tasks[taskIndexToDelete+1:]...)
|
||||
|
||||
plan.ReorderSteps()
|
||||
_, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteDeviceThresholdAlarmByDeviceID 实现了根据设备ID删除一个设备下所有设备阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) DeleteDeviceThresholdAlarmByDeviceID(ctx context.Context, deviceID uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteDeviceThresholdAlarmByDeviceID")
|
||||
tasks, err := s.planRepo.ListTasksByDeviceID(serviceCtx, deviceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取任务列表失败: %w", err)
|
||||
}
|
||||
device, err := s.deviceRepo.FindByID(serviceCtx, deviceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取设备 %d 失败: %w", deviceID, err)
|
||||
}
|
||||
plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取系统计划失败: %w", err)
|
||||
}
|
||||
if plan == nil {
|
||||
logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck)
|
||||
}
|
||||
|
||||
deleteNums := []int{}
|
||||
for i, t := range plan.Tasks {
|
||||
for _, dt := range tasks {
|
||||
if t.ID == dt.ID && t.Type == models.TaskTypeDeviceThresholdCheck {
|
||||
deleteNums = append(deleteNums, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if t.Type == models.TaskTypeAreaCollectorThresholdCheck {
|
||||
var areaParams task.AreaThresholdCheckParams
|
||||
if err := t.ParseParameters(&areaParams); err != nil {
|
||||
return fmt.Errorf("任务 %d: 解析参数失败: %w", t.ID, err)
|
||||
}
|
||||
if areaParams.AreaControllerID == device.AreaControllerID {
|
||||
for ai, ae := range areaParams.ExcludeDeviceIDs {
|
||||
if ae == deviceID {
|
||||
areaParams.ExcludeDeviceIDs = append(areaParams.ExcludeDeviceIDs[:ai], areaParams.ExcludeDeviceIDs[ai+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
err = plan.Tasks[i].SaveParameters(areaParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("任务 %d: 保存参数失败: %w", t.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 为了高效地判断一个索引是否需要被删除,先将 deleteNums 转换为一个 map。
|
||||
deleteMap := make(map[int]struct{}, len(deleteNums))
|
||||
for _, index := range deleteNums {
|
||||
deleteMap[index] = struct{}{}
|
||||
}
|
||||
|
||||
// 创建一个新的任务切片,只包含不需要删除的任务。
|
||||
newTasks := make([]models.Task, 0, len(plan.Tasks)-len(deleteNums))
|
||||
for i, t := range plan.Tasks {
|
||||
if _, found := deleteMap[i]; !found {
|
||||
newTasks = append(newTasks, t)
|
||||
}
|
||||
}
|
||||
plan.Tasks = newTasks
|
||||
|
||||
// 重新排序任务并更新计划
|
||||
plan.ReorderSteps()
|
||||
_, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// CreateAreaThresholdAlarm 实现了创建一个区域阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) CreateAreaThresholdAlarm(ctx context.Context, req *dto.CreateAreaThresholdAlarmDTO) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreateAreaThresholdAlarm")
|
||||
|
||||
// 1. 获取系统健康检查计划
|
||||
plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取系统健康检查计划失败: %w", err)
|
||||
}
|
||||
if plan == nil {
|
||||
logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck)
|
||||
}
|
||||
|
||||
// 2. 获取目标区域下的所有设备,并建立快速查找表
|
||||
devicesInArea, err := s.deviceRepo.ListByAreaControllerID(serviceCtx, req.AreaControllerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取区域 %d 下的设备列表失败: %w", req.AreaControllerID, err)
|
||||
}
|
||||
devicesInAreaMap := make(map[uint32]struct{}, len(devicesInArea))
|
||||
for _, device := range devicesInArea {
|
||||
devicesInAreaMap[device.ID] = struct{}{}
|
||||
}
|
||||
|
||||
// 3. 遍历计划,检查存在性并收集需要排除的设备ID
|
||||
var excludeDeviceIDs []uint32
|
||||
for _, t := range plan.Tasks {
|
||||
switch t.Type {
|
||||
case models.TaskTypeAreaCollectorThresholdCheck:
|
||||
var params task.AreaThresholdCheckParams
|
||||
if err := t.ParseParameters(¶ms); err != nil {
|
||||
return fmt.Errorf("任务 %d: 解析区域阈值检查任务参数失败: %w", t.ID, err)
|
||||
}
|
||||
if params.AreaControllerID == req.AreaControllerID && params.SensorType == req.SensorType {
|
||||
return fmt.Errorf("区域 %d: 该区域已存在针对 %s 的阈值检查任务", req.AreaControllerID, req.SensorType)
|
||||
}
|
||||
case models.TaskTypeDeviceThresholdCheck:
|
||||
var params task.DeviceThresholdCheckParams
|
||||
if err := t.ParseParameters(¶ms); err != nil {
|
||||
return fmt.Errorf("任务 %d: 解析设备阈值检查任务参数失败: %w", t.ID, err)
|
||||
}
|
||||
// 检查该设备是否属于目标区域
|
||||
if _, ok := devicesInAreaMap[params.DeviceID]; ok {
|
||||
excludeDeviceIDs = append(excludeDeviceIDs, params.DeviceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 创建新任务
|
||||
newTask := models.Task{
|
||||
PlanID: plan.ID,
|
||||
Name: fmt.Sprintf("区域 %d 的 %s 阈值检测任务", req.AreaControllerID, req.SensorType),
|
||||
Description: fmt.Sprintf("检测区域 %d 的 %s 是否 %v %v", req.AreaControllerID, req.SensorType, req.Operator, req.Thresholds),
|
||||
ExecutionOrder: len(plan.Tasks) + 1,
|
||||
Type: models.TaskTypeAreaCollectorThresholdCheck,
|
||||
}
|
||||
err = newTask.SaveParameters(task.AreaThresholdCheckParams{
|
||||
AreaControllerID: req.AreaControllerID,
|
||||
SensorType: req.SensorType,
|
||||
Thresholds: req.Thresholds,
|
||||
Operator: req.Operator,
|
||||
Level: req.Level,
|
||||
ExcludeDeviceIDs: excludeDeviceIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存新区域任务的参数失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 更新计划
|
||||
plan.Tasks = append(plan.Tasks, newTask)
|
||||
plan.ReorderSteps()
|
||||
_, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateAreaThresholdAlarm 实现了更新一个区域阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) UpdateAreaThresholdAlarm(ctx context.Context, taskID int, req *dto.UpdateAreaThresholdAlarmDTO) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdateAreaThresholdAlarm")
|
||||
|
||||
// 1. 获取系统健康检查计划
|
||||
plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取系统健康检查计划失败: %w", err)
|
||||
}
|
||||
if plan == nil {
|
||||
logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck)
|
||||
}
|
||||
|
||||
// 2. 遍历任务列表,查找并更新目标任务
|
||||
taskFound := false
|
||||
for i, t := range plan.Tasks {
|
||||
if t.ID == taskID && t.Type == models.TaskTypeAreaCollectorThresholdCheck {
|
||||
taskFound = true
|
||||
|
||||
var params task.AreaThresholdCheckParams
|
||||
if err = t.ParseParameters(¶ms); err != nil {
|
||||
return fmt.Errorf("任务 %d: 解析现有参数失败: %w", taskID, err)
|
||||
}
|
||||
params.Thresholds = req.Thresholds
|
||||
params.Operator = req.Operator
|
||||
params.Level = req.Level
|
||||
// 刷新任务说明
|
||||
plan.Tasks[i].Description = fmt.Sprintf("检测区域 %d 的 %s 是否 %v %v", params.AreaControllerID, params.SensorType, params.Operator, params.Thresholds)
|
||||
|
||||
err = plan.Tasks[i].SaveParameters(params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("任务 %d: 保存参数失败: %w", taskID, err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !taskFound {
|
||||
return fmt.Errorf("任务 %d: 不存在或类型不匹配", taskID)
|
||||
}
|
||||
|
||||
// 全量更新计划
|
||||
_, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAreaThresholdAlarm 实现了根据ID获取一个区域阈值告警任务的逻辑。
|
||||
func (s *thresholdAlarmService) GetAreaThresholdAlarm(ctx context.Context, taskID int) (*dto.AreaThresholdAlarmDTO, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetAreaThresholdAlarm")
|
||||
|
||||
// 1. 使用 planRepo 查询任务
|
||||
t, err := s.planRepo.FindTaskByID(serviceCtx, taskID)
|
||||
if err != nil {
|
||||
return nil, err // 如果未找到或发生其他错误,直接返回
|
||||
}
|
||||
|
||||
// 2. 验证任务类型是否正确
|
||||
if t.Type != models.TaskTypeAreaCollectorThresholdCheck {
|
||||
return nil, fmt.Errorf("任务 %d 不是一个区域阈值检查任务", taskID)
|
||||
}
|
||||
var params task.AreaThresholdCheckParams
|
||||
err = t.ParseParameters(¶ms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("任务 %d: 解析参数失败: %w", taskID, err)
|
||||
}
|
||||
|
||||
resp := &dto.AreaThresholdAlarmDTO{
|
||||
ID: t.ID,
|
||||
AreaControllerID: params.AreaControllerID,
|
||||
SensorType: params.SensorType,
|
||||
Thresholds: params.Thresholds,
|
||||
Operator: params.Operator,
|
||||
Level: params.Level,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteAreaThresholdAlarm 实现了删除一个区域阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) DeleteAreaThresholdAlarm(ctx context.Context, taskID int) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteAreaThresholdAlarm")
|
||||
|
||||
// 获取健康检查计划任务列表
|
||||
plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取系统健康检查计划失败: %w", err)
|
||||
}
|
||||
if plan == nil {
|
||||
logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck)
|
||||
}
|
||||
|
||||
// 找到这个任务并删掉
|
||||
deleteTaskNum := -1
|
||||
for i, t := range plan.Tasks {
|
||||
if t.Type != models.TaskTypeAreaCollectorThresholdCheck {
|
||||
continue
|
||||
}
|
||||
if t.ID != taskID {
|
||||
continue
|
||||
}
|
||||
deleteTaskNum = i
|
||||
}
|
||||
|
||||
if deleteTaskNum == -1 {
|
||||
return fmt.Errorf("任务 %d: 不存在或类型不匹配", taskID)
|
||||
}
|
||||
plan.Tasks = append(plan.Tasks[:deleteTaskNum], plan.Tasks[deleteTaskNum+1:]...)
|
||||
plan.ReorderSteps()
|
||||
_, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteAreaThresholdAlarmByAreaControllerID 实现了根据区域ID删除一个区域下所有区域阈值告警的逻辑。
|
||||
func (s *thresholdAlarmService) DeleteAreaThresholdAlarmByAreaControllerID(ctx context.Context, areaControllerID uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteAreaThresholdAlarmByAreaControllerID")
|
||||
|
||||
// 1. 获取系统健康检查计划
|
||||
plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取系统健康检查计划失败: %w", err)
|
||||
}
|
||||
if plan == nil {
|
||||
logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck)
|
||||
}
|
||||
|
||||
// 2. 收集所有与指定 areaControllerID 相关的区域阈值告警任务的索引
|
||||
var deleteIndices []int
|
||||
for i, t := range plan.Tasks {
|
||||
// 只关心区域阈值检查任务
|
||||
if t.Type != models.TaskTypeAreaCollectorThresholdCheck {
|
||||
continue
|
||||
}
|
||||
|
||||
var params task.AreaThresholdCheckParams
|
||||
if err := t.ParseParameters(¶ms); err != nil {
|
||||
return fmt.Errorf("任务 %d: 解析参数失败: %w", t.ID, err)
|
||||
}
|
||||
|
||||
// 如果 AreaControllerID 匹配,则记录其索引以待删除
|
||||
if params.AreaControllerID == areaControllerID {
|
||||
deleteIndices = append(deleteIndices, i)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到要删除的任务,则直接返回
|
||||
if len(deleteIndices) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. 使用 map 和新切片的方式安全地删除多个任务
|
||||
deleteMap := make(map[int]struct{}, len(deleteIndices))
|
||||
for _, index := range deleteIndices {
|
||||
deleteMap[index] = struct{}{}
|
||||
}
|
||||
|
||||
newTasks := make([]models.Task, 0, len(plan.Tasks)-len(deleteMap))
|
||||
for i, t := range plan.Tasks {
|
||||
if _, found := deleteMap[i]; !found {
|
||||
newTasks = append(newTasks, t)
|
||||
}
|
||||
}
|
||||
plan.Tasks = newTasks
|
||||
|
||||
// 4. 重新排序任务并更新计划
|
||||
plan.ReorderSteps()
|
||||
_, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem)
|
||||
return err
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
type UserService interface {
|
||||
CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*dto.CreateUserResponse, error)
|
||||
Login(ctx context.Context, req *dto.LoginRequest) (*dto.LoginResponse, error)
|
||||
SendTestNotification(ctx context.Context, userID uint, req *dto.SendTestNotificationRequest) error
|
||||
SendTestNotification(ctx context.Context, userID uint32, req *dto.SendTestNotificationRequest) error
|
||||
}
|
||||
|
||||
// userService 实现了 UserService 接口
|
||||
@@ -103,7 +103,7 @@ func (s *userService) Login(ctx context.Context, req *dto.LoginRequest) (*dto.Lo
|
||||
}
|
||||
|
||||
// SendTestNotification 发送测试通知
|
||||
func (s *userService) SendTestNotification(ctx context.Context, userID uint, req *dto.SendTestNotificationRequest) error {
|
||||
func (s *userService) SendTestNotification(ctx context.Context, userID uint32, req *dto.SendTestNotificationRequest) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendTestNotification")
|
||||
err := s.notifyService.SendTestMessage(serviceCtx, userID, req.Type)
|
||||
if err != nil {
|
||||
|
||||
@@ -162,17 +162,17 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent)
|
||||
logger.Infof("开始处理 'up' 事件, DevEui: %s", event.DeviceInfo.DevEui)
|
||||
|
||||
// 1. 查找区域主控设备
|
||||
regionalController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui)
|
||||
areaController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui)
|
||||
if err != nil {
|
||||
logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
|
||||
return
|
||||
}
|
||||
// 依赖 SelfCheck 确保区域主控有效
|
||||
if err := regionalController.SelfCheck(); err != nil {
|
||||
logger.Errorf("处理 'up' 事件失败:区域主控 %v(ID: %d) 未通过自检: %v", regionalController.Name, regionalController.ID, err)
|
||||
if err := areaController.SelfCheck(); err != nil {
|
||||
logger.Errorf("处理 'up' 事件失败:区域主控 %v(ID: %d) 未通过自检: %v", areaController.Name, areaController.ID, err)
|
||||
return
|
||||
}
|
||||
logger.Infof("找到区域主控: %s (ID: %d)", regionalController.Name, regionalController.ID)
|
||||
logger.Infof("找到区域主控: %s (ID: %d)", areaController.Name, areaController.ID)
|
||||
|
||||
// 2. 记录区域主控的信号强度 (如果存在)
|
||||
if len(event.RxInfo) > 0 {
|
||||
@@ -187,8 +187,8 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent)
|
||||
}
|
||||
|
||||
// 记录信号强度
|
||||
c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
|
||||
logger.Infof("已记录区域主控 (ID: %d) 的信号强度: RSSI=%d, SNR=%.2f", regionalController.ID, rx.Rssi, rx.Snr)
|
||||
c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
|
||||
logger.Infof("已记录区域主控 (ID: %d) 的信号强度: RSSI=%d, SNR=%.2f", areaController.ID, rx.Rssi, rx.Snr)
|
||||
} else {
|
||||
logger.Warnf("处理 'up' 事件时未找到 RxInfo,无法记录信号数据。DevEui: %s", event.DeviceInfo.DevEui)
|
||||
}
|
||||
@@ -298,7 +298,7 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent)
|
||||
valueDescriptor := valueDescriptors[0]
|
||||
|
||||
// 5.3 应用乘数和偏移量计算最终值
|
||||
parsedValue := float64(rawSensorValue)*valueDescriptor.Multiplier + valueDescriptor.Offset
|
||||
parsedValue := rawSensorValue*valueDescriptor.Multiplier + valueDescriptor.Offset
|
||||
|
||||
// 5.4 根据传感器类型构建具体的数据结构
|
||||
var dataToRecord interface{}
|
||||
@@ -312,11 +312,11 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent)
|
||||
default:
|
||||
// TODO 未知传感器的数据需要记录吗
|
||||
logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type)
|
||||
dataToRecord = map[string]float64{"value": parsedValue}
|
||||
dataToRecord = map[string]float32{"value": parsedValue}
|
||||
}
|
||||
|
||||
// 5.5 记录传感器数据
|
||||
c.recordSensorData(reqCtx, regionalController.ID, dev.ID, event.Time, valueDescriptor.Type, dataToRecord)
|
||||
c.recordSensorData(reqCtx, areaController.ID, dev.ID, event.Time, valueDescriptor.Type, dataToRecord)
|
||||
logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 原始值=%f, 解析值=%.2f", dev.ID, valueDescriptor.Type, rawSensorValue, parsedValue)
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *Statu
|
||||
logger.Infof("处接收到理 'status' 事件: %+v", event)
|
||||
|
||||
// 查找区域主控设备
|
||||
regionalController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui)
|
||||
areaController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui)
|
||||
if err != nil {
|
||||
logger.Errorf("处理 'status' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
|
||||
return
|
||||
@@ -344,8 +344,8 @@ func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *Statu
|
||||
signalMetrics := models.SignalMetrics{
|
||||
MarginDb: event.Margin,
|
||||
}
|
||||
c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
|
||||
logger.Infof("已记录区域主控 (ID: %d) 的信号状态: %+v", regionalController.ID, signalMetrics)
|
||||
c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
|
||||
logger.Infof("已记录区域主控 (ID: %d) 的信号状态: %+v", areaController.ID, signalMetrics)
|
||||
|
||||
// 记录电量
|
||||
batteryLevel := models.BatteryLevel{
|
||||
@@ -353,8 +353,8 @@ func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *Statu
|
||||
BatteryLevelUnavailable: event.BatteryLevelUnavailable,
|
||||
ExternalPower: event.ExternalPower,
|
||||
}
|
||||
c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeBatteryLevel, batteryLevel)
|
||||
logger.Infof("已记录区域主控 (ID: %d) 的电池状态: %+v", regionalController.ID, batteryLevel)
|
||||
c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeBatteryLevel, batteryLevel)
|
||||
logger.Infof("已记录区域主控 (ID: %d) 的电池状态: %+v", areaController.ID, batteryLevel)
|
||||
}
|
||||
|
||||
// handleAckEvent 处理下行确认事件
|
||||
@@ -425,11 +425,11 @@ func (c *ChirpStackListener) handleIntegrationEvent(ctx context.Context, event *
|
||||
}
|
||||
|
||||
// recordSensorData 是一个通用方法,用于将传感器数据存入数据库。
|
||||
// regionalControllerID: 区域主控设备的ID
|
||||
// areaControllerID: 区域主控设备的ID
|
||||
// sensorDeviceID: 实际产生传感器数据的普通设备的ID
|
||||
// sensorType: 传感器值的类型 (例如 models.SensorTypeTemperature)
|
||||
// data: 具体的传感器数据结构体实例 (例如 models.TemperatureData)
|
||||
func (c *ChirpStackListener) recordSensorData(ctx context.Context, regionalControllerID uint, sensorDeviceID uint, eventTime time.Time, sensorType models.SensorType, data interface{}) {
|
||||
func (c *ChirpStackListener) recordSensorData(ctx context.Context, areaControllerID uint32, sensorDeviceID uint32, eventTime time.Time, sensorType models.SensorType, data interface{}) {
|
||||
reqCtx, logger := logs.Trace(ctx, c.ctx, "recordSensorData")
|
||||
// 1. 将传入的结构体序列化为 JSON
|
||||
jsonData, err := json.Marshal(data)
|
||||
@@ -440,11 +440,11 @@ func (c *ChirpStackListener) recordSensorData(ctx context.Context, regionalContr
|
||||
|
||||
// 2. 构建 SensorData 模型
|
||||
sensorData := &models.SensorData{
|
||||
Time: eventTime,
|
||||
DeviceID: sensorDeviceID,
|
||||
RegionalControllerID: regionalControllerID,
|
||||
SensorType: sensorType,
|
||||
Data: datatypes.JSON(jsonData),
|
||||
Time: eventTime,
|
||||
DeviceID: sensorDeviceID,
|
||||
AreaControllerID: areaControllerID,
|
||||
SensorType: sensorType,
|
||||
Data: datatypes.JSON(jsonData),
|
||||
}
|
||||
|
||||
// 3. 调用仓库创建记录
|
||||
|
||||
@@ -24,9 +24,9 @@ type DeviceInfo struct {
|
||||
|
||||
// Location 包含了地理位置信息。
|
||||
type Location struct {
|
||||
Latitude float64 `json:"latitude"` // 纬度
|
||||
Longitude float64 `json:"longitude"` // 经度
|
||||
Altitude float64 `json:"altitude"` // 海拔
|
||||
Latitude float32 `json:"latitude"` // 纬度
|
||||
Longitude float32 `json:"longitude"` // 经度
|
||||
Altitude float32 `json:"altitude"` // 海拔
|
||||
}
|
||||
|
||||
// --- 可复用的子结构体 ---
|
||||
@@ -61,7 +61,7 @@ type UplinkRxInfo struct {
|
||||
UplinkID uint32 `json:"uplink_id"` // 上行ID
|
||||
Time time.Time `json:"time"` // 接收时间
|
||||
Rssi int `json:"rssi"` // 接收信号强度指示
|
||||
Snr float64 `json:"snr"` // 信噪比
|
||||
Snr float32 `json:"snr"` // 信噪比
|
||||
Channel int `json:"channel"` // 接收通道
|
||||
Location *Location `json:"location"` // 网关位置
|
||||
Context string `json:"context"` // 上下文信息
|
||||
@@ -96,9 +96,9 @@ type DownlinkTxInfo struct {
|
||||
|
||||
// ResolvedLocation 包含了地理位置解析结果。
|
||||
type ResolvedLocation struct {
|
||||
Latitude float64 `json:"latitude"` // 纬度
|
||||
Longitude float64 `json:"longitude"` // 经度
|
||||
Altitude float64 `json:"altitude"` // 海拔
|
||||
Latitude float32 `json:"latitude"` // 纬度
|
||||
Longitude float32 `json:"longitude"` // 经度
|
||||
Altitude float32 `json:"altitude"` // 海拔
|
||||
Source string `json:"source"` // 位置来源
|
||||
Accuracy int `json:"accuracy"` // 精度
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ func NewApplication(configPath string) (*Application, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化基础设施失败: %w", err)
|
||||
}
|
||||
domain := initDomainServices(ctx, cfg, infra)
|
||||
domain, err := initDomainServices(ctx, cfg, infra)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化领域服务失败: %w", err)
|
||||
}
|
||||
appServices := initAppServices(ctx, infra, domain)
|
||||
|
||||
// 3. 初始化 API 入口点
|
||||
@@ -58,6 +61,7 @@ func NewApplication(configPath string) (*Application, error) {
|
||||
appServices.planService,
|
||||
appServices.userService,
|
||||
appServices.auditService,
|
||||
appServices.thresholdAlarmService,
|
||||
infra.tokenGenerator,
|
||||
infra.lora.listenHandler,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ 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/alarm"
|
||||
"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"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -29,7 +31,6 @@ type Infrastructure struct {
|
||||
storage database.Storage
|
||||
repos *Repositories
|
||||
lora *LoraComponents
|
||||
notifyService domain_notify.Service
|
||||
tokenGenerator token.Generator
|
||||
}
|
||||
|
||||
@@ -47,18 +48,12 @@ func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructur
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyService, err := initNotifyService(ctx, cfg.Notify, repos.userRepo, repos.notificationRepo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化通知服务失败: %w", err)
|
||||
}
|
||||
|
||||
tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret))
|
||||
|
||||
return &Infrastructure{
|
||||
storage: storage,
|
||||
repos: repos,
|
||||
lora: lora,
|
||||
notifyService: notifyService,
|
||||
tokenGenerator: tokenGenerator,
|
||||
}, nil
|
||||
}
|
||||
@@ -86,6 +81,7 @@ type Repositories struct {
|
||||
medicationLogRepo repository.MedicationLogRepository
|
||||
rawMaterialRepo repository.RawMaterialRepository
|
||||
notificationRepo repository.NotificationRepository
|
||||
alarmRepo repository.AlarmRepository
|
||||
unitOfWork repository.UnitOfWork
|
||||
}
|
||||
|
||||
@@ -114,6 +110,7 @@ func initRepositories(ctx context.Context, db *gorm.DB) *Repositories {
|
||||
medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), db),
|
||||
rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db),
|
||||
notificationRepo: repository.NewGormNotificationRepository(logs.AddCompName(baseCtx, "NotificationRepo"), db),
|
||||
alarmRepo: repository.NewGormAlarmRepository(logs.AddCompName(baseCtx, "AlarmRepo"), db),
|
||||
unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db),
|
||||
}
|
||||
}
|
||||
@@ -129,12 +126,19 @@ type DomainServices struct {
|
||||
planExecutionManager plan.ExecutionManager
|
||||
analysisPlanTaskManager plan.AnalysisPlanTaskManager
|
||||
planService plan.Service
|
||||
notifyService domain_notify.Service
|
||||
alarmService alarm.AlarmService
|
||||
}
|
||||
|
||||
// initDomainServices 初始化所有的领域服务。
|
||||
func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastructure) *DomainServices {
|
||||
func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastructure) (*DomainServices, error) {
|
||||
baseCtx := context.Background()
|
||||
|
||||
notifyService, err := initNotifyService(ctx, cfg.Notify, infra.repos.userRepo, infra.repos.notificationRepo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化通知服务失败: %w", err)
|
||||
}
|
||||
|
||||
// 猪群管理相关
|
||||
pigPenTransferManager := pig.NewPigPenTransferManager(logs.AddCompName(baseCtx, "PigPenTransferManager"), infra.repos.pigPenRepo, infra.repos.pigTransferLogRepo, infra.repos.pigBatchRepo)
|
||||
pigTradeManager := pig.NewPigTradeManager(logs.AddCompName(baseCtx, "PigTradeManager"), infra.repos.pigTradeRepo)
|
||||
@@ -157,8 +161,22 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
|
||||
infra.lora.comm,
|
||||
)
|
||||
|
||||
// 告警服务
|
||||
alarmService := alarm.NewAlarmService(
|
||||
logs.AddCompName(baseCtx, "AlarmService"),
|
||||
infra.repos.alarmRepo,
|
||||
infra.repos.unitOfWork,
|
||||
)
|
||||
|
||||
// 任务工厂
|
||||
taskFactory := task.NewTaskFactory(logs.AddCompName(baseCtx, "TaskFactory"), infra.repos.sensorDataRepo, infra.repos.deviceRepo, generalDeviceService)
|
||||
taskFactory := task.NewTaskFactory(logs.AddCompName(baseCtx, "TaskFactory"),
|
||||
infra.repos.sensorDataRepo,
|
||||
infra.repos.deviceRepo,
|
||||
infra.repos.alarmRepo,
|
||||
generalDeviceService,
|
||||
notifyService,
|
||||
alarmService,
|
||||
)
|
||||
|
||||
// 计划任务管理器
|
||||
analysisPlanTaskManager := plan.NewAnalysisPlanTaskManager(logs.AddCompName(baseCtx, "AnalysisPlanTaskManager"), infra.repos.planRepo, infra.repos.pendingTaskRepo, infra.repos.executionLogRepo)
|
||||
@@ -199,18 +217,21 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
|
||||
taskFactory: taskFactory,
|
||||
planExecutionManager: planExecutionManager,
|
||||
planService: planService,
|
||||
}
|
||||
notifyService: notifyService,
|
||||
alarmService: alarmService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AppServices 聚合了所有的应用服务实例。
|
||||
type AppServices struct {
|
||||
pigFarmService service.PigFarmService
|
||||
pigBatchService service.PigBatchService
|
||||
monitorService service.MonitorService
|
||||
deviceService service.DeviceService
|
||||
planService service.PlanService
|
||||
userService service.UserService
|
||||
auditService service.AuditService
|
||||
pigFarmService service.PigFarmService
|
||||
pigBatchService service.PigBatchService
|
||||
monitorService service.MonitorService
|
||||
deviceService service.DeviceService
|
||||
planService service.PlanService
|
||||
userService service.UserService
|
||||
auditService service.AuditService
|
||||
thresholdAlarmService service.ThresholdAlarmService
|
||||
}
|
||||
|
||||
// initAppServices 初始化所有的应用服务。
|
||||
@@ -235,25 +256,40 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
|
||||
infra.repos.pigTradeRepo,
|
||||
infra.repos.notificationRepo,
|
||||
)
|
||||
|
||||
// 初始化阈值告警服务
|
||||
thresholdAlarmService := service.NewThresholdAlarmService(
|
||||
logs.AddCompName(baseCtx, "ThresholdAlarmService"),
|
||||
domainServices.alarmService,
|
||||
domainServices.planService,
|
||||
infra.repos.alarmRepo,
|
||||
infra.repos.planRepo,
|
||||
infra.repos.areaControllerRepo,
|
||||
infra.repos.deviceRepo,
|
||||
)
|
||||
|
||||
deviceService := service.NewDeviceService(
|
||||
logs.AddCompName(baseCtx, "DeviceService"),
|
||||
infra.repos.deviceRepo,
|
||||
infra.repos.areaControllerRepo,
|
||||
infra.repos.deviceTemplateRepo,
|
||||
domainServices.generalDeviceService,
|
||||
thresholdAlarmService,
|
||||
)
|
||||
|
||||
auditService := service.NewAuditService(logs.AddCompName(baseCtx, "AuditService"), infra.repos.userActionLogRepo)
|
||||
planService := service.NewPlanService(logs.AddCompName(baseCtx, "AppPlanService"), domainServices.planService)
|
||||
userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, infra.notifyService)
|
||||
userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, domainServices.notifyService)
|
||||
|
||||
return &AppServices{
|
||||
pigFarmService: pigFarmService,
|
||||
pigBatchService: pigBatchService,
|
||||
monitorService: monitorService,
|
||||
deviceService: deviceService,
|
||||
auditService: auditService,
|
||||
planService: planService,
|
||||
userService: userService,
|
||||
pigFarmService: pigFarmService,
|
||||
pigBatchService: pigBatchService,
|
||||
monitorService: monitorService,
|
||||
deviceService: deviceService,
|
||||
auditService: auditService,
|
||||
planService: planService,
|
||||
userService: userService,
|
||||
thresholdAlarmService: thresholdAlarmService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +389,7 @@ func initNotifyService(
|
||||
|
||||
// 3. 动态确定首选通知器
|
||||
var primaryNotifier notify.Notifier
|
||||
primaryNotifierType := notify.NotifierType(cfg.Primary)
|
||||
primaryNotifierType := models.NotifierType(cfg.Primary)
|
||||
|
||||
// 检查用户指定的主渠道是否已启用
|
||||
for _, n := range availableNotifiers {
|
||||
|
||||
@@ -4,16 +4,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
// PlanNameTimedFullDataCollection 是定时全量数据采集计划的名称
|
||||
PlanNameTimedFullDataCollection = "定时全量数据采集"
|
||||
)
|
||||
|
||||
// initializeState 在应用启动时准备其初始数据状态。
|
||||
// 它遵循一个严格的顺序:清理 -> 更新 -> 刷新,以确保数据的一致性和正确性。
|
||||
func (app *Application) initializeState(ctx context.Context) error {
|
||||
@@ -48,13 +44,11 @@ func (app *Application) initializeState(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// initializeSystemPlans 确保预定义的系统计划在数据库中存在并保持最新。
|
||||
// 它通过调用各个独立的计划初始化方法来完成此操作。
|
||||
func (app *Application) initializeSystemPlans(ctx context.Context) error {
|
||||
appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializeSystemPlans")
|
||||
logger.Info("开始检查并更新预定义的系统计划...")
|
||||
|
||||
// 动态构建预定义计划列表
|
||||
predefinedSystemPlans := app.getPredefinedSystemPlans()
|
||||
|
||||
// 1. 获取所有已存在的系统计划
|
||||
existingPlans, _, err := app.Infra.repos.planRepo.ListPlans(appCtx, repository.ListPlansOptions{
|
||||
PlanType: repository.PlanTypeFilterSystem,
|
||||
@@ -64,54 +58,28 @@ func (app *Application) initializeSystemPlans(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 2. 为了方便查找, 将现有计划名放入一个 map
|
||||
existingPlanMap := make(map[string]*models.Plan)
|
||||
existingPlanMap := make(map[models.PlanName]*models.Plan)
|
||||
for i := range existingPlans {
|
||||
existingPlanMap[existingPlans[i].Name] = &existingPlans[i]
|
||||
}
|
||||
|
||||
// 3. 遍历预定义的计划列表
|
||||
for i := range predefinedSystemPlans {
|
||||
predefinedPlan := &predefinedSystemPlans[i] // 获取可修改的指针
|
||||
// 3. 调用独立的初始化方法来处理每个系统计划
|
||||
if err := app.initializePeriodicSystemHealthCheckPlan(appCtx, existingPlanMap); err != nil {
|
||||
return err // 如果任何一个计划初始化失败,则立即返回错误
|
||||
}
|
||||
|
||||
if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok {
|
||||
// 如果计划存在,则进行无差别更新
|
||||
logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name)
|
||||
|
||||
// 将数据库中已存在的计划的ID和运行时状态字段赋值给预定义计划
|
||||
predefinedPlan.ID = foundExistingPlan.ID
|
||||
predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount
|
||||
|
||||
// 1. 使用 UpdatePlanMetadataAndStructure 来更新计划的元数据和关联任务
|
||||
// 这会处理 Name, Description, ExecutionType, ExecuteNum, CronExpression, ContentType
|
||||
// 并且最重要的是,它会正确处理 Tasks 的增删改,确保任务列表与 predefinedPlan.Tasks 完全同步
|
||||
if err := app.Infra.repos.planRepo.UpdatePlanMetadataAndStructure(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("更新预定义计划 '%s' 的元数据和结构失败: %w", predefinedPlan.Name, err)
|
||||
}
|
||||
|
||||
// 2. 接着使用 UpdatePlan 来更新所有顶层字段,包括 PlanType 和 Status
|
||||
// 由于任务已经在上一步正确同步,此步不会导致任务冗余
|
||||
if err := app.Infra.repos.planRepo.UpdatePlan(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("更新预定义计划 '%s' 的所有顶层字段失败: %w", predefinedPlan.Name, err)
|
||||
}
|
||||
|
||||
logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name)
|
||||
} else {
|
||||
// 如果计划不存在, 则创建
|
||||
logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name)
|
||||
if err := app.Infra.repos.planRepo.CreatePlan(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("创建预定义计划 '%s' 失败: %w", predefinedPlan.Name, err)
|
||||
} else {
|
||||
logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name)
|
||||
}
|
||||
}
|
||||
if err := app.initializeAlarmNotificationPlan(appCtx, existingPlanMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("预定义系统计划检查完成。")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPredefinedSystemPlans 返回一个基于当前配置的预定义系统计划列表。
|
||||
func (app *Application) getPredefinedSystemPlans() []models.Plan {
|
||||
// initializePeriodicSystemHealthCheckPlan 负责初始化 "周期性系统健康检查" 计划。
|
||||
// 它会根据当前配置动态构建计划,并决定是创建新计划还是更新现有计划。
|
||||
func (app *Application) initializePeriodicSystemHealthCheckPlan(ctx context.Context, existingPlanMap map[models.PlanName]*models.Plan) error {
|
||||
appCtx, logger := logs.Trace(ctx, app.Ctx, "initializePeriodicSystemHealthCheckPlan")
|
||||
|
||||
// 根据配置创建定时全量采集计划
|
||||
interval := app.Config.Collection.Interval
|
||||
@@ -119,25 +87,141 @@ func (app *Application) getPredefinedSystemPlans() []models.Plan {
|
||||
interval = 1 // 确保间隔至少为1分钟
|
||||
}
|
||||
cronExpression := fmt.Sprintf("*/%d * * * *", interval)
|
||||
timedCollectionPlan := models.Plan{
|
||||
Name: PlanNameTimedFullDataCollection,
|
||||
Description: fmt.Sprintf("这是一个系统预定义的计划, 每 %d 分钟自动触发一次全量数据采集。", app.Config.Collection.Interval),
|
||||
|
||||
// 定义预设的全量采集任务
|
||||
fullCollectionTask := models.Task{
|
||||
Name: "全量采集",
|
||||
Description: "触发一次全量数据采集",
|
||||
ExecutionOrder: 1,
|
||||
Type: models.TaskTypeFullCollection,
|
||||
}
|
||||
|
||||
// 定义预设的延时任务
|
||||
delayParams := task.DelayTaskParams{DelayDuration: 10} // 延时10秒
|
||||
delayTask := models.Task{
|
||||
Name: "延时任务",
|
||||
Description: "系统预设延时任务,用于错峰处理",
|
||||
ExecutionOrder: 2,
|
||||
Type: models.TaskTypeWaiting,
|
||||
}
|
||||
err := delayTask.SaveParameters(delayParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化延时任务参数失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建新的任务列表
|
||||
var newTasks []models.Task
|
||||
newTasks = append(newTasks, fullCollectionTask, delayTask)
|
||||
|
||||
// 如果计划已存在,则获取其现有任务并追加到新任务列表后(排除预设任务)
|
||||
if foundExistingPlan, ok := existingPlanMap[models.PlanNamePeriodicSystemHealthCheck]; ok {
|
||||
for _, existingTask := range foundExistingPlan.Tasks {
|
||||
// 排除已预设的全量采集和延时任务
|
||||
if existingTask.Type != models.TaskTypeFullCollection && existingTask.Type != models.TaskTypeWaiting {
|
||||
newTasks = append(newTasks, existingTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重新设置所有任务的 ExecutionOrder
|
||||
for i := range newTasks {
|
||||
newTasks[i].ExecutionOrder = i + 1
|
||||
}
|
||||
|
||||
predefinedPlan := &models.Plan{
|
||||
Name: models.PlanNamePeriodicSystemHealthCheck,
|
||||
Description: fmt.Sprintf("这是一个系统预定义的计划, 每 %d 分钟自动触发一次全量数据采集, 并进行阈值校验告警。", app.Config.Collection.Interval),
|
||||
PlanType: models.PlanTypeSystem,
|
||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
||||
CronExpression: cronExpression,
|
||||
Status: models.PlanStatusEnabled,
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: newTasks,
|
||||
}
|
||||
|
||||
if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok {
|
||||
// 如果计划存在,则进行无差别更新
|
||||
logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name)
|
||||
|
||||
// 将数据库中已存在的计划的ID和运行时状态字段赋值给预定义计划
|
||||
predefinedPlan.ID = foundExistingPlan.ID
|
||||
predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount
|
||||
|
||||
// 1. 使用 UpdatePlanMetadataAndStructure 来更新计划的元数据和关联任务
|
||||
// 这会处理 Name, Description, ExecutionType, ExecuteNum, CronExpression, ContentType
|
||||
// 并且最重要的是,它会正确处理 Tasks 的增删改,确保任务列表与 predefinedPlan.Tasks 完全同步
|
||||
if err := app.Infra.repos.planRepo.UpdatePlanMetadataAndStructure(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("更新预定义计划 '%s' 的元数据和结构失败: %w", predefinedPlan.Name, err)
|
||||
}
|
||||
|
||||
// 2. 接着使用 UpdatePlan 来更新所有顶层字段,包括 PlanType 和 Status
|
||||
// 由于任务已经在上一步正确同步,此步不会导致任务冗余
|
||||
if err := app.Infra.repos.planRepo.UpdatePlan(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("更新预定义计划 '%s' 的所有顶层字段失败: %w", predefinedPlan.Name, err)
|
||||
}
|
||||
|
||||
logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name)
|
||||
} else {
|
||||
// 如果计划不存在, 则创建
|
||||
logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name)
|
||||
if err := app.Infra.repos.planRepo.CreatePlan(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("创建预定义计划 '%s' 失败: %w", predefinedPlan.Name, err)
|
||||
} else {
|
||||
logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeAlarmNotificationPlan 负责初始化 "告警通知发送" 计划。
|
||||
// 它确保系统中存在一个每分钟执行的、用于发送告警通知的预定义计划。
|
||||
func (app *Application) initializeAlarmNotificationPlan(ctx context.Context, existingPlanMap map[models.PlanName]*models.Plan) error {
|
||||
appCtx, logger := logs.Trace(ctx, app.Ctx, "initializeAlarmNotificationPlan")
|
||||
|
||||
predefinedPlan := &models.Plan{
|
||||
Name: models.PlanNameAlarmNotification,
|
||||
Description: "这是一个系统预定义的计划, 每分钟自动触发一次告警通知发送。",
|
||||
PlanType: models.PlanTypeSystem,
|
||||
ExecutionType: models.PlanExecutionTypeAutomatic,
|
||||
CronExpression: "*/1 * * * *", // 每分钟执行一次
|
||||
Status: models.PlanStatusEnabled,
|
||||
ContentType: models.PlanContentTypeTasks,
|
||||
Tasks: []models.Task{
|
||||
{
|
||||
Name: "全量采集",
|
||||
Description: "触发一次全量数据采集",
|
||||
Name: "告警通知发送",
|
||||
Description: "发送所有待处理的告警通知",
|
||||
ExecutionOrder: 1,
|
||||
Type: models.TaskTypeFullCollection,
|
||||
Type: models.TaskTypeAlarmNotification,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return []models.Plan{timedCollectionPlan}
|
||||
if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok {
|
||||
// 如果计划存在,则进行无差别更新
|
||||
logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name)
|
||||
|
||||
predefinedPlan.ID = foundExistingPlan.ID
|
||||
predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount
|
||||
|
||||
if err := app.Infra.repos.planRepo.UpdatePlanMetadataAndStructure(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("更新预定义计划 '%s' 的元数据和结构失败: %w", predefinedPlan.Name, err)
|
||||
}
|
||||
|
||||
if err := app.Infra.repos.planRepo.UpdatePlan(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("更新预定义计划 '%s' 的所有顶层字段失败: %w", predefinedPlan.Name, err)
|
||||
}
|
||||
|
||||
logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name)
|
||||
} else {
|
||||
// 如果计划不存在, 则创建
|
||||
logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name)
|
||||
if err := app.Infra.repos.planRepo.CreatePlan(appCtx, predefinedPlan); err != nil {
|
||||
return fmt.Errorf("创建预定义计划 '%s' 失败: %w", predefinedPlan.Name, err)
|
||||
} else {
|
||||
logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializePendingCollections 在应用启动时处理所有未完成的采集请求。
|
||||
@@ -209,7 +293,7 @@ func (app *Application) cleanupStaleTasksAndLogs(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 2. 收集所有受影响的唯一 PlanID
|
||||
affectedPlanIDs := make(map[uint]struct{})
|
||||
affectedPlanIDs := make(map[uint32]struct{})
|
||||
for _, log := range incompletePlanLogs {
|
||||
affectedPlanIDs[log.PlanID] = struct{}{}
|
||||
}
|
||||
|
||||
175
internal/domain/alarm/alarm_service.go
Normal file
175
internal/domain/alarm/alarm_service.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package alarm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// AlarmService 定义了告警领域服务接口。
|
||||
type AlarmService interface {
|
||||
// CreateAlarmIfNotExists 检查是否存在相同的活跃告警,如果不存在,则创建一条新的告警记录。
|
||||
// "相同"的定义是:SourceType, SourceID, 和 AlarmCode 都相同。
|
||||
CreateAlarmIfNotExists(ctx context.Context, newAlarm *models.ActiveAlarm) error
|
||||
|
||||
// CloseAlarm 关闭一个活跃告警,将其归档到历史记录。
|
||||
// 如果指定的告警当前不活跃,则不执行任何操作并返回 nil。
|
||||
CloseAlarm(ctx context.Context, sourceType models.AlarmSourceType, sourceID uint32, alarmCode models.AlarmCode, resolveMethod string, resolvedBy *uint32) error
|
||||
|
||||
// SnoozeAlarm 忽略一个活跃告警,或更新其忽略时间。
|
||||
// 如果告警不存在,将返回错误。
|
||||
SnoozeAlarm(ctx context.Context, alarmID uint32, duration time.Duration) error
|
||||
|
||||
// CancelAlarmSnooze 取消对一个告警的忽略状态。
|
||||
// 如果告警不存在,或本就未被忽略,不执行任何操作并返回 nil。
|
||||
CancelAlarmSnooze(ctx context.Context, alarmID uint32) error
|
||||
}
|
||||
|
||||
// alarmService 是 AlarmService 接口的具体实现。
|
||||
type alarmService struct {
|
||||
ctx context.Context
|
||||
alarmRepo repository.AlarmRepository
|
||||
uow repository.UnitOfWork
|
||||
}
|
||||
|
||||
// NewAlarmService 创建一个新的 AlarmService 实例。
|
||||
func NewAlarmService(ctx context.Context, alarmRepo repository.AlarmRepository, uow repository.UnitOfWork) AlarmService {
|
||||
return &alarmService{
|
||||
ctx: ctx,
|
||||
alarmRepo: alarmRepo,
|
||||
uow: uow,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAlarmIfNotExists 实现了创建告警(如果不存在)的逻辑。
|
||||
func (s *alarmService) CreateAlarmIfNotExists(ctx context.Context, newAlarm *models.ActiveAlarm) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreateAlarmIfNotExists")
|
||||
|
||||
// 1. 检查告警是否已处于活跃状态
|
||||
isActive, err := s.alarmRepo.IsAlarmActiveInUse(serviceCtx, newAlarm.SourceType, newAlarm.SourceID, newAlarm.AlarmCode)
|
||||
if err != nil {
|
||||
logger.Errorf("检查告警活跃状态时发生数据库错误: %v", err)
|
||||
return err // 直接返回数据库错误
|
||||
}
|
||||
|
||||
if isActive {
|
||||
// 2. 如果已活跃,则记录日志并忽略
|
||||
logger.Infof("相同的告警已处于活跃状态,已忽略。来源: %s, ID: %d, 告警代码: %s", newAlarm.SourceType, newAlarm.SourceID, newAlarm.AlarmCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. 如果不活跃,则创建新告警
|
||||
logger.Infof("告警尚不活跃,正在创建新告警。来源: %s, ID: %d, 告警代码: %s", newAlarm.SourceType, newAlarm.SourceID, newAlarm.AlarmCode)
|
||||
return s.alarmRepo.CreateActiveAlarm(serviceCtx, newAlarm)
|
||||
}
|
||||
|
||||
// CloseAlarm 实现了关闭告警并将其归档的逻辑。
|
||||
func (s *alarmService) CloseAlarm(ctx context.Context, sourceType models.AlarmSourceType, sourceID uint32, alarmCode models.AlarmCode, resolveMethod string, resolvedBy *uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CloseAlarm")
|
||||
|
||||
// 1. 在事务外进行快速只读检查,避免不必要的事务开销
|
||||
isActive, err := s.alarmRepo.IsAlarmActiveInUse(serviceCtx, sourceType, sourceID, alarmCode)
|
||||
if err != nil {
|
||||
logger.Errorf("关闭告警失败:预检查告警活跃状态失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果告警本就不活跃,则无需任何操作
|
||||
if !isActive {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. 确认告警存在后,再进入事务执行“移动”操作
|
||||
logger.Infof("检测到活跃告警,正在执行关闭和归档操作。来源: %s, ID: %d, 告警代码: %s", sourceType, sourceID, alarmCode)
|
||||
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
// 在事务中再次查找,确保数据一致性并获取完整对象
|
||||
activeAlarm, err := s.alarmRepo.GetActiveAlarmByUniqueFieldsTx(serviceCtx, tx, sourceType, sourceID, alarmCode)
|
||||
if err != nil {
|
||||
// 此时如果没找到,可能在预检查和本事务之间已被其他进程关闭,同样视为正常
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Infof("告警在事务开始前已被关闭,无需操作。")
|
||||
return nil
|
||||
}
|
||||
logger.Errorf("关闭告警失败:在事务中查找活跃告警失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建历史告警记录
|
||||
historicalAlarm := &models.HistoricalAlarm{
|
||||
SourceType: activeAlarm.SourceType,
|
||||
SourceID: activeAlarm.SourceID,
|
||||
AlarmCode: activeAlarm.AlarmCode,
|
||||
AlarmSummary: activeAlarm.AlarmSummary,
|
||||
Level: activeAlarm.Level,
|
||||
AlarmDetails: activeAlarm.AlarmDetails,
|
||||
TriggerTime: activeAlarm.TriggerTime,
|
||||
ResolveTime: time.Now(),
|
||||
ResolveMethod: resolveMethod,
|
||||
ResolvedBy: resolvedBy,
|
||||
}
|
||||
|
||||
// 在事务中插入历史告警
|
||||
if err := s.alarmRepo.CreateHistoricalAlarmTx(serviceCtx, tx, historicalAlarm); err != nil {
|
||||
logger.Errorf("关闭告警失败:归档告警 %d 到历史表失败: %v", activeAlarm.ID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 在事务中删除活跃告警
|
||||
if err := s.alarmRepo.DeleteActiveAlarmTx(serviceCtx, tx, activeAlarm.ID); err != nil {
|
||||
logger.Errorf("关闭告警失败:从活跃表删除告警 %d 失败: %v", activeAlarm.ID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("告警 %d 已成功关闭并归档。", activeAlarm.ID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// SnoozeAlarm 忽略一个活跃告警,或更新其忽略时间。
|
||||
func (s *alarmService) SnoozeAlarm(ctx context.Context, alarmID uint32, duration time.Duration) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "SnoozeAlarm")
|
||||
|
||||
if duration <= 0 {
|
||||
return errors.New("忽略时长必须为正数")
|
||||
}
|
||||
|
||||
ignoredUntil := time.Now().Add(duration)
|
||||
err := s.alarmRepo.UpdateIgnoreStatus(serviceCtx, alarmID, true, &ignoredUntil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Warnf("尝试忽略一个不存在的告警: %d", alarmID)
|
||||
return fmt.Errorf("告警 %d 不存在", alarmID)
|
||||
}
|
||||
logger.Errorf("更新告警 %d 的忽略状态失败: %v", alarmID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("告警 %d 已被成功忽略,持续时间: %v", alarmID, duration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelAlarmSnooze 取消对一个告警的忽略状态。
|
||||
func (s *alarmService) CancelAlarmSnooze(ctx context.Context, alarmID uint32) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CancelAlarmSnooze")
|
||||
|
||||
err := s.alarmRepo.UpdateIgnoreStatus(serviceCtx, alarmID, false, nil)
|
||||
if err != nil {
|
||||
// 如果告警本就不存在,这不是一个需要上报的错误
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Infof("尝试取消忽略一个不存在的告警: %d,无需操作", alarmID)
|
||||
return nil
|
||||
}
|
||||
logger.Errorf("取消告警 %d 的忽略状态失败: %v", alarmID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("告警 %d 的忽略状态已被成功取消。", alarmID)
|
||||
return nil
|
||||
}
|
||||
@@ -28,7 +28,7 @@ type Service interface {
|
||||
Switch(ctx context.Context, device *models.Device, action DeviceAction) error
|
||||
|
||||
// Collect 用于发起对指定区域主控下的多个设备的批量采集请求。
|
||||
Collect(ctx context.Context, regionalControllerID uint, devicesToCollect []*models.Device) error
|
||||
Collect(ctx context.Context, areaControllerID uint32, devicesToCollect []*models.Device) error
|
||||
}
|
||||
|
||||
// 设备操作指令通用结构(最外层)
|
||||
|
||||
@@ -133,7 +133,7 @@ func (g *GeneralDeviceService) Switch(ctx context.Context, device *models.Device
|
||||
}
|
||||
|
||||
// Collect 实现了 Service 接口,用于发起对指定区域主控下的多个设备的批量采集请求。
|
||||
func (g *GeneralDeviceService) Collect(ctx context.Context, regionalControllerID uint, devicesToCollect []*models.Device) error {
|
||||
func (g *GeneralDeviceService) Collect(ctx context.Context, areaControllerID uint32, devicesToCollect []*models.Device) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, g.ctx, "Collect")
|
||||
if len(devicesToCollect) == 0 {
|
||||
logger.Info("待采集设备列表为空,无需执行采集任务。")
|
||||
@@ -141,16 +141,16 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, regionalControllerID
|
||||
}
|
||||
|
||||
// 1. 从设备列表中获取预加载的区域主控,并进行校验
|
||||
regionalController := &devicesToCollect[0].AreaController
|
||||
if regionalController.ID != regionalControllerID {
|
||||
return fmt.Errorf("设备列表与指定的区域主控ID (%d) 不匹配", regionalControllerID)
|
||||
areaController := &devicesToCollect[0].AreaController
|
||||
if areaController.ID != areaControllerID {
|
||||
return fmt.Errorf("设备列表与指定的区域主控ID (%d) 不匹配", areaControllerID)
|
||||
}
|
||||
if err := regionalController.SelfCheck(); err != nil {
|
||||
return fmt.Errorf("区域主控 (ID: %d) 未通过自检: %w", regionalControllerID, err)
|
||||
if err := areaController.SelfCheck(); err != nil {
|
||||
return fmt.Errorf("区域主控 (ID: %d) 未通过自检: %w", areaControllerID, err)
|
||||
}
|
||||
|
||||
// 2. 准备采集任务列表
|
||||
var childDeviceIDs []uint
|
||||
var childDeviceIDs []uint32
|
||||
var collectTasks []*proto.CollectTask
|
||||
|
||||
for _, dev := range devicesToCollect {
|
||||
@@ -208,13 +208,13 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, regionalControllerID
|
||||
}
|
||||
|
||||
// 3. 构建并发送指令
|
||||
networkID := regionalController.NetworkID
|
||||
networkID := areaController.NetworkID
|
||||
|
||||
// 4. 创建待处理请求记录
|
||||
correlationID := uuid.New().String()
|
||||
pendingReq := &models.PendingCollection{
|
||||
CorrelationID: correlationID,
|
||||
DeviceID: regionalController.ID,
|
||||
DeviceID: areaController.ID,
|
||||
CommandMetadata: childDeviceIDs,
|
||||
Status: models.PendingStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
@@ -223,7 +223,7 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, regionalControllerID
|
||||
logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, regionalController.ID)
|
||||
logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, areaController.ID)
|
||||
|
||||
// 5. 构建最终的空中载荷
|
||||
batchCmd := &proto.BatchCollectCommand{
|
||||
|
||||
@@ -11,30 +11,28 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/notify"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Service 定义了通知领域的核心业务逻辑接口
|
||||
type Service interface {
|
||||
// SendBatchAlarm 向一批用户发送告警通知。它会并发地为每个用户执行带故障转移的发送逻辑。
|
||||
SendBatchAlarm(ctx context.Context, userIDs []uint, content notify.AlarmContent) error
|
||||
SendBatchAlarm(ctx context.Context, userIDs []uint32, content notify.AlarmContent) error
|
||||
|
||||
// BroadcastAlarm 向所有用户发送告警通知。它会并发地为每个用户执行带故障转移的发送逻辑。
|
||||
BroadcastAlarm(ctx context.Context, content notify.AlarmContent) error
|
||||
|
||||
// SendTestMessage 向指定用户发送一条测试消息,用于手动验证特定通知渠道的配置。
|
||||
SendTestMessage(ctx context.Context, userID uint, notifierType notify.NotifierType) error
|
||||
SendTestMessage(ctx context.Context, userID uint32, notifierType models.NotifierType) error
|
||||
}
|
||||
|
||||
// failoverService 是 Service 接口的实现,提供了故障转移功能
|
||||
type failoverService struct {
|
||||
ctx context.Context
|
||||
userRepo repository.UserRepository
|
||||
notifiers map[notify.NotifierType]notify.Notifier
|
||||
notifiers map[models.NotifierType]notify.Notifier
|
||||
primaryNotifier notify.Notifier
|
||||
failureThreshold int
|
||||
failureCounters *sync.Map // 使用 sync.Map 来安全地并发读写失败计数, key: userID (uint), value: counter (int)
|
||||
failureCounters *sync.Map // 使用 sync.Map 来安全地并发读写失败计数, key: userID (uint32), value: counter (int)
|
||||
notificationRepo repository.NotificationRepository
|
||||
}
|
||||
|
||||
@@ -43,11 +41,11 @@ func NewFailoverService(
|
||||
ctx context.Context,
|
||||
userRepo repository.UserRepository,
|
||||
notifiers []notify.Notifier,
|
||||
primaryNotifierType notify.NotifierType,
|
||||
primaryNotifierType models.NotifierType,
|
||||
failureThreshold int,
|
||||
notificationRepo repository.NotificationRepository,
|
||||
) (Service, error) {
|
||||
notifierMap := make(map[notify.NotifierType]notify.Notifier)
|
||||
notifierMap := make(map[models.NotifierType]notify.Notifier)
|
||||
for _, n := range notifiers {
|
||||
notifierMap[n.Type()] = n
|
||||
}
|
||||
@@ -69,7 +67,7 @@ func NewFailoverService(
|
||||
}
|
||||
|
||||
// SendBatchAlarm 实现了向多个用户并发发送告警的功能
|
||||
func (s *failoverService) SendBatchAlarm(ctx context.Context, userIDs []uint, content notify.AlarmContent) error {
|
||||
func (s *failoverService) SendBatchAlarm(ctx context.Context, userIDs []uint32, content notify.AlarmContent) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendBatchAlarm")
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
@@ -79,7 +77,7 @@ func (s *failoverService) SendBatchAlarm(ctx context.Context, userIDs []uint, co
|
||||
|
||||
for _, userID := range userIDs {
|
||||
wg.Add(1)
|
||||
go func(id uint) {
|
||||
go func(id uint32) {
|
||||
defer wg.Done()
|
||||
if err := s.sendAlarmToUser(serviceCtx, id, content); err != nil {
|
||||
mu.Lock()
|
||||
@@ -110,7 +108,7 @@ func (s *failoverService) BroadcastAlarm(ctx context.Context, content notify.Ala
|
||||
return fmt.Errorf("广播告警失败:查找所有用户时出错: %w", err)
|
||||
}
|
||||
|
||||
var userIDs []uint
|
||||
var userIDs []uint32
|
||||
for _, user := range users {
|
||||
userIDs = append(userIDs, user.ID)
|
||||
}
|
||||
@@ -121,7 +119,7 @@ func (s *failoverService) BroadcastAlarm(ctx context.Context, content notify.Ala
|
||||
}
|
||||
|
||||
// sendAlarmToUser 是为单个用户发送告警的内部方法,包含了完整的故障转移逻辑
|
||||
func (s *failoverService) sendAlarmToUser(ctx context.Context, userID uint, content notify.AlarmContent) error {
|
||||
func (s *failoverService) sendAlarmToUser(ctx context.Context, userID uint32, content notify.AlarmContent) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "sendAlarmToUser")
|
||||
user, err := s.userRepo.FindByID(serviceCtx, userID)
|
||||
if err != nil {
|
||||
@@ -189,7 +187,7 @@ func (s *failoverService) sendAlarmToUser(ctx context.Context, userID uint, cont
|
||||
}
|
||||
|
||||
// SendTestMessage 实现了手动发送测试消息的功能
|
||||
func (s *failoverService) SendTestMessage(ctx context.Context, userID uint, notifierType notify.NotifierType) error {
|
||||
func (s *failoverService) SendTestMessage(ctx context.Context, userID uint32, notifierType models.NotifierType) error {
|
||||
serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendTestMessage")
|
||||
user, err := s.userRepo.FindByID(serviceCtx, userID)
|
||||
if err != nil {
|
||||
@@ -210,7 +208,7 @@ func (s *failoverService) SendTestMessage(ctx context.Context, userID uint, noti
|
||||
s.recordNotificationAttempt(serviceCtx, userID, notifierType, notify.AlarmContent{
|
||||
Title: "通知服务测试",
|
||||
Message: fmt.Sprintf("这是一条来自【%s】渠道的测试消息。如果您收到此消息,说明您的配置正确。", notifierType),
|
||||
Level: zap.InfoLevel,
|
||||
Level: models.InfoLevel,
|
||||
Timestamp: time.Now(),
|
||||
}, "", models.NotificationStatusFailed, fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifierType))
|
||||
return fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifierType)
|
||||
@@ -219,7 +217,7 @@ func (s *failoverService) SendTestMessage(ctx context.Context, userID uint, noti
|
||||
testContent := notify.AlarmContent{
|
||||
Title: "通知服务测试",
|
||||
Message: fmt.Sprintf("这是一条来自【%s】渠道的测试消息。如果您收到此消息,说明您的配置正确。", notifierType),
|
||||
Level: zap.InfoLevel,
|
||||
Level: models.InfoLevel,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
@@ -239,15 +237,15 @@ func (s *failoverService) SendTestMessage(ctx context.Context, userID uint, noti
|
||||
}
|
||||
|
||||
// getAddressForNotifier 是一个辅助函数,根据通知器类型从 ContactInfo 中获取对应的地址
|
||||
func getAddressForNotifier(notifierType notify.NotifierType, contact models.ContactInfo) string {
|
||||
func getAddressForNotifier(notifierType models.NotifierType, contact models.ContactInfo) string {
|
||||
switch notifierType {
|
||||
case notify.NotifierTypeSMTP:
|
||||
case models.NotifierTypeSMTP:
|
||||
return contact.Email
|
||||
case notify.NotifierTypeWeChat:
|
||||
case models.NotifierTypeWeChat:
|
||||
return contact.WeChat
|
||||
case notify.NotifierTypeLark:
|
||||
case models.NotifierTypeLark:
|
||||
return contact.Feishu
|
||||
case notify.NotifierTypeLog:
|
||||
case models.NotifierTypeLog:
|
||||
return "log" // LogNotifier不需要具体的地址,但为了函数签名一致性,返回一个无意义的非空字符串以绕过配置存在检查
|
||||
default:
|
||||
return ""
|
||||
@@ -263,8 +261,8 @@ func getAddressForNotifier(notifierType notify.NotifierType, contact models.Cont
|
||||
// err: 如果发送失败,记录的错误信息
|
||||
func (s *failoverService) recordNotificationAttempt(
|
||||
ctx context.Context,
|
||||
userID uint,
|
||||
notifierType notify.NotifierType,
|
||||
userID uint32,
|
||||
notifierType models.NotifierType,
|
||||
content notify.AlarmContent,
|
||||
toAddress string,
|
||||
status models.NotificationStatus,
|
||||
@@ -281,7 +279,7 @@ func (s *failoverService) recordNotificationAttempt(
|
||||
UserID: userID,
|
||||
Title: content.Title,
|
||||
Message: content.Message,
|
||||
Level: models.LogLevel(content.Level),
|
||||
Level: content.Level,
|
||||
AlarmTimestamp: content.Timestamp,
|
||||
ToAddress: toAddress,
|
||||
Status: status,
|
||||
|
||||
@@ -19,22 +19,22 @@ type PigPenTransferManager interface {
|
||||
LogTransfer(ctx context.Context, tx *gorm.DB, log *models.PigTransferLog) error
|
||||
|
||||
// GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。
|
||||
GetPenByID(ctx context.Context, tx *gorm.DB, penID uint) (*models.Pen, error)
|
||||
GetPenByID(ctx context.Context, tx *gorm.DB, penID uint32) (*models.Pen, error)
|
||||
|
||||
// GetPensByBatchID 获取一个猪群当前关联的所有猪栏。
|
||||
GetPensByBatchID(ctx context.Context, tx *gorm.DB, batchID uint) ([]*models.Pen, error)
|
||||
GetPensByBatchID(ctx context.Context, tx *gorm.DB, batchID uint32) ([]*models.Pen, error)
|
||||
|
||||
// UpdatePenFields 更新一个猪栏的指定字段。
|
||||
UpdatePenFields(ctx context.Context, tx *gorm.DB, penID uint, updates map[string]interface{}) error
|
||||
UpdatePenFields(ctx context.Context, tx *gorm.DB, penID uint32, updates map[string]interface{}) error
|
||||
|
||||
// GetCurrentPigsInPen 通过汇总猪只迁移日志,计算给定猪栏中的当前猪只数量。
|
||||
GetCurrentPigsInPen(ctx context.Context, tx *gorm.DB, penID uint) (int, error)
|
||||
GetCurrentPigsInPen(ctx context.Context, tx *gorm.DB, penID uint32) (int, error)
|
||||
|
||||
// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
|
||||
GetTotalPigsInPensForBatchTx(ctx context.Context, tx *gorm.DB, batchID uint) (int, error)
|
||||
GetTotalPigsInPensForBatchTx(ctx context.Context, tx *gorm.DB, batchID uint32) (int, error)
|
||||
|
||||
// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。
|
||||
ReleasePen(ctx context.Context, tx *gorm.DB, penID uint) error
|
||||
ReleasePen(ctx context.Context, tx *gorm.DB, penID uint32) error
|
||||
}
|
||||
|
||||
// pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。
|
||||
@@ -63,25 +63,25 @@ func (s *pigPenTransferManager) LogTransfer(ctx context.Context, tx *gorm.DB, lo
|
||||
}
|
||||
|
||||
// GetPenByID 实现了获取猪栏信息的逻辑。
|
||||
func (s *pigPenTransferManager) GetPenByID(ctx context.Context, tx *gorm.DB, penID uint) (*models.Pen, error) {
|
||||
func (s *pigPenTransferManager) GetPenByID(ctx context.Context, tx *gorm.DB, penID uint32) (*models.Pen, error) {
|
||||
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetPenByID")
|
||||
return s.penRepo.GetPenByIDTx(managerCtx, tx, penID)
|
||||
}
|
||||
|
||||
// GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。
|
||||
func (s *pigPenTransferManager) GetPensByBatchID(ctx context.Context, tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
|
||||
func (s *pigPenTransferManager) GetPensByBatchID(ctx context.Context, tx *gorm.DB, batchID uint32) ([]*models.Pen, error) {
|
||||
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetPensByBatchID")
|
||||
return s.penRepo.GetPensByBatchIDTx(managerCtx, tx, batchID)
|
||||
}
|
||||
|
||||
// UpdatePenFields 实现了更新猪栏字段的逻辑。
|
||||
func (s *pigPenTransferManager) UpdatePenFields(ctx context.Context, tx *gorm.DB, penID uint, updates map[string]interface{}) error {
|
||||
func (s *pigPenTransferManager) UpdatePenFields(ctx context.Context, tx *gorm.DB, penID uint32, updates map[string]interface{}) error {
|
||||
managerCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePenFields")
|
||||
return s.penRepo.UpdatePenFieldsTx(managerCtx, tx, penID, updates)
|
||||
}
|
||||
|
||||
// GetCurrentPigsInPen 实现了计算猪栏当前猪只数量的逻辑。
|
||||
func (s *pigPenTransferManager) GetCurrentPigsInPen(ctx context.Context, tx *gorm.DB, penID uint) (int, error) {
|
||||
func (s *pigPenTransferManager) GetCurrentPigsInPen(ctx context.Context, tx *gorm.DB, penID uint32) (int, error) {
|
||||
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigsInPen")
|
||||
// 1. 通过猪栏ID查出所属猪群信息
|
||||
pen, err := s.penRepo.GetPenByIDTx(managerCtx, tx, penID)
|
||||
@@ -137,7 +137,7 @@ func (s *pigPenTransferManager) GetCurrentPigsInPen(ctx context.Context, tx *gor
|
||||
|
||||
// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
|
||||
// 该方法通过遍历猪群下的每个猪栏,并调用 GetCurrentPigsInPen 来累加存栏数。
|
||||
func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(ctx context.Context, tx *gorm.DB, batchID uint) (int, error) {
|
||||
func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(ctx context.Context, tx *gorm.DB, batchID uint32) (int, error) {
|
||||
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetTotalPigsInPensForBatchTx")
|
||||
// 1. 获取该批次下所有猪栏的列表
|
||||
pensInBatch, err := s.GetPensByBatchID(managerCtx, tx, batchID)
|
||||
@@ -160,7 +160,7 @@ func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(ctx context.Context
|
||||
|
||||
// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。
|
||||
// 此操作通常在猪栏被清空后调用。
|
||||
func (s *pigPenTransferManager) ReleasePen(ctx context.Context, tx *gorm.DB, penID uint) error {
|
||||
func (s *pigPenTransferManager) ReleasePen(ctx context.Context, tx *gorm.DB, penID uint32) error {
|
||||
managerCtx := logs.AddFuncName(ctx, s.ctx, "ReleasePen")
|
||||
// 1. 获取猪栏信息
|
||||
pen, err := s.penRepo.GetPenByIDTx(managerCtx, tx, penID)
|
||||
|
||||
@@ -38,58 +38,58 @@ var (
|
||||
// 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。
|
||||
type PigBatchService interface {
|
||||
// CreatePigBatch 创建猪批次,并记录初始日志。
|
||||
CreatePigBatch(ctx context.Context, operatorID uint, batch *models.PigBatch) (*models.PigBatch, error)
|
||||
CreatePigBatch(ctx context.Context, operatorID uint32, batch *models.PigBatch) (*models.PigBatch, error)
|
||||
// GetPigBatch 获取单个猪批次。
|
||||
GetPigBatch(ctx context.Context, id uint) (*models.PigBatch, error)
|
||||
GetPigBatch(ctx context.Context, id uint32) (*models.PigBatch, error)
|
||||
// UpdatePigBatch 更新猪批次信息。
|
||||
UpdatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, error)
|
||||
// DeletePigBatch 删除猪批次,包含业务规则校验。
|
||||
DeletePigBatch(ctx context.Context, id uint) error
|
||||
DeletePigBatch(ctx context.Context, id uint32) error
|
||||
// ListPigBatches 批量查询猪批次。
|
||||
ListPigBatches(ctx context.Context, isActive *bool) ([]*models.PigBatch, error)
|
||||
// AssignEmptyPensToBatch 为猪群分配空栏
|
||||
AssignEmptyPensToBatch(ctx context.Context, batchID uint, penIDs []uint, operatorID uint) error
|
||||
AssignEmptyPensToBatch(ctx context.Context, batchID uint32, penIDs []uint32, operatorID uint32) error
|
||||
// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏
|
||||
MovePigsIntoPen(ctx context.Context, batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
|
||||
MovePigsIntoPen(ctx context.Context, batchID uint32, toPenID uint32, quantity int, operatorID uint32, remarks string) error
|
||||
// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群
|
||||
ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
|
||||
ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint32, toBatchID uint32, penID uint32, operatorID uint32, remarks string) error
|
||||
// RemoveEmptyPenFromBatch 将一个猪栏移除出猪群,此方法需要在猪栏为空的情况下执行。
|
||||
RemoveEmptyPenFromBatch(ctx context.Context, batchID uint, penID uint) error
|
||||
RemoveEmptyPenFromBatch(ctx context.Context, batchID uint32, penID uint32) error
|
||||
|
||||
// GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。
|
||||
GetCurrentPigQuantity(ctx context.Context, batchID uint) (int, error)
|
||||
GetCurrentPigQuantity(ctx context.Context, batchID uint32) (int, error)
|
||||
// GetCurrentPigsInPen 获取指定猪栏的当前存栏量。
|
||||
GetCurrentPigsInPen(ctx context.Context, penID uint) (int, error)
|
||||
GetCurrentPigsInPen(ctx context.Context, penID uint32) (int, error)
|
||||
// GetTotalPigsInPensForBatch 获取指定猪群下所有猪栏的当前总存栏数
|
||||
GetTotalPigsInPensForBatch(ctx context.Context, batchID uint) (int, error)
|
||||
GetTotalPigsInPensForBatch(ctx context.Context, batchID uint32) (int, error)
|
||||
|
||||
UpdatePigBatchQuantity(ctx context.Context, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error
|
||||
UpdatePigBatchQuantity(ctx context.Context, operatorID uint32, batchID uint32, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error
|
||||
|
||||
// ---交易子服务---
|
||||
// SellPigs 处理卖猪的业务逻辑。
|
||||
SellPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
|
||||
SellPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, tatalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error
|
||||
// BuyPigs 处理买猪的业务逻辑。
|
||||
BuyPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
|
||||
BuyPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, tatalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error
|
||||
|
||||
// ---调栏子服务 ---
|
||||
TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
|
||||
TransferPigsWithinBatch(ctx context.Context, batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
|
||||
TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint32, destBatchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error
|
||||
TransferPigsWithinBatch(ctx context.Context, batchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error
|
||||
|
||||
// --- 病猪管理相关方法 ---
|
||||
// RecordSickPigs 记录新增病猪事件。
|
||||
RecordSickPigs(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigs(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
// RecordSickPigRecovery 记录病猪康复事件。
|
||||
RecordSickPigRecovery(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigRecovery(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
// RecordSickPigDeath 记录病猪死亡事件。
|
||||
RecordSickPigDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigDeath(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
// RecordSickPigCull 记录病猪淘汰事件。
|
||||
RecordSickPigCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
RecordSickPigCull(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
|
||||
|
||||
// --- 正常猪只管理相关方法 ---
|
||||
// RecordDeath 记录正常猪只死亡事件。
|
||||
RecordDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
|
||||
RecordDeath(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, happenedAt time.Time, remarks string) error
|
||||
// RecordCull 记录正常猪只淘汰事件。
|
||||
RecordCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
|
||||
RecordCull(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, happenedAt time.Time, remarks string) error
|
||||
}
|
||||
|
||||
// pigBatchService 是 PigBatchService 接口的具体实现。
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// --- 领域服务实现 ---
|
||||
|
||||
// CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。
|
||||
func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) {
|
||||
func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint32, batch *models.PigBatch) (*models.PigBatch, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigBatch")
|
||||
// 业务规则可以在这里添加,例如检查批次号是否唯一等
|
||||
|
||||
@@ -57,7 +57,7 @@ func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint, b
|
||||
}
|
||||
|
||||
// GetPigBatch 实现了获取单个猪批次的逻辑。
|
||||
func (s *pigBatchService) GetPigBatch(ctx context.Context, id uint) (*models.PigBatch, error) {
|
||||
func (s *pigBatchService) GetPigBatch(ctx context.Context, id uint32) (*models.PigBatch, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigBatch")
|
||||
batch, err := s.pigBatchRepo.GetPigBatchByID(serviceCtx, id)
|
||||
if err != nil {
|
||||
@@ -84,7 +84,7 @@ func (s *pigBatchService) UpdatePigBatch(ctx context.Context, batch *models.PigB
|
||||
}
|
||||
|
||||
// DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。
|
||||
func (s *pigBatchService) DeletePigBatch(ctx context.Context, id uint) error {
|
||||
func (s *pigBatchService) DeletePigBatch(ctx context.Context, id uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigBatch")
|
||||
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
// 1. 获取猪批次信息
|
||||
@@ -135,7 +135,7 @@ func (s *pigBatchService) ListPigBatches(ctx context.Context, isActive *bool) ([
|
||||
}
|
||||
|
||||
// GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。
|
||||
func (s *pigBatchService) GetCurrentPigQuantity(ctx context.Context, batchID uint) (int, error) {
|
||||
func (s *pigBatchService) GetCurrentPigQuantity(ctx context.Context, batchID uint32) (int, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigQuantity")
|
||||
var getErr error
|
||||
var quantity int
|
||||
@@ -150,7 +150,7 @@ func (s *pigBatchService) GetCurrentPigQuantity(ctx context.Context, batchID uin
|
||||
}
|
||||
|
||||
// getCurrentPigQuantityTx 实现了获取指定猪批次的当前猪只数量的逻辑。
|
||||
func (s *pigBatchService) getCurrentPigQuantityTx(ctx context.Context, tx *gorm.DB, batchID uint) (int, error) {
|
||||
func (s *pigBatchService) getCurrentPigQuantityTx(ctx context.Context, tx *gorm.DB, batchID uint32) (int, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "getCurrentPigQuantityTx")
|
||||
// 1. 获取猪批次初始信息
|
||||
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
|
||||
@@ -175,14 +175,14 @@ func (s *pigBatchService) getCurrentPigQuantityTx(ctx context.Context, tx *gorm.
|
||||
return lastLog.AfterCount, nil
|
||||
}
|
||||
|
||||
func (s *pigBatchService) UpdatePigBatchQuantity(ctx context.Context, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
|
||||
func (s *pigBatchService) UpdatePigBatchQuantity(ctx context.Context, operatorID uint32, batchID uint32, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigBatchQuantity")
|
||||
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
return s.updatePigBatchQuantityTx(serviceCtx, tx, operatorID, batchID, changeType, changeAmount, changeReason, happenedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *pigBatchService) updatePigBatchQuantityTx(ctx context.Context, tx *gorm.DB, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
|
||||
func (s *pigBatchService) updatePigBatchQuantityTx(ctx context.Context, tx *gorm.DB, operatorID uint32, batchID uint32, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "updatePigBatchQuantityTx")
|
||||
lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(serviceCtx, tx, batchID)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。
|
||||
func (s *pigBatchService) executeTransferAndLog(ctx context.Context, tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType models.PigTransferType, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) executeTransferAndLog(ctx context.Context, tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint32, quantity int, transferType models.PigTransferType, operatorID uint32, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "executeTransferAndLog")
|
||||
|
||||
// 通用校验:任何调出操作都不能超过源猪栏的当前存栏数
|
||||
@@ -67,7 +67,7 @@ func (s *pigBatchService) executeTransferAndLog(ctx context.Context, tx *gorm.DB
|
||||
}
|
||||
|
||||
// TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。
|
||||
func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "TransferPigsWithinBatch")
|
||||
if fromPenID == toPenID {
|
||||
return errors.New("源猪栏和目标猪栏不能相同")
|
||||
@@ -106,7 +106,7 @@ func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID u
|
||||
}
|
||||
|
||||
// TransferPigsAcrossBatches 实现了跨猪群的调栏业务。
|
||||
func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint32, destBatchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "TransferPigsAcrossBatches")
|
||||
if sourceBatchID == destBatchID {
|
||||
return errors.New("源猪群和目标猪群不能相同")
|
||||
@@ -167,7 +167,7 @@ func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceB
|
||||
}
|
||||
|
||||
// AssignEmptyPensToBatch 为猪群分配空栏
|
||||
func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID uint, penIDs []uint, operatorID uint) error {
|
||||
func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID uint32, penIDs []uint32, operatorID uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "AssignEmptyPensToBatch")
|
||||
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
// 1. 验证猪批次是否存在且活跃
|
||||
@@ -204,6 +204,7 @@ func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID ui
|
||||
updates := map[string]interface{}{
|
||||
"pig_batch_id": &batchID,
|
||||
"status": models.PenStatusOccupied,
|
||||
"operator_id": operatorID,
|
||||
}
|
||||
if err := s.transferSvc.UpdatePenFields(serviceCtx, tx, penID, updates); err != nil {
|
||||
return fmt.Errorf("分配猪栏 %d 失败: %w", penID, err)
|
||||
@@ -215,7 +216,7 @@ func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID ui
|
||||
}
|
||||
|
||||
// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏
|
||||
func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint32, toPenID uint32, quantity int, operatorID uint32, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "MovePigsIntoPen")
|
||||
if quantity <= 0 {
|
||||
return errors.New("迁移数量必须大于零")
|
||||
@@ -287,7 +288,7 @@ func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint, toP
|
||||
}
|
||||
|
||||
// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群
|
||||
func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
|
||||
func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint32, toBatchID uint32, penID uint32, operatorID uint32, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ReclassifyPenToNewBatch")
|
||||
if fromBatchID == toBatchID {
|
||||
return errors.New("源猪群和目标猪群不能相同")
|
||||
@@ -392,7 +393,7 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatch
|
||||
})
|
||||
}
|
||||
|
||||
func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID uint, penID uint) error {
|
||||
func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID uint32, penID uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RemoveEmptyPenFromBatch")
|
||||
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
// 1. 检查猪批次是否存在且活跃
|
||||
@@ -438,7 +439,7 @@ func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID u
|
||||
})
|
||||
}
|
||||
|
||||
func (s *pigBatchService) GetCurrentPigsInPen(ctx context.Context, penID uint) (int, error) {
|
||||
func (s *pigBatchService) GetCurrentPigsInPen(ctx context.Context, penID uint32) (int, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigsInPen")
|
||||
var currentPigs int
|
||||
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
@@ -453,7 +454,7 @@ func (s *pigBatchService) GetCurrentPigsInPen(ctx context.Context, penID uint) (
|
||||
}
|
||||
|
||||
// GetTotalPigsInPensForBatch 实现了获取指定猪群下所有猪栏的当前总存栏数的逻辑。
|
||||
func (s *pigBatchService) GetTotalPigsInPensForBatch(ctx context.Context, batchID uint) (int, error) {
|
||||
func (s *pigBatchService) GetTotalPigsInPensForBatch(ctx context.Context, batchID uint32) (int, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetTotalPigsInPensForBatch")
|
||||
var totalPigs int
|
||||
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
// RecordSickPigs 记录新增病猪事件。
|
||||
func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordSickPigs")
|
||||
if quantity <= 0 {
|
||||
return errors.New("新增病猪数量必须大于0")
|
||||
@@ -89,7 +89,7 @@ func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint, b
|
||||
}
|
||||
|
||||
// RecordSickPigRecovery 记录病猪康复事件。
|
||||
func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordSickPigRecovery")
|
||||
if quantity <= 0 {
|
||||
return errors.New("康复猪只数量必须大于0")
|
||||
@@ -158,7 +158,7 @@ func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID
|
||||
}
|
||||
|
||||
// RecordSickPigDeath 记录病猪死亡事件。
|
||||
func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordSickPigDeath")
|
||||
if quantity <= 0 {
|
||||
return errors.New("死亡猪只数量必须大于0")
|
||||
@@ -254,7 +254,7 @@ func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uin
|
||||
}
|
||||
|
||||
// RecordSickPigCull 记录病猪淘汰事件。
|
||||
func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordSickPigCull")
|
||||
if quantity <= 0 {
|
||||
return errors.New("淘汰猪只数量必须大于0")
|
||||
@@ -350,7 +350,7 @@ func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint
|
||||
}
|
||||
|
||||
// RecordDeath 记录正常猪只死亡事件。
|
||||
func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordDeath")
|
||||
if quantity <= 0 {
|
||||
return errors.New("死亡猪只数量必须大于0")
|
||||
@@ -421,7 +421,7 @@ func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint, batc
|
||||
}
|
||||
|
||||
// RecordCull 记录正常猪只淘汰事件。
|
||||
func (s *pigBatchService) RecordCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
|
||||
func (s *pigBatchService) RecordCull(ctx context.Context, operatorID uint32, batchID uint32, penID uint32, quantity int, happenedAt time.Time, remarks string) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordCull")
|
||||
if quantity <= 0 {
|
||||
return errors.New("淘汰猪只数量必须大于0")
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
// SellPigs 处理批量销售猪的业务逻辑。
|
||||
func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
|
||||
func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, tatalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "SellPigs")
|
||||
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
if quantity <= 0 {
|
||||
@@ -85,7 +85,7 @@ func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint, penID uint
|
||||
}
|
||||
|
||||
// BuyPigs 处理批量购买猪的业务逻辑。
|
||||
func (s *pigBatchService) BuyPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
|
||||
func (s *pigBatchService) BuyPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, totalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "BuyPigs")
|
||||
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
|
||||
if quantity <= 0 {
|
||||
|
||||
@@ -21,7 +21,7 @@ type SickPigManager interface {
|
||||
ProcessSickPigLog(ctx context.Context, tx *gorm.DB, log *models.PigSickLog) error
|
||||
|
||||
// GetCurrentSickPigCount 获取指定批次当前患病猪只的总数
|
||||
GetCurrentSickPigCount(ctx context.Context, tx *gorm.DB, batchID uint) (int, error)
|
||||
GetCurrentSickPigCount(ctx context.Context, tx *gorm.DB, batchID uint32) (int, error)
|
||||
}
|
||||
|
||||
// sickPigManager 是 SickPigManager 接口的具体实现。
|
||||
@@ -122,7 +122,7 @@ func (s *sickPigManager) ProcessSickPigLog(ctx context.Context, tx *gorm.DB, log
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sickPigManager) GetCurrentSickPigCount(ctx context.Context, tx *gorm.DB, batchID uint) (int, error) {
|
||||
func (s *sickPigManager) GetCurrentSickPigCount(ctx context.Context, tx *gorm.DB, batchID uint32) (int, error) {
|
||||
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentSickPigCount")
|
||||
lastLog, err := s.sickLogRepo.GetLastLogByBatchTx(managerCtx, tx, batchID)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,10 +18,10 @@ type AnalysisPlanTaskManager interface {
|
||||
Refresh(ctx context.Context) error
|
||||
// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。
|
||||
// 如果触发器已存在,会根据计划类型更新其执行时间。
|
||||
CreateOrUpdateTrigger(ctx context.Context, planID uint) error
|
||||
CreateOrUpdateTrigger(ctx context.Context, planID uint32) error
|
||||
// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。
|
||||
// 如果不存在,则会自动创建。此方法不涉及待执行队列。
|
||||
EnsureAnalysisTaskDefinition(ctx context.Context, planID uint) error
|
||||
EnsureAnalysisTaskDefinition(ctx context.Context, planID uint32) error
|
||||
}
|
||||
|
||||
// analysisPlanTaskManagerImpl 负责管理分析计划的触发器任务。
|
||||
@@ -82,7 +82,7 @@ func (m *analysisPlanTaskManagerImpl) Refresh(ctx context.Context) error {
|
||||
|
||||
// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。
|
||||
// 如果触发器已存在,会根据计划类型更新其执行时间。
|
||||
func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(ctx context.Context, planID uint) error {
|
||||
func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(ctx context.Context, planID uint32) error {
|
||||
managerCtx, logger := logs.Trace(ctx, m.ctx, "CreateOrUpdateTrigger")
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -138,7 +138,7 @@ func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(ctx context.Context,
|
||||
|
||||
// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。
|
||||
// 如果不存在,则会自动创建。此方法不涉及待执行队列。
|
||||
func (m *analysisPlanTaskManagerImpl) EnsureAnalysisTaskDefinition(ctx context.Context, planID uint) error {
|
||||
func (m *analysisPlanTaskManagerImpl) EnsureAnalysisTaskDefinition(ctx context.Context, planID uint32) error {
|
||||
managerCtx, logger := logs.Trace(ctx, m.ctx, "EnsureAnalysisTaskDefinition")
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -170,7 +170,7 @@ func (m *analysisPlanTaskManagerImpl) EnsureAnalysisTaskDefinition(ctx context.C
|
||||
// --- 内部私有方法 ---
|
||||
|
||||
// getRefreshData 从数据库获取刷新所需的所有数据。
|
||||
func (m *analysisPlanTaskManagerImpl) getRefreshData(ctx context.Context) (runnablePlans []*models.Plan, invalidPlanIDs []uint, pendingTasks []models.PendingTask, err error) {
|
||||
func (m *analysisPlanTaskManagerImpl) getRefreshData(ctx context.Context) (runnablePlans []*models.Plan, invalidPlanIDs []uint32, pendingTasks []models.PendingTask, err error) {
|
||||
managerCtx, logger := logs.Trace(ctx, m.ctx, "getRefreshData")
|
||||
runnablePlans, err = m.planRepo.FindRunnablePlans(managerCtx)
|
||||
if err != nil {
|
||||
@@ -183,7 +183,7 @@ func (m *analysisPlanTaskManagerImpl) getRefreshData(ctx context.Context) (runna
|
||||
logger.Errorf("获取失效计划列表失败: %v", err)
|
||||
return
|
||||
}
|
||||
invalidPlanIDs = make([]uint, len(invalidPlans))
|
||||
invalidPlanIDs = make([]uint32, len(invalidPlans))
|
||||
for i, p := range invalidPlans {
|
||||
invalidPlanIDs[i] = p.ID
|
||||
}
|
||||
@@ -197,19 +197,19 @@ func (m *analysisPlanTaskManagerImpl) getRefreshData(ctx context.Context) (runna
|
||||
}
|
||||
|
||||
// cleanupInvalidTasks 清理所有与失效计划相关的待执行任务。
|
||||
func (m *analysisPlanTaskManagerImpl) cleanupInvalidTasks(ctx context.Context, invalidPlanIDs []uint, allPendingTasks []models.PendingTask) error {
|
||||
func (m *analysisPlanTaskManagerImpl) cleanupInvalidTasks(ctx context.Context, invalidPlanIDs []uint32, allPendingTasks []models.PendingTask) error {
|
||||
managerCtx, logger := logs.Trace(ctx, m.ctx, "cleanupInvalidTasks")
|
||||
if len(invalidPlanIDs) == 0 {
|
||||
return nil // 没有需要清理的计划
|
||||
}
|
||||
|
||||
invalidPlanIDSet := make(map[uint]struct{}, len(invalidPlanIDs))
|
||||
invalidPlanIDSet := make(map[uint32]struct{}, len(invalidPlanIDs))
|
||||
for _, id := range invalidPlanIDs {
|
||||
invalidPlanIDSet[id] = struct{}{}
|
||||
}
|
||||
|
||||
var tasksToDeleteIDs []uint
|
||||
var logsToCancelIDs []uint
|
||||
var tasksToDeleteIDs []uint32
|
||||
var logsToCancelIDs []uint32
|
||||
|
||||
for _, pt := range allPendingTasks {
|
||||
if pt.Task == nil { // 防御性编程,确保 Task 被预加载
|
||||
@@ -245,7 +245,7 @@ func (m *analysisPlanTaskManagerImpl) cleanupInvalidTasks(ctx context.Context, i
|
||||
func (m *analysisPlanTaskManagerImpl) addOrUpdateTriggers(ctx context.Context, runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error {
|
||||
managerCtx, logger := logs.Trace(ctx, m.ctx, "addOrUpdateTriggers")
|
||||
// 创建一个映射,存放所有已在队列中的计划触发器
|
||||
pendingTriggersMap := make(map[uint]models.PendingTask)
|
||||
pendingTriggersMap := make(map[uint32]models.PendingTask)
|
||||
for _, pt := range allPendingTasks {
|
||||
if pt.Task != nil && pt.Task.Type == models.TaskPlanAnalysis {
|
||||
pendingTriggersMap[pt.Task.PlanID] = pt
|
||||
|
||||
@@ -25,21 +25,21 @@ type ExecutionManager interface {
|
||||
// ProgressTracker 仅用于在内存中提供计划执行的并发锁
|
||||
type ProgressTracker struct {
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond // 用于实现阻塞锁
|
||||
runningPlans map[uint]bool // key: planExecutionLogID, value: true (用作内存锁)
|
||||
cond *sync.Cond // 用于实现阻塞锁
|
||||
runningPlans map[uint32]bool // key: planExecutionLogID, value: true (用作内存锁)
|
||||
}
|
||||
|
||||
// NewProgressTracker 创建一个新的进度跟踪器
|
||||
func NewProgressTracker() *ProgressTracker {
|
||||
t := &ProgressTracker{
|
||||
runningPlans: make(map[uint]bool),
|
||||
runningPlans: make(map[uint32]bool),
|
||||
}
|
||||
t.cond = sync.NewCond(&t.mu)
|
||||
return t
|
||||
}
|
||||
|
||||
// TryLock (非阻塞) 尝试锁定一个计划。如果计划未被锁定,则锁定并返回 true。
|
||||
func (t *ProgressTracker) TryLock(planLogID uint) bool {
|
||||
func (t *ProgressTracker) TryLock(planLogID uint32) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.runningPlans[planLogID] {
|
||||
@@ -50,7 +50,7 @@ func (t *ProgressTracker) TryLock(planLogID uint) bool {
|
||||
}
|
||||
|
||||
// Lock (阻塞) 获取一个计划的执行锁。如果锁已被占用,则会一直等待直到锁被释放。
|
||||
func (t *ProgressTracker) Lock(planLogID uint) {
|
||||
func (t *ProgressTracker) Lock(planLogID uint32) {
|
||||
t.mu.Lock()
|
||||
// 当计划正在运行时,调用 t.cond.Wait() 会原子地解锁 mu 并挂起当前协程。
|
||||
// 当被唤醒时,它会重新锁定 mu 并再次检查循环条件。
|
||||
@@ -63,7 +63,7 @@ func (t *ProgressTracker) Lock(planLogID uint) {
|
||||
}
|
||||
|
||||
// Unlock 解锁一个计划,并唤醒所有正在等待此锁的协程。
|
||||
func (t *ProgressTracker) Unlock(planLogID uint) {
|
||||
func (t *ProgressTracker) Unlock(planLogID uint32) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
delete(t.runningPlans, planLogID)
|
||||
@@ -72,10 +72,10 @@ func (t *ProgressTracker) Unlock(planLogID uint) {
|
||||
}
|
||||
|
||||
// GetRunningPlanIDs 获取当前所有正在执行的计划ID列表
|
||||
func (t *ProgressTracker) GetRunningPlanIDs() []uint {
|
||||
func (t *ProgressTracker) GetRunningPlanIDs() []uint32 {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
ids := make([]uint, 0, len(t.runningPlans))
|
||||
ids := make([]uint32, 0, len(t.runningPlans))
|
||||
for id := range t.runningPlans {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
@@ -214,7 +214,7 @@ func (s *planExecutionManagerImpl) claimAndSubmit(ctx context.Context) {
|
||||
}
|
||||
|
||||
// handleRequeue 同步地、安全地将一个无法立即执行的任务放回队列。
|
||||
func (s *planExecutionManagerImpl) handleRequeue(ctx context.Context, planExecutionLogID uint, taskToRequeue *models.PendingTask) {
|
||||
func (s *planExecutionManagerImpl) handleRequeue(ctx context.Context, planExecutionLogID uint32, taskToRequeue *models.PendingTask) {
|
||||
managerCtx, logger := logs.Trace(ctx, s.ctx, "handleRequeue")
|
||||
logger.Warnf("计划 %d 正在执行,任务 %d (TaskID: %d) 将等待并重新入队...", planExecutionLogID, taskToRequeue.ID, taskToRequeue.TaskID)
|
||||
|
||||
@@ -308,7 +308,7 @@ func (s *planExecutionManagerImpl) analysisPlan(ctx context.Context, claimedLog
|
||||
// 创建Plan执行记录
|
||||
// 从任务的 Parameters 中解析出真实的 PlanID
|
||||
var params struct {
|
||||
PlanID uint `json:"plan_id"`
|
||||
PlanID uint32 `json:"plan_id"`
|
||||
}
|
||||
if err := claimedLog.Task.ParseParameters(¶ms); err != nil {
|
||||
logger.Errorf("解析任务参数中的计划ID失败,日志ID: %d, 错误: %v", claimedLog.ID, err)
|
||||
@@ -390,7 +390,7 @@ func (s *planExecutionManagerImpl) updateTaskExecutionLogStatus(ctx context.Cont
|
||||
}
|
||||
|
||||
// handlePlanTermination 集中处理计划的终止逻辑(失败或取消)
|
||||
func (s *planExecutionManagerImpl) handlePlanTermination(ctx context.Context, planLogID uint, reason string) {
|
||||
func (s *planExecutionManagerImpl) handlePlanTermination(ctx context.Context, planLogID uint32, reason string) {
|
||||
managerCtx, logger := logs.Trace(ctx, s.ctx, "handlePlanTermination")
|
||||
// 1. 从待执行队列中删除所有相关的子任务
|
||||
if err := s.pendingTaskRepo.DeletePendingTasksByPlanLogID(managerCtx, planLogID); err != nil {
|
||||
@@ -434,7 +434,7 @@ func (s *planExecutionManagerImpl) handlePlanTermination(ctx context.Context, pl
|
||||
}
|
||||
|
||||
// handlePlanCompletion 集中处理计划成功完成后的所有逻辑
|
||||
func (s *planExecutionManagerImpl) handlePlanCompletion(ctx context.Context, planLogID uint) {
|
||||
func (s *planExecutionManagerImpl) handlePlanCompletion(ctx context.Context, planLogID uint32) {
|
||||
managerCtx, logger := logs.Trace(ctx, s.ctx, "handlePlanCompletion")
|
||||
logger.Infof("计划执行 %d 的所有任务已完成,开始处理计划完成逻辑...", planLogID)
|
||||
|
||||
|
||||
@@ -41,17 +41,17 @@ type Service interface {
|
||||
// CreatePlan 创建一个新的计划
|
||||
CreatePlan(ctx context.Context, plan *models.Plan) (*models.Plan, error)
|
||||
// GetPlanByID 根据ID获取计划详情
|
||||
GetPlanByID(ctx context.Context, id uint) (*models.Plan, error)
|
||||
GetPlanByID(ctx context.Context, id uint32) (*models.Plan, error)
|
||||
// ListPlans 获取计划列表,支持过滤和分页
|
||||
ListPlans(ctx context.Context, opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)
|
||||
// UpdatePlan 更新计划
|
||||
UpdatePlan(ctx context.Context, plan *models.Plan) (*models.Plan, error)
|
||||
// UpdatePlan 更新计划, wantPlanType 表示期望被修改的计划是什么类型
|
||||
UpdatePlan(ctx context.Context, plan *models.Plan, wantPlanType models.PlanType) (*models.Plan, error)
|
||||
// DeletePlan 删除计划(软删除)
|
||||
DeletePlan(ctx context.Context, id uint) error
|
||||
DeletePlan(ctx context.Context, id uint32) error
|
||||
// StartPlan 启动计划
|
||||
StartPlan(ctx context.Context, id uint) error
|
||||
StartPlan(ctx context.Context, id uint32) error
|
||||
// StopPlan 停止计划
|
||||
StopPlan(ctx context.Context, id uint) error
|
||||
StopPlan(ctx context.Context, id uint32) error
|
||||
}
|
||||
|
||||
// planServiceImpl 是 Service 接口的具体实现。
|
||||
@@ -150,7 +150,7 @@ func (s *planServiceImpl) CreatePlan(ctx context.Context, planToCreate *models.P
|
||||
// 优化:无需查询完整的设备对象,只需构建包含ID的结构体即可建立关联
|
||||
devices := make([]models.Device, len(deviceIDs))
|
||||
for i, id := range deviceIDs {
|
||||
devices[i] = models.Device{Model: gorm.Model{ID: id}}
|
||||
devices[i] = models.Device{Model: models.Model{ID: id}}
|
||||
}
|
||||
taskModel.Devices = devices
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func (s *planServiceImpl) CreatePlan(ctx context.Context, planToCreate *models.P
|
||||
}
|
||||
|
||||
// GetPlanByID 根据ID获取计划详情
|
||||
func (s *planServiceImpl) GetPlanByID(ctx context.Context, id uint) (*models.Plan, error) {
|
||||
func (s *planServiceImpl) GetPlanByID(ctx context.Context, id uint32) (*models.Plan, error) {
|
||||
planCtx, logger := logs.Trace(ctx, s.ctx, "GetPlanByID")
|
||||
const actionType = "领域层:获取计划详情"
|
||||
|
||||
@@ -207,8 +207,8 @@ func (s *planServiceImpl) ListPlans(ctx context.Context, opts repository.ListPla
|
||||
return plans, total, nil
|
||||
}
|
||||
|
||||
// UpdatePlan 更新计划
|
||||
func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.Plan) (*models.Plan, error) {
|
||||
// UpdatePlan 更新计划, wantPlanType 表示期望被修改的计划是什么类型
|
||||
func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.Plan, wantPlanType models.PlanType) (*models.Plan, error) {
|
||||
planCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePlan")
|
||||
const actionType = "领域层:更新计划"
|
||||
|
||||
@@ -222,9 +222,8 @@ func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.P
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 系统计划不允许修改
|
||||
if existingPlan.PlanType == models.PlanTypeSystem {
|
||||
logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, planToUpdate.ID)
|
||||
if existingPlan.PlanType != wantPlanType {
|
||||
logger.Warnf("%s: 禁止修改 %v 类型计划, ID: %d", actionType, wantPlanType, planToUpdate.ID)
|
||||
return nil, ErrPlanCannotBeModified
|
||||
}
|
||||
|
||||
@@ -263,7 +262,7 @@ func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.P
|
||||
// 优化:无需查询完整的设备对象,只需构建包含ID的结构体即可建立关联
|
||||
devices := make([]models.Device, len(deviceIDs))
|
||||
for i, id := range deviceIDs {
|
||||
devices[i] = models.Device{Model: gorm.Model{ID: id}}
|
||||
devices[i] = models.Device{Model: models.Model{ID: id}}
|
||||
}
|
||||
taskModel.Devices = devices
|
||||
}
|
||||
@@ -291,7 +290,7 @@ func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.P
|
||||
}
|
||||
|
||||
// DeletePlan 删除计划(软删除)
|
||||
func (s *planServiceImpl) DeletePlan(ctx context.Context, id uint) error {
|
||||
func (s *planServiceImpl) DeletePlan(ctx context.Context, id uint32) error {
|
||||
planCtx, logger := logs.Trace(ctx, s.ctx, "DeletePlan")
|
||||
const actionType = "领域层:删除计划"
|
||||
|
||||
@@ -329,7 +328,7 @@ func (s *planServiceImpl) DeletePlan(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
// StartPlan 启动计划
|
||||
func (s *planServiceImpl) StartPlan(ctx context.Context, id uint) error {
|
||||
func (s *planServiceImpl) StartPlan(ctx context.Context, id uint32) error {
|
||||
planCtx, logger := logs.Trace(ctx, s.ctx, "StartPlan")
|
||||
const actionType = "领域层:启动计划"
|
||||
|
||||
@@ -384,7 +383,7 @@ func (s *planServiceImpl) StartPlan(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
// StopPlan 停止计划
|
||||
func (s *planServiceImpl) StopPlan(ctx context.Context, id uint) error {
|
||||
func (s *planServiceImpl) StopPlan(ctx context.Context, id uint32) error {
|
||||
planCtx, logger := logs.Trace(ctx, s.ctx, "StopPlan")
|
||||
const actionType = "领域层:停止计划"
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ type Task interface {
|
||||
// TaskDeviceIDResolver 定义了从任务配置中解析设备ID的方法
|
||||
type TaskDeviceIDResolver interface {
|
||||
// ResolveDeviceIDs 从任务配置中解析并返回所有关联的设备ID列表
|
||||
// 返回值: uint数组,每个字符串代表一个设备ID
|
||||
ResolveDeviceIDs(ctx context.Context) ([]uint, error)
|
||||
// 返回值: uint32数组,每个字符串代表一个设备ID
|
||||
ResolveDeviceIDs(ctx context.Context) ([]uint32, error)
|
||||
}
|
||||
|
||||
// TaskFactory 是一个工厂接口,用于根据任务执行日志创建任务实例。
|
||||
|
||||
138
internal/domain/task/alarm_notification_task.go
Normal file
138
internal/domain/task/alarm_notification_task.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
notify_domain "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
|
||||
"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/notify"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// AlarmNotificationTaskParams 定义了 AlarmNotificationTask 的参数结构
|
||||
// 如果用户没有指定某个等级的配置, 则默认为该等级消息只发送一次
|
||||
type AlarmNotificationTaskParams struct {
|
||||
// NotificationIntervals 告警通知的发送间隔时间,键为告警等级,值为时间间隔(分钟)
|
||||
NotificationIntervals map[models.SeverityLevel]uint32 `json:"notification_intervals"`
|
||||
}
|
||||
|
||||
// AlarmNotificationTask 告警通知发送任务
|
||||
type AlarmNotificationTask struct {
|
||||
ctx context.Context
|
||||
taskLog *models.TaskExecutionLog
|
||||
|
||||
// alarmNotificationTaskParams 是任务配置
|
||||
alarmNotificationTaskParams AlarmNotificationTaskParams
|
||||
|
||||
onceParse sync.Once // 保证解析参数只执行一次
|
||||
|
||||
notificationService notify_domain.Service
|
||||
alarmRepository repository.AlarmRepository
|
||||
}
|
||||
|
||||
// NewAlarmNotificationTask 创建一个新的告警通知发送任务实例
|
||||
func NewAlarmNotificationTask(ctx context.Context, taskLog *models.TaskExecutionLog, service notify_domain.Service, alarmRepository repository.AlarmRepository) plan.Task {
|
||||
return &AlarmNotificationTask{
|
||||
ctx: ctx,
|
||||
taskLog: taskLog,
|
||||
alarmRepository: alarmRepository,
|
||||
notificationService: service,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行告警通知发送任务
|
||||
func (t *AlarmNotificationTask) Execute(ctx context.Context) error {
|
||||
taskCtx, logger := logs.Trace(ctx, t.ctx, "Execute")
|
||||
logger.Infof("开始执行告警通知发送任务, 任务ID: %d", t.taskLog.TaskID)
|
||||
|
||||
if err := t.parseParameters(taskCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取是否有待发送告警通知, 用于优化性能
|
||||
alarmsCount, err := t.alarmRepository.CountAlarmsForNotification(taskCtx, t.alarmNotificationTaskParams.NotificationIntervals)
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 获取告警数量失败: %v", t.taskLog.TaskID, err)
|
||||
return err
|
||||
}
|
||||
if alarmsCount == 0 {
|
||||
logger.Debugf("没有待发送的告警通知, 跳过任务, 任务ID: %d", t.taskLog.TaskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取所有待发送的告警通知
|
||||
alarms, err := t.alarmRepository.ListAlarmsForNotification(taskCtx, t.alarmNotificationTaskParams.NotificationIntervals)
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 获取告警列表失败: %v", t.taskLog.TaskID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送通知
|
||||
for _, alarm := range alarms {
|
||||
// TODO 因为还没做权限管理, 所以暂时通过广播形式发给所有用户
|
||||
err = t.notificationService.BroadcastAlarm(taskCtx, notify.AlarmContent{
|
||||
Title: alarm.AlarmSummary,
|
||||
Message: alarm.AlarmDetails,
|
||||
Level: alarm.Level,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// 非致命错误
|
||||
logger.Errorf("任务 %v: 发送告警通知失败: %v", t.taskLog.TaskID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 能发送通知的告警要么是忽略期已过且到达触发时间, 要么是不忽略且到达触发时间, 二者都应该取消忽略并刷新最后一次发送时间
|
||||
err = t.alarmRepository.UpdateAlarmNotificationStatus(taskCtx, alarm.ID, time.Now(), false, nil)
|
||||
if err != nil {
|
||||
// 非致命错误, 没有必要因为更新失败影响后续消息发送
|
||||
logger.Errorf("任务 %v: 更新告警通知状态失败: %v", t.taskLog.TaskID, err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("告警通知发送任务执行完成, 任务ID: %d", t.taskLog.TaskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnFailure 告警通知发送任务失败时的处理逻辑
|
||||
func (t *AlarmNotificationTask) OnFailure(ctx context.Context, executeErr error) {
|
||||
logger := logs.TraceLogger(ctx, t.ctx, "OnFailure")
|
||||
logger.Errorf("告警通知发送任务执行失败, 任务ID: %d, 错误: %v", t.taskLog.TaskID, executeErr)
|
||||
}
|
||||
|
||||
// ResolveDeviceIDs 从任务配置中解析并返回所有关联的设备ID列表
|
||||
func (t *AlarmNotificationTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) {
|
||||
// 告警通知任务与设备无关
|
||||
return []uint32{}, nil
|
||||
}
|
||||
|
||||
// parseParameters 解析任务参数
|
||||
func (t *AlarmNotificationTask) parseParameters(ctx context.Context) error {
|
||||
logger := logs.TraceLogger(ctx, t.ctx, "parseParameters")
|
||||
var err error
|
||||
t.onceParse.Do(func() {
|
||||
if t.taskLog.Task.Parameters == nil {
|
||||
logger.Errorf("任务 %v: 缺少参数", t.taskLog.TaskID)
|
||||
err = fmt.Errorf("任务 %v: 参数不全", t.taskLog.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
var params AlarmNotificationTaskParams
|
||||
err = t.taskLog.Task.ParseParameters(¶ms)
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 解析参数失败: %v", t.taskLog.TaskID, err)
|
||||
err = fmt.Errorf("任务 %v: 解析参数失败: %v", t.taskLog.TaskID, err)
|
||||
return
|
||||
}
|
||||
|
||||
t.alarmNotificationTaskParams = params
|
||||
|
||||
})
|
||||
return err
|
||||
}
|
||||
166
internal/domain/task/area_threshold_check_task.go
Normal file
166
internal/domain/task/area_threshold_check_task.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm"
|
||||
"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"
|
||||
)
|
||||
|
||||
// AreaThresholdCheckParams 定义了区域阈值检查任务的参数
|
||||
type AreaThresholdCheckParams struct {
|
||||
AreaControllerID uint32 `json:"area_controller_id"` // 区域主控ID
|
||||
SensorType models.SensorType `json:"sensor_type"` // 传感器类型
|
||||
Thresholds float32 `json:"thresholds"` // 阈值
|
||||
Operator models.Operator `json:"operator"` // 操作符
|
||||
Level models.SeverityLevel `json:"level"` // 告警级别
|
||||
ExcludeDeviceIDs []uint32 `json:"exclude_device_ids"` // 排除的传感器ID
|
||||
}
|
||||
|
||||
// AreaThresholdCheckTask 是一个任务,用于检查区域阈值并触发告警, 区域主控下的所有没有独立校验任务的设备都会受到此任务的检查
|
||||
type AreaThresholdCheckTask struct {
|
||||
ctx context.Context
|
||||
onceParse sync.Once
|
||||
|
||||
taskLog *models.TaskExecutionLog
|
||||
params AreaThresholdCheckParams
|
||||
|
||||
sensorDataRepo repository.SensorDataRepository
|
||||
deviceRepo repository.DeviceRepository
|
||||
alarmService alarm.AlarmService
|
||||
}
|
||||
|
||||
func NewAreaThresholdCheckTask(ctx context.Context, taskLog *models.TaskExecutionLog, sensorDataRepo repository.SensorDataRepository, deviceRepo repository.DeviceRepository, alarmService alarm.AlarmService) plan.Task {
|
||||
return &AreaThresholdCheckTask{
|
||||
ctx: ctx,
|
||||
taskLog: taskLog,
|
||||
sensorDataRepo: sensorDataRepo,
|
||||
deviceRepo: deviceRepo,
|
||||
alarmService: alarmService,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行区域阈值检查任务
|
||||
func (a *AreaThresholdCheckTask) Execute(ctx context.Context) error {
|
||||
taskCtx, logger := logs.Trace(ctx, a.ctx, "Execute")
|
||||
err := a.parseParameters(taskCtx)
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 解析参数失败: %v", a.taskLog.TaskID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. 查询区域主控下所有设备
|
||||
devices, err := a.deviceRepo.ListByAreaControllerID(taskCtx, a.params.AreaControllerID)
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 查询区域主控 %d 下设备失败: %v", a.taskLog.TaskID, a.params.AreaControllerID, err)
|
||||
return fmt.Errorf("查询区域主控 %d 下设备失败: %w", a.params.AreaControllerID, err)
|
||||
}
|
||||
|
||||
// 构建忽略设备ID的map,方便快速查找
|
||||
ignoredMap := make(map[uint32]struct{})
|
||||
for _, id := range a.params.ExcludeDeviceIDs {
|
||||
ignoredMap[id] = struct{}{}
|
||||
}
|
||||
|
||||
// 2. 遍历设备,排除忽略列表里的设备,并执行阈值检查
|
||||
for _, device := range devices {
|
||||
if _, ignored := ignoredMap[device.ID]; ignored {
|
||||
logger.Debugf("任务 %v: 设备 %d 在忽略列表中,跳过检查。", a.taskLog.TaskID, device.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
task := a.taskLog.Task
|
||||
err = task.SaveParameters(DeviceThresholdCheckParams{
|
||||
DeviceID: device.ID,
|
||||
SensorType: a.params.SensorType,
|
||||
Thresholds: a.params.Thresholds,
|
||||
Level: a.params.Level,
|
||||
Operator: a.params.Operator,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 保存参数失败: %v", a.taskLog.TaskID, err)
|
||||
continue
|
||||
}
|
||||
// 创建一个临时的 DeviceThresholdCheckTask 实例来复用其核心逻辑
|
||||
deviceCheckTask := NewDeviceThresholdCheckTask(
|
||||
taskCtx,
|
||||
&models.TaskExecutionLog{ // 为每个设备创建一个模拟的 TaskExecutionLog
|
||||
TaskID: a.taskLog.TaskID,
|
||||
Task: task,
|
||||
},
|
||||
a.sensorDataRepo,
|
||||
a.alarmService,
|
||||
).(*DeviceThresholdCheckTask) // 类型断言,以便访问内部参数
|
||||
|
||||
// 执行单设备的阈值检查
|
||||
if err := deviceCheckTask.Execute(taskCtx); err != nil {
|
||||
logger.Errorf("任务 %v: 设备 %d 阈值检查失败: %v", a.taskLog.TaskID, device.ID, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AreaThresholdCheckTask) OnFailure(ctx context.Context, executeErr error) {
|
||||
logger := logs.TraceLogger(ctx, a.ctx, "OnFailure")
|
||||
logger.Errorf("区域阈值检测任务执行失败, 任务ID: %v: 执行失败: %v", a.taskLog.TaskID, executeErr)
|
||||
}
|
||||
|
||||
func (a *AreaThresholdCheckTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) {
|
||||
taskCtx := logs.AddFuncName(ctx, a.ctx, "ResolveDeviceIDs")
|
||||
if err := a.parseParameters(taskCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 排除列表也意味着关联
|
||||
return a.params.ExcludeDeviceIDs, nil
|
||||
}
|
||||
|
||||
// parseParameters 解析任务参数
|
||||
func (a *AreaThresholdCheckTask) parseParameters(ctx context.Context) error {
|
||||
logger := logs.TraceLogger(ctx, a.ctx, "parseParameters")
|
||||
var err error
|
||||
a.onceParse.Do(func() {
|
||||
if a.taskLog.Task.Parameters == nil {
|
||||
logger.Errorf("任务 %v: 缺少参数", a.taskLog.TaskID)
|
||||
err = fmt.Errorf("任务 %v: 参数不全", a.taskLog.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
var params AreaThresholdCheckParams
|
||||
err = a.taskLog.Task.ParseParameters(¶ms)
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 解析参数失败: %v", a.taskLog.TaskID, err)
|
||||
err = fmt.Errorf("任务 %v: 解析参数失败: %v", a.taskLog.TaskID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.SensorType == "" {
|
||||
err = fmt.Errorf("任务 %v: 未配置传感器类型", a.taskLog.TaskID)
|
||||
}
|
||||
if params.Operator == "" {
|
||||
err = fmt.Errorf("任务 %v: 缺少操作符", a.taskLog.TaskID)
|
||||
}
|
||||
if params.Thresholds == 0 {
|
||||
err = fmt.Errorf("任务 %v: 未配置阈值", a.taskLog.TaskID)
|
||||
}
|
||||
if params.AreaControllerID == 0 {
|
||||
err = fmt.Errorf("任务 %v: 未配置区域主控ID", a.taskLog.TaskID)
|
||||
}
|
||||
if params.Level == "" {
|
||||
params.Level = models.WarnLevel
|
||||
}
|
||||
if params.ExcludeDeviceIDs == nil {
|
||||
params.ExcludeDeviceIDs = []uint32{}
|
||||
}
|
||||
|
||||
a.params = params
|
||||
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
type DelayTaskParams struct {
|
||||
DelayDuration float64 `json:"delay_duration"`
|
||||
DelayDuration float32 `json:"delay_duration"`
|
||||
}
|
||||
|
||||
// DelayTask 是一个用于模拟延迟的 Task 实现
|
||||
@@ -70,6 +70,6 @@ func (d *DelayTask) OnFailure(ctx context.Context, executeErr error) {
|
||||
logger.Errorf("任务 %v: 执行失败: %v", d.executionTask.TaskID, executeErr)
|
||||
}
|
||||
|
||||
func (d *DelayTask) ResolveDeviceIDs(ctx context.Context) ([]uint, error) {
|
||||
return []uint{}, nil
|
||||
func (d *DelayTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) {
|
||||
return []uint32{}, nil
|
||||
}
|
||||
|
||||
197
internal/domain/task/device_threshold_check_task.go
Normal file
197
internal/domain/task/device_threshold_check_task.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm"
|
||||
"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"
|
||||
)
|
||||
|
||||
type DeviceThresholdCheckParams struct {
|
||||
DeviceID uint32 `json:"device_id"` // 设备ID
|
||||
SensorType models.SensorType `json:"sensor_type"` // 传感器类型
|
||||
Thresholds float32 `json:"thresholds"` // 阈值
|
||||
Operator models.Operator `json:"operator"` // 操作符
|
||||
Level models.SeverityLevel `json:"level"` // 告警等级
|
||||
}
|
||||
|
||||
// DeviceThresholdCheckTask 是一个任务,用于检查设备传感器数据是否满足阈值条件。
|
||||
type DeviceThresholdCheckTask struct {
|
||||
ctx context.Context
|
||||
onceParse sync.Once
|
||||
|
||||
taskLog *models.TaskExecutionLog
|
||||
params DeviceThresholdCheckParams
|
||||
|
||||
sensorDataRepo repository.SensorDataRepository
|
||||
alarmService alarm.AlarmService
|
||||
}
|
||||
|
||||
func NewDeviceThresholdCheckTask(ctx context.Context, taskLog *models.TaskExecutionLog, sensorDataRepo repository.SensorDataRepository, alarmService alarm.AlarmService) plan.Task {
|
||||
return &DeviceThresholdCheckTask{
|
||||
ctx: ctx,
|
||||
taskLog: taskLog,
|
||||
sensorDataRepo: sensorDataRepo,
|
||||
alarmService: alarmService,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeviceThresholdCheckTask) Execute(ctx context.Context) error {
|
||||
taskCtx, logger := logs.Trace(ctx, d.ctx, "Execute")
|
||||
err := d.parseParameters(taskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sensorData, err := d.sensorDataRepo.GetLatestSensorDataByDeviceIDAndSensorType(taskCtx, d.params.DeviceID, d.params.SensorType)
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 获取最新传感器数据失败: %v", d.taskLog.TaskID, err)
|
||||
return fmt.Errorf("任务 %v: 获取最新传感器数据失败: %v", d.taskLog.TaskID, err)
|
||||
}
|
||||
|
||||
var currentValue float32
|
||||
var alarmCode models.AlarmCode
|
||||
|
||||
switch d.params.SensorType {
|
||||
case models.SensorTypeTemperature:
|
||||
var data models.TemperatureData
|
||||
if err := sensorData.ParseData(&data); err != nil {
|
||||
return fmt.Errorf("任务 %v: 解析温度数据失败: %v", d.taskLog.TaskID, err)
|
||||
}
|
||||
currentValue = data.TemperatureCelsius
|
||||
alarmCode = models.AlarmCodeTemperature
|
||||
case models.SensorTypeHumidity:
|
||||
var data models.HumidityData
|
||||
if err := sensorData.ParseData(&data); err != nil {
|
||||
return fmt.Errorf("任务 %v: 解析湿度数据失败: %v", d.taskLog.TaskID, err)
|
||||
}
|
||||
currentValue = data.HumidityPercent
|
||||
alarmCode = models.AlarmCodeHumidity
|
||||
case models.SensorTypeWeight:
|
||||
var data models.WeightData
|
||||
if err := sensorData.ParseData(&data); err != nil {
|
||||
return fmt.Errorf("任务 %v: 解析重量数据失败: %v", d.taskLog.TaskID, err)
|
||||
}
|
||||
currentValue = data.WeightKilograms
|
||||
alarmCode = models.AlarmCodeWeight
|
||||
|
||||
default:
|
||||
return fmt.Errorf("任务 %v: 不支持的传感器类型: %v", d.taskLog.TaskID, d.params.SensorType)
|
||||
}
|
||||
|
||||
// 阈值检查未通过
|
||||
isExceeded := !d.checkThreshold(currentValue, d.params.Operator, d.params.Thresholds)
|
||||
|
||||
if isExceeded {
|
||||
// 状态一:检查未通过,确保告警开启
|
||||
summary := fmt.Sprintf("设备 %d(%s) 不满足阈值条件 (%s %.2f)", d.params.DeviceID, d.params.SensorType, d.params.Operator, d.params.Thresholds)
|
||||
details := fmt.Sprintf("当前检测值: %.2f", currentValue)
|
||||
logger.Infof("任务 %v: %s。%s", d.taskLog.TaskID, summary, details)
|
||||
|
||||
newAlarm := &models.ActiveAlarm{
|
||||
SourceType: models.AlarmSourceTypeDevice,
|
||||
SourceID: d.params.DeviceID,
|
||||
AlarmCode: alarmCode,
|
||||
AlarmSummary: summary,
|
||||
AlarmDetails: details,
|
||||
Level: d.params.Level,
|
||||
TriggerTime: time.Now(),
|
||||
}
|
||||
|
||||
if err := d.alarmService.CreateAlarmIfNotExists(taskCtx, newAlarm); err != nil {
|
||||
logger.Errorf("任务 %v: 创建告警失败: %v", d.taskLog.TaskID, err)
|
||||
// 根据策略决定是否需要返回错误,这里选择不中断任务执行
|
||||
}
|
||||
} else {
|
||||
// 状态二:检查已通过,确保告警关闭
|
||||
resolveMethod := "系统自动解决:阈值恢复正常"
|
||||
logger.Infof("任务 %v: 设备 %d 的 %s 阈值已恢复正常,正在尝试关闭告警。", d.taskLog.TaskID, d.params.DeviceID, d.params.SensorType)
|
||||
|
||||
if err := d.alarmService.CloseAlarm(taskCtx, models.AlarmSourceTypeDevice, d.params.DeviceID, alarmCode, resolveMethod, nil); err != nil {
|
||||
logger.Errorf("任务 %v: 关闭告警失败: %v", d.taskLog.TaskID, err)
|
||||
// 根据策略决定是否需要返回错误,这里选择不中断任务执行
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkThreshold 校验当前值是否满足阈值条件
|
||||
func (d *DeviceThresholdCheckTask) checkThreshold(currentValue float32, operator models.Operator, threshold float32) bool {
|
||||
switch operator {
|
||||
case models.OperatorLessThan:
|
||||
return currentValue < threshold
|
||||
case models.OperatorLessThanOrEqualTo:
|
||||
return currentValue <= threshold
|
||||
case models.OperatorGreaterThan:
|
||||
return currentValue > threshold
|
||||
case models.OperatorGreaterThanOrEqualTo:
|
||||
return currentValue >= threshold
|
||||
case models.OperatorEqualTo:
|
||||
return currentValue == threshold
|
||||
case models.OperatorNotEqualTo:
|
||||
return currentValue != threshold
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// parseParameters 解析任务参数
|
||||
func (d *DeviceThresholdCheckTask) parseParameters(ctx context.Context) error {
|
||||
logger := logs.TraceLogger(ctx, d.ctx, "parseParameters")
|
||||
var err error
|
||||
d.onceParse.Do(func() {
|
||||
if d.taskLog.Task.Parameters == nil {
|
||||
logger.Errorf("任务 %v: 缺少参数", d.taskLog.TaskID)
|
||||
err = fmt.Errorf("任务 %v: 参数不全", d.taskLog.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
var params DeviceThresholdCheckParams
|
||||
err = d.taskLog.Task.ParseParameters(¶ms)
|
||||
if err != nil {
|
||||
logger.Errorf("任务 %v: 解析参数失败: %v", d.taskLog.TaskID, err)
|
||||
err = fmt.Errorf("任务 %v: 解析参数失败: %v", d.taskLog.TaskID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.SensorType == "" {
|
||||
err = fmt.Errorf("任务 %v: 未配置传感器类型", d.taskLog.TaskID)
|
||||
}
|
||||
if params.Operator == "" {
|
||||
err = fmt.Errorf("任务 %v: 缺少操作符", d.taskLog.TaskID)
|
||||
}
|
||||
if params.Thresholds == 0 {
|
||||
err = fmt.Errorf("任务 %v: 未配置阈值", d.taskLog.TaskID)
|
||||
}
|
||||
if params.DeviceID == 0 {
|
||||
err = fmt.Errorf("任务 %v: 未配置设备ID", d.taskLog.TaskID)
|
||||
}
|
||||
if params.Level == "" {
|
||||
params.Level = models.WarnLevel
|
||||
}
|
||||
|
||||
d.params = params
|
||||
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DeviceThresholdCheckTask) OnFailure(ctx context.Context, executeErr error) {
|
||||
logger := logs.TraceLogger(ctx, d.ctx, "OnFailure")
|
||||
logger.Errorf("设备阈值检测任务执行失败, 任务ID: %v: 执行失败: %v", d.taskLog.TaskID, executeErr)
|
||||
}
|
||||
|
||||
func (d *DeviceThresholdCheckTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) {
|
||||
taskCtx := logs.AddFuncName(ctx, d.ctx, "ResolveDeviceIDs")
|
||||
if err := d.parseParameters(taskCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []uint32{d.params.DeviceID}, nil
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func (t *FullCollectionTask) Execute(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
sensorsByController := make(map[uint][]*models.Device)
|
||||
sensorsByController := make(map[uint32][]*models.Device)
|
||||
for _, sensor := range sensors {
|
||||
sensorsByController[sensor.AreaControllerID] = append(sensorsByController[sensor.AreaControllerID], sensor)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func (t *FullCollectionTask) OnFailure(ctx context.Context, executeErr error) {
|
||||
}
|
||||
|
||||
// ResolveDeviceIDs 获取当前任务需要使用的设备ID列表
|
||||
func (t *FullCollectionTask) ResolveDeviceIDs(ctx context.Context) ([]uint, error) {
|
||||
func (t *FullCollectionTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) {
|
||||
// 全量采集任务不和任何设备绑定, 每轮采集都会重新获取全量传感器
|
||||
return []uint{}, nil
|
||||
return []uint32{}, nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -16,9 +15,9 @@ import (
|
||||
|
||||
// ReleaseFeedWeightTaskParams 定义了 ReleaseFeedWeightTask 的参数结构
|
||||
type ReleaseFeedWeightTaskParams struct {
|
||||
ReleaseWeight float64 `json:"release_weight"` // 需要释放的重量
|
||||
FeedPortDeviceID uint `json:"feed_port_device_id"` // 下料口ID
|
||||
MixingTankDeviceID uint `json:"mixing_tank_device_id"` // 称重传感器ID
|
||||
ReleaseWeight float32 `json:"release_weight"` // 需要释放的重量
|
||||
FeedPortDeviceID uint32 `json:"feed_port_device_id"` // 下料口ID
|
||||
MixingTankDeviceID uint32 `json:"mixing_tank_device_id"` // 称重传感器ID
|
||||
}
|
||||
|
||||
// ReleaseFeedWeightTask 是一个控制下料口释放指定重量的任务
|
||||
@@ -30,8 +29,8 @@ type ReleaseFeedWeightTask struct {
|
||||
claimedLog *models.TaskExecutionLog
|
||||
|
||||
feedPortDevice *models.Device
|
||||
releaseWeight float64
|
||||
mixingTankDeviceID uint
|
||||
releaseWeight float32
|
||||
mixingTankDeviceID uint32
|
||||
|
||||
feedPort device.Service
|
||||
|
||||
@@ -101,7 +100,7 @@ func (r *ReleaseFeedWeightTask) Execute(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 获取当前搅拌罐重量
|
||||
func (r *ReleaseFeedWeightTask) getNowWeight(ctx context.Context) (float64, error) {
|
||||
func (r *ReleaseFeedWeightTask) getNowWeight(ctx context.Context) (float32, error) {
|
||||
taskCtx, logger := logs.Trace(ctx, r.ctx, "getNowWeight")
|
||||
sensorData, err := r.sensorDataRepo.GetLatestSensorDataByDeviceIDAndSensorType(taskCtx, r.mixingTankDeviceID, models.SensorTypeWeight)
|
||||
if err != nil {
|
||||
@@ -114,7 +113,7 @@ func (r *ReleaseFeedWeightTask) getNowWeight(ctx context.Context) (float64, erro
|
||||
}
|
||||
|
||||
wg := &models.WeightData{}
|
||||
err = json.Unmarshal(sensorData.Data, wg)
|
||||
err = sensorData.ParseData(wg)
|
||||
if err != nil {
|
||||
logger.Errorf("反序列化设备 %v 最新传感器数据失败: %v , 日志ID: %v", r.mixingTankDeviceID, err, r.claimedLog.ID)
|
||||
return 0, err
|
||||
@@ -179,10 +178,10 @@ func (r *ReleaseFeedWeightTask) OnFailure(ctx context.Context, executeErr error)
|
||||
logger.Errorf("善后处理完成, 日志ID:%v", r.claimedLog.ID)
|
||||
}
|
||||
|
||||
func (r *ReleaseFeedWeightTask) ResolveDeviceIDs(ctx context.Context) ([]uint, error) {
|
||||
func (r *ReleaseFeedWeightTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) {
|
||||
taskCtx := logs.AddFuncName(ctx, r.ctx, "ResolveDeviceIDs")
|
||||
if err := r.parseParameters(taskCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []uint{r.feedPortDevice.ID, r.mixingTankDeviceID}, nil
|
||||
return []uint32{r.feedPortDevice.ID, r.mixingTankDeviceID}, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
|
||||
"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"
|
||||
@@ -15,26 +17,38 @@ const (
|
||||
CompNameDelayTask = "DelayTask"
|
||||
CompNameReleaseFeedWeight = "ReleaseFeedWeightTask"
|
||||
CompNameFullCollectionTask = "FullCollectionTask"
|
||||
CompNameAlarmNotification = "AlarmNotificationTask"
|
||||
)
|
||||
|
||||
type taskFactory struct {
|
||||
ctx context.Context
|
||||
ctx context.Context
|
||||
|
||||
sensorDataRepo repository.SensorDataRepository
|
||||
deviceRepo repository.DeviceRepository
|
||||
deviceService device.Service
|
||||
alarmRepo repository.AlarmRepository
|
||||
|
||||
deviceService device.Service
|
||||
notificationService notify.Service
|
||||
alarmService alarm.AlarmService
|
||||
}
|
||||
|
||||
func NewTaskFactory(
|
||||
ctx context.Context,
|
||||
sensorDataRepo repository.SensorDataRepository,
|
||||
deviceRepo repository.DeviceRepository,
|
||||
alarmRepo repository.AlarmRepository,
|
||||
deviceService device.Service,
|
||||
notifyService notify.Service,
|
||||
alarmService alarm.AlarmService,
|
||||
) plan.TaskFactory {
|
||||
return &taskFactory{
|
||||
ctx: ctx,
|
||||
sensorDataRepo: sensorDataRepo,
|
||||
deviceRepo: deviceRepo,
|
||||
deviceService: deviceService,
|
||||
ctx: ctx,
|
||||
sensorDataRepo: sensorDataRepo,
|
||||
deviceRepo: deviceRepo,
|
||||
alarmRepo: alarmRepo,
|
||||
deviceService: deviceService,
|
||||
notificationService: notifyService,
|
||||
alarmService: alarmService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +62,12 @@ func (t *taskFactory) Production(ctx context.Context, claimedLog *models.TaskExe
|
||||
return NewReleaseFeedWeightTask(logs.AddCompName(baseCtx, CompNameReleaseFeedWeight), claimedLog, t.sensorDataRepo, t.deviceRepo, t.deviceService)
|
||||
case models.TaskTypeFullCollection:
|
||||
return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), claimedLog, t.deviceRepo, t.deviceService)
|
||||
case models.TaskTypeAlarmNotification:
|
||||
return NewAlarmNotificationTask(logs.AddCompName(baseCtx, CompNameAlarmNotification), claimedLog, t.notificationService, t.alarmRepo)
|
||||
case models.TaskTypeDeviceThresholdCheck:
|
||||
return NewDeviceThresholdCheckTask(logs.AddCompName(baseCtx, "DeviceThresholdCheckTask"), claimedLog, t.sensorDataRepo, t.alarmService)
|
||||
case models.TaskTypeAreaCollectorThresholdCheck:
|
||||
return NewAreaThresholdCheckTask(logs.AddCompName(baseCtx, "AreaCollectorThresholdCheckTask"), claimedLog, t.sensorDataRepo, t.deviceRepo, t.alarmService)
|
||||
default:
|
||||
// TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型
|
||||
logger.Panicf("不支持的任务类型: %s", claimedLog.Task.Type)
|
||||
@@ -75,6 +95,12 @@ func (t *taskFactory) CreateTaskFromModel(ctx context.Context, taskModel *models
|
||||
), nil
|
||||
case models.TaskTypeFullCollection:
|
||||
return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), tempLog, t.deviceRepo, t.deviceService), nil
|
||||
case models.TaskTypeAlarmNotification:
|
||||
return NewAlarmNotificationTask(logs.AddCompName(baseCtx, CompNameAlarmNotification), tempLog, t.notificationService, t.alarmRepo), nil
|
||||
case models.TaskTypeDeviceThresholdCheck:
|
||||
return NewDeviceThresholdCheckTask(logs.AddCompName(baseCtx, "DeviceThresholdCheckTask"), tempLog, t.sensorDataRepo, t.alarmService), nil
|
||||
case models.TaskTypeAreaCollectorThresholdCheck:
|
||||
return NewAreaThresholdCheckTask(logs.AddCompName(baseCtx, "AreaCollectorThresholdCheckTask"), tempLog, t.sensorDataRepo, t.deviceRepo, t.alarmService), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ type Config struct {
|
||||
|
||||
// Collection 定时采集配置
|
||||
Collection CollectionConfig `yaml:"collection"`
|
||||
|
||||
// AlarmNotification 告警通知配置
|
||||
AlarmNotification AlarmNotificationConfig `yaml:"alarm_notification"`
|
||||
}
|
||||
|
||||
// AppConfig 代表应用基础配置
|
||||
@@ -204,6 +207,29 @@ type CollectionConfig struct {
|
||||
Interval int `yaml:"interval"`
|
||||
}
|
||||
|
||||
type NotificationIntervalsConfig struct {
|
||||
// DebugIntervalMinutes Debug级别告警的通知间隔(分钟)
|
||||
DebugIntervalMinutes uint32 `yaml:"debug"`
|
||||
// InfoIntervalMinutes Info级别告警的通知间隔(分钟)
|
||||
InfoIntervalMinutes uint32 `yaml:"info"`
|
||||
// WarnIntervalMinutes Warn级别告警的通知间隔(分钟)
|
||||
WarnIntervalMinutes uint32 `yaml:"warn"`
|
||||
// ErrorIntervalMinutes Error级别告警的通知间隔(分钟)
|
||||
ErrorIntervalMinutes uint32 `yaml:"error"`
|
||||
// DPanicIntervalMinutes DPanic级别告警的通知间隔(分钟)
|
||||
DPanicIntervalMinutes uint32 `yaml:"dpanic"`
|
||||
// PanicIntervalMinutes Panic级别告警的通知间隔(分钟)
|
||||
PanicIntervalMinutes uint32 `yaml:"panic"`
|
||||
// FatalIntervalMinutes Fatal级别告警的通知间隔(分钟)
|
||||
FatalIntervalMinutes uint32 `yaml:"fatal"`
|
||||
}
|
||||
|
||||
// AlarmNotificationConfig 告警通知配置
|
||||
type AlarmNotificationConfig struct {
|
||||
// NotificationIntervals 告警通知的发送间隔时间,键为告警等级,值为时间间隔(秒)
|
||||
NotificationIntervals NotificationIntervalsConfig `yaml:"notification_intervals"`
|
||||
}
|
||||
|
||||
// NewConfig 创建并返回一个新的配置实例
|
||||
func NewConfig() *Config {
|
||||
// 默认值可以在这里设置,但我们优先使用配置文件中的值
|
||||
|
||||
@@ -179,6 +179,7 @@ func (ps *PostgresStorage) creatingHyperTable(ctx context.Context) error {
|
||||
{models.PigPurchase{}, "purchase_date"},
|
||||
{models.PigSale{}, "sale_date"},
|
||||
{models.Notification{}, "alarm_timestamp"},
|
||||
{models.HistoricalAlarm{}, "trigger_time"},
|
||||
}
|
||||
|
||||
for _, table := range tablesToConvert {
|
||||
@@ -221,6 +222,7 @@ func (ps *PostgresStorage) applyCompressionPolicies(ctx context.Context) error {
|
||||
{models.PigPurchase{}, "pig_batch_id"},
|
||||
{models.PigSale{}, "pig_batch_id"},
|
||||
{models.Notification{}, "user_id"},
|
||||
{models.HistoricalAlarm{}, "source_id"},
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
|
||||
@@ -208,7 +208,7 @@ func (g *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql
|
||||
fields := []interface{}{
|
||||
"sql", sql,
|
||||
"rows", rows,
|
||||
"elapsed", fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6),
|
||||
"elapsed", fmt.Sprintf("%.3fms", float32(elapsed.Nanoseconds())/1e6),
|
||||
}
|
||||
|
||||
// 附加调用链信息
|
||||
|
||||
107
internal/infra/models/alarm.go
Normal file
107
internal/infra/models/alarm.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AlarmSourceType 定义了告警的来源类型
|
||||
type AlarmSourceType string
|
||||
|
||||
const (
|
||||
AlarmSourceTypeDevice AlarmSourceType = "普通设备"
|
||||
AlarmSourceTypeAreaController AlarmSourceType = "区域主控"
|
||||
AlarmSourceTypeSystem AlarmSourceType = "系统"
|
||||
)
|
||||
|
||||
// AlarmCode 定义了标准化的告警类型标识
|
||||
type AlarmCode string
|
||||
|
||||
const (
|
||||
// --- 设备相关告警 ---
|
||||
AlarmCodeTemperature AlarmCode = "温度阈值"
|
||||
AlarmCodeHumidity AlarmCode = "湿度阈值"
|
||||
AlarmCodeWeight AlarmCode = "重量阈值"
|
||||
AlarmCodeBatteryLevel AlarmCode = "电池电量阈值"
|
||||
AlarmCodeSignalMetrics AlarmCode = "信号强度阈值"
|
||||
AlarmCodeDeviceOffline AlarmCode = "设备离线"
|
||||
|
||||
// --- 区域主控相关告警 ---
|
||||
AlarmCodeAreaControllerOffline AlarmCode = "区域主控离线"
|
||||
|
||||
// --- 系统相关告警 ---
|
||||
// (可在此处预留或添加)
|
||||
)
|
||||
|
||||
type Operator string
|
||||
|
||||
const (
|
||||
OperatorLessThan Operator = "<"
|
||||
OperatorLessThanOrEqualTo Operator = "<="
|
||||
OperatorGreaterThan Operator = ">"
|
||||
OperatorGreaterThanOrEqualTo Operator = ">="
|
||||
OperatorEqualTo Operator = "="
|
||||
OperatorNotEqualTo Operator = "!="
|
||||
)
|
||||
|
||||
// ActiveAlarm 活跃告警
|
||||
// 活跃告警会被更新(如忽略状态),因此保留 Model 以包含所有标准字段。
|
||||
type ActiveAlarm struct {
|
||||
Model
|
||||
|
||||
SourceType AlarmSourceType `gorm:"type:varchar(50);not null;index:idx_alarm_uniqueness;comment:告警来源类型" json:"source_type"`
|
||||
// SourceID 告警来源ID,其具体含义取决于 SourceType 字段 (例如:设备ID, 区域主控ID, 猪栏ID)。
|
||||
SourceID uint32 `gorm:"not null;index:idx_alarm_uniqueness;comment:告警来源ID" json:"source_id"`
|
||||
|
||||
// AlarmCode 是一个机器可读的、标准化的告警类型标识。
|
||||
// 它与 SourceType 和 SourceID 共同构成一个活跃告警的唯一标识。
|
||||
AlarmCode AlarmCode `gorm:"type:varchar(100);not null;index:idx_alarm_uniqueness;comment:告警代码" json:"alarm_code"`
|
||||
|
||||
AlarmSummary string `gorm:"comment:告警简述" json:"alarm_summary"`
|
||||
Level SeverityLevel `gorm:"type:varchar(10);not null;index:idx_notification_query;comment:严重性等级" json:"level"`
|
||||
AlarmDetails string `gorm:"comment:告警详细内容" json:"alarm_details"`
|
||||
TriggerTime time.Time `gorm:"not null;comment:告警触发时间" json:"trigger_time"`
|
||||
|
||||
// IsIgnored 字段加入到专为通知查询优化的复合索引中
|
||||
IsIgnored bool `gorm:"default:false;index:idx_notification_query;comment:是否被手动忽略" json:"is_ignored"`
|
||||
// IgnoredUntil 忽略截止时间。在此时间之前,即使告警持续,也不会发送通知。
|
||||
// 使用指针类型 *time.Time 来表示可为空的时间。
|
||||
IgnoredUntil *time.Time `gorm:"comment:忽略截止时间" json:"ignored_until"`
|
||||
|
||||
// LastNotifiedAt 字段加入到专为通知查询优化的复合索引中
|
||||
LastNotifiedAt *time.Time `gorm:"index:idx_notification_query;comment:上次发送通知时间" json:"last_notified_at"`
|
||||
}
|
||||
|
||||
// TableName 指定 ActiveAlarm 结构体对应的数据库表名
|
||||
func (ActiveAlarm) TableName() string {
|
||||
return "active_alarms"
|
||||
}
|
||||
|
||||
// HistoricalAlarm 历史告警
|
||||
// 历史告警是不可变归档数据,我们移除 Model,并手动定义字段。
|
||||
// ID 和 TriggerTime 共同构成联合主键,以满足 TimescaleDB 超表的要求。
|
||||
type HistoricalAlarm struct {
|
||||
// 手动定义主键,ID 仍然自增
|
||||
ID uint32 `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
||||
|
||||
SourceType AlarmSourceType `gorm:"type:varchar(50);not null;index;comment:告警来源类型" json:"source_type"`
|
||||
// SourceID 告警来源ID,其具体含义取决于 SourceType 字段 (例如:设备ID, 区域主控ID, 猪栏ID)。
|
||||
SourceID uint32 `gorm:"not null;index;comment:告警来源ID" json:"source_id"`
|
||||
|
||||
// AlarmCode 是一个机器可读的、标准化的告警类型标识。
|
||||
AlarmCode AlarmCode `gorm:"type:varchar(100);not null;index;comment:告警代码" json:"alarm_code"`
|
||||
|
||||
AlarmSummary string `gorm:"comment:告警简述" json:"alarm_summary"`
|
||||
Level SeverityLevel `gorm:"type:varchar(10);not null;comment:严重性等级" json:"level"`
|
||||
AlarmDetails string `gorm:"comment:告警详细内容" json:"alarm_details"`
|
||||
TriggerTime time.Time `gorm:"primaryKey;not null;comment:告警触发时间" json:"trigger_time"`
|
||||
ResolveTime time.Time `gorm:"not null;comment:告警解决时间" json:"resolve_time"`
|
||||
ResolveMethod string `gorm:"comment:告警解决方式" json:"resolve_method"`
|
||||
|
||||
// ResolvedBy 使用指针类型 *uint32 来表示可为空解决人, 当字段为空时表示系统自动解决的
|
||||
ResolvedBy *uint32 `gorm:"comment:告警解决人" json:"resolved_by"`
|
||||
}
|
||||
|
||||
// TableName 指定 HistoricalAlarm 结构体对应的数据库表名
|
||||
func (HistoricalAlarm) TableName() string {
|
||||
return "historical_alarms"
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// --- Properties 结构体定义 ---
|
||||
@@ -19,7 +18,7 @@ type Bus485Properties struct {
|
||||
|
||||
// AreaController 是一个LoRa转总线(如485)的通信网关
|
||||
type AreaController struct {
|
||||
gorm.Model
|
||||
Model
|
||||
|
||||
// Name 是主控的业务名称,例如 "1号猪舍主控"
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
@@ -53,20 +52,20 @@ func (AreaController) TableName() string {
|
||||
|
||||
// Device 代表系统中的所有普通设备
|
||||
type Device struct {
|
||||
// gorm.Model 内嵌了标准模型字段 (ID, CreatedAt, UpdatedAt, DeletedAt)
|
||||
gorm.Model
|
||||
// Model 内嵌了标准模型字段 (ID, CreatedAt, UpdatedAt, DeletedAt)
|
||||
Model
|
||||
|
||||
// Name 是设备的业务名称,应清晰可读,例如 "1号猪舍温度传感器"
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
|
||||
// DeviceTemplateID 是设备模板的外键
|
||||
DeviceTemplateID uint `gorm:"not null;index" json:"device_template_id"`
|
||||
DeviceTemplateID uint32 `gorm:"not null;index" json:"device_template_id"`
|
||||
|
||||
// DeviceTemplate 是设备的模板,包含了设备的通用信息
|
||||
DeviceTemplate DeviceTemplate `json:"device_template"`
|
||||
|
||||
// AreaControllerID 是区域主控的外键
|
||||
AreaControllerID uint `gorm:"not null;index" json:"area_controller_id"`
|
||||
AreaControllerID uint32 `gorm:"not null;index" json:"area_controller_id"`
|
||||
|
||||
// AreaController 是设备所属的区域主控
|
||||
AreaController AreaController `json:"area_controller"`
|
||||
|
||||
@@ -5,10 +5,38 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/command_generater"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ModbusFunctionCode 定义Modbus功能码的枚举类型
|
||||
type ModbusFunctionCode byte
|
||||
|
||||
// 定义常用的Modbus功能码常量及其应用场景
|
||||
const (
|
||||
// ReadCoils 读取线圈状态 (0x01)
|
||||
// 场景: 用于读取数字量输出(DO)或内部标志位的当前状态,这些状态通常是开关量。
|
||||
ReadCoils ModbusFunctionCode = 0x01
|
||||
// ReadDiscreteInputs 读取离散输入状态 (0x02)
|
||||
// 场景: 用于读取数字量输入(DI)的当前状态,这些状态通常是外部传感器的开关量信号。
|
||||
ReadDiscreteInputs ModbusFunctionCode = 0x02
|
||||
// ReadHoldingRegisters 读取保持寄存器 (0x03)
|
||||
// 场景: 用于读取设备内部可读写的参数或数据,例如温度设定值、电机速度等模拟量或配置数据。
|
||||
ReadHoldingRegisters ModbusFunctionCode = 0x03
|
||||
// ReadInputRegisters 读取输入寄存器 (0x04)
|
||||
// 场景: 用于读取设备的模拟量输入(AI)数据,这些数据通常是只读的,例如当前温度、压力、电压等实时测量值。
|
||||
ReadInputRegisters ModbusFunctionCode = 0x04
|
||||
// WriteSingleCoil 写入单个线圈 (0x05)
|
||||
// 场景: 用于控制单个数字量输出(DO),例如打开或关闭一个继电器、指示灯等。
|
||||
WriteSingleCoil ModbusFunctionCode = 0x05
|
||||
// WriteSingleRegister 写入单个保持寄存器 (0x06)
|
||||
// 场景: 用于修改设备内部的单个可写参数,例如设置一个温度控制器的目标温度、调整一个阀门的开度等。
|
||||
WriteSingleRegister ModbusFunctionCode = 0x06
|
||||
// WriteMultipleCoils 写入多个线圈 (0x0F)
|
||||
// 场景: 用于批量控制多个数字量输出(DO),例如同时打开或关闭一组继电器。
|
||||
WriteMultipleCoils ModbusFunctionCode = 0x0F
|
||||
// WriteMultipleRegisters 写入多个保持寄存器 (0x10)
|
||||
// 场景: 用于批量修改设备内部的多个可写参数,例如一次性更新多个配置参数或模拟量输出值。
|
||||
WriteMultipleRegisters ModbusFunctionCode = 0x10
|
||||
)
|
||||
|
||||
// DeviceCategory 定义了设备模板的宽泛类别
|
||||
@@ -25,8 +53,8 @@ const (
|
||||
// 它提供了必要的元数据,以便应用程序能够正确解释从设备读取的原始数据。
|
||||
type ValueDescriptor struct {
|
||||
Type SensorType `json:"type"`
|
||||
Multiplier float64 `json:"multiplier"` // 乘数,用于原始数据转换
|
||||
Offset float64 `json:"offset"` // 偏移量,用于原始数据转换
|
||||
Multiplier float32 `json:"multiplier"` // 乘数,用于原始数据转换
|
||||
Offset float32 `json:"offset"` // 偏移量,用于原始数据转换
|
||||
}
|
||||
|
||||
// --- 指令结构体 (Command Structs) ---
|
||||
@@ -51,7 +79,7 @@ func (sc *SwitchCommands) SelfCheck() error {
|
||||
// SensorCommands 定义了传感器读取指令所需的Modbus参数
|
||||
type SensorCommands struct {
|
||||
// ModbusFunctionCode 记录Modbus功能码,例如 ReadHoldingRegisters。(一般是第二字节)
|
||||
ModbusFunctionCode command_generater.ModbusFunctionCode `json:"modbus_function_code"`
|
||||
ModbusFunctionCode ModbusFunctionCode `json:"modbus_function_code"`
|
||||
// ModbusStartAddress 记录Modbus寄存器的起始地址,用于生成指令。(一般是第三到四字节)
|
||||
ModbusStartAddress uint16 `json:"modbus_start_address"`
|
||||
// ModbusQuantity 记录Modbus寄存器的数量,用于生成指令。(一般是五到六字节)
|
||||
@@ -62,7 +90,7 @@ type SensorCommands struct {
|
||||
func (sc *SensorCommands) SelfCheck() error {
|
||||
// 校验ModbusFunctionCode是否为读取类型
|
||||
switch sc.ModbusFunctionCode {
|
||||
case command_generater.ReadCoils, command_generater.ReadDiscreteInputs, command_generater.ReadHoldingRegisters, command_generater.ReadInputRegisters:
|
||||
case ReadCoils, ReadDiscreteInputs, ReadHoldingRegisters, ReadInputRegisters:
|
||||
// 支持的读取功能码
|
||||
default:
|
||||
return fmt.Errorf("'sensor' 指令集 ModbusFunctionCode %X 无效或不是读取类型", sc.ModbusFunctionCode)
|
||||
@@ -77,7 +105,7 @@ func (sc *SensorCommands) SelfCheck() error {
|
||||
|
||||
// DeviceTemplate 代表一种物理设备的类型。
|
||||
type DeviceTemplate struct {
|
||||
gorm.Model
|
||||
Model
|
||||
|
||||
// Name 是此模板的唯一名称, 例如 "FanModel-XYZ-2000" 或 "TempSensor-T1"
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
|
||||
@@ -27,12 +27,12 @@ const (
|
||||
// PlanExecutionLog 记录整个计划的一次执行历史
|
||||
|
||||
type PlanExecutionLog struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
ID uint32 `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"primaryKey"` // 作为联合主键方便只查询热点数据
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
|
||||
PlanID uint `gorm:"index"`
|
||||
PlanID uint32 `gorm:"index"`
|
||||
Status ExecutionStatus
|
||||
StartedAt time.Time
|
||||
EndedAt time.Time
|
||||
@@ -46,12 +46,12 @@ func (PlanExecutionLog) TableName() string {
|
||||
|
||||
// TaskExecutionLog 记录单个任务的一次执行历史
|
||||
type TaskExecutionLog struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
ID uint32 `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"primaryKey"` // 作为联合主键方便只查询热点数据
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
|
||||
PlanExecutionLogID uint `gorm:"index"` // 关联到某次计划执行
|
||||
PlanExecutionLogID uint32 `gorm:"index"` // 关联到某次计划执行
|
||||
|
||||
// TaskID 使用 int 类型以容纳特殊的负数ID,代表系统任务
|
||||
TaskID int `gorm:"index"`
|
||||
@@ -106,7 +106,7 @@ type DeviceCommandLog struct {
|
||||
|
||||
// DeviceID 是接收此下行任务的设备的ID。
|
||||
// 对于 LoRaWAN,这通常是区域主控设备的ID。
|
||||
DeviceID uint `gorm:"not null;index" json:"device_id"`
|
||||
DeviceID uint32 `gorm:"not null;index" json:"device_id"`
|
||||
|
||||
// SentAt 记录下行任务最初发送的时间。
|
||||
SentAt time.Time `gorm:"primaryKey" json:"sent_at"`
|
||||
@@ -133,7 +133,7 @@ type PendingCollection struct {
|
||||
|
||||
// DeviceID 是接收此任务的设备ID
|
||||
// 对于 LoRaWAN,这通常是区域主控设备的ID。
|
||||
DeviceID uint `gorm:"index"`
|
||||
DeviceID uint32 `gorm:"index"`
|
||||
|
||||
// CommandMetadata 存储了此次采集任务对应的设备ID列表,顺序与设备响应值的顺序一致。
|
||||
CommandMetadata UintArray `gorm:"type:bigint[]"`
|
||||
@@ -154,7 +154,6 @@ func (PendingCollection) TableName() string {
|
||||
}
|
||||
|
||||
// --- 用户审计日志 ---
|
||||
// TODO 这些变量放这个包合适吗?
|
||||
|
||||
// --- 审计日志状态常量 ---
|
||||
type AuditStatus string
|
||||
@@ -184,13 +183,13 @@ func (a AuditContextKey) String() string {
|
||||
// UserActionLog 记录用户的操作历史,用于审计
|
||||
type UserActionLog struct {
|
||||
// 用 ID 和 Time 组成复合主键, 防止高并发时时间重复
|
||||
ID uint `gorm:"primaryKey"`
|
||||
ID uint32 `gorm:"primaryKey"`
|
||||
|
||||
// Time 是操作发生的时间,作为主键和超表的时间分区键
|
||||
Time time.Time `gorm:"primaryKey" json:"time"`
|
||||
|
||||
// --- Who (谁) ---
|
||||
UserID uint `gorm:"not null" json:"user_id,omitempty"`
|
||||
UserID uint32 `gorm:"not null" json:"user_id,omitempty"`
|
||||
Username string `json:"username,omitempty"` // 操作发生时用户名的快照
|
||||
|
||||
// --- Where (何地) ---
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
/*
|
||||
猪场固定资产相关模型
|
||||
*/
|
||||
|
||||
// PigHouse 定义了猪舍,是猪栏的集合
|
||||
type PigHouse struct {
|
||||
gorm.Model
|
||||
Model
|
||||
Name string `gorm:"size:100;not null;unique;comment:猪舍名称, 如 '育肥舍A栋'"`
|
||||
Description string `gorm:"size:255;comment:描述信息"`
|
||||
Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏
|
||||
@@ -30,10 +26,10 @@ const (
|
||||
|
||||
// Pen 是猪栏的物理实体模型, 是所有空间相关数据的“锚点”
|
||||
type Pen struct {
|
||||
gorm.Model
|
||||
Model
|
||||
PenNumber string `gorm:"not null;comment:猪栏的唯一编号, 如 A-01"`
|
||||
HouseID uint `gorm:"index;comment:所属猪舍ID"`
|
||||
PigBatchID *uint `gorm:"index;comment:关联的猪批次ID"`
|
||||
HouseID uint32 `gorm:"index;comment:所属猪舍ID"`
|
||||
PigBatchID *uint32 `gorm:"index;comment:关联的猪批次ID"`
|
||||
Capacity int `gorm:"not null;comment:设计容量 (头)"`
|
||||
Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"`
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -13,10 +11,10 @@ import (
|
||||
// RawMaterial 代表饲料的原料。
|
||||
// 建议:所有重量单位统一存储 (例如, 全部使用 'g'),便于计算和避免转换错误。
|
||||
type RawMaterial struct {
|
||||
gorm.Model
|
||||
Model
|
||||
Name string `gorm:"size:100;unique;not null;comment:原料名称"`
|
||||
Description string `gorm:"size:255;comment:描述"`
|
||||
Quantity float64 `gorm:"not null;comment:库存总量, 单位: g"`
|
||||
Quantity float32 `gorm:"not null;comment:库存总量, 单位: g"`
|
||||
}
|
||||
|
||||
func (RawMaterial) TableName() string {
|
||||
@@ -25,13 +23,13 @@ func (RawMaterial) TableName() string {
|
||||
|
||||
// RawMaterialPurchase 记录了原料的每一次采购。
|
||||
type RawMaterialPurchase struct {
|
||||
gorm.Model
|
||||
RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"`
|
||||
Model
|
||||
RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
|
||||
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
|
||||
Supplier string `gorm:"size:100;comment:供应商"`
|
||||
Amount float64 `gorm:"not null;comment:采购数量, 单位: g"`
|
||||
UnitPrice float64 `gorm:"comment:单价"`
|
||||
TotalPrice float64 `gorm:"comment:总价"`
|
||||
Amount float32 `gorm:"not null;comment:采购数量, 单位: g"`
|
||||
UnitPrice float32 `gorm:"comment:单价"`
|
||||
TotalPrice float32 `gorm:"comment:总价"`
|
||||
PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -54,11 +52,11 @@ const (
|
||||
|
||||
// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
|
||||
type RawMaterialStockLog struct {
|
||||
gorm.Model
|
||||
RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"`
|
||||
ChangeAmount float64 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"`
|
||||
Model
|
||||
RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
|
||||
ChangeAmount float32 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"`
|
||||
SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"`
|
||||
SourceID uint `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"`
|
||||
SourceID uint32 `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"`
|
||||
HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"`
|
||||
Remarks string `gorm:"comment:备注, 如主动领取的理由等"`
|
||||
}
|
||||
@@ -70,7 +68,7 @@ func (RawMaterialStockLog) TableName() string {
|
||||
// FeedFormula 代表饲料配方。
|
||||
// 对于没有配方的外购饲料,可以将其视为一种特殊的 RawMaterial, 并为其创建一个仅包含它自己的 FeedFormula。
|
||||
type FeedFormula struct {
|
||||
gorm.Model
|
||||
Model
|
||||
Name string `gorm:"size:100;unique;not null;comment:配方名称"`
|
||||
Description string `gorm:"size:255;comment:描述"`
|
||||
Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"`
|
||||
@@ -82,11 +80,11 @@ func (FeedFormula) TableName() string {
|
||||
|
||||
// FeedFormulaComponent 代表配方中的一种原料及其占比。
|
||||
type FeedFormulaComponent struct {
|
||||
gorm.Model
|
||||
FeedFormulaID uint `gorm:"not null;index;comment:外键到 FeedFormula"`
|
||||
RawMaterialID uint `gorm:"not null;index;comment:外键到 RawMaterial"`
|
||||
Model
|
||||
FeedFormulaID uint32 `gorm:"not null;index;comment:外键到 FeedFormula"`
|
||||
RawMaterialID uint32 `gorm:"not null;index;comment:外键到 RawMaterial"`
|
||||
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
|
||||
Percentage float64 `gorm:"not null;comment:该原料在配方中的百分比 (0-1.0)"`
|
||||
Percentage float32 `gorm:"not null;comment:该原料在配方中的百分比 (0-1.0)"`
|
||||
}
|
||||
|
||||
func (FeedFormulaComponent) TableName() string {
|
||||
@@ -97,14 +95,14 @@ func (FeedFormulaComponent) TableName() string {
|
||||
// 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula,
|
||||
// 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。
|
||||
type FeedUsageRecord struct {
|
||||
gorm.Model
|
||||
PenID uint `gorm:"not null;index;comment:关联的猪栏ID"`
|
||||
Model
|
||||
PenID uint32 `gorm:"not null;index;comment:关联的猪栏ID"`
|
||||
Pen Pen `gorm:"foreignKey:PenID"`
|
||||
FeedFormulaID uint `gorm:"not null;index;comment:使用的饲料配方ID"`
|
||||
FeedFormulaID uint32 `gorm:"not null;index;comment:使用的饲料配方ID"`
|
||||
FeedFormula FeedFormula `gorm:"foreignKey:FeedFormulaID"`
|
||||
Amount float64 `gorm:"not null;comment:使用数量, 单位: g"`
|
||||
Amount float32 `gorm:"not null;comment:使用数量, 单位: g"`
|
||||
RecordedAt time.Time `gorm:"primaryKey;comment:记录时间"`
|
||||
OperatorID uint `gorm:"not null;comment:操作员"`
|
||||
OperatorID uint32 `gorm:"not null;comment:操作员"`
|
||||
Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"`
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -48,21 +47,21 @@ type PowderInstructions struct {
|
||||
// 出栏前停药期
|
||||
WithdrawalPeriod time.Duration `json:"withdrawal_period"`
|
||||
// 拌料使用计量, 每千克体重用多少克药, 单位: g/kg
|
||||
BodyWeightDosageUsed float64 `json:"body_weight_dosage_used"`
|
||||
BodyWeightDosageUsed float32 `json:"body_weight_dosage_used"`
|
||||
// 拌料使用剂量, 每升水加多少克药或每千克饲料干重加多少克药, 单位: g/kg(L)
|
||||
MixDosageUsed float64 `json:"mix_dosage_used"`
|
||||
MixDosageUsed float32 `json:"mix_dosage_used"`
|
||||
// 拌料使用方式, 兑水/拌料
|
||||
MixType MixType `json:"mix_type"`
|
||||
}
|
||||
|
||||
// Medication 定义了兽药/疫苗的基本信息模型
|
||||
type Medication struct {
|
||||
gorm.Model
|
||||
Model
|
||||
Name string `gorm:"size:100;not null;comment:药品名称" json:"name"`
|
||||
Type MedicationType `gorm:"size:20;not null;comment:兽药类型 (粉剂, 针剂, 疫苗)" json:"type"`
|
||||
Category MedicationCategory `gorm:"size:30;not null;comment:兽药种类 (四环素类, 磺胺类等)" json:"category"`
|
||||
DosagePerUnit float64 `gorm:"size:50;comment:一份药物的计量 (针剂计量单位为毫升, 粉剂为克)" json:"dosage_per_unit"`
|
||||
ActiveIngredientConcentration float64 `gorm:"size:50;comment:有效成分含量百分比" json:"active_ingredient_concentration"`
|
||||
DosagePerUnit float32 `gorm:"size:50;comment:一份药物的计量 (针剂计量单位为毫升, 粉剂为克)" json:"dosage_per_unit"`
|
||||
ActiveIngredientConcentration float32 `gorm:"size:50;comment:有效成分含量百分比" json:"active_ingredient_concentration"`
|
||||
Manufacturer string `gorm:"size:100;comment:生产厂家" json:"manufacturer"`
|
||||
Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"`
|
||||
}
|
||||
@@ -82,15 +81,15 @@ const (
|
||||
|
||||
// MedicationLog 记录了对整个猪批次的用药情况
|
||||
type MedicationLog struct {
|
||||
gorm.Model
|
||||
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
MedicationID uint `gorm:"not null;index;comment:关联的药品ID"`
|
||||
Model
|
||||
PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
MedicationID uint32 `gorm:"not null;index;comment:关联的药品ID"`
|
||||
Medication Medication `gorm:"foreignKey:MedicationID"` // 预加载药品信息
|
||||
DosageUsed float64 `gorm:"not null;comment:使用的总剂量 (单位由药品决定,如g或ml)"`
|
||||
DosageUsed float32 `gorm:"not null;comment:使用的总剂量 (单位由药品决定,如g或ml)"`
|
||||
TargetCount int `gorm:"not null;comment:用药对象数量"`
|
||||
Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"`
|
||||
Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"`
|
||||
OperatorID uint `gorm:"comment:操作员ID"`
|
||||
OperatorID uint32 `gorm:"comment:操作员ID"`
|
||||
HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"`
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,20 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Model 用于代替gorm.Model, 使用uint32以节约空间
|
||||
type Model struct {
|
||||
ID uint32 `gorm:"primarykey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
// GetAllModels 返回一个包含所有数据库模型实例的切片。
|
||||
// 这个函数用于在数据库初始化时自动迁移所有的表结构。
|
||||
func GetAllModels() []interface{} {
|
||||
@@ -61,15 +73,19 @@ func GetAllModels() []interface{} {
|
||||
&Medication{},
|
||||
&MedicationLog{},
|
||||
|
||||
// Alarm Models
|
||||
&ActiveAlarm{},
|
||||
&HistoricalAlarm{},
|
||||
|
||||
// Notification Models
|
||||
&Notification{},
|
||||
}
|
||||
}
|
||||
|
||||
// UintArray 是一个自定义类型,代表 uint 的切片。
|
||||
// UintArray 是一个自定义类型,代表 uint32 的切片。
|
||||
// 它实现了 gorm.Scanner 和 driver.Valuer 接口,
|
||||
// 以便能与数据库的 bigint[] 类型进行原生映射。
|
||||
type UintArray []uint
|
||||
type UintArray []uint32
|
||||
|
||||
// Value 实现了 driver.Valuer 接口。
|
||||
// 它告诉 GORM 如何将 UintArray ([]) 转换为数据库能够理解的格式。
|
||||
@@ -111,21 +127,86 @@ func (a *UintArray) Scan(src interface{}) error {
|
||||
// 去掉花括号
|
||||
srcStr = strings.Trim(srcStr, "{}")
|
||||
if srcStr == "" {
|
||||
*a = []uint{}
|
||||
*a = []uint32{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 按逗号分割
|
||||
parts := strings.Split(srcStr, ",")
|
||||
arr := make([]uint, len(parts))
|
||||
arr := make([]uint32, len(parts))
|
||||
for i, p := range parts {
|
||||
val, err := strconv.ParseUint(p, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析 UintArray 元素失败: %w", err)
|
||||
}
|
||||
arr[i] = uint(val)
|
||||
arr[i] = uint32(val)
|
||||
}
|
||||
|
||||
*a = arr
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeverityLevel 定义了系统中告警、通知、日志的统一级别枚举。
|
||||
// 它以中文形式存储在数据库中,提高了可读性。
|
||||
type SeverityLevel string
|
||||
|
||||
const (
|
||||
// DebugLevel 调试级别,用于开发和诊断问题。
|
||||
DebugLevel SeverityLevel = "Debug"
|
||||
// InfoLevel 信息级别,用于记录常规操作。
|
||||
InfoLevel SeverityLevel = "Info"
|
||||
// WarnLevel 警告级别,表示出现潜在问题,需要关注。
|
||||
WarnLevel SeverityLevel = "Warn"
|
||||
// ErrorLevel 错误级别,表示发生了需要处理的错误。
|
||||
ErrorLevel SeverityLevel = "Error"
|
||||
// DPanicLevel 开发时崩溃级别,在开发模式下会触发 panic。
|
||||
DPanicLevel SeverityLevel = "DPanic"
|
||||
// PanicLevel 崩溃级别,记录日志后会立即触发 panic。
|
||||
PanicLevel SeverityLevel = "Panic"
|
||||
// FatalLevel 致命级别,记录日志后会调用 os.Exit(1) 退出程序。
|
||||
FatalLevel SeverityLevel = "Fatal"
|
||||
)
|
||||
|
||||
// ToZapLevel 将我们的自定义级别转换为 zapcore.Level,以便与日志记录器兼容。
|
||||
func (al SeverityLevel) ToZapLevel() zapcore.Level {
|
||||
switch al {
|
||||
case DebugLevel:
|
||||
return zapcore.DebugLevel
|
||||
case InfoLevel:
|
||||
return zapcore.InfoLevel
|
||||
case WarnLevel:
|
||||
return zapcore.WarnLevel
|
||||
case ErrorLevel:
|
||||
return zapcore.ErrorLevel
|
||||
case DPanicLevel:
|
||||
return zapcore.DPanicLevel
|
||||
case PanicLevel:
|
||||
return zapcore.PanicLevel
|
||||
case FatalLevel:
|
||||
return zapcore.FatalLevel
|
||||
default:
|
||||
// 默认情况下返回 Info 级别,保证程序健壮性
|
||||
return zapcore.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 实现了 sql.Scanner 接口,GORM 在从数据库读取数据时会调用此方法。
|
||||
func (al *SeverityLevel) Scan(value interface{}) error {
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
// 尝试处理其他可能的类型,例如字符串
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("无法将值 %v (类型 %T) 扫描为 SeverityLevel", value, value)
|
||||
}
|
||||
*al = SeverityLevel(s)
|
||||
return nil
|
||||
}
|
||||
*al = SeverityLevel(bytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value 实现了 driver.Valuer 接口,GORM 在将数据写入数据库时会调用此方法。
|
||||
func (al SeverityLevel) Value() (driver.Value, error) {
|
||||
return string(al), nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/notify"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gorm.io/gorm"
|
||||
// NotifierType 定义了通知器的类型。
|
||||
type NotifierType string
|
||||
|
||||
const (
|
||||
// NotifierTypeSMTP 表示 SMTP 邮件通知器。
|
||||
NotifierTypeSMTP NotifierType = "邮件"
|
||||
// NotifierTypeWeChat 表示企业微信通知器。
|
||||
NotifierTypeWeChat NotifierType = "企业微信"
|
||||
// NotifierTypeLark 表示飞书通知器。
|
||||
NotifierTypeLark NotifierType = "飞书"
|
||||
// NotifierTypeLog 表示日志通知器,作为最终的告警记录渠道。
|
||||
NotifierTypeLog NotifierType = "日志"
|
||||
)
|
||||
|
||||
// NotificationStatus 定义了通知发送尝试的状态枚举。
|
||||
@@ -19,48 +27,20 @@ const (
|
||||
NotificationStatusSkipped NotificationStatus = "已跳过" // 通知因某些原因被跳过(例如:用户未配置联系方式)
|
||||
)
|
||||
|
||||
// LogLevel is a custom type for zapcore.Level to handle database scanning and valuing.
|
||||
type LogLevel zapcore.Level
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (l *LogLevel) Scan(value interface{}) error {
|
||||
var s string
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
s = string(v)
|
||||
case string:
|
||||
s = v
|
||||
default:
|
||||
return errors.New("LogLevel的类型无效")
|
||||
}
|
||||
|
||||
var zl zapcore.Level
|
||||
if err := zl.UnmarshalText([]byte(s)); err != nil {
|
||||
return err
|
||||
}
|
||||
*l = LogLevel(zl)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (l LogLevel) Value() (driver.Value, error) {
|
||||
return (zapcore.Level)(l).String(), nil
|
||||
}
|
||||
|
||||
// Notification 表示已发送或尝试发送的通知记录。
|
||||
type Notification struct {
|
||||
gorm.Model
|
||||
Model
|
||||
|
||||
// NotifierType 通知器类型 (例如:"邮件", "企业微信", "飞书", "日志")
|
||||
NotifierType notify.NotifierType `gorm:"type:varchar(20);not null;index" json:"notifier_type"`
|
||||
NotifierType NotifierType `gorm:"type:varchar(20);not null;index" json:"notifier_type"`
|
||||
// UserID 接收通知的用户ID,用于追溯通知记录到特定用户
|
||||
UserID uint `gorm:"index" json:"user_id"` // 增加 UserID 字段,并添加索引
|
||||
UserID uint32 `gorm:"index" json:"user_id"` // 增加 UserID 字段,并添加索引
|
||||
// Title 通知标题
|
||||
Title string `gorm:"type:varchar(255);not null" json:"title"`
|
||||
// Message 通知内容
|
||||
Message string `gorm:"type:text;not null" json:"message"`
|
||||
// Level 通知级别 (例如:INFO, WARN, ERROR)
|
||||
Level LogLevel `gorm:"type:varchar(10);not null" json:"level"`
|
||||
Level SeverityLevel `gorm:"type:varchar(10);not null" json:"level"`
|
||||
// AlarmTimestamp 通知内容生成时的时间戳,与 ID 构成复合主键
|
||||
AlarmTimestamp time.Time `gorm:"primaryKey;not null" json:"alarm_timestamp"`
|
||||
// ToAddress 接收地址 (例如:邮箱地址, 企业微信ID, 日志标识符)
|
||||
|
||||
@@ -2,8 +2,6 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -32,7 +30,7 @@ const (
|
||||
|
||||
// PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪
|
||||
type PigBatch struct {
|
||||
gorm.Model
|
||||
Model
|
||||
BatchNumber string `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"`
|
||||
OriginType PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"`
|
||||
StartDate time.Time `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"`
|
||||
@@ -65,14 +63,14 @@ const (
|
||||
|
||||
// PigBatchLog 记录了猪批次数量或状态的每一次变更
|
||||
type PigBatchLog struct {
|
||||
gorm.Model
|
||||
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
Model
|
||||
PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"`
|
||||
ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"`
|
||||
Reason string `gorm:"size:255;comment:变更原因描述"`
|
||||
BeforeCount int `gorm:"not null;comment:变更前总数"`
|
||||
AfterCount int `gorm:"not null;comment:变更后总数"`
|
||||
OperatorID uint `gorm:"comment:操作员ID"`
|
||||
OperatorID uint32 `gorm:"comment:操作员ID"`
|
||||
HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"`
|
||||
}
|
||||
|
||||
@@ -82,10 +80,10 @@ func (PigBatchLog) TableName() string {
|
||||
|
||||
// WeighingBatch 记录了一次批次称重的信息
|
||||
type WeighingBatch struct {
|
||||
gorm.Model
|
||||
Model
|
||||
WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"`
|
||||
Description string `gorm:"size:255;comment:批次称重描述"`
|
||||
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
}
|
||||
|
||||
func (WeighingBatch) TableName() string {
|
||||
@@ -94,11 +92,11 @@ func (WeighingBatch) TableName() string {
|
||||
|
||||
// WeighingRecord 记录了单次称重信息
|
||||
type WeighingRecord struct {
|
||||
gorm.Model
|
||||
Weight float64 `gorm:"not null;comment:单只猪重量 (kg)"`
|
||||
WeighingBatchID uint `gorm:"not null;index;comment:关联的批次称重ID"`
|
||||
PenID uint `gorm:"not null;index;comment:所在猪圈ID"`
|
||||
OperatorID uint `gorm:"not null;comment:操作员ID"`
|
||||
Model
|
||||
Weight float32 `gorm:"not null;comment:单只猪重量 (kg)"`
|
||||
WeighingBatchID uint32 `gorm:"not null;index;comment:关联的批次称重ID"`
|
||||
PenID uint32 `gorm:"not null;index;comment:所在猪圈ID"`
|
||||
OperatorID uint32 `gorm:"not null;comment:操作员ID"`
|
||||
Remark string `gorm:"size:255;comment:备注"`
|
||||
WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"`
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PigBatchSickPigTreatmentLocation 定义了病猪治疗地点
|
||||
@@ -29,16 +27,16 @@ const (
|
||||
|
||||
// PigSickLog 记录了猪批次中病猪数量的变化日志
|
||||
type PigSickLog struct {
|
||||
gorm.Model
|
||||
PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"`
|
||||
PenID uint `gorm:"not null;index;comment:所在猪圈ID"`
|
||||
Model
|
||||
PigBatchID uint32 `gorm:"primaryKey;comment:关联的猪批次ID"`
|
||||
PenID uint32 `gorm:"not null;index;comment:所在猪圈ID"`
|
||||
ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"`
|
||||
Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"`
|
||||
BeforeCount int `gorm:"comment:变化前的数量"`
|
||||
AfterCount int `gorm:"comment:变化后的数量"`
|
||||
Remarks string `gorm:"size:255;comment:备注"`
|
||||
TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"`
|
||||
OperatorID uint `gorm:"comment:操作员ID"`
|
||||
OperatorID uint32 `gorm:"comment:操作员ID"`
|
||||
HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"`
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,19 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PigPurchase 记录了猪只采购信息
|
||||
type PigPurchase struct {
|
||||
gorm.Model
|
||||
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
Model
|
||||
PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
|
||||
Supplier string `gorm:"comment:供应商"`
|
||||
Quantity int `gorm:"not null;comment:采购数量"`
|
||||
UnitPrice float64 `gorm:"not null;comment:单价"`
|
||||
TotalPrice float64 `gorm:"not null;comment:总价"`
|
||||
UnitPrice float32 `gorm:"not null;comment:单价"`
|
||||
TotalPrice float32 `gorm:"not null;comment:总价"`
|
||||
Remarks string `gorm:"size:255;comment:备注"`
|
||||
OperatorID uint `gorm:"comment:操作员ID"`
|
||||
OperatorID uint32 `gorm:"comment:操作员ID"`
|
||||
}
|
||||
|
||||
func (PigPurchase) TableName() string {
|
||||
@@ -25,15 +23,15 @@ func (PigPurchase) TableName() string {
|
||||
|
||||
// PigSale 记录了猪只销售信息
|
||||
type PigSale struct {
|
||||
gorm.Model
|
||||
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
Model
|
||||
PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
|
||||
SaleDate time.Time `gorm:"primaryKey;comment:销售日期"`
|
||||
Buyer string `gorm:"comment:购买方"`
|
||||
Quantity int `gorm:"not null;comment:销售数量"`
|
||||
UnitPrice float64 `gorm:"not null;comment:单价"`
|
||||
TotalPrice float64 `gorm:"not null;comment:总价"`
|
||||
UnitPrice float32 `gorm:"not null;comment:单价"`
|
||||
TotalPrice float32 `gorm:"not null;comment:总价"`
|
||||
Remarks string `gorm:"size:255;comment:备注"`
|
||||
OperatorID uint `gorm:"comment:操作员ID"`
|
||||
OperatorID uint32 `gorm:"comment:操作员ID"`
|
||||
}
|
||||
|
||||
func (PigSale) TableName() string {
|
||||
|
||||
@@ -2,8 +2,6 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PigTransferType 定义了猪只迁移的类型
|
||||
@@ -23,14 +21,14 @@ const (
|
||||
// PigTransferLog 记录了每一次猪只数量在猪栏间的变动事件。
|
||||
// 它作为事件溯源的基础,用于推算任意时间点猪栏的猪只数量。
|
||||
type PigTransferLog struct {
|
||||
gorm.Model
|
||||
Model
|
||||
TransferTime time.Time `gorm:"primaryKey;comment:迁移发生时间" json:"transfer_time"` // 迁移发生时间,作为联合主键
|
||||
PigBatchID uint `gorm:"primaryKey;comment:关联的猪群ID" json:"pig_batch_id"` // 关联的猪群ID,作为联合主键
|
||||
PenID uint `gorm:"primaryKey;comment:发生变动的猪栏ID" json:"pen_id"` // 发生变动的猪栏ID,作为联合主键
|
||||
PigBatchID uint32 `gorm:"primaryKey;comment:关联的猪群ID" json:"pig_batch_id"` // 关联的猪群ID,作为联合主键
|
||||
PenID uint32 `gorm:"primaryKey;comment:发生变动的猪栏ID" json:"pen_id"` // 发生变动的猪栏ID,作为联合主键
|
||||
Quantity int `gorm:"not null;comment:变动数量(正数表示增加,负数表示减少)" json:"quantity"` // 变动数量(正数表示增加,负数减少)
|
||||
Type PigTransferType `gorm:"not null;comment:变动类型" json:"type"` // 变动类型,使用枚举类型
|
||||
CorrelationID string `gorm:"comment:用于关联一次完整操作(如一次调栏会产生两条日志)" json:"correlation_id"` // 用于关联一次完整操作
|
||||
OperatorID uint `gorm:"not null;comment:操作员ID" json:"operator_id"` // 操作员ID
|
||||
OperatorID uint32 `gorm:"not null;comment:操作员ID" json:"operator_id"` // 操作员ID
|
||||
Remarks string `gorm:"comment:备注" json:"remarks"`
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,15 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PlanName string
|
||||
|
||||
const (
|
||||
// PlanNamePeriodicSystemHealthCheck 是周期性系统健康检查计划的名称
|
||||
PlanNamePeriodicSystemHealthCheck PlanName = "周期性系统健康检查"
|
||||
// PlanNameAlarmNotification 是告警通知发送计划的名称
|
||||
PlanNameAlarmNotification PlanName = "告警通知发送"
|
||||
)
|
||||
|
||||
// PlanExecutionType 定义了计划的执行类型
|
||||
type PlanExecutionType string
|
||||
|
||||
@@ -31,10 +40,13 @@ const (
|
||||
type TaskType string
|
||||
|
||||
const (
|
||||
TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务
|
||||
TaskTypeWaiting TaskType = "等待" // 等待任务
|
||||
TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务
|
||||
TaskTypeFullCollection TaskType = "全量采集" // 新增的全量采集任务
|
||||
TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务
|
||||
TaskTypeWaiting TaskType = "等待" // 等待任务
|
||||
TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务
|
||||
TaskTypeFullCollection TaskType = "全量采集" // 新增的全量采集任务
|
||||
TaskTypeAlarmNotification TaskType = "告警通知" // 告警通知任务
|
||||
TaskTypeDeviceThresholdCheck TaskType = "设备阈值检查" // 设备阈值检查任务
|
||||
TaskTypeAreaCollectorThresholdCheck TaskType = "区域阈值检查" // 区域阈值检查任务
|
||||
)
|
||||
|
||||
// -- Task Parameters --
|
||||
@@ -62,15 +74,15 @@ const (
|
||||
|
||||
// Plan 代表系统中的一个计划,可以包含子计划或任务
|
||||
type Plan struct {
|
||||
gorm.Model
|
||||
Model
|
||||
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Name PlanName `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"` // 计划预期执行次数
|
||||
ExecuteCount uint `gorm:"default:0" json:"execute_count"` // 执行计数器
|
||||
ExecuteNum uint32 `gorm:"default:0" json:"execute_num"` // 计划预期执行次数
|
||||
ExecuteCount uint32 `gorm:"default:0" json:"execute_count"` // 执行计数器
|
||||
|
||||
// 针对 PlanExecutionTypeAutomatic,使用 Cron 表达式定义调度规则
|
||||
CronExpression string `json:"cron_expression"`
|
||||
@@ -151,12 +163,12 @@ func (p *Plan) ReorderSteps() {
|
||||
|
||||
// SubPlan 代表作为另一个计划一部分的子计划,具有执行顺序
|
||||
type SubPlan struct {
|
||||
gorm.Model
|
||||
Model
|
||||
|
||||
ParentPlanID uint `gorm:"not null;index" json:"parent_plan_id"` // 父计划的ID
|
||||
ChildPlanID uint `gorm:"not null;index" json:"child_plan_id"` // 子计划的ID (它本身也是一个 Plan)
|
||||
ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在父计划中的执行顺序
|
||||
ChildPlan *Plan `gorm:"-" json:"child_plan"` // 完整子计划数据,仅内存中
|
||||
ParentPlanID uint32 `gorm:"not null;index" json:"parent_plan_id"` // 父计划的ID
|
||||
ChildPlanID uint32 `gorm:"not null;index" json:"child_plan_id"` // 子计划的ID (它本身也是一个 Plan)
|
||||
ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在父计划中的执行顺序
|
||||
ChildPlan *Plan `gorm:"-" json:"child_plan"` // 完整子计划数据,仅内存中
|
||||
}
|
||||
|
||||
// TableName 自定义 GORM 使用的数据库表名
|
||||
@@ -172,7 +184,7 @@ type Task struct {
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"` // 保持软删除功能
|
||||
|
||||
PlanID uint `gorm:"not null;index" json:"plan_id"` // 此任务所属计划的ID
|
||||
PlanID uint32 `gorm:"not null;index" json:"plan_id"` // 此任务所属计划的ID
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Description string `json:"description"`
|
||||
ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在计划中的执行顺序
|
||||
@@ -201,11 +213,25 @@ func (t Task) ParseParameters(v interface{}) error {
|
||||
return json.Unmarshal(t.Parameters, v)
|
||||
}
|
||||
|
||||
// SaveParameters 将一个结构体序列化为 JSON 并保存到 Task 的 Parameters 字段。
|
||||
// 示例:
|
||||
//
|
||||
// params := LoraParameters{...}
|
||||
// if err := task.SaveParameters(params); err != nil { ... }
|
||||
func (t *Task) SaveParameters(v interface{}) error {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化任务参数失败: %w", err)
|
||||
}
|
||||
t.Parameters = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeviceTask 是设备和任务之间的关联模型,表示一个设备可以执行多个任务,一个任务可以被多个设备执行。
|
||||
type DeviceTask struct {
|
||||
gorm.Model
|
||||
DeviceID uint `gorm:"not null;index"` // 设备ID
|
||||
TaskID uint `gorm:"not null;index"` // 任务ID
|
||||
Model
|
||||
DeviceID uint32 `gorm:"not null;index"` // 设备ID
|
||||
TaskID uint32 `gorm:"not null;index"` // 任务ID
|
||||
|
||||
// 可选:如果需要存储关联的额外信息,可以在这里添加字段,例如:
|
||||
// Configuration datatypes.JSON `json:"configuration"` // 任务在特定设备上的配置
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
// PendingTask 是一个待执行任务队列, 里面会储存待执行的Task以及这个Task什么时候执行
|
||||
// 它是一个纯粹的工作队列,任务被认领后即被删除。
|
||||
type PendingTask struct {
|
||||
// 手动填充必须字段以实现硬删除,不内嵌 gorm.Model
|
||||
ID uint `gorm:"primarykey"`
|
||||
// 手动填充必须字段以实现硬删除,不内嵌 Model
|
||||
ID uint32 `gorm:"primarykey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
@@ -21,7 +21,7 @@ type PendingTask struct {
|
||||
Task *Task `gorm:"foreignKey:TaskID"`
|
||||
|
||||
ExecuteAt time.Time `gorm:"index"` // 任务执行时间
|
||||
TaskExecutionLogID uint `gorm:"unique;not null;index"` // 对应的执行历史记录ID
|
||||
TaskExecutionLogID uint32 `gorm:"unique;not null;index"` // 对应的执行历史记录ID
|
||||
|
||||
// 通过 TaskExecutionLogID 关联到唯一的 TaskExecutionLog 记录
|
||||
// ON DELETE CASCADE 确保如果日志被删除,这个待办任务也会被自动清理
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
@@ -20,7 +22,7 @@ const (
|
||||
// SignalMetrics 存储信号强度数据
|
||||
type SignalMetrics struct {
|
||||
RssiDbm int `json:"rssi_dbm"` // 绝对信号强度(dBm),受距离、障碍物影响
|
||||
SnrDb float64 `json:"snr_db"` // 信号与噪声的相对比率(dB),由 RSSI 减去噪声地板(Noise Floor)
|
||||
SnrDb float32 `json:"snr_db"` // 信号与噪声的相对比率(dB),由 RSSI 减去噪声地板(Noise Floor)
|
||||
SensitivityDbm int `json:"sensitivity_dbm"` // 网关的最低检测阈值(dBm)
|
||||
MarginDb int `json:"margin_db"` // SNR 相对于接收器灵敏度的余量, Margin = SNR - Sensitivity
|
||||
}
|
||||
@@ -34,17 +36,17 @@ type BatteryLevel struct {
|
||||
|
||||
// TemperatureData 存储温度数据
|
||||
type TemperatureData struct {
|
||||
TemperatureCelsius float64 `json:"temperature_celsius"` // 温度值 (摄氏度)
|
||||
TemperatureCelsius float32 `json:"temperature_celsius"` // 温度值 (摄氏度)
|
||||
}
|
||||
|
||||
// HumidityData 存储湿度数据
|
||||
type HumidityData struct {
|
||||
HumidityPercent float64 `json:"humidity_percent"` // 湿度值 (%)
|
||||
HumidityPercent float32 `json:"humidity_percent"` // 湿度值 (%)
|
||||
}
|
||||
|
||||
// WeightData 存储重量数据
|
||||
type WeightData struct {
|
||||
WeightKilograms float64 `json:"weight_kilograms"` // 重量值 (公斤)
|
||||
WeightKilograms float32 `json:"weight_kilograms"` // 重量值 (公斤)
|
||||
}
|
||||
|
||||
// SensorData 存储所有类型的传感器数据,对应数据库中的 'sensor_data' 表。
|
||||
@@ -53,10 +55,10 @@ type SensorData struct {
|
||||
Time time.Time `gorm:"primaryKey" json:"time"`
|
||||
|
||||
// DeviceID 是传感器的唯一标识符,作为复合主键的另一部分。
|
||||
DeviceID uint `gorm:"primaryKey" json:"device_id"`
|
||||
DeviceID uint32 `gorm:"primaryKey" json:"device_id"`
|
||||
|
||||
// RegionalControllerID 是上报此数据的区域主控的ID。
|
||||
RegionalControllerID uint `json:"regional_controller_id"`
|
||||
// AreaControllerID 是上报此数据的区域主控的ID。
|
||||
AreaControllerID uint32 `json:"area_controller_id"`
|
||||
|
||||
// SensorType 是传感数据的类型
|
||||
SensorType SensorType `gorm:"not null;index" json:"sensor_type"`
|
||||
@@ -68,3 +70,12 @@ type SensorData struct {
|
||||
func (SensorData) TableName() string {
|
||||
return "sensor_data"
|
||||
}
|
||||
|
||||
// ParseData 解析 JSON 数据到一个具体的结构体中。
|
||||
// 调用方需要传入一个指向目标结构体实例的指针。
|
||||
func (s *SensorData) ParseData(v interface{}) error {
|
||||
if s.Data == nil {
|
||||
return errors.New("传感器数据为空,无法解析")
|
||||
}
|
||||
return json.Unmarshal(s.Data, v)
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ func (ci ContactInfo) Value() (driver.Value, error) {
|
||||
|
||||
// User 代表系统中的用户模型
|
||||
type User struct {
|
||||
// gorm.Model 内嵌了 ID, CreatedAt, UpdatedAt, 和 DeletedAt
|
||||
// Model 内嵌了 ID, CreatedAt, UpdatedAt, 和 DeletedAt
|
||||
// DeletedAt 字段的存在自动为 GORM 开启了软删除模式
|
||||
gorm.Model
|
||||
Model
|
||||
|
||||
// Username 是用户的登录名,应该是唯一的
|
||||
// 修正了 gorm 标签的拼写错误 (移除了 gorm 后面的冒号)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -65,7 +66,7 @@ func (l *larkNotifier) Send(ctx context.Context, content AlarmContent, toAddr st
|
||||
"tag": "lark_md",
|
||||
"content": fmt.Sprintf("## %s\n**级别**: %s\n**时间**: %s\n\n%s",
|
||||
content.Title,
|
||||
content.Level.String(),
|
||||
content.Level,
|
||||
content.Timestamp.Format(DefaultTimeFormat),
|
||||
content.Message,
|
||||
),
|
||||
@@ -171,8 +172,8 @@ func (l *larkNotifier) getAccessToken(ctx context.Context) (string, error) {
|
||||
}
|
||||
|
||||
// Type 返回通知器的类型
|
||||
func (l *larkNotifier) Type() NotifierType {
|
||||
return NotifierTypeLark
|
||||
func (l *larkNotifier) Type() models.NotifierType {
|
||||
return models.NotifierTypeLark
|
||||
}
|
||||
|
||||
// --- API 数据结构 ---
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
// logNotifier 实现了 Notifier 接口,用于将告警信息记录到日志中。
|
||||
@@ -24,10 +25,10 @@ func NewLogNotifier(ctx context.Context) Notifier {
|
||||
func (l *logNotifier) Send(ctx context.Context, content AlarmContent, toAddr string) error {
|
||||
logger := logs.TraceLogger(ctx, l.ctx, "Send")
|
||||
logger.Infow("告警已记录到日志",
|
||||
"notifierType", NotifierTypeLog,
|
||||
"notifierType", models.NotifierTypeLog,
|
||||
"title", content.Title,
|
||||
"message", content.Message,
|
||||
"level", content.Level.String(),
|
||||
"level", content.Level,
|
||||
"timestamp", content.Timestamp.Format(DefaultTimeFormat),
|
||||
"toAddr", toAddr,
|
||||
)
|
||||
@@ -35,6 +36,6 @@ func (l *logNotifier) Send(ctx context.Context, content AlarmContent, toAddr str
|
||||
}
|
||||
|
||||
// Type 返回通知器的类型。
|
||||
func (l *logNotifier) Type() NotifierType {
|
||||
return NotifierTypeLog
|
||||
func (l *logNotifier) Type() models.NotifierType {
|
||||
return models.NotifierTypeLog
|
||||
}
|
||||
|
||||
@@ -4,26 +4,12 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
// DefaultTimeFormat 定义了所有通知中统一使用的时间格式。
|
||||
const DefaultTimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
// NotifierType 定义了通知器的类型。
|
||||
type NotifierType string
|
||||
|
||||
const (
|
||||
// NotifierTypeSMTP 表示 SMTP 邮件通知器。
|
||||
NotifierTypeSMTP NotifierType = "邮件"
|
||||
// NotifierTypeWeChat 表示企业微信通知器。
|
||||
NotifierTypeWeChat NotifierType = "企业微信"
|
||||
// NotifierTypeLark 表示飞书通知器。
|
||||
NotifierTypeLark NotifierType = "飞书"
|
||||
// NotifierTypeLog 表示日志通知器,作为最终的告警记录渠道。
|
||||
NotifierTypeLog NotifierType = "日志"
|
||||
)
|
||||
|
||||
// AlarmContent 定义了通知的内容
|
||||
type AlarmContent struct {
|
||||
// 通知标题
|
||||
@@ -31,7 +17,7 @@ type AlarmContent struct {
|
||||
// 通知信息
|
||||
Message string
|
||||
// 通知级别
|
||||
Level zapcore.Level
|
||||
Level models.SeverityLevel
|
||||
// 通知时间
|
||||
Timestamp time.Time
|
||||
}
|
||||
@@ -41,5 +27,5 @@ type Notifier interface {
|
||||
// Send 发送通知
|
||||
Send(ctx context.Context, content AlarmContent, toAddr string) error
|
||||
// Type 返回通知器的类型
|
||||
Type() NotifierType
|
||||
Type() models.NotifierType
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
// smtpNotifier 实现了 Notifier 接口,用于通过 SMTP 发送邮件通知。
|
||||
@@ -45,7 +47,7 @@ func (s *smtpNotifier) Send(ctx context.Context, content AlarmContent, toAddr st
|
||||
|
||||
// 邮件正文
|
||||
body := fmt.Sprintf("级别: %s\n时间: %s\n\n%s",
|
||||
content.Level.String(),
|
||||
content.Level,
|
||||
content.Timestamp.Format(DefaultTimeFormat),
|
||||
content.Message,
|
||||
)
|
||||
@@ -71,6 +73,6 @@ func (s *smtpNotifier) Send(ctx context.Context, content AlarmContent, toAddr st
|
||||
}
|
||||
|
||||
// Type 返回通知器的类型
|
||||
func (s *smtpNotifier) Type() NotifierType {
|
||||
return NotifierTypeSMTP
|
||||
func (s *smtpNotifier) Type() models.NotifierType {
|
||||
return models.NotifierTypeSMTP
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -55,7 +57,7 @@ func (w *wechatNotifier) Send(ctx context.Context, content AlarmContent, toAddr
|
||||
// 2. 构建 markdown 内容
|
||||
markdownContent := fmt.Sprintf("## %s\n> 级别: <font color=\"warning\">%s</font>\n> 时间: %s\n\n%s",
|
||||
content.Title,
|
||||
content.Level.String(),
|
||||
content.Level,
|
||||
content.Timestamp.Format(DefaultTimeFormat),
|
||||
content.Message,
|
||||
)
|
||||
@@ -142,8 +144,8 @@ func (w *wechatNotifier) getAccessToken() (string, error) {
|
||||
}
|
||||
|
||||
// Type 返回通知器的类型
|
||||
func (w *wechatNotifier) Type() NotifierType {
|
||||
return NotifierTypeWeChat
|
||||
func (w *wechatNotifier) Type() models.NotifierType {
|
||||
return models.NotifierTypeWeChat
|
||||
}
|
||||
|
||||
// --- API 数据结构 ---
|
||||
|
||||
430
internal/infra/repository/alarm_repository.go
Normal file
430
internal/infra/repository/alarm_repository.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ActiveAlarmListOptions 定义了查询活跃告警列表时的可选参数
|
||||
type ActiveAlarmListOptions struct {
|
||||
SourceType *models.AlarmSourceType // 按告警来源类型过滤
|
||||
SourceID *uint32 // 按告警来源ID过滤
|
||||
Level *models.SeverityLevel // 按告警严重性等级过滤
|
||||
IsIgnored *bool // 按是否被忽略过滤
|
||||
TriggerTime *time.Time // 告警触发时间范围 - 开始时间
|
||||
EndTime *time.Time // 告警触发时间范围 - 结束时间
|
||||
OrderBy string // 排序字段,例如 "trigger_time DESC"
|
||||
}
|
||||
|
||||
// HistoricalAlarmListOptions 定义了查询历史告警列表时的可选参数
|
||||
type HistoricalAlarmListOptions struct {
|
||||
SourceType *models.AlarmSourceType // 按告警来源类型过滤
|
||||
SourceID *uint32 // 按告警来源ID过滤
|
||||
Level *models.SeverityLevel // 按告警严重性等级过滤
|
||||
TriggerTimeStart *time.Time // 告警触发时间范围 - 开始时间
|
||||
TriggerTimeEnd *time.Time // 告警触发时间范围 - 结束时间
|
||||
ResolveTimeStart *time.Time // 告警解决时间范围 - 开始时间 (对应 models.HistoricalAlarm.ResolveTime)
|
||||
ResolveTimeEnd *time.Time // 告警解决时间范围 - 结束时间 (对应 models.HistoricalAlarm.ResolveTime)
|
||||
OrderBy string // 排序字段,例如 "trigger_time DESC"
|
||||
}
|
||||
|
||||
// AlarmRepository 定义了对告警模型的数据库操作接口
|
||||
type AlarmRepository interface {
|
||||
// CreateActiveAlarm 创建一条新的活跃告警记录
|
||||
CreateActiveAlarm(ctx context.Context, alarm *models.ActiveAlarm) error
|
||||
|
||||
// IsAlarmActiveInUse 检查具有相同来源和告警代码的告警当前是否处于活跃表中
|
||||
IsAlarmActiveInUse(ctx context.Context, sourceType models.AlarmSourceType, sourceID uint32, alarmCode models.AlarmCode) (bool, error)
|
||||
|
||||
// GetActiveAlarmByUniqueFieldsTx 在指定事务中根据唯一业务键获取一个活跃告警
|
||||
GetActiveAlarmByUniqueFieldsTx(ctx context.Context, tx *gorm.DB, sourceType models.AlarmSourceType, sourceID uint32, alarmCode models.AlarmCode) (*models.ActiveAlarm, error)
|
||||
|
||||
// CreateHistoricalAlarmTx 在指定事务中创建一条历史告警记录
|
||||
CreateHistoricalAlarmTx(ctx context.Context, tx *gorm.DB, alarm *models.HistoricalAlarm) error
|
||||
|
||||
// DeleteActiveAlarmTx 在指定事务中根据主键 ID 删除一个活跃告警
|
||||
DeleteActiveAlarmTx(ctx context.Context, tx *gorm.DB, id uint32) error
|
||||
|
||||
// UpdateIgnoreStatus 更新指定告警的忽略状态
|
||||
UpdateIgnoreStatus(ctx context.Context, id uint32, isIgnored bool, ignoredUntil *time.Time) error
|
||||
|
||||
// ListActiveAlarms 支持分页和过滤的活跃告警列表查询。
|
||||
// 返回活跃告警列表、总记录数和错误。
|
||||
ListActiveAlarms(ctx context.Context, opts ActiveAlarmListOptions, page, pageSize int) ([]models.ActiveAlarm, int64, error)
|
||||
|
||||
// ListHistoricalAlarms 支持分页和过滤的历史告警列表查询。
|
||||
// 返回历史告警列表、总记录数和错误。
|
||||
ListHistoricalAlarms(ctx context.Context, opts HistoricalAlarmListOptions, page, pageSize int) ([]models.HistoricalAlarm, int64, error)
|
||||
|
||||
// UpdateAlarmNotificationStatus 显式更新告警的通知相关状态字段。
|
||||
// lastNotifiedAt: 传入具体的发送时间。
|
||||
// isIgnored: 告警新的忽略状态。
|
||||
// ignoredUntil: 告警新的忽略截止时间 (nil 表示没有忽略截止时间/已取消忽略)。
|
||||
UpdateAlarmNotificationStatus(ctx context.Context, alarmID uint32, lastNotifiedAt time.Time, isIgnored bool, ignoredUntil *time.Time) error
|
||||
|
||||
// <-- 下列两个方法是为了性能做出的架构妥协, 业务逻辑入侵仓库层带来的收益远大于通过业务层进行数据筛选 -->
|
||||
|
||||
// ListAlarmsForNotification 查询满足发送告警消息条件的活跃告警列表。
|
||||
// 返回活跃告警列表和错误。
|
||||
// intervalByLevel: key=SeverityLevel, value=interval_in_minutes
|
||||
ListAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]uint32) ([]models.ActiveAlarm, error)
|
||||
// 查询满足发送告警消息条件的记录总数
|
||||
CountAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]uint32) (int64, error)
|
||||
}
|
||||
|
||||
// gormAlarmRepository 是 AlarmRepository 的 GORM 实现。
|
||||
type gormAlarmRepository struct {
|
||||
ctx context.Context
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormAlarmRepository 创建一个新的 AlarmRepository GORM 实现实例。
|
||||
func NewGormAlarmRepository(ctx context.Context, db *gorm.DB) AlarmRepository {
|
||||
return &gormAlarmRepository{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateActiveAlarm 创建一条新的活跃告警记录
|
||||
func (r *gormAlarmRepository) CreateActiveAlarm(ctx context.Context, alarm *models.ActiveAlarm) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateActiveAlarm")
|
||||
return r.db.WithContext(repoCtx).Create(alarm).Error
|
||||
}
|
||||
|
||||
// IsAlarmActiveInUse 检查具有相同来源和告警代码的告警当前是否处于活跃表中
|
||||
func (r *gormAlarmRepository) IsAlarmActiveInUse(ctx context.Context, sourceType models.AlarmSourceType, sourceID uint32, alarmCode models.AlarmCode) (bool, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsAlarmActiveInUse")
|
||||
var count int64
|
||||
err := r.db.WithContext(repoCtx).Model(&models.ActiveAlarm{}).
|
||||
Where("source_type = ? AND source_id = ? AND alarm_code = ?", sourceType, sourceID, alarmCode).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetActiveAlarmByUniqueFieldsTx 在指定事务中根据唯一业务键获取一个活跃告警
|
||||
func (r *gormAlarmRepository) GetActiveAlarmByUniqueFieldsTx(ctx context.Context, tx *gorm.DB, sourceType models.AlarmSourceType, sourceID uint32, alarmCode models.AlarmCode) (*models.ActiveAlarm, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetActiveAlarmByUniqueFieldsTx")
|
||||
var alarm models.ActiveAlarm
|
||||
err := tx.WithContext(repoCtx).
|
||||
Where("source_type = ? AND source_id = ? AND alarm_code = ?", sourceType, sourceID, alarmCode).
|
||||
First(&alarm).Error
|
||||
return &alarm, err
|
||||
}
|
||||
|
||||
// CreateHistoricalAlarmTx 在指定事务中创建一条历史告警记录
|
||||
func (r *gormAlarmRepository) CreateHistoricalAlarmTx(ctx context.Context, tx *gorm.DB, alarm *models.HistoricalAlarm) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateHistoricalAlarmTx")
|
||||
return tx.WithContext(repoCtx).Create(alarm).Error
|
||||
}
|
||||
|
||||
// DeleteActiveAlarmTx 在指定事务中根据主键 ID 删除一个活跃告警
|
||||
func (r *gormAlarmRepository) DeleteActiveAlarmTx(ctx context.Context, tx *gorm.DB, id uint32) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteActiveAlarmTx")
|
||||
// 使用 Unscoped() 确保执行物理删除,而不是软删除
|
||||
return tx.WithContext(repoCtx).Unscoped().Delete(&models.ActiveAlarm{}, id).Error
|
||||
}
|
||||
|
||||
// UpdateIgnoreStatus 更新指定告警的忽略状态
|
||||
func (r *gormAlarmRepository) UpdateIgnoreStatus(ctx context.Context, id uint32, isIgnored bool, ignoredUntil *time.Time) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateIgnoreStatus")
|
||||
updates := map[string]interface{}{
|
||||
"is_ignored": isIgnored,
|
||||
"ignored_until": ignoredUntil,
|
||||
}
|
||||
|
||||
result := r.db.WithContext(repoCtx).
|
||||
Model(&models.ActiveAlarm{}).
|
||||
Where("id = ?", id).
|
||||
Updates(updates)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListActiveAlarms 实现了分页和过滤查询活跃告警记录的功能
|
||||
func (r *gormAlarmRepository) ListActiveAlarms(ctx context.Context, opts ActiveAlarmListOptions, page, pageSize int) ([]models.ActiveAlarm, int64, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListActiveAlarms")
|
||||
// --- 校验分页参数 ---
|
||||
if page <= 0 || pageSize <= 0 {
|
||||
return nil, 0, ErrInvalidPagination
|
||||
}
|
||||
|
||||
var results []models.ActiveAlarm
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(repoCtx).Model(&models.ActiveAlarm{})
|
||||
|
||||
// --- 应用过滤条件 ---
|
||||
if opts.SourceType != nil {
|
||||
query = query.Where("source_type = ?", *opts.SourceType)
|
||||
}
|
||||
if opts.SourceID != nil {
|
||||
query = query.Where("source_id = ?", *opts.SourceID)
|
||||
}
|
||||
if opts.Level != nil {
|
||||
query = query.Where("level = ?", *opts.Level)
|
||||
}
|
||||
if opts.IsIgnored != nil {
|
||||
query = query.Where("is_ignored = ?", *opts.IsIgnored)
|
||||
}
|
||||
if opts.TriggerTime != nil {
|
||||
query = query.Where("trigger_time >= ?", *opts.TriggerTime)
|
||||
}
|
||||
if opts.EndTime != nil {
|
||||
query = query.Where("trigger_time <= ?", *opts.EndTime)
|
||||
}
|
||||
|
||||
// --- 计算总数 ---
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// --- 应用排序条件 ---
|
||||
orderBy := "trigger_time DESC" // 默认按触发时间倒序
|
||||
if opts.OrderBy != "" {
|
||||
orderBy = opts.OrderBy
|
||||
}
|
||||
query = query.Order(orderBy)
|
||||
|
||||
// --- 分页 ---
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
|
||||
|
||||
return results, total, err
|
||||
}
|
||||
|
||||
// ListHistoricalAlarms 实现了分页和过滤查询历史告警记录的功能
|
||||
func (r *gormAlarmRepository) ListHistoricalAlarms(ctx context.Context, opts HistoricalAlarmListOptions, page, pageSize int) ([]models.HistoricalAlarm, int64, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListHistoricalAlarms")
|
||||
// --- 校验分页参数 ---
|
||||
if page <= 0 || pageSize <= 0 {
|
||||
return nil, 0, ErrInvalidPagination
|
||||
}
|
||||
|
||||
var results []models.HistoricalAlarm
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(repoCtx).Model(&models.HistoricalAlarm{})
|
||||
|
||||
// --- 应用过滤条件 ---
|
||||
if opts.SourceType != nil {
|
||||
query = query.Where("source_type = ?", *opts.SourceType)
|
||||
}
|
||||
if opts.SourceID != nil {
|
||||
query = query.Where("source_id = ?", *opts.SourceID)
|
||||
}
|
||||
if opts.Level != nil {
|
||||
query = query.Where("level = ?", *opts.Level)
|
||||
}
|
||||
if opts.TriggerTimeStart != nil {
|
||||
query = query.Where("trigger_time >= ?", *opts.TriggerTimeStart)
|
||||
}
|
||||
if opts.TriggerTimeEnd != nil {
|
||||
query = query.Where("trigger_time <= ?", *opts.TriggerTimeEnd)
|
||||
}
|
||||
if opts.ResolveTimeStart != nil { // 修改字段名
|
||||
query = query.Where("resolve_time >= ?", *opts.ResolveTimeStart) // 修改查询字段名
|
||||
}
|
||||
if opts.ResolveTimeEnd != nil { // 修改字段名
|
||||
query = query.Where("resolve_time <= ?", *opts.ResolveTimeEnd) // 修改查询字段名
|
||||
}
|
||||
|
||||
// --- 计算总数 ---
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// --- 应用排序条件 ---
|
||||
orderBy := "trigger_time DESC" // 默认按触发时间倒序
|
||||
if opts.OrderBy != "" {
|
||||
orderBy = opts.OrderBy
|
||||
}
|
||||
query = query.Order(orderBy)
|
||||
|
||||
// --- 分页 ---
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
|
||||
|
||||
return results, total, err
|
||||
}
|
||||
|
||||
func (r *gormAlarmRepository) UpdateAlarmNotificationStatus(ctx context.Context, alarmID uint32, lastNotifiedAt time.Time, isIgnored bool, ignoredUntil *time.Time) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateAlarmNotificationStatus")
|
||||
|
||||
// 1. 内部安全地构造 map,将强类型参数转换为 GORM 需要的格式
|
||||
// GORM 的 Updates 方法会正确处理 *time.Time (nil -> DB NULL)
|
||||
updates := map[string]interface{}{
|
||||
"last_notified_at": lastNotifiedAt, // time.Time 会被 GORM 视为非空时间
|
||||
"is_ignored": isIgnored,
|
||||
"ignored_until": ignoredUntil, // *time.Time (nil) 会被 GORM 写入 NULL
|
||||
}
|
||||
|
||||
// 2. 执行更新
|
||||
result := r.db.WithContext(repoCtx).
|
||||
Model(&models.ActiveAlarm{}).
|
||||
Where("id = ?", alarmID).
|
||||
Updates(updates) // 仅更新 updates map 中指定的三个字段
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountAlarmsForNotification 查询满足发送告警消息条件的记录总数
|
||||
func (r *gormAlarmRepository) CountAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]uint32) (int64, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "CountAlarmsForNotification")
|
||||
var total int64
|
||||
|
||||
// 1. 构造基础查询对象 (包含 Context 和 Model)
|
||||
baseTx := r.db.WithContext(repoCtx).Model(&models.ActiveAlarm{})
|
||||
|
||||
// 2. 传递给辅助函数应用所有 WHERE 逻辑
|
||||
query := r.buildNotificationBaseQuery(baseTx, intervalByLevel)
|
||||
|
||||
// 3. 只执行 Count
|
||||
err := query.Count(&total).Error
|
||||
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// ListAlarmsForNotification 查询满足发送告警消息条件的活跃告警列表
|
||||
func (r *gormAlarmRepository) ListAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]uint32) ([]models.ActiveAlarm, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListAlarmsForNotification")
|
||||
var results []models.ActiveAlarm
|
||||
|
||||
// 1. 构造基础查询对象 (包含 Context 和 Model)
|
||||
baseTx := r.db.WithContext(repoCtx).Model(&models.ActiveAlarm{})
|
||||
|
||||
// 2. 传递给辅助函数应用所有 WHERE 逻辑
|
||||
query := r.buildNotificationBaseQuery(baseTx, intervalByLevel)
|
||||
|
||||
// 3. 执行 Find (不排序,高性能)
|
||||
err := query.Find(&results).Error
|
||||
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// buildNotificationBaseQuery 负责组合 Group A 和 Group B 的逻辑
|
||||
func (r *gormAlarmRepository) buildNotificationBaseQuery(tx *gorm.DB, intervalByLevel map[models.SeverityLevel]uint32) *gorm.DB {
|
||||
|
||||
// 1. 获取所有配置的 Level 列表
|
||||
configuredLevels := make([]models.SeverityLevel, 0, len(intervalByLevel))
|
||||
for level := range intervalByLevel {
|
||||
configuredLevels = append(configuredLevels, level)
|
||||
}
|
||||
|
||||
// 2. 构造 Group A (只发送一次)
|
||||
// Group A 是一个独立的 GORM SubQuery,用于构建 OR 关系
|
||||
groupAQuery := r.buildGroupAClause(tx.Session(&gorm.Session{}), configuredLevels)
|
||||
|
||||
// 3. 构造 Group B (间隔发送)
|
||||
// Group B 也是一个独立的 GORM SubQuery
|
||||
groupBQuery := r.buildGroupBClause(tx.Session(&gorm.Session{}), intervalByLevel, configuredLevels)
|
||||
|
||||
// 4. 最终组合:Group A OR Group B
|
||||
|
||||
// 核心逻辑:利用 GORM 的 Where(SubQuery) OR Where(SubQuery) 特性。
|
||||
// GORM 允许将 WHERE 或 OR 的参数写成 func(db *gorm.DB) *gorm.DB
|
||||
// 这样可以确保子查询的括号被正确处理,实现 (A) OR (B) 结构。
|
||||
|
||||
// 注意:我们必须检查配置,因为如果 Group B 配置为空,我们不应该将其添加到 OR 关系中。
|
||||
if len(configuredLevels) == 0 {
|
||||
// 只有 Group A 存在(即 Level NOT IN 的条件是 1=1)
|
||||
return tx.Where(groupAQuery)
|
||||
}
|
||||
|
||||
// 存在 Group A 和 Group B,用 OR 连接
|
||||
return tx.Where(groupAQuery).Or(groupBQuery)
|
||||
}
|
||||
|
||||
// buildGroupAClause 构造 Group A 的 WHERE 语句和参数列表。
|
||||
// 针对 Level 缺失配置(或所有 Level)的告警,使用“只发送一次”逻辑:LastNotifiedAt IS NULL
|
||||
// 参数 configuredLevels: 用于构建 Level NOT IN (?) 子句。
|
||||
func (r *gormAlarmRepository) buildGroupAClause(tx *gorm.DB, configuredLevels []models.SeverityLevel) *gorm.DB {
|
||||
now := time.Now()
|
||||
|
||||
// A.1. 构造 Level 范围检查子句 (Level NOT IN 或 1=1)
|
||||
if len(configuredLevels) > 0 {
|
||||
tx = tx.Where("level NOT IN (?)", configuredLevels)
|
||||
} else {
|
||||
// 如果配置列表为空,则所有 Level 都符合,使用 1=1
|
||||
tx = tx.Where("1 = 1")
|
||||
}
|
||||
|
||||
// A.2. 构造 Group A 核心逻辑 (LastNotifiedAt IS NULL 且满足忽略条件)
|
||||
|
||||
// C_A_Ignored: 被忽略但忽略期结束 且 仅发送一次
|
||||
ignoredQuery := tx.Where("is_ignored = ? AND ignored_until <= ? AND last_notified_at IS NULL", true, now)
|
||||
|
||||
// C_A_NotIgnored: 未被忽略 且 仅发送一次
|
||||
notIgnoredQuery := tx.Where("is_ignored = ? AND last_notified_at IS NULL", false)
|
||||
|
||||
// A.3. 组合 Group A 核心逻辑: (C_A_Ignored OR C_A_NotIgnored)
|
||||
return tx.Where(ignoredQuery).Or(notIgnoredQuery)
|
||||
}
|
||||
|
||||
// buildGroupBClause 构造 Group B 的 WHERE 语句和参数列表。
|
||||
// 针对 Level 存在配置的告警,使用“间隔发送”逻辑。
|
||||
func (r *gormAlarmRepository) buildGroupBClause(tx *gorm.DB, intervalByLevel map[models.SeverityLevel]uint32, configuredLevels []models.SeverityLevel) *gorm.DB {
|
||||
now := time.Now()
|
||||
|
||||
// B.1. 构造 Level IN 子句
|
||||
tx = tx.Where("level IN (?)", configuredLevels)
|
||||
|
||||
// B.2. 构造 Level-Based 间隔检查 (OR 部分)
|
||||
// 核心思想:利用 GORM 的 Or 链式调用构建 Level 间隔检查子句
|
||||
|
||||
// 初始化 Level 间隔检查查询 (ICC)
|
||||
iccQuery := tx.Session(&gorm.Session{}) // 创建一个干净的子查询对象来构建 ICC
|
||||
|
||||
// 动态添加 Level 间隔检查 OR 条件
|
||||
for level, minutes := range intervalByLevel {
|
||||
// PostgreSQL 语法: last_notified_at + (5 * interval '1 minute') <= ?
|
||||
sql := fmt.Sprintf("level = ? AND last_notified_at + (%d * interval '1 minute') <= ?", minutes)
|
||||
|
||||
// 每次使用 Or 叠加新的 Level 检查
|
||||
iccQuery = iccQuery.Or(sql, level, now)
|
||||
}
|
||||
|
||||
// B.3. 组合 Group B 核心逻辑: (last_notified_at IS NULL OR [ICC])
|
||||
|
||||
// C_B_NotIgnored: 未被忽略
|
||||
notIgnoredQuery := tx.Where("is_ignored = ?", false).Where(
|
||||
tx.Where("last_notified_at IS NULL").Or(iccQuery), // LastNotifiedAt IS NULL OR ICC
|
||||
)
|
||||
|
||||
// C_B_Ignored: 被忽略但忽略期结束
|
||||
ignoredQuery := tx.Where("is_ignored = ? AND ignored_until <= ?", true, now).Where(
|
||||
tx.Where("last_notified_at IS NULL").Or(iccQuery), // LastNotifiedAt IS NULL OR ICC
|
||||
)
|
||||
|
||||
// B.4. 组合 Group B 核心逻辑: (C_B_NotIgnored OR C_B_Ignored)
|
||||
return tx.Where(notIgnoredQuery).Or(ignoredQuery)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
@@ -12,12 +13,14 @@ import (
|
||||
|
||||
// AreaControllerRepository 定义了对 AreaController 模型的数据库操作接口
|
||||
type AreaControllerRepository interface {
|
||||
FindByID(ctx context.Context, id uint) (*models.AreaController, error)
|
||||
FindByID(ctx context.Context, id uint32) (*models.AreaController, error)
|
||||
FindByNetworkID(ctx context.Context, networkID string) (*models.AreaController, error)
|
||||
Create(ctx context.Context, ac *models.AreaController) error
|
||||
ListAll(ctx context.Context) ([]*models.AreaController, error)
|
||||
Update(ctx context.Context, ac *models.AreaController) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
Delete(ctx context.Context, id uint32) error
|
||||
// IsAreaControllerUsedByTasks 检查区域主控是否被特定任务类型使用,可以忽略指定任务类型
|
||||
IsAreaControllerUsedByTasks(ctx context.Context, areaControllerID uint32, ignoredTaskTypes []models.TaskType) (bool, error)
|
||||
}
|
||||
|
||||
// gormAreaControllerRepository 是 AreaControllerRepository 的 GORM 实现。
|
||||
@@ -57,7 +60,7 @@ func (r *gormAreaControllerRepository) Update(ctx context.Context, ac *models.Ar
|
||||
}
|
||||
|
||||
// Delete 删除一个 AreaController 记录。
|
||||
func (r *gormAreaControllerRepository) Delete(ctx context.Context, id uint) error {
|
||||
func (r *gormAreaControllerRepository) Delete(ctx context.Context, id uint32) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "Delete")
|
||||
if err := r.db.WithContext(repoCtx).Delete(&models.AreaController{}, id).Error; err != nil {
|
||||
return fmt.Errorf("删除区域主控失败: %w", err)
|
||||
@@ -66,7 +69,7 @@ func (r *gormAreaControllerRepository) Delete(ctx context.Context, id uint) erro
|
||||
}
|
||||
|
||||
// FindByID 通过 ID 查找一个 AreaController。
|
||||
func (r *gormAreaControllerRepository) FindByID(ctx context.Context, id uint) (*models.AreaController, error) {
|
||||
func (r *gormAreaControllerRepository) FindByID(ctx context.Context, id uint32) (*models.AreaController, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
|
||||
var areaController models.AreaController
|
||||
if err := r.db.WithContext(repoCtx).First(&areaController, id).Error; err != nil {
|
||||
@@ -84,3 +87,66 @@ func (r *gormAreaControllerRepository) FindByNetworkID(ctx context.Context, netw
|
||||
}
|
||||
return &areaController, nil
|
||||
}
|
||||
|
||||
// IsAreaControllerUsedByTasks 检查区域主控是否被特定任务类型使用,可以忽略指定任务类型
|
||||
func (r *gormAreaControllerRepository) IsAreaControllerUsedByTasks(ctx context.Context, areaControllerID uint32, ignoredTaskTypes []models.TaskType) (bool, error) {
|
||||
repoCtx, logger := logs.Trace(ctx, r.ctx, "IsAreaControllerUsedByTasks")
|
||||
|
||||
// 将 ignoredTaskTypes 转换为 map,以便高效查找
|
||||
ignoredMap := make(map[models.TaskType]struct{})
|
||||
for _, tt := range ignoredTaskTypes {
|
||||
ignoredMap[tt] = struct{}{}
|
||||
}
|
||||
|
||||
areaControllerIDStr := strconv.FormatUint(uint64(areaControllerID), 10)
|
||||
|
||||
// 定义所有可能与 AreaControllerID 相关的任务类型列表
|
||||
// 方便未来扩展,如果新增任务类型与区域主控关联,只需在此处添加
|
||||
relevantTaskTypes := []models.TaskType{
|
||||
models.TaskTypeAreaCollectorThresholdCheck,
|
||||
// TODO: 如果未来有其他任务类型通过 parameters 关联 AreaControllerID,请在此处添加
|
||||
// 例如: models.TaskTypeAnotherAreaControllerTask,
|
||||
}
|
||||
|
||||
for _, taskType := range relevantTaskTypes {
|
||||
// 如果当前任务类型在忽略列表中,则跳过检查
|
||||
if _, ok := ignoredMap[taskType]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var taskCount int64
|
||||
var query *gorm.DB
|
||||
|
||||
// 根据任务类型构建不同的查询条件
|
||||
switch taskType {
|
||||
case models.TaskTypeAreaCollectorThresholdCheck:
|
||||
// TaskTypeAreaCollectorThresholdCheck 任务的 AreaControllerID 存储在 parameters->>'AreaControllerID'
|
||||
query = r.db.WithContext(repoCtx).
|
||||
Model(&models.Task{}).
|
||||
Where("type = ?", models.TaskTypeAreaCollectorThresholdCheck).
|
||||
Where("parameters->>'AreaControllerID' = ?", areaControllerIDStr)
|
||||
// TODO: 如果未来有其他任务类型通过不同的 parameters 字段关联 AreaControllerID,请在此处添加 case
|
||||
// case models.TaskTypeAnotherAreaControllerTask:
|
||||
// query = r.db.WithContext(repoCtx).
|
||||
// Model(&models.Task{}).
|
||||
// Where("type = ?", models.TaskTypeAnotherAreaControllerTask).
|
||||
// Where("parameters->>'AnotherFieldForAreaControllerID' = ?", areaControllerIDStr)
|
||||
default:
|
||||
// 对于未明确处理的 relevantTaskTypes,可以记录警告或直接跳过
|
||||
logger.Warnf(fmt.Sprintf("IsAreaControllerUsedByTasks: 未处理的区域主控相关任务类型: %s", taskType))
|
||||
continue
|
||||
}
|
||||
|
||||
if query != nil {
|
||||
err := query.Count(&taskCount).Error
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("查询区域主控任务使用情况失败 (任务类型: %s): %w", taskType, err)
|
||||
}
|
||||
if taskCount > 0 {
|
||||
return true, nil // 发现有未被忽略的任务正在使用此区域主控
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil // 没有发现任何未被忽略的任务正在使用此区域主控
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// DeviceCommandLogListOptions 定义了查询设备命令日志时的可选参数
|
||||
type DeviceCommandLogListOptions struct {
|
||||
DeviceID *uint
|
||||
DeviceID *uint32
|
||||
ReceivedSuccess *bool
|
||||
StartTime *time.Time // 基于 sent_at 字段
|
||||
EndTime *time.Time // 基于 sent_at 字段
|
||||
|
||||
@@ -18,7 +18,7 @@ type DeviceRepository interface {
|
||||
Create(ctx context.Context, device *models.Device) error
|
||||
|
||||
// FindByID 根据主键 ID 查找设备
|
||||
FindByID(ctx context.Context, id uint) (*models.Device, error)
|
||||
FindByID(ctx context.Context, id uint32) (*models.Device, error)
|
||||
|
||||
// FindByIDString 根据字符串形式的主键 ID 查找设备
|
||||
FindByIDString(ctx context.Context, id string) (*models.Device, error)
|
||||
@@ -30,28 +30,28 @@ type DeviceRepository interface {
|
||||
ListAllSensors(ctx context.Context) ([]*models.Device, error)
|
||||
|
||||
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备
|
||||
ListByAreaControllerID(ctx context.Context, areaControllerID uint) ([]*models.Device, error)
|
||||
ListByAreaControllerID(ctx context.Context, areaControllerID uint32) ([]*models.Device, error)
|
||||
|
||||
// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备
|
||||
FindByDeviceTemplateID(ctx context.Context, deviceTemplateID uint) ([]*models.Device, error)
|
||||
FindByDeviceTemplateID(ctx context.Context, deviceTemplateID uint32) ([]*models.Device, error)
|
||||
|
||||
// Update 更新一个已有的设备信息
|
||||
Update(ctx context.Context, device *models.Device) error
|
||||
|
||||
// Delete 根据主键 ID 删除一个设备
|
||||
Delete(ctx context.Context, id uint) error
|
||||
Delete(ctx context.Context, id uint32) error
|
||||
|
||||
// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备
|
||||
FindByAreaControllerAndPhysicalAddress(ctx context.Context, areaControllerID uint, busNumber int, busAddress int) (*models.Device, error)
|
||||
FindByAreaControllerAndPhysicalAddress(ctx context.Context, areaControllerID uint32, busNumber int, busAddress int) (*models.Device, error)
|
||||
|
||||
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
|
||||
GetDevicesByIDsTx(ctx context.Context, tx *gorm.DB, ids []uint) ([]models.Device, error)
|
||||
GetDevicesByIDsTx(ctx context.Context, tx *gorm.DB, ids []uint32) ([]models.Device, error)
|
||||
|
||||
// IsDeviceInUse 检查设备是否被任何任务使用
|
||||
IsDeviceInUse(ctx context.Context, deviceID uint) (bool, error)
|
||||
// IsDeviceInUse 检查设备是否被任何任务使用,可以忽略指定任务类型
|
||||
IsDeviceInUse(ctx context.Context, deviceID uint32, ignoredTaskTypes []models.TaskType) (bool, error)
|
||||
|
||||
// IsAreaControllerInUse 检查区域主控是否被任何设备使用
|
||||
IsAreaControllerInUse(ctx context.Context, areaControllerID uint) (bool, error)
|
||||
IsAreaControllerInUse(ctx context.Context, areaControllerID uint32) (bool, error)
|
||||
}
|
||||
|
||||
// gormDeviceRepository 是 DeviceRepository 的 GORM 实现
|
||||
@@ -73,7 +73,7 @@ func (r *gormDeviceRepository) Create(ctx context.Context, device *models.Device
|
||||
}
|
||||
|
||||
// FindByID 根据 ID 查找设备
|
||||
func (r *gormDeviceRepository) FindByID(ctx context.Context, id uint) (*models.Device, error) {
|
||||
func (r *gormDeviceRepository) FindByID(ctx context.Context, id uint32) (*models.Device, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
|
||||
var device models.Device
|
||||
if err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").First(&device, id).Error; err != nil {
|
||||
@@ -83,7 +83,7 @@ func (r *gormDeviceRepository) FindByID(ctx context.Context, id uint) (*models.D
|
||||
}
|
||||
|
||||
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
|
||||
func (r *gormDeviceRepository) GetDevicesByIDsTx(ctx context.Context, tx *gorm.DB, ids []uint) ([]models.Device, error) {
|
||||
func (r *gormDeviceRepository) GetDevicesByIDsTx(ctx context.Context, tx *gorm.DB, ids []uint32) ([]models.Device, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetDevicesByIDsTx")
|
||||
var devices []models.Device
|
||||
if len(ids) == 0 {
|
||||
@@ -98,14 +98,13 @@ func (r *gormDeviceRepository) GetDevicesByIDsTx(ctx context.Context, tx *gorm.D
|
||||
// FindByIDString 根据字符串形式的主键 ID 查找设备
|
||||
func (r *gormDeviceRepository) FindByIDString(ctx context.Context, id string) (*models.Device, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByIDString")
|
||||
// 将字符串ID转换为uint64
|
||||
idInt, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
// 如果转换失败,说明ID格式不正确,返回一个明确的错误
|
||||
return nil, fmt.Errorf("无效的设备ID格式: %w", err)
|
||||
}
|
||||
// 调用已有的 FindByID 方法
|
||||
return r.FindByID(repoCtx, uint(idInt))
|
||||
return r.FindByID(repoCtx, uint32(idInt))
|
||||
}
|
||||
|
||||
// ListAll 获取所有设备的列表
|
||||
@@ -133,7 +132,7 @@ func (r *gormDeviceRepository) ListAllSensors(ctx context.Context) ([]*models.De
|
||||
}
|
||||
|
||||
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备
|
||||
func (r *gormDeviceRepository) ListByAreaControllerID(ctx context.Context, areaControllerID uint) ([]*models.Device, error) {
|
||||
func (r *gormDeviceRepository) ListByAreaControllerID(ctx context.Context, areaControllerID uint32) ([]*models.Device, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListByAreaControllerID")
|
||||
var devices []*models.Device
|
||||
err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").Where("area_controller_id = ?", areaControllerID).Find(&devices).Error
|
||||
@@ -144,7 +143,7 @@ func (r *gormDeviceRepository) ListByAreaControllerID(ctx context.Context, areaC
|
||||
}
|
||||
|
||||
// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备
|
||||
func (r *gormDeviceRepository) FindByDeviceTemplateID(ctx context.Context, deviceTemplateID uint) ([]*models.Device, error) {
|
||||
func (r *gormDeviceRepository) FindByDeviceTemplateID(ctx context.Context, deviceTemplateID uint32) ([]*models.Device, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByDeviceTemplateID")
|
||||
var devices []*models.Device
|
||||
err := r.db.WithContext(repoCtx).Where("device_template_id = ?", deviceTemplateID).Find(&devices).Error
|
||||
@@ -163,13 +162,13 @@ func (r *gormDeviceRepository) Update(ctx context.Context, device *models.Device
|
||||
|
||||
// Delete 根据 ID 删除一个设备
|
||||
// GORM 使用软删除,记录不会从数据库中物理移除,而是设置 DeletedAt 字段。
|
||||
func (r *gormDeviceRepository) Delete(ctx context.Context, id uint) error {
|
||||
func (r *gormDeviceRepository) Delete(ctx context.Context, id uint32) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "Delete")
|
||||
return r.db.WithContext(repoCtx).Delete(&models.Device{}, id).Error
|
||||
}
|
||||
|
||||
// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备
|
||||
func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(ctx context.Context, areaControllerID uint, busNumber int, busAddress int) (*models.Device, error) {
|
||||
func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(ctx context.Context, areaControllerID uint32, busNumber int, busAddress int) (*models.Device, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByAreaControllerAndPhysicalAddress")
|
||||
var device models.Device
|
||||
err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").
|
||||
@@ -184,12 +183,22 @@ func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(ctx contex
|
||||
return &device, nil
|
||||
}
|
||||
|
||||
// IsDeviceInUse 检查设备是否被任何任务使用
|
||||
func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint) (bool, error) {
|
||||
// IsDeviceInUse 检查设备是否被任何任务使用,可以忽略指定任务类型
|
||||
func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint32, ignoredTaskTypes []models.TaskType) (bool, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsDeviceInUse")
|
||||
var count int64
|
||||
// 直接对 device_tasks 关联表进行 COUNT 操作,性能最高
|
||||
err := r.db.WithContext(repoCtx).Model(&models.DeviceTask{}).Where("device_id = ?", deviceID).Count(&count).Error
|
||||
|
||||
// 构建查询,需要 JOIN tasks 表来过滤 TaskType
|
||||
query := r.db.WithContext(repoCtx).
|
||||
Model(&models.DeviceTask{}).
|
||||
Joins("JOIN tasks ON tasks.id = device_tasks.task_id").
|
||||
Where("device_tasks.device_id = ?", deviceID)
|
||||
|
||||
if len(ignoredTaskTypes) > 0 {
|
||||
query = query.Where("tasks.type NOT IN (?)", ignoredTaskTypes)
|
||||
}
|
||||
|
||||
err := query.Count(&count).Error
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("查询设备任务关联失败: %w", err)
|
||||
}
|
||||
@@ -197,7 +206,7 @@ func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint)
|
||||
}
|
||||
|
||||
// IsAreaControllerInUse 检查区域主控是否被任何设备使用
|
||||
func (r *gormDeviceRepository) IsAreaControllerInUse(ctx context.Context, areaControllerID uint) (bool, error) {
|
||||
func (r *gormDeviceRepository) IsAreaControllerInUse(ctx context.Context, areaControllerID uint32) (bool, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsAreaControllerInUse")
|
||||
var count int64
|
||||
if err := r.db.WithContext(repoCtx).Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil {
|
||||
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
// DeviceTemplateRepository 定义了设备模板数据访问的接口
|
||||
type DeviceTemplateRepository interface {
|
||||
Create(ctx context.Context, deviceTemplate *models.DeviceTemplate) error
|
||||
FindByID(ctx context.Context, id uint) (*models.DeviceTemplate, error)
|
||||
FindByID(ctx context.Context, id uint32) (*models.DeviceTemplate, error)
|
||||
FindByName(ctx context.Context, name string) (*models.DeviceTemplate, error)
|
||||
ListAll(ctx context.Context) ([]*models.DeviceTemplate, error)
|
||||
Update(ctx context.Context, deviceTemplate *models.DeviceTemplate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
IsInUse(ctx context.Context, id uint) (bool, error)
|
||||
Delete(ctx context.Context, id uint32) error
|
||||
IsInUse(ctx context.Context, id uint32) (bool, error)
|
||||
}
|
||||
|
||||
// gormDeviceTemplateRepository 是 DeviceTemplateRepository 的 GORM 实现
|
||||
@@ -40,7 +40,7 @@ func (r *gormDeviceTemplateRepository) Create(ctx context.Context, deviceTemplat
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找设备模板
|
||||
func (r *gormDeviceTemplateRepository) FindByID(ctx context.Context, id uint) (*models.DeviceTemplate, error) {
|
||||
func (r *gormDeviceTemplateRepository) FindByID(ctx context.Context, id uint32) (*models.DeviceTemplate, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
|
||||
var deviceTemplate models.DeviceTemplate
|
||||
if err := r.db.WithContext(repoCtx).First(&deviceTemplate, id).Error; err != nil {
|
||||
@@ -82,7 +82,7 @@ func (r *gormDeviceTemplateRepository) Update(ctx context.Context, deviceTemplat
|
||||
}
|
||||
|
||||
// IsInUse 检查设备模板是否正在被设备使用
|
||||
func (r *gormDeviceTemplateRepository) IsInUse(ctx context.Context, id uint) (bool, error) {
|
||||
func (r *gormDeviceTemplateRepository) IsInUse(ctx context.Context, id uint32) (bool, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsInUse")
|
||||
var count int64
|
||||
if err := r.db.WithContext(repoCtx).Model(&models.Device{}).Where("device_template_id = ?", id).Count(&count).Error; err != nil {
|
||||
@@ -92,7 +92,7 @@ func (r *gormDeviceTemplateRepository) IsInUse(ctx context.Context, id uint) (bo
|
||||
}
|
||||
|
||||
// Delete 软删除数据库中的设备模板
|
||||
func (r *gormDeviceTemplateRepository) Delete(ctx context.Context, id uint) error {
|
||||
func (r *gormDeviceTemplateRepository) Delete(ctx context.Context, id uint32) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "Delete")
|
||||
if err := r.db.WithContext(repoCtx).Delete(&models.DeviceTemplate{}, id).Error; err != nil {
|
||||
return fmt.Errorf("删除设备模板失败: %w", err)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// PlanExecutionLogListOptions 定义了查询计划执行日志时的可选参数
|
||||
type PlanExecutionLogListOptions struct {
|
||||
PlanID *uint
|
||||
PlanID *uint32
|
||||
Status *models.ExecutionStatus
|
||||
StartTime *time.Time // 基于 created_at 字段
|
||||
EndTime *time.Time // 基于 created_at 字段
|
||||
@@ -22,7 +22,7 @@ type PlanExecutionLogListOptions struct {
|
||||
|
||||
// TaskExecutionLogListOptions 定义了查询任务执行日志时的可选参数
|
||||
type TaskExecutionLogListOptions struct {
|
||||
PlanExecutionLogID *uint
|
||||
PlanExecutionLogID *uint32
|
||||
TaskID *int
|
||||
Status *models.ExecutionStatus
|
||||
StartTime *time.Time // 基于 created_at 字段
|
||||
@@ -33,26 +33,26 @@ type TaskExecutionLogListOptions struct {
|
||||
// ExecutionLogRepository 定义了与执行日志交互的接口。
|
||||
type ExecutionLogRepository interface {
|
||||
// --- Existing methods ---
|
||||
UpdateTaskExecutionLogStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error
|
||||
UpdateTaskExecutionLogStatus(ctx context.Context, logID uint, status models.ExecutionStatus) error
|
||||
UpdateTaskExecutionLogStatusByIDs(ctx context.Context, logIDs []uint32, status models.ExecutionStatus) error
|
||||
UpdateTaskExecutionLogStatus(ctx context.Context, logID uint32, status models.ExecutionStatus) error
|
||||
CreateTaskExecutionLog(ctx context.Context, log *models.TaskExecutionLog) error
|
||||
CreatePlanExecutionLog(ctx context.Context, log *models.PlanExecutionLog) error
|
||||
UpdatePlanExecutionLog(ctx context.Context, log *models.PlanExecutionLog) error
|
||||
CreateTaskExecutionLogsInBatch(ctx context.Context, logs []*models.TaskExecutionLog) error
|
||||
UpdateTaskExecutionLog(ctx context.Context, log *models.TaskExecutionLog) error
|
||||
FindTaskExecutionLogByID(ctx context.Context, id uint) (*models.TaskExecutionLog, error)
|
||||
FindTaskExecutionLogByID(ctx context.Context, id uint32) (*models.TaskExecutionLog, error)
|
||||
// UpdatePlanExecutionLogStatus 更新计划执行日志的状态
|
||||
UpdatePlanExecutionLogStatus(ctx context.Context, logID uint, status models.ExecutionStatus) error
|
||||
UpdatePlanExecutionLogStatus(ctx context.Context, logID uint32, status models.ExecutionStatus) error
|
||||
|
||||
// UpdatePlanExecutionLogsStatusByIDs 批量更新计划执行日志的状态
|
||||
UpdatePlanExecutionLogsStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error
|
||||
UpdatePlanExecutionLogsStatusByIDs(ctx context.Context, logIDs []uint32, status models.ExecutionStatus) error
|
||||
|
||||
// FindIncompletePlanExecutionLogs 查找所有未完成的计划执行日志
|
||||
FindIncompletePlanExecutionLogs(ctx context.Context) ([]models.PlanExecutionLog, error)
|
||||
// FindInProgressPlanExecutionLogByPlanID 根据 PlanID 查找正在进行的计划执行日志
|
||||
FindInProgressPlanExecutionLogByPlanID(ctx context.Context, planID uint) (*models.PlanExecutionLog, error)
|
||||
FindInProgressPlanExecutionLogByPlanID(ctx context.Context, planID uint32) (*models.PlanExecutionLog, error)
|
||||
// FindIncompleteTaskExecutionLogsByPlanLogID 根据计划日志ID查找所有未完成的任务日志
|
||||
FindIncompleteTaskExecutionLogsByPlanLogID(ctx context.Context, planLogID uint) ([]models.TaskExecutionLog, error)
|
||||
FindIncompleteTaskExecutionLogsByPlanLogID(ctx context.Context, planLogID uint32) ([]models.TaskExecutionLog, error)
|
||||
|
||||
// FailAllIncompletePlanExecutionLogs 将所有状态为 ExecutionStatusStarted 和 ExecutionStatusWaiting 的计划状态都修改为 ExecutionStatusFailed
|
||||
FailAllIncompletePlanExecutionLogs(ctx context.Context) error
|
||||
@@ -60,16 +60,16 @@ type ExecutionLogRepository interface {
|
||||
CancelAllIncompleteTaskExecutionLogs(ctx context.Context) error
|
||||
|
||||
// FindPlanExecutionLogByID 根据ID查找计划执行日志
|
||||
FindPlanExecutionLogByID(ctx context.Context, id uint) (*models.PlanExecutionLog, error)
|
||||
FindPlanExecutionLogByID(ctx context.Context, id uint32) (*models.PlanExecutionLog, error)
|
||||
|
||||
// CountIncompleteTasksByPlanLogID 计算一个计划执行中未完成的任务数量
|
||||
CountIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint) (int64, error)
|
||||
CountIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint32) (int64, error)
|
||||
|
||||
// FailPlanExecution 将指定的计划执行标记为失败
|
||||
FailPlanExecution(ctx context.Context, planLogID uint, errorMessage string) error
|
||||
FailPlanExecution(ctx context.Context, planLogID uint32, errorMessage string) error
|
||||
|
||||
// CancelIncompleteTasksByPlanLogID 取消一个计划执行中的所有未完成任务
|
||||
CancelIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint, reason string) error
|
||||
CancelIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint32, reason string) error
|
||||
|
||||
// --- New methods ---
|
||||
ListPlanExecutionLogs(ctx context.Context, opts PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, int64, error)
|
||||
@@ -175,7 +175,7 @@ func (r *gormExecutionLogRepository) ListTaskExecutionLogs(ctx context.Context,
|
||||
|
||||
// --- Existing method implementations ---
|
||||
|
||||
func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error {
|
||||
func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatusByIDs(ctx context.Context, logIDs []uint32, status models.ExecutionStatus) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateTaskExecutionLogStatusByIDs")
|
||||
if len(logIDs) == 0 {
|
||||
return nil
|
||||
@@ -183,7 +183,7 @@ func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatusByIDs(ctx conte
|
||||
return r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).Where("id IN ?", logIDs).Update("status", status).Error
|
||||
}
|
||||
|
||||
func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatus(ctx context.Context, logID uint, status models.ExecutionStatus) error {
|
||||
func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatus(ctx context.Context, logID uint32, status models.ExecutionStatus) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateTaskExecutionLogStatus")
|
||||
return r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).Where("id = ?", logID).Update("status", status).Error
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func (r *gormExecutionLogRepository) UpdateTaskExecutionLog(ctx context.Context,
|
||||
|
||||
// FindTaskExecutionLogByID 根据 ID 查找单个任务执行日志。
|
||||
// 它会预加载关联的 Task 信息。
|
||||
func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(ctx context.Context, id uint) (*models.TaskExecutionLog, error) {
|
||||
func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(ctx context.Context, id uint32) (*models.TaskExecutionLog, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindTaskExecutionLogByID")
|
||||
var log models.TaskExecutionLog
|
||||
// 使用 Preload("Task") 来确保关联的任务信息被一并加载
|
||||
@@ -240,13 +240,13 @@ func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(ctx context.Contex
|
||||
}
|
||||
|
||||
// UpdatePlanExecutionLogStatus 更新计划执行日志的状态
|
||||
func (r *gormExecutionLogRepository) UpdatePlanExecutionLogStatus(ctx context.Context, logID uint, status models.ExecutionStatus) error {
|
||||
func (r *gormExecutionLogRepository) UpdatePlanExecutionLogStatus(ctx context.Context, logID uint32, status models.ExecutionStatus) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanExecutionLogStatus")
|
||||
return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}).Where("id = ?", logID).Update("status", status).Error
|
||||
}
|
||||
|
||||
// UpdatePlanExecutionLogsStatusByIDs 批量更新计划执行日志的状态
|
||||
func (r *gormExecutionLogRepository) UpdatePlanExecutionLogsStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error {
|
||||
func (r *gormExecutionLogRepository) UpdatePlanExecutionLogsStatusByIDs(ctx context.Context, logIDs []uint32, status models.ExecutionStatus) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanExecutionLogsStatusByIDs")
|
||||
if len(logIDs) == 0 {
|
||||
return nil
|
||||
@@ -263,7 +263,7 @@ func (r *gormExecutionLogRepository) FindIncompletePlanExecutionLogs(ctx context
|
||||
}
|
||||
|
||||
// FindInProgressPlanExecutionLogByPlanID 根据 PlanID 查找正在进行的计划执行日志
|
||||
func (r *gormExecutionLogRepository) FindInProgressPlanExecutionLogByPlanID(ctx context.Context, planID uint) (*models.PlanExecutionLog, error) {
|
||||
func (r *gormExecutionLogRepository) FindInProgressPlanExecutionLogByPlanID(ctx context.Context, planID uint32) (*models.PlanExecutionLog, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindInProgressPlanExecutionLogByPlanID")
|
||||
var log models.PlanExecutionLog
|
||||
err := r.db.WithContext(repoCtx).Where("plan_id = ? AND status = ?", planID, models.ExecutionStatusStarted).First(&log).Error
|
||||
@@ -279,7 +279,7 @@ func (r *gormExecutionLogRepository) FindInProgressPlanExecutionLogByPlanID(ctx
|
||||
}
|
||||
|
||||
// FindIncompleteTaskExecutionLogsByPlanLogID 根据计划日志ID查找所有未完成的任务日志
|
||||
func (r *gormExecutionLogRepository) FindIncompleteTaskExecutionLogsByPlanLogID(ctx context.Context, planLogID uint) ([]models.TaskExecutionLog, error) {
|
||||
func (r *gormExecutionLogRepository) FindIncompleteTaskExecutionLogsByPlanLogID(ctx context.Context, planLogID uint32) ([]models.TaskExecutionLog, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindIncompleteTaskExecutionLogsByPlanLogID")
|
||||
var logs []models.TaskExecutionLog
|
||||
err := r.db.WithContext(repoCtx).Where("plan_execution_log_id = ? AND (status = ? OR status = ?)",
|
||||
@@ -304,7 +304,7 @@ func (r *gormExecutionLogRepository) CancelAllIncompleteTaskExecutionLogs(ctx co
|
||||
}
|
||||
|
||||
// FindPlanExecutionLogByID 根据ID查找计划执行日志
|
||||
func (r *gormExecutionLogRepository) FindPlanExecutionLogByID(ctx context.Context, id uint) (*models.PlanExecutionLog, error) {
|
||||
func (r *gormExecutionLogRepository) FindPlanExecutionLogByID(ctx context.Context, id uint32) (*models.PlanExecutionLog, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPlanExecutionLogByID")
|
||||
var log models.PlanExecutionLog
|
||||
err := r.db.WithContext(repoCtx).First(&log, id).Error
|
||||
@@ -315,7 +315,7 @@ func (r *gormExecutionLogRepository) FindPlanExecutionLogByID(ctx context.Contex
|
||||
}
|
||||
|
||||
// CountIncompleteTasksByPlanLogID 计算一个计划执行中未完成的任务数量
|
||||
func (r *gormExecutionLogRepository) CountIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint) (int64, error) {
|
||||
func (r *gormExecutionLogRepository) CountIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint32) (int64, error) {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "CountIncompleteTasksByPlanLogID")
|
||||
var count int64
|
||||
err := r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).
|
||||
@@ -326,7 +326,7 @@ func (r *gormExecutionLogRepository) CountIncompleteTasksByPlanLogID(ctx context
|
||||
}
|
||||
|
||||
// FailPlanExecution 将指定的计划执行标记为失败
|
||||
func (r *gormExecutionLogRepository) FailPlanExecution(ctx context.Context, planLogID uint, errorMessage string) error {
|
||||
func (r *gormExecutionLogRepository) FailPlanExecution(ctx context.Context, planLogID uint32, errorMessage string) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "FailPlanExecution")
|
||||
return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}).
|
||||
Where("id = ?", planLogID).
|
||||
@@ -338,7 +338,7 @@ func (r *gormExecutionLogRepository) FailPlanExecution(ctx context.Context, plan
|
||||
}
|
||||
|
||||
// CancelIncompleteTasksByPlanLogID 取消一个计划执行中的所有未完成任务
|
||||
func (r *gormExecutionLogRepository) CancelIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint, reason string) error {
|
||||
func (r *gormExecutionLogRepository) CancelIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint32, reason string) error {
|
||||
repoCtx := logs.AddFuncName(ctx, r.ctx, "CancelIncompleteTasksByPlanLogID")
|
||||
return r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).
|
||||
Where("plan_execution_log_id = ? AND status IN (?, ?)",
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
|
||||
// MedicationLogListOptions 定义了查询用药记录时的可选参数
|
||||
type MedicationLogListOptions struct {
|
||||
PigBatchID *uint
|
||||
MedicationID *uint
|
||||
PigBatchID *uint32
|
||||
MedicationID *uint32
|
||||
Reason *models.MedicationReasonType
|
||||
OperatorID *uint
|
||||
OperatorID *uint32
|
||||
StartTime *time.Time
|
||||
EndTime *time.Time
|
||||
OrderBy string // 例如 "happened_at desc"
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"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/notify"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gorm.io/gorm"
|
||||
@@ -14,8 +13,8 @@ import (
|
||||
|
||||
// NotificationListOptions 定义了查询通知列表时的可选参数
|
||||
type NotificationListOptions struct {
|
||||
UserID *uint // 按用户ID过滤
|
||||
NotifierType *notify.NotifierType // 按通知器类型过滤
|
||||
UserID *uint32 // 按用户ID过滤
|
||||
NotifierType *models.NotifierType // 按通知器类型过滤
|
||||
Status *models.NotificationStatus // 按通知状态过滤 (例如:"success", "failed")
|
||||
Level *zapcore.Level // 按通知等级过滤 (例如:"info", "warning", "error")
|
||||
StartTime *time.Time // 通知内容生成时间范围 - 开始时间 (对应 AlarmTimestamp)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// PendingCollectionListOptions 定义了查询待采集请求时的可选参数
|
||||
type PendingCollectionListOptions struct {
|
||||
DeviceID *uint
|
||||
DeviceID *uint32
|
||||
Status *models.PendingCollectionStatus
|
||||
StartTime *time.Time // 基于 created_at 字段
|
||||
EndTime *time.Time // 基于 created_at 字段
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user