Merge pull request 'issue_62' (#65) from issue_62 into main

Reviewed-on: #65
This commit is contained in:
2025-11-10 22:48:42 +08:00
117 changed files with 7172 additions and 1147 deletions

View File

@@ -113,3 +113,14 @@ notify:
# 定时采集配置 # 定时采集配置
collection: collection:
interval: 1 # 采集间隔 (分钟) interval: 1 # 采集间隔 (分钟)
# 告警通知配置
alarm_notification:
notification_intervals: # 告警通知间隔(分钟)
debug: 1
info: 1
warn: 1
error: 1
dpanic: 1
panic: 1
fatal: 1

View File

@@ -90,4 +90,15 @@ lora_mesh:
# 定时采集配置 # 定时采集配置
collection: collection:
interval: 1 # 采集间隔 (分钟) interval: 1 # 采集间隔 (分钟)
# 告警通知配置
alarm_notification:
notification_intervals: # 告警通知间隔 (分钟)
debug: 1
info: 1
warn: 1
error: 1
dpanic: 1
panic: 1
fatal: 1

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -53,6 +53,35 @@ definitions:
- CodeConflict - CodeConflict
- CodeInternalError - CodeInternalError
- CodeServiceUnavailable - 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: dto.AreaControllerResponse:
properties: properties:
created_at: created_at:
@@ -73,6 +102,21 @@ definitions:
updated_at: updated_at:
type: string type: string
type: object 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: dto.AssignEmptyPensToBatchRequest:
properties: properties:
pen_ids: pen_ids:
@@ -137,6 +181,32 @@ definitions:
- name - name
- network_id - network_id
type: object 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: dto.CreateDeviceRequest:
properties: properties:
area_controller_id: area_controller_id:
@@ -177,6 +247,32 @@ definitions:
- commands - commands
- name - name
type: object 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: dto.CreatePenRequest:
properties: properties:
capacity: capacity:
@@ -251,6 +347,15 @@ definitions:
example: newuser example: newuser
type: string type: string
type: object type: object
dto.DeleteDeviceThresholdAlarmDTO:
properties:
sensor_type:
allOf:
- $ref: '#/definitions/models.SensorType'
description: 传感器类型
required:
- sensor_type
type: object
dto.DeviceCommandLogDTO: dto.DeviceCommandLogDTO:
properties: properties:
acknowledged_at: acknowledged_at:
@@ -312,6 +417,21 @@ definitions:
$ref: '#/definitions/models.ValueDescriptor' $ref: '#/definitions/models.ValueDescriptor'
type: array type: array
type: object 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: dto.FeedFormulaDTO:
properties: properties:
id: id:
@@ -340,6 +460,40 @@ definitions:
remarks: remarks:
type: string type: string
type: object 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: dto.ListDeviceCommandLogResponse:
properties: properties:
list: list:
@@ -358,6 +512,15 @@ definitions:
pagination: pagination:
$ref: '#/definitions/dto.PaginationDTO' $ref: '#/definitions/dto.PaginationDTO'
type: object type: object
dto.ListHistoricalAlarmResponse:
properties:
list:
items:
$ref: '#/definitions/dto.HistoricalAlarmDTO'
type: array
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListMedicationLogResponse: dto.ListMedicationLogResponse:
properties: properties:
list: list:
@@ -600,11 +763,11 @@ definitions:
id: id:
type: integer type: integer
level: level:
$ref: '#/definitions/zapcore.Level' $ref: '#/definitions/models.SeverityLevel'
message: message:
type: string type: string
notifier_type: notifier_type:
$ref: '#/definitions/notify.NotifierType' $ref: '#/definitions/models.NotifierType'
status: status:
$ref: '#/definitions/models.NotificationStatus' $ref: '#/definitions/models.NotificationStatus'
title: title:
@@ -1199,26 +1362,35 @@ definitions:
properties: properties:
type: type:
allOf: allOf:
- $ref: '#/definitions/notify.NotifierType' - $ref: '#/definitions/models.NotifierType'
description: Type 指定要测试的通知渠道 description: Type 指定要测试的通知渠道
required: required:
- type - type
type: object type: object
dto.SensorDataDTO: dto.SensorDataDTO:
properties: properties:
area_controller_id:
type: integer
data: data:
items: items:
type: integer type: integer
type: array type: array
device_id: device_id:
type: integer type: integer
regional_controller_id:
type: integer
sensor_type: sensor_type:
$ref: '#/definitions/models.SensorType' $ref: '#/definitions/models.SensorType'
time: time:
type: string type: string
type: object type: object
dto.SnoozeAlarmRequest:
properties:
duration_minutes:
description: 忽略时长,单位分钟
minimum: 1
type: integer
required:
- duration_minutes
type: object
dto.SubPlanResponse: dto.SubPlanResponse:
properties: properties:
child_plan: child_plan:
@@ -1373,6 +1545,23 @@ definitions:
- name - name
- network_id - network_id
type: object 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: dto.UpdateDeviceRequest:
properties: properties:
area_controller_id: area_controller_id:
@@ -1413,6 +1602,23 @@ definitions:
- commands - commands
- name - name
type: object 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: dto.UpdatePenRequest:
properties: properties:
capacity: capacity:
@@ -1558,6 +1764,34 @@ definitions:
weight: weight:
type: number type: number
type: object 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: models.AuditStatus:
enum: enum:
- 成功 - 成功
@@ -1646,6 +1880,34 @@ definitions:
- NotificationStatusSuccess - NotificationStatusSuccess
- NotificationStatusFailed - NotificationStatusFailed
- NotificationStatusSkipped - 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: models.PenStatus:
enum: enum:
- 空闲 - 空闲
@@ -1877,6 +2139,24 @@ definitions:
- SensorTypeTemperature - SensorTypeTemperature
- SensorTypeHumidity - SensorTypeHumidity
- SensorTypeWeight - 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: models.StockLogSourceType:
enum: enum:
- 采购入库 - 采购入库
@@ -1899,9 +2179,15 @@ definitions:
- 等待 - 等待
- 下料 - 下料
- 全量采集 - 全量采集
- 告警通知
- 设备阈值检查
- 区域阈值检查
type: string type: string
x-enum-comments: x-enum-comments:
TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务 TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务
TaskTypeAlarmNotification: 告警通知任务
TaskTypeAreaCollectorThresholdCheck: 区域阈值检查任务
TaskTypeDeviceThresholdCheck: 设备阈值检查任务
TaskTypeFullCollection: 新增的全量采集任务 TaskTypeFullCollection: 新增的全量采集任务
TaskTypeReleaseFeedWeight: 下料口释放指定重量任务 TaskTypeReleaseFeedWeight: 下料口释放指定重量任务
TaskTypeWaiting: 等待任务 TaskTypeWaiting: 等待任务
@@ -1910,11 +2196,17 @@ definitions:
- 等待任务 - 等待任务
- 下料口释放指定重量任务 - 下料口释放指定重量任务
- 新增的全量采集任务 - 新增的全量采集任务
- 告警通知任务
- 设备阈值检查任务
- 区域阈值检查任务
x-enum-varnames: x-enum-varnames:
- TaskPlanAnalysis - TaskPlanAnalysis
- TaskTypeWaiting - TaskTypeWaiting
- TaskTypeReleaseFeedWeight - TaskTypeReleaseFeedWeight
- TaskTypeFullCollection - TaskTypeFullCollection
- TaskTypeAlarmNotification
- TaskTypeDeviceThresholdCheck
- TaskTypeAreaCollectorThresholdCheck
models.ValueDescriptor: models.ValueDescriptor:
properties: properties:
multiplier: multiplier:
@@ -1926,18 +2218,6 @@ definitions:
type: type:
$ref: '#/definitions/models.SensorType' $ref: '#/definitions/models.SensorType'
type: object type: object
notify.NotifierType:
enum:
- 邮件
- 企业微信
- 飞书
- 日志
type: string
x-enum-varnames:
- NotifierTypeSMTP
- NotifierTypeWeChat
- NotifierTypeLark
- NotifierTypeLog
repository.PlanTypeFilter: repository.PlanTypeFilter:
enum: enum:
- 所有任务 - 所有任务
@@ -1950,7 +2230,6 @@ definitions:
- PlanTypeFilterSystem - PlanTypeFilterSystem
zapcore.Level: zapcore.Level:
enum: enum:
- 7
- -1 - -1
- 0 - 0
- 1 - 1
@@ -1961,10 +2240,10 @@ definitions:
- -1 - -1
- 5 - 5
- 6 - 6
- 7
format: int32 format: int32
type: integer type: integer
x-enum-varnames: x-enum-varnames:
- _numLevels
- DebugLevel - DebugLevel
- InfoLevel - InfoLevel
- WarnLevel - WarnLevel
@@ -1975,6 +2254,7 @@ definitions:
- _minLevel - _minLevel
- _maxLevel - _maxLevel
- InvalidLevel - InvalidLevel
- _numLevels
info: info:
contact: contact:
email: divano@example.com email: divano@example.com
@@ -1987,6 +2267,428 @@ info:
title: 猪场管理系统 API title: 猪场管理系统 API
version: "1.0" version: "1.0"
paths: 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: /api/v1/area-controllers:
get: get:
description: 获取系统中所有区域主控的列表 description: 获取系统中所有区域主控的列表
@@ -2546,7 +3248,6 @@ paths:
name: end_time name: end_time
type: string type: string
- enum: - enum:
- 7
- -1 - -1
- 0 - 0
- 1 - 1
@@ -2557,12 +3258,12 @@ paths:
- -1 - -1
- 5 - 5
- 6 - 6
- 7
format: int32 format: int32
in: query in: query
name: level name: level
type: integer type: integer
x-enum-varnames: x-enum-varnames:
- _numLevels
- DebugLevel - DebugLevel
- InfoLevel - InfoLevel
- WarnLevel - WarnLevel
@@ -2573,6 +3274,7 @@ paths:
- _minLevel - _minLevel
- _maxLevel - _maxLevel
- InvalidLevel - InvalidLevel
- _numLevels
- enum: - enum:
- 邮件 - 邮件
- 企业微信 - 企业微信

View File

@@ -19,6 +19,7 @@ import (
"time" "time"
_ "git.huangwc.com/pig/pig-farm-controller/docs" // 引入 swag 生成的 docs _ "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/device"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/health" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/health"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/management" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/management"
@@ -53,6 +54,7 @@ type API struct {
pigBatchController *management.PigBatchController // 猪群控制器实例 pigBatchController *management.PigBatchController // 猪群控制器实例
monitorController *monitor.Controller // 数据监控控制器实例 monitorController *monitor.Controller // 数据监控控制器实例
healthController *health.Controller // 健康检查控制器实例 healthController *health.Controller // 健康检查控制器实例
alarmController *alarm.ThresholdAlarmController // 阈值告警控制器
listenHandler webhook.ListenHandler // 设备上行事件监听器 listenHandler webhook.ListenHandler // 设备上行事件监听器
analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例 analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例
} }
@@ -69,6 +71,7 @@ func NewAPI(cfg config.ServerConfig,
planService service.PlanService, planService service.PlanService,
userService service.UserService, userService service.UserService,
auditService service.AuditService, auditService service.AuditService,
alarmService service.ThresholdAlarmService,
tokenGenerator token.Generator, tokenGenerator token.Generator,
listenHandler webhook.ListenHandler, listenHandler webhook.ListenHandler,
) *API { ) *API {
@@ -106,6 +109,8 @@ func NewAPI(cfg config.ServerConfig,
monitorController: monitor.NewController(logs.AddCompName(baseCtx, "MonitorController"), monitorService), monitorController: monitor.NewController(logs.AddCompName(baseCtx, "MonitorController"), monitorService),
// 在 NewAPI 中初始化健康检查控制器 // 在 NewAPI 中初始化健康检查控制器
healthController: health.NewController(logs.AddCompName(baseCtx, "HealthController")), healthController: health.NewController(logs.AddCompName(baseCtx, "HealthController")),
// 在 NewAPI 中初始化阈
alarmController: alarm.NewThresholdAlarmController(logs.AddCompName(baseCtx, "ThresholdAlarmController"), alarmService),
} }
api.setupRoutes() // 设置所有路由 api.setupRoutes() // 设置所有路由

View File

@@ -187,6 +187,32 @@ func (a *API) setupRoutes() {
monitorGroup.GET("/notifications", a.monitorController.ListNotifications) monitorGroup.GET("/notifications", a.monitorController.ListNotifications)
} }
logger.Debug("数据监控相关接口注册成功 (需要认证和审计)") 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("所有接口注册成功") logger.Debug("所有接口注册成功")

View 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)
}

View File

@@ -17,7 +17,7 @@ var (
// GetOperatorIDFromContext 从 echo.Context 中提取操作者ID。 // GetOperatorIDFromContext 从 echo.Context 中提取操作者ID。
// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 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()) userVal := c.Get(models.ContextUserKey.String())
if userVal == nil { if userVal == nil {
return 0, ErrUserNotFoundInContext return 0, ErrUserNotFoundInContext

View File

@@ -84,7 +84,7 @@ func (c *Controller) GetDevice(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID) 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) 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) 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) 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) 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 { switch {
case errors.Is(err, gorm.ErrRecordNotFound): case errors.Is(err, gorm.ErrRecordNotFound):
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) 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) 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) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", 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) 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID) 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) 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID) 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) 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 { switch {
case errors.Is(err, gorm.ErrRecordNotFound): case errors.Is(err, gorm.ErrRecordNotFound):
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID) 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) 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) 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) 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) 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) 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 { switch {
case errors.Is(err, gorm.ErrRecordNotFound): case errors.Is(err, gorm.ErrRecordNotFound):
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)

View File

@@ -15,7 +15,7 @@ import (
// mapAndSendError 统一映射服务层错误并发送响应。 // mapAndSendError 统一映射服务层错误并发送响应。
// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。 // 这个函数将服务层返回的错误转换为控制器层应返回的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) || if errors.Is(err, service.ErrPigBatchNotFound) ||
errors.Is(err, service.ErrPenNotFound) || errors.Is(err, service.ErrPenNotFound) ||
errors.Is(err, service.ErrPenNotAssociatedWithBatch) { errors.Is(err, service.ErrPenNotAssociatedWithBatch) {
@@ -34,7 +34,7 @@ func mapAndSendError(reqContext context.Context, c *PigBatchController, ctx echo
} }
// idExtractorFunc 定义了一个函数类型用于从echo.Context中提取主ID。 // 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的通用逻辑。 // extractOperatorAndPrimaryID 封装了从echo.Context中提取操作员ID和主ID的通用逻辑。
// 它负责处理ID提取过程中的错误并发送相应的HTTP响应。 // 它负责处理ID提取过程中的错误并发送相应的HTTP响应。
@@ -48,15 +48,15 @@ type idExtractorFunc func(ctx echo.Context) (uint, error)
// //
// 返回值: // 返回值:
// //
// operatorID: uint - 提取到的操作员ID。 // operatorID: uint32 - 提取到的操作员ID。
// primaryID: uint - 提取到的主ID。 // primaryID: uint32 - 提取到的主ID。
// err: error - 如果ID提取失败或发送错误响应则返回错误。 // err: error - 如果ID提取失败或发送错误响应则返回错误。
func extractOperatorAndPrimaryID( func extractOperatorAndPrimaryID(
c *PigBatchController, c *PigBatchController,
ctx echo.Context, ctx echo.Context,
action string, action string,
idExtractor idExtractorFunc, idExtractor idExtractorFunc,
) (operatorID uint, primaryID uint, err error) { ) (operatorID uint32, primaryID uint32, err error) {
// 1. 获取操作员ID // 1. 获取操作员ID
operatorID, err = controller.GetOperatorIDFromContext(ctx) operatorID, err = controller.GetOperatorIDFromContext(ctx)
if err != nil { if err != nil {
@@ -78,7 +78,7 @@ func extractOperatorAndPrimaryID(
if err != nil { if err != nil {
return 0, 0, controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) 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, ctx echo.Context,
action string, action string,
reqDTO Req, 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, successMsg string,
idExtractor idExtractorFunc, idExtractor idExtractorFunc,
) error { ) error {
@@ -124,7 +124,7 @@ func handleNoBodyAPIRequest(
c *PigBatchController, c *PigBatchController,
ctx echo.Context, ctx echo.Context,
action string, action string,
serviceExecutor func(ctx echo.Context, operatorID uint, primaryID uint) error, serviceExecutor func(ctx echo.Context, operatorID uint32, primaryID uint32) error,
successMsg string, successMsg string,
idExtractor idExtractorFunc, idExtractor idExtractorFunc,
) error { ) error {
@@ -151,7 +151,7 @@ func handleAPIRequestWithResponse[Req any, Resp any](
ctx echo.Context, ctx echo.Context,
action string, action string,
reqDTO Req, 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, successMsg string,
idExtractor idExtractorFunc, idExtractor idExtractorFunc,
) error { ) error {
@@ -182,7 +182,7 @@ func handleNoBodyAPIRequestWithResponse[Resp any](
c *PigBatchController, c *PigBatchController,
ctx echo.Context, ctx echo.Context,
action string, 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, successMsg string,
idExtractor idExtractorFunc, idExtractor idExtractorFunc,
) error { ) error {
@@ -209,7 +209,7 @@ func handleQueryAPIRequestWithResponse[Query any, Resp any](
ctx echo.Context, ctx echo.Context,
action string, action string,
queryDTO Query, 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, successMsg string,
) error { ) error {
// 1. 绑定查询参数 // 1. 绑定查询参数

View File

@@ -43,7 +43,7 @@ func (c *PigBatchController) CreatePigBatch(ctx echo.Context) error {
return handleAPIRequestWithResponse( return handleAPIRequestWithResponse(
reqCtx, c, ctx, action, &req, 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通常不从路径中获取而是由服务层生成 // 对于创建操作primaryID通常不从路径中获取而是由服务层生成
return c.service.CreatePigBatch(reqCtx, operatorID, req) return c.service.CreatePigBatch(reqCtx, operatorID, req)
}, },
@@ -68,7 +68,7 @@ func (c *PigBatchController) GetPigBatch(ctx echo.Context) error {
return handleNoBodyAPIRequestWithResponse( return handleNoBodyAPIRequestWithResponse(
reqCtx, c, ctx, action, 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) return c.service.GetPigBatch(reqCtx, primaryID)
}, },
"获取成功", "获取成功",
@@ -95,7 +95,7 @@ func (c *PigBatchController) UpdatePigBatch(ctx echo.Context) error {
return handleAPIRequestWithResponse( return handleAPIRequestWithResponse(
reqCtx, c, ctx, action, &req, 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) return c.service.UpdatePigBatch(reqCtx, primaryID, req)
}, },
"更新成功", "更新成功",
@@ -119,7 +119,7 @@ func (c *PigBatchController) DeletePigBatch(ctx echo.Context) error {
return handleNoBodyAPIRequest( return handleNoBodyAPIRequest(
reqCtx, c, ctx, action, 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) return c.service.DeletePigBatch(reqCtx, primaryID)
}, },
"删除成功", "删除成功",
@@ -144,7 +144,7 @@ func (c *PigBatchController) ListPigBatches(ctx echo.Context) error {
return handleQueryAPIRequestWithResponse( return handleQueryAPIRequestWithResponse(
reqCtx, c, ctx, action, &query, 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) return c.service.ListPigBatches(reqCtx, query.IsActive)
}, },
"获取成功", "获取成功",
@@ -170,7 +170,7 @@ func (c *PigBatchController) AssignEmptyPensToBatch(ctx echo.Context) error {
return handleAPIRequest( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) return c.service.AssignEmptyPensToBatch(reqCtx, primaryID, req.PenIDs, operatorID)
}, },
"分配成功", "分配成功",
@@ -197,18 +197,18 @@ func (c *PigBatchController) ReclassifyPenToNewBatch(ctx echo.Context) error {
return handleAPIRequest( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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 // primaryID 在这里是 fromBatchID
return c.service.ReclassifyPenToNewBatch(reqCtx, primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks) 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") idParam := ctx.Param("fromBatchID")
parsedID, err := strconv.ParseUint(idParam, 10, 32) parsedID, err := strconv.ParseUint(idParam, 10, 32)
if err != nil { if err != nil {
return 0, err 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( return handleNoBodyAPIRequest(
reqCtx, c, ctx, action, reqCtx, c, ctx, action,
func(ctx echo.Context, operatorID uint, primaryID uint) error { func(ctx echo.Context, operatorID uint32, primaryID uint32) error {
// primaryID 在这里是 batchID // primaryID 在这里是 batchID
penIDParam := ctx.Param("penID") penIDParam := ctx.Param("penID")
parsedPenID, err := strconv.ParseUint(penIDParam, 10, 32) parsedPenID, err := strconv.ParseUint(penIDParam, 10, 32)
if err != nil { if err != nil {
return err // 返回错误,因为 penID 格式无效 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") idParam := ctx.Param("batchID")
parsedID, err := strconv.ParseUint(idParam, 10, 32) parsedID, err := strconv.ParseUint(idParam, 10, 32)
if err != nil { if err != nil {
return 0, err 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( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) return c.service.MovePigsIntoPen(reqCtx, primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
}, },
"移入成功", "移入成功",

View File

@@ -26,7 +26,7 @@ func (c *PigBatchController) RecordSickPigs(ctx echo.Context) error {
return handleAPIRequest( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) 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( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) 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( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) 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( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) 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( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) 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( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) return c.service.RecordCull(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
}, },
"记录成功", "记录成功",

View File

@@ -25,7 +25,7 @@ func (c *PigBatchController) SellPigs(ctx echo.Context) error {
return handleAPIRequest( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) 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( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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) return c.service.BuyPigs(reqCtx, primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
}, },
"买猪成功", "买猪成功",

View File

@@ -28,18 +28,18 @@ func (c *PigBatchController) TransferPigsAcrossBatches(ctx echo.Context) error {
return handleAPIRequest( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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 // primaryID 在这里是 sourceBatchID
return c.service.TransferPigsAcrossBatches(reqCtx, primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks) 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") idParam := ctx.Param("sourceBatchID")
parsedID, err := strconv.ParseUint(idParam, 10, 32) parsedID, err := strconv.ParseUint(idParam, 10, 32)
if err != nil { if err != nil {
return 0, err 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( return handleAPIRequest(
reqCtx, c, ctx, action, &req, 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 // primaryID 在这里是 batchID
return c.service.TransferPigsWithinBatch(reqCtx, primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks) return c.service.TransferPigsWithinBatch(reqCtx, primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
}, },

View File

@@ -76,7 +76,7 @@ func (c *PigFarmController) GetPigHouse(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) 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 err != nil {
if errors.Is(err, service.ErrHouseNotFound) { if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) 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) 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 err != nil {
if errors.Is(err, service.ErrHouseNotFound) { if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) 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")) 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) { if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) 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")) 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 err != nil {
if errors.Is(err, service.ErrPenNotFound) { if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) 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) 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 err != nil {
if errors.Is(err, service.ErrPenNotFound) { if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) 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")) 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) { if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) 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) 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 err != nil {
if errors.Is(err, service.ErrPenNotFound) { if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id) return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)

View File

@@ -81,14 +81,14 @@ func (c *Controller) GetPlan(ctx echo.Context) error {
const actionType = "获取计划详情" const actionType = "获取计划详情"
// 1. 从 URL 路径中获取 ID // 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id") idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil { if err != nil {
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr) logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", 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 { if err != nil {
logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id) logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
@@ -147,7 +147,7 @@ func (c *Controller) UpdatePlan(ctx echo.Context) error {
const actionType = "更新计划" const actionType = "更新计划"
// 1. 从 URL 路径中获取 ID // 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id") idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil { if err != nil {
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr) logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", 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 { if err != nil {
logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id) logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
@@ -191,14 +191,14 @@ func (c *Controller) DeletePlan(ctx echo.Context) error {
const actionType = "删除计划" const actionType = "删除计划"
// 1. 从 URL 路径中获取 ID // 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id") idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil { if err != nil {
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr) logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", 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 { if err != nil {
logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id) logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
@@ -228,14 +228,14 @@ func (c *Controller) StartPlan(ctx echo.Context) error {
const actionType = "启动计划" const actionType = "启动计划"
// 1. 从 URL 路径中获取 ID // 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id") idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil { if err != nil {
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr) logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", 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 { if err != nil {
logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id) logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
@@ -267,14 +267,14 @@ func (c *Controller) StopPlan(ctx echo.Context) error {
const actionType = "停止计划" const actionType = "停止计划"
// 1. 从 URL 路径中获取 ID // 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id") idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil { if err != nil {
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr) logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", 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 { if err != nil {
logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id) logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound

View File

@@ -101,7 +101,7 @@ func (c *Controller) SendTestNotification(ctx echo.Context) error {
const actionType = "发送测试通知" const actionType = "发送测试通知"
// 1. 从 URL 中获取用户 ID // 1. 从 URL 中获取用户 ID
userID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) userID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err) logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", ctx.Param("id")) 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. 调用服务层 // 3. 调用服务层
err = c.userService.SendTestNotification(reqCtx, uint(userID), &req) err = c.userService.SendTestNotification(reqCtx, uint32(userID), &req)
if err != nil { if err != nil {
logger.Errorf("%s: 服务层调用失败: %v", actionType, err) logger.Errorf("%s: 服务层调用失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", map[string]interface{}{"userID": userID, "type": req.Type}) return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", map[string]interface{}{"userID": userID, "type": req.Type})

View 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,
},
}
}

View 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"`
}

View File

@@ -5,8 +5,8 @@ import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
// CreateDeviceRequest 定义了创建设备时需要传入的参数 // CreateDeviceRequest 定义了创建设备时需要传入的参数
type CreateDeviceRequest struct { type CreateDeviceRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
DeviceTemplateID uint `json:"device_template_id" validate:"required"` DeviceTemplateID uint32 `json:"device_template_id" validate:"required"`
AreaControllerID uint `json:"area_controller_id" validate:"required"` AreaControllerID uint32 `json:"area_controller_id" validate:"required"`
Location string `json:"location,omitempty" validate:"omitempty"` Location string `json:"location,omitempty" validate:"omitempty"`
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"` Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
} }
@@ -14,8 +14,8 @@ type CreateDeviceRequest struct {
// UpdateDeviceRequest 定义了更新设备时需要传入的参数 // UpdateDeviceRequest 定义了更新设备时需要传入的参数
type UpdateDeviceRequest struct { type UpdateDeviceRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
DeviceTemplateID uint `json:"device_template_id" validate:"required"` DeviceTemplateID uint32 `json:"device_template_id" validate:"required"`
AreaControllerID uint `json:"area_controller_id" validate:"required"` AreaControllerID uint32 `json:"area_controller_id" validate:"required"`
Location string `json:"location,omitempty" validate:"omitempty"` Location string `json:"location,omitempty" validate:"omitempty"`
Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"` Properties map[string]interface{} `json:"properties,omitempty" validate:"omitempty"`
} }
@@ -64,11 +64,11 @@ type UpdateDeviceTemplateRequest struct {
// DeviceResponse 定义了返回给客户端的单个设备信息的结构 // DeviceResponse 定义了返回给客户端的单个设备信息的结构
type DeviceResponse struct { type DeviceResponse struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
DeviceTemplateID uint `json:"device_template_id"` DeviceTemplateID uint32 `json:"device_template_id"`
DeviceTemplateName string `json:"device_template_name"` DeviceTemplateName string `json:"device_template_name"`
AreaControllerID uint `json:"area_controller_id"` AreaControllerID uint32 `json:"area_controller_id"`
AreaControllerName string `json:"area_controller_name"` AreaControllerName string `json:"area_controller_name"`
Location string `json:"location"` Location string `json:"location"`
Properties map[string]interface{} `json:"properties"` Properties map[string]interface{} `json:"properties"`
@@ -78,7 +78,7 @@ type DeviceResponse struct {
// AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构 // AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构
type AreaControllerResponse struct { type AreaControllerResponse struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
NetworkID string `json:"network_id"` NetworkID string `json:"network_id"`
Location string `json:"location"` Location string `json:"location"`
@@ -90,7 +90,7 @@ type AreaControllerResponse struct {
// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构 // DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构
type DeviceTemplateResponse struct { type DeviceTemplateResponse struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`
Description string `json:"description"` Description string `json:"description"`

10
internal/app/dto/dto.go Normal file
View 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"`
}

View File

@@ -11,11 +11,11 @@ func NewListSensorDataResponse(data []models.SensorData, total int64, page, page
dtos := make([]SensorDataDTO, len(data)) dtos := make([]SensorDataDTO, len(data))
for i, item := range data { for i, item := range data {
dtos[i] = SensorDataDTO{ dtos[i] = SensorDataDTO{
Time: item.Time, Time: item.Time,
DeviceID: item.DeviceID, DeviceID: item.DeviceID,
RegionalControllerID: item.RegionalControllerID, AreaControllerID: item.AreaControllerID,
SensorType: item.SensorType, SensorType: item.SensorType,
Data: json.RawMessage(item.Data), Data: json.RawMessage(item.Data),
} }
} }
@@ -54,9 +54,9 @@ func NewListDeviceCommandLogResponse(data []models.DeviceCommandLog, total int64
// NewListPlanExecutionLogResponse 从模型数据创建列表响应 DTO // NewListPlanExecutionLogResponse 从模型数据创建列表响应 DTO
func NewListPlanExecutionLogResponse(planLogs []models.PlanExecutionLog, plans []models.Plan, total int64, page, pageSize int) *ListPlanExecutionLogResponse { 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 { for _, plan := range plans {
planId2Name[plan.ID] = plan.Name planId2Name[plan.ID] = string(plan.Name)
} }
dtos := make([]PlanExecutionLogDTO, len(planLogs)) dtos := make([]PlanExecutionLogDTO, len(planLogs))
@@ -95,7 +95,7 @@ func NewListTaskExecutionLogResponse(data []models.TaskExecutionLog, total int64
PlanExecutionLogID: item.PlanExecutionLogID, PlanExecutionLogID: item.PlanExecutionLogID,
TaskID: item.TaskID, TaskID: item.TaskID,
Task: TaskDTO{ Task: TaskDTO{
ID: uint(item.Task.ID), ID: uint32(item.Task.ID),
Name: item.Task.Name, Name: item.Task.Name,
Description: item.Task.Description, 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 { func NewListPigTransferLogResponse(data []models.PigTransferLog, total int64, page, pageSize int) *ListPigTransferLogResponse {
dtos := make([]PigTransferLogDTO, len(data)) dtos := make([]PigTransferLogDTO, len(data))
for i, item := range data { for i, item := range data {
// 注意PigTransferLog 的 ID, CreatedAt, UpdatedAt 字段是 gorm.Model 嵌入的 // 注意PigTransferLog 的 ID, CreatedAt, UpdatedAt 字段是 Model 嵌入的
dtos[i] = PigTransferLogDTO{ dtos[i] = PigTransferLogDTO{
ID: item.ID, ID: item.ID,
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,

View File

@@ -7,22 +7,13 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "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 --- // --- SensorData ---
// ListSensorDataRequest 定义了获取传感器数据列表的请求参数 // ListSensorDataRequest 定义了获取传感器数据列表的请求参数
type ListSensorDataRequest struct { type ListSensorDataRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` SensorType *string `json:"sensor_type" query:"sensor_type"`
StartTime *time.Time `json:"start_time" query:"start_time"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
@@ -31,11 +22,11 @@ type ListSensorDataRequest struct {
// SensorDataDTO 是用于API响应的传感器数据结构 // SensorDataDTO 是用于API响应的传感器数据结构
type SensorDataDTO struct { type SensorDataDTO struct {
Time time.Time `json:"time"` Time time.Time `json:"time"`
DeviceID uint `json:"device_id"` DeviceID uint32 `json:"device_id"`
RegionalControllerID uint `json:"regional_controller_id"` AreaControllerID uint32 `json:"area_controller_id"`
SensorType models.SensorType `json:"sensor_type"` SensorType models.SensorType `json:"sensor_type"`
Data json.RawMessage `json:"data"` Data json.RawMessage `json:"data"`
} }
// ListSensorDataResponse 是获取传感器数据列表的响应结构 // ListSensorDataResponse 是获取传感器数据列表的响应结构
@@ -50,7 +41,7 @@ type ListSensorDataResponse struct {
type ListDeviceCommandLogRequest struct { type ListDeviceCommandLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` ReceivedSuccess *bool `json:"received_success" query:"received_success"`
StartTime *time.Time `json:"start_time" query:"start_time"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
@@ -60,7 +51,7 @@ type ListDeviceCommandLogRequest struct {
// DeviceCommandLogDTO 是用于API响应的设备命令日志结构 // DeviceCommandLogDTO 是用于API响应的设备命令日志结构
type DeviceCommandLogDTO struct { type DeviceCommandLogDTO struct {
MessageID string `json:"message_id"` MessageID string `json:"message_id"`
DeviceID uint `json:"device_id"` DeviceID uint32 `json:"device_id"`
SentAt time.Time `json:"sent_at"` SentAt time.Time `json:"sent_at"`
AcknowledgedAt *time.Time `json:"acknowledged_at"` AcknowledgedAt *time.Time `json:"acknowledged_at"`
ReceivedSuccess bool `json:"received_success"` ReceivedSuccess bool `json:"received_success"`
@@ -78,7 +69,7 @@ type ListDeviceCommandLogResponse struct {
type ListPlanExecutionLogRequest struct { type ListPlanExecutionLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` Status *string `json:"status" query:"status"`
StartTime *time.Time `json:"start_time" query:"start_time"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
@@ -87,10 +78,10 @@ type ListPlanExecutionLogRequest struct {
// PlanExecutionLogDTO 是用于API响应的计划执行日志结构 // PlanExecutionLogDTO 是用于API响应的计划执行日志结构
type PlanExecutionLogDTO struct { type PlanExecutionLogDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
PlanID uint `json:"plan_id"` PlanID uint32 `json:"plan_id"`
PlanName string `json:"plan_name"` PlanName string `json:"plan_name"`
Status models.ExecutionStatus `json:"status"` Status models.ExecutionStatus `json:"status"`
StartedAt time.Time `json:"started_at"` StartedAt time.Time `json:"started_at"`
@@ -110,7 +101,7 @@ type ListPlanExecutionLogResponse struct {
type ListTaskExecutionLogRequest struct { type ListTaskExecutionLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` TaskID *int `json:"task_id" query:"task_id"`
Status *string `json:"status" query:"status"` Status *string `json:"status" query:"status"`
StartTime *time.Time `json:"start_time" query:"start_time"` StartTime *time.Time `json:"start_time" query:"start_time"`
@@ -120,17 +111,17 @@ type ListTaskExecutionLogRequest struct {
// TaskDTO 是用于API响应的简化版任务结构 // TaskDTO 是用于API响应的简化版任务结构
type TaskDTO struct { type TaskDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
} }
// TaskExecutionLogDTO 是用于API响应的任务执行日志结构 // TaskExecutionLogDTO 是用于API响应的任务执行日志结构
type TaskExecutionLogDTO struct { type TaskExecutionLogDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_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"` TaskID int `json:"task_id"`
Task TaskDTO `json:"task"` // 嵌套的任务信息 Task TaskDTO `json:"task"` // 嵌套的任务信息
Status models.ExecutionStatus `json:"status"` Status models.ExecutionStatus `json:"status"`
@@ -151,7 +142,7 @@ type ListTaskExecutionLogResponse struct {
type ListPendingCollectionRequest struct { type ListPendingCollectionRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` Status *string `json:"status" query:"status"`
StartTime *time.Time `json:"start_time" query:"start_time"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
@@ -161,7 +152,7 @@ type ListPendingCollectionRequest struct {
// PendingCollectionDTO 是用于API响应的待采集请求结构 // PendingCollectionDTO 是用于API响应的待采集请求结构
type PendingCollectionDTO struct { type PendingCollectionDTO struct {
CorrelationID string `json:"correlation_id"` CorrelationID string `json:"correlation_id"`
DeviceID uint `json:"device_id"` DeviceID uint32 `json:"device_id"`
CommandMetadata models.UintArray `json:"command_metadata"` CommandMetadata models.UintArray `json:"command_metadata"`
Status models.PendingCollectionStatus `json:"status"` Status models.PendingCollectionStatus `json:"status"`
FulfilledAt *time.Time `json:"fulfilled_at"` FulfilledAt *time.Time `json:"fulfilled_at"`
@@ -180,7 +171,7 @@ type ListPendingCollectionResponse struct {
type ListUserActionLogRequest struct { type ListUserActionLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` Username *string `json:"username" query:"username"`
ActionType *string `json:"action_type" query:"action_type"` ActionType *string `json:"action_type" query:"action_type"`
Status *string `json:"status" query:"status"` Status *string `json:"status" query:"status"`
@@ -191,9 +182,9 @@ type ListUserActionLogRequest struct {
// UserActionLogDTO 是用于API响应的用户操作日志结构 // UserActionLogDTO 是用于API响应的用户操作日志结构
type UserActionLogDTO struct { type UserActionLogDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Time time.Time `json:"time"` Time time.Time `json:"time"`
UserID uint `json:"user_id"` UserID uint32 `json:"user_id"`
Username string `json:"username"` Username string `json:"username"`
SourceIP string `json:"source_ip"` SourceIP string `json:"source_ip"`
ActionType string `json:"action_type"` ActionType string `json:"action_type"`
@@ -217,7 +208,7 @@ type ListUserActionLogResponse struct {
type ListRawMaterialPurchaseRequest struct { type ListRawMaterialPurchaseRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` Supplier *string `json:"supplier" query:"supplier"`
StartTime *time.Time `json:"start_time" query:"start_time"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
@@ -226,19 +217,19 @@ type ListRawMaterialPurchaseRequest struct {
// RawMaterialDTO 是用于API响应的简化版原料结构 // RawMaterialDTO 是用于API响应的简化版原料结构
type RawMaterialDTO struct { type RawMaterialDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} }
// RawMaterialPurchaseDTO 是用于API响应的原料采购结构 // RawMaterialPurchaseDTO 是用于API响应的原料采购结构
type RawMaterialPurchaseDTO struct { type RawMaterialPurchaseDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
RawMaterialID uint `json:"raw_material_id"` RawMaterialID uint32 `json:"raw_material_id"`
RawMaterial RawMaterialDTO `json:"raw_material"` RawMaterial RawMaterialDTO `json:"raw_material"`
Supplier string `json:"supplier"` Supplier string `json:"supplier"`
Amount float64 `json:"amount"` Amount float32 `json:"amount"`
UnitPrice float64 `json:"unit_price"` UnitPrice float32 `json:"unit_price"`
TotalPrice float64 `json:"total_price"` TotalPrice float32 `json:"total_price"`
PurchaseDate time.Time `json:"purchase_date"` PurchaseDate time.Time `json:"purchase_date"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
@@ -255,9 +246,9 @@ type ListRawMaterialPurchaseResponse struct {
type ListRawMaterialStockLogRequest struct { type ListRawMaterialStockLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` 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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -265,11 +256,11 @@ type ListRawMaterialStockLogRequest struct {
// RawMaterialStockLogDTO 是用于API响应的原料库存日志结构 // RawMaterialStockLogDTO 是用于API响应的原料库存日志结构
type RawMaterialStockLogDTO struct { type RawMaterialStockLogDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
RawMaterialID uint `json:"raw_material_id"` RawMaterialID uint32 `json:"raw_material_id"`
ChangeAmount float64 `json:"change_amount"` ChangeAmount float32 `json:"change_amount"`
SourceType models.StockLogSourceType `json:"source_type"` SourceType models.StockLogSourceType `json:"source_type"`
SourceID uint `json:"source_id"` SourceID uint32 `json:"source_id"`
HappenedAt time.Time `json:"happened_at"` HappenedAt time.Time `json:"happened_at"`
Remarks string `json:"remarks"` Remarks string `json:"remarks"`
} }
@@ -286,9 +277,9 @@ type ListRawMaterialStockLogResponse struct {
type ListFeedUsageRecordRequest struct { type ListFeedUsageRecordRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` PageSize int `json:"page_size" query:"page_size"`
PenID *uint `json:"pen_id" query:"pen_id"` PenID *uint32 `json:"pen_id" query:"pen_id"`
FeedFormulaID *uint `json:"feed_formula_id" query:"feed_formula_id"` FeedFormulaID *uint32 `json:"feed_formula_id" query:"feed_formula_id"`
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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -296,26 +287,26 @@ type ListFeedUsageRecordRequest struct {
// PenDTO 是用于API响应的简化版猪栏结构 // PenDTO 是用于API响应的简化版猪栏结构
type PenDTO struct { type PenDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} }
// FeedFormulaDTO 是用于API响应的简化版饲料配方结构 // FeedFormulaDTO 是用于API响应的简化版饲料配方结构
type FeedFormulaDTO struct { type FeedFormulaDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} }
// FeedUsageRecordDTO 是用于API响应的饲料使用记录结构 // FeedUsageRecordDTO 是用于API响应的饲料使用记录结构
type FeedUsageRecordDTO struct { type FeedUsageRecordDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
PenID uint `json:"pen_id"` PenID uint32 `json:"pen_id"`
Pen PenDTO `json:"pen"` Pen PenDTO `json:"pen"`
FeedFormulaID uint `json:"feed_formula_id"` FeedFormulaID uint32 `json:"feed_formula_id"`
FeedFormula FeedFormulaDTO `json:"feed_formula"` FeedFormula FeedFormulaDTO `json:"feed_formula"`
Amount float64 `json:"amount"` Amount float32 `json:"amount"`
RecordedAt time.Time `json:"recorded_at"` RecordedAt time.Time `json:"recorded_at"`
OperatorID uint `json:"operator_id"` OperatorID uint32 `json:"operator_id"`
Remarks string `json:"remarks"` Remarks string `json:"remarks"`
} }
@@ -331,10 +322,10 @@ type ListFeedUsageRecordResponse struct {
type ListMedicationLogRequest struct { type ListMedicationLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"`
MedicationID *uint `json:"medication_id" query:"medication_id"` MedicationID *uint32 `json:"medication_id" query:"medication_id"`
Reason *string `json:"reason" query:"reason"` 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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -342,21 +333,21 @@ type ListMedicationLogRequest struct {
// MedicationDTO 是用于API响应的简化版药品结构 // MedicationDTO 是用于API响应的简化版药品结构
type MedicationDTO struct { type MedicationDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} }
// MedicationLogDTO 是用于API响应的用药记录结构 // MedicationLogDTO 是用于API响应的用药记录结构
type MedicationLogDTO struct { type MedicationLogDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
PigBatchID uint `json:"pig_batch_id"` PigBatchID uint32 `json:"pig_batch_id"`
MedicationID uint `json:"medication_id"` MedicationID uint32 `json:"medication_id"`
Medication MedicationDTO `json:"medication"` Medication MedicationDTO `json:"medication"`
DosageUsed float64 `json:"dosage_used"` DosageUsed float32 `json:"dosage_used"`
TargetCount int `json:"target_count"` TargetCount int `json:"target_count"`
Reason models.MedicationReasonType `json:"reason"` Reason models.MedicationReasonType `json:"reason"`
Description string `json:"description"` Description string `json:"description"`
OperatorID uint `json:"operator_id"` OperatorID uint32 `json:"operator_id"`
HappenedAt time.Time `json:"happened_at"` HappenedAt time.Time `json:"happened_at"`
} }
@@ -372,9 +363,9 @@ type ListMedicationLogResponse struct {
type ListPigBatchLogRequest struct { type ListPigBatchLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` 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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -382,16 +373,16 @@ type ListPigBatchLogRequest struct {
// PigBatchLogDTO 是用于API响应的猪批次日志结构 // PigBatchLogDTO 是用于API响应的猪批次日志结构
type PigBatchLogDTO struct { type PigBatchLogDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_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"` ChangeType models.LogChangeType `json:"change_type"`
ChangeCount int `json:"change_count"` ChangeCount int `json:"change_count"`
Reason string `json:"reason"` Reason string `json:"reason"`
BeforeCount int `json:"before_count"` BeforeCount int `json:"before_count"`
AfterCount int `json:"after_count"` AfterCount int `json:"after_count"`
OperatorID uint `json:"operator_id"` OperatorID uint32 `json:"operator_id"`
HappenedAt time.Time `json:"happened_at"` HappenedAt time.Time `json:"happened_at"`
} }
@@ -407,7 +398,7 @@ type ListPigBatchLogResponse struct {
type ListWeighingBatchRequest struct { type ListWeighingBatchRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -415,12 +406,12 @@ type ListWeighingBatchRequest struct {
// WeighingBatchDTO 是用于API响应的批次称重记录结构 // WeighingBatchDTO 是用于API响应的批次称重记录结构
type WeighingBatchDTO struct { type WeighingBatchDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
WeighingTime time.Time `json:"weighing_time"` WeighingTime time.Time `json:"weighing_time"`
Description string `json:"description"` Description string `json:"description"`
PigBatchID uint `json:"pig_batch_id"` PigBatchID uint32 `json:"pig_batch_id"`
} }
// ListWeighingBatchResponse 是获取批次称重记录列表的响应结构 // ListWeighingBatchResponse 是获取批次称重记录列表的响应结构
@@ -435,9 +426,9 @@ type ListWeighingBatchResponse struct {
type ListWeighingRecordRequest struct { type ListWeighingRecordRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` PageSize int `json:"page_size" query:"page_size"`
WeighingBatchID *uint `json:"weighing_batch_id" query:"weighing_batch_id"` WeighingBatchID *uint32 `json:"weighing_batch_id" query:"weighing_batch_id"`
PenID *uint `json:"pen_id" query:"pen_id"` PenID *uint32 `json:"pen_id" query:"pen_id"`
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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -445,13 +436,13 @@ type ListWeighingRecordRequest struct {
// WeighingRecordDTO 是用于API响应的单次称重记录结构 // WeighingRecordDTO 是用于API响应的单次称重记录结构
type WeighingRecordDTO struct { type WeighingRecordDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Weight float64 `json:"weight"` Weight float32 `json:"weight"`
WeighingBatchID uint `json:"weighing_batch_id"` WeighingBatchID uint32 `json:"weighing_batch_id"`
PenID uint `json:"pen_id"` PenID uint32 `json:"pen_id"`
OperatorID uint `json:"operator_id"` OperatorID uint32 `json:"operator_id"`
Remark string `json:"remark"` Remark string `json:"remark"`
WeighingTime time.Time `json:"weighing_time"` WeighingTime time.Time `json:"weighing_time"`
} }
@@ -468,10 +459,10 @@ type ListWeighingRecordResponse struct {
type ListPigTransferLogRequest struct { type ListPigTransferLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"`
PenID *uint `json:"pen_id" query:"pen_id"` PenID *uint32 `json:"pen_id" query:"pen_id"`
TransferType *string `json:"transfer_type" query:"transfer_type"` 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"` CorrelationID *string `json:"correlation_id" query:"correlation_id"`
StartTime *time.Time `json:"start_time" query:"start_time"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
@@ -480,16 +471,16 @@ type ListPigTransferLogRequest struct {
// PigTransferLogDTO 是用于API响应的猪只迁移日志结构 // PigTransferLogDTO 是用于API响应的猪只迁移日志结构
type PigTransferLogDTO struct { type PigTransferLogDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
TransferTime time.Time `json:"transfer_time"` TransferTime time.Time `json:"transfer_time"`
PigBatchID uint `json:"pig_batch_id"` PigBatchID uint32 `json:"pig_batch_id"`
PenID uint `json:"pen_id"` PenID uint32 `json:"pen_id"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
Type models.PigTransferType `json:"type"` Type models.PigTransferType `json:"type"`
CorrelationID string `json:"correlation_id"` CorrelationID string `json:"correlation_id"`
OperatorID uint `json:"operator_id"` OperatorID uint32 `json:"operator_id"`
Remarks string `json:"remarks"` Remarks string `json:"remarks"`
} }
@@ -505,11 +496,11 @@ type ListPigTransferLogResponse struct {
type ListPigSickLogRequest struct { type ListPigSickLogRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"`
PenID *uint `json:"pen_id" query:"pen_id"` PenID *uint32 `json:"pen_id" query:"pen_id"`
Reason *string `json:"reason" query:"reason"` Reason *string `json:"reason" query:"reason"`
TreatmentLocation *string `json:"treatment_location" query:"treatment_location"` 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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -517,18 +508,18 @@ type ListPigSickLogRequest struct {
// PigSickLogDTO 是用于API响应的病猪日志结构 // PigSickLogDTO 是用于API响应的病猪日志结构
type PigSickLogDTO struct { type PigSickLogDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
PigBatchID uint `json:"pig_batch_id"` PigBatchID uint32 `json:"pig_batch_id"`
PenID uint `json:"pen_id"` PenID uint32 `json:"pen_id"`
ChangeCount int `json:"change_count"` ChangeCount int `json:"change_count"`
Reason models.PigBatchSickPigReasonType `json:"reason"` Reason models.PigBatchSickPigReasonType `json:"reason"`
BeforeCount int `json:"before_count"` BeforeCount int `json:"before_count"`
AfterCount int `json:"after_count"` AfterCount int `json:"after_count"`
Remarks string `json:"remarks"` Remarks string `json:"remarks"`
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location"` TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location"`
OperatorID uint `json:"operator_id"` OperatorID uint32 `json:"operator_id"`
HappenedAt time.Time `json:"happened_at"` HappenedAt time.Time `json:"happened_at"`
} }
@@ -544,9 +535,9 @@ type ListPigSickLogResponse struct {
type ListPigPurchaseRequest struct { type ListPigPurchaseRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` 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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -554,17 +545,17 @@ type ListPigPurchaseRequest struct {
// PigPurchaseDTO 是用于API响应的猪只采购记录结构 // PigPurchaseDTO 是用于API响应的猪只采购记录结构
type PigPurchaseDTO struct { type PigPurchaseDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_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"` PurchaseDate time.Time `json:"purchase_date"`
Supplier string `json:"supplier"` Supplier string `json:"supplier"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"` UnitPrice float32 `json:"unit_price"`
TotalPrice float64 `json:"total_price"` TotalPrice float32 `json:"total_price"`
Remarks string `json:"remarks"` Remarks string `json:"remarks"`
OperatorID uint `json:"operator_id"` OperatorID uint32 `json:"operator_id"`
} }
// ListPigPurchaseResponse 是获取猪只采购记录列表的响应结构 // ListPigPurchaseResponse 是获取猪只采购记录列表的响应结构
@@ -579,9 +570,9 @@ type ListPigPurchaseResponse struct {
type ListPigSaleRequest struct { type ListPigSaleRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` 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"` 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"` StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"` EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"` OrderBy string `json:"order_by" query:"order_by"`
@@ -589,17 +580,17 @@ type ListPigSaleRequest struct {
// PigSaleDTO 是用于API响应的猪只销售记录结构 // PigSaleDTO 是用于API响应的猪只销售记录结构
type PigSaleDTO struct { type PigSaleDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_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"` SaleDate time.Time `json:"sale_date"`
Buyer string `json:"buyer"` Buyer string `json:"buyer"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"` UnitPrice float32 `json:"unit_price"`
TotalPrice float64 `json:"total_price"` TotalPrice float32 `json:"total_price"`
Remarks string `json:"remarks"` Remarks string `json:"remarks"`
OperatorID uint `json:"operator_id"` OperatorID uint32 `json:"operator_id"`
} }
// ListPigSaleResponse 是获取猪只销售记录列表的响应结构 // ListPigSaleResponse 是获取猪只销售记录列表的响应结构

View File

@@ -2,8 +2,6 @@ package dto
import ( import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"go.uber.org/zap/zapcore"
) )
// NewListNotificationResponse 从模型数据创建通知列表响应 DTO // NewListNotificationResponse 从模型数据创建通知列表响应 DTO
@@ -18,7 +16,7 @@ func NewListNotificationResponse(data []models.Notification, total int64, page,
UserID: item.UserID, UserID: item.UserID,
Title: item.Title, Title: item.Title,
Message: item.Message, Message: item.Message,
Level: zapcore.Level(item.Level), Level: item.Level,
AlarmTimestamp: item.AlarmTimestamp, AlarmTimestamp: item.AlarmTimestamp,
ToAddress: item.ToAddress, ToAddress: item.ToAddress,
Status: item.Status, Status: item.Status,

View File

@@ -4,7 +4,6 @@ import (
"time" "time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "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" "go.uber.org/zap/zapcore"
) )
@@ -12,15 +11,15 @@ import (
// SendTestNotificationRequest 定义了发送测试通知请求的 JSON 结构 // SendTestNotificationRequest 定义了发送测试通知请求的 JSON 结构
type SendTestNotificationRequest struct { type SendTestNotificationRequest struct {
// Type 指定要测试的通知渠道 // Type 指定要测试的通知渠道
Type notify.NotifierType `json:"type" validate:"required"` Type models.NotifierType `json:"type" validate:"required"`
} }
// ListNotificationRequest 定义了获取通知列表的请求参数 // ListNotificationRequest 定义了获取通知列表的请求参数
type ListNotificationRequest struct { type ListNotificationRequest struct {
Page int `json:"page" query:"page"` Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"` PageSize int `json:"page_size" query:"page_size"`
UserID *uint `json:"user_id" query:"user_id"` UserID *uint32 `json:"user_id" query:"user_id"`
NotifierType *notify.NotifierType `json:"notifier_type" query:"notifier_type"` NotifierType *models.NotifierType `json:"notifier_type" query:"notifier_type"`
Status *models.NotificationStatus `json:"status" query:"status"` Status *models.NotificationStatus `json:"status" query:"status"`
Level *zapcore.Level `json:"level" query:"level"` Level *zapcore.Level `json:"level" query:"level"`
StartTime *time.Time `json:"start_time" query:"start_time"` StartTime *time.Time `json:"start_time" query:"start_time"`
@@ -30,14 +29,14 @@ type ListNotificationRequest struct {
// NotificationDTO 是用于API响应的通知结构 // NotificationDTO 是用于API响应的通知结构
type NotificationDTO struct { type NotificationDTO struct {
ID uint `json:"id"` ID uint32 `json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
NotifierType notify.NotifierType `json:"notifier_type"` NotifierType models.NotifierType `json:"notifier_type"`
UserID uint `json:"user_id"` UserID uint32 `json:"user_id"`
Title string `json:"title"` Title string `json:"title"`
Message string `json:"message"` Message string `json:"message"`
Level zapcore.Level `json:"level"` Level models.SeverityLevel `json:"level"`
AlarmTimestamp time.Time `json:"alarm_timestamp"` AlarmTimestamp time.Time `json:"alarm_timestamp"`
ToAddress string `json:"to_address"` ToAddress string `json:"to_address"`
Status models.NotificationStatus `json:"status"` Status models.NotificationStatus `json:"status"`

View File

@@ -32,7 +32,7 @@ type PigBatchQueryDTO struct {
// PigBatchResponseDTO 定义了猪批次信息的响应结构 // PigBatchResponseDTO 定义了猪批次信息的响应结构
type PigBatchResponseDTO struct { type PigBatchResponseDTO struct {
ID uint `json:"id"` // 批次ID ID uint32 `json:"id"` // 批次ID
BatchNumber string `json:"batch_number"` // 批次编号 BatchNumber string `json:"batch_number"` // 批次编号
OriginType models.PigBatchOriginType `json:"origin_type"` // 批次来源 OriginType models.PigBatchOriginType `json:"origin_type"` // 批次来源
StartDate time.Time `json:"start_date"` // 批次开始日期 StartDate time.Time `json:"start_date"` // 批次开始日期
@@ -48,34 +48,34 @@ type PigBatchResponseDTO struct {
// AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体 // AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体
type AssignEmptyPensToBatchRequest struct { 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 用于将猪栏划拨到新批次的请求体 // ReclassifyPenToNewBatchRequest 用于将猪栏划拨到新批次的请求体
type ReclassifyPenToNewBatchRequest struct { type ReclassifyPenToNewBatchRequest struct {
ToBatchID uint `json:"to_batch_id" validate:"required"` // 目标猪批次ID ToBatchID uint32 `json:"to_batch_id" validate:"required"` // 目标猪批次ID
PenID uint `json:"pen_id" validate:"required"` // 待划拨的猪栏ID PenID uint32 `json:"pen_id" validate:"required"` // 待划拨的猪栏ID
Remarks string `json:"remarks"` // 备注 Remarks string `json:"remarks"` // 备注
} }
// RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体 // RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体
type RemoveEmptyPenFromBatchRequest struct { type RemoveEmptyPenFromBatchRequest struct {
PenID uint `json:"pen_id" validate:"required"` // 待移除的猪栏ID PenID uint32 `json:"pen_id" validate:"required"` // 待移除的猪栏ID
} }
// MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体 // MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体
type MovePigsIntoPenRequest struct { 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"` // 移入猪只数量 Quantity int `json:"quantity" validate:"required,min=1"` // 移入猪只数量
Remarks string `json:"remarks"` // 备注 Remarks string `json:"remarks"` // 备注
} }
// SellPigsRequest 用于处理卖猪的请求体 // SellPigsRequest 用于处理卖猪的请求体
type SellPigsRequest struct { 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"` // 卖出猪只数量 Quantity int `json:"quantity" validate:"required,min=1"` // 卖出猪只数量
UnitPrice float64 `json:"unit_price" validate:"required,min=0"` // 单价 UnitPrice float32 `json:"unit_price" validate:"required,min=0"` // 单价
TotalPrice float64 `json:"total_price" validate:"required,min=0"` // 总价 TotalPrice float32 `json:"total_price" validate:"required,min=0"` // 总价
TraderName string `json:"trader_name" validate:"required"` // 交易方名称 TraderName string `json:"trader_name" validate:"required"` // 交易方名称
TradeDate time.Time `json:"trade_date" validate:"required"` // 交易日期 TradeDate time.Time `json:"trade_date" validate:"required"` // 交易日期
Remarks string `json:"remarks"` // 备注 Remarks string `json:"remarks"` // 备注
@@ -83,10 +83,10 @@ type SellPigsRequest struct {
// BuyPigsRequest 用于处理买猪的请求体 // BuyPigsRequest 用于处理买猪的请求体
type BuyPigsRequest struct { 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"` // 买入猪只数量 Quantity int `json:"quantity" validate:"required,min=1"` // 买入猪只数量
UnitPrice float64 `json:"unit_price" validate:"required,min=0"` // 单价 UnitPrice float32 `json:"unit_price" validate:"required,min=0"` // 单价
TotalPrice float64 `json:"total_price" validate:"required,min=0"` // 总价 TotalPrice float32 `json:"total_price" validate:"required,min=0"` // 总价
TraderName string `json:"trader_name" validate:"required"` // 交易方名称 TraderName string `json:"trader_name" validate:"required"` // 交易方名称
TradeDate time.Time `json:"trade_date" validate:"required"` // 交易日期 TradeDate time.Time `json:"trade_date" validate:"required"` // 交易日期
Remarks string `json:"remarks"` // 备注 Remarks string `json:"remarks"` // 备注
@@ -94,24 +94,24 @@ type BuyPigsRequest struct {
// TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体 // TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体
type TransferPigsAcrossBatchesRequest struct { type TransferPigsAcrossBatchesRequest struct {
DestBatchID uint `json:"dest_batch_id" validate:"required"` // 目标猪批次ID DestBatchID uint32 `json:"dest_batch_id" validate:"required"` // 目标猪批次ID
FromPenID uint `json:"from_pen_id" validate:"required"` // 源猪栏ID FromPenID uint32 `json:"from_pen_id" validate:"required"` // 源猪栏ID
ToPenID uint `json:"to_pen_id" validate:"required"` // 目标猪栏ID ToPenID uint32 `json:"to_pen_id" validate:"required"` // 目标猪栏ID
Quantity uint `json:"quantity" validate:"required,min=1"` // 调栏猪只数量 Quantity uint32 `json:"quantity" validate:"required,min=1"` // 调栏猪只数量
Remarks string `json:"remarks"` // 备注 Remarks string `json:"remarks"` // 备注
} }
// TransferPigsWithinBatchRequest 用于群内调栏的请求体 // TransferPigsWithinBatchRequest 用于群内调栏的请求体
type TransferPigsWithinBatchRequest struct { type TransferPigsWithinBatchRequest struct {
FromPenID uint `json:"from_pen_id" validate:"required"` // 源猪栏ID FromPenID uint32 `json:"from_pen_id" validate:"required"` // 源猪栏ID
ToPenID uint `json:"to_pen_id" validate:"required"` // 目标猪栏ID ToPenID uint32 `json:"to_pen_id" validate:"required"` // 目标猪栏ID
Quantity uint `json:"quantity" validate:"required,min=1"` // 调栏猪只数量 Quantity uint32 `json:"quantity" validate:"required,min=1"` // 调栏猪只数量
Remarks string `json:"remarks"` // 备注 Remarks string `json:"remarks"` // 备注
} }
// RecordSickPigsRequest 用于记录新增病猪事件的请求体 // RecordSickPigsRequest 用于记录新增病猪事件的请求体
type RecordSickPigsRequest struct { 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"` // 病猪数量 Quantity int `json:"quantity" validate:"required,min=1"` // 病猪数量
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点 TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间 HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
@@ -120,7 +120,7 @@ type RecordSickPigsRequest struct {
// RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体 // RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体
type RecordSickPigRecoveryRequest struct { 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"` // 康复猪数量 Quantity int `json:"quantity" validate:"required,min=1"` // 康复猪数量
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点 TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间 HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
@@ -129,7 +129,7 @@ type RecordSickPigRecoveryRequest struct {
// RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体 // RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体
type RecordSickPigDeathRequest struct { 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"` // 死亡猪数量 Quantity int `json:"quantity" validate:"required,min=1"` // 死亡猪数量
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点 TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间 HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
@@ -138,7 +138,7 @@ type RecordSickPigDeathRequest struct {
// RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体 // RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体
type RecordSickPigCullRequest struct { 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"` // 淘汰猪数量 Quantity int `json:"quantity" validate:"required,min=1"` // 淘汰猪数量
TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点 TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location" validate:"required"` // 治疗地点
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间 HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
@@ -147,7 +147,7 @@ type RecordSickPigCullRequest struct {
// RecordDeathRequest 用于记录正常猪只死亡事件的请求体 // RecordDeathRequest 用于记录正常猪只死亡事件的请求体
type RecordDeathRequest struct { 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"` // 死亡猪数量 Quantity int `json:"quantity" validate:"required,min=1"` // 死亡猪数量
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间 HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
Remarks string `json:"remarks"` // 备注 Remarks string `json:"remarks"` // 备注
@@ -155,7 +155,7 @@ type RecordDeathRequest struct {
// RecordCullRequest 用于记录正常猪只淘汰事件的请求体 // RecordCullRequest 用于记录正常猪只淘汰事件的请求体
type RecordCullRequest struct { 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"` // 淘汰猪数量 Quantity int `json:"quantity" validate:"required,min=1"` // 淘汰猪数量
HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间 HappenedAt time.Time `json:"happened_at" validate:"required"` // 发生时间
Remarks string `json:"remarks"` // 备注 Remarks string `json:"remarks"` // 备注

View File

@@ -4,19 +4,19 @@ import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
// PigHouseResponse 定义了猪舍信息的响应结构 // PigHouseResponse 定义了猪舍信息的响应结构
type PigHouseResponse struct { type PigHouseResponse struct {
ID uint `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
} }
// PenResponse 定义了猪栏信息的响应结构 // PenResponse 定义了猪栏信息的响应结构
type PenResponse struct { type PenResponse struct {
ID uint `json:"id"` ID uint32 `json:"id"`
PenNumber string `json:"pen_number"` PenNumber string `json:"pen_number"`
HouseID uint `json:"house_id"` HouseID uint32 `json:"house_id"`
Capacity int `json:"capacity"` Capacity int `json:"capacity"`
Status models.PenStatus `json:"status"` 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"` CurrentPigCount int `json:"current_pig_count"`
} }
@@ -35,14 +35,14 @@ type UpdatePigHouseRequest struct {
// CreatePenRequest 定义了创建猪栏的请求结构 // CreatePenRequest 定义了创建猪栏的请求结构
type CreatePenRequest struct { type CreatePenRequest struct {
PenNumber string `json:"pen_number" validate:"required"` 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"` Capacity int `json:"capacity" validate:"required"`
} }
// UpdatePenRequest 定义了更新猪栏的请求结构 // UpdatePenRequest 定义了更新猪栏的请求结构
type UpdatePenRequest struct { type UpdatePenRequest struct {
PenNumber string `json:"pen_number" validate:"required"` 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"` Capacity int `json:"capacity" validate:"required"`
Status models.PenStatus `json:"status" validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验 Status models.PenStatus `json:"status" validate:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验
} }

View File

@@ -15,7 +15,7 @@ func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) {
response := &PlanResponse{ response := &PlanResponse{
ID: plan.ID, ID: plan.ID,
Name: plan.Name, Name: string(plan.Name),
Description: plan.Description, Description: plan.Description,
PlanType: plan.PlanType, PlanType: plan.PlanType,
ExecutionType: plan.ExecutionType, ExecutionType: plan.ExecutionType,
@@ -60,7 +60,7 @@ func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) {
} }
plan := &models.Plan{ plan := &models.Plan{
Name: req.Name, Name: models.PlanName(req.Name),
Description: req.Description, Description: req.Description,
ExecutionType: req.ExecutionType, ExecutionType: req.ExecutionType,
ExecuteNum: req.ExecuteNum, ExecuteNum: req.ExecuteNum,
@@ -103,7 +103,7 @@ func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) {
} }
plan := &models.Plan{ plan := &models.Plan{
Name: req.Name, Name: models.PlanName(req.Name),
Description: req.Description, Description: req.Description,
ExecutionType: req.ExecutionType, ExecutionType: req.ExecutionType,
ExecuteNum: req.ExecuteNum, ExecuteNum: req.ExecuteNum,

View File

@@ -17,22 +17,22 @@ type CreatePlanRequest struct {
Name string `json:"name" validate:"required" example:"猪舍温度控制计划"` Name string `json:"name" validate:"required" example:"猪舍温度控制计划"`
Description string `json:"description" example:"根据温度自动调节风扇和加热器"` Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
ExecutionType models.PlanExecutionType `json:"execution_type" validate:"required" 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 * * *"` 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"` Tasks []TaskRequest `json:"tasks,omitempty" validate:"omitempty,dive"`
} }
// PlanResponse 定义计划详情响应的结构体 // PlanResponse 定义计划详情响应的结构体
type PlanResponse struct { type PlanResponse struct {
ID uint `json:"id" example:"1"` ID uint32 `json:"id" example:"1"`
Name string `json:"name" example:"猪舍温度控制计划"` Name string `json:"name" example:"猪舍温度控制计划"`
Description string `json:"description" example:"根据温度自动调节风扇和加热器"` Description string `json:"description" example:"根据温度自动调节风扇和加热器"`
PlanType models.PlanType `json:"plan_type" example:"自定义任务"` PlanType models.PlanType `json:"plan_type" example:"自定义任务"`
ExecutionType models.PlanExecutionType `json:"execution_type" example:"自动"` ExecutionType models.PlanExecutionType `json:"execution_type" example:"自动"`
Status models.PlanStatus `json:"status" example:"已启用"` Status models.PlanStatus `json:"status" example:"已启用"`
ExecuteNum uint `json:"execute_num" example:"10"` ExecuteNum uint32 `json:"execute_num" example:"10"`
ExecuteCount uint `json:"execute_count" example:"0"` ExecuteCount uint32 `json:"execute_count" example:"0"`
CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` CronExpression string `json:"cron_expression" example:"0 0 6 * * *"`
ContentType models.PlanContentType `json:"content_type" example:"任务"` ContentType models.PlanContentType `json:"content_type" example:"任务"`
SubPlans []SubPlanResponse `json:"sub_plans,omitempty"` SubPlans []SubPlanResponse `json:"sub_plans,omitempty"`
@@ -50,17 +50,17 @@ type UpdatePlanRequest struct {
Name string `json:"name" example:"猪舍温度控制计划V2"` Name string `json:"name" example:"猪舍温度控制计划V2"`
Description string `json:"description" example:"更新后的描述"` Description string `json:"description" example:"更新后的描述"`
ExecutionType models.PlanExecutionType `json:"execution_type" validate:"required" 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 * * *"` 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"` Tasks []TaskRequest `json:"tasks,omitempty" validate:"omitempty,dive"`
} }
// SubPlanResponse 定义子计划响应结构体 // SubPlanResponse 定义子计划响应结构体
type SubPlanResponse struct { type SubPlanResponse struct {
ID uint `json:"id" example:"1"` ID uint32 `json:"id" example:"1"`
ParentPlanID uint `json:"parent_plan_id" example:"1"` ParentPlanID uint32 `json:"parent_plan_id" example:"1"`
ChildPlanID uint `json:"child_plan_id" example:"2"` ChildPlanID uint32 `json:"child_plan_id" example:"2"`
ExecutionOrder int `json:"execution_order" example:"1"` ExecutionOrder int `json:"execution_order" example:"1"`
ChildPlan *PlanResponse `json:"child_plan,omitempty"` ChildPlan *PlanResponse `json:"child_plan,omitempty"`
} }
@@ -77,7 +77,7 @@ type TaskRequest struct {
// TaskResponse 定义任务响应结构体 // TaskResponse 定义任务响应结构体
type TaskResponse struct { type TaskResponse struct {
ID int `json:"id" example:"1"` 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:"打开风扇"` Name string `json:"name" example:"打开风扇"`
Description string `json:"description" example:"打开1号风扇"` Description string `json:"description" example:"打开1号风扇"`
ExecutionOrder int `json:"execution_order" example:"1"` ExecutionOrder int `json:"execution_order" example:"1"`

View File

@@ -16,19 +16,19 @@ type LoginRequest struct {
// CreateUserResponse 定义创建用户成功响应的结构体 // CreateUserResponse 定义创建用户成功响应的结构体
type CreateUserResponse struct { type CreateUserResponse struct {
Username string `json:"username" example:"newuser"` Username string `json:"username" example:"newuser"`
ID uint `json:"id" example:"1"` ID uint32 `json:"id" example:"1"`
} }
// LoginResponse 定义登录成功响应的结构体 // LoginResponse 定义登录成功响应的结构体
type LoginResponse struct { type LoginResponse struct {
Username string `json:"username" example:"testuser"` Username string `json:"username" example:"testuser"`
ID uint `json:"id" example:"1"` ID uint32 `json:"id" example:"1"`
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
} }
// HistoryResponse 定义单条操作历史的响应结构体 // HistoryResponse 定义单条操作历史的响应结构体
type HistoryResponse struct { type HistoryResponse struct {
UserID uint `json:"user_id" example:"101"` UserID uint32 `json:"user_id" example:"101"`
Username string `json:"username" example:"testuser"` Username string `json:"username" example:"testuser"`
ActionType string `json:"action_type" example:"更新设备"` ActionType string `json:"action_type" example:"更新设备"`
Description string `json:"description" example:"设备更新成功"` Description string `json:"description" example:"设备更新成功"`

View File

@@ -28,32 +28,33 @@ var (
// DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。 // DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。
type DeviceService interface { type DeviceService interface {
CreateDevice(ctx context.Context, req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) 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) ListDevices(ctx context.Context) ([]*dto.DeviceResponse, error)
UpdateDevice(ctx context.Context, id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) UpdateDevice(ctx context.Context, id uint32, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error)
DeleteDevice(ctx context.Context, id uint) error DeleteDevice(ctx context.Context, id uint32) error
ManualControl(ctx context.Context, id uint, req *dto.ManualControlDeviceRequest) error ManualControl(ctx context.Context, id uint32, req *dto.ManualControlDeviceRequest) error
CreateAreaController(ctx context.Context, req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, 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) ListAreaControllers(ctx context.Context) ([]*dto.AreaControllerResponse, error)
UpdateAreaController(ctx context.Context, id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) UpdateAreaController(ctx context.Context, id uint32, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error)
DeleteAreaController(ctx context.Context, id uint) error DeleteAreaController(ctx context.Context, id uint32) error
CreateDeviceTemplate(ctx context.Context, req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, 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) ListDeviceTemplates(ctx context.Context) ([]*dto.DeviceTemplateResponse, error)
UpdateDeviceTemplate(ctx context.Context, id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) UpdateDeviceTemplate(ctx context.Context, id uint32, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
DeleteDeviceTemplate(ctx context.Context, id uint) error DeleteDeviceTemplate(ctx context.Context, id uint32) error
} }
// deviceService 是 DeviceService 接口的具体实现。 // deviceService 是 DeviceService 接口的具体实现。
type deviceService struct { type deviceService struct {
ctx context.Context ctx context.Context
deviceRepo repository.DeviceRepository deviceRepo repository.DeviceRepository
areaControllerRepo repository.AreaControllerRepository areaControllerRepo repository.AreaControllerRepository
deviceTemplateRepo repository.DeviceTemplateRepository deviceTemplateRepo repository.DeviceTemplateRepository
deviceDomainSvc device.Service deviceDomainSvc device.Service
thresholdAlarmService ThresholdAlarmService
} }
// NewDeviceService 创建一个新的 DeviceService 实例。 // NewDeviceService 创建一个新的 DeviceService 实例。
@@ -63,13 +64,15 @@ func NewDeviceService(
areaControllerRepo repository.AreaControllerRepository, areaControllerRepo repository.AreaControllerRepository,
deviceTemplateRepo repository.DeviceTemplateRepository, deviceTemplateRepo repository.DeviceTemplateRepository,
deviceDomainSvc device.Service, deviceDomainSvc device.Service,
thresholdAlarmService ThresholdAlarmService,
) DeviceService { ) DeviceService {
return &deviceService{ return &deviceService{
ctx: ctx, ctx: ctx,
deviceRepo: deviceRepo, deviceRepo: deviceRepo,
areaControllerRepo: areaControllerRepo, areaControllerRepo: areaControllerRepo,
deviceTemplateRepo: deviceTemplateRepo, deviceTemplateRepo: deviceTemplateRepo,
deviceDomainSvc: deviceDomainSvc, deviceDomainSvc: deviceDomainSvc,
thresholdAlarmService: thresholdAlarmService,
} }
} }
@@ -106,7 +109,7 @@ func (s *deviceService) CreateDevice(ctx context.Context, req *dto.CreateDeviceR
return dto.NewDeviceResponse(createdDevice) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDevice")
device, err := s.deviceRepo.FindByID(serviceCtx, id) device, err := s.deviceRepo.FindByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -124,7 +127,7 @@ func (s *deviceService) ListDevices(ctx context.Context) ([]*dto.DeviceResponse,
return dto.NewListDeviceResponse(devices) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateDevice")
existingDevice, err := s.deviceRepo.FindByID(serviceCtx, id) existingDevice, err := s.deviceRepo.FindByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -158,7 +161,7 @@ func (s *deviceService) UpdateDevice(ctx context.Context, id uint, req *dto.Upda
return dto.NewDeviceResponse(updatedDevice) 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") 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 { if err != nil {
// 如果检查过程中发生数据库错误,则返回错误 // 如果检查过程中发生数据库错误,则返回错误
return fmt.Errorf("检查设备使用情况失败: %w", err) return fmt.Errorf("检查设备使用情况失败: %w", err)
@@ -178,11 +181,17 @@ func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error {
return ErrDeviceInUse return ErrDeviceInUse
} }
// TODO 这个应该用事务处理
err = s.thresholdAlarmService.DeleteDeviceThresholdAlarmByDeviceID(serviceCtx, id)
if err != nil {
return fmt.Errorf("删除设备阈值告警失败: %w", err)
}
// 只有在未被使用时,才执行删除操作 // 只有在未被使用时,才执行删除操作
return s.deviceRepo.Delete(serviceCtx, id) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "ManualControl")
dev, err := s.deviceRepo.FindByID(serviceCtx, id) dev, err := s.deviceRepo.FindByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -232,7 +241,7 @@ func (s *deviceService) CreateAreaController(ctx context.Context, req *dto.Creat
return dto.NewAreaControllerResponse(ac) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetAreaController")
ac, err := s.areaControllerRepo.FindByID(serviceCtx, id) ac, err := s.areaControllerRepo.FindByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -250,7 +259,7 @@ func (s *deviceService) ListAreaControllers(ctx context.Context) ([]*dto.AreaCon
return dto.NewListAreaControllerResponse(acs) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateAreaController")
existingAC, err := s.areaControllerRepo.FindByID(serviceCtx, id) existingAC, err := s.areaControllerRepo.FindByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -278,7 +287,7 @@ func (s *deviceService) UpdateAreaController(ctx context.Context, id uint, req *
return dto.NewAreaControllerResponse(existingAC) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteAreaController")
// 1. 检查是否存在 // 1. 检查是否存在
@@ -288,7 +297,7 @@ func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error
} }
// 2. 检查是否被使用(业务逻辑) // 2. 检查是否被使用(业务逻辑)
inUse, err := s.deviceRepo.IsAreaControllerInUse(serviceCtx, id) inUse, err := s.areaControllerRepo.IsAreaControllerUsedByTasks(serviceCtx, id, []models.TaskType{models.TaskTypeAreaCollectorThresholdCheck})
if err != nil { if err != nil {
return err // 返回数据库检查错误 return err // 返回数据库检查错误
} }
@@ -296,6 +305,12 @@ func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error
return ErrAreaControllerInUse // 返回业务错误 return ErrAreaControllerInUse // 返回业务错误
} }
// TODO 这个应该用事务处理
err = s.thresholdAlarmService.DeleteAreaThresholdAlarmByAreaControllerID(serviceCtx, id)
if err != nil {
return fmt.Errorf("删除区域阈值告警失败: %w", err)
}
// 3. 执行删除 // 3. 执行删除
return s.areaControllerRepo.Delete(serviceCtx, id) return s.areaControllerRepo.Delete(serviceCtx, id)
} }
@@ -334,7 +349,7 @@ func (s *deviceService) CreateDeviceTemplate(ctx context.Context, req *dto.Creat
return dto.NewDeviceTemplateResponse(deviceTemplate) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDeviceTemplate")
deviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id) deviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -352,7 +367,7 @@ func (s *deviceService) ListDeviceTemplates(ctx context.Context) ([]*dto.DeviceT
return dto.NewListDeviceTemplateResponse(deviceTemplates) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateDeviceTemplate")
existingDeviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id) existingDeviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -387,7 +402,7 @@ func (s *deviceService) UpdateDeviceTemplate(ctx context.Context, id uint, req *
return dto.NewDeviceTemplateResponse(existingDeviceTemplate) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteDeviceTemplate")
// 1. 检查是否存在 // 1. 检查是否存在

View File

@@ -147,7 +147,7 @@ func (s *monitorService) ListPlanExecutionLogs(ctx context.Context, req *dto.Lis
return nil, err return nil, err
} }
planIds := make([]uint, 0, len(planLogs)) planIds := make([]uint32, 0, len(planLogs))
for _, datum := range planLogs { for _, datum := range planLogs {
has := false has := false
for _, id := range planIds { for _, id := range planIds {

View File

@@ -12,35 +12,35 @@ import (
// PigBatchService 接口定义保持不变,继续作为应用层对外的契约。 // PigBatchService 接口定义保持不变,继续作为应用层对外的契约。
type PigBatchService interface { type PigBatchService interface {
CreatePigBatch(ctx context.Context, operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) CreatePigBatch(ctx context.Context, operatorID uint32, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error)
GetPigBatch(ctx context.Context, id uint) (*dto.PigBatchResponseDTO, error) GetPigBatch(ctx context.Context, id uint32) (*dto.PigBatchResponseDTO, error)
UpdatePigBatch(ctx context.Context, id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) UpdatePigBatch(ctx context.Context, id uint32, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error)
DeletePigBatch(ctx context.Context, id uint) error DeletePigBatch(ctx context.Context, id uint32) error
ListPigBatches(ctx context.Context, isActive *bool) ([]*dto.PigBatchResponseDTO, error) ListPigBatches(ctx context.Context, isActive *bool) ([]*dto.PigBatchResponseDTO, error)
// Pig Pen Management // Pig Pen Management
AssignEmptyPensToBatch(ctx context.Context, batchID uint, penIDs []uint, operatorID uint) error AssignEmptyPensToBatch(ctx context.Context, batchID uint32, penIDs []uint32, operatorID uint32) error
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(ctx context.Context, batchID uint, penID uint) error RemoveEmptyPenFromBatch(ctx context.Context, batchID uint32, penID uint32) error
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
// Trade Sub-service // 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 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 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
// Transfer Sub-service // Transfer Sub-service
TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint, destBatchID 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 uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error TransferPigsWithinBatch(ctx context.Context, batchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error
// Sick Pig Management // Sick Pig Management
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(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(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(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
// Normal Pig Management // Normal Pig Management
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(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 的实现现在依赖于领域服务接口。
@@ -79,7 +79,7 @@ func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch, currentT
} }
// CreatePigBatch 现在将请求委托给领域服务处理。 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreatePigBatch")
// 1. DTO -> 领域模型 // 1. DTO -> 领域模型
batch := &models.PigBatch{ batch := &models.PigBatch{
@@ -102,7 +102,7 @@ func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint, d
} }
// GetPigBatch 从领域服务获取数据并转换为DTO同时处理错误转换。 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPigBatch")
batch, err := s.domainService.GetPigBatch(serviceCtx, id) batch, err := s.domainService.GetPigBatch(serviceCtx, id)
if err != nil { if err != nil {
@@ -123,7 +123,7 @@ func (s *pigBatchService) GetPigBatch(ctx context.Context, id uint) (*dto.PigBat
} }
// UpdatePigBatch 协调获取、更新和保存的流程,并处理错误转换。 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePigBatch")
// 1. 先获取最新的领域模型 // 1. 先获取最新的领域模型
existingBatch, err := s.domainService.GetPigBatch(serviceCtx, id) existingBatch, err := s.domainService.GetPigBatch(serviceCtx, id)
@@ -176,7 +176,7 @@ func (s *pigBatchService) UpdatePigBatch(ctx context.Context, id uint, dto *dto.
} }
// DeletePigBatch 将删除操作委托给领域服务,并转换领域错误为应用层错误。 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeletePigBatch")
err := s.domainService.DeletePigBatch(serviceCtx, id) err := s.domainService.DeletePigBatch(serviceCtx, id)
if err != nil { if err != nil {
@@ -214,7 +214,7 @@ func (s *pigBatchService) ListPigBatches(ctx context.Context, isActive *bool) ([
} }
// AssignEmptyPensToBatch 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "AssignEmptyPensToBatch")
err := s.domainService.AssignEmptyPensToBatch(serviceCtx, batchID, penIDs, operatorID) err := s.domainService.AssignEmptyPensToBatch(serviceCtx, batchID, penIDs, operatorID)
if err != nil { if err != nil {
@@ -225,7 +225,7 @@ func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID ui
} }
// ReclassifyPenToNewBatch 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "ReclassifyPenToNewBatch")
err := s.domainService.ReclassifyPenToNewBatch(serviceCtx, fromBatchID, toBatchID, penID, operatorID, remarks) err := s.domainService.ReclassifyPenToNewBatch(serviceCtx, fromBatchID, toBatchID, penID, operatorID, remarks)
if err != nil { if err != nil {
@@ -236,7 +236,7 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatch
} }
// RemoveEmptyPenFromBatch 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "RemoveEmptyPenFromBatch")
err := s.domainService.RemoveEmptyPenFromBatch(serviceCtx, batchID, penID) err := s.domainService.RemoveEmptyPenFromBatch(serviceCtx, batchID, penID)
if err != nil { if err != nil {
@@ -247,7 +247,7 @@ func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID u
} }
// MovePigsIntoPen 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "MovePigsIntoPen")
err := s.domainService.MovePigsIntoPen(serviceCtx, batchID, toPenID, quantity, operatorID, remarks) err := s.domainService.MovePigsIntoPen(serviceCtx, batchID, toPenID, quantity, operatorID, remarks)
if err != nil { if err != nil {
@@ -258,7 +258,7 @@ func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint, toP
} }
// SellPigs 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "SellPigs")
err := s.domainService.SellPigs(serviceCtx, batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID) err := s.domainService.SellPigs(serviceCtx, batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
if err != nil { if err != nil {
@@ -269,7 +269,7 @@ func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint, penID uint
} }
// BuyPigs 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "BuyPigs")
err := s.domainService.BuyPigs(serviceCtx, batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID) err := s.domainService.BuyPigs(serviceCtx, batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
if err != nil { if err != nil {
@@ -280,7 +280,7 @@ func (s *pigBatchService) BuyPigs(ctx context.Context, batchID uint, penID uint,
} }
// TransferPigsAcrossBatches 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "TransferPigsAcrossBatches")
err := s.domainService.TransferPigsAcrossBatches(serviceCtx, sourceBatchID, destBatchID, fromPenID, toPenID, quantity, operatorID, remarks) err := s.domainService.TransferPigsAcrossBatches(serviceCtx, sourceBatchID, destBatchID, fromPenID, toPenID, quantity, operatorID, remarks)
if err != nil { if err != nil {
@@ -291,7 +291,7 @@ func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceB
} }
// TransferPigsWithinBatch 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "TransferPigsWithinBatch")
err := s.domainService.TransferPigsWithinBatch(serviceCtx, batchID, fromPenID, toPenID, quantity, operatorID, remarks) err := s.domainService.TransferPigsWithinBatch(serviceCtx, batchID, fromPenID, toPenID, quantity, operatorID, remarks)
if err != nil { if err != nil {
@@ -302,7 +302,7 @@ func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID u
} }
// RecordSickPigs 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordSickPigs")
err := s.domainService.RecordSickPigs(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks) err := s.domainService.RecordSickPigs(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
if err != nil { if err != nil {
@@ -313,7 +313,7 @@ func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint, b
} }
// RecordSickPigRecovery 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordSickPigRecovery")
err := s.domainService.RecordSickPigRecovery(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks) err := s.domainService.RecordSickPigRecovery(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
if err != nil { if err != nil {
@@ -324,7 +324,7 @@ func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID
} }
// RecordSickPigDeath 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordSickPigDeath")
err := s.domainService.RecordSickPigDeath(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks) err := s.domainService.RecordSickPigDeath(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
if err != nil { if err != nil {
@@ -335,7 +335,7 @@ func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uin
} }
// RecordSickPigCull 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordSickPigCull")
err := s.domainService.RecordSickPigCull(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks) err := s.domainService.RecordSickPigCull(serviceCtx, operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
if err != nil { if err != nil {
@@ -346,7 +346,7 @@ func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint
} }
// RecordDeath 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordDeath")
err := s.domainService.RecordDeath(serviceCtx, operatorID, batchID, penID, quantity, happenedAt, remarks) err := s.domainService.RecordDeath(serviceCtx, operatorID, batchID, penID, quantity, happenedAt, remarks)
if err != nil { if err != nil {
@@ -357,7 +357,7 @@ func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint, batc
} }
// RecordCull 委托给领域服务 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "RecordCull")
err := s.domainService.RecordCull(serviceCtx, operatorID, batchID, penID, quantity, happenedAt, remarks) err := s.domainService.RecordCull(serviceCtx, operatorID, batchID, penID, quantity, happenedAt, remarks)
if err != nil { if err != nil {

View File

@@ -18,19 +18,19 @@ import (
type PigFarmService interface { type PigFarmService interface {
// PigHouse methods // PigHouse methods
CreatePigHouse(ctx context.Context, name, description string) (*dto.PigHouseResponse, error) 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) ListPigHouses(ctx context.Context) ([]dto.PigHouseResponse, error)
UpdatePigHouse(ctx context.Context, id uint, name, description string) (*dto.PigHouseResponse, error) UpdatePigHouse(ctx context.Context, id uint32, name, description string) (*dto.PigHouseResponse, error)
DeletePigHouse(ctx context.Context, id uint) error DeletePigHouse(ctx context.Context, id uint32) error
// Pen methods // Pen methods
CreatePen(ctx context.Context, penNumber string, houseID uint, capacity int) (*dto.PenResponse, error) CreatePen(ctx context.Context, penNumber string, houseID uint32, capacity int) (*dto.PenResponse, error)
GetPenByID(ctx context.Context, id uint) (*dto.PenResponse, error) GetPenByID(ctx context.Context, id uint32) (*dto.PenResponse, error)
ListPens(ctx context.Context) ([]*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) UpdatePen(ctx context.Context, id uint32, penNumber string, houseID uint32, capacity int, status models.PenStatus) (*dto.PenResponse, error)
DeletePen(ctx context.Context, id uint) error DeletePen(ctx context.Context, id uint32) error
// UpdatePenStatus 更新猪栏状态 // 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 { type pigFarmService struct {
@@ -79,7 +79,7 @@ func (s *pigFarmService) CreatePigHouse(ctx context.Context, name, description s
}, nil }, 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigHouseByID")
house, err := s.farmRepository.GetPigHouseByID(serviceCtx, id) house, err := s.farmRepository.GetPigHouseByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -109,10 +109,10 @@ func (s *pigFarmService) ListPigHouses(ctx context.Context) ([]dto.PigHouseRespo
return resp, nil 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigHouse")
house := &models.PigHouse{ house := &models.PigHouse{
Model: gorm.Model{ID: id}, Model: models.Model{ID: id},
Name: name, Name: name,
Description: description, Description: description,
} }
@@ -135,7 +135,7 @@ func (s *pigFarmService) UpdatePigHouse(ctx context.Context, id uint, name, desc
}, nil }, 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigHouse")
// 业务逻辑:检查猪舍是否包含猪栏 // 业务逻辑:检查猪舍是否包含猪栏
penCount, err := s.farmRepository.CountPensInHouse(serviceCtx, id) penCount, err := s.farmRepository.CountPensInHouse(serviceCtx, id)
@@ -159,7 +159,7 @@ func (s *pigFarmService) DeletePigHouse(ctx context.Context, id uint) error {
// --- Pen Implementation --- // --- 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePen")
// 业务逻辑:验证所属猪舍是否存在 // 业务逻辑:验证所属猪舍是否存在
_, err := s.farmRepository.GetPigHouseByID(serviceCtx, houseID) _, err := s.farmRepository.GetPigHouseByID(serviceCtx, houseID)
@@ -189,7 +189,7 @@ func (s *pigFarmService) CreatePen(ctx context.Context, penNumber string, houseI
}, nil }, 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPenByID")
pen, err := s.penRepository.GetPenByID(serviceCtx, id) pen, err := s.penRepository.GetPenByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -251,7 +251,7 @@ func (s *pigFarmService) ListPens(ctx context.Context) ([]*dto.PenResponse, erro
return response, nil 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePen")
// 业务逻辑:验证所属猪舍是否存在 // 业务逻辑:验证所属猪舍是否存在
_, err := s.farmRepository.GetPigHouseByID(serviceCtx, houseID) _, err := s.farmRepository.GetPigHouseByID(serviceCtx, houseID)
@@ -263,7 +263,7 @@ func (s *pigFarmService) UpdatePen(ctx context.Context, id uint, penNumber strin
} }
pen := &models.Pen{ pen := &models.Pen{
Model: gorm.Model{ID: id}, Model: models.Model{ID: id},
PenNumber: penNumber, PenNumber: penNumber,
HouseID: houseID, HouseID: houseID,
Capacity: capacity, Capacity: capacity,
@@ -291,7 +291,7 @@ func (s *pigFarmService) UpdatePen(ctx context.Context, id uint, penNumber strin
}, nil }, 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePen")
// 业务逻辑:检查猪栏是否被活跃批次使用 // 业务逻辑:检查猪栏是否被活跃批次使用
pen, err := s.penRepository.GetPenByID(serviceCtx, id) pen, err := s.penRepository.GetPenByID(serviceCtx, id)
@@ -327,7 +327,7 @@ func (s *pigFarmService) DeletePen(ctx context.Context, id uint) error {
} }
// UpdatePenStatus 更新猪栏状态 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePenStatus")
var updatedPen *models.Pen var updatedPen *models.Pen
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {

View File

@@ -7,6 +7,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto" "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/domain/plan"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "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" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
) )
@@ -15,17 +16,17 @@ type PlanService interface {
// CreatePlan 创建一个新的计划 // CreatePlan 创建一个新的计划
CreatePlan(ctx context.Context, req *dto.CreatePlanRequest) (*dto.PlanResponse, error) CreatePlan(ctx context.Context, req *dto.CreatePlanRequest) (*dto.PlanResponse, error)
// GetPlanByID 根据ID获取计划详情 // GetPlanByID 根据ID获取计划详情
GetPlanByID(ctx context.Context, id uint) (*dto.PlanResponse, error) GetPlanByID(ctx context.Context, id uint32) (*dto.PlanResponse, error)
// ListPlans 获取计划列表,支持过滤和分页 // ListPlans 获取计划列表,支持过滤和分页
ListPlans(ctx context.Context, query *dto.ListPlansQuery) (*dto.ListPlansResponse, error) ListPlans(ctx context.Context, query *dto.ListPlansQuery) (*dto.ListPlansResponse, error)
// UpdatePlan 更新计划 // 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 删除计划(软删除)
DeletePlan(ctx context.Context, id uint) error DeletePlan(ctx context.Context, id uint32) error
// StartPlan 启动计划 // StartPlan 启动计划
StartPlan(ctx context.Context, id uint) error StartPlan(ctx context.Context, id uint32) error
// StopPlan 停止计划 // StopPlan 停止计划
StopPlan(ctx context.Context, id uint) error StopPlan(ctx context.Context, id uint32) error
} }
// planService 是 PlanService 接口的实现 // planService 是 PlanService 接口的实现
@@ -76,7 +77,7 @@ func (s *planService) CreatePlan(ctx context.Context, req *dto.CreatePlanRequest
} }
// GetPlanByID 根据ID获取计划详情 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPlanByID")
const actionType = "应用服务层:获取计划详情" const actionType = "应用服务层:获取计划详情"
@@ -134,7 +135,7 @@ func (s *planService) ListPlans(ctx context.Context, query *dto.ListPlansQuery)
} }
// UpdatePlan 更新计划 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePlan")
const actionType = "应用服务层:更新计划" const actionType = "应用服务层:更新计划"
@@ -147,7 +148,7 @@ func (s *planService) UpdatePlan(ctx context.Context, id uint, req *dto.UpdatePl
planToUpdate.ID = id // 确保ID被设置 planToUpdate.ID = id // 确保ID被设置
// 调用领域服务更新计划 // 调用领域服务更新计划
updatedPlan, err := s.domainPlanService.UpdatePlan(serviceCtx, planToUpdate) updatedPlan, err := s.domainPlanService.UpdatePlan(serviceCtx, planToUpdate, models.PlanTypeCustom)
if err != nil { if err != nil {
logger.Errorf("%s: 领域服务更新计划失败: %v, ID: %d", actionType, err, id) logger.Errorf("%s: 领域服务更新计划失败: %v, ID: %d", actionType, err, id)
return nil, err // 直接返回领域层错误 return nil, err // 直接返回领域层错误
@@ -165,7 +166,7 @@ func (s *planService) UpdatePlan(ctx context.Context, id uint, req *dto.UpdatePl
} }
// DeletePlan 删除计划(软删除) // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeletePlan")
const actionType = "应用服务层:删除计划" const actionType = "应用服务层:删除计划"
@@ -181,7 +182,7 @@ func (s *planService) DeletePlan(ctx context.Context, id uint) error {
} }
// StartPlan 启动计划 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "StartPlan")
const actionType = "应用服务层:启动计划" const actionType = "应用服务层:启动计划"
@@ -197,7 +198,7 @@ func (s *planService) StartPlan(ctx context.Context, id uint) error {
} }
// StopPlan 停止计划 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "StopPlan")
const actionType = "应用服务层:停止计划" const actionType = "应用服务层:停止计划"

View 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(&params)
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(&params)
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(&params); 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(&params)
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(&params); 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(&params); 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(&params); 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(&params)
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(&params); 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
}

View File

@@ -18,7 +18,7 @@ import (
type UserService interface { type UserService interface {
CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*dto.CreateUserResponse, error) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*dto.CreateUserResponse, error)
Login(ctx context.Context, req *dto.LoginRequest) (*dto.LoginResponse, 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 接口 // userService 实现了 UserService 接口
@@ -103,7 +103,7 @@ func (s *userService) Login(ctx context.Context, req *dto.LoginRequest) (*dto.Lo
} }
// SendTestNotification 发送测试通知 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendTestNotification")
err := s.notifyService.SendTestMessage(serviceCtx, userID, req.Type) err := s.notifyService.SendTestMessage(serviceCtx, userID, req.Type)
if err != nil { if err != nil {

View File

@@ -162,17 +162,17 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent)
logger.Infof("开始处理 'up' 事件, DevEui: %s", event.DeviceInfo.DevEui) logger.Infof("开始处理 'up' 事件, DevEui: %s", event.DeviceInfo.DevEui)
// 1. 查找区域主控设备 // 1. 查找区域主控设备
regionalController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui) areaController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui)
if err != nil { if err != nil {
logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err) logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
return return
} }
// 依赖 SelfCheck 确保区域主控有效 // 依赖 SelfCheck 确保区域主控有效
if err := regionalController.SelfCheck(); err != nil { if err := areaController.SelfCheck(); err != nil {
logger.Errorf("处理 'up' 事件失败:区域主控 %v(ID: %d) 未通过自检: %v", regionalController.Name, regionalController.ID, err) logger.Errorf("处理 'up' 事件失败:区域主控 %v(ID: %d) 未通过自检: %v", areaController.Name, areaController.ID, err)
return return
} }
logger.Infof("找到区域主控: %s (ID: %d)", regionalController.Name, regionalController.ID) logger.Infof("找到区域主控: %s (ID: %d)", areaController.Name, areaController.ID)
// 2. 记录区域主控的信号强度 (如果存在) // 2. 记录区域主控的信号强度 (如果存在)
if len(event.RxInfo) > 0 { 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) c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
logger.Infof("已记录区域主控 (ID: %d) 的信号强度: RSSI=%d, SNR=%.2f", regionalController.ID, rx.Rssi, rx.Snr) logger.Infof("已记录区域主控 (ID: %d) 的信号强度: RSSI=%d, SNR=%.2f", areaController.ID, rx.Rssi, rx.Snr)
} else { } else {
logger.Warnf("处理 'up' 事件时未找到 RxInfo无法记录信号数据。DevEui: %s", event.DeviceInfo.DevEui) 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] valueDescriptor := valueDescriptors[0]
// 5.3 应用乘数和偏移量计算最终值 // 5.3 应用乘数和偏移量计算最终值
parsedValue := float64(rawSensorValue)*valueDescriptor.Multiplier + valueDescriptor.Offset parsedValue := rawSensorValue*valueDescriptor.Multiplier + valueDescriptor.Offset
// 5.4 根据传感器类型构建具体的数据结构 // 5.4 根据传感器类型构建具体的数据结构
var dataToRecord interface{} var dataToRecord interface{}
@@ -312,11 +312,11 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent)
default: default:
// TODO 未知传感器的数据需要记录吗 // TODO 未知传感器的数据需要记录吗
logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type) logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type)
dataToRecord = map[string]float64{"value": parsedValue} dataToRecord = map[string]float32{"value": parsedValue}
} }
// 5.5 记录传感器数据 // 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) 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) 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 { if err != nil {
logger.Errorf("处理 'status' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err) logger.Errorf("处理 'status' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
return return
@@ -344,8 +344,8 @@ func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *Statu
signalMetrics := models.SignalMetrics{ signalMetrics := models.SignalMetrics{
MarginDb: event.Margin, MarginDb: event.Margin,
} }
c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics) c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
logger.Infof("已记录区域主控 (ID: %d) 的信号状态: %+v", regionalController.ID, signalMetrics) logger.Infof("已记录区域主控 (ID: %d) 的信号状态: %+v", areaController.ID, signalMetrics)
// 记录电量 // 记录电量
batteryLevel := models.BatteryLevel{ batteryLevel := models.BatteryLevel{
@@ -353,8 +353,8 @@ func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *Statu
BatteryLevelUnavailable: event.BatteryLevelUnavailable, BatteryLevelUnavailable: event.BatteryLevelUnavailable,
ExternalPower: event.ExternalPower, ExternalPower: event.ExternalPower,
} }
c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeBatteryLevel, batteryLevel) c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeBatteryLevel, batteryLevel)
logger.Infof("已记录区域主控 (ID: %d) 的电池状态: %+v", regionalController.ID, batteryLevel) logger.Infof("已记录区域主控 (ID: %d) 的电池状态: %+v", areaController.ID, batteryLevel)
} }
// handleAckEvent 处理下行确认事件 // handleAckEvent 处理下行确认事件
@@ -425,11 +425,11 @@ func (c *ChirpStackListener) handleIntegrationEvent(ctx context.Context, event *
} }
// recordSensorData 是一个通用方法,用于将传感器数据存入数据库。 // recordSensorData 是一个通用方法,用于将传感器数据存入数据库。
// regionalControllerID: 区域主控设备的ID // areaControllerID: 区域主控设备的ID
// sensorDeviceID: 实际产生传感器数据的普通设备的ID // sensorDeviceID: 实际产生传感器数据的普通设备的ID
// sensorType: 传感器值的类型 (例如 models.SensorTypeTemperature) // sensorType: 传感器值的类型 (例如 models.SensorTypeTemperature)
// data: 具体的传感器数据结构体实例 (例如 models.TemperatureData) // 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") reqCtx, logger := logs.Trace(ctx, c.ctx, "recordSensorData")
// 1. 将传入的结构体序列化为 JSON // 1. 将传入的结构体序列化为 JSON
jsonData, err := json.Marshal(data) jsonData, err := json.Marshal(data)
@@ -440,11 +440,11 @@ func (c *ChirpStackListener) recordSensorData(ctx context.Context, regionalContr
// 2. 构建 SensorData 模型 // 2. 构建 SensorData 模型
sensorData := &models.SensorData{ sensorData := &models.SensorData{
Time: eventTime, Time: eventTime,
DeviceID: sensorDeviceID, DeviceID: sensorDeviceID,
RegionalControllerID: regionalControllerID, AreaControllerID: areaControllerID,
SensorType: sensorType, SensorType: sensorType,
Data: datatypes.JSON(jsonData), Data: datatypes.JSON(jsonData),
} }
// 3. 调用仓库创建记录 // 3. 调用仓库创建记录

View File

@@ -24,9 +24,9 @@ type DeviceInfo struct {
// Location 包含了地理位置信息。 // Location 包含了地理位置信息。
type Location struct { type Location struct {
Latitude float64 `json:"latitude"` // 纬度 Latitude float32 `json:"latitude"` // 纬度
Longitude float64 `json:"longitude"` // 经度 Longitude float32 `json:"longitude"` // 经度
Altitude float64 `json:"altitude"` // 海拔 Altitude float32 `json:"altitude"` // 海拔
} }
// --- 可复用的子结构体 --- // --- 可复用的子结构体 ---
@@ -61,7 +61,7 @@ type UplinkRxInfo struct {
UplinkID uint32 `json:"uplink_id"` // 上行ID UplinkID uint32 `json:"uplink_id"` // 上行ID
Time time.Time `json:"time"` // 接收时间 Time time.Time `json:"time"` // 接收时间
Rssi int `json:"rssi"` // 接收信号强度指示 Rssi int `json:"rssi"` // 接收信号强度指示
Snr float64 `json:"snr"` // 信噪比 Snr float32 `json:"snr"` // 信噪比
Channel int `json:"channel"` // 接收通道 Channel int `json:"channel"` // 接收通道
Location *Location `json:"location"` // 网关位置 Location *Location `json:"location"` // 网关位置
Context string `json:"context"` // 上下文信息 Context string `json:"context"` // 上下文信息
@@ -96,9 +96,9 @@ type DownlinkTxInfo struct {
// ResolvedLocation 包含了地理位置解析结果。 // ResolvedLocation 包含了地理位置解析结果。
type ResolvedLocation struct { type ResolvedLocation struct {
Latitude float64 `json:"latitude"` // 纬度 Latitude float32 `json:"latitude"` // 纬度
Longitude float64 `json:"longitude"` // 经度 Longitude float32 `json:"longitude"` // 经度
Altitude float64 `json:"altitude"` // 海拔 Altitude float32 `json:"altitude"` // 海拔
Source string `json:"source"` // 位置来源 Source string `json:"source"` // 位置来源
Accuracy int `json:"accuracy"` // 精度 Accuracy int `json:"accuracy"` // 精度
} }

View File

@@ -43,7 +43,10 @@ func NewApplication(configPath string) (*Application, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("初始化基础设施失败: %w", err) 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) appServices := initAppServices(ctx, infra, domain)
// 3. 初始化 API 入口点 // 3. 初始化 API 入口点
@@ -58,6 +61,7 @@ func NewApplication(configPath string) (*Application, error) {
appServices.planService, appServices.planService,
appServices.userService, appServices.userService,
appServices.auditService, appServices.auditService,
appServices.thresholdAlarmService,
infra.tokenGenerator, infra.tokenGenerator,
infra.lora.listenHandler, infra.lora.listenHandler,
) )

View File

@@ -7,6 +7,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/service" "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/app/webhook"
"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/device"
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/pig" "git.huangwc.com/pig/pig-farm-controller/internal/domain/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"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora" "git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -29,7 +31,6 @@ type Infrastructure struct {
storage database.Storage storage database.Storage
repos *Repositories repos *Repositories
lora *LoraComponents lora *LoraComponents
notifyService domain_notify.Service
tokenGenerator token.Generator tokenGenerator token.Generator
} }
@@ -47,18 +48,12 @@ func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructur
return nil, err 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)) tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret))
return &Infrastructure{ return &Infrastructure{
storage: storage, storage: storage,
repos: repos, repos: repos,
lora: lora, lora: lora,
notifyService: notifyService,
tokenGenerator: tokenGenerator, tokenGenerator: tokenGenerator,
}, nil }, nil
} }
@@ -86,6 +81,7 @@ type Repositories struct {
medicationLogRepo repository.MedicationLogRepository medicationLogRepo repository.MedicationLogRepository
rawMaterialRepo repository.RawMaterialRepository rawMaterialRepo repository.RawMaterialRepository
notificationRepo repository.NotificationRepository notificationRepo repository.NotificationRepository
alarmRepo repository.AlarmRepository
unitOfWork repository.UnitOfWork unitOfWork repository.UnitOfWork
} }
@@ -114,6 +110,7 @@ func initRepositories(ctx context.Context, db *gorm.DB) *Repositories {
medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), db), medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), db),
rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db), rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db),
notificationRepo: repository.NewGormNotificationRepository(logs.AddCompName(baseCtx, "NotificationRepo"), 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), unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db),
} }
} }
@@ -129,12 +126,19 @@ type DomainServices struct {
planExecutionManager plan.ExecutionManager planExecutionManager plan.ExecutionManager
analysisPlanTaskManager plan.AnalysisPlanTaskManager analysisPlanTaskManager plan.AnalysisPlanTaskManager
planService plan.Service planService plan.Service
notifyService domain_notify.Service
alarmService alarm.AlarmService
} }
// initDomainServices 初始化所有的领域服务。 // 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() 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) 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) 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, 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) 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, taskFactory: taskFactory,
planExecutionManager: planExecutionManager, planExecutionManager: planExecutionManager,
planService: planService, planService: planService,
} notifyService: notifyService,
alarmService: alarmService,
}, nil
} }
// AppServices 聚合了所有的应用服务实例。 // AppServices 聚合了所有的应用服务实例。
type AppServices struct { type AppServices struct {
pigFarmService service.PigFarmService pigFarmService service.PigFarmService
pigBatchService service.PigBatchService pigBatchService service.PigBatchService
monitorService service.MonitorService monitorService service.MonitorService
deviceService service.DeviceService deviceService service.DeviceService
planService service.PlanService planService service.PlanService
userService service.UserService userService service.UserService
auditService service.AuditService auditService service.AuditService
thresholdAlarmService service.ThresholdAlarmService
} }
// initAppServices 初始化所有的应用服务。 // initAppServices 初始化所有的应用服务。
@@ -235,25 +256,40 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
infra.repos.pigTradeRepo, infra.repos.pigTradeRepo,
infra.repos.notificationRepo, 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( deviceService := service.NewDeviceService(
logs.AddCompName(baseCtx, "DeviceService"), logs.AddCompName(baseCtx, "DeviceService"),
infra.repos.deviceRepo, infra.repos.deviceRepo,
infra.repos.areaControllerRepo, infra.repos.areaControllerRepo,
infra.repos.deviceTemplateRepo, infra.repos.deviceTemplateRepo,
domainServices.generalDeviceService, domainServices.generalDeviceService,
thresholdAlarmService,
) )
auditService := service.NewAuditService(logs.AddCompName(baseCtx, "AuditService"), infra.repos.userActionLogRepo) auditService := service.NewAuditService(logs.AddCompName(baseCtx, "AuditService"), infra.repos.userActionLogRepo)
planService := service.NewPlanService(logs.AddCompName(baseCtx, "AppPlanService"), domainServices.planService) 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{ return &AppServices{
pigFarmService: pigFarmService, pigFarmService: pigFarmService,
pigBatchService: pigBatchService, pigBatchService: pigBatchService,
monitorService: monitorService, monitorService: monitorService,
deviceService: deviceService, deviceService: deviceService,
auditService: auditService, auditService: auditService,
planService: planService, planService: planService,
userService: userService, userService: userService,
thresholdAlarmService: thresholdAlarmService,
} }
} }
@@ -353,7 +389,7 @@ func initNotifyService(
// 3. 动态确定首选通知器 // 3. 动态确定首选通知器
var primaryNotifier notify.Notifier var primaryNotifier notify.Notifier
primaryNotifierType := notify.NotifierType(cfg.Primary) primaryNotifierType := models.NotifierType(cfg.Primary)
// 检查用户指定的主渠道是否已启用 // 检查用户指定的主渠道是否已启用
for _, n := range availableNotifiers { for _, n := range availableNotifiers {

View File

@@ -4,16 +4,12 @@ import (
"context" "context"
"fmt" "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/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
) )
const (
// PlanNameTimedFullDataCollection 是定时全量数据采集计划的名称
PlanNameTimedFullDataCollection = "定时全量数据采集"
)
// initializeState 在应用启动时准备其初始数据状态。 // initializeState 在应用启动时准备其初始数据状态。
// 它遵循一个严格的顺序:清理 -> 更新 -> 刷新,以确保数据的一致性和正确性。 // 它遵循一个严格的顺序:清理 -> 更新 -> 刷新,以确保数据的一致性和正确性。
func (app *Application) initializeState(ctx context.Context) error { func (app *Application) initializeState(ctx context.Context) error {
@@ -48,13 +44,11 @@ func (app *Application) initializeState(ctx context.Context) error {
} }
// initializeSystemPlans 确保预定义的系统计划在数据库中存在并保持最新。 // initializeSystemPlans 确保预定义的系统计划在数据库中存在并保持最新。
// 它通过调用各个独立的计划初始化方法来完成此操作。
func (app *Application) initializeSystemPlans(ctx context.Context) error { func (app *Application) initializeSystemPlans(ctx context.Context) error {
appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializeSystemPlans") appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializeSystemPlans")
logger.Info("开始检查并更新预定义的系统计划...") logger.Info("开始检查并更新预定义的系统计划...")
// 动态构建预定义计划列表
predefinedSystemPlans := app.getPredefinedSystemPlans()
// 1. 获取所有已存在的系统计划 // 1. 获取所有已存在的系统计划
existingPlans, _, err := app.Infra.repos.planRepo.ListPlans(appCtx, repository.ListPlansOptions{ existingPlans, _, err := app.Infra.repos.planRepo.ListPlans(appCtx, repository.ListPlansOptions{
PlanType: repository.PlanTypeFilterSystem, PlanType: repository.PlanTypeFilterSystem,
@@ -64,54 +58,28 @@ func (app *Application) initializeSystemPlans(ctx context.Context) error {
} }
// 2. 为了方便查找, 将现有计划名放入一个 map // 2. 为了方便查找, 将现有计划名放入一个 map
existingPlanMap := make(map[string]*models.Plan) existingPlanMap := make(map[models.PlanName]*models.Plan)
for i := range existingPlans { for i := range existingPlans {
existingPlanMap[existingPlans[i].Name] = &existingPlans[i] existingPlanMap[existingPlans[i].Name] = &existingPlans[i]
} }
// 3. 遍历预定义的计划列表 // 3. 调用独立的初始化方法来处理每个系统计划
for i := range predefinedSystemPlans { if err := app.initializePeriodicSystemHealthCheckPlan(appCtx, existingPlanMap); err != nil {
predefinedPlan := &predefinedSystemPlans[i] // 获取可修改的指针 return err // 如果任何一个计划初始化失败,则立即返回错误
}
if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok { if err := app.initializeAlarmNotificationPlan(appCtx, existingPlanMap); err != nil {
// 如果计划存在,则进行无差别更新 return err
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)
}
}
} }
logger.Info("预定义系统计划检查完成。") logger.Info("预定义系统计划检查完成。")
return nil return nil
} }
// getPredefinedSystemPlans 返回一个基于当前配置的预定义系统计划列表 // initializePeriodicSystemHealthCheckPlan 负责初始化 "周期性系统健康检查" 计划
func (app *Application) getPredefinedSystemPlans() []models.Plan { // 它会根据当前配置动态构建计划,并决定是创建新计划还是更新现有计划。
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 interval := app.Config.Collection.Interval
@@ -119,25 +87,141 @@ func (app *Application) getPredefinedSystemPlans() []models.Plan {
interval = 1 // 确保间隔至少为1分钟 interval = 1 // 确保间隔至少为1分钟
} }
cronExpression := fmt.Sprintf("*/%d * * * *", interval) 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, PlanType: models.PlanTypeSystem,
ExecutionType: models.PlanExecutionTypeAutomatic, ExecutionType: models.PlanExecutionTypeAutomatic,
CronExpression: cronExpression, CronExpression: cronExpression,
Status: models.PlanStatusEnabled, Status: models.PlanStatusEnabled,
ContentType: models.PlanContentTypeTasks, 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{ Tasks: []models.Task{
{ {
Name: "全量采集", Name: "告警通知发送",
Description: "触发一次全量数据采集", Description: "发送所有待处理的告警通知",
ExecutionOrder: 1, 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 在应用启动时处理所有未完成的采集请求。 // initializePendingCollections 在应用启动时处理所有未完成的采集请求。
@@ -209,7 +293,7 @@ func (app *Application) cleanupStaleTasksAndLogs(ctx context.Context) error {
} }
// 2. 收集所有受影响的唯一 PlanID // 2. 收集所有受影响的唯一 PlanID
affectedPlanIDs := make(map[uint]struct{}) affectedPlanIDs := make(map[uint32]struct{})
for _, log := range incompletePlanLogs { for _, log := range incompletePlanLogs {
affectedPlanIDs[log.PlanID] = struct{}{} affectedPlanIDs[log.PlanID] = struct{}{}
} }

View 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
}

View File

@@ -28,7 +28,7 @@ type Service interface {
Switch(ctx context.Context, device *models.Device, action DeviceAction) error Switch(ctx context.Context, device *models.Device, action DeviceAction) error
// Collect 用于发起对指定区域主控下的多个设备的批量采集请求。 // Collect 用于发起对指定区域主控下的多个设备的批量采集请求。
Collect(ctx context.Context, regionalControllerID uint, devicesToCollect []*models.Device) error Collect(ctx context.Context, areaControllerID uint32, devicesToCollect []*models.Device) error
} }
// 设备操作指令通用结构(最外层) // 设备操作指令通用结构(最外层)

View File

@@ -133,7 +133,7 @@ func (g *GeneralDeviceService) Switch(ctx context.Context, device *models.Device
} }
// Collect 实现了 Service 接口,用于发起对指定区域主控下的多个设备的批量采集请求。 // 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") serviceCtx, logger := logs.Trace(ctx, g.ctx, "Collect")
if len(devicesToCollect) == 0 { if len(devicesToCollect) == 0 {
logger.Info("待采集设备列表为空,无需执行采集任务。") logger.Info("待采集设备列表为空,无需执行采集任务。")
@@ -141,16 +141,16 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, regionalControllerID
} }
// 1. 从设备列表中获取预加载的区域主控,并进行校验 // 1. 从设备列表中获取预加载的区域主控,并进行校验
regionalController := &devicesToCollect[0].AreaController areaController := &devicesToCollect[0].AreaController
if regionalController.ID != regionalControllerID { if areaController.ID != areaControllerID {
return fmt.Errorf("设备列表与指定的区域主控ID (%d) 不匹配", regionalControllerID) return fmt.Errorf("设备列表与指定的区域主控ID (%d) 不匹配", areaControllerID)
} }
if err := regionalController.SelfCheck(); err != nil { if err := areaController.SelfCheck(); err != nil {
return fmt.Errorf("区域主控 (ID: %d) 未通过自检: %w", regionalControllerID, err) return fmt.Errorf("区域主控 (ID: %d) 未通过自检: %w", areaControllerID, err)
} }
// 2. 准备采集任务列表 // 2. 准备采集任务列表
var childDeviceIDs []uint var childDeviceIDs []uint32
var collectTasks []*proto.CollectTask var collectTasks []*proto.CollectTask
for _, dev := range devicesToCollect { for _, dev := range devicesToCollect {
@@ -208,13 +208,13 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, regionalControllerID
} }
// 3. 构建并发送指令 // 3. 构建并发送指令
networkID := regionalController.NetworkID networkID := areaController.NetworkID
// 4. 创建待处理请求记录 // 4. 创建待处理请求记录
correlationID := uuid.New().String() correlationID := uuid.New().String()
pendingReq := &models.PendingCollection{ pendingReq := &models.PendingCollection{
CorrelationID: correlationID, CorrelationID: correlationID,
DeviceID: regionalController.ID, DeviceID: areaController.ID,
CommandMetadata: childDeviceIDs, CommandMetadata: childDeviceIDs,
Status: models.PendingStatusPending, Status: models.PendingStatusPending,
CreatedAt: time.Now(), CreatedAt: time.Now(),
@@ -223,7 +223,7 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, regionalControllerID
logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err) logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err)
return err return err
} }
logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, regionalController.ID) logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, areaController.ID)
// 5. 构建最终的空中载荷 // 5. 构建最终的空中载荷
batchCmd := &proto.BatchCollectCommand{ batchCmd := &proto.BatchCollectCommand{

View File

@@ -11,30 +11,28 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "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/notify"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"go.uber.org/zap"
) )
// Service 定义了通知领域的核心业务逻辑接口 // Service 定义了通知领域的核心业务逻辑接口
type Service interface { type Service interface {
// SendBatchAlarm 向一批用户发送告警通知。它会并发地为每个用户执行带故障转移的发送逻辑。 // SendBatchAlarm 向一批用户发送告警通知。它会并发地为每个用户执行带故障转移的发送逻辑。
SendBatchAlarm(ctx context.Context, userIDs []uint, content notify.AlarmContent) error SendBatchAlarm(ctx context.Context, userIDs []uint32, content notify.AlarmContent) error
// BroadcastAlarm 向所有用户发送告警通知。它会并发地为每个用户执行带故障转移的发送逻辑。 // BroadcastAlarm 向所有用户发送告警通知。它会并发地为每个用户执行带故障转移的发送逻辑。
BroadcastAlarm(ctx context.Context, content notify.AlarmContent) error BroadcastAlarm(ctx context.Context, content notify.AlarmContent) error
// SendTestMessage 向指定用户发送一条测试消息,用于手动验证特定通知渠道的配置。 // SendTestMessage 向指定用户发送一条测试消息,用于手动验证特定通知渠道的配置。
SendTestMessage(ctx context.Context, userID uint, notifierType notify.NotifierType) error SendTestMessage(ctx context.Context, userID uint32, notifierType models.NotifierType) error
} }
// failoverService 是 Service 接口的实现,提供了故障转移功能 // failoverService 是 Service 接口的实现,提供了故障转移功能
type failoverService struct { type failoverService struct {
ctx context.Context ctx context.Context
userRepo repository.UserRepository userRepo repository.UserRepository
notifiers map[notify.NotifierType]notify.Notifier notifiers map[models.NotifierType]notify.Notifier
primaryNotifier notify.Notifier primaryNotifier notify.Notifier
failureThreshold int 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 notificationRepo repository.NotificationRepository
} }
@@ -43,11 +41,11 @@ func NewFailoverService(
ctx context.Context, ctx context.Context,
userRepo repository.UserRepository, userRepo repository.UserRepository,
notifiers []notify.Notifier, notifiers []notify.Notifier,
primaryNotifierType notify.NotifierType, primaryNotifierType models.NotifierType,
failureThreshold int, failureThreshold int,
notificationRepo repository.NotificationRepository, notificationRepo repository.NotificationRepository,
) (Service, error) { ) (Service, error) {
notifierMap := make(map[notify.NotifierType]notify.Notifier) notifierMap := make(map[models.NotifierType]notify.Notifier)
for _, n := range notifiers { for _, n := range notifiers {
notifierMap[n.Type()] = n notifierMap[n.Type()] = n
} }
@@ -69,7 +67,7 @@ func NewFailoverService(
} }
// SendBatchAlarm 实现了向多个用户并发发送告警的功能 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendBatchAlarm")
var wg sync.WaitGroup var wg sync.WaitGroup
var mu sync.Mutex var mu sync.Mutex
@@ -79,7 +77,7 @@ func (s *failoverService) SendBatchAlarm(ctx context.Context, userIDs []uint, co
for _, userID := range userIDs { for _, userID := range userIDs {
wg.Add(1) wg.Add(1)
go func(id uint) { go func(id uint32) {
defer wg.Done() defer wg.Done()
if err := s.sendAlarmToUser(serviceCtx, id, content); err != nil { if err := s.sendAlarmToUser(serviceCtx, id, content); err != nil {
mu.Lock() mu.Lock()
@@ -110,7 +108,7 @@ func (s *failoverService) BroadcastAlarm(ctx context.Context, content notify.Ala
return fmt.Errorf("广播告警失败:查找所有用户时出错: %w", err) return fmt.Errorf("广播告警失败:查找所有用户时出错: %w", err)
} }
var userIDs []uint var userIDs []uint32
for _, user := range users { for _, user := range users {
userIDs = append(userIDs, user.ID) userIDs = append(userIDs, user.ID)
} }
@@ -121,7 +119,7 @@ func (s *failoverService) BroadcastAlarm(ctx context.Context, content notify.Ala
} }
// sendAlarmToUser 是为单个用户发送告警的内部方法,包含了完整的故障转移逻辑 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "sendAlarmToUser")
user, err := s.userRepo.FindByID(serviceCtx, userID) user, err := s.userRepo.FindByID(serviceCtx, userID)
if err != nil { if err != nil {
@@ -189,7 +187,7 @@ func (s *failoverService) sendAlarmToUser(ctx context.Context, userID uint, cont
} }
// SendTestMessage 实现了手动发送测试消息的功能 // 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") serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendTestMessage")
user, err := s.userRepo.FindByID(serviceCtx, userID) user, err := s.userRepo.FindByID(serviceCtx, userID)
if err != nil { if err != nil {
@@ -210,7 +208,7 @@ func (s *failoverService) SendTestMessage(ctx context.Context, userID uint, noti
s.recordNotificationAttempt(serviceCtx, userID, notifierType, notify.AlarmContent{ s.recordNotificationAttempt(serviceCtx, userID, notifierType, notify.AlarmContent{
Title: "通知服务测试", Title: "通知服务测试",
Message: fmt.Sprintf("这是一条来自【%s】渠道的测试消息。如果您收到此消息说明您的配置正确。", notifierType), Message: fmt.Sprintf("这是一条来自【%s】渠道的测试消息。如果您收到此消息说明您的配置正确。", notifierType),
Level: zap.InfoLevel, Level: models.InfoLevel,
Timestamp: time.Now(), Timestamp: time.Now(),
}, "", models.NotificationStatusFailed, fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifierType)) }, "", models.NotificationStatusFailed, fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifierType))
return 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{ testContent := notify.AlarmContent{
Title: "通知服务测试", Title: "通知服务测试",
Message: fmt.Sprintf("这是一条来自【%s】渠道的测试消息。如果您收到此消息说明您的配置正确。", notifierType), Message: fmt.Sprintf("这是一条来自【%s】渠道的测试消息。如果您收到此消息说明您的配置正确。", notifierType),
Level: zap.InfoLevel, Level: models.InfoLevel,
Timestamp: time.Now(), Timestamp: time.Now(),
} }
@@ -239,15 +237,15 @@ func (s *failoverService) SendTestMessage(ctx context.Context, userID uint, noti
} }
// getAddressForNotifier 是一个辅助函数,根据通知器类型从 ContactInfo 中获取对应的地址 // getAddressForNotifier 是一个辅助函数,根据通知器类型从 ContactInfo 中获取对应的地址
func getAddressForNotifier(notifierType notify.NotifierType, contact models.ContactInfo) string { func getAddressForNotifier(notifierType models.NotifierType, contact models.ContactInfo) string {
switch notifierType { switch notifierType {
case notify.NotifierTypeSMTP: case models.NotifierTypeSMTP:
return contact.Email return contact.Email
case notify.NotifierTypeWeChat: case models.NotifierTypeWeChat:
return contact.WeChat return contact.WeChat
case notify.NotifierTypeLark: case models.NotifierTypeLark:
return contact.Feishu return contact.Feishu
case notify.NotifierTypeLog: case models.NotifierTypeLog:
return "log" // LogNotifier不需要具体的地址但为了函数签名一致性返回一个无意义的非空字符串以绕过配置存在检查 return "log" // LogNotifier不需要具体的地址但为了函数签名一致性返回一个无意义的非空字符串以绕过配置存在检查
default: default:
return "" return ""
@@ -263,8 +261,8 @@ func getAddressForNotifier(notifierType notify.NotifierType, contact models.Cont
// err: 如果发送失败,记录的错误信息 // err: 如果发送失败,记录的错误信息
func (s *failoverService) recordNotificationAttempt( func (s *failoverService) recordNotificationAttempt(
ctx context.Context, ctx context.Context,
userID uint, userID uint32,
notifierType notify.NotifierType, notifierType models.NotifierType,
content notify.AlarmContent, content notify.AlarmContent,
toAddress string, toAddress string,
status models.NotificationStatus, status models.NotificationStatus,
@@ -281,7 +279,7 @@ func (s *failoverService) recordNotificationAttempt(
UserID: userID, UserID: userID,
Title: content.Title, Title: content.Title,
Message: content.Message, Message: content.Message,
Level: models.LogLevel(content.Level), Level: content.Level,
AlarmTimestamp: content.Timestamp, AlarmTimestamp: content.Timestamp,
ToAddress: toAddress, ToAddress: toAddress,
Status: status, Status: status,

View File

@@ -19,22 +19,22 @@ type PigPenTransferManager interface {
LogTransfer(ctx context.Context, tx *gorm.DB, log *models.PigTransferLog) error LogTransfer(ctx context.Context, tx *gorm.DB, log *models.PigTransferLog) error
// GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。 // 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 获取一个猪群当前关联的所有猪栏。
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 更新一个猪栏的指定字段。
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 通过汇总猪只迁移日志,计算给定猪栏中的当前猪只数量。
GetCurrentPigsInPen(ctx context.Context, tx *gorm.DB, penID uint) (int, error) GetCurrentPigsInPen(ctx context.Context, tx *gorm.DB, penID uint32) (int, error)
// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数 // 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 将猪栏的猪群归属移除,并将其状态标记为空闲。
ReleasePen(ctx context.Context, tx *gorm.DB, penID uint) error ReleasePen(ctx context.Context, tx *gorm.DB, penID uint32) error
} }
// pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。 // pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。
@@ -63,25 +63,25 @@ func (s *pigPenTransferManager) LogTransfer(ctx context.Context, tx *gorm.DB, lo
} }
// GetPenByID 实现了获取猪栏信息的逻辑。 // 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") managerCtx := logs.AddFuncName(ctx, s.ctx, "GetPenByID")
return s.penRepo.GetPenByIDTx(managerCtx, tx, penID) return s.penRepo.GetPenByIDTx(managerCtx, tx, penID)
} }
// GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。 // 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") managerCtx := logs.AddFuncName(ctx, s.ctx, "GetPensByBatchID")
return s.penRepo.GetPensByBatchIDTx(managerCtx, tx, batchID) return s.penRepo.GetPensByBatchIDTx(managerCtx, tx, batchID)
} }
// UpdatePenFields 实现了更新猪栏字段的逻辑。 // 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") managerCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePenFields")
return s.penRepo.UpdatePenFieldsTx(managerCtx, tx, penID, updates) return s.penRepo.UpdatePenFieldsTx(managerCtx, tx, penID, updates)
} }
// GetCurrentPigsInPen 实现了计算猪栏当前猪只数量的逻辑。 // 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") managerCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigsInPen")
// 1. 通过猪栏ID查出所属猪群信息 // 1. 通过猪栏ID查出所属猪群信息
pen, err := s.penRepo.GetPenByIDTx(managerCtx, tx, penID) pen, err := s.penRepo.GetPenByIDTx(managerCtx, tx, penID)
@@ -137,7 +137,7 @@ func (s *pigPenTransferManager) GetCurrentPigsInPen(ctx context.Context, tx *gor
// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数 // GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
// 该方法通过遍历猪群下的每个猪栏,并调用 GetCurrentPigsInPen 来累加存栏数。 // 该方法通过遍历猪群下的每个猪栏,并调用 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") managerCtx := logs.AddFuncName(ctx, s.ctx, "GetTotalPigsInPensForBatchTx")
// 1. 获取该批次下所有猪栏的列表 // 1. 获取该批次下所有猪栏的列表
pensInBatch, err := s.GetPensByBatchID(managerCtx, tx, batchID) pensInBatch, err := s.GetPensByBatchID(managerCtx, tx, batchID)
@@ -160,7 +160,7 @@ func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(ctx context.Context
// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。 // 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") managerCtx := logs.AddFuncName(ctx, s.ctx, "ReleasePen")
// 1. 获取猪栏信息 // 1. 获取猪栏信息
pen, err := s.penRepo.GetPenByIDTx(managerCtx, tx, penID) pen, err := s.penRepo.GetPenByIDTx(managerCtx, tx, penID)

View File

@@ -38,58 +38,58 @@ var (
// 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。 // 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。
type PigBatchService interface { type PigBatchService interface {
// CreatePigBatch 创建猪批次,并记录初始日志。 // 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 获取单个猪批次。
GetPigBatch(ctx context.Context, id uint) (*models.PigBatch, error) GetPigBatch(ctx context.Context, id uint32) (*models.PigBatch, error)
// UpdatePigBatch 更新猪批次信息。 // UpdatePigBatch 更新猪批次信息。
UpdatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, error) UpdatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, error)
// DeletePigBatch 删除猪批次,包含业务规则校验。 // DeletePigBatch 删除猪批次,包含业务规则校验。
DeletePigBatch(ctx context.Context, id uint) error DeletePigBatch(ctx context.Context, id uint32) error
// ListPigBatches 批量查询猪批次。 // ListPigBatches 批量查询猪批次。
ListPigBatches(ctx context.Context, isActive *bool) ([]*models.PigBatch, error) ListPigBatches(ctx context.Context, isActive *bool) ([]*models.PigBatch, error)
// AssignEmptyPensToBatch 为猪群分配空栏 // 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 将猪只从“虚拟库存”移入指定猪栏
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 连猪带栏,整体划拨到另一个猪群
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 将一个猪栏移除出猪群,此方法需要在猪栏为空的情况下执行。
RemoveEmptyPenFromBatch(ctx context.Context, batchID uint, penID uint) error RemoveEmptyPenFromBatch(ctx context.Context, batchID uint32, penID uint32) error
// GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。 // GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。
GetCurrentPigQuantity(ctx context.Context, batchID uint) (int, error) GetCurrentPigQuantity(ctx context.Context, batchID uint32) (int, error)
// GetCurrentPigsInPen 获取指定猪栏的当前存栏量。 // GetCurrentPigsInPen 获取指定猪栏的当前存栏量。
GetCurrentPigsInPen(ctx context.Context, penID uint) (int, error) GetCurrentPigsInPen(ctx context.Context, penID uint32) (int, error)
// GetTotalPigsInPensForBatch 获取指定猪群下所有猪栏的当前总存栏数 // 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 处理卖猪的业务逻辑。
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 处理买猪的业务逻辑。
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 TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint32, destBatchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error
TransferPigsWithinBatch(ctx context.Context, batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error TransferPigsWithinBatch(ctx context.Context, batchID uint32, fromPenID uint32, toPenID uint32, quantity uint32, operatorID uint32, remarks string) error
// --- 病猪管理相关方法 --- // --- 病猪管理相关方法 ---
// RecordSickPigs 记录新增病猪事件。 // 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 记录病猪康复事件。
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 记录病猪死亡事件。
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 记录病猪淘汰事件。
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 记录正常猪只死亡事件。
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 记录正常猪只淘汰事件。
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 接口的具体实现。 // pigBatchService 是 PigBatchService 接口的具体实现。

View File

@@ -15,7 +15,7 @@ import (
// --- 领域服务实现 --- // --- 领域服务实现 ---
// CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigBatch")
// 业务规则可以在这里添加,例如检查批次号是否唯一等 // 业务规则可以在这里添加,例如检查批次号是否唯一等
@@ -57,7 +57,7 @@ func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint, b
} }
// GetPigBatch 实现了获取单个猪批次的逻辑。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigBatch")
batch, err := s.pigBatchRepo.GetPigBatchByID(serviceCtx, id) batch, err := s.pigBatchRepo.GetPigBatchByID(serviceCtx, id)
if err != nil { if err != nil {
@@ -84,7 +84,7 @@ func (s *pigBatchService) UpdatePigBatch(ctx context.Context, batch *models.PigB
} }
// DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigBatch")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 获取猪批次信息 // 1. 获取猪批次信息
@@ -135,7 +135,7 @@ func (s *pigBatchService) ListPigBatches(ctx context.Context, isActive *bool) ([
} }
// GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigQuantity")
var getErr error var getErr error
var quantity int var quantity int
@@ -150,7 +150,7 @@ func (s *pigBatchService) GetCurrentPigQuantity(ctx context.Context, batchID uin
} }
// getCurrentPigQuantityTx 实现了获取指定猪批次的当前猪只数量的逻辑。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "getCurrentPigQuantityTx")
// 1. 获取猪批次初始信息 // 1. 获取猪批次初始信息
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID) 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 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigBatchQuantity")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
return s.updatePigBatchQuantityTx(serviceCtx, tx, operatorID, batchID, changeType, changeAmount, changeReason, happenedAt) 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "updatePigBatchQuantityTx")
lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(serviceCtx, tx, batchID) lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(serviceCtx, tx, batchID)
if err != nil { if err != nil {

View File

@@ -14,7 +14,7 @@ import (
) )
// executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "executeTransferAndLog")
// 通用校验:任何调出操作都不能超过源猪栏的当前存栏数 // 通用校验:任何调出操作都不能超过源猪栏的当前存栏数
@@ -67,7 +67,7 @@ func (s *pigBatchService) executeTransferAndLog(ctx context.Context, tx *gorm.DB
} }
// TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "TransferPigsWithinBatch")
if fromPenID == toPenID { if fromPenID == toPenID {
return errors.New("源猪栏和目标猪栏不能相同") return errors.New("源猪栏和目标猪栏不能相同")
@@ -106,7 +106,7 @@ func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID u
} }
// TransferPigsAcrossBatches 实现了跨猪群的调栏业务。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "TransferPigsAcrossBatches")
if sourceBatchID == destBatchID { if sourceBatchID == destBatchID {
return errors.New("源猪群和目标猪群不能相同") return errors.New("源猪群和目标猪群不能相同")
@@ -167,7 +167,7 @@ func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceB
} }
// AssignEmptyPensToBatch 为猪群分配空栏 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "AssignEmptyPensToBatch")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 验证猪批次是否存在且活跃 // 1. 验证猪批次是否存在且活跃
@@ -204,6 +204,7 @@ func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID ui
updates := map[string]interface{}{ updates := map[string]interface{}{
"pig_batch_id": &batchID, "pig_batch_id": &batchID,
"status": models.PenStatusOccupied, "status": models.PenStatusOccupied,
"operator_id": operatorID,
} }
if err := s.transferSvc.UpdatePenFields(serviceCtx, tx, penID, updates); err != nil { if err := s.transferSvc.UpdatePenFields(serviceCtx, tx, penID, updates); err != nil {
return fmt.Errorf("分配猪栏 %d 失败: %w", penID, err) return fmt.Errorf("分配猪栏 %d 失败: %w", penID, err)
@@ -215,7 +216,7 @@ func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID ui
} }
// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "MovePigsIntoPen")
if quantity <= 0 { if quantity <= 0 {
return errors.New("迁移数量必须大于零") return errors.New("迁移数量必须大于零")
@@ -287,7 +288,7 @@ func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint, toP
} }
// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "ReclassifyPenToNewBatch")
if fromBatchID == toBatchID { if fromBatchID == toBatchID {
return errors.New("源猪群和目标猪群不能相同") 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "RemoveEmptyPenFromBatch")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 检查猪批次是否存在且活跃 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigsInPen")
var currentPigs int var currentPigs int
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
@@ -453,7 +454,7 @@ func (s *pigBatchService) GetCurrentPigsInPen(ctx context.Context, penID uint) (
} }
// GetTotalPigsInPensForBatch 实现了获取指定猪群下所有猪栏的当前总存栏数的逻辑。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetTotalPigsInPensForBatch")
var totalPigs int var totalPigs int
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {

View File

@@ -13,7 +13,7 @@ import (
) )
// RecordSickPigs 记录新增病猪事件。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordSickPigs")
if quantity <= 0 { if quantity <= 0 {
return errors.New("新增病猪数量必须大于0") return errors.New("新增病猪数量必须大于0")
@@ -89,7 +89,7 @@ func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint, b
} }
// RecordSickPigRecovery 记录病猪康复事件。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordSickPigRecovery")
if quantity <= 0 { if quantity <= 0 {
return errors.New("康复猪只数量必须大于0") return errors.New("康复猪只数量必须大于0")
@@ -158,7 +158,7 @@ func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID
} }
// RecordSickPigDeath 记录病猪死亡事件。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordSickPigDeath")
if quantity <= 0 { if quantity <= 0 {
return errors.New("死亡猪只数量必须大于0") return errors.New("死亡猪只数量必须大于0")
@@ -254,7 +254,7 @@ func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uin
} }
// RecordSickPigCull 记录病猪淘汰事件。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordSickPigCull")
if quantity <= 0 { if quantity <= 0 {
return errors.New("淘汰猪只数量必须大于0") return errors.New("淘汰猪只数量必须大于0")
@@ -350,7 +350,7 @@ func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint
} }
// RecordDeath 记录正常猪只死亡事件。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordDeath")
if quantity <= 0 { if quantity <= 0 {
return errors.New("死亡猪只数量必须大于0") return errors.New("死亡猪只数量必须大于0")
@@ -421,7 +421,7 @@ func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint, batc
} }
// RecordCull 记录正常猪只淘汰事件。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordCull")
if quantity <= 0 { if quantity <= 0 {
return errors.New("淘汰猪只数量必须大于0") return errors.New("淘汰猪只数量必须大于0")

View File

@@ -13,7 +13,7 @@ import (
) )
// SellPigs 处理批量销售猪的业务逻辑。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "SellPigs")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
if quantity <= 0 { if quantity <= 0 {
@@ -85,7 +85,7 @@ func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint, penID uint
} }
// BuyPigs 处理批量购买猪的业务逻辑。 // 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") serviceCtx := logs.AddFuncName(ctx, s.ctx, "BuyPigs")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error { return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
if quantity <= 0 { if quantity <= 0 {

View File

@@ -21,7 +21,7 @@ type SickPigManager interface {
ProcessSickPigLog(ctx context.Context, tx *gorm.DB, log *models.PigSickLog) error ProcessSickPigLog(ctx context.Context, tx *gorm.DB, log *models.PigSickLog) error
// GetCurrentSickPigCount 获取指定批次当前患病猪只的总数 // 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 接口的具体实现。 // sickPigManager 是 SickPigManager 接口的具体实现。
@@ -122,7 +122,7 @@ func (s *sickPigManager) ProcessSickPigLog(ctx context.Context, tx *gorm.DB, log
return nil 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") managerCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentSickPigCount")
lastLog, err := s.sickLogRepo.GetLastLogByBatchTx(managerCtx, tx, batchID) lastLog, err := s.sickLogRepo.GetLastLogByBatchTx(managerCtx, tx, batchID)
if err != nil { if err != nil {

View File

@@ -18,10 +18,10 @@ type AnalysisPlanTaskManager interface {
Refresh(ctx context.Context) error Refresh(ctx context.Context) error
// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。 // CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。
// 如果触发器已存在,会根据计划类型更新其执行时间。 // 如果触发器已存在,会根据计划类型更新其执行时间。
CreateOrUpdateTrigger(ctx context.Context, planID uint) error CreateOrUpdateTrigger(ctx context.Context, planID uint32) error
// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。 // EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。
// 如果不存在,则会自动创建。此方法不涉及待执行队列。 // 如果不存在,则会自动创建。此方法不涉及待执行队列。
EnsureAnalysisTaskDefinition(ctx context.Context, planID uint) error EnsureAnalysisTaskDefinition(ctx context.Context, planID uint32) error
} }
// analysisPlanTaskManagerImpl 负责管理分析计划的触发器任务。 // analysisPlanTaskManagerImpl 负责管理分析计划的触发器任务。
@@ -82,7 +82,7 @@ func (m *analysisPlanTaskManagerImpl) Refresh(ctx context.Context) error {
// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。 // 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") managerCtx, logger := logs.Trace(ctx, m.ctx, "CreateOrUpdateTrigger")
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@@ -138,7 +138,7 @@ func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(ctx context.Context,
// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。 // 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") managerCtx, logger := logs.Trace(ctx, m.ctx, "EnsureAnalysisTaskDefinition")
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@@ -170,7 +170,7 @@ func (m *analysisPlanTaskManagerImpl) EnsureAnalysisTaskDefinition(ctx context.C
// --- 内部私有方法 --- // --- 内部私有方法 ---
// getRefreshData 从数据库获取刷新所需的所有数据。 // 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") managerCtx, logger := logs.Trace(ctx, m.ctx, "getRefreshData")
runnablePlans, err = m.planRepo.FindRunnablePlans(managerCtx) runnablePlans, err = m.planRepo.FindRunnablePlans(managerCtx)
if err != nil { if err != nil {
@@ -183,7 +183,7 @@ func (m *analysisPlanTaskManagerImpl) getRefreshData(ctx context.Context) (runna
logger.Errorf("获取失效计划列表失败: %v", err) logger.Errorf("获取失效计划列表失败: %v", err)
return return
} }
invalidPlanIDs = make([]uint, len(invalidPlans)) invalidPlanIDs = make([]uint32, len(invalidPlans))
for i, p := range invalidPlans { for i, p := range invalidPlans {
invalidPlanIDs[i] = p.ID invalidPlanIDs[i] = p.ID
} }
@@ -197,19 +197,19 @@ func (m *analysisPlanTaskManagerImpl) getRefreshData(ctx context.Context) (runna
} }
// cleanupInvalidTasks 清理所有与失效计划相关的待执行任务。 // 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") managerCtx, logger := logs.Trace(ctx, m.ctx, "cleanupInvalidTasks")
if len(invalidPlanIDs) == 0 { if len(invalidPlanIDs) == 0 {
return nil // 没有需要清理的计划 return nil // 没有需要清理的计划
} }
invalidPlanIDSet := make(map[uint]struct{}, len(invalidPlanIDs)) invalidPlanIDSet := make(map[uint32]struct{}, len(invalidPlanIDs))
for _, id := range invalidPlanIDs { for _, id := range invalidPlanIDs {
invalidPlanIDSet[id] = struct{}{} invalidPlanIDSet[id] = struct{}{}
} }
var tasksToDeleteIDs []uint var tasksToDeleteIDs []uint32
var logsToCancelIDs []uint var logsToCancelIDs []uint32
for _, pt := range allPendingTasks { for _, pt := range allPendingTasks {
if pt.Task == nil { // 防御性编程,确保 Task 被预加载 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 { func (m *analysisPlanTaskManagerImpl) addOrUpdateTriggers(ctx context.Context, runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error {
managerCtx, logger := logs.Trace(ctx, m.ctx, "addOrUpdateTriggers") managerCtx, logger := logs.Trace(ctx, m.ctx, "addOrUpdateTriggers")
// 创建一个映射,存放所有已在队列中的计划触发器 // 创建一个映射,存放所有已在队列中的计划触发器
pendingTriggersMap := make(map[uint]models.PendingTask) pendingTriggersMap := make(map[uint32]models.PendingTask)
for _, pt := range allPendingTasks { for _, pt := range allPendingTasks {
if pt.Task != nil && pt.Task.Type == models.TaskPlanAnalysis { if pt.Task != nil && pt.Task.Type == models.TaskPlanAnalysis {
pendingTriggersMap[pt.Task.PlanID] = pt pendingTriggersMap[pt.Task.PlanID] = pt

View File

@@ -25,21 +25,21 @@ type ExecutionManager interface {
// ProgressTracker 仅用于在内存中提供计划执行的并发锁 // ProgressTracker 仅用于在内存中提供计划执行的并发锁
type ProgressTracker struct { type ProgressTracker struct {
mu sync.Mutex mu sync.Mutex
cond *sync.Cond // 用于实现阻塞锁 cond *sync.Cond // 用于实现阻塞锁
runningPlans map[uint]bool // key: planExecutionLogID, value: true (用作内存锁) runningPlans map[uint32]bool // key: planExecutionLogID, value: true (用作内存锁)
} }
// NewProgressTracker 创建一个新的进度跟踪器 // NewProgressTracker 创建一个新的进度跟踪器
func NewProgressTracker() *ProgressTracker { func NewProgressTracker() *ProgressTracker {
t := &ProgressTracker{ t := &ProgressTracker{
runningPlans: make(map[uint]bool), runningPlans: make(map[uint32]bool),
} }
t.cond = sync.NewCond(&t.mu) t.cond = sync.NewCond(&t.mu)
return t return t
} }
// TryLock (非阻塞) 尝试锁定一个计划。如果计划未被锁定,则锁定并返回 true。 // TryLock (非阻塞) 尝试锁定一个计划。如果计划未被锁定,则锁定并返回 true。
func (t *ProgressTracker) TryLock(planLogID uint) bool { func (t *ProgressTracker) TryLock(planLogID uint32) bool {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
if t.runningPlans[planLogID] { if t.runningPlans[planLogID] {
@@ -50,7 +50,7 @@ func (t *ProgressTracker) TryLock(planLogID uint) bool {
} }
// Lock (阻塞) 获取一个计划的执行锁。如果锁已被占用,则会一直等待直到锁被释放。 // Lock (阻塞) 获取一个计划的执行锁。如果锁已被占用,则会一直等待直到锁被释放。
func (t *ProgressTracker) Lock(planLogID uint) { func (t *ProgressTracker) Lock(planLogID uint32) {
t.mu.Lock() t.mu.Lock()
// 当计划正在运行时,调用 t.cond.Wait() 会原子地解锁 mu 并挂起当前协程。 // 当计划正在运行时,调用 t.cond.Wait() 会原子地解锁 mu 并挂起当前协程。
// 当被唤醒时,它会重新锁定 mu 并再次检查循环条件。 // 当被唤醒时,它会重新锁定 mu 并再次检查循环条件。
@@ -63,7 +63,7 @@ func (t *ProgressTracker) Lock(planLogID uint) {
} }
// Unlock 解锁一个计划,并唤醒所有正在等待此锁的协程。 // Unlock 解锁一个计划,并唤醒所有正在等待此锁的协程。
func (t *ProgressTracker) Unlock(planLogID uint) { func (t *ProgressTracker) Unlock(planLogID uint32) {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
delete(t.runningPlans, planLogID) delete(t.runningPlans, planLogID)
@@ -72,10 +72,10 @@ func (t *ProgressTracker) Unlock(planLogID uint) {
} }
// GetRunningPlanIDs 获取当前所有正在执行的计划ID列表 // GetRunningPlanIDs 获取当前所有正在执行的计划ID列表
func (t *ProgressTracker) GetRunningPlanIDs() []uint { func (t *ProgressTracker) GetRunningPlanIDs() []uint32 {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
ids := make([]uint, 0, len(t.runningPlans)) ids := make([]uint32, 0, len(t.runningPlans))
for id := range t.runningPlans { for id := range t.runningPlans {
ids = append(ids, id) ids = append(ids, id)
} }
@@ -214,7 +214,7 @@ func (s *planExecutionManagerImpl) claimAndSubmit(ctx context.Context) {
} }
// handleRequeue 同步地、安全地将一个无法立即执行的任务放回队列。 // 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") managerCtx, logger := logs.Trace(ctx, s.ctx, "handleRequeue")
logger.Warnf("计划 %d 正在执行,任务 %d (TaskID: %d) 将等待并重新入队...", planExecutionLogID, taskToRequeue.ID, taskToRequeue.TaskID) logger.Warnf("计划 %d 正在执行,任务 %d (TaskID: %d) 将等待并重新入队...", planExecutionLogID, taskToRequeue.ID, taskToRequeue.TaskID)
@@ -308,7 +308,7 @@ func (s *planExecutionManagerImpl) analysisPlan(ctx context.Context, claimedLog
// 创建Plan执行记录 // 创建Plan执行记录
// 从任务的 Parameters 中解析出真实的 PlanID // 从任务的 Parameters 中解析出真实的 PlanID
var params struct { var params struct {
PlanID uint `json:"plan_id"` PlanID uint32 `json:"plan_id"`
} }
if err := claimedLog.Task.ParseParameters(&params); err != nil { if err := claimedLog.Task.ParseParameters(&params); err != nil {
logger.Errorf("解析任务参数中的计划ID失败日志ID: %d, 错误: %v", claimedLog.ID, err) logger.Errorf("解析任务参数中的计划ID失败日志ID: %d, 错误: %v", claimedLog.ID, err)
@@ -390,7 +390,7 @@ func (s *planExecutionManagerImpl) updateTaskExecutionLogStatus(ctx context.Cont
} }
// handlePlanTermination 集中处理计划的终止逻辑(失败或取消) // 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") managerCtx, logger := logs.Trace(ctx, s.ctx, "handlePlanTermination")
// 1. 从待执行队列中删除所有相关的子任务 // 1. 从待执行队列中删除所有相关的子任务
if err := s.pendingTaskRepo.DeletePendingTasksByPlanLogID(managerCtx, planLogID); err != nil { if err := s.pendingTaskRepo.DeletePendingTasksByPlanLogID(managerCtx, planLogID); err != nil {
@@ -434,7 +434,7 @@ func (s *planExecutionManagerImpl) handlePlanTermination(ctx context.Context, pl
} }
// handlePlanCompletion 集中处理计划成功完成后的所有逻辑 // 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") managerCtx, logger := logs.Trace(ctx, s.ctx, "handlePlanCompletion")
logger.Infof("计划执行 %d 的所有任务已完成,开始处理计划完成逻辑...", planLogID) logger.Infof("计划执行 %d 的所有任务已完成,开始处理计划完成逻辑...", planLogID)

View File

@@ -41,17 +41,17 @@ type Service interface {
// CreatePlan 创建一个新的计划 // CreatePlan 创建一个新的计划
CreatePlan(ctx context.Context, plan *models.Plan) (*models.Plan, error) CreatePlan(ctx context.Context, plan *models.Plan) (*models.Plan, error)
// GetPlanByID 根据ID获取计划详情 // GetPlanByID 根据ID获取计划详情
GetPlanByID(ctx context.Context, id uint) (*models.Plan, error) GetPlanByID(ctx context.Context, id uint32) (*models.Plan, error)
// ListPlans 获取计划列表,支持过滤和分页 // ListPlans 获取计划列表,支持过滤和分页
ListPlans(ctx context.Context, opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) ListPlans(ctx context.Context, opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)
// UpdatePlan 更新计划 // UpdatePlan 更新计划, wantPlanType 表示期望被修改的计划是什么类型
UpdatePlan(ctx context.Context, plan *models.Plan) (*models.Plan, error) UpdatePlan(ctx context.Context, plan *models.Plan, wantPlanType models.PlanType) (*models.Plan, error)
// DeletePlan 删除计划(软删除) // DeletePlan 删除计划(软删除)
DeletePlan(ctx context.Context, id uint) error DeletePlan(ctx context.Context, id uint32) error
// StartPlan 启动计划 // StartPlan 启动计划
StartPlan(ctx context.Context, id uint) error StartPlan(ctx context.Context, id uint32) error
// StopPlan 停止计划 // StopPlan 停止计划
StopPlan(ctx context.Context, id uint) error StopPlan(ctx context.Context, id uint32) error
} }
// planServiceImpl 是 Service 接口的具体实现。 // planServiceImpl 是 Service 接口的具体实现。
@@ -150,7 +150,7 @@ func (s *planServiceImpl) CreatePlan(ctx context.Context, planToCreate *models.P
// 优化无需查询完整的设备对象只需构建包含ID的结构体即可建立关联 // 优化无需查询完整的设备对象只需构建包含ID的结构体即可建立关联
devices := make([]models.Device, len(deviceIDs)) devices := make([]models.Device, len(deviceIDs))
for i, id := range 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 taskModel.Devices = devices
} }
@@ -174,7 +174,7 @@ func (s *planServiceImpl) CreatePlan(ctx context.Context, planToCreate *models.P
} }
// GetPlanByID 根据ID获取计划详情 // 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") planCtx, logger := logs.Trace(ctx, s.ctx, "GetPlanByID")
const actionType = "领域层:获取计划详情" const actionType = "领域层:获取计划详情"
@@ -207,8 +207,8 @@ func (s *planServiceImpl) ListPlans(ctx context.Context, opts repository.ListPla
return plans, total, nil return plans, total, nil
} }
// UpdatePlan 更新计划 // UpdatePlan 更新计划, wantPlanType 表示期望被修改的计划是什么类型
func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.Plan) (*models.Plan, error) { func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.Plan, wantPlanType models.PlanType) (*models.Plan, error) {
planCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePlan") planCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePlan")
const actionType = "领域层:更新计划" const actionType = "领域层:更新计划"
@@ -222,9 +222,8 @@ func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.P
return nil, err return nil, err
} }
// 系统计划不允许修改 if existingPlan.PlanType != wantPlanType {
if existingPlan.PlanType == models.PlanTypeSystem { logger.Warnf("%s: 禁止修改 %v 类型计划, ID: %d", actionType, wantPlanType, planToUpdate.ID)
logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, planToUpdate.ID)
return nil, ErrPlanCannotBeModified return nil, ErrPlanCannotBeModified
} }
@@ -263,7 +262,7 @@ func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.P
// 优化无需查询完整的设备对象只需构建包含ID的结构体即可建立关联 // 优化无需查询完整的设备对象只需构建包含ID的结构体即可建立关联
devices := make([]models.Device, len(deviceIDs)) devices := make([]models.Device, len(deviceIDs))
for i, id := range 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 taskModel.Devices = devices
} }
@@ -291,7 +290,7 @@ func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.P
} }
// DeletePlan 删除计划(软删除) // 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") planCtx, logger := logs.Trace(ctx, s.ctx, "DeletePlan")
const actionType = "领域层:删除计划" const actionType = "领域层:删除计划"
@@ -329,7 +328,7 @@ func (s *planServiceImpl) DeletePlan(ctx context.Context, id uint) error {
} }
// StartPlan 启动计划 // 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") planCtx, logger := logs.Trace(ctx, s.ctx, "StartPlan")
const actionType = "领域层:启动计划" const actionType = "领域层:启动计划"
@@ -384,7 +383,7 @@ func (s *planServiceImpl) StartPlan(ctx context.Context, id uint) error {
} }
// StopPlan 停止计划 // 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") planCtx, logger := logs.Trace(ctx, s.ctx, "StopPlan")
const actionType = "领域层:停止计划" const actionType = "领域层:停止计划"

View File

@@ -25,8 +25,8 @@ type Task interface {
// TaskDeviceIDResolver 定义了从任务配置中解析设备ID的方法 // TaskDeviceIDResolver 定义了从任务配置中解析设备ID的方法
type TaskDeviceIDResolver interface { type TaskDeviceIDResolver interface {
// ResolveDeviceIDs 从任务配置中解析并返回所有关联的设备ID列表 // ResolveDeviceIDs 从任务配置中解析并返回所有关联的设备ID列表
// 返回值: uint数组每个字符串代表一个设备ID // 返回值: uint32数组每个字符串代表一个设备ID
ResolveDeviceIDs(ctx context.Context) ([]uint, error) ResolveDeviceIDs(ctx context.Context) ([]uint32, error)
} }
// TaskFactory 是一个工厂接口,用于根据任务执行日志创建任务实例。 // TaskFactory 是一个工厂接口,用于根据任务执行日志创建任务实例。

View 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(&params)
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
}

View 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(&params)
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
}

View File

@@ -11,7 +11,7 @@ import (
) )
type DelayTaskParams struct { type DelayTaskParams struct {
DelayDuration float64 `json:"delay_duration"` DelayDuration float32 `json:"delay_duration"`
} }
// DelayTask 是一个用于模拟延迟的 Task 实现 // DelayTask 是一个用于模拟延迟的 Task 实现
@@ -70,6 +70,6 @@ func (d *DelayTask) OnFailure(ctx context.Context, executeErr error) {
logger.Errorf("任务 %v: 执行失败: %v", d.executionTask.TaskID, executeErr) logger.Errorf("任务 %v: 执行失败: %v", d.executionTask.TaskID, executeErr)
} }
func (d *DelayTask) ResolveDeviceIDs(ctx context.Context) ([]uint, error) { func (d *DelayTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) {
return []uint{}, nil return []uint32{}, nil
} }

View 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(&params)
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
}

View File

@@ -49,7 +49,7 @@ func (t *FullCollectionTask) Execute(ctx context.Context) error {
return nil return nil
} }
sensorsByController := make(map[uint][]*models.Device) sensorsByController := make(map[uint32][]*models.Device)
for _, sensor := range sensors { for _, sensor := range sensors {
sensorsByController[sensor.AreaControllerID] = append(sensorsByController[sensor.AreaControllerID], sensor) sensorsByController[sensor.AreaControllerID] = append(sensorsByController[sensor.AreaControllerID], sensor)
} }
@@ -97,7 +97,7 @@ func (t *FullCollectionTask) OnFailure(ctx context.Context, executeErr error) {
} }
// ResolveDeviceIDs 获取当前任务需要使用的设备ID列表 // 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
} }

View File

@@ -2,7 +2,6 @@ package task
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"sync" "sync"
"time" "time"
@@ -16,9 +15,9 @@ import (
// ReleaseFeedWeightTaskParams 定义了 ReleaseFeedWeightTask 的参数结构 // ReleaseFeedWeightTaskParams 定义了 ReleaseFeedWeightTask 的参数结构
type ReleaseFeedWeightTaskParams struct { type ReleaseFeedWeightTaskParams struct {
ReleaseWeight float64 `json:"release_weight"` // 需要释放的重量 ReleaseWeight float32 `json:"release_weight"` // 需要释放的重量
FeedPortDeviceID uint `json:"feed_port_device_id"` // 下料口ID FeedPortDeviceID uint32 `json:"feed_port_device_id"` // 下料口ID
MixingTankDeviceID uint `json:"mixing_tank_device_id"` // 称重传感器ID MixingTankDeviceID uint32 `json:"mixing_tank_device_id"` // 称重传感器ID
} }
// ReleaseFeedWeightTask 是一个控制下料口释放指定重量的任务 // ReleaseFeedWeightTask 是一个控制下料口释放指定重量的任务
@@ -30,8 +29,8 @@ type ReleaseFeedWeightTask struct {
claimedLog *models.TaskExecutionLog claimedLog *models.TaskExecutionLog
feedPortDevice *models.Device feedPortDevice *models.Device
releaseWeight float64 releaseWeight float32
mixingTankDeviceID uint mixingTankDeviceID uint32
feedPort device.Service 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") taskCtx, logger := logs.Trace(ctx, r.ctx, "getNowWeight")
sensorData, err := r.sensorDataRepo.GetLatestSensorDataByDeviceIDAndSensorType(taskCtx, r.mixingTankDeviceID, models.SensorTypeWeight) sensorData, err := r.sensorDataRepo.GetLatestSensorDataByDeviceIDAndSensorType(taskCtx, r.mixingTankDeviceID, models.SensorTypeWeight)
if err != nil { if err != nil {
@@ -114,7 +113,7 @@ func (r *ReleaseFeedWeightTask) getNowWeight(ctx context.Context) (float64, erro
} }
wg := &models.WeightData{} wg := &models.WeightData{}
err = json.Unmarshal(sensorData.Data, wg) err = sensorData.ParseData(wg)
if err != nil { if err != nil {
logger.Errorf("反序列化设备 %v 最新传感器数据失败: %v , 日志ID: %v", r.mixingTankDeviceID, err, r.claimedLog.ID) logger.Errorf("反序列化设备 %v 最新传感器数据失败: %v , 日志ID: %v", r.mixingTankDeviceID, err, r.claimedLog.ID)
return 0, err return 0, err
@@ -179,10 +178,10 @@ func (r *ReleaseFeedWeightTask) OnFailure(ctx context.Context, executeErr error)
logger.Errorf("善后处理完成, 日志ID:%v", r.claimedLog.ID) 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") taskCtx := logs.AddFuncName(ctx, r.ctx, "ResolveDeviceIDs")
if err := r.parseParameters(taskCtx); err != nil { if err := r.parseParameters(taskCtx); err != nil {
return nil, err return nil, err
} }
return []uint{r.feedPortDevice.ID, r.mixingTankDeviceID}, nil return []uint32{r.feedPortDevice.ID, r.mixingTankDeviceID}, nil
} }

View File

@@ -4,7 +4,9 @@ import (
"context" "context"
"fmt" "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/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/domain/plan"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "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/models"
@@ -15,26 +17,38 @@ const (
CompNameDelayTask = "DelayTask" CompNameDelayTask = "DelayTask"
CompNameReleaseFeedWeight = "ReleaseFeedWeightTask" CompNameReleaseFeedWeight = "ReleaseFeedWeightTask"
CompNameFullCollectionTask = "FullCollectionTask" CompNameFullCollectionTask = "FullCollectionTask"
CompNameAlarmNotification = "AlarmNotificationTask"
) )
type taskFactory struct { type taskFactory struct {
ctx context.Context ctx context.Context
sensorDataRepo repository.SensorDataRepository sensorDataRepo repository.SensorDataRepository
deviceRepo repository.DeviceRepository deviceRepo repository.DeviceRepository
deviceService device.Service alarmRepo repository.AlarmRepository
deviceService device.Service
notificationService notify.Service
alarmService alarm.AlarmService
} }
func NewTaskFactory( func NewTaskFactory(
ctx context.Context, ctx context.Context,
sensorDataRepo repository.SensorDataRepository, sensorDataRepo repository.SensorDataRepository,
deviceRepo repository.DeviceRepository, deviceRepo repository.DeviceRepository,
alarmRepo repository.AlarmRepository,
deviceService device.Service, deviceService device.Service,
notifyService notify.Service,
alarmService alarm.AlarmService,
) plan.TaskFactory { ) plan.TaskFactory {
return &taskFactory{ return &taskFactory{
ctx: ctx, ctx: ctx,
sensorDataRepo: sensorDataRepo, sensorDataRepo: sensorDataRepo,
deviceRepo: deviceRepo, deviceRepo: deviceRepo,
deviceService: deviceService, 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) return NewReleaseFeedWeightTask(logs.AddCompName(baseCtx, CompNameReleaseFeedWeight), claimedLog, t.sensorDataRepo, t.deviceRepo, t.deviceService)
case models.TaskTypeFullCollection: case models.TaskTypeFullCollection:
return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), claimedLog, t.deviceRepo, t.deviceService) 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: default:
// TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型 // TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型
logger.Panicf("不支持的任务类型: %s", claimedLog.Task.Type) logger.Panicf("不支持的任务类型: %s", claimedLog.Task.Type)
@@ -75,6 +95,12 @@ func (t *taskFactory) CreateTaskFromModel(ctx context.Context, taskModel *models
), nil ), nil
case models.TaskTypeFullCollection: case models.TaskTypeFullCollection:
return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), tempLog, t.deviceRepo, t.deviceService), nil 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: default:
return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type) return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type)
} }

View File

@@ -47,6 +47,9 @@ type Config struct {
// Collection 定时采集配置 // Collection 定时采集配置
Collection CollectionConfig `yaml:"collection"` Collection CollectionConfig `yaml:"collection"`
// AlarmNotification 告警通知配置
AlarmNotification AlarmNotificationConfig `yaml:"alarm_notification"`
} }
// AppConfig 代表应用基础配置 // AppConfig 代表应用基础配置
@@ -204,6 +207,29 @@ type CollectionConfig struct {
Interval int `yaml:"interval"` 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 创建并返回一个新的配置实例 // NewConfig 创建并返回一个新的配置实例
func NewConfig() *Config { func NewConfig() *Config {
// 默认值可以在这里设置,但我们优先使用配置文件中的值 // 默认值可以在这里设置,但我们优先使用配置文件中的值

View File

@@ -179,6 +179,7 @@ func (ps *PostgresStorage) creatingHyperTable(ctx context.Context) error {
{models.PigPurchase{}, "purchase_date"}, {models.PigPurchase{}, "purchase_date"},
{models.PigSale{}, "sale_date"}, {models.PigSale{}, "sale_date"},
{models.Notification{}, "alarm_timestamp"}, {models.Notification{}, "alarm_timestamp"},
{models.HistoricalAlarm{}, "trigger_time"},
} }
for _, table := range tablesToConvert { for _, table := range tablesToConvert {
@@ -221,6 +222,7 @@ func (ps *PostgresStorage) applyCompressionPolicies(ctx context.Context) error {
{models.PigPurchase{}, "pig_batch_id"}, {models.PigPurchase{}, "pig_batch_id"},
{models.PigSale{}, "pig_batch_id"}, {models.PigSale{}, "pig_batch_id"},
{models.Notification{}, "user_id"}, {models.Notification{}, "user_id"},
{models.HistoricalAlarm{}, "source_id"},
} }
for _, policy := range policies { for _, policy := range policies {

View File

@@ -208,7 +208,7 @@ func (g *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql
fields := []interface{}{ fields := []interface{}{
"sql", sql, "sql", sql,
"rows", rows, "rows", rows,
"elapsed", fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6), "elapsed", fmt.Sprintf("%.3fms", float32(elapsed.Nanoseconds())/1e6),
} }
// 附加调用链信息 // 附加调用链信息

View 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"
}

View File

@@ -6,7 +6,6 @@ import (
"strings" "strings"
"gorm.io/datatypes" "gorm.io/datatypes"
"gorm.io/gorm"
) )
// --- Properties 结构体定义 --- // --- Properties 结构体定义 ---
@@ -19,7 +18,7 @@ type Bus485Properties struct {
// AreaController 是一个LoRa转总线(如485)的通信网关 // AreaController 是一个LoRa转总线(如485)的通信网关
type AreaController struct { type AreaController struct {
gorm.Model Model
// Name 是主控的业务名称,例如 "1号猪舍主控" // Name 是主控的业务名称,例如 "1号猪舍主控"
Name string `gorm:"not null;unique" json:"name"` Name string `gorm:"not null;unique" json:"name"`
@@ -53,20 +52,20 @@ func (AreaController) TableName() string {
// Device 代表系统中的所有普通设备 // Device 代表系统中的所有普通设备
type Device struct { type Device struct {
// gorm.Model 内嵌了标准模型字段 (ID, CreatedAt, UpdatedAt, DeletedAt) // Model 内嵌了标准模型字段 (ID, CreatedAt, UpdatedAt, DeletedAt)
gorm.Model Model
// Name 是设备的业务名称,应清晰可读,例如 "1号猪舍温度传感器" // Name 是设备的业务名称,应清晰可读,例如 "1号猪舍温度传感器"
Name string `gorm:"not null" json:"name"` Name string `gorm:"not null" json:"name"`
// DeviceTemplateID 是设备模板的外键 // DeviceTemplateID 是设备模板的外键
DeviceTemplateID uint `gorm:"not null;index" json:"device_template_id"` DeviceTemplateID uint32 `gorm:"not null;index" json:"device_template_id"`
// DeviceTemplate 是设备的模板,包含了设备的通用信息 // DeviceTemplate 是设备的模板,包含了设备的通用信息
DeviceTemplate DeviceTemplate `json:"device_template"` DeviceTemplate DeviceTemplate `json:"device_template"`
// AreaControllerID 是区域主控的外键 // AreaControllerID 是区域主控的外键
AreaControllerID uint `gorm:"not null;index" json:"area_controller_id"` AreaControllerID uint32 `gorm:"not null;index" json:"area_controller_id"`
// AreaController 是设备所属的区域主控 // AreaController 是设备所属的区域主控
AreaController AreaController `json:"area_controller"` AreaController AreaController `json:"area_controller"`

View File

@@ -5,10 +5,38 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/command_generater"
"gorm.io/datatypes" "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 定义了设备模板的宽泛类别 // DeviceCategory 定义了设备模板的宽泛类别
@@ -25,8 +53,8 @@ const (
// 它提供了必要的元数据,以便应用程序能够正确解释从设备读取的原始数据。 // 它提供了必要的元数据,以便应用程序能够正确解释从设备读取的原始数据。
type ValueDescriptor struct { type ValueDescriptor struct {
Type SensorType `json:"type"` Type SensorType `json:"type"`
Multiplier float64 `json:"multiplier"` // 乘数,用于原始数据转换 Multiplier float32 `json:"multiplier"` // 乘数,用于原始数据转换
Offset float64 `json:"offset"` // 偏移量,用于原始数据转换 Offset float32 `json:"offset"` // 偏移量,用于原始数据转换
} }
// --- 指令结构体 (Command Structs) --- // --- 指令结构体 (Command Structs) ---
@@ -51,7 +79,7 @@ func (sc *SwitchCommands) SelfCheck() error {
// SensorCommands 定义了传感器读取指令所需的Modbus参数 // SensorCommands 定义了传感器读取指令所需的Modbus参数
type SensorCommands struct { type SensorCommands struct {
// ModbusFunctionCode 记录Modbus功能码例如 ReadHoldingRegisters。(一般是第二字节) // ModbusFunctionCode 记录Modbus功能码例如 ReadHoldingRegisters。(一般是第二字节)
ModbusFunctionCode command_generater.ModbusFunctionCode `json:"modbus_function_code"` ModbusFunctionCode ModbusFunctionCode `json:"modbus_function_code"`
// ModbusStartAddress 记录Modbus寄存器的起始地址用于生成指令。(一般是第三到四字节) // ModbusStartAddress 记录Modbus寄存器的起始地址用于生成指令。(一般是第三到四字节)
ModbusStartAddress uint16 `json:"modbus_start_address"` ModbusStartAddress uint16 `json:"modbus_start_address"`
// ModbusQuantity 记录Modbus寄存器的数量用于生成指令。(一般是五到六字节) // ModbusQuantity 记录Modbus寄存器的数量用于生成指令。(一般是五到六字节)
@@ -62,7 +90,7 @@ type SensorCommands struct {
func (sc *SensorCommands) SelfCheck() error { func (sc *SensorCommands) SelfCheck() error {
// 校验ModbusFunctionCode是否为读取类型 // 校验ModbusFunctionCode是否为读取类型
switch sc.ModbusFunctionCode { switch sc.ModbusFunctionCode {
case command_generater.ReadCoils, command_generater.ReadDiscreteInputs, command_generater.ReadHoldingRegisters, command_generater.ReadInputRegisters: case ReadCoils, ReadDiscreteInputs, ReadHoldingRegisters, ReadInputRegisters:
// 支持的读取功能码 // 支持的读取功能码
default: default:
return fmt.Errorf("'sensor' 指令集 ModbusFunctionCode %X 无效或不是读取类型", sc.ModbusFunctionCode) return fmt.Errorf("'sensor' 指令集 ModbusFunctionCode %X 无效或不是读取类型", sc.ModbusFunctionCode)
@@ -77,7 +105,7 @@ func (sc *SensorCommands) SelfCheck() error {
// DeviceTemplate 代表一种物理设备的类型。 // DeviceTemplate 代表一种物理设备的类型。
type DeviceTemplate struct { type DeviceTemplate struct {
gorm.Model Model
// Name 是此模板的唯一名称, 例如 "FanModel-XYZ-2000" 或 "TempSensor-T1" // Name 是此模板的唯一名称, 例如 "FanModel-XYZ-2000" 或 "TempSensor-T1"
Name string `gorm:"not null;unique" json:"name"` Name string `gorm:"not null;unique" json:"name"`

View File

@@ -27,12 +27,12 @@ const (
// PlanExecutionLog 记录整个计划的一次执行历史 // PlanExecutionLog 记录整个计划的一次执行历史
type PlanExecutionLog struct { type PlanExecutionLog struct {
ID uint `gorm:"primaryKey"` ID uint32 `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"primaryKey"` // 作为联合主键方便只查询热点数据 CreatedAt time.Time `gorm:"primaryKey"` // 作为联合主键方便只查询热点数据
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
PlanID uint `gorm:"index"` PlanID uint32 `gorm:"index"`
Status ExecutionStatus Status ExecutionStatus
StartedAt time.Time StartedAt time.Time
EndedAt time.Time EndedAt time.Time
@@ -46,12 +46,12 @@ func (PlanExecutionLog) TableName() string {
// TaskExecutionLog 记录单个任务的一次执行历史 // TaskExecutionLog 记录单个任务的一次执行历史
type TaskExecutionLog struct { type TaskExecutionLog struct {
ID uint `gorm:"primaryKey"` ID uint32 `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"primaryKey"` // 作为联合主键方便只查询热点数据 CreatedAt time.Time `gorm:"primaryKey"` // 作为联合主键方便只查询热点数据
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
PlanExecutionLogID uint `gorm:"index"` // 关联到某次计划执行 PlanExecutionLogID uint32 `gorm:"index"` // 关联到某次计划执行
// TaskID 使用 int 类型以容纳特殊的负数ID代表系统任务 // TaskID 使用 int 类型以容纳特殊的负数ID代表系统任务
TaskID int `gorm:"index"` TaskID int `gorm:"index"`
@@ -106,7 +106,7 @@ type DeviceCommandLog struct {
// DeviceID 是接收此下行任务的设备的ID。 // DeviceID 是接收此下行任务的设备的ID。
// 对于 LoRaWAN这通常是区域主控设备的ID。 // 对于 LoRaWAN这通常是区域主控设备的ID。
DeviceID uint `gorm:"not null;index" json:"device_id"` DeviceID uint32 `gorm:"not null;index" json:"device_id"`
// SentAt 记录下行任务最初发送的时间。 // SentAt 记录下行任务最初发送的时间。
SentAt time.Time `gorm:"primaryKey" json:"sent_at"` SentAt time.Time `gorm:"primaryKey" json:"sent_at"`
@@ -133,7 +133,7 @@ type PendingCollection struct {
// DeviceID 是接收此任务的设备ID // DeviceID 是接收此任务的设备ID
// 对于 LoRaWAN这通常是区域主控设备的ID。 // 对于 LoRaWAN这通常是区域主控设备的ID。
DeviceID uint `gorm:"index"` DeviceID uint32 `gorm:"index"`
// CommandMetadata 存储了此次采集任务对应的设备ID列表顺序与设备响应值的顺序一致。 // CommandMetadata 存储了此次采集任务对应的设备ID列表顺序与设备响应值的顺序一致。
CommandMetadata UintArray `gorm:"type:bigint[]"` CommandMetadata UintArray `gorm:"type:bigint[]"`
@@ -154,7 +154,6 @@ func (PendingCollection) TableName() string {
} }
// --- 用户审计日志 --- // --- 用户审计日志 ---
// TODO 这些变量放这个包合适吗?
// --- 审计日志状态常量 --- // --- 审计日志状态常量 ---
type AuditStatus string type AuditStatus string
@@ -184,13 +183,13 @@ func (a AuditContextKey) String() string {
// UserActionLog 记录用户的操作历史,用于审计 // UserActionLog 记录用户的操作历史,用于审计
type UserActionLog struct { type UserActionLog struct {
// 用 ID 和 Time 组成复合主键, 防止高并发时时间重复 // 用 ID 和 Time 组成复合主键, 防止高并发时时间重复
ID uint `gorm:"primaryKey"` ID uint32 `gorm:"primaryKey"`
// Time 是操作发生的时间,作为主键和超表的时间分区键 // Time 是操作发生的时间,作为主键和超表的时间分区键
Time time.Time `gorm:"primaryKey" json:"time"` Time time.Time `gorm:"primaryKey" json:"time"`
// --- Who (谁) --- // --- Who (谁) ---
UserID uint `gorm:"not null" json:"user_id,omitempty"` UserID uint32 `gorm:"not null" json:"user_id,omitempty"`
Username string `json:"username,omitempty"` // 操作发生时用户名的快照 Username string `json:"username,omitempty"` // 操作发生时用户名的快照
// --- Where (何地) --- // --- Where (何地) ---

View File

@@ -1,16 +1,12 @@
package models package models
import (
"gorm.io/gorm"
)
/* /*
猪场固定资产相关模型 猪场固定资产相关模型
*/ */
// PigHouse 定义了猪舍,是猪栏的集合 // PigHouse 定义了猪舍,是猪栏的集合
type PigHouse struct { type PigHouse struct {
gorm.Model Model
Name string `gorm:"size:100;not null;unique;comment:猪舍名称, 如 '育肥舍A栋'"` Name string `gorm:"size:100;not null;unique;comment:猪舍名称, 如 '育肥舍A栋'"`
Description string `gorm:"size:255;comment:描述信息"` Description string `gorm:"size:255;comment:描述信息"`
Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏 Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏
@@ -30,10 +26,10 @@ const (
// Pen 是猪栏的物理实体模型, 是所有空间相关数据的“锚点” // Pen 是猪栏的物理实体模型, 是所有空间相关数据的“锚点”
type Pen struct { type Pen struct {
gorm.Model Model
PenNumber string `gorm:"not null;comment:猪栏的唯一编号, 如 A-01"` PenNumber string `gorm:"not null;comment:猪栏的唯一编号, 如 A-01"`
HouseID uint `gorm:"index;comment:所属猪舍ID"` HouseID uint32 `gorm:"index;comment:所属猪舍ID"`
PigBatchID *uint `gorm:"index;comment:关联的猪批次ID"` PigBatchID *uint32 `gorm:"index;comment:关联的猪批次ID"`
Capacity int `gorm:"not null;comment:设计容量 (头)"` Capacity int `gorm:"not null;comment:设计容量 (头)"`
Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"` Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"`
} }

View File

@@ -2,8 +2,6 @@ package models
import ( import (
"time" "time"
"gorm.io/gorm"
) )
/* /*
@@ -13,10 +11,10 @@ import (
// RawMaterial 代表饲料的原料。 // RawMaterial 代表饲料的原料。
// 建议:所有重量单位统一存储 (例如, 全部使用 'g'),便于计算和避免转换错误。 // 建议:所有重量单位统一存储 (例如, 全部使用 'g'),便于计算和避免转换错误。
type RawMaterial struct { type RawMaterial struct {
gorm.Model Model
Name string `gorm:"size:100;unique;not null;comment:原料名称"` Name string `gorm:"size:100;unique;not null;comment:原料名称"`
Description string `gorm:"size:255;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 { func (RawMaterial) TableName() string {
@@ -25,13 +23,13 @@ func (RawMaterial) TableName() string {
// RawMaterialPurchase 记录了原料的每一次采购。 // RawMaterialPurchase 记录了原料的每一次采购。
type RawMaterialPurchase struct { type RawMaterialPurchase struct {
gorm.Model Model
RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"` RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"` RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
Supplier string `gorm:"size:100;comment:供应商"` Supplier string `gorm:"size:100;comment:供应商"`
Amount float64 `gorm:"not null;comment:采购数量, 单位: g"` Amount float32 `gorm:"not null;comment:采购数量, 单位: g"`
UnitPrice float64 `gorm:"comment:单价"` UnitPrice float32 `gorm:"comment:单价"`
TotalPrice float64 `gorm:"comment:总价"` TotalPrice float32 `gorm:"comment:总价"`
PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"` PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
CreatedAt time.Time CreatedAt time.Time
} }
@@ -54,11 +52,11 @@ const (
// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。 // RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
type RawMaterialStockLog struct { type RawMaterialStockLog struct {
gorm.Model Model
RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"` RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
ChangeAmount float64 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"` ChangeAmount float32 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"`
SourceType StockLogSourceType `gorm:"size:50;not null;index;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:业务发生时间"` HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"`
Remarks string `gorm:"comment:备注, 如主动领取的理由等"` Remarks string `gorm:"comment:备注, 如主动领取的理由等"`
} }
@@ -70,7 +68,7 @@ func (RawMaterialStockLog) TableName() string {
// FeedFormula 代表饲料配方。 // FeedFormula 代表饲料配方。
// 对于没有配方的外购饲料,可以将其视为一种特殊的 RawMaterial, 并为其创建一个仅包含它自己的 FeedFormula。 // 对于没有配方的外购饲料,可以将其视为一种特殊的 RawMaterial, 并为其创建一个仅包含它自己的 FeedFormula。
type FeedFormula struct { type FeedFormula struct {
gorm.Model Model
Name string `gorm:"size:100;unique;not null;comment:配方名称"` Name string `gorm:"size:100;unique;not null;comment:配方名称"`
Description string `gorm:"size:255;comment:描述"` Description string `gorm:"size:255;comment:描述"`
Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"` Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"`
@@ -82,11 +80,11 @@ func (FeedFormula) TableName() string {
// FeedFormulaComponent 代表配方中的一种原料及其占比。 // FeedFormulaComponent 代表配方中的一种原料及其占比。
type FeedFormulaComponent struct { type FeedFormulaComponent struct {
gorm.Model Model
FeedFormulaID uint `gorm:"not null;index;comment:外键到 FeedFormula"` FeedFormulaID uint32 `gorm:"not null;index;comment:外键到 FeedFormula"`
RawMaterialID uint `gorm:"not null;index;comment:外键到 RawMaterial"` RawMaterialID uint32 `gorm:"not null;index;comment:外键到 RawMaterial"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"` 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 { func (FeedFormulaComponent) TableName() string {
@@ -97,14 +95,14 @@ func (FeedFormulaComponent) TableName() string {
// 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula, // 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula,
// 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。 // 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。
type FeedUsageRecord struct { type FeedUsageRecord struct {
gorm.Model Model
PenID uint `gorm:"not null;index;comment:关联的猪栏ID"` PenID uint32 `gorm:"not null;index;comment:关联的猪栏ID"`
Pen Pen `gorm:"foreignKey:PenID"` 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"` 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:记录时间"` RecordedAt time.Time `gorm:"primaryKey;comment:记录时间"`
OperatorID uint `gorm:"not null;comment:操作员"` OperatorID uint32 `gorm:"not null;comment:操作员"`
Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"` Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"`
} }

View File

@@ -4,7 +4,6 @@ import (
"time" "time"
"gorm.io/datatypes" "gorm.io/datatypes"
"gorm.io/gorm"
) )
/* /*
@@ -48,21 +47,21 @@ type PowderInstructions struct {
// 出栏前停药期 // 出栏前停药期
WithdrawalPeriod time.Duration `json:"withdrawal_period"` WithdrawalPeriod time.Duration `json:"withdrawal_period"`
// 拌料使用计量, 每千克体重用多少克药, 单位: g/kg // 拌料使用计量, 每千克体重用多少克药, 单位: g/kg
BodyWeightDosageUsed float64 `json:"body_weight_dosage_used"` BodyWeightDosageUsed float32 `json:"body_weight_dosage_used"`
// 拌料使用剂量, 每升水加多少克药或每千克饲料干重加多少克药, 单位: g/kg(L) // 拌料使用剂量, 每升水加多少克药或每千克饲料干重加多少克药, 单位: g/kg(L)
MixDosageUsed float64 `json:"mix_dosage_used"` MixDosageUsed float32 `json:"mix_dosage_used"`
// 拌料使用方式, 兑水/拌料 // 拌料使用方式, 兑水/拌料
MixType MixType `json:"mix_type"` MixType MixType `json:"mix_type"`
} }
// Medication 定义了兽药/疫苗的基本信息模型 // Medication 定义了兽药/疫苗的基本信息模型
type Medication struct { type Medication struct {
gorm.Model Model
Name string `gorm:"size:100;not null;comment:药品名称" json:"name"` Name string `gorm:"size:100;not null;comment:药品名称" json:"name"`
Type MedicationType `gorm:"size:20;not null;comment:兽药类型 (粉剂, 针剂, 疫苗)" json:"type"` Type MedicationType `gorm:"size:20;not null;comment:兽药类型 (粉剂, 针剂, 疫苗)" json:"type"`
Category MedicationCategory `gorm:"size:30;not null;comment:兽药种类 (四环素类, 磺胺类等)" json:"category"` Category MedicationCategory `gorm:"size:30;not null;comment:兽药种类 (四环素类, 磺胺类等)" json:"category"`
DosagePerUnit float64 `gorm:"size:50;comment:一份药物的计量 (针剂计量单位为毫升, 粉剂为克)" json:"dosage_per_unit"` DosagePerUnit float32 `gorm:"size:50;comment:一份药物的计量 (针剂计量单位为毫升, 粉剂为克)" json:"dosage_per_unit"`
ActiveIngredientConcentration float64 `gorm:"size:50;comment:有效成分含量百分比" json:"active_ingredient_concentration"` ActiveIngredientConcentration float32 `gorm:"size:50;comment:有效成分含量百分比" json:"active_ingredient_concentration"`
Manufacturer string `gorm:"size:100;comment:生产厂家" json:"manufacturer"` Manufacturer string `gorm:"size:100;comment:生产厂家" json:"manufacturer"`
Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"` Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"`
} }
@@ -82,15 +81,15 @@ const (
// MedicationLog 记录了对整个猪批次的用药情况 // MedicationLog 记录了对整个猪批次的用药情况
type MedicationLog struct { type MedicationLog struct {
gorm.Model Model
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
MedicationID uint `gorm:"not null;index;comment:关联的药品ID"` MedicationID uint32 `gorm:"not null;index;comment:关联的药品ID"`
Medication Medication `gorm:"foreignKey:MedicationID"` // 预加载药品信息 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:用药对象数量"` TargetCount int `gorm:"not null;comment:用药对象数量"`
Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"` Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"`
Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"` Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"`
OperatorID uint `gorm:"comment:操作员ID"` OperatorID uint32 `gorm:"comment:操作员ID"`
HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"` HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"`
} }

View File

@@ -6,8 +6,20 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "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 返回一个包含所有数据库模型实例的切片。 // GetAllModels 返回一个包含所有数据库模型实例的切片。
// 这个函数用于在数据库初始化时自动迁移所有的表结构。 // 这个函数用于在数据库初始化时自动迁移所有的表结构。
func GetAllModels() []interface{} { func GetAllModels() []interface{} {
@@ -61,15 +73,19 @@ func GetAllModels() []interface{} {
&Medication{}, &Medication{},
&MedicationLog{}, &MedicationLog{},
// Alarm Models
&ActiveAlarm{},
&HistoricalAlarm{},
// Notification Models // Notification Models
&Notification{}, &Notification{},
} }
} }
// UintArray 是一个自定义类型,代表 uint 的切片。 // UintArray 是一个自定义类型,代表 uint32 的切片。
// 它实现了 gorm.Scanner 和 driver.Valuer 接口, // 它实现了 gorm.Scanner 和 driver.Valuer 接口,
// 以便能与数据库的 bigint[] 类型进行原生映射。 // 以便能与数据库的 bigint[] 类型进行原生映射。
type UintArray []uint type UintArray []uint32
// Value 实现了 driver.Valuer 接口。 // Value 实现了 driver.Valuer 接口。
// 它告诉 GORM 如何将 UintArray ([]) 转换为数据库能够理解的格式。 // 它告诉 GORM 如何将 UintArray ([]) 转换为数据库能够理解的格式。
@@ -111,21 +127,86 @@ func (a *UintArray) Scan(src interface{}) error {
// 去掉花括号 // 去掉花括号
srcStr = strings.Trim(srcStr, "{}") srcStr = strings.Trim(srcStr, "{}")
if srcStr == "" { if srcStr == "" {
*a = []uint{} *a = []uint32{}
return nil return nil
} }
// 按逗号分割 // 按逗号分割
parts := strings.Split(srcStr, ",") parts := strings.Split(srcStr, ",")
arr := make([]uint, len(parts)) arr := make([]uint32, len(parts))
for i, p := range parts { for i, p := range parts {
val, err := strconv.ParseUint(p, 10, 64) val, err := strconv.ParseUint(p, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("解析 UintArray 元素失败: %w", err) return fmt.Errorf("解析 UintArray 元素失败: %w", err)
} }
arr[i] = uint(val) arr[i] = uint32(val)
} }
*a = arr *a = arr
return nil 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
}

View File

@@ -1,13 +1,21 @@
package models package models
import ( import (
"database/sql/driver"
"errors"
"time" "time"
)
"git.huangwc.com/pig/pig-farm-controller/internal/infra/notify" // NotifierType 定义了通知器的类型。
"go.uber.org/zap/zapcore" type NotifierType string
"gorm.io/gorm"
const (
// NotifierTypeSMTP 表示 SMTP 邮件通知器。
NotifierTypeSMTP NotifierType = "邮件"
// NotifierTypeWeChat 表示企业微信通知器。
NotifierTypeWeChat NotifierType = "企业微信"
// NotifierTypeLark 表示飞书通知器。
NotifierTypeLark NotifierType = "飞书"
// NotifierTypeLog 表示日志通知器,作为最终的告警记录渠道。
NotifierTypeLog NotifierType = "日志"
) )
// NotificationStatus 定义了通知发送尝试的状态枚举。 // NotificationStatus 定义了通知发送尝试的状态枚举。
@@ -19,48 +27,20 @@ const (
NotificationStatusSkipped NotificationStatus = "已跳过" // 通知因某些原因被跳过(例如:用户未配置联系方式) 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 表示已发送或尝试发送的通知记录。 // Notification 表示已发送或尝试发送的通知记录。
type Notification struct { type Notification struct {
gorm.Model Model
// NotifierType 通知器类型 (例如:"邮件", "企业微信", "飞书", "日志") // 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 接收通知的用户ID用于追溯通知记录到特定用户
UserID uint `gorm:"index" json:"user_id"` // 增加 UserID 字段,并添加索引 UserID uint32 `gorm:"index" json:"user_id"` // 增加 UserID 字段,并添加索引
// Title 通知标题 // Title 通知标题
Title string `gorm:"type:varchar(255);not null" json:"title"` Title string `gorm:"type:varchar(255);not null" json:"title"`
// Message 通知内容 // Message 通知内容
Message string `gorm:"type:text;not null" json:"message"` Message string `gorm:"type:text;not null" json:"message"`
// Level 通知级别 (例如INFO, WARN, ERROR) // 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 通知内容生成时的时间戳,与 ID 构成复合主键
AlarmTimestamp time.Time `gorm:"primaryKey;not null" json:"alarm_timestamp"` AlarmTimestamp time.Time `gorm:"primaryKey;not null" json:"alarm_timestamp"`
// ToAddress 接收地址 (例如:邮箱地址, 企业微信ID, 日志标识符) // ToAddress 接收地址 (例如:邮箱地址, 企业微信ID, 日志标识符)

View File

@@ -2,8 +2,6 @@ package models
import ( import (
"time" "time"
"gorm.io/gorm"
) )
/* /*
@@ -32,7 +30,7 @@ const (
// PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪 // PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪
type PigBatch struct { type PigBatch struct {
gorm.Model Model
BatchNumber string `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"` BatchNumber string `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"`
OriginType PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"` OriginType PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"`
StartDate time.Time `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"` StartDate time.Time `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"`
@@ -65,14 +63,14 @@ const (
// PigBatchLog 记录了猪批次数量或状态的每一次变更 // PigBatchLog 记录了猪批次数量或状态的每一次变更
type PigBatchLog struct { type PigBatchLog struct {
gorm.Model Model
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"` ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"`
ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"` ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"`
Reason string `gorm:"size:255;comment:变更原因描述"` Reason string `gorm:"size:255;comment:变更原因描述"`
BeforeCount int `gorm:"not null;comment:变更前总数"` BeforeCount int `gorm:"not null;comment:变更前总数"`
AfterCount 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:事件发生时间"` HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"`
} }
@@ -82,10 +80,10 @@ func (PigBatchLog) TableName() string {
// WeighingBatch 记录了一次批次称重的信息 // WeighingBatch 记录了一次批次称重的信息
type WeighingBatch struct { type WeighingBatch struct {
gorm.Model Model
WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"` WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"`
Description string `gorm:"size:255;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 { func (WeighingBatch) TableName() string {
@@ -94,11 +92,11 @@ func (WeighingBatch) TableName() string {
// WeighingRecord 记录了单次称重信息 // WeighingRecord 记录了单次称重信息
type WeighingRecord struct { type WeighingRecord struct {
gorm.Model Model
Weight float64 `gorm:"not null;comment:单只猪重量 (kg)"` Weight float32 `gorm:"not null;comment:单只猪重量 (kg)"`
WeighingBatchID uint `gorm:"not null;index;comment:关联的批次称重ID"` WeighingBatchID uint32 `gorm:"not null;index;comment:关联的批次称重ID"`
PenID uint `gorm:"not null;index;comment:所在猪圈ID"` PenID uint32 `gorm:"not null;index;comment:所在猪圈ID"`
OperatorID uint `gorm:"not null;comment:操作员ID"` OperatorID uint32 `gorm:"not null;comment:操作员ID"`
Remark string `gorm:"size:255;comment:备注"` Remark string `gorm:"size:255;comment:备注"`
WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"` WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"`
} }

View File

@@ -2,8 +2,6 @@ package models
import ( import (
"time" "time"
"gorm.io/gorm"
) )
// PigBatchSickPigTreatmentLocation 定义了病猪治疗地点 // PigBatchSickPigTreatmentLocation 定义了病猪治疗地点
@@ -29,16 +27,16 @@ const (
// PigSickLog 记录了猪批次中病猪数量的变化日志 // PigSickLog 记录了猪批次中病猪数量的变化日志
type PigSickLog struct { type PigSickLog struct {
gorm.Model Model
PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"` PigBatchID uint32 `gorm:"primaryKey;comment:关联的猪批次ID"`
PenID uint `gorm:"not null;index;comment:所在猪圈ID"` PenID uint32 `gorm:"not null;index;comment:所在猪圈ID"`
ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"` ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"`
Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"` Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"`
BeforeCount int `gorm:"comment:变化前的数量"` BeforeCount int `gorm:"comment:变化前的数量"`
AfterCount int `gorm:"comment:变化后的数量"` AfterCount int `gorm:"comment:变化后的数量"`
Remarks string `gorm:"size:255;comment:备注"` Remarks string `gorm:"size:255;comment:备注"`
TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"` TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"`
OperatorID uint `gorm:"comment:操作员ID"` OperatorID uint32 `gorm:"comment:操作员ID"`
HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"`
} }

View File

@@ -2,21 +2,19 @@ package models
import ( import (
"time" "time"
"gorm.io/gorm"
) )
// PigPurchase 记录了猪只采购信息 // PigPurchase 记录了猪只采购信息
type PigPurchase struct { type PigPurchase struct {
gorm.Model Model
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"` PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
Supplier string `gorm:"comment:供应商"` Supplier string `gorm:"comment:供应商"`
Quantity int `gorm:"not null;comment:采购数量"` Quantity int `gorm:"not null;comment:采购数量"`
UnitPrice float64 `gorm:"not null;comment:单价"` UnitPrice float32 `gorm:"not null;comment:单价"`
TotalPrice float64 `gorm:"not null;comment:总价"` TotalPrice float32 `gorm:"not null;comment:总价"`
Remarks string `gorm:"size:255;comment:备注"` Remarks string `gorm:"size:255;comment:备注"`
OperatorID uint `gorm:"comment:操作员ID"` OperatorID uint32 `gorm:"comment:操作员ID"`
} }
func (PigPurchase) TableName() string { func (PigPurchase) TableName() string {
@@ -25,15 +23,15 @@ func (PigPurchase) TableName() string {
// PigSale 记录了猪只销售信息 // PigSale 记录了猪只销售信息
type PigSale struct { type PigSale struct {
gorm.Model Model
PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` PigBatchID uint32 `gorm:"not null;index;comment:关联的猪批次ID"`
SaleDate time.Time `gorm:"primaryKey;comment:销售日期"` SaleDate time.Time `gorm:"primaryKey;comment:销售日期"`
Buyer string `gorm:"comment:购买方"` Buyer string `gorm:"comment:购买方"`
Quantity int `gorm:"not null;comment:销售数量"` Quantity int `gorm:"not null;comment:销售数量"`
UnitPrice float64 `gorm:"not null;comment:单价"` UnitPrice float32 `gorm:"not null;comment:单价"`
TotalPrice float64 `gorm:"not null;comment:总价"` TotalPrice float32 `gorm:"not null;comment:总价"`
Remarks string `gorm:"size:255;comment:备注"` Remarks string `gorm:"size:255;comment:备注"`
OperatorID uint `gorm:"comment:操作员ID"` OperatorID uint32 `gorm:"comment:操作员ID"`
} }
func (PigSale) TableName() string { func (PigSale) TableName() string {

View File

@@ -2,8 +2,6 @@ package models
import ( import (
"time" "time"
"gorm.io/gorm"
) )
// PigTransferType 定义了猪只迁移的类型 // PigTransferType 定义了猪只迁移的类型
@@ -23,14 +21,14 @@ const (
// PigTransferLog 记录了每一次猪只数量在猪栏间的变动事件。 // PigTransferLog 记录了每一次猪只数量在猪栏间的变动事件。
// 它作为事件溯源的基础,用于推算任意时间点猪栏的猪只数量。 // 它作为事件溯源的基础,用于推算任意时间点猪栏的猪只数量。
type PigTransferLog struct { type PigTransferLog struct {
gorm.Model Model
TransferTime time.Time `gorm:"primaryKey;comment:迁移发生时间" json:"transfer_time"` // 迁移发生时间,作为联合主键 TransferTime time.Time `gorm:"primaryKey;comment:迁移发生时间" json:"transfer_time"` // 迁移发生时间,作为联合主键
PigBatchID uint `gorm:"primaryKey;comment:关联的猪群ID" json:"pig_batch_id"` // 关联的猪群ID作为联合主键 PigBatchID uint32 `gorm:"primaryKey;comment:关联的猪群ID" json:"pig_batch_id"` // 关联的猪群ID作为联合主键
PenID uint `gorm:"primaryKey;comment:发生变动的猪栏ID" json:"pen_id"` // 发生变动的猪栏ID作为联合主键 PenID uint32 `gorm:"primaryKey;comment:发生变动的猪栏ID" json:"pen_id"` // 发生变动的猪栏ID作为联合主键
Quantity int `gorm:"not null;comment:变动数量(正数表示增加,负数表示减少)" json:"quantity"` // 变动数量(正数表示增加,负数减少) Quantity int `gorm:"not null;comment:变动数量(正数表示增加,负数表示减少)" json:"quantity"` // 变动数量(正数表示增加,负数减少)
Type PigTransferType `gorm:"not null;comment:变动类型" json:"type"` // 变动类型,使用枚举类型 Type PigTransferType `gorm:"not null;comment:变动类型" json:"type"` // 变动类型,使用枚举类型
CorrelationID string `gorm:"comment:用于关联一次完整操作(如一次调栏会产生两条日志)" json:"correlation_id"` // 用于关联一次完整操作 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"` Remarks string `gorm:"comment:备注" json:"remarks"`
} }

View File

@@ -11,6 +11,15 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type PlanName string
const (
// PlanNamePeriodicSystemHealthCheck 是周期性系统健康检查计划的名称
PlanNamePeriodicSystemHealthCheck PlanName = "周期性系统健康检查"
// PlanNameAlarmNotification 是告警通知发送计划的名称
PlanNameAlarmNotification PlanName = "告警通知发送"
)
// PlanExecutionType 定义了计划的执行类型 // PlanExecutionType 定义了计划的执行类型
type PlanExecutionType string type PlanExecutionType string
@@ -31,10 +40,13 @@ const (
type TaskType string type TaskType string
const ( const (
TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务 TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务
TaskTypeWaiting TaskType = "等待" // 等待任务 TaskTypeWaiting TaskType = "等待" // 等待任务
TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务 TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务
TaskTypeFullCollection TaskType = "全量采集" // 新增的全量采集任务 TaskTypeFullCollection TaskType = "全量采集" // 新增的全量采集任务
TaskTypeAlarmNotification TaskType = "告警通知" // 告警通知任务
TaskTypeDeviceThresholdCheck TaskType = "设备阈值检查" // 设备阈值检查任务
TaskTypeAreaCollectorThresholdCheck TaskType = "区域阈值检查" // 区域阈值检查任务
) )
// -- Task Parameters -- // -- Task Parameters --
@@ -62,15 +74,15 @@ const (
// Plan 代表系统中的一个计划,可以包含子计划或任务 // Plan 代表系统中的一个计划,可以包含子计划或任务
type Plan struct { type Plan struct {
gorm.Model Model
Name string `gorm:"not null" json:"name"` Name PlanName `gorm:"not null" json:"name"`
Description string `json:"description"` Description string `json:"description"`
PlanType PlanType `gorm:"not null;index" json:"plan_type"` // 任务类型, 包括系统任务和用户自定义任务 PlanType PlanType `gorm:"not null;index" json:"plan_type"` // 任务类型, 包括系统任务和用户自定义任务
ExecutionType PlanExecutionType `gorm:"not null;index" json:"execution_type"` ExecutionType PlanExecutionType `gorm:"not null;index" json:"execution_type"`
Status PlanStatus `gorm:"default:'已禁用';index" json:"status"` // 计划是否被启动 Status PlanStatus `gorm:"default:'已禁用';index" json:"status"` // 计划是否被启动
ExecuteNum uint `gorm:"default:0" json:"execute_num"` // 计划预期执行次数 ExecuteNum uint32 `gorm:"default:0" json:"execute_num"` // 计划预期执行次数
ExecuteCount uint `gorm:"default:0" json:"execute_count"` // 执行计数器 ExecuteCount uint32 `gorm:"default:0" json:"execute_count"` // 执行计数器
// 针对 PlanExecutionTypeAutomatic使用 Cron 表达式定义调度规则 // 针对 PlanExecutionTypeAutomatic使用 Cron 表达式定义调度规则
CronExpression string `json:"cron_expression"` CronExpression string `json:"cron_expression"`
@@ -151,12 +163,12 @@ func (p *Plan) ReorderSteps() {
// SubPlan 代表作为另一个计划一部分的子计划,具有执行顺序 // SubPlan 代表作为另一个计划一部分的子计划,具有执行顺序
type SubPlan struct { type SubPlan struct {
gorm.Model Model
ParentPlanID uint `gorm:"not null;index" json:"parent_plan_id"` // 父计划的ID ParentPlanID uint32 `gorm:"not null;index" json:"parent_plan_id"` // 父计划的ID
ChildPlanID uint `gorm:"not null;index" json:"child_plan_id"` // 子计划的ID (它本身也是一个 Plan) ChildPlanID uint32 `gorm:"not null;index" json:"child_plan_id"` // 子计划的ID (它本身也是一个 Plan)
ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在父计划中的执行顺序 ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在父计划中的执行顺序
ChildPlan *Plan `gorm:"-" json:"child_plan"` // 完整子计划数据,仅内存中 ChildPlan *Plan `gorm:"-" json:"child_plan"` // 完整子计划数据,仅内存中
} }
// TableName 自定义 GORM 使用的数据库表名 // TableName 自定义 GORM 使用的数据库表名
@@ -172,7 +184,7 @@ type Task struct {
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` // 保持软删除功能 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"` Name string `gorm:"not null" json:"name"`
Description string `json:"description"` Description string `json:"description"`
ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在计划中的执行顺序 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) 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 是设备和任务之间的关联模型,表示一个设备可以执行多个任务,一个任务可以被多个设备执行。 // DeviceTask 是设备和任务之间的关联模型,表示一个设备可以执行多个任务,一个任务可以被多个设备执行。
type DeviceTask struct { type DeviceTask struct {
gorm.Model Model
DeviceID uint `gorm:"not null;index"` // 设备ID DeviceID uint32 `gorm:"not null;index"` // 设备ID
TaskID uint `gorm:"not null;index"` // 任务ID TaskID uint32 `gorm:"not null;index"` // 任务ID
// 可选:如果需要存储关联的额外信息,可以在这里添加字段,例如: // 可选:如果需要存储关联的额外信息,可以在这里添加字段,例如:
// Configuration datatypes.JSON `json:"configuration"` // 任务在特定设备上的配置 // Configuration datatypes.JSON `json:"configuration"` // 任务在特定设备上的配置

View File

@@ -7,8 +7,8 @@ import (
// PendingTask 是一个待执行任务队列, 里面会储存待执行的Task以及这个Task什么时候执行 // PendingTask 是一个待执行任务队列, 里面会储存待执行的Task以及这个Task什么时候执行
// 它是一个纯粹的工作队列,任务被认领后即被删除。 // 它是一个纯粹的工作队列,任务被认领后即被删除。
type PendingTask struct { type PendingTask struct {
// 手动填充必须字段以实现硬删除,不内嵌 gorm.Model // 手动填充必须字段以实现硬删除,不内嵌 Model
ID uint `gorm:"primarykey"` ID uint32 `gorm:"primarykey"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
@@ -21,7 +21,7 @@ type PendingTask struct {
Task *Task `gorm:"foreignKey:TaskID"` Task *Task `gorm:"foreignKey:TaskID"`
ExecuteAt time.Time `gorm:"index"` // 任务执行时间 ExecuteAt time.Time `gorm:"index"` // 任务执行时间
TaskExecutionLogID uint `gorm:"unique;not null;index"` // 对应的执行历史记录ID TaskExecutionLogID uint32 `gorm:"unique;not null;index"` // 对应的执行历史记录ID
// 通过 TaskExecutionLogID 关联到唯一的 TaskExecutionLog 记录 // 通过 TaskExecutionLogID 关联到唯一的 TaskExecutionLog 记录
// ON DELETE CASCADE 确保如果日志被删除,这个待办任务也会被自动清理 // ON DELETE CASCADE 确保如果日志被删除,这个待办任务也会被自动清理

View File

@@ -1,6 +1,8 @@
package models package models
import ( import (
"encoding/json"
"errors"
"time" "time"
"gorm.io/datatypes" "gorm.io/datatypes"
@@ -20,7 +22,7 @@ const (
// SignalMetrics 存储信号强度数据 // SignalMetrics 存储信号强度数据
type SignalMetrics struct { type SignalMetrics struct {
RssiDbm int `json:"rssi_dbm"` // 绝对信号强度dBm受距离、障碍物影响 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) SensitivityDbm int `json:"sensitivity_dbm"` // 网关的最低检测阈值(dBm)
MarginDb int `json:"margin_db"` // SNR 相对于接收器灵敏度的余量, Margin = SNR - Sensitivity MarginDb int `json:"margin_db"` // SNR 相对于接收器灵敏度的余量, Margin = SNR - Sensitivity
} }
@@ -34,17 +36,17 @@ type BatteryLevel struct {
// TemperatureData 存储温度数据 // TemperatureData 存储温度数据
type TemperatureData struct { type TemperatureData struct {
TemperatureCelsius float64 `json:"temperature_celsius"` // 温度值 (摄氏度) TemperatureCelsius float32 `json:"temperature_celsius"` // 温度值 (摄氏度)
} }
// HumidityData 存储湿度数据 // HumidityData 存储湿度数据
type HumidityData struct { type HumidityData struct {
HumidityPercent float64 `json:"humidity_percent"` // 湿度值 (%) HumidityPercent float32 `json:"humidity_percent"` // 湿度值 (%)
} }
// WeightData 存储重量数据 // WeightData 存储重量数据
type WeightData struct { type WeightData struct {
WeightKilograms float64 `json:"weight_kilograms"` // 重量值 (公斤) WeightKilograms float32 `json:"weight_kilograms"` // 重量值 (公斤)
} }
// SensorData 存储所有类型的传感器数据,对应数据库中的 'sensor_data' 表。 // SensorData 存储所有类型的传感器数据,对应数据库中的 'sensor_data' 表。
@@ -53,10 +55,10 @@ type SensorData struct {
Time time.Time `gorm:"primaryKey" json:"time"` Time time.Time `gorm:"primaryKey" json:"time"`
// DeviceID 是传感器的唯一标识符,作为复合主键的另一部分。 // DeviceID 是传感器的唯一标识符,作为复合主键的另一部分。
DeviceID uint `gorm:"primaryKey" json:"device_id"` DeviceID uint32 `gorm:"primaryKey" json:"device_id"`
// RegionalControllerID 是上报此数据的区域主控的ID。 // AreaControllerID 是上报此数据的区域主控的ID。
RegionalControllerID uint `json:"regional_controller_id"` AreaControllerID uint32 `json:"area_controller_id"`
// SensorType 是传感数据的类型 // SensorType 是传感数据的类型
SensorType SensorType `gorm:"not null;index" json:"sensor_type"` SensorType SensorType `gorm:"not null;index" json:"sensor_type"`
@@ -68,3 +70,12 @@ type SensorData struct {
func (SensorData) TableName() string { func (SensorData) TableName() string {
return "sensor_data" 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)
}

View File

@@ -38,9 +38,9 @@ func (ci ContactInfo) Value() (driver.Value, error) {
// User 代表系统中的用户模型 // User 代表系统中的用户模型
type User struct { type User struct {
// gorm.Model 内嵌了 ID, CreatedAt, UpdatedAt, 和 DeletedAt // Model 内嵌了 ID, CreatedAt, UpdatedAt, 和 DeletedAt
// DeletedAt 字段的存在自动为 GORM 开启了软删除模式 // DeletedAt 字段的存在自动为 GORM 开启了软删除模式
gorm.Model Model
// Username 是用户的登录名,应该是唯一的 // Username 是用户的登录名,应该是唯一的
// 修正了 gorm 标签的拼写错误 (移除了 gorm 后面的冒号) // 修正了 gorm 标签的拼写错误 (移除了 gorm 后面的冒号)

View File

@@ -10,6 +10,7 @@ import (
"time" "time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
) )
const ( const (
@@ -65,7 +66,7 @@ func (l *larkNotifier) Send(ctx context.Context, content AlarmContent, toAddr st
"tag": "lark_md", "tag": "lark_md",
"content": fmt.Sprintf("## %s\n**级别**: %s\n**时间**: %s\n\n%s", "content": fmt.Sprintf("## %s\n**级别**: %s\n**时间**: %s\n\n%s",
content.Title, content.Title,
content.Level.String(), content.Level,
content.Timestamp.Format(DefaultTimeFormat), content.Timestamp.Format(DefaultTimeFormat),
content.Message, content.Message,
), ),
@@ -171,8 +172,8 @@ func (l *larkNotifier) getAccessToken(ctx context.Context) (string, error) {
} }
// Type 返回通知器的类型 // Type 返回通知器的类型
func (l *larkNotifier) Type() NotifierType { func (l *larkNotifier) Type() models.NotifierType {
return NotifierTypeLark return models.NotifierTypeLark
} }
// --- API 数据结构 --- // --- API 数据结构 ---

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
) )
// logNotifier 实现了 Notifier 接口,用于将告警信息记录到日志中。 // logNotifier 实现了 Notifier 接口,用于将告警信息记录到日志中。
@@ -24,10 +25,10 @@ func NewLogNotifier(ctx context.Context) Notifier {
func (l *logNotifier) Send(ctx context.Context, content AlarmContent, toAddr string) error { func (l *logNotifier) Send(ctx context.Context, content AlarmContent, toAddr string) error {
logger := logs.TraceLogger(ctx, l.ctx, "Send") logger := logs.TraceLogger(ctx, l.ctx, "Send")
logger.Infow("告警已记录到日志", logger.Infow("告警已记录到日志",
"notifierType", NotifierTypeLog, "notifierType", models.NotifierTypeLog,
"title", content.Title, "title", content.Title,
"message", content.Message, "message", content.Message,
"level", content.Level.String(), "level", content.Level,
"timestamp", content.Timestamp.Format(DefaultTimeFormat), "timestamp", content.Timestamp.Format(DefaultTimeFormat),
"toAddr", toAddr, "toAddr", toAddr,
) )
@@ -35,6 +36,6 @@ func (l *logNotifier) Send(ctx context.Context, content AlarmContent, toAddr str
} }
// Type 返回通知器的类型。 // Type 返回通知器的类型。
func (l *logNotifier) Type() NotifierType { func (l *logNotifier) Type() models.NotifierType {
return NotifierTypeLog return models.NotifierTypeLog
} }

View File

@@ -4,26 +4,12 @@ import (
"context" "context"
"time" "time"
"go.uber.org/zap/zapcore" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
) )
// DefaultTimeFormat 定义了所有通知中统一使用的时间格式。 // DefaultTimeFormat 定义了所有通知中统一使用的时间格式。
const DefaultTimeFormat = "2006-01-02 15:04:05" 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 定义了通知的内容 // AlarmContent 定义了通知的内容
type AlarmContent struct { type AlarmContent struct {
// 通知标题 // 通知标题
@@ -31,7 +17,7 @@ type AlarmContent struct {
// 通知信息 // 通知信息
Message string Message string
// 通知级别 // 通知级别
Level zapcore.Level Level models.SeverityLevel
// 通知时间 // 通知时间
Timestamp time.Time Timestamp time.Time
} }
@@ -41,5 +27,5 @@ type Notifier interface {
// Send 发送通知 // Send 发送通知
Send(ctx context.Context, content AlarmContent, toAddr string) error Send(ctx context.Context, content AlarmContent, toAddr string) error
// Type 返回通知器的类型 // Type 返回通知器的类型
Type() NotifierType Type() models.NotifierType
} }

View File

@@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"net/smtp" "net/smtp"
"strings" "strings"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
) )
// smtpNotifier 实现了 Notifier 接口,用于通过 SMTP 发送邮件通知。 // 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", body := fmt.Sprintf("级别: %s\n时间: %s\n\n%s",
content.Level.String(), content.Level,
content.Timestamp.Format(DefaultTimeFormat), content.Timestamp.Format(DefaultTimeFormat),
content.Message, content.Message,
) )
@@ -71,6 +73,6 @@ func (s *smtpNotifier) Send(ctx context.Context, content AlarmContent, toAddr st
} }
// Type 返回通知器的类型 // Type 返回通知器的类型
func (s *smtpNotifier) Type() NotifierType { func (s *smtpNotifier) Type() models.NotifierType {
return NotifierTypeSMTP return models.NotifierTypeSMTP
} }

View File

@@ -9,6 +9,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
) )
const ( const (
@@ -55,7 +57,7 @@ func (w *wechatNotifier) Send(ctx context.Context, content AlarmContent, toAddr
// 2. 构建 markdown 内容 // 2. 构建 markdown 内容
markdownContent := fmt.Sprintf("## %s\n> 级别: <font color=\"warning\">%s</font>\n> 时间: %s\n\n%s", markdownContent := fmt.Sprintf("## %s\n> 级别: <font color=\"warning\">%s</font>\n> 时间: %s\n\n%s",
content.Title, content.Title,
content.Level.String(), content.Level,
content.Timestamp.Format(DefaultTimeFormat), content.Timestamp.Format(DefaultTimeFormat),
content.Message, content.Message,
) )
@@ -142,8 +144,8 @@ func (w *wechatNotifier) getAccessToken() (string, error) {
} }
// Type 返回通知器的类型 // Type 返回通知器的类型
func (w *wechatNotifier) Type() NotifierType { func (w *wechatNotifier) Type() models.NotifierType {
return NotifierTypeWeChat return models.NotifierTypeWeChat
} }
// --- API 数据结构 --- // --- API 数据结构 ---

View 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)
}

View File

@@ -3,6 +3,7 @@ package repository
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "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/models"
@@ -12,12 +13,14 @@ import (
// AreaControllerRepository 定义了对 AreaController 模型的数据库操作接口 // AreaControllerRepository 定义了对 AreaController 模型的数据库操作接口
type AreaControllerRepository interface { 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) FindByNetworkID(ctx context.Context, networkID string) (*models.AreaController, error)
Create(ctx context.Context, ac *models.AreaController) error Create(ctx context.Context, ac *models.AreaController) error
ListAll(ctx context.Context) ([]*models.AreaController, error) ListAll(ctx context.Context) ([]*models.AreaController, error)
Update(ctx context.Context, ac *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 实现。 // gormAreaControllerRepository 是 AreaControllerRepository 的 GORM 实现。
@@ -57,7 +60,7 @@ func (r *gormAreaControllerRepository) Update(ctx context.Context, ac *models.Ar
} }
// Delete 删除一个 AreaController 记录。 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "Delete")
if err := r.db.WithContext(repoCtx).Delete(&models.AreaController{}, id).Error; err != nil { if err := r.db.WithContext(repoCtx).Delete(&models.AreaController{}, id).Error; err != nil {
return fmt.Errorf("删除区域主控失败: %w", err) return fmt.Errorf("删除区域主控失败: %w", err)
@@ -66,7 +69,7 @@ func (r *gormAreaControllerRepository) Delete(ctx context.Context, id uint) erro
} }
// FindByID 通过 ID 查找一个 AreaController。 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
var areaController models.AreaController var areaController models.AreaController
if err := r.db.WithContext(repoCtx).First(&areaController, id).Error; err != nil { 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 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 // 没有发现任何未被忽略的任务正在使用此区域主控
}

View File

@@ -12,7 +12,7 @@ import (
// DeviceCommandLogListOptions 定义了查询设备命令日志时的可选参数 // DeviceCommandLogListOptions 定义了查询设备命令日志时的可选参数
type DeviceCommandLogListOptions struct { type DeviceCommandLogListOptions struct {
DeviceID *uint DeviceID *uint32
ReceivedSuccess *bool ReceivedSuccess *bool
StartTime *time.Time // 基于 sent_at 字段 StartTime *time.Time // 基于 sent_at 字段
EndTime *time.Time // 基于 sent_at 字段 EndTime *time.Time // 基于 sent_at 字段

View File

@@ -18,7 +18,7 @@ type DeviceRepository interface {
Create(ctx context.Context, device *models.Device) error Create(ctx context.Context, device *models.Device) error
// FindByID 根据主键 ID 查找设备 // FindByID 根据主键 ID 查找设备
FindByID(ctx context.Context, id uint) (*models.Device, error) FindByID(ctx context.Context, id uint32) (*models.Device, error)
// FindByIDString 根据字符串形式的主键 ID 查找设备 // FindByIDString 根据字符串形式的主键 ID 查找设备
FindByIDString(ctx context.Context, id string) (*models.Device, error) FindByIDString(ctx context.Context, id string) (*models.Device, error)
@@ -30,28 +30,28 @@ type DeviceRepository interface {
ListAllSensors(ctx context.Context) ([]*models.Device, error) ListAllSensors(ctx context.Context) ([]*models.Device, error)
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备 // ListByAreaControllerID 根据区域主控 ID 列出所有子设备
ListByAreaControllerID(ctx context.Context, areaControllerID uint) ([]*models.Device, error) ListByAreaControllerID(ctx context.Context, areaControllerID uint32) ([]*models.Device, error)
// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备 // FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备
FindByDeviceTemplateID(ctx context.Context, deviceTemplateID uint) ([]*models.Device, error) FindByDeviceTemplateID(ctx context.Context, deviceTemplateID uint32) ([]*models.Device, error)
// Update 更新一个已有的设备信息 // Update 更新一个已有的设备信息
Update(ctx context.Context, device *models.Device) error Update(ctx context.Context, device *models.Device) error
// Delete 根据主键 ID 删除一个设备 // Delete 根据主键 ID 删除一个设备
Delete(ctx context.Context, id uint) error Delete(ctx context.Context, id uint32) error
// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备 // 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 在指定事务中根据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 检查设备是否被任何任务使用,可以忽略指定任务类型
IsDeviceInUse(ctx context.Context, deviceID uint) (bool, error) IsDeviceInUse(ctx context.Context, deviceID uint32, ignoredTaskTypes []models.TaskType) (bool, error)
// IsAreaControllerInUse 检查区域主控是否被任何设备使用 // IsAreaControllerInUse 检查区域主控是否被任何设备使用
IsAreaControllerInUse(ctx context.Context, areaControllerID uint) (bool, error) IsAreaControllerInUse(ctx context.Context, areaControllerID uint32) (bool, error)
} }
// gormDeviceRepository 是 DeviceRepository 的 GORM 实现 // gormDeviceRepository 是 DeviceRepository 的 GORM 实现
@@ -73,7 +73,7 @@ func (r *gormDeviceRepository) Create(ctx context.Context, device *models.Device
} }
// FindByID 根据 ID 查找设备 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
var device models.Device var device models.Device
if err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").First(&device, id).Error; err != nil { 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列表获取设备 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "GetDevicesByIDsTx")
var devices []models.Device var devices []models.Device
if len(ids) == 0 { if len(ids) == 0 {
@@ -98,14 +98,13 @@ func (r *gormDeviceRepository) GetDevicesByIDsTx(ctx context.Context, tx *gorm.D
// FindByIDString 根据字符串形式的主键 ID 查找设备 // FindByIDString 根据字符串形式的主键 ID 查找设备
func (r *gormDeviceRepository) FindByIDString(ctx context.Context, id string) (*models.Device, error) { func (r *gormDeviceRepository) FindByIDString(ctx context.Context, id string) (*models.Device, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByIDString") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByIDString")
// 将字符串ID转换为uint64
idInt, err := strconv.ParseUint(id, 10, 64) idInt, err := strconv.ParseUint(id, 10, 64)
if err != nil { if err != nil {
// 如果转换失败说明ID格式不正确返回一个明确的错误 // 如果转换失败说明ID格式不正确返回一个明确的错误
return nil, fmt.Errorf("无效的设备ID格式: %w", err) return nil, fmt.Errorf("无效的设备ID格式: %w", err)
} }
// 调用已有的 FindByID 方法 // 调用已有的 FindByID 方法
return r.FindByID(repoCtx, uint(idInt)) return r.FindByID(repoCtx, uint32(idInt))
} }
// ListAll 获取所有设备的列表 // ListAll 获取所有设备的列表
@@ -133,7 +132,7 @@ func (r *gormDeviceRepository) ListAllSensors(ctx context.Context) ([]*models.De
} }
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "ListByAreaControllerID")
var devices []*models.Device var devices []*models.Device
err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").Where("area_controller_id = ?", areaControllerID).Find(&devices).Error 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查找所有使用该模板的设备 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByDeviceTemplateID")
var devices []*models.Device var devices []*models.Device
err := r.db.WithContext(repoCtx).Where("device_template_id = ?", deviceTemplateID).Find(&devices).Error 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 删除一个设备 // Delete 根据 ID 删除一个设备
// GORM 使用软删除,记录不会从数据库中物理移除,而是设置 DeletedAt 字段。 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "Delete")
return r.db.WithContext(repoCtx).Delete(&models.Device{}, id).Error return r.db.WithContext(repoCtx).Delete(&models.Device{}, id).Error
} }
// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByAreaControllerAndPhysicalAddress")
var device models.Device var device models.Device
err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate"). err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").
@@ -184,12 +183,22 @@ func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(ctx contex
return &device, nil return &device, nil
} }
// IsDeviceInUse 检查设备是否被任何任务使用 // IsDeviceInUse 检查设备是否被任何任务使用,可以忽略指定任务类型
func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint) (bool, error) { func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint32, ignoredTaskTypes []models.TaskType) (bool, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsDeviceInUse") repoCtx := logs.AddFuncName(ctx, r.ctx, "IsDeviceInUse")
var count int64 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 { if err != nil {
return false, fmt.Errorf("查询设备任务关联失败: %w", err) return false, fmt.Errorf("查询设备任务关联失败: %w", err)
} }
@@ -197,7 +206,7 @@ func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint)
} }
// IsAreaControllerInUse 检查区域主控是否被任何设备使用 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "IsAreaControllerInUse")
var count int64 var count int64
if err := r.db.WithContext(repoCtx).Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil { if err := r.db.WithContext(repoCtx).Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil {

View File

@@ -14,12 +14,12 @@ import (
// DeviceTemplateRepository 定义了设备模板数据访问的接口 // DeviceTemplateRepository 定义了设备模板数据访问的接口
type DeviceTemplateRepository interface { type DeviceTemplateRepository interface {
Create(ctx context.Context, deviceTemplate *models.DeviceTemplate) error 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) FindByName(ctx context.Context, name string) (*models.DeviceTemplate, error)
ListAll(ctx context.Context) ([]*models.DeviceTemplate, error) ListAll(ctx context.Context) ([]*models.DeviceTemplate, error)
Update(ctx context.Context, deviceTemplate *models.DeviceTemplate) error Update(ctx context.Context, deviceTemplate *models.DeviceTemplate) error
Delete(ctx context.Context, id uint) error Delete(ctx context.Context, id uint32) error
IsInUse(ctx context.Context, id uint) (bool, error) IsInUse(ctx context.Context, id uint32) (bool, error)
} }
// gormDeviceTemplateRepository 是 DeviceTemplateRepository 的 GORM 实现 // gormDeviceTemplateRepository 是 DeviceTemplateRepository 的 GORM 实现
@@ -40,7 +40,7 @@ func (r *gormDeviceTemplateRepository) Create(ctx context.Context, deviceTemplat
} }
// FindByID 根据ID查找设备模板 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
var deviceTemplate models.DeviceTemplate var deviceTemplate models.DeviceTemplate
if err := r.db.WithContext(repoCtx).First(&deviceTemplate, id).Error; err != nil { 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 检查设备模板是否正在被设备使用 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "IsInUse")
var count int64 var count int64
if err := r.db.WithContext(repoCtx).Model(&models.Device{}).Where("device_template_id = ?", id).Count(&count).Error; err != nil { 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 软删除数据库中的设备模板 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "Delete")
if err := r.db.WithContext(repoCtx).Delete(&models.DeviceTemplate{}, id).Error; err != nil { if err := r.db.WithContext(repoCtx).Delete(&models.DeviceTemplate{}, id).Error; err != nil {
return fmt.Errorf("删除设备模板失败: %w", err) return fmt.Errorf("删除设备模板失败: %w", err)

View File

@@ -13,7 +13,7 @@ import (
// PlanExecutionLogListOptions 定义了查询计划执行日志时的可选参数 // PlanExecutionLogListOptions 定义了查询计划执行日志时的可选参数
type PlanExecutionLogListOptions struct { type PlanExecutionLogListOptions struct {
PlanID *uint PlanID *uint32
Status *models.ExecutionStatus Status *models.ExecutionStatus
StartTime *time.Time // 基于 created_at 字段 StartTime *time.Time // 基于 created_at 字段
EndTime *time.Time // 基于 created_at 字段 EndTime *time.Time // 基于 created_at 字段
@@ -22,7 +22,7 @@ type PlanExecutionLogListOptions struct {
// TaskExecutionLogListOptions 定义了查询任务执行日志时的可选参数 // TaskExecutionLogListOptions 定义了查询任务执行日志时的可选参数
type TaskExecutionLogListOptions struct { type TaskExecutionLogListOptions struct {
PlanExecutionLogID *uint PlanExecutionLogID *uint32
TaskID *int TaskID *int
Status *models.ExecutionStatus Status *models.ExecutionStatus
StartTime *time.Time // 基于 created_at 字段 StartTime *time.Time // 基于 created_at 字段
@@ -33,26 +33,26 @@ type TaskExecutionLogListOptions struct {
// ExecutionLogRepository 定义了与执行日志交互的接口。 // ExecutionLogRepository 定义了与执行日志交互的接口。
type ExecutionLogRepository interface { type ExecutionLogRepository interface {
// --- Existing methods --- // --- Existing methods ---
UpdateTaskExecutionLogStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error UpdateTaskExecutionLogStatusByIDs(ctx context.Context, logIDs []uint32, status models.ExecutionStatus) error
UpdateTaskExecutionLogStatus(ctx context.Context, logID uint, status models.ExecutionStatus) error UpdateTaskExecutionLogStatus(ctx context.Context, logID uint32, status models.ExecutionStatus) error
CreateTaskExecutionLog(ctx context.Context, log *models.TaskExecutionLog) error CreateTaskExecutionLog(ctx context.Context, log *models.TaskExecutionLog) error
CreatePlanExecutionLog(ctx context.Context, log *models.PlanExecutionLog) error CreatePlanExecutionLog(ctx context.Context, log *models.PlanExecutionLog) error
UpdatePlanExecutionLog(ctx context.Context, log *models.PlanExecutionLog) error UpdatePlanExecutionLog(ctx context.Context, log *models.PlanExecutionLog) error
CreateTaskExecutionLogsInBatch(ctx context.Context, logs []*models.TaskExecutionLog) error CreateTaskExecutionLogsInBatch(ctx context.Context, logs []*models.TaskExecutionLog) error
UpdateTaskExecutionLog(ctx context.Context, log *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 更新计划执行日志的状态
UpdatePlanExecutionLogStatus(ctx context.Context, logID uint, status models.ExecutionStatus) error UpdatePlanExecutionLogStatus(ctx context.Context, logID uint32, status models.ExecutionStatus) error
// UpdatePlanExecutionLogsStatusByIDs 批量更新计划执行日志的状态 // UpdatePlanExecutionLogsStatusByIDs 批量更新计划执行日志的状态
UpdatePlanExecutionLogsStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error UpdatePlanExecutionLogsStatusByIDs(ctx context.Context, logIDs []uint32, status models.ExecutionStatus) error
// FindIncompletePlanExecutionLogs 查找所有未完成的计划执行日志 // FindIncompletePlanExecutionLogs 查找所有未完成的计划执行日志
FindIncompletePlanExecutionLogs(ctx context.Context) ([]models.PlanExecutionLog, error) FindIncompletePlanExecutionLogs(ctx context.Context) ([]models.PlanExecutionLog, error)
// FindInProgressPlanExecutionLogByPlanID 根据 PlanID 查找正在进行的计划执行日志 // FindInProgressPlanExecutionLogByPlanID 根据 PlanID 查找正在进行的计划执行日志
FindInProgressPlanExecutionLogByPlanID(ctx context.Context, planID uint) (*models.PlanExecutionLog, error) FindInProgressPlanExecutionLogByPlanID(ctx context.Context, planID uint32) (*models.PlanExecutionLog, error)
// FindIncompleteTaskExecutionLogsByPlanLogID 根据计划日志ID查找所有未完成的任务日志 // 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 将所有状态为 ExecutionStatusStarted 和 ExecutionStatusWaiting 的计划状态都修改为 ExecutionStatusFailed
FailAllIncompletePlanExecutionLogs(ctx context.Context) error FailAllIncompletePlanExecutionLogs(ctx context.Context) error
@@ -60,16 +60,16 @@ type ExecutionLogRepository interface {
CancelAllIncompleteTaskExecutionLogs(ctx context.Context) error CancelAllIncompleteTaskExecutionLogs(ctx context.Context) error
// FindPlanExecutionLogByID 根据ID查找计划执行日志 // FindPlanExecutionLogByID 根据ID查找计划执行日志
FindPlanExecutionLogByID(ctx context.Context, id uint) (*models.PlanExecutionLog, error) FindPlanExecutionLogByID(ctx context.Context, id uint32) (*models.PlanExecutionLog, error)
// CountIncompleteTasksByPlanLogID 计算一个计划执行中未完成的任务数量 // CountIncompleteTasksByPlanLogID 计算一个计划执行中未完成的任务数量
CountIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint) (int64, error) CountIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint32) (int64, error)
// FailPlanExecution 将指定的计划执行标记为失败 // FailPlanExecution 将指定的计划执行标记为失败
FailPlanExecution(ctx context.Context, planLogID uint, errorMessage string) error FailPlanExecution(ctx context.Context, planLogID uint32, errorMessage string) error
// CancelIncompleteTasksByPlanLogID 取消一个计划执行中的所有未完成任务 // CancelIncompleteTasksByPlanLogID 取消一个计划执行中的所有未完成任务
CancelIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint, reason string) error CancelIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint32, reason string) error
// --- New methods --- // --- New methods ---
ListPlanExecutionLogs(ctx context.Context, opts PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, int64, error) 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 --- // --- 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateTaskExecutionLogStatusByIDs")
if len(logIDs) == 0 { if len(logIDs) == 0 {
return nil 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 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateTaskExecutionLogStatus")
return r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).Where("id = ?", logID).Update("status", status).Error 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 查找单个任务执行日志。 // FindTaskExecutionLogByID 根据 ID 查找单个任务执行日志。
// 它会预加载关联的 Task 信息。 // 它会预加载关联的 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindTaskExecutionLogByID")
var log models.TaskExecutionLog var log models.TaskExecutionLog
// 使用 Preload("Task") 来确保关联的任务信息被一并加载 // 使用 Preload("Task") 来确保关联的任务信息被一并加载
@@ -240,13 +240,13 @@ func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(ctx context.Contex
} }
// UpdatePlanExecutionLogStatus 更新计划执行日志的状态 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanExecutionLogStatus")
return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}).Where("id = ?", logID).Update("status", status).Error return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}).Where("id = ?", logID).Update("status", status).Error
} }
// UpdatePlanExecutionLogsStatusByIDs 批量更新计划执行日志的状态 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanExecutionLogsStatusByIDs")
if len(logIDs) == 0 { if len(logIDs) == 0 {
return nil return nil
@@ -263,7 +263,7 @@ func (r *gormExecutionLogRepository) FindIncompletePlanExecutionLogs(ctx context
} }
// FindInProgressPlanExecutionLogByPlanID 根据 PlanID 查找正在进行的计划执行日志 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindInProgressPlanExecutionLogByPlanID")
var log models.PlanExecutionLog var log models.PlanExecutionLog
err := r.db.WithContext(repoCtx).Where("plan_id = ? AND status = ?", planID, models.ExecutionStatusStarted).First(&log).Error 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查找所有未完成的任务日志 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindIncompleteTaskExecutionLogsByPlanLogID")
var logs []models.TaskExecutionLog var logs []models.TaskExecutionLog
err := r.db.WithContext(repoCtx).Where("plan_execution_log_id = ? AND (status = ? OR status = ?)", 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查找计划执行日志 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPlanExecutionLogByID")
var log models.PlanExecutionLog var log models.PlanExecutionLog
err := r.db.WithContext(repoCtx).First(&log, id).Error err := r.db.WithContext(repoCtx).First(&log, id).Error
@@ -315,7 +315,7 @@ func (r *gormExecutionLogRepository) FindPlanExecutionLogByID(ctx context.Contex
} }
// CountIncompleteTasksByPlanLogID 计算一个计划执行中未完成的任务数量 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "CountIncompleteTasksByPlanLogID")
var count int64 var count int64
err := r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}). err := r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).
@@ -326,7 +326,7 @@ func (r *gormExecutionLogRepository) CountIncompleteTasksByPlanLogID(ctx context
} }
// FailPlanExecution 将指定的计划执行标记为失败 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "FailPlanExecution")
return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}). return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}).
Where("id = ?", planLogID). Where("id = ?", planLogID).
@@ -338,7 +338,7 @@ func (r *gormExecutionLogRepository) FailPlanExecution(ctx context.Context, plan
} }
// CancelIncompleteTasksByPlanLogID 取消一个计划执行中的所有未完成任务 // 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") repoCtx := logs.AddFuncName(ctx, r.ctx, "CancelIncompleteTasksByPlanLogID")
return r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}). return r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).
Where("plan_execution_log_id = ? AND status IN (?, ?)", Where("plan_execution_log_id = ? AND status IN (?, ?)",

View File

@@ -12,10 +12,10 @@ import (
// MedicationLogListOptions 定义了查询用药记录时的可选参数 // MedicationLogListOptions 定义了查询用药记录时的可选参数
type MedicationLogListOptions struct { type MedicationLogListOptions struct {
PigBatchID *uint PigBatchID *uint32
MedicationID *uint MedicationID *uint32
Reason *models.MedicationReasonType Reason *models.MedicationReasonType
OperatorID *uint OperatorID *uint32
StartTime *time.Time StartTime *time.Time
EndTime *time.Time EndTime *time.Time
OrderBy string // 例如 "happened_at desc" OrderBy string // 例如 "happened_at desc"

View File

@@ -6,7 +6,6 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "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/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/notify"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"gorm.io/gorm" "gorm.io/gorm"
@@ -14,8 +13,8 @@ import (
// NotificationListOptions 定义了查询通知列表时的可选参数 // NotificationListOptions 定义了查询通知列表时的可选参数
type NotificationListOptions struct { type NotificationListOptions struct {
UserID *uint // 按用户ID过滤 UserID *uint32 // 按用户ID过滤
NotifierType *notify.NotifierType // 按通知器类型过滤 NotifierType *models.NotifierType // 按通知器类型过滤
Status *models.NotificationStatus // 按通知状态过滤 (例如:"success", "failed") Status *models.NotificationStatus // 按通知状态过滤 (例如:"success", "failed")
Level *zapcore.Level // 按通知等级过滤 (例如:"info", "warning", "error") Level *zapcore.Level // 按通知等级过滤 (例如:"info", "warning", "error")
StartTime *time.Time // 通知内容生成时间范围 - 开始时间 (对应 AlarmTimestamp) StartTime *time.Time // 通知内容生成时间范围 - 开始时间 (对应 AlarmTimestamp)

View File

@@ -12,7 +12,7 @@ import (
// PendingCollectionListOptions 定义了查询待采集请求时的可选参数 // PendingCollectionListOptions 定义了查询待采集请求时的可选参数
type PendingCollectionListOptions struct { type PendingCollectionListOptions struct {
DeviceID *uint DeviceID *uint32
Status *models.PendingCollectionStatus Status *models.PendingCollectionStatus
StartTime *time.Time // 基于 created_at 字段 StartTime *time.Time // 基于 created_at 字段
EndTime *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