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:
interval: 1 # 采集间隔 (分钟)
# 告警通知配置
alarm_notification:
notification_intervals: # 告警通知间隔(分钟)
debug: 1
info: 1
warn: 1
error: 1
dpanic: 1
panic: 1
fatal: 1

View File

@@ -91,3 +91,14 @@ lora_mesh:
# 定时采集配置
collection:
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
- CodeInternalError
- CodeServiceUnavailable
dto.ActiveAlarmDTO:
properties:
alarm_code:
$ref: '#/definitions/models.AlarmCode'
alarm_details:
type: string
alarm_summary:
type: string
created_at:
type: string
id:
type: integer
ignored_until:
type: string
is_ignored:
type: boolean
last_notified_at:
type: string
level:
$ref: '#/definitions/models.SeverityLevel'
source_id:
type: integer
source_type:
$ref: '#/definitions/models.AlarmSourceType'
trigger_time:
type: string
updated_at:
type: string
type: object
dto.AreaControllerResponse:
properties:
created_at:
@@ -73,6 +102,21 @@ definitions:
updated_at:
type: string
type: object
dto.AreaThresholdAlarmDTO:
properties:
area_controller_id:
type: integer
id:
type: integer
level:
$ref: '#/definitions/models.SeverityLevel'
operator:
$ref: '#/definitions/models.Operator'
sensor_type:
$ref: '#/definitions/models.SensorType'
thresholds:
type: number
type: object
dto.AssignEmptyPensToBatchRequest:
properties:
pen_ids:
@@ -137,6 +181,32 @@ definitions:
- name
- network_id
type: object
dto.CreateAreaThresholdAlarmDTO:
properties:
area_controller_id:
description: 区域主控ID
type: integer
level:
allOf:
- $ref: '#/definitions/models.SeverityLevel'
description: 告警等级,可选
operator:
allOf:
- $ref: '#/definitions/models.Operator'
description: 操作符
sensor_type:
allOf:
- $ref: '#/definitions/models.SensorType'
description: 传感器类型
thresholds:
description: 阈值
type: number
required:
- area_controller_id
- operator
- sensor_type
- thresholds
type: object
dto.CreateDeviceRequest:
properties:
area_controller_id:
@@ -177,6 +247,32 @@ definitions:
- commands
- name
type: object
dto.CreateDeviceThresholdAlarmDTO:
properties:
device_id:
description: 设备ID
type: integer
level:
allOf:
- $ref: '#/definitions/models.SeverityLevel'
description: 告警等级,可选,如果未提供则使用默认值
operator:
allOf:
- $ref: '#/definitions/models.Operator'
description: 操作符 (使用string类型与前端交互更通用)
sensor_type:
allOf:
- $ref: '#/definitions/models.SensorType'
description: 传感器类型
thresholds:
description: 阈值
type: number
required:
- device_id
- operator
- sensor_type
- thresholds
type: object
dto.CreatePenRequest:
properties:
capacity:
@@ -251,6 +347,15 @@ definitions:
example: newuser
type: string
type: object
dto.DeleteDeviceThresholdAlarmDTO:
properties:
sensor_type:
allOf:
- $ref: '#/definitions/models.SensorType'
description: 传感器类型
required:
- sensor_type
type: object
dto.DeviceCommandLogDTO:
properties:
acknowledged_at:
@@ -312,6 +417,21 @@ definitions:
$ref: '#/definitions/models.ValueDescriptor'
type: array
type: object
dto.DeviceThresholdAlarmDTO:
properties:
device_id:
type: integer
id:
type: integer
level:
$ref: '#/definitions/models.SeverityLevel'
operator:
$ref: '#/definitions/models.Operator'
sensor_type:
$ref: '#/definitions/models.SensorType'
thresholds:
type: number
type: object
dto.FeedFormulaDTO:
properties:
id:
@@ -340,6 +460,40 @@ definitions:
remarks:
type: string
type: object
dto.HistoricalAlarmDTO:
properties:
alarm_code:
$ref: '#/definitions/models.AlarmCode'
alarm_details:
type: string
alarm_summary:
type: string
id:
type: integer
level:
$ref: '#/definitions/models.SeverityLevel'
resolve_method:
type: string
resolve_time:
type: string
resolved_by:
type: integer
source_id:
type: integer
source_type:
$ref: '#/definitions/models.AlarmSourceType'
trigger_time:
type: string
type: object
dto.ListActiveAlarmResponse:
properties:
list:
items:
$ref: '#/definitions/dto.ActiveAlarmDTO'
type: array
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListDeviceCommandLogResponse:
properties:
list:
@@ -358,6 +512,15 @@ definitions:
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListHistoricalAlarmResponse:
properties:
list:
items:
$ref: '#/definitions/dto.HistoricalAlarmDTO'
type: array
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListMedicationLogResponse:
properties:
list:
@@ -600,11 +763,11 @@ definitions:
id:
type: integer
level:
$ref: '#/definitions/zapcore.Level'
$ref: '#/definitions/models.SeverityLevel'
message:
type: string
notifier_type:
$ref: '#/definitions/notify.NotifierType'
$ref: '#/definitions/models.NotifierType'
status:
$ref: '#/definitions/models.NotificationStatus'
title:
@@ -1199,26 +1362,35 @@ definitions:
properties:
type:
allOf:
- $ref: '#/definitions/notify.NotifierType'
- $ref: '#/definitions/models.NotifierType'
description: Type 指定要测试的通知渠道
required:
- type
type: object
dto.SensorDataDTO:
properties:
area_controller_id:
type: integer
data:
items:
type: integer
type: array
device_id:
type: integer
regional_controller_id:
type: integer
sensor_type:
$ref: '#/definitions/models.SensorType'
time:
type: string
type: object
dto.SnoozeAlarmRequest:
properties:
duration_minutes:
description: 忽略时长,单位分钟
minimum: 1
type: integer
required:
- duration_minutes
type: object
dto.SubPlanResponse:
properties:
child_plan:
@@ -1373,6 +1545,23 @@ definitions:
- name
- network_id
type: object
dto.UpdateAreaThresholdAlarmDTO:
properties:
level:
allOf:
- $ref: '#/definitions/models.SeverityLevel'
description: 新的告警等级,可选
operator:
allOf:
- $ref: '#/definitions/models.Operator'
description: 新的操作符
thresholds:
description: 新的阈值
type: number
required:
- operator
- thresholds
type: object
dto.UpdateDeviceRequest:
properties:
area_controller_id:
@@ -1413,6 +1602,23 @@ definitions:
- commands
- name
type: object
dto.UpdateDeviceThresholdAlarmDTO:
properties:
level:
allOf:
- $ref: '#/definitions/models.SeverityLevel'
description: 新的告警等级,可选
operator:
allOf:
- $ref: '#/definitions/models.Operator'
description: 新的操作符
thresholds:
description: 新的阈值
type: number
required:
- operator
- thresholds
type: object
dto.UpdatePenRequest:
properties:
capacity:
@@ -1558,6 +1764,34 @@ definitions:
weight:
type: number
type: object
models.AlarmCode:
enum:
- 温度阈值
- 湿度阈值
- 重量阈值
- 电池电量阈值
- 信号强度阈值
- 设备离线
- 区域主控离线
type: string
x-enum-varnames:
- AlarmCodeTemperature
- AlarmCodeHumidity
- AlarmCodeWeight
- AlarmCodeBatteryLevel
- AlarmCodeSignalMetrics
- AlarmCodeDeviceOffline
- AlarmCodeAreaControllerOffline
models.AlarmSourceType:
enum:
- 普通设备
- 区域主控
- 系统
type: string
x-enum-varnames:
- AlarmSourceTypeDevice
- AlarmSourceTypeAreaController
- AlarmSourceTypeSystem
models.AuditStatus:
enum:
- 成功
@@ -1646,6 +1880,34 @@ definitions:
- NotificationStatusSuccess
- NotificationStatusFailed
- NotificationStatusSkipped
models.NotifierType:
enum:
- 邮件
- 企业微信
- 飞书
- 日志
type: string
x-enum-varnames:
- NotifierTypeSMTP
- NotifierTypeWeChat
- NotifierTypeLark
- NotifierTypeLog
models.Operator:
enum:
- <
- <=
- '>'
- '>='
- =
- '!='
type: string
x-enum-varnames:
- OperatorLessThan
- OperatorLessThanOrEqualTo
- OperatorGreaterThan
- OperatorGreaterThanOrEqualTo
- OperatorEqualTo
- OperatorNotEqualTo
models.PenStatus:
enum:
- 空闲
@@ -1877,6 +2139,24 @@ definitions:
- SensorTypeTemperature
- SensorTypeHumidity
- SensorTypeWeight
models.SeverityLevel:
enum:
- Debug
- Info
- Warn
- Error
- DPanic
- Panic
- Fatal
type: string
x-enum-varnames:
- DebugLevel
- InfoLevel
- WarnLevel
- ErrorLevel
- DPanicLevel
- PanicLevel
- FatalLevel
models.StockLogSourceType:
enum:
- 采购入库
@@ -1899,9 +2179,15 @@ definitions:
- 等待
- 下料
- 全量采集
- 告警通知
- 设备阈值检查
- 区域阈值检查
type: string
x-enum-comments:
TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务
TaskTypeAlarmNotification: 告警通知任务
TaskTypeAreaCollectorThresholdCheck: 区域阈值检查任务
TaskTypeDeviceThresholdCheck: 设备阈值检查任务
TaskTypeFullCollection: 新增的全量采集任务
TaskTypeReleaseFeedWeight: 下料口释放指定重量任务
TaskTypeWaiting: 等待任务
@@ -1910,11 +2196,17 @@ definitions:
- 等待任务
- 下料口释放指定重量任务
- 新增的全量采集任务
- 告警通知任务
- 设备阈值检查任务
- 区域阈值检查任务
x-enum-varnames:
- TaskPlanAnalysis
- TaskTypeWaiting
- TaskTypeReleaseFeedWeight
- TaskTypeFullCollection
- TaskTypeAlarmNotification
- TaskTypeDeviceThresholdCheck
- TaskTypeAreaCollectorThresholdCheck
models.ValueDescriptor:
properties:
multiplier:
@@ -1926,18 +2218,6 @@ definitions:
type:
$ref: '#/definitions/models.SensorType'
type: object
notify.NotifierType:
enum:
- 邮件
- 企业微信
- 飞书
- 日志
type: string
x-enum-varnames:
- NotifierTypeSMTP
- NotifierTypeWeChat
- NotifierTypeLark
- NotifierTypeLog
repository.PlanTypeFilter:
enum:
- 所有任务
@@ -1950,7 +2230,6 @@ definitions:
- PlanTypeFilterSystem
zapcore.Level:
enum:
- 7
- -1
- 0
- 1
@@ -1961,10 +2240,10 @@ definitions:
- -1
- 5
- 6
- 7
format: int32
type: integer
x-enum-varnames:
- _numLevels
- DebugLevel
- InfoLevel
- WarnLevel
@@ -1975,6 +2254,7 @@ definitions:
- _minLevel
- _maxLevel
- InvalidLevel
- _numLevels
info:
contact:
email: divano@example.com
@@ -1987,6 +2267,428 @@ info:
title: 猪场管理系统 API
version: "1.0"
paths:
/api/v1/alarm/threshold/{id}/cancel-snooze:
post:
consumes:
- application/json
description: 根据告警ID取消对一个阈值告警的忽略状态
parameters:
- description: 告警ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: 成功取消忽略告警
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 取消忽略阈值告警
tags:
- 告警管理
/api/v1/alarm/threshold/{id}/snooze:
post:
consumes:
- application/json
description: 根据告警ID忽略一个活跃的阈值告警或更新其忽略时间
parameters:
- description: 告警ID
in: path
name: id
required: true
type: string
- description: 忽略告警请求体
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SnoozeAlarmRequest'
produces:
- application/json
responses:
"200":
description: 成功忽略告警
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 忽略阈值告警
tags:
- 告警管理
/api/v1/alarm/threshold/active-alarms:
get:
consumes:
- application/json
description: 根据过滤条件和分页参数查询活跃告警列表
parameters:
- description: 告警触发时间范围 - 结束时间
in: query
name: end_time
type: string
- description: 按是否被忽略过滤
in: query
name: is_ignored
type: boolean
- description: 按告警严重性等级过滤
enum:
- Debug
- Info
- Warn
- Error
- DPanic
- Panic
- Fatal
in: query
name: level
type: string
x-enum-varnames:
- DebugLevel
- InfoLevel
- WarnLevel
- ErrorLevel
- DPanicLevel
- PanicLevel
- FatalLevel
- description: 排序字段,例如 "trigger_time DESC"
in: query
name: order_by
type: string
- in: query
name: page
type: integer
- in: query
name: page_size
type: integer
- description: 按告警来源ID过滤
in: query
name: source_id
type: integer
- description: 按告警来源类型过滤
enum:
- 普通设备
- 区域主控
- 系统
in: query
name: source_type
type: string
x-enum-varnames:
- AlarmSourceTypeDevice
- AlarmSourceTypeAreaController
- AlarmSourceTypeSystem
- description: 告警触发时间范围 - 开始时间
in: query
name: trigger_time
type: string
produces:
- application/json
responses:
"200":
description: 成功获取活跃告警列表
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.ListActiveAlarmResponse'
type: object
security:
- BearerAuth: []
summary: 批量查询活跃告警
tags:
- 告警管理
/api/v1/alarm/threshold/area:
post:
consumes:
- application/json
description: 为指定的区域主控创建一个新的阈值告警规则
parameters:
- description: 创建区域阈值告警请求体
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.CreateAreaThresholdAlarmDTO'
produces:
- application/json
responses:
"200":
description: 成功创建区域阈值告警
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 创建区域阈值告警
tags:
- 告警管理
/api/v1/alarm/threshold/area/{task_id}:
delete:
description: 根据任务ID删除区域阈值告警规则
parameters:
- description: 任务ID
in: path
name: task_id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: 成功删除区域阈值告警
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 删除区域阈值告警
tags:
- 告警管理
get:
description: 根据任务ID获取单个区域阈值告警规则的详细信息
parameters:
- description: 任务ID
in: path
name: task_id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: 成功获取区域阈值告警
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.AreaThresholdAlarmDTO'
type: object
security:
- BearerAuth: []
summary: 获取区域阈值告警
tags:
- 告警管理
put:
consumes:
- application/json
description: 根据任务ID更新已存在的区域阈值告警规则
parameters:
- description: 任务ID
in: path
name: task_id
required: true
type: integer
- description: 更新区域阈值告警请求体
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.UpdateAreaThresholdAlarmDTO'
produces:
- application/json
responses:
"200":
description: 成功更新区域阈值告警
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 更新区域阈值告警
tags:
- 告警管理
/api/v1/alarm/threshold/device:
post:
consumes:
- application/json
description: 为单个设备创建一条新的阈值告警规则
parameters:
- description: 创建设备阈值告警请求体
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.CreateDeviceThresholdAlarmDTO'
produces:
- application/json
responses:
"200":
description: 成功创建设备阈值告警
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 创建设备阈值告警
tags:
- 告警管理
/api/v1/alarm/threshold/device/{task_id}:
delete:
consumes:
- application/json
description: 根据任务ID删除设备阈值告警规则
parameters:
- description: 任务ID
in: path
name: task_id
required: true
type: integer
- description: 删除设备阈值告警请求体
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.DeleteDeviceThresholdAlarmDTO'
produces:
- application/json
responses:
"200":
description: 成功删除设备阈值告警
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 删除设备阈值告警
tags:
- 告警管理
get:
description: 根据任务ID获取单个设备阈值告警规则的详细信息
parameters:
- description: 任务ID
in: path
name: task_id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: 成功获取设备阈值告警
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.DeviceThresholdAlarmDTO'
type: object
security:
- BearerAuth: []
summary: 获取设备阈值告警
tags:
- 告警管理
put:
consumes:
- application/json
description: 根据任务ID更新已存在的设备阈值告警规则
parameters:
- description: 任务ID
in: path
name: task_id
required: true
type: integer
- description: 更新设备阈值告警请求体
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.UpdateDeviceThresholdAlarmDTO'
produces:
- application/json
responses:
"200":
description: 成功更新设备阈值告警
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 更新设备阈值告警
tags:
- 告警管理
/api/v1/alarm/threshold/historical-alarms:
get:
consumes:
- application/json
description: 根据过滤条件和分页参数查询历史告警列表
parameters:
- description: 按告警严重性等级过滤
enum:
- Debug
- Info
- Warn
- Error
- DPanic
- Panic
- Fatal
in: query
name: level
type: string
x-enum-varnames:
- DebugLevel
- InfoLevel
- WarnLevel
- ErrorLevel
- DPanicLevel
- PanicLevel
- FatalLevel
- description: 排序字段,例如 "trigger_time DESC"
in: query
name: order_by
type: string
- in: query
name: page
type: integer
- in: query
name: page_size
type: integer
- description: 告警解决时间范围 - 结束时间
in: query
name: resolve_time_end
type: string
- description: 告警解决时间范围 - 开始时间
in: query
name: resolve_time_start
type: string
- description: 按告警来源ID过滤
in: query
name: source_id
type: integer
- description: 按告警来源类型过滤
enum:
- 普通设备
- 区域主控
- 系统
in: query
name: source_type
type: string
x-enum-varnames:
- AlarmSourceTypeDevice
- AlarmSourceTypeAreaController
- AlarmSourceTypeSystem
- description: 告警触发时间范围 - 结束时间
in: query
name: trigger_time_end
type: string
- description: 告警触发时间范围 - 开始时间
in: query
name: trigger_time_start
type: string
produces:
- application/json
responses:
"200":
description: 成功获取历史告警列表
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.ListHistoricalAlarmResponse'
type: object
security:
- BearerAuth: []
summary: 批量查询历史告警
tags:
- 告警管理
/api/v1/area-controllers:
get:
description: 获取系统中所有区域主控的列表
@@ -2546,7 +3248,6 @@ paths:
name: end_time
type: string
- enum:
- 7
- -1
- 0
- 1
@@ -2557,12 +3258,12 @@ paths:
- -1
- 5
- 6
- 7
format: int32
in: query
name: level
type: integer
x-enum-varnames:
- _numLevels
- DebugLevel
- InfoLevel
- WarnLevel
@@ -2573,6 +3274,7 @@ paths:
- _minLevel
- _maxLevel
- InvalidLevel
- _numLevels
- enum:
- 邮件
- 企业微信

View File

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

View File

@@ -187,6 +187,32 @@ func (a *API) setupRoutes() {
monitorGroup.GET("/notifications", a.monitorController.ListNotifications)
}
logger.Debug("数据监控相关接口注册成功 (需要认证和审计)")
// 告警相关路由组
alarmGroup := authGroup.Group("/alarm")
{
thresholdGroup := alarmGroup.Group("/thresholds")
{
thresholdGroup.POST("/:id/snooze", a.alarmController.SnoozeThresholdAlarm) // 忽略阈值告警
thresholdGroup.POST("/:id/cancel-snooze", a.alarmController.CancelSnoozeThresholdAlarm) // 取消忽略阈值告警
thresholdGroup.GET("/active-alarms", a.alarmController.ListActiveAlarms) // 获取活跃告警
thresholdGroup.GET("/historical-alarms", a.alarmController.ListHistoricalAlarms) // 获取历史告警
// 设备阈值告警配置
thresholdGroup.POST("/device", a.alarmController.CreateDeviceThresholdAlarm)
thresholdGroup.GET("/device/:task_id", a.alarmController.GetDeviceThresholdAlarm)
thresholdGroup.PUT("/device/:task_id", a.alarmController.UpdateDeviceThresholdAlarm)
thresholdGroup.DELETE("/device/:task_id", a.alarmController.DeleteDeviceThresholdAlarm)
// 区域阈值告警配置
thresholdGroup.POST("/area", a.alarmController.CreateAreaThresholdAlarm)
thresholdGroup.GET("/area/:task_id", a.alarmController.GetAreaThresholdAlarm)
thresholdGroup.PUT("/area/:task_id", a.alarmController.UpdateAreaThresholdAlarm)
thresholdGroup.DELETE("/area/:task_id", a.alarmController.DeleteAreaThresholdAlarm)
}
}
logger.Debug("告警相关接口注册成功 (需要认证和审计)")
}
logger.Debug("所有接口注册成功")

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。
// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 ID 字段。
func GetOperatorIDFromContext(c echo.Context) (uint, error) {
func GetOperatorIDFromContext(c echo.Context) (uint32, error) {
userVal := c.Get(models.ContextUserKey.String())
if userVal == nil {
return 0, ErrUserNotFoundInContext

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)
}
resp, err := c.deviceService.GetDevice(reqCtx, uint(id))
resp, err := c.deviceService.GetDevice(reqCtx, uint32(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
@@ -149,7 +149,7 @@ func (c *Controller) UpdateDevice(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
}
resp, err := c.deviceService.UpdateDevice(reqCtx, uint(id), &req)
resp, err := c.deviceService.UpdateDevice(reqCtx, uint32(id), &req)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
@@ -184,7 +184,7 @@ func (c *Controller) DeleteDevice(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
}
if err := c.deviceService.DeleteDevice(reqCtx, uint(id)); err != nil {
if err := c.deviceService.DeleteDevice(reqCtx, uint32(id)); err != nil {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
@@ -232,7 +232,7 @@ func (c *Controller) ManualControl(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
}
if err := c.deviceService.ManualControl(reqCtx, uint(id), &req); err != nil {
if err := c.deviceService.ManualControl(reqCtx, uint32(id), &req); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
@@ -297,7 +297,7 @@ func (c *Controller) GetAreaController(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
}
resp, err := c.deviceService.GetAreaController(reqCtx, uint(id))
resp, err := c.deviceService.GetAreaController(reqCtx, uint32(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
@@ -361,7 +361,7 @@ func (c *Controller) UpdateAreaController(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
}
resp, err := c.deviceService.UpdateAreaController(reqCtx, uint(id), &req)
resp, err := c.deviceService.UpdateAreaController(reqCtx, uint32(id), &req)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
@@ -396,7 +396,7 @@ func (c *Controller) DeleteAreaController(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
}
if err := c.deviceService.DeleteAreaController(reqCtx, uint(id)); err != nil {
if err := c.deviceService.DeleteAreaController(reqCtx, uint32(id)); err != nil {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
@@ -467,7 +467,7 @@ func (c *Controller) GetDeviceTemplate(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
}
resp, err := c.deviceService.GetDeviceTemplate(reqCtx, uint(id))
resp, err := c.deviceService.GetDeviceTemplate(reqCtx, uint32(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
@@ -532,7 +532,7 @@ func (c *Controller) UpdateDeviceTemplate(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
}
resp, err := c.deviceService.UpdateDeviceTemplate(reqCtx, uint(id), &req)
resp, err := c.deviceService.UpdateDeviceTemplate(reqCtx, uint32(id), &req)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
@@ -567,7 +567,7 @@ func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
}
if err := c.deviceService.DeleteDeviceTemplate(reqCtx, uint(id)); err != nil {
if err := c.deviceService.DeleteDeviceTemplate(reqCtx, uint32(id)); err != nil {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ func (c *PigBatchController) RecordSickPigs(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordSickPigsRequest) error {
return c.service.RecordSickPigs(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
@@ -53,7 +53,7 @@ func (c *PigBatchController) RecordSickPigRecovery(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordSickPigRecoveryRequest) error {
return c.service.RecordSickPigRecovery(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
@@ -80,7 +80,7 @@ func (c *PigBatchController) RecordSickPigDeath(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordSickPigDeathRequest) error {
return c.service.RecordSickPigDeath(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
@@ -107,7 +107,7 @@ func (c *PigBatchController) RecordSickPigCull(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordSickPigCullRequest) error {
return c.service.RecordSickPigCull(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
@@ -134,7 +134,7 @@ func (c *PigBatchController) RecordDeath(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordDeathRequest) error {
return c.service.RecordDeath(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
},
"记录成功",
@@ -161,7 +161,7 @@ func (c *PigBatchController) RecordCull(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.RecordCullRequest) error {
return c.service.RecordCull(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
},
"记录成功",

View File

@@ -25,7 +25,7 @@ func (c *PigBatchController) SellPigs(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.SellPigsRequest) error {
return c.service.SellPigs(reqCtx, primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
},
"卖猪成功",
@@ -51,7 +51,7 @@ func (c *PigBatchController) BuyPigs(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.BuyPigsRequest) error {
return c.service.BuyPigs(reqCtx, primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
},
"买猪成功",

View File

@@ -28,18 +28,18 @@ func (c *PigBatchController) TransferPigsAcrossBatches(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.TransferPigsAcrossBatchesRequest) error {
// primaryID 在这里是 sourceBatchID
return c.service.TransferPigsAcrossBatches(reqCtx, primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
},
"调栏成功",
func(ctx echo.Context) (uint, error) { // 自定义ID提取器从 ":sourceBatchID" 路径参数提取
func(ctx echo.Context) (uint32, error) { // 自定义ID提取器从 ":sourceBatchID" 路径参数提取
idParam := ctx.Param("sourceBatchID")
parsedID, err := strconv.ParseUint(idParam, 10, 32)
if err != nil {
return 0, err
}
return uint(parsedID), nil
return uint32(parsedID), nil
},
)
}
@@ -63,7 +63,7 @@ func (c *PigBatchController) TransferPigsWithinBatch(ctx echo.Context) error {
return handleAPIRequest(
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
func(ctx echo.Context, operatorID uint32, primaryID uint32, req *dto.TransferPigsWithinBatchRequest) error {
// primaryID 在这里是 batchID
return c.service.TransferPigsWithinBatch(reqCtx, primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
},

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"))
}
house, err := c.service.GetPigHouseByID(reqCtx, uint(id))
house, err := c.service.GetPigHouseByID(reqCtx, uint32(id))
if err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
@@ -132,7 +132,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
}
house, err := c.service.UpdatePigHouse(reqCtx, uint(id), req.Name, req.Description)
house, err := c.service.UpdatePigHouse(reqCtx, uint32(id), req.Name, req.Description)
if err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
@@ -161,7 +161,7 @@ func (c *PigFarmController) DeletePigHouse(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
}
if err := c.service.DeletePigHouse(reqCtx, uint(id)); err != nil {
if err := c.service.DeletePigHouse(reqCtx, uint32(id)); err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
}
@@ -226,7 +226,7 @@ func (c *PigFarmController) GetPen(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
}
pen, err := c.service.GetPenByID(reqCtx, uint(id))
pen, err := c.service.GetPenByID(reqCtx, uint32(id))
if err != nil {
if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
@@ -282,7 +282,7 @@ func (c *PigFarmController) UpdatePen(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
}
pen, err := c.service.UpdatePen(reqCtx, uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
pen, err := c.service.UpdatePen(reqCtx, uint32(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
if err != nil {
if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
@@ -312,7 +312,7 @@ func (c *PigFarmController) DeletePen(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
}
if err := c.service.DeletePen(reqCtx, uint(id)); err != nil {
if err := c.service.DeletePen(reqCtx, uint32(id)); err != nil {
if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
}
@@ -351,7 +351,7 @@ func (c *PigFarmController) UpdatePenStatus(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
}
pen, err := c.service.UpdatePenStatus(reqCtx, uint(id), req.Status)
pen, err := c.service.UpdatePenStatus(reqCtx, uint32(id), req.Status)
if err != nil {
if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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/webhook"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
@@ -21,6 +22,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token"
"gorm.io/gorm"
)
@@ -29,7 +31,6 @@ type Infrastructure struct {
storage database.Storage
repos *Repositories
lora *LoraComponents
notifyService domain_notify.Service
tokenGenerator token.Generator
}
@@ -47,18 +48,12 @@ func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructur
return nil, err
}
notifyService, err := initNotifyService(ctx, cfg.Notify, repos.userRepo, repos.notificationRepo)
if err != nil {
return nil, fmt.Errorf("初始化通知服务失败: %w", err)
}
tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret))
return &Infrastructure{
storage: storage,
repos: repos,
lora: lora,
notifyService: notifyService,
tokenGenerator: tokenGenerator,
}, nil
}
@@ -86,6 +81,7 @@ type Repositories struct {
medicationLogRepo repository.MedicationLogRepository
rawMaterialRepo repository.RawMaterialRepository
notificationRepo repository.NotificationRepository
alarmRepo repository.AlarmRepository
unitOfWork repository.UnitOfWork
}
@@ -114,6 +110,7 @@ func initRepositories(ctx context.Context, db *gorm.DB) *Repositories {
medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), db),
rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db),
notificationRepo: repository.NewGormNotificationRepository(logs.AddCompName(baseCtx, "NotificationRepo"), db),
alarmRepo: repository.NewGormAlarmRepository(logs.AddCompName(baseCtx, "AlarmRepo"), db),
unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db),
}
}
@@ -129,12 +126,19 @@ type DomainServices struct {
planExecutionManager plan.ExecutionManager
analysisPlanTaskManager plan.AnalysisPlanTaskManager
planService plan.Service
notifyService domain_notify.Service
alarmService alarm.AlarmService
}
// initDomainServices 初始化所有的领域服务。
func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastructure) *DomainServices {
func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastructure) (*DomainServices, error) {
baseCtx := context.Background()
notifyService, err := initNotifyService(ctx, cfg.Notify, infra.repos.userRepo, infra.repos.notificationRepo)
if err != nil {
return nil, fmt.Errorf("初始化通知服务失败: %w", err)
}
// 猪群管理相关
pigPenTransferManager := pig.NewPigPenTransferManager(logs.AddCompName(baseCtx, "PigPenTransferManager"), infra.repos.pigPenRepo, infra.repos.pigTransferLogRepo, infra.repos.pigBatchRepo)
pigTradeManager := pig.NewPigTradeManager(logs.AddCompName(baseCtx, "PigTradeManager"), infra.repos.pigTradeRepo)
@@ -157,8 +161,22 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
infra.lora.comm,
)
// 告警服务
alarmService := alarm.NewAlarmService(
logs.AddCompName(baseCtx, "AlarmService"),
infra.repos.alarmRepo,
infra.repos.unitOfWork,
)
// 任务工厂
taskFactory := task.NewTaskFactory(logs.AddCompName(baseCtx, "TaskFactory"), infra.repos.sensorDataRepo, infra.repos.deviceRepo, generalDeviceService)
taskFactory := task.NewTaskFactory(logs.AddCompName(baseCtx, "TaskFactory"),
infra.repos.sensorDataRepo,
infra.repos.deviceRepo,
infra.repos.alarmRepo,
generalDeviceService,
notifyService,
alarmService,
)
// 计划任务管理器
analysisPlanTaskManager := plan.NewAnalysisPlanTaskManager(logs.AddCompName(baseCtx, "AnalysisPlanTaskManager"), infra.repos.planRepo, infra.repos.pendingTaskRepo, infra.repos.executionLogRepo)
@@ -199,7 +217,9 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
taskFactory: taskFactory,
planExecutionManager: planExecutionManager,
planService: planService,
}
notifyService: notifyService,
alarmService: alarmService,
}, nil
}
// AppServices 聚合了所有的应用服务实例。
@@ -211,6 +231,7 @@ type AppServices struct {
planService service.PlanService
userService service.UserService
auditService service.AuditService
thresholdAlarmService service.ThresholdAlarmService
}
// initAppServices 初始化所有的应用服务。
@@ -235,16 +256,30 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
infra.repos.pigTradeRepo,
infra.repos.notificationRepo,
)
// 初始化阈值告警服务
thresholdAlarmService := service.NewThresholdAlarmService(
logs.AddCompName(baseCtx, "ThresholdAlarmService"),
domainServices.alarmService,
domainServices.planService,
infra.repos.alarmRepo,
infra.repos.planRepo,
infra.repos.areaControllerRepo,
infra.repos.deviceRepo,
)
deviceService := service.NewDeviceService(
logs.AddCompName(baseCtx, "DeviceService"),
infra.repos.deviceRepo,
infra.repos.areaControllerRepo,
infra.repos.deviceTemplateRepo,
domainServices.generalDeviceService,
thresholdAlarmService,
)
auditService := service.NewAuditService(logs.AddCompName(baseCtx, "AuditService"), infra.repos.userActionLogRepo)
planService := service.NewPlanService(logs.AddCompName(baseCtx, "AppPlanService"), domainServices.planService)
userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, infra.notifyService)
userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, domainServices.notifyService)
return &AppServices{
pigFarmService: pigFarmService,
@@ -254,6 +289,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
auditService: auditService,
planService: planService,
userService: userService,
thresholdAlarmService: thresholdAlarmService,
}
}
@@ -353,7 +389,7 @@ func initNotifyService(
// 3. 动态确定首选通知器
var primaryNotifier notify.Notifier
primaryNotifierType := notify.NotifierType(cfg.Primary)
primaryNotifierType := models.NotifierType(cfg.Primary)
// 检查用户指定的主渠道是否已启用
for _, n := range availableNotifiers {

View File

@@ -4,16 +4,12 @@ import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
)
const (
// PlanNameTimedFullDataCollection 是定时全量数据采集计划的名称
PlanNameTimedFullDataCollection = "定时全量数据采集"
)
// initializeState 在应用启动时准备其初始数据状态。
// 它遵循一个严格的顺序:清理 -> 更新 -> 刷新,以确保数据的一致性和正确性。
func (app *Application) initializeState(ctx context.Context) error {
@@ -48,13 +44,11 @@ func (app *Application) initializeState(ctx context.Context) error {
}
// initializeSystemPlans 确保预定义的系统计划在数据库中存在并保持最新。
// 它通过调用各个独立的计划初始化方法来完成此操作。
func (app *Application) initializeSystemPlans(ctx context.Context) error {
appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializeSystemPlans")
logger.Info("开始检查并更新预定义的系统计划...")
// 动态构建预定义计划列表
predefinedSystemPlans := app.getPredefinedSystemPlans()
// 1. 获取所有已存在的系统计划
existingPlans, _, err := app.Infra.repos.planRepo.ListPlans(appCtx, repository.ListPlansOptions{
PlanType: repository.PlanTypeFilterSystem,
@@ -64,14 +58,86 @@ func (app *Application) initializeSystemPlans(ctx context.Context) error {
}
// 2. 为了方便查找, 将现有计划名放入一个 map
existingPlanMap := make(map[string]*models.Plan)
existingPlanMap := make(map[models.PlanName]*models.Plan)
for i := range existingPlans {
existingPlanMap[existingPlans[i].Name] = &existingPlans[i]
}
// 3. 遍历预定义的计划列表
for i := range predefinedSystemPlans {
predefinedPlan := &predefinedSystemPlans[i] // 获取可修改的指针
// 3. 调用独立的初始化方法来处理每个系统计划
if err := app.initializePeriodicSystemHealthCheckPlan(appCtx, existingPlanMap); err != nil {
return err // 如果任何一个计划初始化失败,则立即返回错误
}
if err := app.initializeAlarmNotificationPlan(appCtx, existingPlanMap); err != nil {
return err
}
logger.Info("预定义系统计划检查完成。")
return nil
}
// initializePeriodicSystemHealthCheckPlan 负责初始化 "周期性系统健康检查" 计划。
// 它会根据当前配置动态构建计划,并决定是创建新计划还是更新现有计划。
func (app *Application) initializePeriodicSystemHealthCheckPlan(ctx context.Context, existingPlanMap map[models.PlanName]*models.Plan) error {
appCtx, logger := logs.Trace(ctx, app.Ctx, "initializePeriodicSystemHealthCheckPlan")
// 根据配置创建定时全量采集计划
interval := app.Config.Collection.Interval
if interval <= 0 {
interval = 1 // 确保间隔至少为1分钟
}
cronExpression := fmt.Sprintf("*/%d * * * *", interval)
// 定义预设的全量采集任务
fullCollectionTask := models.Task{
Name: "全量采集",
Description: "触发一次全量数据采集",
ExecutionOrder: 1,
Type: models.TaskTypeFullCollection,
}
// 定义预设的延时任务
delayParams := task.DelayTaskParams{DelayDuration: 10} // 延时10秒
delayTask := models.Task{
Name: "延时任务",
Description: "系统预设延时任务,用于错峰处理",
ExecutionOrder: 2,
Type: models.TaskTypeWaiting,
}
err := delayTask.SaveParameters(delayParams)
if err != nil {
return fmt.Errorf("序列化延时任务参数失败: %w", err)
}
// 构建新的任务列表
var newTasks []models.Task
newTasks = append(newTasks, fullCollectionTask, delayTask)
// 如果计划已存在,则获取其现有任务并追加到新任务列表后(排除预设任务)
if foundExistingPlan, ok := existingPlanMap[models.PlanNamePeriodicSystemHealthCheck]; ok {
for _, existingTask := range foundExistingPlan.Tasks {
// 排除已预设的全量采集和延时任务
if existingTask.Type != models.TaskTypeFullCollection && existingTask.Type != models.TaskTypeWaiting {
newTasks = append(newTasks, existingTask)
}
}
}
// 重新设置所有任务的 ExecutionOrder
for i := range newTasks {
newTasks[i].ExecutionOrder = i + 1
}
predefinedPlan := &models.Plan{
Name: models.PlanNamePeriodicSystemHealthCheck,
Description: fmt.Sprintf("这是一个系统预定义的计划, 每 %d 分钟自动触发一次全量数据采集, 并进行阈值校验告警。", app.Config.Collection.Interval),
PlanType: models.PlanTypeSystem,
ExecutionType: models.PlanExecutionTypeAutomatic,
CronExpression: cronExpression,
Status: models.PlanStatusEnabled,
ContentType: models.PlanContentTypeTasks,
Tasks: newTasks,
}
if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok {
// 如果计划存在,则进行无差别更新
@@ -104,40 +170,58 @@ func (app *Application) initializeSystemPlans(ctx context.Context) error {
logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name)
}
}
}
logger.Info("预定义系统计划检查完成。")
return nil
}
// getPredefinedSystemPlans 返回一个基于当前配置的预定义系统计划列表
func (app *Application) getPredefinedSystemPlans() []models.Plan {
// initializeAlarmNotificationPlan 负责初始化 "告警通知发送" 计划
// 它确保系统中存在一个每分钟执行的、用于发送告警通知的预定义计划。
func (app *Application) initializeAlarmNotificationPlan(ctx context.Context, existingPlanMap map[models.PlanName]*models.Plan) error {
appCtx, logger := logs.Trace(ctx, app.Ctx, "initializeAlarmNotificationPlan")
// 根据配置创建定时全量采集计划
interval := app.Config.Collection.Interval
if interval <= 0 {
interval = 1 // 确保间隔至少为1分钟
}
cronExpression := fmt.Sprintf("*/%d * * * *", interval)
timedCollectionPlan := models.Plan{
Name: PlanNameTimedFullDataCollection,
Description: fmt.Sprintf("这是一个系统预定义的计划, 每 %d 分钟自动触发一次全量数据采集。", app.Config.Collection.Interval),
predefinedPlan := &models.Plan{
Name: models.PlanNameAlarmNotification,
Description: "这是一个系统预定义的计划, 每分钟自动触发一次告警通知发送。",
PlanType: models.PlanTypeSystem,
ExecutionType: models.PlanExecutionTypeAutomatic,
CronExpression: cronExpression,
CronExpression: "*/1 * * * *", // 每分钟执行一次
Status: models.PlanStatusEnabled,
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{
Name: "全量采集",
Description: "触发一次全量数据采集",
Name: "告警通知发送",
Description: "发送所有待处理的告警通知",
ExecutionOrder: 1,
Type: models.TaskTypeFullCollection,
Type: models.TaskTypeAlarmNotification,
},
},
}
return []models.Plan{timedCollectionPlan}
if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok {
// 如果计划存在,则进行无差别更新
logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name)
predefinedPlan.ID = foundExistingPlan.ID
predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount
if err := app.Infra.repos.planRepo.UpdatePlanMetadataAndStructure(appCtx, predefinedPlan); err != nil {
return fmt.Errorf("更新预定义计划 '%s' 的元数据和结构失败: %w", predefinedPlan.Name, err)
}
if err := app.Infra.repos.planRepo.UpdatePlan(appCtx, predefinedPlan); err != nil {
return fmt.Errorf("更新预定义计划 '%s' 的所有顶层字段失败: %w", predefinedPlan.Name, err)
}
logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name)
} else {
// 如果计划不存在, 则创建
logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name)
if err := app.Infra.repos.planRepo.CreatePlan(appCtx, predefinedPlan); err != nil {
return fmt.Errorf("创建预定义计划 '%s' 失败: %w", predefinedPlan.Name, err)
} else {
logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name)
}
}
return nil
}
// initializePendingCollections 在应用启动时处理所有未完成的采集请求。
@@ -209,7 +293,7 @@ func (app *Application) cleanupStaleTasksAndLogs(ctx context.Context) error {
}
// 2. 收集所有受影响的唯一 PlanID
affectedPlanIDs := make(map[uint]struct{})
affectedPlanIDs := make(map[uint32]struct{})
for _, log := range incompletePlanLogs {
affectedPlanIDs[log.PlanID] = struct{}{}
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ import (
)
// SellPigs 处理批量销售猪的业务逻辑。
func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, tatalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "SellPigs")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
if quantity <= 0 {
@@ -85,7 +85,7 @@ func (s *pigBatchService) SellPigs(ctx context.Context, batchID uint, penID uint
}
// BuyPigs 处理批量购买猪的业务逻辑。
func (s *pigBatchService) BuyPigs(ctx context.Context, batchID uint, penID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
func (s *pigBatchService) BuyPigs(ctx context.Context, batchID uint32, penID uint32, quantity int, unitPrice float32, totalPrice float32, traderName string, tradeDate time.Time, remarks string, operatorID uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "BuyPigs")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
if quantity <= 0 {

View File

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

View File

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

View File

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

View File

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

View File

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

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

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
}
sensorsByController := make(map[uint][]*models.Device)
sensorsByController := make(map[uint32][]*models.Device)
for _, sensor := range sensors {
sensorsByController[sensor.AreaControllerID] = append(sensorsByController[sensor.AreaControllerID], sensor)
}
@@ -97,7 +97,7 @@ func (t *FullCollectionTask) OnFailure(ctx context.Context, executeErr error) {
}
// ResolveDeviceIDs 获取当前任务需要使用的设备ID列表
func (t *FullCollectionTask) ResolveDeviceIDs(ctx context.Context) ([]uint, error) {
func (t *FullCollectionTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) {
// 全量采集任务不和任何设备绑定, 每轮采集都会重新获取全量传感器
return []uint{}, nil
return []uint32{}, nil
}

View File

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

View File

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

View File

@@ -47,6 +47,9 @@ type Config struct {
// Collection 定时采集配置
Collection CollectionConfig `yaml:"collection"`
// AlarmNotification 告警通知配置
AlarmNotification AlarmNotificationConfig `yaml:"alarm_notification"`
}
// AppConfig 代表应用基础配置
@@ -204,6 +207,29 @@ type CollectionConfig struct {
Interval int `yaml:"interval"`
}
type NotificationIntervalsConfig struct {
// DebugIntervalMinutes Debug级别告警的通知间隔分钟
DebugIntervalMinutes uint32 `yaml:"debug"`
// InfoIntervalMinutes Info级别告警的通知间隔分钟
InfoIntervalMinutes uint32 `yaml:"info"`
// WarnIntervalMinutes Warn级别告警的通知间隔分钟
WarnIntervalMinutes uint32 `yaml:"warn"`
// ErrorIntervalMinutes Error级别告警的通知间隔分钟
ErrorIntervalMinutes uint32 `yaml:"error"`
// DPanicIntervalMinutes DPanic级别告警的通知间隔分钟
DPanicIntervalMinutes uint32 `yaml:"dpanic"`
// PanicIntervalMinutes Panic级别告警的通知间隔分钟
PanicIntervalMinutes uint32 `yaml:"panic"`
// FatalIntervalMinutes Fatal级别告警的通知间隔分钟
FatalIntervalMinutes uint32 `yaml:"fatal"`
}
// AlarmNotificationConfig 告警通知配置
type AlarmNotificationConfig struct {
// NotificationIntervals 告警通知的发送间隔时间,键为告警等级,值为时间间隔(秒)
NotificationIntervals NotificationIntervalsConfig `yaml:"notification_intervals"`
}
// NewConfig 创建并返回一个新的配置实例
func NewConfig() *Config {
// 默认值可以在这里设置,但我们优先使用配置文件中的值

View File

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

View File

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

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

View File

@@ -5,10 +5,38 @@ import (
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/command_generater"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// ModbusFunctionCode 定义Modbus功能码的枚举类型
type ModbusFunctionCode byte
// 定义常用的Modbus功能码常量及其应用场景
const (
// ReadCoils 读取线圈状态 (0x01)
// 场景: 用于读取数字量输出DO或内部标志位的当前状态这些状态通常是开关量。
ReadCoils ModbusFunctionCode = 0x01
// ReadDiscreteInputs 读取离散输入状态 (0x02)
// 场景: 用于读取数字量输入DI的当前状态这些状态通常是外部传感器的开关量信号。
ReadDiscreteInputs ModbusFunctionCode = 0x02
// ReadHoldingRegisters 读取保持寄存器 (0x03)
// 场景: 用于读取设备内部可读写的参数或数据,例如温度设定值、电机速度等模拟量或配置数据。
ReadHoldingRegisters ModbusFunctionCode = 0x03
// ReadInputRegisters 读取输入寄存器 (0x04)
// 场景: 用于读取设备的模拟量输入AI数据这些数据通常是只读的例如当前温度、压力、电压等实时测量值。
ReadInputRegisters ModbusFunctionCode = 0x04
// WriteSingleCoil 写入单个线圈 (0x05)
// 场景: 用于控制单个数字量输出DO例如打开或关闭一个继电器、指示灯等。
WriteSingleCoil ModbusFunctionCode = 0x05
// WriteSingleRegister 写入单个保持寄存器 (0x06)
// 场景: 用于修改设备内部的单个可写参数,例如设置一个温度控制器的目标温度、调整一个阀门的开度等。
WriteSingleRegister ModbusFunctionCode = 0x06
// WriteMultipleCoils 写入多个线圈 (0x0F)
// 场景: 用于批量控制多个数字量输出DO例如同时打开或关闭一组继电器。
WriteMultipleCoils ModbusFunctionCode = 0x0F
// WriteMultipleRegisters 写入多个保持寄存器 (0x10)
// 场景: 用于批量修改设备内部的多个可写参数,例如一次性更新多个配置参数或模拟量输出值。
WriteMultipleRegisters ModbusFunctionCode = 0x10
)
// DeviceCategory 定义了设备模板的宽泛类别
@@ -25,8 +53,8 @@ const (
// 它提供了必要的元数据,以便应用程序能够正确解释从设备读取的原始数据。
type ValueDescriptor struct {
Type SensorType `json:"type"`
Multiplier float64 `json:"multiplier"` // 乘数,用于原始数据转换
Offset float64 `json:"offset"` // 偏移量,用于原始数据转换
Multiplier float32 `json:"multiplier"` // 乘数,用于原始数据转换
Offset float32 `json:"offset"` // 偏移量,用于原始数据转换
}
// --- 指令结构体 (Command Structs) ---
@@ -51,7 +79,7 @@ func (sc *SwitchCommands) SelfCheck() error {
// SensorCommands 定义了传感器读取指令所需的Modbus参数
type SensorCommands struct {
// ModbusFunctionCode 记录Modbus功能码例如 ReadHoldingRegisters。(一般是第二字节)
ModbusFunctionCode command_generater.ModbusFunctionCode `json:"modbus_function_code"`
ModbusFunctionCode ModbusFunctionCode `json:"modbus_function_code"`
// ModbusStartAddress 记录Modbus寄存器的起始地址用于生成指令。(一般是第三到四字节)
ModbusStartAddress uint16 `json:"modbus_start_address"`
// ModbusQuantity 记录Modbus寄存器的数量用于生成指令。(一般是五到六字节)
@@ -62,7 +90,7 @@ type SensorCommands struct {
func (sc *SensorCommands) SelfCheck() error {
// 校验ModbusFunctionCode是否为读取类型
switch sc.ModbusFunctionCode {
case command_generater.ReadCoils, command_generater.ReadDiscreteInputs, command_generater.ReadHoldingRegisters, command_generater.ReadInputRegisters:
case ReadCoils, ReadDiscreteInputs, ReadHoldingRegisters, ReadInputRegisters:
// 支持的读取功能码
default:
return fmt.Errorf("'sensor' 指令集 ModbusFunctionCode %X 无效或不是读取类型", sc.ModbusFunctionCode)
@@ -77,7 +105,7 @@ func (sc *SensorCommands) SelfCheck() error {
// DeviceTemplate 代表一种物理设备的类型。
type DeviceTemplate struct {
gorm.Model
Model
// Name 是此模板的唯一名称, 例如 "FanModel-XYZ-2000" 或 "TempSensor-T1"
Name string `gorm:"not null;unique" json:"name"`

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,8 +6,20 @@ import (
"fmt"
"strconv"
"strings"
"time"
"go.uber.org/zap/zapcore"
"gorm.io/gorm"
)
// Model 用于代替gorm.Model, 使用uint32以节约空间
type Model struct {
ID uint32 `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// GetAllModels 返回一个包含所有数据库模型实例的切片。
// 这个函数用于在数据库初始化时自动迁移所有的表结构。
func GetAllModels() []interface{} {
@@ -61,15 +73,19 @@ func GetAllModels() []interface{} {
&Medication{},
&MedicationLog{},
// Alarm Models
&ActiveAlarm{},
&HistoricalAlarm{},
// Notification Models
&Notification{},
}
}
// UintArray 是一个自定义类型,代表 uint 的切片。
// UintArray 是一个自定义类型,代表 uint32 的切片。
// 它实现了 gorm.Scanner 和 driver.Valuer 接口,
// 以便能与数据库的 bigint[] 类型进行原生映射。
type UintArray []uint
type UintArray []uint32
// Value 实现了 driver.Valuer 接口。
// 它告诉 GORM 如何将 UintArray ([]) 转换为数据库能够理解的格式。
@@ -111,21 +127,86 @@ func (a *UintArray) Scan(src interface{}) error {
// 去掉花括号
srcStr = strings.Trim(srcStr, "{}")
if srcStr == "" {
*a = []uint{}
*a = []uint32{}
return nil
}
// 按逗号分割
parts := strings.Split(srcStr, ",")
arr := make([]uint, len(parts))
arr := make([]uint32, len(parts))
for i, p := range parts {
val, err := strconv.ParseUint(p, 10, 64)
if err != nil {
return fmt.Errorf("解析 UintArray 元素失败: %w", err)
}
arr[i] = uint(val)
arr[i] = uint32(val)
}
*a = arr
return nil
}
// SeverityLevel 定义了系统中告警、通知、日志的统一级别枚举。
// 它以中文形式存储在数据库中,提高了可读性。
type SeverityLevel string
const (
// DebugLevel 调试级别,用于开发和诊断问题。
DebugLevel SeverityLevel = "Debug"
// InfoLevel 信息级别,用于记录常规操作。
InfoLevel SeverityLevel = "Info"
// WarnLevel 警告级别,表示出现潜在问题,需要关注。
WarnLevel SeverityLevel = "Warn"
// ErrorLevel 错误级别,表示发生了需要处理的错误。
ErrorLevel SeverityLevel = "Error"
// DPanicLevel 开发时崩溃级别,在开发模式下会触发 panic。
DPanicLevel SeverityLevel = "DPanic"
// PanicLevel 崩溃级别,记录日志后会立即触发 panic。
PanicLevel SeverityLevel = "Panic"
// FatalLevel 致命级别,记录日志后会调用 os.Exit(1) 退出程序。
FatalLevel SeverityLevel = "Fatal"
)
// ToZapLevel 将我们的自定义级别转换为 zapcore.Level以便与日志记录器兼容。
func (al SeverityLevel) ToZapLevel() zapcore.Level {
switch al {
case DebugLevel:
return zapcore.DebugLevel
case InfoLevel:
return zapcore.InfoLevel
case WarnLevel:
return zapcore.WarnLevel
case ErrorLevel:
return zapcore.ErrorLevel
case DPanicLevel:
return zapcore.DPanicLevel
case PanicLevel:
return zapcore.PanicLevel
case FatalLevel:
return zapcore.FatalLevel
default:
// 默认情况下返回 Info 级别,保证程序健壮性
return zapcore.InfoLevel
}
}
// Scan 实现了 sql.Scanner 接口GORM 在从数据库读取数据时会调用此方法。
func (al *SeverityLevel) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
// 尝试处理其他可能的类型,例如字符串
s, ok := value.(string)
if !ok {
return fmt.Errorf("无法将值 %v (类型 %T) 扫描为 SeverityLevel", value, value)
}
*al = SeverityLevel(s)
return nil
}
*al = SeverityLevel(bytes)
return nil
}
// Value 实现了 driver.Valuer 接口GORM 在将数据写入数据库时会调用此方法。
func (al SeverityLevel) Value() (driver.Value, error) {
return string(al), nil
}

View File

@@ -1,13 +1,21 @@
package models
import (
"database/sql/driver"
"errors"
"time"
)
"git.huangwc.com/pig/pig-farm-controller/internal/infra/notify"
"go.uber.org/zap/zapcore"
"gorm.io/gorm"
// NotifierType 定义了通知器的类型。
type NotifierType string
const (
// NotifierTypeSMTP 表示 SMTP 邮件通知器。
NotifierTypeSMTP NotifierType = "邮件"
// NotifierTypeWeChat 表示企业微信通知器。
NotifierTypeWeChat NotifierType = "企业微信"
// NotifierTypeLark 表示飞书通知器。
NotifierTypeLark NotifierType = "飞书"
// NotifierTypeLog 表示日志通知器,作为最终的告警记录渠道。
NotifierTypeLog NotifierType = "日志"
)
// NotificationStatus 定义了通知发送尝试的状态枚举。
@@ -19,48 +27,20 @@ const (
NotificationStatusSkipped NotificationStatus = "已跳过" // 通知因某些原因被跳过(例如:用户未配置联系方式)
)
// LogLevel is a custom type for zapcore.Level to handle database scanning and valuing.
type LogLevel zapcore.Level
// Scan implements the sql.Scanner interface.
func (l *LogLevel) Scan(value interface{}) error {
var s string
switch v := value.(type) {
case []byte:
s = string(v)
case string:
s = v
default:
return errors.New("LogLevel的类型无效")
}
var zl zapcore.Level
if err := zl.UnmarshalText([]byte(s)); err != nil {
return err
}
*l = LogLevel(zl)
return nil
}
// Value implements the driver.Valuer interface.
func (l LogLevel) Value() (driver.Value, error) {
return (zapcore.Level)(l).String(), nil
}
// Notification 表示已发送或尝试发送的通知记录。
type Notification struct {
gorm.Model
Model
// NotifierType 通知器类型 (例如:"邮件", "企业微信", "飞书", "日志")
NotifierType notify.NotifierType `gorm:"type:varchar(20);not null;index" json:"notifier_type"`
NotifierType NotifierType `gorm:"type:varchar(20);not null;index" json:"notifier_type"`
// UserID 接收通知的用户ID用于追溯通知记录到特定用户
UserID uint `gorm:"index" json:"user_id"` // 增加 UserID 字段,并添加索引
UserID uint32 `gorm:"index" json:"user_id"` // 增加 UserID 字段,并添加索引
// Title 通知标题
Title string `gorm:"type:varchar(255);not null" json:"title"`
// Message 通知内容
Message string `gorm:"type:text;not null" json:"message"`
// Level 通知级别 (例如INFO, WARN, ERROR)
Level LogLevel `gorm:"type:varchar(10);not null" json:"level"`
Level SeverityLevel `gorm:"type:varchar(10);not null" json:"level"`
// AlarmTimestamp 通知内容生成时的时间戳,与 ID 构成复合主键
AlarmTimestamp time.Time `gorm:"primaryKey;not null" json:"alarm_timestamp"`
// ToAddress 接收地址 (例如:邮箱地址, 企业微信ID, 日志标识符)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package models
import (
"encoding/json"
"errors"
"time"
"gorm.io/datatypes"
@@ -20,7 +22,7 @@ const (
// SignalMetrics 存储信号强度数据
type SignalMetrics struct {
RssiDbm int `json:"rssi_dbm"` // 绝对信号强度dBm受距离、障碍物影响
SnrDb float64 `json:"snr_db"` // 信号与噪声的相对比率dB由 RSSI 减去噪声地板Noise Floor
SnrDb float32 `json:"snr_db"` // 信号与噪声的相对比率dB由 RSSI 减去噪声地板Noise Floor
SensitivityDbm int `json:"sensitivity_dbm"` // 网关的最低检测阈值(dBm)
MarginDb int `json:"margin_db"` // SNR 相对于接收器灵敏度的余量, Margin = SNR - Sensitivity
}
@@ -34,17 +36,17 @@ type BatteryLevel struct {
// TemperatureData 存储温度数据
type TemperatureData struct {
TemperatureCelsius float64 `json:"temperature_celsius"` // 温度值 (摄氏度)
TemperatureCelsius float32 `json:"temperature_celsius"` // 温度值 (摄氏度)
}
// HumidityData 存储湿度数据
type HumidityData struct {
HumidityPercent float64 `json:"humidity_percent"` // 湿度值 (%)
HumidityPercent float32 `json:"humidity_percent"` // 湿度值 (%)
}
// WeightData 存储重量数据
type WeightData struct {
WeightKilograms float64 `json:"weight_kilograms"` // 重量值 (公斤)
WeightKilograms float32 `json:"weight_kilograms"` // 重量值 (公斤)
}
// SensorData 存储所有类型的传感器数据,对应数据库中的 'sensor_data' 表。
@@ -53,10 +55,10 @@ type SensorData struct {
Time time.Time `gorm:"primaryKey" json:"time"`
// DeviceID 是传感器的唯一标识符,作为复合主键的另一部分。
DeviceID uint `gorm:"primaryKey" json:"device_id"`
DeviceID uint32 `gorm:"primaryKey" json:"device_id"`
// RegionalControllerID 是上报此数据的区域主控的ID。
RegionalControllerID uint `json:"regional_controller_id"`
// AreaControllerID 是上报此数据的区域主控的ID。
AreaControllerID uint32 `json:"area_controller_id"`
// SensorType 是传感数据的类型
SensorType SensorType `gorm:"not null;index" json:"sensor_type"`
@@ -68,3 +70,12 @@ type SensorData struct {
func (SensorData) TableName() string {
return "sensor_data"
}
// ParseData 解析 JSON 数据到一个具体的结构体中。
// 调用方需要传入一个指向目标结构体实例的指针。
func (s *SensorData) ParseData(v interface{}) error {
if s.Data == nil {
return errors.New("传感器数据为空,无法解析")
}
return json.Unmarshal(s.Data, v)
}

View File

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

View File

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

View File

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

View File

@@ -4,26 +4,12 @@ import (
"context"
"time"
"go.uber.org/zap/zapcore"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// DefaultTimeFormat 定义了所有通知中统一使用的时间格式。
const DefaultTimeFormat = "2006-01-02 15:04:05"
// NotifierType 定义了通知器的类型。
type NotifierType string
const (
// NotifierTypeSMTP 表示 SMTP 邮件通知器。
NotifierTypeSMTP NotifierType = "邮件"
// NotifierTypeWeChat 表示企业微信通知器。
NotifierTypeWeChat NotifierType = "企业微信"
// NotifierTypeLark 表示飞书通知器。
NotifierTypeLark NotifierType = "飞书"
// NotifierTypeLog 表示日志通知器,作为最终的告警记录渠道。
NotifierTypeLog NotifierType = "日志"
)
// AlarmContent 定义了通知的内容
type AlarmContent struct {
// 通知标题
@@ -31,7 +17,7 @@ type AlarmContent struct {
// 通知信息
Message string
// 通知级别
Level zapcore.Level
Level models.SeverityLevel
// 通知时间
Timestamp time.Time
}
@@ -41,5 +27,5 @@ type Notifier interface {
// Send 发送通知
Send(ctx context.Context, content AlarmContent, toAddr string) error
// Type 返回通知器的类型
Type() NotifierType
Type() models.NotifierType
}

View File

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

View File

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

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 (
"context"
"fmt"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
@@ -12,12 +13,14 @@ import (
// AreaControllerRepository 定义了对 AreaController 模型的数据库操作接口
type AreaControllerRepository interface {
FindByID(ctx context.Context, id uint) (*models.AreaController, error)
FindByID(ctx context.Context, id uint32) (*models.AreaController, error)
FindByNetworkID(ctx context.Context, networkID string) (*models.AreaController, error)
Create(ctx context.Context, ac *models.AreaController) error
ListAll(ctx context.Context) ([]*models.AreaController, error)
Update(ctx context.Context, ac *models.AreaController) error
Delete(ctx context.Context, id uint) error
Delete(ctx context.Context, id uint32) error
// IsAreaControllerUsedByTasks 检查区域主控是否被特定任务类型使用,可以忽略指定任务类型
IsAreaControllerUsedByTasks(ctx context.Context, areaControllerID uint32, ignoredTaskTypes []models.TaskType) (bool, error)
}
// gormAreaControllerRepository 是 AreaControllerRepository 的 GORM 实现。
@@ -57,7 +60,7 @@ func (r *gormAreaControllerRepository) Update(ctx context.Context, ac *models.Ar
}
// Delete 删除一个 AreaController 记录。
func (r *gormAreaControllerRepository) Delete(ctx context.Context, id uint) error {
func (r *gormAreaControllerRepository) Delete(ctx context.Context, id uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Delete")
if err := r.db.WithContext(repoCtx).Delete(&models.AreaController{}, id).Error; err != nil {
return fmt.Errorf("删除区域主控失败: %w", err)
@@ -66,7 +69,7 @@ func (r *gormAreaControllerRepository) Delete(ctx context.Context, id uint) erro
}
// FindByID 通过 ID 查找一个 AreaController。
func (r *gormAreaControllerRepository) FindByID(ctx context.Context, id uint) (*models.AreaController, error) {
func (r *gormAreaControllerRepository) FindByID(ctx context.Context, id uint32) (*models.AreaController, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
var areaController models.AreaController
if err := r.db.WithContext(repoCtx).First(&areaController, id).Error; err != nil {
@@ -84,3 +87,66 @@ func (r *gormAreaControllerRepository) FindByNetworkID(ctx context.Context, netw
}
return &areaController, nil
}
// IsAreaControllerUsedByTasks 检查区域主控是否被特定任务类型使用,可以忽略指定任务类型
func (r *gormAreaControllerRepository) IsAreaControllerUsedByTasks(ctx context.Context, areaControllerID uint32, ignoredTaskTypes []models.TaskType) (bool, error) {
repoCtx, logger := logs.Trace(ctx, r.ctx, "IsAreaControllerUsedByTasks")
// 将 ignoredTaskTypes 转换为 map以便高效查找
ignoredMap := make(map[models.TaskType]struct{})
for _, tt := range ignoredTaskTypes {
ignoredMap[tt] = struct{}{}
}
areaControllerIDStr := strconv.FormatUint(uint64(areaControllerID), 10)
// 定义所有可能与 AreaControllerID 相关的任务类型列表
// 方便未来扩展,如果新增任务类型与区域主控关联,只需在此处添加
relevantTaskTypes := []models.TaskType{
models.TaskTypeAreaCollectorThresholdCheck,
// TODO: 如果未来有其他任务类型通过 parameters 关联 AreaControllerID请在此处添加
// 例如: models.TaskTypeAnotherAreaControllerTask,
}
for _, taskType := range relevantTaskTypes {
// 如果当前任务类型在忽略列表中,则跳过检查
if _, ok := ignoredMap[taskType]; ok {
continue
}
var taskCount int64
var query *gorm.DB
// 根据任务类型构建不同的查询条件
switch taskType {
case models.TaskTypeAreaCollectorThresholdCheck:
// TaskTypeAreaCollectorThresholdCheck 任务的 AreaControllerID 存储在 parameters->>'AreaControllerID'
query = r.db.WithContext(repoCtx).
Model(&models.Task{}).
Where("type = ?", models.TaskTypeAreaCollectorThresholdCheck).
Where("parameters->>'AreaControllerID' = ?", areaControllerIDStr)
// TODO: 如果未来有其他任务类型通过不同的 parameters 字段关联 AreaControllerID请在此处添加 case
// case models.TaskTypeAnotherAreaControllerTask:
// query = r.db.WithContext(repoCtx).
// Model(&models.Task{}).
// Where("type = ?", models.TaskTypeAnotherAreaControllerTask).
// Where("parameters->>'AnotherFieldForAreaControllerID' = ?", areaControllerIDStr)
default:
// 对于未明确处理的 relevantTaskTypes可以记录警告或直接跳过
logger.Warnf(fmt.Sprintf("IsAreaControllerUsedByTasks: 未处理的区域主控相关任务类型: %s", taskType))
continue
}
if query != nil {
err := query.Count(&taskCount).Error
if err != nil {
return false, fmt.Errorf("查询区域主控任务使用情况失败 (任务类型: %s): %w", taskType, err)
}
if taskCount > 0 {
return true, nil // 发现有未被忽略的任务正在使用此区域主控
}
}
}
return false, nil // 没有发现任何未被忽略的任务正在使用此区域主控
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

Some files were not shown because too many files have changed in this diff Show More