diff --git a/config.example.yml b/config.example.yml index 6641bac..ed4f81a 100644 --- a/config.example.yml +++ b/config.example.yml @@ -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 diff --git a/config.yml b/config.yml index 56cbeeb..728e14a 100644 --- a/config.yml +++ b/config.yml @@ -90,4 +90,15 @@ lora_mesh: # 定时采集配置 collection: - interval: 1 # 采集间隔 (分钟) \ No newline at end of file + interval: 1 # 采集间隔 (分钟) + +# 告警通知配置 +alarm_notification: + notification_intervals: # 告警通知间隔 (分钟) + debug: 1 + info: 1 + warn: 1 + error: 1 + dpanic: 1 + panic: 1 + fatal: 1 diff --git a/design/exceeding-threshold-alarm/index.md b/design/exceeding-threshold-alarm/index.md new file mode 100644 index 0000000..d93188d --- /dev/null +++ b/design/exceeding-threshold-alarm/index.md @@ -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 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 7be6316..8884ef9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -23,6 +23,673 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/v1/alarm/threshold/active-alarms": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据过滤条件和分页参数查询活跃告警列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "批量查询活跃告警", + "parameters": [ + { + "type": "string", + "description": "告警触发时间范围 - 结束时间", + "name": "end_time", + "in": "query" + }, + { + "type": "boolean", + "description": "按是否被忽略过滤", + "name": "is_ignored", + "in": "query" + }, + { + "enum": [ + "Debug", + "Info", + "Warn", + "Error", + "DPanic", + "Panic", + "Fatal" + ], + "type": "string", + "x-enum-varnames": [ + "DebugLevel", + "InfoLevel", + "WarnLevel", + "ErrorLevel", + "DPanicLevel", + "PanicLevel", + "FatalLevel" + ], + "description": "按告警严重性等级过滤", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"trigger_time DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "按告警来源ID过滤", + "name": "source_id", + "in": "query" + }, + { + "enum": [ + "普通设备", + "区域主控", + "系统" + ], + "type": "string", + "x-enum-varnames": [ + "AlarmSourceTypeDevice", + "AlarmSourceTypeAreaController", + "AlarmSourceTypeSystem" + ], + "description": "按告警来源类型过滤", + "name": "source_type", + "in": "query" + }, + { + "type": "string", + "description": "告警触发时间范围 - 开始时间", + "name": "trigger_time", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功获取活跃告警列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListActiveAlarmResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/alarm/threshold/area": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为指定的区域主控创建一个新的阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "创建区域阈值告警", + "parameters": [ + { + "description": "创建区域阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateAreaThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功创建区域阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/area/{task_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID获取单个区域阈值告警规则的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "获取区域阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功获取区域阈值告警", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.AreaThresholdAlarmDTO" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID更新已存在的区域阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "更新区域阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + }, + { + "description": "更新区域阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateAreaThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功更新区域阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID删除区域阈值告警规则", + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "删除区域阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功删除区域阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/device": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为单个设备创建一条新的阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "创建设备阈值告警", + "parameters": [ + { + "description": "创建设备阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateDeviceThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功创建设备阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/device/{task_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID获取单个设备阈值告警规则的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "获取设备阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功获取设备阈值告警", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceThresholdAlarmDTO" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID更新已存在的设备阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "更新设备阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + }, + { + "description": "更新设备阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDeviceThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功更新设备阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID删除设备阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "删除设备阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + }, + { + "description": "删除设备阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DeleteDeviceThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功删除设备阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/historical-alarms": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据过滤条件和分页参数查询历史告警列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "批量查询历史告警", + "parameters": [ + { + "enum": [ + "Debug", + "Info", + "Warn", + "Error", + "DPanic", + "Panic", + "Fatal" + ], + "type": "string", + "x-enum-varnames": [ + "DebugLevel", + "InfoLevel", + "WarnLevel", + "ErrorLevel", + "DPanicLevel", + "PanicLevel", + "FatalLevel" + ], + "description": "按告警严重性等级过滤", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"trigger_time DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "告警解决时间范围 - 结束时间", + "name": "resolve_time_end", + "in": "query" + }, + { + "type": "string", + "description": "告警解决时间范围 - 开始时间", + "name": "resolve_time_start", + "in": "query" + }, + { + "type": "integer", + "description": "按告警来源ID过滤", + "name": "source_id", + "in": "query" + }, + { + "enum": [ + "普通设备", + "区域主控", + "系统" + ], + "type": "string", + "x-enum-varnames": [ + "AlarmSourceTypeDevice", + "AlarmSourceTypeAreaController", + "AlarmSourceTypeSystem" + ], + "description": "按告警来源类型过滤", + "name": "source_type", + "in": "query" + }, + { + "type": "string", + "description": "告警触发时间范围 - 结束时间", + "name": "trigger_time_end", + "in": "query" + }, + { + "type": "string", + "description": "告警触发时间范围 - 开始时间", + "name": "trigger_time_start", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功获取历史告警列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListHistoricalAlarmResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/alarm/threshold/{id}/cancel-snooze": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据告警ID取消对一个阈值告警的忽略状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "取消忽略阈值告警", + "parameters": [ + { + "type": "string", + "description": "告警ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功取消忽略告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/{id}/snooze": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据告警ID忽略一个活跃的阈值告警,或更新其忽略时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "忽略阈值告警", + "parameters": [ + { + "type": "string", + "description": "告警ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "忽略告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnoozeAlarmRequest" + } + } + ], + "responses": { + "200": { + "description": "成功忽略告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/area-controllers": { "get": { "security": [ @@ -998,7 +1665,6 @@ const docTemplate = `{ }, { "enum": [ - 7, -1, 0, 1, @@ -1008,12 +1674,12 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -1023,7 +1689,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -4170,6 +4837,50 @@ const docTemplate = `{ "CodeServiceUnavailable" ] }, + "dto.ActiveAlarmDTO": { + "type": "object", + "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" + } + } + }, "dto.AreaControllerResponse": { "type": "object", "properties": { @@ -4200,6 +4911,29 @@ const docTemplate = `{ } } }, + "dto.AreaThresholdAlarmDTO": { + "type": "object", + "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" + } + } + }, "dto.AssignEmptyPensToBatchRequest": { "type": "object", "required": [ @@ -4287,6 +5021,49 @@ const docTemplate = `{ } } }, + "dto.CreateAreaThresholdAlarmDTO": { + "type": "object", + "required": [ + "area_controller_id", + "operator", + "sensor_type", + "thresholds" + ], + "properties": { + "area_controller_id": { + "description": "区域主控ID", + "type": "integer" + }, + "level": { + "description": "告警等级,可选", + "allOf": [ + { + "$ref": "#/definitions/models.SeverityLevel" + } + ] + }, + "operator": { + "description": "操作符", + "allOf": [ + { + "$ref": "#/definitions/models.Operator" + } + ] + }, + "sensor_type": { + "description": "传感器类型", + "allOf": [ + { + "$ref": "#/definitions/models.SensorType" + } + ] + }, + "thresholds": { + "description": "阈值", + "type": "number" + } + } + }, "dto.CreateDeviceRequest": { "type": "object", "required": [ @@ -4345,6 +5122,49 @@ const docTemplate = `{ } } }, + "dto.CreateDeviceThresholdAlarmDTO": { + "type": "object", + "required": [ + "device_id", + "operator", + "sensor_type", + "thresholds" + ], + "properties": { + "device_id": { + "description": "设备ID", + "type": "integer" + }, + "level": { + "description": "告警等级,可选,如果未提供则使用默认值", + "allOf": [ + { + "$ref": "#/definitions/models.SeverityLevel" + } + ] + }, + "operator": { + "description": "操作符 (使用string类型,与前端交互更通用)", + "allOf": [ + { + "$ref": "#/definitions/models.Operator" + } + ] + }, + "sensor_type": { + "description": "传感器类型", + "allOf": [ + { + "$ref": "#/definitions/models.SensorType" + } + ] + }, + "thresholds": { + "description": "阈值", + "type": "number" + } + } + }, "dto.CreatePenRequest": { "type": "object", "required": [ @@ -4454,6 +5274,22 @@ const docTemplate = `{ } } }, + "dto.DeleteDeviceThresholdAlarmDTO": { + "type": "object", + "required": [ + "sensor_type" + ], + "properties": { + "sensor_type": { + "description": "传感器类型", + "allOf": [ + { + "$ref": "#/definitions/models.SensorType" + } + ] + } + } + }, "dto.DeviceCommandLogDTO": { "type": "object", "properties": { @@ -4546,6 +5382,29 @@ const docTemplate = `{ } } }, + "dto.DeviceThresholdAlarmDTO": { + "type": "object", + "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" + } + } + }, "dto.FeedFormulaDTO": { "type": "object", "properties": { @@ -4589,6 +5448,58 @@ const docTemplate = `{ } } }, + "dto.HistoricalAlarmDTO": { + "type": "object", + "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" + } + } + }, + "dto.ListActiveAlarmResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ActiveAlarmDTO" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListDeviceCommandLogResponse": { "type": "object", "properties": { @@ -4617,6 +5528,20 @@ const docTemplate = `{ } } }, + "dto.ListHistoricalAlarmResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HistoricalAlarmDTO" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListMedicationLogResponse": { "type": "object", "properties": { @@ -4984,13 +5909,13 @@ const docTemplate = `{ "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" @@ -5882,7 +6807,7 @@ const docTemplate = `{ "description": "Type 指定要测试的通知渠道", "allOf": [ { - "$ref": "#/definitions/notify.NotifierType" + "$ref": "#/definitions/models.NotifierType" } ] } @@ -5891,6 +6816,9 @@ const docTemplate = `{ "dto.SensorDataDTO": { "type": "object", "properties": { + "area_controller_id": { + "type": "integer" + }, "data": { "type": "array", "items": { @@ -5900,9 +6828,6 @@ const docTemplate = `{ "device_id": { "type": "integer" }, - "regional_controller_id": { - "type": "integer" - }, "sensor_type": { "$ref": "#/definitions/models.SensorType" }, @@ -5911,6 +6836,19 @@ const docTemplate = `{ } } }, + "dto.SnoozeAlarmRequest": { + "type": "object", + "required": [ + "duration_minutes" + ], + "properties": { + "duration_minutes": { + "description": "忽略时长,单位分钟", + "type": "integer", + "minimum": 1 + } + } + }, "dto.SubPlanResponse": { "type": "object", "properties": { @@ -6136,6 +7074,35 @@ const docTemplate = `{ } } }, + "dto.UpdateAreaThresholdAlarmDTO": { + "type": "object", + "required": [ + "operator", + "thresholds" + ], + "properties": { + "level": { + "description": "新的告警等级,可选", + "allOf": [ + { + "$ref": "#/definitions/models.SeverityLevel" + } + ] + }, + "operator": { + "description": "新的操作符", + "allOf": [ + { + "$ref": "#/definitions/models.Operator" + } + ] + }, + "thresholds": { + "description": "新的阈值", + "type": "number" + } + } + }, "dto.UpdateDeviceRequest": { "type": "object", "required": [ @@ -6194,6 +7161,35 @@ const docTemplate = `{ } } }, + "dto.UpdateDeviceThresholdAlarmDTO": { + "type": "object", + "required": [ + "operator", + "thresholds" + ], + "properties": { + "level": { + "description": "新的告警等级,可选", + "allOf": [ + { + "$ref": "#/definitions/models.SeverityLevel" + } + ] + }, + "operator": { + "description": "新的操作符", + "allOf": [ + { + "$ref": "#/definitions/models.Operator" + } + ] + }, + "thresholds": { + "description": "新的阈值", + "type": "number" + } + } + }, "dto.UpdatePenRequest": { "type": "object", "required": [ @@ -6412,6 +7408,40 @@ const docTemplate = `{ } } }, + "models.AlarmCode": { + "type": "string", + "enum": [ + "温度阈值", + "湿度阈值", + "重量阈值", + "电池电量阈值", + "信号强度阈值", + "设备离线", + "区域主控离线" + ], + "x-enum-varnames": [ + "AlarmCodeTemperature", + "AlarmCodeHumidity", + "AlarmCodeWeight", + "AlarmCodeBatteryLevel", + "AlarmCodeSignalMetrics", + "AlarmCodeDeviceOffline", + "AlarmCodeAreaControllerOffline" + ] + }, + "models.AlarmSourceType": { + "type": "string", + "enum": [ + "普通设备", + "区域主控", + "系统" + ], + "x-enum-varnames": [ + "AlarmSourceTypeDevice", + "AlarmSourceTypeAreaController", + "AlarmSourceTypeSystem" + ] + }, "models.AuditStatus": { "type": "string", "enum": [ @@ -6522,6 +7552,40 @@ const docTemplate = `{ "NotificationStatusSkipped" ] }, + "models.NotifierType": { + "type": "string", + "enum": [ + "邮件", + "企业微信", + "飞书", + "日志" + ], + "x-enum-varnames": [ + "NotifierTypeSMTP", + "NotifierTypeWeChat", + "NotifierTypeLark", + "NotifierTypeLog" + ] + }, + "models.Operator": { + "type": "string", + "enum": [ + "\u003c", + "\u003c=", + "\u003e", + "\u003e=", + "=", + "!=" + ], + "x-enum-varnames": [ + "OperatorLessThan", + "OperatorLessThanOrEqualTo", + "OperatorGreaterThan", + "OperatorGreaterThanOrEqualTo", + "OperatorEqualTo", + "OperatorNotEqualTo" + ] + }, "models.PenStatus": { "type": "string", "enum": [ @@ -6805,6 +7869,27 @@ const docTemplate = `{ "SensorTypeWeight" ] }, + "models.SeverityLevel": { + "type": "string", + "enum": [ + "Debug", + "Info", + "Warn", + "Error", + "DPanic", + "Panic", + "Fatal" + ], + "x-enum-varnames": [ + "DebugLevel", + "InfoLevel", + "WarnLevel", + "ErrorLevel", + "DPanicLevel", + "PanicLevel", + "FatalLevel" + ] + }, "models.StockLogSourceType": { "type": "string", "enum": [ @@ -6830,10 +7915,16 @@ const docTemplate = `{ "计划分析", "等待", "下料", - "全量采集" + "全量采集", + "告警通知", + "设备阈值检查", + "区域阈值检查" ], "x-enum-comments": { "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", + "TaskTypeAlarmNotification": "告警通知任务", + "TaskTypeAreaCollectorThresholdCheck": "区域阈值检查任务", + "TaskTypeDeviceThresholdCheck": "设备阈值检查任务", "TaskTypeFullCollection": "新增的全量采集任务", "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", "TaskTypeWaiting": "等待任务" @@ -6842,13 +7933,19 @@ const docTemplate = `{ "解析Plan的Task列表并添加到待执行队列的特殊任务", "等待任务", "下料口释放指定重量任务", - "新增的全量采集任务" + "新增的全量采集任务", + "告警通知任务", + "设备阈值检查任务", + "区域阈值检查任务" ], "x-enum-varnames": [ "TaskPlanAnalysis", "TaskTypeWaiting", "TaskTypeReleaseFeedWeight", - "TaskTypeFullCollection" + "TaskTypeFullCollection", + "TaskTypeAlarmNotification", + "TaskTypeDeviceThresholdCheck", + "TaskTypeAreaCollectorThresholdCheck" ] }, "models.ValueDescriptor": { @@ -6867,21 +7964,6 @@ const docTemplate = `{ } } }, - "notify.NotifierType": { - "type": "string", - "enum": [ - "邮件", - "企业微信", - "飞书", - "日志" - ], - "x-enum-varnames": [ - "NotifierTypeSMTP", - "NotifierTypeWeChat", - "NotifierTypeLark", - "NotifierTypeLog" - ] - }, "repository.PlanTypeFilter": { "type": "string", "enum": [ @@ -6899,7 +7981,6 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -6909,10 +7990,10 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -6922,7 +8003,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index 9d1a871..aadfac4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -15,6 +15,673 @@ "version": "1.0" }, "paths": { + "/api/v1/alarm/threshold/active-alarms": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据过滤条件和分页参数查询活跃告警列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "批量查询活跃告警", + "parameters": [ + { + "type": "string", + "description": "告警触发时间范围 - 结束时间", + "name": "end_time", + "in": "query" + }, + { + "type": "boolean", + "description": "按是否被忽略过滤", + "name": "is_ignored", + "in": "query" + }, + { + "enum": [ + "Debug", + "Info", + "Warn", + "Error", + "DPanic", + "Panic", + "Fatal" + ], + "type": "string", + "x-enum-varnames": [ + "DebugLevel", + "InfoLevel", + "WarnLevel", + "ErrorLevel", + "DPanicLevel", + "PanicLevel", + "FatalLevel" + ], + "description": "按告警严重性等级过滤", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"trigger_time DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "按告警来源ID过滤", + "name": "source_id", + "in": "query" + }, + { + "enum": [ + "普通设备", + "区域主控", + "系统" + ], + "type": "string", + "x-enum-varnames": [ + "AlarmSourceTypeDevice", + "AlarmSourceTypeAreaController", + "AlarmSourceTypeSystem" + ], + "description": "按告警来源类型过滤", + "name": "source_type", + "in": "query" + }, + { + "type": "string", + "description": "告警触发时间范围 - 开始时间", + "name": "trigger_time", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功获取活跃告警列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListActiveAlarmResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/alarm/threshold/area": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为指定的区域主控创建一个新的阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "创建区域阈值告警", + "parameters": [ + { + "description": "创建区域阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateAreaThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功创建区域阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/area/{task_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID获取单个区域阈值告警规则的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "获取区域阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功获取区域阈值告警", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.AreaThresholdAlarmDTO" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID更新已存在的区域阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "更新区域阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + }, + { + "description": "更新区域阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateAreaThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功更新区域阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID删除区域阈值告警规则", + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "删除区域阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功删除区域阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/device": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为单个设备创建一条新的阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "创建设备阈值告警", + "parameters": [ + { + "description": "创建设备阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateDeviceThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功创建设备阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/device/{task_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID获取单个设备阈值告警规则的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "获取设备阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功获取设备阈值告警", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceThresholdAlarmDTO" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID更新已存在的设备阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "更新设备阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + }, + { + "description": "更新设备阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDeviceThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功更新设备阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据任务ID删除设备阈值告警规则", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "删除设备阈值告警", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "task_id", + "in": "path", + "required": true + }, + { + "description": "删除设备阈值告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DeleteDeviceThresholdAlarmDTO" + } + } + ], + "responses": { + "200": { + "description": "成功删除设备阈值告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/historical-alarms": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据过滤条件和分页参数查询历史告警列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "批量查询历史告警", + "parameters": [ + { + "enum": [ + "Debug", + "Info", + "Warn", + "Error", + "DPanic", + "Panic", + "Fatal" + ], + "type": "string", + "x-enum-varnames": [ + "DebugLevel", + "InfoLevel", + "WarnLevel", + "ErrorLevel", + "DPanicLevel", + "PanicLevel", + "FatalLevel" + ], + "description": "按告警严重性等级过滤", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"trigger_time DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "告警解决时间范围 - 结束时间", + "name": "resolve_time_end", + "in": "query" + }, + { + "type": "string", + "description": "告警解决时间范围 - 开始时间", + "name": "resolve_time_start", + "in": "query" + }, + { + "type": "integer", + "description": "按告警来源ID过滤", + "name": "source_id", + "in": "query" + }, + { + "enum": [ + "普通设备", + "区域主控", + "系统" + ], + "type": "string", + "x-enum-varnames": [ + "AlarmSourceTypeDevice", + "AlarmSourceTypeAreaController", + "AlarmSourceTypeSystem" + ], + "description": "按告警来源类型过滤", + "name": "source_type", + "in": "query" + }, + { + "type": "string", + "description": "告警触发时间范围 - 结束时间", + "name": "trigger_time_end", + "in": "query" + }, + { + "type": "string", + "description": "告警触发时间范围 - 开始时间", + "name": "trigger_time_start", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功获取历史告警列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListHistoricalAlarmResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/alarm/threshold/{id}/cancel-snooze": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据告警ID取消对一个阈值告警的忽略状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "取消忽略阈值告警", + "parameters": [ + { + "type": "string", + "description": "告警ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功取消忽略告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/alarm/threshold/{id}/snooze": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据告警ID忽略一个活跃的阈值告警,或更新其忽略时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "告警管理" + ], + "summary": "忽略阈值告警", + "parameters": [ + { + "type": "string", + "description": "告警ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "忽略告警请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnoozeAlarmRequest" + } + } + ], + "responses": { + "200": { + "description": "成功忽略告警", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/area-controllers": { "get": { "security": [ @@ -990,7 +1657,6 @@ }, { "enum": [ - 7, -1, 0, 1, @@ -1000,12 +1666,12 @@ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -1015,7 +1681,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -4162,6 +4829,50 @@ "CodeServiceUnavailable" ] }, + "dto.ActiveAlarmDTO": { + "type": "object", + "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" + } + } + }, "dto.AreaControllerResponse": { "type": "object", "properties": { @@ -4192,6 +4903,29 @@ } } }, + "dto.AreaThresholdAlarmDTO": { + "type": "object", + "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" + } + } + }, "dto.AssignEmptyPensToBatchRequest": { "type": "object", "required": [ @@ -4279,6 +5013,49 @@ } } }, + "dto.CreateAreaThresholdAlarmDTO": { + "type": "object", + "required": [ + "area_controller_id", + "operator", + "sensor_type", + "thresholds" + ], + "properties": { + "area_controller_id": { + "description": "区域主控ID", + "type": "integer" + }, + "level": { + "description": "告警等级,可选", + "allOf": [ + { + "$ref": "#/definitions/models.SeverityLevel" + } + ] + }, + "operator": { + "description": "操作符", + "allOf": [ + { + "$ref": "#/definitions/models.Operator" + } + ] + }, + "sensor_type": { + "description": "传感器类型", + "allOf": [ + { + "$ref": "#/definitions/models.SensorType" + } + ] + }, + "thresholds": { + "description": "阈值", + "type": "number" + } + } + }, "dto.CreateDeviceRequest": { "type": "object", "required": [ @@ -4337,6 +5114,49 @@ } } }, + "dto.CreateDeviceThresholdAlarmDTO": { + "type": "object", + "required": [ + "device_id", + "operator", + "sensor_type", + "thresholds" + ], + "properties": { + "device_id": { + "description": "设备ID", + "type": "integer" + }, + "level": { + "description": "告警等级,可选,如果未提供则使用默认值", + "allOf": [ + { + "$ref": "#/definitions/models.SeverityLevel" + } + ] + }, + "operator": { + "description": "操作符 (使用string类型,与前端交互更通用)", + "allOf": [ + { + "$ref": "#/definitions/models.Operator" + } + ] + }, + "sensor_type": { + "description": "传感器类型", + "allOf": [ + { + "$ref": "#/definitions/models.SensorType" + } + ] + }, + "thresholds": { + "description": "阈值", + "type": "number" + } + } + }, "dto.CreatePenRequest": { "type": "object", "required": [ @@ -4446,6 +5266,22 @@ } } }, + "dto.DeleteDeviceThresholdAlarmDTO": { + "type": "object", + "required": [ + "sensor_type" + ], + "properties": { + "sensor_type": { + "description": "传感器类型", + "allOf": [ + { + "$ref": "#/definitions/models.SensorType" + } + ] + } + } + }, "dto.DeviceCommandLogDTO": { "type": "object", "properties": { @@ -4538,6 +5374,29 @@ } } }, + "dto.DeviceThresholdAlarmDTO": { + "type": "object", + "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" + } + } + }, "dto.FeedFormulaDTO": { "type": "object", "properties": { @@ -4581,6 +5440,58 @@ } } }, + "dto.HistoricalAlarmDTO": { + "type": "object", + "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" + } + } + }, + "dto.ListActiveAlarmResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ActiveAlarmDTO" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListDeviceCommandLogResponse": { "type": "object", "properties": { @@ -4609,6 +5520,20 @@ } } }, + "dto.ListHistoricalAlarmResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HistoricalAlarmDTO" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListMedicationLogResponse": { "type": "object", "properties": { @@ -4976,13 +5901,13 @@ "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" @@ -5874,7 +6799,7 @@ "description": "Type 指定要测试的通知渠道", "allOf": [ { - "$ref": "#/definitions/notify.NotifierType" + "$ref": "#/definitions/models.NotifierType" } ] } @@ -5883,6 +6808,9 @@ "dto.SensorDataDTO": { "type": "object", "properties": { + "area_controller_id": { + "type": "integer" + }, "data": { "type": "array", "items": { @@ -5892,9 +6820,6 @@ "device_id": { "type": "integer" }, - "regional_controller_id": { - "type": "integer" - }, "sensor_type": { "$ref": "#/definitions/models.SensorType" }, @@ -5903,6 +6828,19 @@ } } }, + "dto.SnoozeAlarmRequest": { + "type": "object", + "required": [ + "duration_minutes" + ], + "properties": { + "duration_minutes": { + "description": "忽略时长,单位分钟", + "type": "integer", + "minimum": 1 + } + } + }, "dto.SubPlanResponse": { "type": "object", "properties": { @@ -6128,6 +7066,35 @@ } } }, + "dto.UpdateAreaThresholdAlarmDTO": { + "type": "object", + "required": [ + "operator", + "thresholds" + ], + "properties": { + "level": { + "description": "新的告警等级,可选", + "allOf": [ + { + "$ref": "#/definitions/models.SeverityLevel" + } + ] + }, + "operator": { + "description": "新的操作符", + "allOf": [ + { + "$ref": "#/definitions/models.Operator" + } + ] + }, + "thresholds": { + "description": "新的阈值", + "type": "number" + } + } + }, "dto.UpdateDeviceRequest": { "type": "object", "required": [ @@ -6186,6 +7153,35 @@ } } }, + "dto.UpdateDeviceThresholdAlarmDTO": { + "type": "object", + "required": [ + "operator", + "thresholds" + ], + "properties": { + "level": { + "description": "新的告警等级,可选", + "allOf": [ + { + "$ref": "#/definitions/models.SeverityLevel" + } + ] + }, + "operator": { + "description": "新的操作符", + "allOf": [ + { + "$ref": "#/definitions/models.Operator" + } + ] + }, + "thresholds": { + "description": "新的阈值", + "type": "number" + } + } + }, "dto.UpdatePenRequest": { "type": "object", "required": [ @@ -6404,6 +7400,40 @@ } } }, + "models.AlarmCode": { + "type": "string", + "enum": [ + "温度阈值", + "湿度阈值", + "重量阈值", + "电池电量阈值", + "信号强度阈值", + "设备离线", + "区域主控离线" + ], + "x-enum-varnames": [ + "AlarmCodeTemperature", + "AlarmCodeHumidity", + "AlarmCodeWeight", + "AlarmCodeBatteryLevel", + "AlarmCodeSignalMetrics", + "AlarmCodeDeviceOffline", + "AlarmCodeAreaControllerOffline" + ] + }, + "models.AlarmSourceType": { + "type": "string", + "enum": [ + "普通设备", + "区域主控", + "系统" + ], + "x-enum-varnames": [ + "AlarmSourceTypeDevice", + "AlarmSourceTypeAreaController", + "AlarmSourceTypeSystem" + ] + }, "models.AuditStatus": { "type": "string", "enum": [ @@ -6514,6 +7544,40 @@ "NotificationStatusSkipped" ] }, + "models.NotifierType": { + "type": "string", + "enum": [ + "邮件", + "企业微信", + "飞书", + "日志" + ], + "x-enum-varnames": [ + "NotifierTypeSMTP", + "NotifierTypeWeChat", + "NotifierTypeLark", + "NotifierTypeLog" + ] + }, + "models.Operator": { + "type": "string", + "enum": [ + "\u003c", + "\u003c=", + "\u003e", + "\u003e=", + "=", + "!=" + ], + "x-enum-varnames": [ + "OperatorLessThan", + "OperatorLessThanOrEqualTo", + "OperatorGreaterThan", + "OperatorGreaterThanOrEqualTo", + "OperatorEqualTo", + "OperatorNotEqualTo" + ] + }, "models.PenStatus": { "type": "string", "enum": [ @@ -6797,6 +7861,27 @@ "SensorTypeWeight" ] }, + "models.SeverityLevel": { + "type": "string", + "enum": [ + "Debug", + "Info", + "Warn", + "Error", + "DPanic", + "Panic", + "Fatal" + ], + "x-enum-varnames": [ + "DebugLevel", + "InfoLevel", + "WarnLevel", + "ErrorLevel", + "DPanicLevel", + "PanicLevel", + "FatalLevel" + ] + }, "models.StockLogSourceType": { "type": "string", "enum": [ @@ -6822,10 +7907,16 @@ "计划分析", "等待", "下料", - "全量采集" + "全量采集", + "告警通知", + "设备阈值检查", + "区域阈值检查" ], "x-enum-comments": { "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", + "TaskTypeAlarmNotification": "告警通知任务", + "TaskTypeAreaCollectorThresholdCheck": "区域阈值检查任务", + "TaskTypeDeviceThresholdCheck": "设备阈值检查任务", "TaskTypeFullCollection": "新增的全量采集任务", "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", "TaskTypeWaiting": "等待任务" @@ -6834,13 +7925,19 @@ "解析Plan的Task列表并添加到待执行队列的特殊任务", "等待任务", "下料口释放指定重量任务", - "新增的全量采集任务" + "新增的全量采集任务", + "告警通知任务", + "设备阈值检查任务", + "区域阈值检查任务" ], "x-enum-varnames": [ "TaskPlanAnalysis", "TaskTypeWaiting", "TaskTypeReleaseFeedWeight", - "TaskTypeFullCollection" + "TaskTypeFullCollection", + "TaskTypeAlarmNotification", + "TaskTypeDeviceThresholdCheck", + "TaskTypeAreaCollectorThresholdCheck" ] }, "models.ValueDescriptor": { @@ -6859,21 +7956,6 @@ } } }, - "notify.NotifierType": { - "type": "string", - "enum": [ - "邮件", - "企业微信", - "飞书", - "日志" - ], - "x-enum-varnames": [ - "NotifierTypeSMTP", - "NotifierTypeWeChat", - "NotifierTypeLark", - "NotifierTypeLog" - ] - }, "repository.PlanTypeFilter": { "type": "string", "enum": [ @@ -6891,7 +7973,6 @@ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -6901,10 +7982,10 @@ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -6914,7 +7995,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2392e19..10e5f77 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: - 邮件 - 企业微信 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index a858f7c..43804b4 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -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() // 设置所有路由 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index cc09cb4..6540165 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -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("所有接口注册成功") diff --git a/internal/app/controller/alarm/threshold_alarm_controller.go b/internal/app/controller/alarm/threshold_alarm_controller.go new file mode 100644 index 0000000..6e69afd --- /dev/null +++ b/internal/app/controller/alarm/threshold_alarm_controller.go @@ -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) +} diff --git a/internal/app/controller/auth_utils.go b/internal/app/controller/auth_utils.go index 25d28b8..2b0fe81 100644 --- a/internal/app/controller/auth_utils.go +++ b/internal/app/controller/auth_utils.go @@ -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 diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index 451b22b..448fecc 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -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) diff --git a/internal/app/controller/management/controller_helpers.go b/internal/app/controller/management/controller_helpers.go index 8e68435..08345ee 100644 --- a/internal/app/controller/management/controller_helpers.go +++ b/internal/app/controller/management/controller_helpers.go @@ -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. 绑定查询参数 diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index 6a82809..a0dd7e9 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -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) }, "移入成功", diff --git a/internal/app/controller/management/pig_batch_health_controller.go b/internal/app/controller/management/pig_batch_health_controller.go index cd82e8b..905af89 100644 --- a/internal/app/controller/management/pig_batch_health_controller.go +++ b/internal/app/controller/management/pig_batch_health_controller.go @@ -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) }, "记录成功", diff --git a/internal/app/controller/management/pig_batch_trade_controller.go b/internal/app/controller/management/pig_batch_trade_controller.go index 6380c40..e33602d 100644 --- a/internal/app/controller/management/pig_batch_trade_controller.go +++ b/internal/app/controller/management/pig_batch_trade_controller.go @@ -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) }, "买猪成功", diff --git a/internal/app/controller/management/pig_batch_transfer_controller.go b/internal/app/controller/management/pig_batch_transfer_controller.go index 1d8c7b4..0bb478a 100644 --- a/internal/app/controller/management/pig_batch_transfer_controller.go +++ b/internal/app/controller/management/pig_batch_transfer_controller.go @@ -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) }, diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go index 0a6733c..69dd7c7 100644 --- a/internal/app/controller/management/pig_farm_controller.go +++ b/internal/app/controller/management/pig_farm_controller.go @@ -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) diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 35fa6b5..dfce780 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -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 diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index 1aa38c3..dada6ad 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -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}) diff --git a/internal/app/dto/alarm_converter.go b/internal/app/dto/alarm_converter.go new file mode 100644 index 0000000..90a3b33 --- /dev/null +++ b/internal/app/dto/alarm_converter.go @@ -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, + }, + } +} diff --git a/internal/app/dto/alarm_dto.go b/internal/app/dto/alarm_dto.go new file mode 100644 index 0000000..b83527a --- /dev/null +++ b/internal/app/dto/alarm_dto.go @@ -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"` +} diff --git a/internal/app/dto/device_dto.go b/internal/app/dto/device_dto.go index 4dd4f20..5ac018b 100644 --- a/internal/app/dto/device_dto.go +++ b/internal/app/dto/device_dto.go @@ -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"` diff --git a/internal/app/dto/dto.go b/internal/app/dto/dto.go new file mode 100644 index 0000000..ac02148 --- /dev/null +++ b/internal/app/dto/dto.go @@ -0,0 +1,10 @@ +package dto + +// --- General --- + +// PaginationDTO 定义了分页信息的标准结构 +type PaginationDTO struct { + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} diff --git a/internal/app/dto/monitor_converter.go b/internal/app/dto/monitor_converter.go index c45d597..657a4d4 100644 --- a/internal/app/dto/monitor_converter.go +++ b/internal/app/dto/monitor_converter.go @@ -11,11 +11,11 @@ func NewListSensorDataResponse(data []models.SensorData, total int64, page, page dtos := make([]SensorDataDTO, len(data)) for i, item := range data { dtos[i] = SensorDataDTO{ - Time: item.Time, - DeviceID: item.DeviceID, - RegionalControllerID: item.RegionalControllerID, - SensorType: item.SensorType, - Data: json.RawMessage(item.Data), + Time: item.Time, + DeviceID: item.DeviceID, + AreaControllerID: item.AreaControllerID, + SensorType: item.SensorType, + Data: json.RawMessage(item.Data), } } @@ -54,9 +54,9 @@ func NewListDeviceCommandLogResponse(data []models.DeviceCommandLog, total int64 // NewListPlanExecutionLogResponse 从模型数据创建列表响应 DTO func NewListPlanExecutionLogResponse(planLogs []models.PlanExecutionLog, plans []models.Plan, total int64, page, pageSize int) *ListPlanExecutionLogResponse { - planId2Name := make(map[uint]string) + planId2Name := make(map[uint32]string) for _, plan := range plans { - planId2Name[plan.ID] = plan.Name + planId2Name[plan.ID] = string(plan.Name) } dtos := make([]PlanExecutionLogDTO, len(planLogs)) @@ -95,7 +95,7 @@ func NewListTaskExecutionLogResponse(data []models.TaskExecutionLog, total int64 PlanExecutionLogID: item.PlanExecutionLogID, TaskID: item.TaskID, Task: TaskDTO{ - ID: uint(item.Task.ID), + ID: uint32(item.Task.ID), Name: item.Task.Name, Description: item.Task.Description, }, @@ -373,7 +373,7 @@ func NewListWeighingRecordResponse(data []models.WeighingRecord, total int64, pa func NewListPigTransferLogResponse(data []models.PigTransferLog, total int64, page, pageSize int) *ListPigTransferLogResponse { dtos := make([]PigTransferLogDTO, len(data)) for i, item := range data { - // 注意:PigTransferLog 的 ID, CreatedAt, UpdatedAt 字段是 gorm.Model 嵌入的 + // 注意:PigTransferLog 的 ID, CreatedAt, UpdatedAt 字段是 Model 嵌入的 dtos[i] = PigTransferLogDTO{ ID: item.ID, CreatedAt: item.CreatedAt, diff --git a/internal/app/dto/monitor_dto.go b/internal/app/dto/monitor_dto.go index bfb135f..6fcf4b6 100644 --- a/internal/app/dto/monitor_dto.go +++ b/internal/app/dto/monitor_dto.go @@ -7,22 +7,13 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" ) -// --- General --- - -// PaginationDTO 定义了分页信息的标准结构 -type PaginationDTO struct { - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` -} - // --- SensorData --- // ListSensorDataRequest 定义了获取传感器数据列表的请求参数 type ListSensorDataRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - DeviceID *uint `json:"device_id" query:"device_id"` + DeviceID *uint32 `json:"device_id" query:"device_id"` SensorType *string `json:"sensor_type" query:"sensor_type"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` @@ -31,11 +22,11 @@ type ListSensorDataRequest struct { // SensorDataDTO 是用于API响应的传感器数据结构 type SensorDataDTO struct { - Time time.Time `json:"time"` - DeviceID uint `json:"device_id"` - RegionalControllerID uint `json:"regional_controller_id"` - SensorType models.SensorType `json:"sensor_type"` - Data json.RawMessage `json:"data"` + Time time.Time `json:"time"` + DeviceID uint32 `json:"device_id"` + AreaControllerID uint32 `json:"area_controller_id"` + SensorType models.SensorType `json:"sensor_type"` + Data json.RawMessage `json:"data"` } // ListSensorDataResponse 是获取传感器数据列表的响应结构 @@ -50,7 +41,7 @@ type ListSensorDataResponse struct { type ListDeviceCommandLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - DeviceID *uint `json:"device_id" query:"device_id"` + DeviceID *uint32 `json:"device_id" query:"device_id"` ReceivedSuccess *bool `json:"received_success" query:"received_success"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` @@ -60,7 +51,7 @@ type ListDeviceCommandLogRequest struct { // DeviceCommandLogDTO 是用于API响应的设备命令日志结构 type DeviceCommandLogDTO struct { MessageID string `json:"message_id"` - DeviceID uint `json:"device_id"` + DeviceID uint32 `json:"device_id"` SentAt time.Time `json:"sent_at"` AcknowledgedAt *time.Time `json:"acknowledged_at"` ReceivedSuccess bool `json:"received_success"` @@ -78,7 +69,7 @@ type ListDeviceCommandLogResponse struct { type ListPlanExecutionLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PlanID *uint `json:"plan_id" query:"plan_id"` + PlanID *uint32 `json:"plan_id" query:"plan_id"` Status *string `json:"status" query:"status"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` @@ -87,10 +78,10 @@ type ListPlanExecutionLogRequest struct { // PlanExecutionLogDTO 是用于API响应的计划执行日志结构 type PlanExecutionLogDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - PlanID uint `json:"plan_id"` + PlanID uint32 `json:"plan_id"` PlanName string `json:"plan_name"` Status models.ExecutionStatus `json:"status"` StartedAt time.Time `json:"started_at"` @@ -110,7 +101,7 @@ type ListPlanExecutionLogResponse struct { type ListTaskExecutionLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PlanExecutionLogID *uint `json:"plan_execution_log_id" query:"plan_execution_log_id"` + PlanExecutionLogID *uint32 `json:"plan_execution_log_id" query:"plan_execution_log_id"` TaskID *int `json:"task_id" query:"task_id"` Status *string `json:"status" query:"status"` StartTime *time.Time `json:"start_time" query:"start_time"` @@ -120,17 +111,17 @@ type ListTaskExecutionLogRequest struct { // TaskDTO 是用于API响应的简化版任务结构 type TaskDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` Name string `json:"name"` Description string `json:"description"` } // TaskExecutionLogDTO 是用于API响应的任务执行日志结构 type TaskExecutionLogDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - PlanExecutionLogID uint `json:"plan_execution_log_id"` + PlanExecutionLogID uint32 `json:"plan_execution_log_id"` TaskID int `json:"task_id"` Task TaskDTO `json:"task"` // 嵌套的任务信息 Status models.ExecutionStatus `json:"status"` @@ -151,7 +142,7 @@ type ListTaskExecutionLogResponse struct { type ListPendingCollectionRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - DeviceID *uint `json:"device_id" query:"device_id"` + DeviceID *uint32 `json:"device_id" query:"device_id"` Status *string `json:"status" query:"status"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` @@ -161,7 +152,7 @@ type ListPendingCollectionRequest struct { // PendingCollectionDTO 是用于API响应的待采集请求结构 type PendingCollectionDTO struct { CorrelationID string `json:"correlation_id"` - DeviceID uint `json:"device_id"` + DeviceID uint32 `json:"device_id"` CommandMetadata models.UintArray `json:"command_metadata"` Status models.PendingCollectionStatus `json:"status"` FulfilledAt *time.Time `json:"fulfilled_at"` @@ -180,7 +171,7 @@ type ListPendingCollectionResponse struct { type ListUserActionLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - UserID *uint `json:"user_id" query:"user_id"` + UserID *uint32 `json:"user_id" query:"user_id"` Username *string `json:"username" query:"username"` ActionType *string `json:"action_type" query:"action_type"` Status *string `json:"status" query:"status"` @@ -191,9 +182,9 @@ type ListUserActionLogRequest struct { // UserActionLogDTO 是用于API响应的用户操作日志结构 type UserActionLogDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` Time time.Time `json:"time"` - UserID uint `json:"user_id"` + UserID uint32 `json:"user_id"` Username string `json:"username"` SourceIP string `json:"source_ip"` ActionType string `json:"action_type"` @@ -217,7 +208,7 @@ type ListUserActionLogResponse struct { type ListRawMaterialPurchaseRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - RawMaterialID *uint `json:"raw_material_id" query:"raw_material_id"` + RawMaterialID *uint32 `json:"raw_material_id" query:"raw_material_id"` Supplier *string `json:"supplier" query:"supplier"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` @@ -226,19 +217,19 @@ type ListRawMaterialPurchaseRequest struct { // RawMaterialDTO 是用于API响应的简化版原料结构 type RawMaterialDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` Name string `json:"name"` } // RawMaterialPurchaseDTO 是用于API响应的原料采购结构 type RawMaterialPurchaseDTO struct { - ID uint `json:"id"` - RawMaterialID uint `json:"raw_material_id"` + ID uint32 `json:"id"` + RawMaterialID uint32 `json:"raw_material_id"` RawMaterial RawMaterialDTO `json:"raw_material"` Supplier string `json:"supplier"` - Amount float64 `json:"amount"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` + Amount float32 `json:"amount"` + UnitPrice float32 `json:"unit_price"` + TotalPrice float32 `json:"total_price"` PurchaseDate time.Time `json:"purchase_date"` CreatedAt time.Time `json:"created_at"` } @@ -255,9 +246,9 @@ type ListRawMaterialPurchaseResponse struct { type ListRawMaterialStockLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - RawMaterialID *uint `json:"raw_material_id" query:"raw_material_id"` + RawMaterialID *uint32 `json:"raw_material_id" query:"raw_material_id"` SourceType *string `json:"source_type" query:"source_type"` - SourceID *uint `json:"source_id" query:"source_id"` + SourceID *uint32 `json:"source_id" query:"source_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -265,11 +256,11 @@ type ListRawMaterialStockLogRequest struct { // RawMaterialStockLogDTO 是用于API响应的原料库存日志结构 type RawMaterialStockLogDTO struct { - ID uint `json:"id"` - RawMaterialID uint `json:"raw_material_id"` - ChangeAmount float64 `json:"change_amount"` + ID uint32 `json:"id"` + RawMaterialID uint32 `json:"raw_material_id"` + ChangeAmount float32 `json:"change_amount"` SourceType models.StockLogSourceType `json:"source_type"` - SourceID uint `json:"source_id"` + SourceID uint32 `json:"source_id"` HappenedAt time.Time `json:"happened_at"` Remarks string `json:"remarks"` } @@ -286,9 +277,9 @@ type ListRawMaterialStockLogResponse struct { type ListFeedUsageRecordRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PenID *uint `json:"pen_id" query:"pen_id"` - FeedFormulaID *uint `json:"feed_formula_id" query:"feed_formula_id"` - OperatorID *uint `json:"operator_id" query:"operator_id"` + PenID *uint32 `json:"pen_id" query:"pen_id"` + FeedFormulaID *uint32 `json:"feed_formula_id" query:"feed_formula_id"` + OperatorID *uint32 `json:"operator_id" query:"operator_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -296,26 +287,26 @@ type ListFeedUsageRecordRequest struct { // PenDTO 是用于API响应的简化版猪栏结构 type PenDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` Name string `json:"name"` } // FeedFormulaDTO 是用于API响应的简化版饲料配方结构 type FeedFormulaDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` Name string `json:"name"` } // FeedUsageRecordDTO 是用于API响应的饲料使用记录结构 type FeedUsageRecordDTO struct { - ID uint `json:"id"` - PenID uint `json:"pen_id"` + ID uint32 `json:"id"` + PenID uint32 `json:"pen_id"` Pen PenDTO `json:"pen"` - FeedFormulaID uint `json:"feed_formula_id"` + FeedFormulaID uint32 `json:"feed_formula_id"` FeedFormula FeedFormulaDTO `json:"feed_formula"` - Amount float64 `json:"amount"` + Amount float32 `json:"amount"` RecordedAt time.Time `json:"recorded_at"` - OperatorID uint `json:"operator_id"` + OperatorID uint32 `json:"operator_id"` Remarks string `json:"remarks"` } @@ -331,10 +322,10 @@ type ListFeedUsageRecordResponse struct { type ListMedicationLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"` - MedicationID *uint `json:"medication_id" query:"medication_id"` + PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"` + MedicationID *uint32 `json:"medication_id" query:"medication_id"` Reason *string `json:"reason" query:"reason"` - OperatorID *uint `json:"operator_id" query:"operator_id"` + OperatorID *uint32 `json:"operator_id" query:"operator_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -342,21 +333,21 @@ type ListMedicationLogRequest struct { // MedicationDTO 是用于API响应的简化版药品结构 type MedicationDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` Name string `json:"name"` } // MedicationLogDTO 是用于API响应的用药记录结构 type MedicationLogDTO struct { - ID uint `json:"id"` - PigBatchID uint `json:"pig_batch_id"` - MedicationID uint `json:"medication_id"` + ID uint32 `json:"id"` + PigBatchID uint32 `json:"pig_batch_id"` + MedicationID uint32 `json:"medication_id"` Medication MedicationDTO `json:"medication"` - DosageUsed float64 `json:"dosage_used"` + DosageUsed float32 `json:"dosage_used"` TargetCount int `json:"target_count"` Reason models.MedicationReasonType `json:"reason"` Description string `json:"description"` - OperatorID uint `json:"operator_id"` + OperatorID uint32 `json:"operator_id"` HappenedAt time.Time `json:"happened_at"` } @@ -372,9 +363,9 @@ type ListMedicationLogResponse struct { type ListPigBatchLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"` + PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"` ChangeType *string `json:"change_type" query:"change_type"` - OperatorID *uint `json:"operator_id" query:"operator_id"` + OperatorID *uint32 `json:"operator_id" query:"operator_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -382,16 +373,16 @@ type ListPigBatchLogRequest struct { // PigBatchLogDTO 是用于API响应的猪批次日志结构 type PigBatchLogDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - PigBatchID uint `json:"pig_batch_id"` + PigBatchID uint32 `json:"pig_batch_id"` ChangeType models.LogChangeType `json:"change_type"` ChangeCount int `json:"change_count"` Reason string `json:"reason"` BeforeCount int `json:"before_count"` AfterCount int `json:"after_count"` - OperatorID uint `json:"operator_id"` + OperatorID uint32 `json:"operator_id"` HappenedAt time.Time `json:"happened_at"` } @@ -407,7 +398,7 @@ type ListPigBatchLogResponse struct { type ListWeighingBatchRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"` + PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -415,12 +406,12 @@ type ListWeighingBatchRequest struct { // WeighingBatchDTO 是用于API响应的批次称重记录结构 type WeighingBatchDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` WeighingTime time.Time `json:"weighing_time"` Description string `json:"description"` - PigBatchID uint `json:"pig_batch_id"` + PigBatchID uint32 `json:"pig_batch_id"` } // ListWeighingBatchResponse 是获取批次称重记录列表的响应结构 @@ -435,9 +426,9 @@ type ListWeighingBatchResponse struct { type ListWeighingRecordRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - WeighingBatchID *uint `json:"weighing_batch_id" query:"weighing_batch_id"` - PenID *uint `json:"pen_id" query:"pen_id"` - OperatorID *uint `json:"operator_id" query:"operator_id"` + WeighingBatchID *uint32 `json:"weighing_batch_id" query:"weighing_batch_id"` + PenID *uint32 `json:"pen_id" query:"pen_id"` + OperatorID *uint32 `json:"operator_id" query:"operator_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -445,13 +436,13 @@ type ListWeighingRecordRequest struct { // WeighingRecordDTO 是用于API响应的单次称重记录结构 type WeighingRecordDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - Weight float64 `json:"weight"` - WeighingBatchID uint `json:"weighing_batch_id"` - PenID uint `json:"pen_id"` - OperatorID uint `json:"operator_id"` + Weight float32 `json:"weight"` + WeighingBatchID uint32 `json:"weighing_batch_id"` + PenID uint32 `json:"pen_id"` + OperatorID uint32 `json:"operator_id"` Remark string `json:"remark"` WeighingTime time.Time `json:"weighing_time"` } @@ -468,10 +459,10 @@ type ListWeighingRecordResponse struct { type ListPigTransferLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"` - PenID *uint `json:"pen_id" query:"pen_id"` + PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"` + PenID *uint32 `json:"pen_id" query:"pen_id"` TransferType *string `json:"transfer_type" query:"transfer_type"` - OperatorID *uint `json:"operator_id" query:"operator_id"` + OperatorID *uint32 `json:"operator_id" query:"operator_id"` CorrelationID *string `json:"correlation_id" query:"correlation_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` @@ -480,16 +471,16 @@ type ListPigTransferLogRequest struct { // PigTransferLogDTO 是用于API响应的猪只迁移日志结构 type PigTransferLogDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` TransferTime time.Time `json:"transfer_time"` - PigBatchID uint `json:"pig_batch_id"` - PenID uint `json:"pen_id"` + PigBatchID uint32 `json:"pig_batch_id"` + PenID uint32 `json:"pen_id"` Quantity int `json:"quantity"` Type models.PigTransferType `json:"type"` CorrelationID string `json:"correlation_id"` - OperatorID uint `json:"operator_id"` + OperatorID uint32 `json:"operator_id"` Remarks string `json:"remarks"` } @@ -505,11 +496,11 @@ type ListPigTransferLogResponse struct { type ListPigSickLogRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"` - PenID *uint `json:"pen_id" query:"pen_id"` + PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"` + PenID *uint32 `json:"pen_id" query:"pen_id"` Reason *string `json:"reason" query:"reason"` TreatmentLocation *string `json:"treatment_location" query:"treatment_location"` - OperatorID *uint `json:"operator_id" query:"operator_id"` + OperatorID *uint32 `json:"operator_id" query:"operator_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -517,18 +508,18 @@ type ListPigSickLogRequest struct { // PigSickLogDTO 是用于API响应的病猪日志结构 type PigSickLogDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - PigBatchID uint `json:"pig_batch_id"` - PenID uint `json:"pen_id"` + PigBatchID uint32 `json:"pig_batch_id"` + PenID uint32 `json:"pen_id"` ChangeCount int `json:"change_count"` Reason models.PigBatchSickPigReasonType `json:"reason"` BeforeCount int `json:"before_count"` AfterCount int `json:"after_count"` Remarks string `json:"remarks"` TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatment_location"` - OperatorID uint `json:"operator_id"` + OperatorID uint32 `json:"operator_id"` HappenedAt time.Time `json:"happened_at"` } @@ -544,9 +535,9 @@ type ListPigSickLogResponse struct { type ListPigPurchaseRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"` + PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"` Supplier *string `json:"supplier" query:"supplier"` - OperatorID *uint `json:"operator_id" query:"operator_id"` + OperatorID *uint32 `json:"operator_id" query:"operator_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -554,17 +545,17 @@ type ListPigPurchaseRequest struct { // PigPurchaseDTO 是用于API响应的猪只采购记录结构 type PigPurchaseDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - PigBatchID uint `json:"pig_batch_id"` + PigBatchID uint32 `json:"pig_batch_id"` PurchaseDate time.Time `json:"purchase_date"` Supplier string `json:"supplier"` Quantity int `json:"quantity"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` + UnitPrice float32 `json:"unit_price"` + TotalPrice float32 `json:"total_price"` Remarks string `json:"remarks"` - OperatorID uint `json:"operator_id"` + OperatorID uint32 `json:"operator_id"` } // ListPigPurchaseResponse 是获取猪只采购记录列表的响应结构 @@ -579,9 +570,9 @@ type ListPigPurchaseResponse struct { type ListPigSaleRequest struct { Page int `json:"page" query:"page"` PageSize int `json:"page_size" query:"page_size"` - PigBatchID *uint `json:"pig_batch_id" query:"pig_batch_id"` + PigBatchID *uint32 `json:"pig_batch_id" query:"pig_batch_id"` Buyer *string `json:"buyer" query:"buyer"` - OperatorID *uint `json:"operator_id" query:"operator_id"` + OperatorID *uint32 `json:"operator_id" query:"operator_id"` StartTime *time.Time `json:"start_time" query:"start_time"` EndTime *time.Time `json:"end_time" query:"end_time"` OrderBy string `json:"order_by" query:"order_by"` @@ -589,17 +580,17 @@ type ListPigSaleRequest struct { // PigSaleDTO 是用于API响应的猪只销售记录结构 type PigSaleDTO struct { - ID uint `json:"id"` + ID uint32 `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - PigBatchID uint `json:"pig_batch_id"` + PigBatchID uint32 `json:"pig_batch_id"` SaleDate time.Time `json:"sale_date"` Buyer string `json:"buyer"` Quantity int `json:"quantity"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` + UnitPrice float32 `json:"unit_price"` + TotalPrice float32 `json:"total_price"` Remarks string `json:"remarks"` - OperatorID uint `json:"operator_id"` + OperatorID uint32 `json:"operator_id"` } // ListPigSaleResponse 是获取猪只销售记录列表的响应结构 diff --git a/internal/app/dto/notification_converter.go b/internal/app/dto/notification_converter.go index fbc49b7..dddc5d2 100644 --- a/internal/app/dto/notification_converter.go +++ b/internal/app/dto/notification_converter.go @@ -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, diff --git a/internal/app/dto/notification_dto.go b/internal/app/dto/notification_dto.go index b493d4e..15d2b48 100644 --- a/internal/app/dto/notification_dto.go +++ b/internal/app/dto/notification_dto.go @@ -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"` diff --git a/internal/app/dto/pig_batch_dto.go b/internal/app/dto/pig_batch_dto.go index 1cdfc3c..2f3fbab 100644 --- a/internal/app/dto/pig_batch_dto.go +++ b/internal/app/dto/pig_batch_dto.go @@ -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"` // 备注 diff --git a/internal/app/dto/pig_farm_dto.go b/internal/app/dto/pig_farm_dto.go index fd789bc..913e374 100644 --- a/internal/app/dto/pig_farm_dto.go +++ b/internal/app/dto/pig_farm_dto.go @@ -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校验 } diff --git a/internal/app/dto/plan_converter.go b/internal/app/dto/plan_converter.go index adc018c..e7313cc 100644 --- a/internal/app/dto/plan_converter.go +++ b/internal/app/dto/plan_converter.go @@ -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, diff --git a/internal/app/dto/plan_dto.go b/internal/app/dto/plan_dto.go index 51b82f1..b46efdb 100644 --- a/internal/app/dto/plan_dto.go +++ b/internal/app/dto/plan_dto.go @@ -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"` diff --git a/internal/app/dto/user_dto.go b/internal/app/dto/user_dto.go index 1a758c7..17700f2 100644 --- a/internal/app/dto/user_dto.go +++ b/internal/app/dto/user_dto.go @@ -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:"设备更新成功"` diff --git a/internal/app/service/device_service.go b/internal/app/service/device_service.go index d00080e..89cc49c 100644 --- a/internal/app/service/device_service.go +++ b/internal/app/service/device_service.go @@ -28,32 +28,33 @@ var ( // DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。 type DeviceService interface { CreateDevice(ctx context.Context, req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) - GetDevice(ctx context.Context, id uint) (*dto.DeviceResponse, error) + GetDevice(ctx context.Context, id uint32) (*dto.DeviceResponse, error) ListDevices(ctx context.Context) ([]*dto.DeviceResponse, error) - UpdateDevice(ctx context.Context, id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) - DeleteDevice(ctx context.Context, id uint) error - ManualControl(ctx context.Context, id uint, req *dto.ManualControlDeviceRequest) error + UpdateDevice(ctx context.Context, id uint32, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) + DeleteDevice(ctx context.Context, id uint32) error + ManualControl(ctx context.Context, id uint32, req *dto.ManualControlDeviceRequest) error CreateAreaController(ctx context.Context, req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error) - GetAreaController(ctx context.Context, id uint) (*dto.AreaControllerResponse, error) + GetAreaController(ctx context.Context, id uint32) (*dto.AreaControllerResponse, error) ListAreaControllers(ctx context.Context) ([]*dto.AreaControllerResponse, error) - UpdateAreaController(ctx context.Context, id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) - DeleteAreaController(ctx context.Context, id uint) error + UpdateAreaController(ctx context.Context, id uint32, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) + DeleteAreaController(ctx context.Context, id uint32) error CreateDeviceTemplate(ctx context.Context, req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) - GetDeviceTemplate(ctx context.Context, id uint) (*dto.DeviceTemplateResponse, error) + GetDeviceTemplate(ctx context.Context, id uint32) (*dto.DeviceTemplateResponse, error) ListDeviceTemplates(ctx context.Context) ([]*dto.DeviceTemplateResponse, error) - UpdateDeviceTemplate(ctx context.Context, id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) - DeleteDeviceTemplate(ctx context.Context, id uint) error + UpdateDeviceTemplate(ctx context.Context, id uint32, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) + DeleteDeviceTemplate(ctx context.Context, id uint32) error } // deviceService 是 DeviceService 接口的具体实现。 type deviceService struct { - ctx context.Context - deviceRepo repository.DeviceRepository - areaControllerRepo repository.AreaControllerRepository - deviceTemplateRepo repository.DeviceTemplateRepository - deviceDomainSvc device.Service + ctx context.Context + deviceRepo repository.DeviceRepository + areaControllerRepo repository.AreaControllerRepository + deviceTemplateRepo repository.DeviceTemplateRepository + deviceDomainSvc device.Service + thresholdAlarmService ThresholdAlarmService } // NewDeviceService 创建一个新的 DeviceService 实例。 @@ -63,13 +64,15 @@ func NewDeviceService( areaControllerRepo repository.AreaControllerRepository, deviceTemplateRepo repository.DeviceTemplateRepository, deviceDomainSvc device.Service, + thresholdAlarmService ThresholdAlarmService, ) DeviceService { return &deviceService{ - ctx: ctx, - deviceRepo: deviceRepo, - areaControllerRepo: areaControllerRepo, - deviceTemplateRepo: deviceTemplateRepo, - deviceDomainSvc: deviceDomainSvc, + ctx: ctx, + deviceRepo: deviceRepo, + areaControllerRepo: areaControllerRepo, + deviceTemplateRepo: deviceTemplateRepo, + deviceDomainSvc: deviceDomainSvc, + thresholdAlarmService: thresholdAlarmService, } } @@ -106,7 +109,7 @@ func (s *deviceService) CreateDevice(ctx context.Context, req *dto.CreateDeviceR return dto.NewDeviceResponse(createdDevice) } -func (s *deviceService) GetDevice(ctx context.Context, id uint) (*dto.DeviceResponse, error) { +func (s *deviceService) GetDevice(ctx context.Context, id uint32) (*dto.DeviceResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDevice") device, err := s.deviceRepo.FindByID(serviceCtx, id) if err != nil { @@ -124,7 +127,7 @@ func (s *deviceService) ListDevices(ctx context.Context) ([]*dto.DeviceResponse, return dto.NewListDeviceResponse(devices) } -func (s *deviceService) UpdateDevice(ctx context.Context, id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) { +func (s *deviceService) UpdateDevice(ctx context.Context, id uint32, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateDevice") existingDevice, err := s.deviceRepo.FindByID(serviceCtx, id) if err != nil { @@ -158,7 +161,7 @@ func (s *deviceService) UpdateDevice(ctx context.Context, id uint, req *dto.Upda return dto.NewDeviceResponse(updatedDevice) } -func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error { +func (s *deviceService) DeleteDevice(ctx context.Context, id uint32) error { serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteDevice") // 检查设备是否存在 @@ -168,7 +171,7 @@ func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error { } // 在删除前检查设备是否被任务使用 - inUse, err := s.deviceRepo.IsDeviceInUse(serviceCtx, id) + inUse, err := s.deviceRepo.IsDeviceInUse(serviceCtx, id, []models.TaskType{models.TaskTypeDeviceThresholdCheck}) if err != nil { // 如果检查过程中发生数据库错误,则返回错误 return fmt.Errorf("检查设备使用情况失败: %w", err) @@ -178,11 +181,17 @@ func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error { return ErrDeviceInUse } + // TODO 这个应该用事务处理 + err = s.thresholdAlarmService.DeleteDeviceThresholdAlarmByDeviceID(serviceCtx, id) + if err != nil { + return fmt.Errorf("删除设备阈值告警失败: %w", err) + } + // 只有在未被使用时,才执行删除操作 return s.deviceRepo.Delete(serviceCtx, id) } -func (s *deviceService) ManualControl(ctx context.Context, id uint, req *dto.ManualControlDeviceRequest) error { +func (s *deviceService) ManualControl(ctx context.Context, id uint32, req *dto.ManualControlDeviceRequest) error { serviceCtx := logs.AddFuncName(ctx, s.ctx, "ManualControl") dev, err := s.deviceRepo.FindByID(serviceCtx, id) if err != nil { @@ -232,7 +241,7 @@ func (s *deviceService) CreateAreaController(ctx context.Context, req *dto.Creat return dto.NewAreaControllerResponse(ac) } -func (s *deviceService) GetAreaController(ctx context.Context, id uint) (*dto.AreaControllerResponse, error) { +func (s *deviceService) GetAreaController(ctx context.Context, id uint32) (*dto.AreaControllerResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetAreaController") ac, err := s.areaControllerRepo.FindByID(serviceCtx, id) if err != nil { @@ -250,7 +259,7 @@ func (s *deviceService) ListAreaControllers(ctx context.Context) ([]*dto.AreaCon return dto.NewListAreaControllerResponse(acs) } -func (s *deviceService) UpdateAreaController(ctx context.Context, id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) { +func (s *deviceService) UpdateAreaController(ctx context.Context, id uint32, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateAreaController") existingAC, err := s.areaControllerRepo.FindByID(serviceCtx, id) if err != nil { @@ -278,7 +287,7 @@ func (s *deviceService) UpdateAreaController(ctx context.Context, id uint, req * return dto.NewAreaControllerResponse(existingAC) } -func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error { +func (s *deviceService) DeleteAreaController(ctx context.Context, id uint32) error { serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteAreaController") // 1. 检查是否存在 @@ -288,7 +297,7 @@ func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error } // 2. 检查是否被使用(业务逻辑) - inUse, err := s.deviceRepo.IsAreaControllerInUse(serviceCtx, id) + inUse, err := s.areaControllerRepo.IsAreaControllerUsedByTasks(serviceCtx, id, []models.TaskType{models.TaskTypeAreaCollectorThresholdCheck}) if err != nil { return err // 返回数据库检查错误 } @@ -296,6 +305,12 @@ func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error return ErrAreaControllerInUse // 返回业务错误 } + // TODO 这个应该用事务处理 + err = s.thresholdAlarmService.DeleteAreaThresholdAlarmByAreaControllerID(serviceCtx, id) + if err != nil { + return fmt.Errorf("删除区域阈值告警失败: %w", err) + } + // 3. 执行删除 return s.areaControllerRepo.Delete(serviceCtx, id) } @@ -334,7 +349,7 @@ func (s *deviceService) CreateDeviceTemplate(ctx context.Context, req *dto.Creat return dto.NewDeviceTemplateResponse(deviceTemplate) } -func (s *deviceService) GetDeviceTemplate(ctx context.Context, id uint) (*dto.DeviceTemplateResponse, error) { +func (s *deviceService) GetDeviceTemplate(ctx context.Context, id uint32) (*dto.DeviceTemplateResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDeviceTemplate") deviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id) if err != nil { @@ -352,7 +367,7 @@ func (s *deviceService) ListDeviceTemplates(ctx context.Context) ([]*dto.DeviceT return dto.NewListDeviceTemplateResponse(deviceTemplates) } -func (s *deviceService) UpdateDeviceTemplate(ctx context.Context, id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) { +func (s *deviceService) UpdateDeviceTemplate(ctx context.Context, id uint32, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateDeviceTemplate") existingDeviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id) if err != nil { @@ -387,7 +402,7 @@ func (s *deviceService) UpdateDeviceTemplate(ctx context.Context, id uint, req * return dto.NewDeviceTemplateResponse(existingDeviceTemplate) } -func (s *deviceService) DeleteDeviceTemplate(ctx context.Context, id uint) error { +func (s *deviceService) DeleteDeviceTemplate(ctx context.Context, id uint32) error { serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteDeviceTemplate") // 1. 检查是否存在 diff --git a/internal/app/service/monitor_service.go b/internal/app/service/monitor_service.go index ea3bbab..64be63e 100644 --- a/internal/app/service/monitor_service.go +++ b/internal/app/service/monitor_service.go @@ -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 { diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index 4ee78ac..8702bbf 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -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 { diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index 4ea98f3..ba85089 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -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 { diff --git a/internal/app/service/plan_service.go b/internal/app/service/plan_service.go index ba8b416..3d74823 100644 --- a/internal/app/service/plan_service.go +++ b/internal/app/service/plan_service.go @@ -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 = "应用服务层:停止计划" diff --git a/internal/app/service/threshold_alarm_service.go b/internal/app/service/threshold_alarm_service.go new file mode 100644 index 0000000..834a003 --- /dev/null +++ b/internal/app/service/threshold_alarm_service.go @@ -0,0 +1,688 @@ +package service + +import ( + "context" + "fmt" + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" + domainAlarm "git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" +) + +// ThresholdAlarmService 定义了阈值告警配置服务的接口。 +// 该服务负责管理阈值告警任务的配置,并将其与计划进行联动。 +type ThresholdAlarmService interface { + // SnoozeThresholdAlarm 忽略一个阈值告警,或更新其忽略时间。 + SnoozeThresholdAlarm(ctx context.Context, alarmID uint32, durationMinutes uint32) error + // CancelSnoozeThresholdAlarm 取消对一个阈值告警的忽略状态。 + CancelSnoozeThresholdAlarm(ctx context.Context, alarmID uint32) error + // ListActiveAlarms 批量查询活跃告警。 + ListActiveAlarms(ctx context.Context, req *dto.ListActiveAlarmRequest) (*dto.ListActiveAlarmResponse, error) + // ListHistoricalAlarms 批量查询历史告警。 + ListHistoricalAlarms(ctx context.Context, req *dto.ListHistoricalAlarmRequest) (*dto.ListHistoricalAlarmResponse, error) + + // CreateDeviceThresholdAlarm 创建一个设备阈值告警。 + CreateDeviceThresholdAlarm(ctx context.Context, req *dto.CreateDeviceThresholdAlarmDTO) error + // UpdateDeviceThresholdAlarm 更新一个设备阈值告警。 + UpdateDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.UpdateDeviceThresholdAlarmDTO) error + // GetDeviceThresholdAlarm 根据ID获取一个设备阈值告警任务。 + GetDeviceThresholdAlarm(ctx context.Context, taskID int) (*dto.DeviceThresholdAlarmDTO, error) + // DeleteDeviceThresholdAlarm 删除一个设备阈值告警。 + DeleteDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.DeleteDeviceThresholdAlarmDTO) error + // DeleteDeviceThresholdAlarmByDeviceID 实现了根据设备ID删除一个设备下所有设备阈值告警的逻辑。 + DeleteDeviceThresholdAlarmByDeviceID(ctx context.Context, deviceID uint32) error + + // CreateAreaThresholdAlarm 创建一个区域阈值告警。 + CreateAreaThresholdAlarm(ctx context.Context, req *dto.CreateAreaThresholdAlarmDTO) error + // UpdateAreaThresholdAlarm 更新一个区域阈值告警。 + UpdateAreaThresholdAlarm(ctx context.Context, taskID int, req *dto.UpdateAreaThresholdAlarmDTO) error + // GetAreaThresholdAlarm 根据ID获取一个区域阈值告警任务。 + GetAreaThresholdAlarm(ctx context.Context, taskID int) (*dto.AreaThresholdAlarmDTO, error) + // DeleteAreaThresholdAlarm 实现了删除一个区域阈值告警的逻辑。 + DeleteAreaThresholdAlarm(ctx context.Context, taskID int) error + // DeleteAreaThresholdAlarmByAreaControllerID 实现了根据区域ID删除一个区域下所有区域阈值告警的逻辑。 + DeleteAreaThresholdAlarmByAreaControllerID(ctx context.Context, areaControllerID uint32) error +} + +// thresholdAlarmService 是 ThresholdAlarmService 接口的具体实现。 +type thresholdAlarmService struct { + ctx context.Context + alarmService domainAlarm.AlarmService + planService plan.Service + + alarmRepo repository.AlarmRepository + planRepo repository.PlanRepository + areaRepo repository.AreaControllerRepository + deviceRepo repository.DeviceRepository +} + +// NewThresholdAlarmService 创建一个新的 ThresholdAlarmService 实例。 +func NewThresholdAlarmService(ctx context.Context, + alarmService domainAlarm.AlarmService, + planService plan.Service, + alarmRepo repository.AlarmRepository, + planRepo repository.PlanRepository, + areaRepo repository.AreaControllerRepository, + deviceRepo repository.DeviceRepository, +) ThresholdAlarmService { + return &thresholdAlarmService{ + ctx: ctx, + alarmService: alarmService, + planService: planService, + alarmRepo: alarmRepo, + planRepo: planRepo, + areaRepo: areaRepo, + deviceRepo: deviceRepo, + } +} + +// SnoozeThresholdAlarm 实现了忽略阈值告警的逻辑。 +func (s *thresholdAlarmService) SnoozeThresholdAlarm(ctx context.Context, alarmID uint32, durationMinutes uint32) error { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "SnoozeThresholdAlarm") + return s.alarmService.SnoozeAlarm(serviceCtx, alarmID, time.Duration(durationMinutes)*time.Minute) +} + +// CancelSnoozeThresholdAlarm 实现了取消忽略阈值告警的逻辑。 +func (s *thresholdAlarmService) CancelSnoozeThresholdAlarm(ctx context.Context, alarmID uint32) error { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "CancelSnoozeThresholdAlarm") + return s.alarmService.CancelAlarmSnooze(serviceCtx, alarmID) +} + +// ListActiveAlarms 实现了批量查询活跃告警的逻辑。 +func (s *thresholdAlarmService) ListActiveAlarms(ctx context.Context, req *dto.ListActiveAlarmRequest) (*dto.ListActiveAlarmResponse, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListActiveAlarms") + + opts := repository.ActiveAlarmListOptions{ + SourceType: req.SourceType, + SourceID: req.SourceID, + Level: req.Level, + IsIgnored: req.IsIgnored, + TriggerTime: req.TriggerTime, + EndTime: req.EndTime, + OrderBy: req.OrderBy, + } + + alarms, total, err := s.alarmRepo.ListActiveAlarms(serviceCtx, opts, req.Page, req.PageSize) + if err != nil { + return nil, err + } + + return dto.NewListActiveAlarmResponse(alarms, total, req.Page, req.PageSize), nil +} + +// ListHistoricalAlarms 实现了批量查询历史告警的逻辑。 +func (s *thresholdAlarmService) ListHistoricalAlarms(ctx context.Context, req *dto.ListHistoricalAlarmRequest) (*dto.ListHistoricalAlarmResponse, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListHistoricalAlarms") + + opts := repository.HistoricalAlarmListOptions{ + SourceType: req.SourceType, + SourceID: req.SourceID, + Level: req.Level, + TriggerTimeStart: req.TriggerTimeStart, + TriggerTimeEnd: req.TriggerTimeEnd, + ResolveTimeStart: req.ResolveTimeStart, + ResolveTimeEnd: req.ResolveTimeEnd, + OrderBy: req.OrderBy, + } + + alarms, total, err := s.alarmRepo.ListHistoricalAlarms(serviceCtx, opts, req.Page, req.PageSize) + if err != nil { + return nil, err + } + + return dto.NewListHistoricalAlarmResponse(alarms, total, req.Page, req.PageSize), nil +} + +// CreateDeviceThresholdAlarm 实现了创建一个设备阈值告警的逻辑。 +func (s *thresholdAlarmService) CreateDeviceThresholdAlarm(ctx context.Context, req *dto.CreateDeviceThresholdAlarmDTO) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreateThresholdAlarm") + + device, err := s.deviceRepo.FindByID(serviceCtx, req.DeviceID) + if err != nil { + return fmt.Errorf("获取设备 %v 失败: %v", req.DeviceID, err) + } + + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统计划 %v 失败: %v", models.PlanNamePeriodicSystemHealthCheck, err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + // 系统计划肯定是子任务 + for i, t := range plan.Tasks { + switch t.Type { + case models.TaskTypeDeviceThresholdCheck: // 检查任务是否存在 + var params task.DeviceThresholdCheckParams + err = t.ParseParameters(¶ms) + if err != nil { + return fmt.Errorf("任务 %v: 解析设备阈值检查任务参数失败: %v", t.ID, err) + } + if params.DeviceID == req.DeviceID && params.SensorType == req.SensorType { + return fmt.Errorf("设备 %v: 该设备已存在阈值检查任务", req.DeviceID) + } + case models.TaskTypeAreaCollectorThresholdCheck: // 向区域阈值检查任务过滤列表中添加该设备 + params := task.AreaThresholdCheckParams{ + ExcludeDeviceIDs: []uint32{}, + } + err = t.ParseParameters(¶ms) + if err != nil { + return fmt.Errorf("任务 %v: 解析区域阈值检查任务参数失败: %v", t.ID, err) + } + if params.AreaControllerID == device.AreaControllerID { + has := false + for _, d := range params.ExcludeDeviceIDs { + if d == req.DeviceID { + has = true + break + } + } + if !has { + params.ExcludeDeviceIDs = append(params.ExcludeDeviceIDs, req.DeviceID) + err = plan.Tasks[i].SaveParameters(params) + if err != nil { + return fmt.Errorf("任务 %v: 保存任务参数失败: %v", t.ID, err) + } + } + } + default: + continue + } + } + + t := models.Task{ + PlanID: plan.ID, + Name: fmt.Sprintf("设备 %v 的阈值检测任务", req.DeviceID), + Description: fmt.Sprintf("检测该设备 %v 是否 %v %v", req.SensorType, req.Operator, req.Thresholds), + ExecutionOrder: len(plan.Tasks) + 1, + Type: models.TaskTypeDeviceThresholdCheck, + } + err = t.SaveParameters(task.DeviceThresholdCheckParams{ + DeviceID: req.DeviceID, + SensorType: req.SensorType, + Thresholds: req.Thresholds, + Level: req.Level, + Operator: req.Operator, + }) + if err != nil { + return fmt.Errorf("保存任务参数失败: %v", err) + } + + plan.Tasks = append(plan.Tasks, t) + plan.ReorderSteps() + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + if err != nil { + return fmt.Errorf("更新计划失败: %v", err) + } + return nil +} + +// UpdateDeviceThresholdAlarm 实现了更新一个设备阈值告警的逻辑。 +func (s *thresholdAlarmService) UpdateDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.UpdateDeviceThresholdAlarmDTO) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdateDeviceThresholdAlarm") + + // 1. 获取系统健康检查计划 + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统健康检查计划失败: %w", err) + } + if plan == nil { + // 这个系统计划必须存在 + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + // 2. 遍历任务列表,查找并更新目标任务 + taskFound := false + for i, t := range plan.Tasks { + if t.ID == taskID && t.Type == models.TaskTypeDeviceThresholdCheck { + taskFound = true + + var params task.DeviceThresholdCheckParams + if err = t.ParseParameters(¶ms); err != nil { + return fmt.Errorf("任务 %d: 解析现有参数失败: %w", taskID, err) + } + params.Thresholds = req.Thresholds + params.Operator = req.Operator + params.Level = req.Level + // 刷新任务说明 + plan.Tasks[i].Description = fmt.Sprintf("检测该设备 %v 是否 %v %v", params.SensorType, params.Operator, params.Thresholds) + + err = plan.Tasks[i].SaveParameters(params) + if err != nil { + return fmt.Errorf("任务 %d: 保存参数失败: %w", taskID, err) + } + + break + } + } + + if !taskFound { + return fmt.Errorf("任务 %d: 不存在", taskID) + } + + // 全量更新计划 + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err +} + +// GetDeviceThresholdAlarm 实现了根据ID获取一个设备阈值告警任务的逻辑。 +func (s *thresholdAlarmService) GetDeviceThresholdAlarm(ctx context.Context, taskID int) (*dto.DeviceThresholdAlarmDTO, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDeviceThresholdAlarm") + + // 1. 使用 planRepo 查询任务 + t, err := s.planRepo.FindTaskByID(serviceCtx, taskID) + if err != nil { + return nil, err // 如果未找到或发生其他错误,直接返回 + } + + // 2. 验证任务类型是否正确 + if t.Type != models.TaskTypeDeviceThresholdCheck { + return nil, fmt.Errorf("任务 %d 不是一个设备阈值检查任务", taskID) + } + var params task.DeviceThresholdCheckParams + err = t.ParseParameters(¶ms) + if err != nil { + return nil, fmt.Errorf("任务 %d: 解析参数失败: %w", taskID, err) + } + + resp := &dto.DeviceThresholdAlarmDTO{ + ID: t.ID, + DeviceID: params.DeviceID, + SensorType: params.SensorType, + Thresholds: params.Thresholds, + Operator: params.Operator, + Level: params.Level, + } + + return resp, nil +} + +// DeleteDeviceThresholdAlarm 实现了删除一个设备阈值告警的逻辑。 +func (s *thresholdAlarmService) DeleteDeviceThresholdAlarm(ctx context.Context, taskID int, req *dto.DeleteDeviceThresholdAlarmDTO) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteDeviceThresholdAlarm") + + // 获取待删除任务并校验 + deleteTask, err := s.planRepo.FindTaskByID(serviceCtx, taskID) + if err != nil { + return fmt.Errorf("获取任务失败: %w", err) + } + if deleteTask.Type != models.TaskTypeDeviceThresholdCheck { + return fmt.Errorf("任务 %d 不是一个设备阈值检查任务", taskID) + } + var deviceParams task.DeviceThresholdCheckParams + if err := deleteTask.ParseParameters(&deviceParams); err != nil { + return fmt.Errorf("任务 %d: 解析参数失败: %w", taskID, err) + } + + // 获得任务对应设备对应区域主控 + device, err := s.deviceRepo.FindByID(serviceCtx, deviceParams.DeviceID) + if err != nil { + return fmt.Errorf("获取设备 %d 失败: %w", deviceParams.DeviceID, err) + } + area, err := s.areaRepo.FindByID(serviceCtx, device.AreaControllerID) + if err != nil { + return fmt.Errorf("获取区域 %d 失败: %w", device.AreaControllerID, err) + } + + // 获取健康检查计划任务列表 + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统健康检查计划失败: %w", err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + taskIndexToDelete := -1 + for i, t := range plan.Tasks { + if t.ID == taskID { + taskIndexToDelete = i + } + if t.Type == models.TaskTypeAreaCollectorThresholdCheck { + var areaParams task.AreaThresholdCheckParams + if err := t.ParseParameters(&areaParams); err != nil { + return fmt.Errorf("任务 %d: 解析参数失败: %w", t.ID, err) + } + if areaParams.AreaControllerID == area.ID && areaParams.SensorType == deviceParams.SensorType { + for ia, e := range areaParams.ExcludeDeviceIDs { + if e == deviceParams.DeviceID { + areaParams.ExcludeDeviceIDs = append(areaParams.ExcludeDeviceIDs[:ia], areaParams.ExcludeDeviceIDs[ia+1:]...) + continue + } + } + err = plan.Tasks[i].SaveParameters(areaParams) + if err != nil { + return fmt.Errorf("任务 %d: 保存参数失败: %w", t.ID, err) + } + } + } + } + + if taskIndexToDelete == -1 { + return fmt.Errorf("任务 %d 在系统计划中未找到", taskID) + } + plan.Tasks = append(plan.Tasks[:taskIndexToDelete], plan.Tasks[taskIndexToDelete+1:]...) + + plan.ReorderSteps() + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err +} + +// DeleteDeviceThresholdAlarmByDeviceID 实现了根据设备ID删除一个设备下所有设备阈值告警的逻辑。 +func (s *thresholdAlarmService) DeleteDeviceThresholdAlarmByDeviceID(ctx context.Context, deviceID uint32) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteDeviceThresholdAlarmByDeviceID") + tasks, err := s.planRepo.ListTasksByDeviceID(serviceCtx, deviceID) + if err != nil { + return fmt.Errorf("获取任务列表失败: %w", err) + } + device, err := s.deviceRepo.FindByID(serviceCtx, deviceID) + if err != nil { + return fmt.Errorf("获取设备 %d 失败: %w", deviceID, err) + } + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统计划失败: %w", err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + deleteNums := []int{} + for i, t := range plan.Tasks { + for _, dt := range tasks { + if t.ID == dt.ID && t.Type == models.TaskTypeDeviceThresholdCheck { + deleteNums = append(deleteNums, i) + break + } + } + + if t.Type == models.TaskTypeAreaCollectorThresholdCheck { + var areaParams task.AreaThresholdCheckParams + if err := t.ParseParameters(&areaParams); err != nil { + return fmt.Errorf("任务 %d: 解析参数失败: %w", t.ID, err) + } + if areaParams.AreaControllerID == device.AreaControllerID { + for ai, ae := range areaParams.ExcludeDeviceIDs { + if ae == deviceID { + areaParams.ExcludeDeviceIDs = append(areaParams.ExcludeDeviceIDs[:ai], areaParams.ExcludeDeviceIDs[ai+1:]...) + break + } + } + err = plan.Tasks[i].SaveParameters(areaParams) + if err != nil { + return fmt.Errorf("任务 %d: 保存参数失败: %w", t.ID, err) + } + } + } + } + + // 为了高效地判断一个索引是否需要被删除,先将 deleteNums 转换为一个 map。 + deleteMap := make(map[int]struct{}, len(deleteNums)) + for _, index := range deleteNums { + deleteMap[index] = struct{}{} + } + + // 创建一个新的任务切片,只包含不需要删除的任务。 + newTasks := make([]models.Task, 0, len(plan.Tasks)-len(deleteNums)) + for i, t := range plan.Tasks { + if _, found := deleteMap[i]; !found { + newTasks = append(newTasks, t) + } + } + plan.Tasks = newTasks + + // 重新排序任务并更新计划 + plan.ReorderSteps() + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err + +} + +// CreateAreaThresholdAlarm 实现了创建一个区域阈值告警的逻辑。 +func (s *thresholdAlarmService) CreateAreaThresholdAlarm(ctx context.Context, req *dto.CreateAreaThresholdAlarmDTO) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreateAreaThresholdAlarm") + + // 1. 获取系统健康检查计划 + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统健康检查计划失败: %w", err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + // 2. 获取目标区域下的所有设备,并建立快速查找表 + devicesInArea, err := s.deviceRepo.ListByAreaControllerID(serviceCtx, req.AreaControllerID) + if err != nil { + return fmt.Errorf("获取区域 %d 下的设备列表失败: %w", req.AreaControllerID, err) + } + devicesInAreaMap := make(map[uint32]struct{}, len(devicesInArea)) + for _, device := range devicesInArea { + devicesInAreaMap[device.ID] = struct{}{} + } + + // 3. 遍历计划,检查存在性并收集需要排除的设备ID + var excludeDeviceIDs []uint32 + for _, t := range plan.Tasks { + switch t.Type { + case models.TaskTypeAreaCollectorThresholdCheck: + var params task.AreaThresholdCheckParams + if err := t.ParseParameters(¶ms); err != nil { + return fmt.Errorf("任务 %d: 解析区域阈值检查任务参数失败: %w", t.ID, err) + } + if params.AreaControllerID == req.AreaControllerID && params.SensorType == req.SensorType { + return fmt.Errorf("区域 %d: 该区域已存在针对 %s 的阈值检查任务", req.AreaControllerID, req.SensorType) + } + case models.TaskTypeDeviceThresholdCheck: + var params task.DeviceThresholdCheckParams + if err := t.ParseParameters(¶ms); err != nil { + return fmt.Errorf("任务 %d: 解析设备阈值检查任务参数失败: %w", t.ID, err) + } + // 检查该设备是否属于目标区域 + if _, ok := devicesInAreaMap[params.DeviceID]; ok { + excludeDeviceIDs = append(excludeDeviceIDs, params.DeviceID) + } + } + } + + // 4. 创建新任务 + newTask := models.Task{ + PlanID: plan.ID, + Name: fmt.Sprintf("区域 %d 的 %s 阈值检测任务", req.AreaControllerID, req.SensorType), + Description: fmt.Sprintf("检测区域 %d 的 %s 是否 %v %v", req.AreaControllerID, req.SensorType, req.Operator, req.Thresholds), + ExecutionOrder: len(plan.Tasks) + 1, + Type: models.TaskTypeAreaCollectorThresholdCheck, + } + err = newTask.SaveParameters(task.AreaThresholdCheckParams{ + AreaControllerID: req.AreaControllerID, + SensorType: req.SensorType, + Thresholds: req.Thresholds, + Operator: req.Operator, + Level: req.Level, + ExcludeDeviceIDs: excludeDeviceIDs, + }) + if err != nil { + return fmt.Errorf("保存新区域任务的参数失败: %w", err) + } + + // 5. 更新计划 + plan.Tasks = append(plan.Tasks, newTask) + plan.ReorderSteps() + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err +} + +// UpdateAreaThresholdAlarm 实现了更新一个区域阈值告警的逻辑。 +func (s *thresholdAlarmService) UpdateAreaThresholdAlarm(ctx context.Context, taskID int, req *dto.UpdateAreaThresholdAlarmDTO) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdateAreaThresholdAlarm") + + // 1. 获取系统健康检查计划 + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统健康检查计划失败: %w", err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + // 2. 遍历任务列表,查找并更新目标任务 + taskFound := false + for i, t := range plan.Tasks { + if t.ID == taskID && t.Type == models.TaskTypeAreaCollectorThresholdCheck { + taskFound = true + + var params task.AreaThresholdCheckParams + if err = t.ParseParameters(¶ms); err != nil { + return fmt.Errorf("任务 %d: 解析现有参数失败: %w", taskID, err) + } + params.Thresholds = req.Thresholds + params.Operator = req.Operator + params.Level = req.Level + // 刷新任务说明 + plan.Tasks[i].Description = fmt.Sprintf("检测区域 %d 的 %s 是否 %v %v", params.AreaControllerID, params.SensorType, params.Operator, params.Thresholds) + + err = plan.Tasks[i].SaveParameters(params) + if err != nil { + return fmt.Errorf("任务 %d: 保存参数失败: %w", taskID, err) + } + + break + } + } + + if !taskFound { + return fmt.Errorf("任务 %d: 不存在或类型不匹配", taskID) + } + + // 全量更新计划 + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err +} + +// GetAreaThresholdAlarm 实现了根据ID获取一个区域阈值告警任务的逻辑。 +func (s *thresholdAlarmService) GetAreaThresholdAlarm(ctx context.Context, taskID int) (*dto.AreaThresholdAlarmDTO, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetAreaThresholdAlarm") + + // 1. 使用 planRepo 查询任务 + t, err := s.planRepo.FindTaskByID(serviceCtx, taskID) + if err != nil { + return nil, err // 如果未找到或发生其他错误,直接返回 + } + + // 2. 验证任务类型是否正确 + if t.Type != models.TaskTypeAreaCollectorThresholdCheck { + return nil, fmt.Errorf("任务 %d 不是一个区域阈值检查任务", taskID) + } + var params task.AreaThresholdCheckParams + err = t.ParseParameters(¶ms) + if err != nil { + return nil, fmt.Errorf("任务 %d: 解析参数失败: %w", taskID, err) + } + + resp := &dto.AreaThresholdAlarmDTO{ + ID: t.ID, + AreaControllerID: params.AreaControllerID, + SensorType: params.SensorType, + Thresholds: params.Thresholds, + Operator: params.Operator, + Level: params.Level, + } + return resp, nil +} + +// DeleteAreaThresholdAlarm 实现了删除一个区域阈值告警的逻辑。 +func (s *thresholdAlarmService) DeleteAreaThresholdAlarm(ctx context.Context, taskID int) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteAreaThresholdAlarm") + + // 获取健康检查计划任务列表 + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统健康检查计划失败: %w", err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + // 找到这个任务并删掉 + deleteTaskNum := -1 + for i, t := range plan.Tasks { + if t.Type != models.TaskTypeAreaCollectorThresholdCheck { + continue + } + if t.ID != taskID { + continue + } + deleteTaskNum = i + } + + if deleteTaskNum == -1 { + return fmt.Errorf("任务 %d: 不存在或类型不匹配", taskID) + } + plan.Tasks = append(plan.Tasks[:deleteTaskNum], plan.Tasks[deleteTaskNum+1:]...) + plan.ReorderSteps() + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err +} + +// DeleteAreaThresholdAlarmByAreaControllerID 实现了根据区域ID删除一个区域下所有区域阈值告警的逻辑。 +func (s *thresholdAlarmService) DeleteAreaThresholdAlarmByAreaControllerID(ctx context.Context, areaControllerID uint32) error { + serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeleteAreaThresholdAlarmByAreaControllerID") + + // 1. 获取系统健康检查计划 + plan, err := s.planRepo.GetSystemPlanByName(serviceCtx, models.PlanNamePeriodicSystemHealthCheck) + if err != nil { + return fmt.Errorf("获取系统健康检查计划失败: %w", err) + } + if plan == nil { + logger.Panicf("系统计划 %v 不存在", models.PlanNamePeriodicSystemHealthCheck) + } + + // 2. 收集所有与指定 areaControllerID 相关的区域阈值告警任务的索引 + var deleteIndices []int + for i, t := range plan.Tasks { + // 只关心区域阈值检查任务 + if t.Type != models.TaskTypeAreaCollectorThresholdCheck { + continue + } + + var params task.AreaThresholdCheckParams + if err := t.ParseParameters(¶ms); err != nil { + return fmt.Errorf("任务 %d: 解析参数失败: %w", t.ID, err) + } + + // 如果 AreaControllerID 匹配,则记录其索引以待删除 + if params.AreaControllerID == areaControllerID { + deleteIndices = append(deleteIndices, i) + } + } + + // 如果没有找到要删除的任务,则直接返回 + if len(deleteIndices) == 0 { + return nil + } + + // 3. 使用 map 和新切片的方式安全地删除多个任务 + deleteMap := make(map[int]struct{}, len(deleteIndices)) + for _, index := range deleteIndices { + deleteMap[index] = struct{}{} + } + + newTasks := make([]models.Task, 0, len(plan.Tasks)-len(deleteMap)) + for i, t := range plan.Tasks { + if _, found := deleteMap[i]; !found { + newTasks = append(newTasks, t) + } + } + plan.Tasks = newTasks + + // 4. 重新排序任务并更新计划 + plan.ReorderSteps() + _, err = s.planService.UpdatePlan(serviceCtx, plan, models.PlanTypeSystem) + return err +} diff --git a/internal/app/service/user_service.go b/internal/app/service/user_service.go index 3efabc8..b9281e9 100644 --- a/internal/app/service/user_service.go +++ b/internal/app/service/user_service.go @@ -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 { diff --git a/internal/app/webhook/chirp_stack.go b/internal/app/webhook/chirp_stack.go index 1af2d83..c3cf005 100644 --- a/internal/app/webhook/chirp_stack.go +++ b/internal/app/webhook/chirp_stack.go @@ -162,17 +162,17 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent) logger.Infof("开始处理 'up' 事件, DevEui: %s", event.DeviceInfo.DevEui) // 1. 查找区域主控设备 - regionalController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui) + areaController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui) if err != nil { logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err) return } // 依赖 SelfCheck 确保区域主控有效 - if err := regionalController.SelfCheck(); err != nil { - logger.Errorf("处理 'up' 事件失败:区域主控 %v(ID: %d) 未通过自检: %v", regionalController.Name, regionalController.ID, err) + if err := areaController.SelfCheck(); err != nil { + logger.Errorf("处理 'up' 事件失败:区域主控 %v(ID: %d) 未通过自检: %v", areaController.Name, areaController.ID, err) return } - logger.Infof("找到区域主控: %s (ID: %d)", regionalController.Name, regionalController.ID) + logger.Infof("找到区域主控: %s (ID: %d)", areaController.Name, areaController.ID) // 2. 记录区域主控的信号强度 (如果存在) if len(event.RxInfo) > 0 { @@ -187,8 +187,8 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent) } // 记录信号强度 - c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics) - logger.Infof("已记录区域主控 (ID: %d) 的信号强度: RSSI=%d, SNR=%.2f", regionalController.ID, rx.Rssi, rx.Snr) + c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics) + logger.Infof("已记录区域主控 (ID: %d) 的信号强度: RSSI=%d, SNR=%.2f", areaController.ID, rx.Rssi, rx.Snr) } else { logger.Warnf("处理 'up' 事件时未找到 RxInfo,无法记录信号数据。DevEui: %s", event.DeviceInfo.DevEui) } @@ -298,7 +298,7 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent) valueDescriptor := valueDescriptors[0] // 5.3 应用乘数和偏移量计算最终值 - parsedValue := float64(rawSensorValue)*valueDescriptor.Multiplier + valueDescriptor.Offset + parsedValue := rawSensorValue*valueDescriptor.Multiplier + valueDescriptor.Offset // 5.4 根据传感器类型构建具体的数据结构 var dataToRecord interface{} @@ -312,11 +312,11 @@ func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent) default: // TODO 未知传感器的数据需要记录吗 logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type) - dataToRecord = map[string]float64{"value": parsedValue} + dataToRecord = map[string]float32{"value": parsedValue} } // 5.5 记录传感器数据 - c.recordSensorData(reqCtx, regionalController.ID, dev.ID, event.Time, valueDescriptor.Type, dataToRecord) + c.recordSensorData(reqCtx, areaController.ID, dev.ID, event.Time, valueDescriptor.Type, dataToRecord) logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 原始值=%f, 解析值=%.2f", dev.ID, valueDescriptor.Type, rawSensorValue, parsedValue) } @@ -334,7 +334,7 @@ func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *Statu logger.Infof("处接收到理 'status' 事件: %+v", event) // 查找区域主控设备 - regionalController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui) + areaController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui) if err != nil { logger.Errorf("处理 'status' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err) return @@ -344,8 +344,8 @@ func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *Statu signalMetrics := models.SignalMetrics{ MarginDb: event.Margin, } - c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics) - logger.Infof("已记录区域主控 (ID: %d) 的信号状态: %+v", regionalController.ID, signalMetrics) + c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics) + logger.Infof("已记录区域主控 (ID: %d) 的信号状态: %+v", areaController.ID, signalMetrics) // 记录电量 batteryLevel := models.BatteryLevel{ @@ -353,8 +353,8 @@ func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *Statu BatteryLevelUnavailable: event.BatteryLevelUnavailable, ExternalPower: event.ExternalPower, } - c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeBatteryLevel, batteryLevel) - logger.Infof("已记录区域主控 (ID: %d) 的电池状态: %+v", regionalController.ID, batteryLevel) + c.recordSensorData(reqCtx, areaController.ID, areaController.ID, event.Time, models.SensorTypeBatteryLevel, batteryLevel) + logger.Infof("已记录区域主控 (ID: %d) 的电池状态: %+v", areaController.ID, batteryLevel) } // handleAckEvent 处理下行确认事件 @@ -425,11 +425,11 @@ func (c *ChirpStackListener) handleIntegrationEvent(ctx context.Context, event * } // recordSensorData 是一个通用方法,用于将传感器数据存入数据库。 -// regionalControllerID: 区域主控设备的ID +// areaControllerID: 区域主控设备的ID // sensorDeviceID: 实际产生传感器数据的普通设备的ID // sensorType: 传感器值的类型 (例如 models.SensorTypeTemperature) // data: 具体的传感器数据结构体实例 (例如 models.TemperatureData) -func (c *ChirpStackListener) recordSensorData(ctx context.Context, regionalControllerID uint, sensorDeviceID uint, eventTime time.Time, sensorType models.SensorType, data interface{}) { +func (c *ChirpStackListener) recordSensorData(ctx context.Context, areaControllerID uint32, sensorDeviceID uint32, eventTime time.Time, sensorType models.SensorType, data interface{}) { reqCtx, logger := logs.Trace(ctx, c.ctx, "recordSensorData") // 1. 将传入的结构体序列化为 JSON jsonData, err := json.Marshal(data) @@ -440,11 +440,11 @@ func (c *ChirpStackListener) recordSensorData(ctx context.Context, regionalContr // 2. 构建 SensorData 模型 sensorData := &models.SensorData{ - Time: eventTime, - DeviceID: sensorDeviceID, - RegionalControllerID: regionalControllerID, - SensorType: sensorType, - Data: datatypes.JSON(jsonData), + Time: eventTime, + DeviceID: sensorDeviceID, + AreaControllerID: areaControllerID, + SensorType: sensorType, + Data: datatypes.JSON(jsonData), } // 3. 调用仓库创建记录 diff --git a/internal/app/webhook/chirp_stack_types.go b/internal/app/webhook/chirp_stack_types.go index 55d7de5..05247d2 100644 --- a/internal/app/webhook/chirp_stack_types.go +++ b/internal/app/webhook/chirp_stack_types.go @@ -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"` // 精度 } diff --git a/internal/core/application.go b/internal/core/application.go index 7c5c3a3..2dff55d 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -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, ) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index fb94a45..73a9c05 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -7,6 +7,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/service" "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm" "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig" @@ -21,6 +22,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/transport" "git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora" "git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token" + "gorm.io/gorm" ) @@ -29,7 +31,6 @@ type Infrastructure struct { storage database.Storage repos *Repositories lora *LoraComponents - notifyService domain_notify.Service tokenGenerator token.Generator } @@ -47,18 +48,12 @@ func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructur return nil, err } - notifyService, err := initNotifyService(ctx, cfg.Notify, repos.userRepo, repos.notificationRepo) - if err != nil { - return nil, fmt.Errorf("初始化通知服务失败: %w", err) - } - tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret)) return &Infrastructure{ storage: storage, repos: repos, lora: lora, - notifyService: notifyService, tokenGenerator: tokenGenerator, }, nil } @@ -86,6 +81,7 @@ type Repositories struct { medicationLogRepo repository.MedicationLogRepository rawMaterialRepo repository.RawMaterialRepository notificationRepo repository.NotificationRepository + alarmRepo repository.AlarmRepository unitOfWork repository.UnitOfWork } @@ -114,6 +110,7 @@ func initRepositories(ctx context.Context, db *gorm.DB) *Repositories { medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), db), rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db), notificationRepo: repository.NewGormNotificationRepository(logs.AddCompName(baseCtx, "NotificationRepo"), db), + alarmRepo: repository.NewGormAlarmRepository(logs.AddCompName(baseCtx, "AlarmRepo"), db), unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db), } } @@ -129,12 +126,19 @@ type DomainServices struct { planExecutionManager plan.ExecutionManager analysisPlanTaskManager plan.AnalysisPlanTaskManager planService plan.Service + notifyService domain_notify.Service + alarmService alarm.AlarmService } // initDomainServices 初始化所有的领域服务。 -func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastructure) *DomainServices { +func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastructure) (*DomainServices, error) { baseCtx := context.Background() + notifyService, err := initNotifyService(ctx, cfg.Notify, infra.repos.userRepo, infra.repos.notificationRepo) + if err != nil { + return nil, fmt.Errorf("初始化通知服务失败: %w", err) + } + // 猪群管理相关 pigPenTransferManager := pig.NewPigPenTransferManager(logs.AddCompName(baseCtx, "PigPenTransferManager"), infra.repos.pigPenRepo, infra.repos.pigTransferLogRepo, infra.repos.pigBatchRepo) pigTradeManager := pig.NewPigTradeManager(logs.AddCompName(baseCtx, "PigTradeManager"), infra.repos.pigTradeRepo) @@ -157,8 +161,22 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr infra.lora.comm, ) + // 告警服务 + alarmService := alarm.NewAlarmService( + logs.AddCompName(baseCtx, "AlarmService"), + infra.repos.alarmRepo, + infra.repos.unitOfWork, + ) + // 任务工厂 - taskFactory := task.NewTaskFactory(logs.AddCompName(baseCtx, "TaskFactory"), infra.repos.sensorDataRepo, infra.repos.deviceRepo, generalDeviceService) + taskFactory := task.NewTaskFactory(logs.AddCompName(baseCtx, "TaskFactory"), + infra.repos.sensorDataRepo, + infra.repos.deviceRepo, + infra.repos.alarmRepo, + generalDeviceService, + notifyService, + alarmService, + ) // 计划任务管理器 analysisPlanTaskManager := plan.NewAnalysisPlanTaskManager(logs.AddCompName(baseCtx, "AnalysisPlanTaskManager"), infra.repos.planRepo, infra.repos.pendingTaskRepo, infra.repos.executionLogRepo) @@ -199,18 +217,21 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr taskFactory: taskFactory, planExecutionManager: planExecutionManager, planService: planService, - } + notifyService: notifyService, + alarmService: alarmService, + }, nil } // AppServices 聚合了所有的应用服务实例。 type AppServices struct { - pigFarmService service.PigFarmService - pigBatchService service.PigBatchService - monitorService service.MonitorService - deviceService service.DeviceService - planService service.PlanService - userService service.UserService - auditService service.AuditService + pigFarmService service.PigFarmService + pigBatchService service.PigBatchService + monitorService service.MonitorService + deviceService service.DeviceService + planService service.PlanService + userService service.UserService + auditService service.AuditService + thresholdAlarmService service.ThresholdAlarmService } // initAppServices 初始化所有的应用服务。 @@ -235,25 +256,40 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices infra.repos.pigTradeRepo, infra.repos.notificationRepo, ) + + // 初始化阈值告警服务 + thresholdAlarmService := service.NewThresholdAlarmService( + logs.AddCompName(baseCtx, "ThresholdAlarmService"), + domainServices.alarmService, + domainServices.planService, + infra.repos.alarmRepo, + infra.repos.planRepo, + infra.repos.areaControllerRepo, + infra.repos.deviceRepo, + ) + deviceService := service.NewDeviceService( logs.AddCompName(baseCtx, "DeviceService"), infra.repos.deviceRepo, infra.repos.areaControllerRepo, infra.repos.deviceTemplateRepo, domainServices.generalDeviceService, + thresholdAlarmService, ) + auditService := service.NewAuditService(logs.AddCompName(baseCtx, "AuditService"), infra.repos.userActionLogRepo) planService := service.NewPlanService(logs.AddCompName(baseCtx, "AppPlanService"), domainServices.planService) - userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, infra.notifyService) + userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, domainServices.notifyService) return &AppServices{ - pigFarmService: pigFarmService, - pigBatchService: pigBatchService, - monitorService: monitorService, - deviceService: deviceService, - auditService: auditService, - planService: planService, - userService: userService, + pigFarmService: pigFarmService, + pigBatchService: pigBatchService, + monitorService: monitorService, + deviceService: deviceService, + auditService: auditService, + planService: planService, + userService: userService, + thresholdAlarmService: thresholdAlarmService, } } @@ -353,7 +389,7 @@ func initNotifyService( // 3. 动态确定首选通知器 var primaryNotifier notify.Notifier - primaryNotifierType := notify.NotifierType(cfg.Primary) + primaryNotifierType := models.NotifierType(cfg.Primary) // 检查用户指定的主渠道是否已启用 for _, n := range availableNotifiers { diff --git a/internal/core/data_initializer.go b/internal/core/data_initializer.go index 5a4e93d..cfe4894 100644 --- a/internal/core/data_initializer.go +++ b/internal/core/data_initializer.go @@ -4,16 +4,12 @@ import ( "context" "fmt" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" ) -const ( - // PlanNameTimedFullDataCollection 是定时全量数据采集计划的名称 - PlanNameTimedFullDataCollection = "定时全量数据采集" -) - // initializeState 在应用启动时准备其初始数据状态。 // 它遵循一个严格的顺序:清理 -> 更新 -> 刷新,以确保数据的一致性和正确性。 func (app *Application) initializeState(ctx context.Context) error { @@ -48,13 +44,11 @@ func (app *Application) initializeState(ctx context.Context) error { } // initializeSystemPlans 确保预定义的系统计划在数据库中存在并保持最新。 +// 它通过调用各个独立的计划初始化方法来完成此操作。 func (app *Application) initializeSystemPlans(ctx context.Context) error { appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializeSystemPlans") logger.Info("开始检查并更新预定义的系统计划...") - // 动态构建预定义计划列表 - predefinedSystemPlans := app.getPredefinedSystemPlans() - // 1. 获取所有已存在的系统计划 existingPlans, _, err := app.Infra.repos.planRepo.ListPlans(appCtx, repository.ListPlansOptions{ PlanType: repository.PlanTypeFilterSystem, @@ -64,54 +58,28 @@ func (app *Application) initializeSystemPlans(ctx context.Context) error { } // 2. 为了方便查找, 将现有计划名放入一个 map - existingPlanMap := make(map[string]*models.Plan) + existingPlanMap := make(map[models.PlanName]*models.Plan) for i := range existingPlans { existingPlanMap[existingPlans[i].Name] = &existingPlans[i] } - // 3. 遍历预定义的计划列表 - for i := range predefinedSystemPlans { - predefinedPlan := &predefinedSystemPlans[i] // 获取可修改的指针 + // 3. 调用独立的初始化方法来处理每个系统计划 + if err := app.initializePeriodicSystemHealthCheckPlan(appCtx, existingPlanMap); err != nil { + return err // 如果任何一个计划初始化失败,则立即返回错误 + } - if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok { - // 如果计划存在,则进行无差别更新 - logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name) - - // 将数据库中已存在的计划的ID和运行时状态字段赋值给预定义计划 - predefinedPlan.ID = foundExistingPlan.ID - predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount - - // 1. 使用 UpdatePlanMetadataAndStructure 来更新计划的元数据和关联任务 - // 这会处理 Name, Description, ExecutionType, ExecuteNum, CronExpression, ContentType - // 并且最重要的是,它会正确处理 Tasks 的增删改,确保任务列表与 predefinedPlan.Tasks 完全同步 - if err := app.Infra.repos.planRepo.UpdatePlanMetadataAndStructure(appCtx, predefinedPlan); err != nil { - return fmt.Errorf("更新预定义计划 '%s' 的元数据和结构失败: %w", predefinedPlan.Name, err) - } - - // 2. 接着使用 UpdatePlan 来更新所有顶层字段,包括 PlanType 和 Status - // 由于任务已经在上一步正确同步,此步不会导致任务冗余 - if err := app.Infra.repos.planRepo.UpdatePlan(appCtx, predefinedPlan); err != nil { - return fmt.Errorf("更新预定义计划 '%s' 的所有顶层字段失败: %w", predefinedPlan.Name, err) - } - - logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name) - } else { - // 如果计划不存在, 则创建 - logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name) - if err := app.Infra.repos.planRepo.CreatePlan(appCtx, predefinedPlan); err != nil { - return fmt.Errorf("创建预定义计划 '%s' 失败: %w", predefinedPlan.Name, err) - } else { - logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name) - } - } + if err := app.initializeAlarmNotificationPlan(appCtx, existingPlanMap); err != nil { + return err } logger.Info("预定义系统计划检查完成。") return nil } -// getPredefinedSystemPlans 返回一个基于当前配置的预定义系统计划列表。 -func (app *Application) getPredefinedSystemPlans() []models.Plan { +// initializePeriodicSystemHealthCheckPlan 负责初始化 "周期性系统健康检查" 计划。 +// 它会根据当前配置动态构建计划,并决定是创建新计划还是更新现有计划。 +func (app *Application) initializePeriodicSystemHealthCheckPlan(ctx context.Context, existingPlanMap map[models.PlanName]*models.Plan) error { + appCtx, logger := logs.Trace(ctx, app.Ctx, "initializePeriodicSystemHealthCheckPlan") // 根据配置创建定时全量采集计划 interval := app.Config.Collection.Interval @@ -119,25 +87,141 @@ func (app *Application) getPredefinedSystemPlans() []models.Plan { interval = 1 // 确保间隔至少为1分钟 } cronExpression := fmt.Sprintf("*/%d * * * *", interval) - timedCollectionPlan := models.Plan{ - Name: PlanNameTimedFullDataCollection, - Description: fmt.Sprintf("这是一个系统预定义的计划, 每 %d 分钟自动触发一次全量数据采集。", app.Config.Collection.Interval), + + // 定义预设的全量采集任务 + fullCollectionTask := models.Task{ + Name: "全量采集", + Description: "触发一次全量数据采集", + ExecutionOrder: 1, + Type: models.TaskTypeFullCollection, + } + + // 定义预设的延时任务 + delayParams := task.DelayTaskParams{DelayDuration: 10} // 延时10秒 + delayTask := models.Task{ + Name: "延时任务", + Description: "系统预设延时任务,用于错峰处理", + ExecutionOrder: 2, + Type: models.TaskTypeWaiting, + } + err := delayTask.SaveParameters(delayParams) + if err != nil { + return fmt.Errorf("序列化延时任务参数失败: %w", err) + } + + // 构建新的任务列表 + var newTasks []models.Task + newTasks = append(newTasks, fullCollectionTask, delayTask) + + // 如果计划已存在,则获取其现有任务并追加到新任务列表后(排除预设任务) + if foundExistingPlan, ok := existingPlanMap[models.PlanNamePeriodicSystemHealthCheck]; ok { + for _, existingTask := range foundExistingPlan.Tasks { + // 排除已预设的全量采集和延时任务 + if existingTask.Type != models.TaskTypeFullCollection && existingTask.Type != models.TaskTypeWaiting { + newTasks = append(newTasks, existingTask) + } + } + } + + // 重新设置所有任务的 ExecutionOrder + for i := range newTasks { + newTasks[i].ExecutionOrder = i + 1 + } + + predefinedPlan := &models.Plan{ + Name: models.PlanNamePeriodicSystemHealthCheck, + Description: fmt.Sprintf("这是一个系统预定义的计划, 每 %d 分钟自动触发一次全量数据采集, 并进行阈值校验告警。", app.Config.Collection.Interval), PlanType: models.PlanTypeSystem, ExecutionType: models.PlanExecutionTypeAutomatic, CronExpression: cronExpression, Status: models.PlanStatusEnabled, ContentType: models.PlanContentTypeTasks, + Tasks: newTasks, + } + + if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok { + // 如果计划存在,则进行无差别更新 + logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name) + + // 将数据库中已存在的计划的ID和运行时状态字段赋值给预定义计划 + predefinedPlan.ID = foundExistingPlan.ID + predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount + + // 1. 使用 UpdatePlanMetadataAndStructure 来更新计划的元数据和关联任务 + // 这会处理 Name, Description, ExecutionType, ExecuteNum, CronExpression, ContentType + // 并且最重要的是,它会正确处理 Tasks 的增删改,确保任务列表与 predefinedPlan.Tasks 完全同步 + if err := app.Infra.repos.planRepo.UpdatePlanMetadataAndStructure(appCtx, predefinedPlan); err != nil { + return fmt.Errorf("更新预定义计划 '%s' 的元数据和结构失败: %w", predefinedPlan.Name, err) + } + + // 2. 接着使用 UpdatePlan 来更新所有顶层字段,包括 PlanType 和 Status + // 由于任务已经在上一步正确同步,此步不会导致任务冗余 + if err := app.Infra.repos.planRepo.UpdatePlan(appCtx, predefinedPlan); err != nil { + return fmt.Errorf("更新预定义计划 '%s' 的所有顶层字段失败: %w", predefinedPlan.Name, err) + } + + logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name) + } else { + // 如果计划不存在, 则创建 + logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name) + if err := app.Infra.repos.planRepo.CreatePlan(appCtx, predefinedPlan); err != nil { + return fmt.Errorf("创建预定义计划 '%s' 失败: %w", predefinedPlan.Name, err) + } else { + logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name) + } + } + return nil +} + +// initializeAlarmNotificationPlan 负责初始化 "告警通知发送" 计划。 +// 它确保系统中存在一个每分钟执行的、用于发送告警通知的预定义计划。 +func (app *Application) initializeAlarmNotificationPlan(ctx context.Context, existingPlanMap map[models.PlanName]*models.Plan) error { + appCtx, logger := logs.Trace(ctx, app.Ctx, "initializeAlarmNotificationPlan") + + predefinedPlan := &models.Plan{ + Name: models.PlanNameAlarmNotification, + Description: "这是一个系统预定义的计划, 每分钟自动触发一次告警通知发送。", + PlanType: models.PlanTypeSystem, + ExecutionType: models.PlanExecutionTypeAutomatic, + CronExpression: "*/1 * * * *", // 每分钟执行一次 + Status: models.PlanStatusEnabled, + ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{ { - Name: "全量采集", - Description: "触发一次全量数据采集", + Name: "告警通知发送", + Description: "发送所有待处理的告警通知", ExecutionOrder: 1, - Type: models.TaskTypeFullCollection, + Type: models.TaskTypeAlarmNotification, }, }, } - return []models.Plan{timedCollectionPlan} + if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok { + // 如果计划存在,则进行无差别更新 + logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name) + + predefinedPlan.ID = foundExistingPlan.ID + predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount + + if err := app.Infra.repos.planRepo.UpdatePlanMetadataAndStructure(appCtx, predefinedPlan); err != nil { + return fmt.Errorf("更新预定义计划 '%s' 的元数据和结构失败: %w", predefinedPlan.Name, err) + } + + if err := app.Infra.repos.planRepo.UpdatePlan(appCtx, predefinedPlan); err != nil { + return fmt.Errorf("更新预定义计划 '%s' 的所有顶层字段失败: %w", predefinedPlan.Name, err) + } + + logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name) + } else { + // 如果计划不存在, 则创建 + logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name) + if err := app.Infra.repos.planRepo.CreatePlan(appCtx, predefinedPlan); err != nil { + return fmt.Errorf("创建预定义计划 '%s' 失败: %w", predefinedPlan.Name, err) + } else { + logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name) + } + } + return nil } // initializePendingCollections 在应用启动时处理所有未完成的采集请求。 @@ -209,7 +293,7 @@ func (app *Application) cleanupStaleTasksAndLogs(ctx context.Context) error { } // 2. 收集所有受影响的唯一 PlanID - affectedPlanIDs := make(map[uint]struct{}) + affectedPlanIDs := make(map[uint32]struct{}) for _, log := range incompletePlanLogs { affectedPlanIDs[log.PlanID] = struct{}{} } diff --git a/internal/domain/alarm/alarm_service.go b/internal/domain/alarm/alarm_service.go new file mode 100644 index 0000000..a542935 --- /dev/null +++ b/internal/domain/alarm/alarm_service.go @@ -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 +} diff --git a/internal/domain/device/device_service.go b/internal/domain/device/device_service.go index 9dd6a1b..db445c0 100644 --- a/internal/domain/device/device_service.go +++ b/internal/domain/device/device_service.go @@ -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 } // 设备操作指令通用结构(最外层) diff --git a/internal/domain/device/general_device_service.go b/internal/domain/device/general_device_service.go index cfdc9d5..4364735 100644 --- a/internal/domain/device/general_device_service.go +++ b/internal/domain/device/general_device_service.go @@ -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{ diff --git a/internal/domain/notify/notify.go b/internal/domain/notify/notify.go index 88f9647..0c95842 100644 --- a/internal/domain/notify/notify.go +++ b/internal/domain/notify/notify.go @@ -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, diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go index 6b522a5..266db08 100644 --- a/internal/domain/pig/pen_transfer_manager.go +++ b/internal/domain/pig/pen_transfer_manager.go @@ -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) diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index a3d5ea2..d4deb40 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -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 接口的具体实现。 diff --git a/internal/domain/pig/pig_batch_service_method.go b/internal/domain/pig/pig_batch_service_method.go index 9f070a2..289e3c1 100644 --- a/internal/domain/pig/pig_batch_service_method.go +++ b/internal/domain/pig/pig_batch_service_method.go @@ -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 { diff --git a/internal/domain/pig/pig_batch_service_pen_transfer.go b/internal/domain/pig/pig_batch_service_pen_transfer.go index e41f06c..292955e 100644 --- a/internal/domain/pig/pig_batch_service_pen_transfer.go +++ b/internal/domain/pig/pig_batch_service_pen_transfer.go @@ -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 { diff --git a/internal/domain/pig/pig_batch_service_pig_sick.go b/internal/domain/pig/pig_batch_service_pig_sick.go index 2fca6f4..db1a3bf 100644 --- a/internal/domain/pig/pig_batch_service_pig_sick.go +++ b/internal/domain/pig/pig_batch_service_pig_sick.go @@ -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") diff --git a/internal/domain/pig/pig_batch_service_pig_trade.go b/internal/domain/pig/pig_batch_service_pig_trade.go index 9335795..0e60283 100644 --- a/internal/domain/pig/pig_batch_service_pig_trade.go +++ b/internal/domain/pig/pig_batch_service_pig_trade.go @@ -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 { diff --git a/internal/domain/pig/pig_sick_manager.go b/internal/domain/pig/pig_sick_manager.go index fd93b29..be48017 100644 --- a/internal/domain/pig/pig_sick_manager.go +++ b/internal/domain/pig/pig_sick_manager.go @@ -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 { diff --git a/internal/domain/plan/analysis_plan_task_manager.go b/internal/domain/plan/analysis_plan_task_manager.go index 05fb741..add3f20 100644 --- a/internal/domain/plan/analysis_plan_task_manager.go +++ b/internal/domain/plan/analysis_plan_task_manager.go @@ -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 diff --git a/internal/domain/plan/plan_execution_manager.go b/internal/domain/plan/plan_execution_manager.go index fc76e4c..8bc9566 100644 --- a/internal/domain/plan/plan_execution_manager.go +++ b/internal/domain/plan/plan_execution_manager.go @@ -25,21 +25,21 @@ type ExecutionManager interface { // ProgressTracker 仅用于在内存中提供计划执行的并发锁 type ProgressTracker struct { mu sync.Mutex - cond *sync.Cond // 用于实现阻塞锁 - runningPlans map[uint]bool // key: planExecutionLogID, value: true (用作内存锁) + cond *sync.Cond // 用于实现阻塞锁 + runningPlans map[uint32]bool // key: planExecutionLogID, value: true (用作内存锁) } // NewProgressTracker 创建一个新的进度跟踪器 func NewProgressTracker() *ProgressTracker { t := &ProgressTracker{ - runningPlans: make(map[uint]bool), + runningPlans: make(map[uint32]bool), } t.cond = sync.NewCond(&t.mu) return t } // TryLock (非阻塞) 尝试锁定一个计划。如果计划未被锁定,则锁定并返回 true。 -func (t *ProgressTracker) TryLock(planLogID uint) bool { +func (t *ProgressTracker) TryLock(planLogID uint32) bool { t.mu.Lock() defer t.mu.Unlock() if t.runningPlans[planLogID] { @@ -50,7 +50,7 @@ func (t *ProgressTracker) TryLock(planLogID uint) bool { } // Lock (阻塞) 获取一个计划的执行锁。如果锁已被占用,则会一直等待直到锁被释放。 -func (t *ProgressTracker) Lock(planLogID uint) { +func (t *ProgressTracker) Lock(planLogID uint32) { t.mu.Lock() // 当计划正在运行时,调用 t.cond.Wait() 会原子地解锁 mu 并挂起当前协程。 // 当被唤醒时,它会重新锁定 mu 并再次检查循环条件。 @@ -63,7 +63,7 @@ func (t *ProgressTracker) Lock(planLogID uint) { } // Unlock 解锁一个计划,并唤醒所有正在等待此锁的协程。 -func (t *ProgressTracker) Unlock(planLogID uint) { +func (t *ProgressTracker) Unlock(planLogID uint32) { t.mu.Lock() defer t.mu.Unlock() delete(t.runningPlans, planLogID) @@ -72,10 +72,10 @@ func (t *ProgressTracker) Unlock(planLogID uint) { } // GetRunningPlanIDs 获取当前所有正在执行的计划ID列表 -func (t *ProgressTracker) GetRunningPlanIDs() []uint { +func (t *ProgressTracker) GetRunningPlanIDs() []uint32 { t.mu.Lock() defer t.mu.Unlock() - ids := make([]uint, 0, len(t.runningPlans)) + ids := make([]uint32, 0, len(t.runningPlans)) for id := range t.runningPlans { ids = append(ids, id) } @@ -214,7 +214,7 @@ func (s *planExecutionManagerImpl) claimAndSubmit(ctx context.Context) { } // handleRequeue 同步地、安全地将一个无法立即执行的任务放回队列。 -func (s *planExecutionManagerImpl) handleRequeue(ctx context.Context, planExecutionLogID uint, taskToRequeue *models.PendingTask) { +func (s *planExecutionManagerImpl) handleRequeue(ctx context.Context, planExecutionLogID uint32, taskToRequeue *models.PendingTask) { managerCtx, logger := logs.Trace(ctx, s.ctx, "handleRequeue") logger.Warnf("计划 %d 正在执行,任务 %d (TaskID: %d) 将等待并重新入队...", planExecutionLogID, taskToRequeue.ID, taskToRequeue.TaskID) @@ -308,7 +308,7 @@ func (s *planExecutionManagerImpl) analysisPlan(ctx context.Context, claimedLog // 创建Plan执行记录 // 从任务的 Parameters 中解析出真实的 PlanID var params struct { - PlanID uint `json:"plan_id"` + PlanID uint32 `json:"plan_id"` } if err := claimedLog.Task.ParseParameters(¶ms); err != nil { logger.Errorf("解析任务参数中的计划ID失败,日志ID: %d, 错误: %v", claimedLog.ID, err) @@ -390,7 +390,7 @@ func (s *planExecutionManagerImpl) updateTaskExecutionLogStatus(ctx context.Cont } // handlePlanTermination 集中处理计划的终止逻辑(失败或取消) -func (s *planExecutionManagerImpl) handlePlanTermination(ctx context.Context, planLogID uint, reason string) { +func (s *planExecutionManagerImpl) handlePlanTermination(ctx context.Context, planLogID uint32, reason string) { managerCtx, logger := logs.Trace(ctx, s.ctx, "handlePlanTermination") // 1. 从待执行队列中删除所有相关的子任务 if err := s.pendingTaskRepo.DeletePendingTasksByPlanLogID(managerCtx, planLogID); err != nil { @@ -434,7 +434,7 @@ func (s *planExecutionManagerImpl) handlePlanTermination(ctx context.Context, pl } // handlePlanCompletion 集中处理计划成功完成后的所有逻辑 -func (s *planExecutionManagerImpl) handlePlanCompletion(ctx context.Context, planLogID uint) { +func (s *planExecutionManagerImpl) handlePlanCompletion(ctx context.Context, planLogID uint32) { managerCtx, logger := logs.Trace(ctx, s.ctx, "handlePlanCompletion") logger.Infof("计划执行 %d 的所有任务已完成,开始处理计划完成逻辑...", planLogID) diff --git a/internal/domain/plan/plan_service.go b/internal/domain/plan/plan_service.go index f83c655..48c5cf7 100644 --- a/internal/domain/plan/plan_service.go +++ b/internal/domain/plan/plan_service.go @@ -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 = "领域层:停止计划" diff --git a/internal/domain/plan/task.go b/internal/domain/plan/task.go index ba16106..9b010f4 100644 --- a/internal/domain/plan/task.go +++ b/internal/domain/plan/task.go @@ -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 是一个工厂接口,用于根据任务执行日志创建任务实例。 diff --git a/internal/domain/task/alarm_notification_task.go b/internal/domain/task/alarm_notification_task.go new file mode 100644 index 0000000..df7642f --- /dev/null +++ b/internal/domain/task/alarm_notification_task.go @@ -0,0 +1,138 @@ +package task + +import ( + "context" + "fmt" + "sync" + "time" + + notify_domain "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/notify" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" +) + +// AlarmNotificationTaskParams 定义了 AlarmNotificationTask 的参数结构 +// 如果用户没有指定某个等级的配置, 则默认为该等级消息只发送一次 +type AlarmNotificationTaskParams struct { + // NotificationIntervals 告警通知的发送间隔时间,键为告警等级,值为时间间隔(分钟) + NotificationIntervals map[models.SeverityLevel]uint32 `json:"notification_intervals"` +} + +// AlarmNotificationTask 告警通知发送任务 +type AlarmNotificationTask struct { + ctx context.Context + taskLog *models.TaskExecutionLog + + // alarmNotificationTaskParams 是任务配置 + alarmNotificationTaskParams AlarmNotificationTaskParams + + onceParse sync.Once // 保证解析参数只执行一次 + + notificationService notify_domain.Service + alarmRepository repository.AlarmRepository +} + +// NewAlarmNotificationTask 创建一个新的告警通知发送任务实例 +func NewAlarmNotificationTask(ctx context.Context, taskLog *models.TaskExecutionLog, service notify_domain.Service, alarmRepository repository.AlarmRepository) plan.Task { + return &AlarmNotificationTask{ + ctx: ctx, + taskLog: taskLog, + alarmRepository: alarmRepository, + notificationService: service, + } +} + +// Execute 执行告警通知发送任务 +func (t *AlarmNotificationTask) Execute(ctx context.Context) error { + taskCtx, logger := logs.Trace(ctx, t.ctx, "Execute") + logger.Infof("开始执行告警通知发送任务, 任务ID: %d", t.taskLog.TaskID) + + if err := t.parseParameters(taskCtx); err != nil { + return err + } + + // 获取是否有待发送告警通知, 用于优化性能 + alarmsCount, err := t.alarmRepository.CountAlarmsForNotification(taskCtx, t.alarmNotificationTaskParams.NotificationIntervals) + if err != nil { + logger.Errorf("任务 %v: 获取告警数量失败: %v", t.taskLog.TaskID, err) + return err + } + if alarmsCount == 0 { + logger.Debugf("没有待发送的告警通知, 跳过任务, 任务ID: %d", t.taskLog.TaskID) + return nil + } + + // 获取所有待发送的告警通知 + alarms, err := t.alarmRepository.ListAlarmsForNotification(taskCtx, t.alarmNotificationTaskParams.NotificationIntervals) + if err != nil { + logger.Errorf("任务 %v: 获取告警列表失败: %v", t.taskLog.TaskID, err) + return err + } + + // 发送通知 + for _, alarm := range alarms { + // TODO 因为还没做权限管理, 所以暂时通过广播形式发给所有用户 + err = t.notificationService.BroadcastAlarm(taskCtx, notify.AlarmContent{ + Title: alarm.AlarmSummary, + Message: alarm.AlarmDetails, + Level: alarm.Level, + Timestamp: time.Now(), + }) + + if err != nil { + // 非致命错误 + logger.Errorf("任务 %v: 发送告警通知失败: %v", t.taskLog.TaskID, err) + continue + } + + // 能发送通知的告警要么是忽略期已过且到达触发时间, 要么是不忽略且到达触发时间, 二者都应该取消忽略并刷新最后一次发送时间 + err = t.alarmRepository.UpdateAlarmNotificationStatus(taskCtx, alarm.ID, time.Now(), false, nil) + if err != nil { + // 非致命错误, 没有必要因为更新失败影响后续消息发送 + logger.Errorf("任务 %v: 更新告警通知状态失败: %v", t.taskLog.TaskID, err) + } + } + + logger.Infof("告警通知发送任务执行完成, 任务ID: %d", t.taskLog.TaskID) + return nil +} + +// OnFailure 告警通知发送任务失败时的处理逻辑 +func (t *AlarmNotificationTask) OnFailure(ctx context.Context, executeErr error) { + logger := logs.TraceLogger(ctx, t.ctx, "OnFailure") + logger.Errorf("告警通知发送任务执行失败, 任务ID: %d, 错误: %v", t.taskLog.TaskID, executeErr) +} + +// ResolveDeviceIDs 从任务配置中解析并返回所有关联的设备ID列表 +func (t *AlarmNotificationTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) { + // 告警通知任务与设备无关 + return []uint32{}, nil +} + +// parseParameters 解析任务参数 +func (t *AlarmNotificationTask) parseParameters(ctx context.Context) error { + logger := logs.TraceLogger(ctx, t.ctx, "parseParameters") + var err error + t.onceParse.Do(func() { + if t.taskLog.Task.Parameters == nil { + logger.Errorf("任务 %v: 缺少参数", t.taskLog.TaskID) + err = fmt.Errorf("任务 %v: 参数不全", t.taskLog.TaskID) + return + } + + var params AlarmNotificationTaskParams + err = t.taskLog.Task.ParseParameters(¶ms) + if err != nil { + logger.Errorf("任务 %v: 解析参数失败: %v", t.taskLog.TaskID, err) + err = fmt.Errorf("任务 %v: 解析参数失败: %v", t.taskLog.TaskID, err) + return + } + + t.alarmNotificationTaskParams = params + + }) + return err +} diff --git a/internal/domain/task/area_threshold_check_task.go b/internal/domain/task/area_threshold_check_task.go new file mode 100644 index 0000000..2545796 --- /dev/null +++ b/internal/domain/task/area_threshold_check_task.go @@ -0,0 +1,166 @@ +package task + +import ( + "context" + "fmt" + "sync" + + "git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" +) + +// AreaThresholdCheckParams 定义了区域阈值检查任务的参数 +type AreaThresholdCheckParams struct { + AreaControllerID uint32 `json:"area_controller_id"` // 区域主控ID + SensorType models.SensorType `json:"sensor_type"` // 传感器类型 + Thresholds float32 `json:"thresholds"` // 阈值 + Operator models.Operator `json:"operator"` // 操作符 + Level models.SeverityLevel `json:"level"` // 告警级别 + ExcludeDeviceIDs []uint32 `json:"exclude_device_ids"` // 排除的传感器ID +} + +// AreaThresholdCheckTask 是一个任务,用于检查区域阈值并触发告警, 区域主控下的所有没有独立校验任务的设备都会受到此任务的检查 +type AreaThresholdCheckTask struct { + ctx context.Context + onceParse sync.Once + + taskLog *models.TaskExecutionLog + params AreaThresholdCheckParams + + sensorDataRepo repository.SensorDataRepository + deviceRepo repository.DeviceRepository + alarmService alarm.AlarmService +} + +func NewAreaThresholdCheckTask(ctx context.Context, taskLog *models.TaskExecutionLog, sensorDataRepo repository.SensorDataRepository, deviceRepo repository.DeviceRepository, alarmService alarm.AlarmService) plan.Task { + return &AreaThresholdCheckTask{ + ctx: ctx, + taskLog: taskLog, + sensorDataRepo: sensorDataRepo, + deviceRepo: deviceRepo, + alarmService: alarmService, + } +} + +// Execute 执行区域阈值检查任务 +func (a *AreaThresholdCheckTask) Execute(ctx context.Context) error { + taskCtx, logger := logs.Trace(ctx, a.ctx, "Execute") + err := a.parseParameters(taskCtx) + if err != nil { + logger.Errorf("任务 %v: 解析参数失败: %v", a.taskLog.TaskID, err) + return err + } + + // 1. 查询区域主控下所有设备 + devices, err := a.deviceRepo.ListByAreaControllerID(taskCtx, a.params.AreaControllerID) + if err != nil { + logger.Errorf("任务 %v: 查询区域主控 %d 下设备失败: %v", a.taskLog.TaskID, a.params.AreaControllerID, err) + return fmt.Errorf("查询区域主控 %d 下设备失败: %w", a.params.AreaControllerID, err) + } + + // 构建忽略设备ID的map,方便快速查找 + ignoredMap := make(map[uint32]struct{}) + for _, id := range a.params.ExcludeDeviceIDs { + ignoredMap[id] = struct{}{} + } + + // 2. 遍历设备,排除忽略列表里的设备,并执行阈值检查 + for _, device := range devices { + if _, ignored := ignoredMap[device.ID]; ignored { + logger.Debugf("任务 %v: 设备 %d 在忽略列表中,跳过检查。", a.taskLog.TaskID, device.ID) + continue + } + + task := a.taskLog.Task + err = task.SaveParameters(DeviceThresholdCheckParams{ + DeviceID: device.ID, + SensorType: a.params.SensorType, + Thresholds: a.params.Thresholds, + Level: a.params.Level, + Operator: a.params.Operator, + }) + if err != nil { + logger.Errorf("任务 %v: 保存参数失败: %v", a.taskLog.TaskID, err) + continue + } + // 创建一个临时的 DeviceThresholdCheckTask 实例来复用其核心逻辑 + deviceCheckTask := NewDeviceThresholdCheckTask( + taskCtx, + &models.TaskExecutionLog{ // 为每个设备创建一个模拟的 TaskExecutionLog + TaskID: a.taskLog.TaskID, + Task: task, + }, + a.sensorDataRepo, + a.alarmService, + ).(*DeviceThresholdCheckTask) // 类型断言,以便访问内部参数 + + // 执行单设备的阈值检查 + if err := deviceCheckTask.Execute(taskCtx); err != nil { + logger.Errorf("任务 %v: 设备 %d 阈值检查失败: %v", a.taskLog.TaskID, device.ID, err) + continue + } + } + + return nil +} + +func (a *AreaThresholdCheckTask) OnFailure(ctx context.Context, executeErr error) { + logger := logs.TraceLogger(ctx, a.ctx, "OnFailure") + logger.Errorf("区域阈值检测任务执行失败, 任务ID: %v: 执行失败: %v", a.taskLog.TaskID, executeErr) +} + +func (a *AreaThresholdCheckTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) { + taskCtx := logs.AddFuncName(ctx, a.ctx, "ResolveDeviceIDs") + if err := a.parseParameters(taskCtx); err != nil { + return nil, err + } + // 排除列表也意味着关联 + return a.params.ExcludeDeviceIDs, nil +} + +// parseParameters 解析任务参数 +func (a *AreaThresholdCheckTask) parseParameters(ctx context.Context) error { + logger := logs.TraceLogger(ctx, a.ctx, "parseParameters") + var err error + a.onceParse.Do(func() { + if a.taskLog.Task.Parameters == nil { + logger.Errorf("任务 %v: 缺少参数", a.taskLog.TaskID) + err = fmt.Errorf("任务 %v: 参数不全", a.taskLog.TaskID) + return + } + + var params AreaThresholdCheckParams + err = a.taskLog.Task.ParseParameters(¶ms) + if err != nil { + logger.Errorf("任务 %v: 解析参数失败: %v", a.taskLog.TaskID, err) + err = fmt.Errorf("任务 %v: 解析参数失败: %v", a.taskLog.TaskID, err) + return + } + + if params.SensorType == "" { + err = fmt.Errorf("任务 %v: 未配置传感器类型", a.taskLog.TaskID) + } + if params.Operator == "" { + err = fmt.Errorf("任务 %v: 缺少操作符", a.taskLog.TaskID) + } + if params.Thresholds == 0 { + err = fmt.Errorf("任务 %v: 未配置阈值", a.taskLog.TaskID) + } + if params.AreaControllerID == 0 { + err = fmt.Errorf("任务 %v: 未配置区域主控ID", a.taskLog.TaskID) + } + if params.Level == "" { + params.Level = models.WarnLevel + } + if params.ExcludeDeviceIDs == nil { + params.ExcludeDeviceIDs = []uint32{} + } + + a.params = params + + }) + return err +} diff --git a/internal/domain/task/delay_task.go b/internal/domain/task/delay_task.go index f801ce4..24bcd9a 100644 --- a/internal/domain/task/delay_task.go +++ b/internal/domain/task/delay_task.go @@ -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 } diff --git a/internal/domain/task/device_threshold_check_task.go b/internal/domain/task/device_threshold_check_task.go new file mode 100644 index 0000000..d351650 --- /dev/null +++ b/internal/domain/task/device_threshold_check_task.go @@ -0,0 +1,197 @@ +package task + +import ( + "context" + "fmt" + "sync" + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" +) + +type DeviceThresholdCheckParams struct { + DeviceID uint32 `json:"device_id"` // 设备ID + SensorType models.SensorType `json:"sensor_type"` // 传感器类型 + Thresholds float32 `json:"thresholds"` // 阈值 + Operator models.Operator `json:"operator"` // 操作符 + Level models.SeverityLevel `json:"level"` // 告警等级 +} + +// DeviceThresholdCheckTask 是一个任务,用于检查设备传感器数据是否满足阈值条件。 +type DeviceThresholdCheckTask struct { + ctx context.Context + onceParse sync.Once + + taskLog *models.TaskExecutionLog + params DeviceThresholdCheckParams + + sensorDataRepo repository.SensorDataRepository + alarmService alarm.AlarmService +} + +func NewDeviceThresholdCheckTask(ctx context.Context, taskLog *models.TaskExecutionLog, sensorDataRepo repository.SensorDataRepository, alarmService alarm.AlarmService) plan.Task { + return &DeviceThresholdCheckTask{ + ctx: ctx, + taskLog: taskLog, + sensorDataRepo: sensorDataRepo, + alarmService: alarmService, + } +} + +func (d *DeviceThresholdCheckTask) Execute(ctx context.Context) error { + taskCtx, logger := logs.Trace(ctx, d.ctx, "Execute") + err := d.parseParameters(taskCtx) + if err != nil { + return err + } + + sensorData, err := d.sensorDataRepo.GetLatestSensorDataByDeviceIDAndSensorType(taskCtx, d.params.DeviceID, d.params.SensorType) + if err != nil { + logger.Errorf("任务 %v: 获取最新传感器数据失败: %v", d.taskLog.TaskID, err) + return fmt.Errorf("任务 %v: 获取最新传感器数据失败: %v", d.taskLog.TaskID, err) + } + + var currentValue float32 + var alarmCode models.AlarmCode + + switch d.params.SensorType { + case models.SensorTypeTemperature: + var data models.TemperatureData + if err := sensorData.ParseData(&data); err != nil { + return fmt.Errorf("任务 %v: 解析温度数据失败: %v", d.taskLog.TaskID, err) + } + currentValue = data.TemperatureCelsius + alarmCode = models.AlarmCodeTemperature + case models.SensorTypeHumidity: + var data models.HumidityData + if err := sensorData.ParseData(&data); err != nil { + return fmt.Errorf("任务 %v: 解析湿度数据失败: %v", d.taskLog.TaskID, err) + } + currentValue = data.HumidityPercent + alarmCode = models.AlarmCodeHumidity + case models.SensorTypeWeight: + var data models.WeightData + if err := sensorData.ParseData(&data); err != nil { + return fmt.Errorf("任务 %v: 解析重量数据失败: %v", d.taskLog.TaskID, err) + } + currentValue = data.WeightKilograms + alarmCode = models.AlarmCodeWeight + + default: + return fmt.Errorf("任务 %v: 不支持的传感器类型: %v", d.taskLog.TaskID, d.params.SensorType) + } + + // 阈值检查未通过 + isExceeded := !d.checkThreshold(currentValue, d.params.Operator, d.params.Thresholds) + + if isExceeded { + // 状态一:检查未通过,确保告警开启 + summary := fmt.Sprintf("设备 %d(%s) 不满足阈值条件 (%s %.2f)", d.params.DeviceID, d.params.SensorType, d.params.Operator, d.params.Thresholds) + details := fmt.Sprintf("当前检测值: %.2f", currentValue) + logger.Infof("任务 %v: %s。%s", d.taskLog.TaskID, summary, details) + + newAlarm := &models.ActiveAlarm{ + SourceType: models.AlarmSourceTypeDevice, + SourceID: d.params.DeviceID, + AlarmCode: alarmCode, + AlarmSummary: summary, + AlarmDetails: details, + Level: d.params.Level, + TriggerTime: time.Now(), + } + + if err := d.alarmService.CreateAlarmIfNotExists(taskCtx, newAlarm); err != nil { + logger.Errorf("任务 %v: 创建告警失败: %v", d.taskLog.TaskID, err) + // 根据策略决定是否需要返回错误,这里选择不中断任务执行 + } + } else { + // 状态二:检查已通过,确保告警关闭 + resolveMethod := "系统自动解决:阈值恢复正常" + logger.Infof("任务 %v: 设备 %d 的 %s 阈值已恢复正常,正在尝试关闭告警。", d.taskLog.TaskID, d.params.DeviceID, d.params.SensorType) + + if err := d.alarmService.CloseAlarm(taskCtx, models.AlarmSourceTypeDevice, d.params.DeviceID, alarmCode, resolveMethod, nil); err != nil { + logger.Errorf("任务 %v: 关闭告警失败: %v", d.taskLog.TaskID, err) + // 根据策略决定是否需要返回错误,这里选择不中断任务执行 + } + } + + return nil +} + +// checkThreshold 校验当前值是否满足阈值条件 +func (d *DeviceThresholdCheckTask) checkThreshold(currentValue float32, operator models.Operator, threshold float32) bool { + switch operator { + case models.OperatorLessThan: + return currentValue < threshold + case models.OperatorLessThanOrEqualTo: + return currentValue <= threshold + case models.OperatorGreaterThan: + return currentValue > threshold + case models.OperatorGreaterThanOrEqualTo: + return currentValue >= threshold + case models.OperatorEqualTo: + return currentValue == threshold + case models.OperatorNotEqualTo: + return currentValue != threshold + default: + return false + } +} + +// parseParameters 解析任务参数 +func (d *DeviceThresholdCheckTask) parseParameters(ctx context.Context) error { + logger := logs.TraceLogger(ctx, d.ctx, "parseParameters") + var err error + d.onceParse.Do(func() { + if d.taskLog.Task.Parameters == nil { + logger.Errorf("任务 %v: 缺少参数", d.taskLog.TaskID) + err = fmt.Errorf("任务 %v: 参数不全", d.taskLog.TaskID) + return + } + + var params DeviceThresholdCheckParams + err = d.taskLog.Task.ParseParameters(¶ms) + if err != nil { + logger.Errorf("任务 %v: 解析参数失败: %v", d.taskLog.TaskID, err) + err = fmt.Errorf("任务 %v: 解析参数失败: %v", d.taskLog.TaskID, err) + return + } + + if params.SensorType == "" { + err = fmt.Errorf("任务 %v: 未配置传感器类型", d.taskLog.TaskID) + } + if params.Operator == "" { + err = fmt.Errorf("任务 %v: 缺少操作符", d.taskLog.TaskID) + } + if params.Thresholds == 0 { + err = fmt.Errorf("任务 %v: 未配置阈值", d.taskLog.TaskID) + } + if params.DeviceID == 0 { + err = fmt.Errorf("任务 %v: 未配置设备ID", d.taskLog.TaskID) + } + if params.Level == "" { + params.Level = models.WarnLevel + } + + d.params = params + + }) + return err +} + +func (d *DeviceThresholdCheckTask) OnFailure(ctx context.Context, executeErr error) { + logger := logs.TraceLogger(ctx, d.ctx, "OnFailure") + logger.Errorf("设备阈值检测任务执行失败, 任务ID: %v: 执行失败: %v", d.taskLog.TaskID, executeErr) +} + +func (d *DeviceThresholdCheckTask) ResolveDeviceIDs(ctx context.Context) ([]uint32, error) { + taskCtx := logs.AddFuncName(ctx, d.ctx, "ResolveDeviceIDs") + if err := d.parseParameters(taskCtx); err != nil { + return nil, err + } + return []uint32{d.params.DeviceID}, nil +} diff --git a/internal/domain/task/full_collection_task.go b/internal/domain/task/full_collection_task.go index b963500..9f94d7d 100644 --- a/internal/domain/task/full_collection_task.go +++ b/internal/domain/task/full_collection_task.go @@ -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 } diff --git a/internal/domain/task/release_feed_weight_task.go b/internal/domain/task/release_feed_weight_task.go index 530e929..ad58d2e 100644 --- a/internal/domain/task/release_feed_weight_task.go +++ b/internal/domain/task/release_feed_weight_task.go @@ -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 } diff --git a/internal/domain/task/task.go b/internal/domain/task/task.go index dfc2d8b..e8c51cf 100644 --- a/internal/domain/task/task.go +++ b/internal/domain/task/task.go @@ -4,7 +4,9 @@ import ( "context" "fmt" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm" "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -15,26 +17,38 @@ const ( CompNameDelayTask = "DelayTask" CompNameReleaseFeedWeight = "ReleaseFeedWeightTask" CompNameFullCollectionTask = "FullCollectionTask" + CompNameAlarmNotification = "AlarmNotificationTask" ) type taskFactory struct { - ctx context.Context + ctx context.Context + sensorDataRepo repository.SensorDataRepository deviceRepo repository.DeviceRepository - deviceService device.Service + alarmRepo repository.AlarmRepository + + deviceService device.Service + notificationService notify.Service + alarmService alarm.AlarmService } func NewTaskFactory( ctx context.Context, sensorDataRepo repository.SensorDataRepository, deviceRepo repository.DeviceRepository, + alarmRepo repository.AlarmRepository, deviceService device.Service, + notifyService notify.Service, + alarmService alarm.AlarmService, ) plan.TaskFactory { return &taskFactory{ - ctx: ctx, - sensorDataRepo: sensorDataRepo, - deviceRepo: deviceRepo, - deviceService: deviceService, + ctx: ctx, + sensorDataRepo: sensorDataRepo, + deviceRepo: deviceRepo, + alarmRepo: alarmRepo, + deviceService: deviceService, + notificationService: notifyService, + alarmService: alarmService, } } @@ -48,6 +62,12 @@ func (t *taskFactory) Production(ctx context.Context, claimedLog *models.TaskExe return NewReleaseFeedWeightTask(logs.AddCompName(baseCtx, CompNameReleaseFeedWeight), claimedLog, t.sensorDataRepo, t.deviceRepo, t.deviceService) case models.TaskTypeFullCollection: return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), claimedLog, t.deviceRepo, t.deviceService) + case models.TaskTypeAlarmNotification: + return NewAlarmNotificationTask(logs.AddCompName(baseCtx, CompNameAlarmNotification), claimedLog, t.notificationService, t.alarmRepo) + case models.TaskTypeDeviceThresholdCheck: + return NewDeviceThresholdCheckTask(logs.AddCompName(baseCtx, "DeviceThresholdCheckTask"), claimedLog, t.sensorDataRepo, t.alarmService) + case models.TaskTypeAreaCollectorThresholdCheck: + return NewAreaThresholdCheckTask(logs.AddCompName(baseCtx, "AreaCollectorThresholdCheckTask"), claimedLog, t.sensorDataRepo, t.deviceRepo, t.alarmService) default: // TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型 logger.Panicf("不支持的任务类型: %s", claimedLog.Task.Type) @@ -75,6 +95,12 @@ func (t *taskFactory) CreateTaskFromModel(ctx context.Context, taskModel *models ), nil case models.TaskTypeFullCollection: return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), tempLog, t.deviceRepo, t.deviceService), nil + case models.TaskTypeAlarmNotification: + return NewAlarmNotificationTask(logs.AddCompName(baseCtx, CompNameAlarmNotification), tempLog, t.notificationService, t.alarmRepo), nil + case models.TaskTypeDeviceThresholdCheck: + return NewDeviceThresholdCheckTask(logs.AddCompName(baseCtx, "DeviceThresholdCheckTask"), tempLog, t.sensorDataRepo, t.alarmService), nil + case models.TaskTypeAreaCollectorThresholdCheck: + return NewAreaThresholdCheckTask(logs.AddCompName(baseCtx, "AreaCollectorThresholdCheckTask"), tempLog, t.sensorDataRepo, t.deviceRepo, t.alarmService), nil default: return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type) } diff --git a/internal/infra/config/config.go b/internal/infra/config/config.go index 81256b5..b592074 100644 --- a/internal/infra/config/config.go +++ b/internal/infra/config/config.go @@ -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 { // 默认值可以在这里设置,但我们优先使用配置文件中的值 diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 7b29ebd..d86d578 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -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 { diff --git a/internal/infra/logs/logs.go b/internal/infra/logs/logs.go index 13e8c4d..d44cf9d 100644 --- a/internal/infra/logs/logs.go +++ b/internal/infra/logs/logs.go @@ -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), } // 附加调用链信息 diff --git a/internal/infra/models/alarm.go b/internal/infra/models/alarm.go new file mode 100644 index 0000000..4cd02a1 --- /dev/null +++ b/internal/infra/models/alarm.go @@ -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" +} diff --git a/internal/infra/models/device.go b/internal/infra/models/device.go index 3b15bab..aa2af91 100644 --- a/internal/infra/models/device.go +++ b/internal/infra/models/device.go @@ -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"` diff --git a/internal/infra/models/device_template.go b/internal/infra/models/device_template.go index bbb432b..0f5cb6d 100644 --- a/internal/infra/models/device_template.go +++ b/internal/infra/models/device_template.go @@ -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"` diff --git a/internal/infra/models/execution.go b/internal/infra/models/execution.go index 3c20751..fc69e13 100644 --- a/internal/infra/models/execution.go +++ b/internal/infra/models/execution.go @@ -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 (何地) --- diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go index 7d5731c..c4723b0 100644 --- a/internal/infra/models/farm_asset.go +++ b/internal/infra/models/farm_asset.go @@ -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:猪栏当前状态"` } diff --git a/internal/infra/models/feed.go b/internal/infra/models/feed.go index a16378f..472ba2d 100644 --- a/internal/infra/models/feed.go +++ b/internal/infra/models/feed.go @@ -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:备注, 如 '例行喂料, 弱猪补料' 等"` } diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go index bd60718..052fb60 100644 --- a/internal/infra/models/medication.go +++ b/internal/infra/models/medication.go @@ -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:用药时间"` } diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 3d360a4..fa35ec0 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -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 +} diff --git a/internal/infra/models/notify.go b/internal/infra/models/notify.go index 291ee9b..2602267 100644 --- a/internal/infra/models/notify.go +++ b/internal/infra/models/notify.go @@ -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, 日志标识符) diff --git a/internal/infra/models/pig_batch.go b/internal/infra/models/pig_batch.go index 658c31b..ee9ca0d 100644 --- a/internal/infra/models/pig_batch.go +++ b/internal/infra/models/pig_batch.go @@ -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:称重时间"` } diff --git a/internal/infra/models/pig_sick.go b/internal/infra/models/pig_sick.go index caf21a9..be85b19 100644 --- a/internal/infra/models/pig_sick.go +++ b/internal/infra/models/pig_sick.go @@ -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:事件发生时间"` } diff --git a/internal/infra/models/pig_trade.go b/internal/infra/models/pig_trade.go index 4d4e835..4893c98 100644 --- a/internal/infra/models/pig_trade.go +++ b/internal/infra/models/pig_trade.go @@ -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 { diff --git a/internal/infra/models/pig_transfer.go b/internal/infra/models/pig_transfer.go index 3fc3a04..0af3059 100644 --- a/internal/infra/models/pig_transfer.go +++ b/internal/infra/models/pig_transfer.go @@ -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"` } diff --git a/internal/infra/models/plan.go b/internal/infra/models/plan.go index 0c43fe4..7827980 100644 --- a/internal/infra/models/plan.go +++ b/internal/infra/models/plan.go @@ -11,6 +11,15 @@ import ( "gorm.io/gorm" ) +type PlanName string + +const ( + // PlanNamePeriodicSystemHealthCheck 是周期性系统健康检查计划的名称 + PlanNamePeriodicSystemHealthCheck PlanName = "周期性系统健康检查" + // PlanNameAlarmNotification 是告警通知发送计划的名称 + PlanNameAlarmNotification PlanName = "告警通知发送" +) + // PlanExecutionType 定义了计划的执行类型 type PlanExecutionType string @@ -31,10 +40,13 @@ const ( type TaskType string const ( - TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务 - TaskTypeWaiting TaskType = "等待" // 等待任务 - TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务 - TaskTypeFullCollection TaskType = "全量采集" // 新增的全量采集任务 + TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务 + TaskTypeWaiting TaskType = "等待" // 等待任务 + TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务 + TaskTypeFullCollection TaskType = "全量采集" // 新增的全量采集任务 + TaskTypeAlarmNotification TaskType = "告警通知" // 告警通知任务 + TaskTypeDeviceThresholdCheck TaskType = "设备阈值检查" // 设备阈值检查任务 + TaskTypeAreaCollectorThresholdCheck TaskType = "区域阈值检查" // 区域阈值检查任务 ) // -- Task Parameters -- @@ -62,15 +74,15 @@ const ( // Plan 代表系统中的一个计划,可以包含子计划或任务 type Plan struct { - gorm.Model + Model - Name string `gorm:"not null" json:"name"` + Name PlanName `gorm:"not null" json:"name"` Description string `json:"description"` PlanType PlanType `gorm:"not null;index" json:"plan_type"` // 任务类型, 包括系统任务和用户自定义任务 ExecutionType PlanExecutionType `gorm:"not null;index" json:"execution_type"` Status PlanStatus `gorm:"default:'已禁用';index" json:"status"` // 计划是否被启动 - ExecuteNum uint `gorm:"default:0" json:"execute_num"` // 计划预期执行次数 - ExecuteCount uint `gorm:"default:0" json:"execute_count"` // 执行计数器 + ExecuteNum uint32 `gorm:"default:0" json:"execute_num"` // 计划预期执行次数 + ExecuteCount uint32 `gorm:"default:0" json:"execute_count"` // 执行计数器 // 针对 PlanExecutionTypeAutomatic,使用 Cron 表达式定义调度规则 CronExpression string `json:"cron_expression"` @@ -151,12 +163,12 @@ func (p *Plan) ReorderSteps() { // SubPlan 代表作为另一个计划一部分的子计划,具有执行顺序 type SubPlan struct { - gorm.Model + Model - ParentPlanID uint `gorm:"not null;index" json:"parent_plan_id"` // 父计划的ID - ChildPlanID uint `gorm:"not null;index" json:"child_plan_id"` // 子计划的ID (它本身也是一个 Plan) - ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在父计划中的执行顺序 - ChildPlan *Plan `gorm:"-" json:"child_plan"` // 完整子计划数据,仅内存中 + ParentPlanID uint32 `gorm:"not null;index" json:"parent_plan_id"` // 父计划的ID + ChildPlanID uint32 `gorm:"not null;index" json:"child_plan_id"` // 子计划的ID (它本身也是一个 Plan) + ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在父计划中的执行顺序 + ChildPlan *Plan `gorm:"-" json:"child_plan"` // 完整子计划数据,仅内存中 } // TableName 自定义 GORM 使用的数据库表名 @@ -172,7 +184,7 @@ type Task struct { UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` // 保持软删除功能 - PlanID uint `gorm:"not null;index" json:"plan_id"` // 此任务所属计划的ID + PlanID uint32 `gorm:"not null;index" json:"plan_id"` // 此任务所属计划的ID Name string `gorm:"not null" json:"name"` Description string `json:"description"` ExecutionOrder int `gorm:"not null" json:"execution_order"` // 在计划中的执行顺序 @@ -201,11 +213,25 @@ func (t Task) ParseParameters(v interface{}) error { return json.Unmarshal(t.Parameters, v) } +// SaveParameters 将一个结构体序列化为 JSON 并保存到 Task 的 Parameters 字段。 +// 示例: +// +// params := LoraParameters{...} +// if err := task.SaveParameters(params); err != nil { ... } +func (t *Task) SaveParameters(v interface{}) error { + data, err := json.Marshal(v) + if err != nil { + return fmt.Errorf("序列化任务参数失败: %w", err) + } + t.Parameters = data + return nil +} + // DeviceTask 是设备和任务之间的关联模型,表示一个设备可以执行多个任务,一个任务可以被多个设备执行。 type DeviceTask struct { - gorm.Model - DeviceID uint `gorm:"not null;index"` // 设备ID - TaskID uint `gorm:"not null;index"` // 任务ID + Model + DeviceID uint32 `gorm:"not null;index"` // 设备ID + TaskID uint32 `gorm:"not null;index"` // 任务ID // 可选:如果需要存储关联的额外信息,可以在这里添加字段,例如: // Configuration datatypes.JSON `json:"configuration"` // 任务在特定设备上的配置 diff --git a/internal/infra/models/schedule.go b/internal/infra/models/schedule.go index 12a6384..98f06fc 100644 --- a/internal/infra/models/schedule.go +++ b/internal/infra/models/schedule.go @@ -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 确保如果日志被删除,这个待办任务也会被自动清理 diff --git a/internal/infra/models/sensor_data.go b/internal/infra/models/sensor_data.go index 4c50221..8165bcc 100644 --- a/internal/infra/models/sensor_data.go +++ b/internal/infra/models/sensor_data.go @@ -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) +} diff --git a/internal/infra/models/user.go b/internal/infra/models/user.go index 6212136..2d66ca7 100644 --- a/internal/infra/models/user.go +++ b/internal/infra/models/user.go @@ -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 后面的冒号) diff --git a/internal/infra/notify/lark.go b/internal/infra/notify/lark.go index c1971ca..f49ff7e 100644 --- a/internal/infra/notify/lark.go +++ b/internal/infra/notify/lark.go @@ -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 数据结构 --- diff --git a/internal/infra/notify/log_notifier.go b/internal/infra/notify/log_notifier.go index 001bdb6..f799c6c 100644 --- a/internal/infra/notify/log_notifier.go +++ b/internal/infra/notify/log_notifier.go @@ -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 } diff --git a/internal/infra/notify/notify.go b/internal/infra/notify/notify.go index 4f86074..e34db31 100644 --- a/internal/infra/notify/notify.go +++ b/internal/infra/notify/notify.go @@ -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 } diff --git a/internal/infra/notify/smtp.go b/internal/infra/notify/smtp.go index 131e788..31a06bf 100644 --- a/internal/infra/notify/smtp.go +++ b/internal/infra/notify/smtp.go @@ -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 } diff --git a/internal/infra/notify/wechat.go b/internal/infra/notify/wechat.go index ec5c33d..7e783b5 100644 --- a/internal/infra/notify/wechat.go +++ b/internal/infra/notify/wechat.go @@ -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> 级别: %s\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 数据结构 --- diff --git a/internal/infra/repository/alarm_repository.go b/internal/infra/repository/alarm_repository.go new file mode 100644 index 0000000..7eea0be --- /dev/null +++ b/internal/infra/repository/alarm_repository.go @@ -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) +} diff --git a/internal/infra/repository/area_controller_repository.go b/internal/infra/repository/area_controller_repository.go index 4c0b62f..d9ed656 100644 --- a/internal/infra/repository/area_controller_repository.go +++ b/internal/infra/repository/area_controller_repository.go @@ -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 // 没有发现任何未被忽略的任务正在使用此区域主控 +} diff --git a/internal/infra/repository/device_command_log_repository.go b/internal/infra/repository/device_command_log_repository.go index b4bc0bb..e974dfd 100644 --- a/internal/infra/repository/device_command_log_repository.go +++ b/internal/infra/repository/device_command_log_repository.go @@ -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 字段 diff --git a/internal/infra/repository/device_repository.go b/internal/infra/repository/device_repository.go index 32757e3..72b01d6 100644 --- a/internal/infra/repository/device_repository.go +++ b/internal/infra/repository/device_repository.go @@ -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 { diff --git a/internal/infra/repository/device_template_repository.go b/internal/infra/repository/device_template_repository.go index dd9c0b1..22d3fa3 100644 --- a/internal/infra/repository/device_template_repository.go +++ b/internal/infra/repository/device_template_repository.go @@ -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) diff --git a/internal/infra/repository/execution_log_repository.go b/internal/infra/repository/execution_log_repository.go index bd4a1fb..9d58856 100644 --- a/internal/infra/repository/execution_log_repository.go +++ b/internal/infra/repository/execution_log_repository.go @@ -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 (?, ?)", diff --git a/internal/infra/repository/medication_log_repository.go b/internal/infra/repository/medication_log_repository.go index 6db863d..ac99400 100644 --- a/internal/infra/repository/medication_log_repository.go +++ b/internal/infra/repository/medication_log_repository.go @@ -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" diff --git a/internal/infra/repository/notification_repository.go b/internal/infra/repository/notification_repository.go index 4055966..3f27a57 100644 --- a/internal/infra/repository/notification_repository.go +++ b/internal/infra/repository/notification_repository.go @@ -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) diff --git a/internal/infra/repository/pending_collection_repository.go b/internal/infra/repository/pending_collection_repository.go index 77ab3ad..55360e8 100644 --- a/internal/infra/repository/pending_collection_repository.go +++ b/internal/infra/repository/pending_collection_repository.go @@ -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 字段 diff --git a/internal/infra/repository/pending_task_repository.go b/internal/infra/repository/pending_task_repository.go index 06824df..25f7d7e 100644 --- a/internal/infra/repository/pending_task_repository.go +++ b/internal/infra/repository/pending_task_repository.go @@ -16,27 +16,27 @@ import ( // PendingTaskRepository 定义了与待执行任务队列交互的接口。 type PendingTaskRepository interface { FindAllPendingTasks(ctx context.Context) ([]models.PendingTask, error) - FindPendingTriggerByPlanID(ctx context.Context, planID uint) (*models.PendingTask, error) - DeletePendingTasksByIDs(ctx context.Context, ids []uint) error + FindPendingTriggerByPlanID(ctx context.Context, planID uint32) (*models.PendingTask, error) + DeletePendingTasksByIDs(ctx context.Context, ids []uint32) error CreatePendingTask(ctx context.Context, task *models.PendingTask) error CreatePendingTasksInBatch(ctx context.Context, tasks []*models.PendingTask) error // UpdatePendingTaskExecuteAt 更新指定待执行任务的执行时间 - UpdatePendingTaskExecuteAt(ctx context.Context, id uint, executeAt time.Time) error + UpdatePendingTaskExecuteAt(ctx context.Context, id uint32, executeAt time.Time) error // ClearAllPendingTasks 清空所有待执行任务 ClearAllPendingTasks(ctx context.Context) error // ClaimNextAvailableTask 原子地认领下一个可用的任务。 // 它会同时返回被认领任务对应的日志对象,以及被删除的待办任务对象的内存副本。 - ClaimNextAvailableTask(ctx context.Context, excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error) + ClaimNextAvailableTask(ctx context.Context, excludePlanIDs []uint32) (*models.TaskExecutionLog, *models.PendingTask, error) // RequeueTask 安全地将一个任务重新放回队列。 RequeueTask(ctx context.Context, originalPendingTask *models.PendingTask) error // FindPendingTasksByTaskLogIDs 根据 TaskExecutionLogID 列表查找对应的待执行任务 - FindPendingTasksByTaskLogIDs(ctx context.Context, taskLogIDs []uint) ([]models.PendingTask, error) + FindPendingTasksByTaskLogIDs(ctx context.Context, taskLogIDs []uint32) ([]models.PendingTask, error) // DeletePendingTasksByPlanLogID 删除与指定计划执行日志ID相关的所有待执行任务 - DeletePendingTasksByPlanLogID(ctx context.Context, planLogID uint) error + DeletePendingTasksByPlanLogID(ctx context.Context, planLogID uint32) error } // gormPendingTaskRepository 是使用 GORM 的具体实现。 @@ -59,7 +59,7 @@ func (r *gormPendingTaskRepository) FindAllPendingTasks(ctx context.Context) ([] return tasks, err } -func (r *gormPendingTaskRepository) FindPendingTriggerByPlanID(ctx context.Context, planID uint) (*models.PendingTask, error) { +func (r *gormPendingTaskRepository) FindPendingTriggerByPlanID(ctx context.Context, planID uint32) (*models.PendingTask, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPendingTriggerByPlanID") var pendingTask models.PendingTask // 关键修改:通过 JOIN tasks 表并查询 parameters JSON 字段来查找触发器,而不是依赖 task.plan_id @@ -73,7 +73,7 @@ func (r *gormPendingTaskRepository) FindPendingTriggerByPlanID(ctx context.Conte return &pendingTask, err } -func (r *gormPendingTaskRepository) DeletePendingTasksByIDs(ctx context.Context, ids []uint) error { +func (r *gormPendingTaskRepository) DeletePendingTasksByIDs(ctx context.Context, ids []uint32) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePendingTasksByIDs") if len(ids) == 0 { return nil @@ -96,7 +96,7 @@ func (r *gormPendingTaskRepository) CreatePendingTasksInBatch(ctx context.Contex } // UpdatePendingTaskExecuteAt 更新指定待执行任务的执行时间 -func (r *gormPendingTaskRepository) UpdatePendingTaskExecuteAt(ctx context.Context, id uint, executeAt time.Time) error { +func (r *gormPendingTaskRepository) UpdatePendingTaskExecuteAt(ctx context.Context, id uint32, executeAt time.Time) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePendingTaskExecuteAt") return r.db.WithContext(repoCtx).Model(&models.PendingTask{}).Where("id = ?", id).Update("execute_at", executeAt).Error } @@ -108,7 +108,7 @@ func (r *gormPendingTaskRepository) ClearAllPendingTasks(ctx context.Context) er } // ClaimNextAvailableTask 以原子方式认领下一个可用的任务。 -func (r *gormPendingTaskRepository) ClaimNextAvailableTask(ctx context.Context, excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error) { +func (r *gormPendingTaskRepository) ClaimNextAvailableTask(ctx context.Context, excludePlanIDs []uint32) (*models.TaskExecutionLog, *models.PendingTask, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "ClaimNextAvailableTask") var log models.TaskExecutionLog var pendingTask models.PendingTask @@ -175,7 +175,7 @@ func (r *gormPendingTaskRepository) RequeueTask(ctx context.Context, originalPen } // FindPendingTasksByTaskLogIDs 根据 TaskExecutionLogID 列表查找对应的待执行任务 -func (r *gormPendingTaskRepository) FindPendingTasksByTaskLogIDs(ctx context.Context, taskLogIDs []uint) ([]models.PendingTask, error) { +func (r *gormPendingTaskRepository) FindPendingTasksByTaskLogIDs(ctx context.Context, taskLogIDs []uint32) ([]models.PendingTask, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPendingTasksByTaskLogIDs") if len(taskLogIDs) == 0 { return []models.PendingTask{}, nil @@ -186,7 +186,7 @@ func (r *gormPendingTaskRepository) FindPendingTasksByTaskLogIDs(ctx context.Con } // DeletePendingTasksByPlanLogID 删除与指定计划执行日志ID相关的所有待执行任务 -func (r *gormPendingTaskRepository) DeletePendingTasksByPlanLogID(ctx context.Context, planLogID uint) error { +func (r *gormPendingTaskRepository) DeletePendingTasksByPlanLogID(ctx context.Context, planLogID uint32) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePendingTasksByPlanLogID") // 使用子查询找到所有与 planLogID 相关的 task_execution_log_id subQuery := r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).Select("id").Where("plan_execution_log_id = ?", planLogID) diff --git a/internal/infra/repository/pig_batch_log_repository.go b/internal/infra/repository/pig_batch_log_repository.go index c07b70c..54eaf4a 100644 --- a/internal/infra/repository/pig_batch_log_repository.go +++ b/internal/infra/repository/pig_batch_log_repository.go @@ -12,9 +12,9 @@ import ( // PigBatchLogListOptions 定义了查询猪批次日志时的可选参数 type PigBatchLogListOptions struct { - PigBatchID *uint + PigBatchID *uint32 ChangeType *models.LogChangeType - OperatorID *uint + OperatorID *uint32 StartTime *time.Time // 基于 happened_at 字段 EndTime *time.Time // 基于 happened_at 字段 OrderBy string // 例如 "happened_at asc" @@ -26,10 +26,10 @@ type PigBatchLogRepository interface { CreateTx(ctx context.Context, tx *gorm.DB, log *models.PigBatchLog) error // GetLogsByBatchIDAndDateRangeTx 在指定的事务中,获取指定批次在特定时间范围内的所有日志记录。 - GetLogsByBatchIDAndDateRangeTx(ctx context.Context, tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) + GetLogsByBatchIDAndDateRangeTx(ctx context.Context, tx *gorm.DB, batchID uint32, startDate, endDate time.Time) ([]*models.PigBatchLog, error) // GetLastLogByBatchIDTx 在指定的事务中,获取某批次的最后一条日志记录。 - GetLastLogByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint) (*models.PigBatchLog, error) + GetLastLogByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint32) (*models.PigBatchLog, error) // List 支持分页和过滤的列表查询 List(ctx context.Context, opts PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error) @@ -53,7 +53,7 @@ func (r *gormPigBatchLogRepository) CreateTx(ctx context.Context, tx *gorm.DB, l } // GetLogsByBatchIDAndDateRangeTx 实现了在指定的事务中,获取指定批次在特定时间范围内的所有日志记录的逻辑。 -func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(ctx context.Context, tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) { +func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(ctx context.Context, tx *gorm.DB, batchID uint32, startDate, endDate time.Time) ([]*models.PigBatchLog, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLogsByBatchIDAndDateRangeTx") var logs []*models.PigBatchLog err := tx.WithContext(repoCtx).Where("pig_batch_id = ? AND created_at >= ? AND created_at <= ?", batchID, startDate, endDate).Find(&logs).Error @@ -64,7 +64,7 @@ func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(ctx context.C } // GetLastLogByBatchIDTx 实现了在指定的事务中,获取某批次的最后一条日志记录的逻辑。 -func (r *gormPigBatchLogRepository) GetLastLogByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint) (*models.PigBatchLog, error) { +func (r *gormPigBatchLogRepository) GetLastLogByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint32) (*models.PigBatchLog, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLastLogByBatchIDTx") var log models.PigBatchLog err := tx.WithContext(repoCtx).Where("pig_batch_id = ?", batchID).Order("id DESC").First(&log).Error diff --git a/internal/infra/repository/pig_batch_repository.go b/internal/infra/repository/pig_batch_repository.go index ae37ecd..40c94e0 100644 --- a/internal/infra/repository/pig_batch_repository.go +++ b/internal/infra/repository/pig_batch_repository.go @@ -14,13 +14,13 @@ import ( type PigBatchRepository interface { CreatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, error) CreatePigBatchTx(ctx context.Context, tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error) - GetPigBatchByID(ctx context.Context, id uint) (*models.PigBatch, error) - GetPigBatchByIDTx(ctx context.Context, tx *gorm.DB, id uint) (*models.PigBatch, error) + GetPigBatchByID(ctx context.Context, id uint32) (*models.PigBatch, error) + GetPigBatchByIDTx(ctx context.Context, tx *gorm.DB, id uint32) (*models.PigBatch, error) // UpdatePigBatch 更新一个猪批次,返回更新后的批次、受影响的行数和错误 UpdatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, int64, error) // DeletePigBatch 根据ID删除一个猪批次,返回受影响的行数和错误 - DeletePigBatch(ctx context.Context, id uint) (int64, error) - DeletePigBatchTx(ctx context.Context, tx *gorm.DB, id uint) (int64, error) + DeletePigBatch(ctx context.Context, id uint32) (int64, error) + DeletePigBatchTx(ctx context.Context, tx *gorm.DB, id uint32) (int64, error) ListPigBatches(ctx context.Context, isActive *bool) ([]*models.PigBatch, error) // ListWeighingBatches 支持分页和过滤的批次称重列表查询 @@ -32,7 +32,7 @@ type PigBatchRepository interface { // WeighingBatchListOptions 定义了查询批次称重记录时的可选参数 type WeighingBatchListOptions struct { - PigBatchID *uint + PigBatchID *uint32 StartTime *time.Time // 基于 weighing_time 字段 EndTime *time.Time // 基于 weighing_time 字段 OrderBy string // 例如 "weighing_time asc" @@ -40,9 +40,9 @@ type WeighingBatchListOptions struct { // WeighingRecordListOptions 定义了查询单次称重记录时的可选参数 type WeighingRecordListOptions struct { - WeighingBatchID *uint - PenID *uint - OperatorID *uint + WeighingBatchID *uint32 + PenID *uint32 + OperatorID *uint32 StartTime *time.Time // 基于 weighing_time 字段 EndTime *time.Time // 基于 weighing_time 字段 OrderBy string // 例如 "weighing_time asc" @@ -75,7 +75,7 @@ func (r *gormPigBatchRepository) CreatePigBatchTx(ctx context.Context, tx *gorm. } // GetPigBatchByID 根据ID获取单个猪批次 -func (r *gormPigBatchRepository) GetPigBatchByID(ctx context.Context, id uint) (*models.PigBatch, error) { +func (r *gormPigBatchRepository) GetPigBatchByID(ctx context.Context, id uint32) (*models.PigBatch, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigBatchByID") return r.GetPigBatchByIDTx(repoCtx, r.db, id) } @@ -92,12 +92,12 @@ func (r *gormPigBatchRepository) UpdatePigBatch(ctx context.Context, batch *mode } // DeletePigBatch 根据ID删除一个猪批次 (GORM 会执行软删除) -func (r *gormPigBatchRepository) DeletePigBatch(ctx context.Context, id uint) (int64, error) { +func (r *gormPigBatchRepository) DeletePigBatch(ctx context.Context, id uint32) (int64, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigBatch") return r.DeletePigBatchTx(repoCtx, r.db, id) } -func (r *gormPigBatchRepository) DeletePigBatchTx(ctx context.Context, tx *gorm.DB, id uint) (int64, error) { +func (r *gormPigBatchRepository) DeletePigBatchTx(ctx context.Context, tx *gorm.DB, id uint32) (int64, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigBatchTx") result := tx.WithContext(repoCtx).Delete(&models.PigBatch{}, id) if result.Error != nil { @@ -130,7 +130,7 @@ func (r *gormPigBatchRepository) ListPigBatches(ctx context.Context, isActive *b } // GetPigBatchByIDTx 在指定的事务中,通过ID获取单个猪批次 -func (r *gormPigBatchRepository) GetPigBatchByIDTx(ctx context.Context, tx *gorm.DB, id uint) (*models.PigBatch, error) { +func (r *gormPigBatchRepository) GetPigBatchByIDTx(ctx context.Context, tx *gorm.DB, id uint32) (*models.PigBatch, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigBatchByIDTx") var batch models.PigBatch if err := tx.WithContext(repoCtx).First(&batch, id).Error; err != nil { diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go index f70c75b..10068c5 100644 --- a/internal/infra/repository/pig_farm_repository.go +++ b/internal/infra/repository/pig_farm_repository.go @@ -13,13 +13,13 @@ import ( type PigFarmRepository interface { // PigHouse methods CreatePigHouse(ctx context.Context, house *models.PigHouse) error - GetPigHouseByID(ctx context.Context, id uint) (*models.PigHouse, error) + GetPigHouseByID(ctx context.Context, id uint32) (*models.PigHouse, error) ListPigHouses(ctx context.Context) ([]models.PigHouse, error) // UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误 UpdatePigHouse(ctx context.Context, house *models.PigHouse) (int64, error) // DeletePigHouse 根据ID删除一个猪舍,返回受影响的行数和错误 - DeletePigHouse(ctx context.Context, id uint) (int64, error) - CountPensInHouse(ctx context.Context, houseID uint) (int64, error) + DeletePigHouse(ctx context.Context, id uint32) (int64, error) + CountPensInHouse(ctx context.Context, houseID uint32) (int64, error) } // gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现 @@ -42,7 +42,7 @@ func (r *gormPigFarmRepository) CreatePigHouse(ctx context.Context, house *model } // GetPigHouseByID 根据ID获取单个猪舍 -func (r *gormPigFarmRepository) GetPigHouseByID(ctx context.Context, id uint) (*models.PigHouse, error) { +func (r *gormPigFarmRepository) GetPigHouseByID(ctx context.Context, id uint32) (*models.PigHouse, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigHouseByID") var house models.PigHouse if err := r.db.WithContext(repoCtx).First(&house, id).Error; err != nil { @@ -72,7 +72,7 @@ func (r *gormPigFarmRepository) UpdatePigHouse(ctx context.Context, house *model } // DeletePigHouse 根据ID删除一个猪舍,返回受影响的行数和错误 -func (r *gormPigFarmRepository) DeletePigHouse(ctx context.Context, id uint) (int64, error) { +func (r *gormPigFarmRepository) DeletePigHouse(ctx context.Context, id uint32) (int64, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigHouse") result := r.db.WithContext(repoCtx).Delete(&models.PigHouse{}, id) if result.Error != nil { @@ -82,7 +82,7 @@ func (r *gormPigFarmRepository) DeletePigHouse(ctx context.Context, id uint) (in } // CountPensInHouse 统计猪舍中的猪栏数量 -func (r *gormPigFarmRepository) CountPensInHouse(ctx context.Context, houseID uint) (int64, error) { +func (r *gormPigFarmRepository) CountPensInHouse(ctx context.Context, houseID uint32) (int64, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "CountPensInHouse") var count int64 err := r.db.WithContext(repoCtx).Model(&models.Pen{}).Where("house_id = ?", houseID).Count(&count).Error diff --git a/internal/infra/repository/pig_pen_repository.go b/internal/infra/repository/pig_pen_repository.go index ac83df9..4ec91e5 100644 --- a/internal/infra/repository/pig_pen_repository.go +++ b/internal/infra/repository/pig_pen_repository.go @@ -13,18 +13,18 @@ import ( type PigPenRepository interface { CreatePen(ctx context.Context, pen *models.Pen) error // GetPenByID 根据ID获取单个猪栏 (非事务性) - GetPenByID(ctx context.Context, id uint) (*models.Pen, error) + GetPenByID(ctx context.Context, id uint32) (*models.Pen, error) // GetPenByIDTx 根据ID获取单个猪栏 (事务性) - GetPenByIDTx(ctx context.Context, tx *gorm.DB, id uint) (*models.Pen, error) + GetPenByIDTx(ctx context.Context, tx *gorm.DB, id uint32) (*models.Pen, error) ListPens(ctx context.Context) ([]models.Pen, error) // UpdatePen 更新一个猪栏,返回受影响的行数和错误 UpdatePen(ctx context.Context, pen *models.Pen) (int64, error) // DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 - DeletePen(ctx context.Context, id uint) (int64, error) + DeletePen(ctx context.Context, id uint32) (int64, error) // GetPensByBatchIDTx 根据批次ID获取所有关联的猪栏 (事务性) - GetPensByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint) ([]*models.Pen, error) + GetPensByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint32) ([]*models.Pen, error) // UpdatePenFieldsTx 更新猪栏的指定字段 (事务性) - UpdatePenFieldsTx(ctx context.Context, tx *gorm.DB, penID uint, updates map[string]interface{}) error + UpdatePenFieldsTx(ctx context.Context, tx *gorm.DB, penID uint32, updates map[string]interface{}) error } // gormPigPenRepository 是 PigPenRepository 接口的 GORM 实现。 @@ -45,13 +45,13 @@ func (r *gormPigPenRepository) CreatePen(ctx context.Context, pen *models.Pen) e } // GetPenByID 根据ID获取单个猪栏 (非事务性) -func (r *gormPigPenRepository) GetPenByID(ctx context.Context, id uint) (*models.Pen, error) { +func (r *gormPigPenRepository) GetPenByID(ctx context.Context, id uint32) (*models.Pen, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPenByID") return r.GetPenByIDTx(repoCtx, r.db, id) // 非Tx方法直接调用Tx方法 } // GetPenByIDTx 在指定的事务中,通过ID获取单个猪栏信息。 -func (r *gormPigPenRepository) GetPenByIDTx(ctx context.Context, tx *gorm.DB, id uint) (*models.Pen, error) { +func (r *gormPigPenRepository) GetPenByIDTx(ctx context.Context, tx *gorm.DB, id uint32) (*models.Pen, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPenByIDTx") var pen models.Pen if err := tx.WithContext(repoCtx).First(&pen, id).Error; err != nil { @@ -81,7 +81,7 @@ func (r *gormPigPenRepository) UpdatePen(ctx context.Context, pen *models.Pen) ( } // DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 -func (r *gormPigPenRepository) DeletePen(ctx context.Context, id uint) (int64, error) { +func (r *gormPigPenRepository) DeletePen(ctx context.Context, id uint32) (int64, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePen") result := r.db.WithContext(repoCtx).Delete(&models.Pen{}, id) if result.Error != nil { @@ -91,7 +91,7 @@ func (r *gormPigPenRepository) DeletePen(ctx context.Context, id uint) (int64, e } // GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。 -func (r *gormPigPenRepository) GetPensByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint) ([]*models.Pen, error) { +func (r *gormPigPenRepository) GetPensByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint32) ([]*models.Pen, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPensByBatchIDTx") var pens []*models.Pen // 注意:PigBatchID 是指针类型,需要处理 nil 值 @@ -103,7 +103,7 @@ func (r *gormPigPenRepository) GetPensByBatchIDTx(ctx context.Context, tx *gorm. } // UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。 -func (r *gormPigPenRepository) UpdatePenFieldsTx(ctx context.Context, tx *gorm.DB, penID uint, updates map[string]interface{}) error { +func (r *gormPigPenRepository) UpdatePenFieldsTx(ctx context.Context, tx *gorm.DB, penID uint32, updates map[string]interface{}) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePenFieldsTx") result := tx.WithContext(repoCtx).Model(&models.Pen{}).Where("id = ?", penID).Updates(updates) return result.Error diff --git a/internal/infra/repository/pig_sick_repository.go b/internal/infra/repository/pig_sick_repository.go index 61f63e9..8a8b569 100644 --- a/internal/infra/repository/pig_sick_repository.go +++ b/internal/infra/repository/pig_sick_repository.go @@ -13,11 +13,11 @@ import ( // PigSickLogListOptions 定义了查询病猪日志时的可选参数 type PigSickLogListOptions struct { - PigBatchID *uint - PenID *uint + PigBatchID *uint32 + PenID *uint32 Reason *models.PigBatchSickPigReasonType TreatmentLocation *models.PigBatchSickPigTreatmentLocation - OperatorID *uint + OperatorID *uint32 StartTime *time.Time // 基于 happened_at 字段 EndTime *time.Time // 基于 happened_at 字段 OrderBy string // 例如 "happened_at desc" @@ -30,7 +30,7 @@ type PigSickLogRepository interface { CreatePigSickLogTx(ctx context.Context, tx *gorm.DB, log *models.PigSickLog) error // GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录 - GetLastLogByBatchTx(ctx context.Context, tx *gorm.DB, batchID uint) (*models.PigSickLog, error) + GetLastLogByBatchTx(ctx context.Context, tx *gorm.DB, batchID uint32) (*models.PigSickLog, error) // ListPigSickLogs 支持分页和过滤的病猪日志列表查询 ListPigSickLogs(ctx context.Context, opts PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error) @@ -58,7 +58,7 @@ func (r *gormPigSickLogRepository) CreatePigSickLogTx(ctx context.Context, tx *g } // GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录 -func (r *gormPigSickLogRepository) GetLastLogByBatchTx(ctx context.Context, tx *gorm.DB, batchID uint) (*models.PigSickLog, error) { +func (r *gormPigSickLogRepository) GetLastLogByBatchTx(ctx context.Context, tx *gorm.DB, batchID uint32) (*models.PigSickLog, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLastLogByBatchTx") var lastLog models.PigSickLog err := tx.WithContext(repoCtx). diff --git a/internal/infra/repository/pig_trade_repository.go b/internal/infra/repository/pig_trade_repository.go index 3e6cd25..06f85bb 100644 --- a/internal/infra/repository/pig_trade_repository.go +++ b/internal/infra/repository/pig_trade_repository.go @@ -12,9 +12,9 @@ import ( // PigPurchaseListOptions 定义了查询猪只采购记录时的可选参数 type PigPurchaseListOptions struct { - PigBatchID *uint + PigBatchID *uint32 Supplier *string - OperatorID *uint + OperatorID *uint32 StartTime *time.Time // 基于 purchase_date 字段 EndTime *time.Time // 基于 purchase_date 字段 OrderBy string // 例如 "purchase_date desc" @@ -22,9 +22,9 @@ type PigPurchaseListOptions struct { // PigSaleListOptions 定义了查询猪只销售记录时的可选参数 type PigSaleListOptions struct { - PigBatchID *uint + PigBatchID *uint32 Buyer *string - OperatorID *uint + OperatorID *uint32 StartTime *time.Time // 基于 sale_date 字段 EndTime *time.Time // 基于 sale_date 字段 OrderBy string // 例如 "sale_date desc" diff --git a/internal/infra/repository/pig_transfer_log_repository.go b/internal/infra/repository/pig_transfer_log_repository.go index ca0d5e5..2a5fc85 100644 --- a/internal/infra/repository/pig_transfer_log_repository.go +++ b/internal/infra/repository/pig_transfer_log_repository.go @@ -12,10 +12,10 @@ import ( // PigTransferLogListOptions 定义了查询猪只迁移日志时的可选参数 type PigTransferLogListOptions struct { - PigBatchID *uint - PenID *uint + PigBatchID *uint32 + PenID *uint32 TransferType *models.PigTransferType // 迁移类型 - OperatorID *uint + OperatorID *uint32 CorrelationID *string StartTime *time.Time // 基于 transfer_time 字段 EndTime *time.Time // 基于 transfer_time 字段 @@ -28,7 +28,7 @@ type PigTransferLogRepository interface { CreatePigTransferLog(ctx context.Context, tx *gorm.DB, log *models.PigTransferLog) error // GetLogsForPenSince 获取指定猪栏自特定时间点以来的所有迁移日志,按时间倒序排列。 - GetLogsForPenSince(ctx context.Context, tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error) + GetLogsForPenSince(ctx context.Context, tx *gorm.DB, penID uint32, since time.Time) ([]*models.PigTransferLog, error) // ListPigTransferLogs 支持分页和过滤的猪只迁移日志列表查询 ListPigTransferLogs(ctx context.Context, opts PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error) @@ -52,7 +52,7 @@ func (r *gormPigTransferLogRepository) CreatePigTransferLog(ctx context.Context, } // GetLogsForPenSince 实现了获取猪栏自特定时间点以来所有迁移日志的逻辑。 -func (r *gormPigTransferLogRepository) GetLogsForPenSince(ctx context.Context, tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error) { +func (r *gormPigTransferLogRepository) GetLogsForPenSince(ctx context.Context, tx *gorm.DB, penID uint32, since time.Time) ([]*models.PigTransferLog, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLogsForPenSince") var logs []*models.PigTransferLog err := tx.WithContext(repoCtx).Where("pen_id = ? AND transfer_time >= ?", penID, since).Order("transfer_time DESC").Find(&logs).Error diff --git a/internal/infra/repository/plan_repository.go b/internal/infra/repository/plan_repository.go index 6ce886b..9f01f57 100644 --- a/internal/infra/repository/plan_repository.go +++ b/internal/infra/repository/plan_repository.go @@ -44,11 +44,13 @@ type PlanRepository interface { // ListPlans 获取计划列表,支持过滤和分页 ListPlans(ctx context.Context, opts ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) // GetBasicPlanByID 根据ID获取计划的基本信息,不包含子计划和任务详情 - GetBasicPlanByID(ctx context.Context, id uint) (*models.Plan, error) + GetBasicPlanByID(ctx context.Context, id uint32) (*models.Plan, error) // GetPlanByID 根据ID获取计划,包含子计划和任务详情 - GetPlanByID(ctx context.Context, id uint) (*models.Plan, error) + GetPlanByID(ctx context.Context, id uint32) (*models.Plan, error) // GetPlansByIDs 根据ID列表获取计划,不包含子计划和任务详情 - GetPlansByIDs(ctx context.Context, ids []uint) ([]models.Plan, error) + GetPlansByIDs(ctx context.Context, ids []uint32) ([]models.Plan, error) + // GetSystemPlanByName 根据计划名称获取系统计划,包含子计划和任务详情 + GetSystemPlanByName(ctx context.Context, planName models.PlanName) (*models.Plan, error) // CreatePlan 创建一个新的计划 CreatePlan(ctx context.Context, plan *models.Plan) error // CreatePlanTx 在指定事务中创建一个新的计划 @@ -58,36 +60,35 @@ type PlanRepository interface { // UpdatePlan 更新计划的所有字段 UpdatePlan(ctx context.Context, plan *models.Plan) error // UpdatePlanStatus 更新指定计划的状态 - UpdatePlanStatus(ctx context.Context, id uint, status models.PlanStatus) error + UpdatePlanStatus(ctx context.Context, id uint32, status models.PlanStatus) error // UpdateExecuteCount 更新指定计划的执行计数 - UpdateExecuteCount(ctx context.Context, id uint, count uint) error + UpdateExecuteCount(ctx context.Context, id uint32, count uint32) error // DeletePlan 根据ID删除计划,同时删除其关联的任务(非子任务)或子计划关联 - DeletePlan(ctx context.Context, id uint) error + DeletePlan(ctx context.Context, id uint32) error // FlattenPlanTasks 递归展开计划,返回按执行顺序排列的所有任务列表 - FlattenPlanTasks(ctx context.Context, planID uint) ([]models.Task, error) + FlattenPlanTasks(ctx context.Context, planID uint32) ([]models.Task, error) // DeleteTask 根据ID删除任务 DeleteTask(ctx context.Context, id int) error + // FindTaskByID 根据ID获取任务的基本信息 + FindTaskByID(ctx context.Context, id int) (*models.Task, error) // FindPlanAnalysisTaskByParamsPlanID 根据Parameters中的ParamsPlanID字段值查找TaskPlanAnalysis类型的Task - FindPlanAnalysisTaskByParamsPlanID(ctx context.Context, paramsPlanID uint) (*models.Task, error) + FindPlanAnalysisTaskByParamsPlanID(ctx context.Context, paramsPlanID uint32) (*models.Task, error) // FindRunnablePlans 获取所有应执行的计划 FindRunnablePlans(ctx context.Context) ([]*models.Plan, error) // FindInactivePlans 获取所有已禁用或已停止的计划 FindInactivePlans(ctx context.Context) ([]*models.Plan, error) - // FindPlanAnalysisTaskByPlanID 根据 PlanID 找到其关联的 'plan_analysis' 任务 - FindPlanAnalysisTaskByPlanID(ctx context.Context, planID uint) (*models.Task, error) - + FindPlanAnalysisTaskByPlanID(ctx context.Context, planID uint32) (*models.Task, error) // CreatePlanAnalysisTask 创建一个 plan_analysis 类型的任务并返回它 CreatePlanAnalysisTask(ctx context.Context, plan *models.Plan) (*models.Task, error) - // FindPlansWithPendingTasks 查找所有正在执行的计划 FindPlansWithPendingTasks(ctx context.Context) ([]*models.Plan, error) - // StopPlanTransactionally 停止一个计划的执行,包括更新状态、移除待执行任务和更新执行日志 - StopPlanTransactionally(ctx context.Context, planID uint) error - + StopPlanTransactionally(ctx context.Context, planID uint32) error // UpdatePlanStateAfterExecution 更新计划执行后的状态(计数和状态) - UpdatePlanStateAfterExecution(ctx context.Context, planID uint, newCount uint, newStatus models.PlanStatus) error + UpdatePlanStateAfterExecution(ctx context.Context, planID uint32, newCount uint32, newStatus models.PlanStatus) error + // ListTasksByDeviceID 根据设备ID获取关联任务列表 + ListTasksByDeviceID(ctx context.Context, deviceID uint32) ([]*models.Task, error) } // gormPlanRepository 是 PlanRepository 的 GORM 实现 @@ -139,7 +140,7 @@ func (r *gormPlanRepository) ListPlans(ctx context.Context, opts ListPlansOption } // GetBasicPlanByID 根据ID获取计划的基本信息,不包含子计划和任务详情 -func (r *gormPlanRepository) GetBasicPlanByID(ctx context.Context, id uint) (*models.Plan, error) { +func (r *gormPlanRepository) GetBasicPlanByID(ctx context.Context, id uint32) (*models.Plan, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetBasicPlanByID") var plan models.Plan // GORM 默认不会加载关联,除非使用 Preload,所以直接 First 即可满足要求 @@ -151,7 +152,7 @@ func (r *gormPlanRepository) GetBasicPlanByID(ctx context.Context, id uint) (*mo } // GetPlansByIDs 根据ID列表获取计划,不包含子计划和任务详情 -func (r *gormPlanRepository) GetPlansByIDs(ctx context.Context, ids []uint) ([]models.Plan, error) { +func (r *gormPlanRepository) GetPlansByIDs(ctx context.Context, ids []uint32) ([]models.Plan, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPlansByIDs") var plans []models.Plan if len(ids) == 0 { @@ -164,8 +165,25 @@ func (r *gormPlanRepository) GetPlansByIDs(ctx context.Context, ids []uint) ([]m return plans, nil } +// GetSystemPlanByName 根据计划名称获取系统计划,包含子计划和任务详情, 系统任务不该有重名情况, 所以可以这么查询 +func (r *gormPlanRepository) GetSystemPlanByName(ctx context.Context, planName models.PlanName) (*models.Plan, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "GetSystemPlanByName") + var plan models.Plan + // 首先只查询计划的基本信息,获取其ID + err := r.db.WithContext(repoCtx).Select("id").Where("name = ? AND plan_type = ?", planName, models.PlanTypeSystem).First(&plan).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil // 未找到系统计划不是错误 + } + if err != nil { + return nil, fmt.Errorf("查询系统计划 '%s' 失败: %w", planName, err) + } + + // 如果找到了计划ID,则复用 GetPlanByID 来获取完整的计划详情 + return r.GetPlanByID(repoCtx, plan.ID) +} + // GetPlanByID 根据ID获取计划,包含子计划和任务详情 -func (r *gormPlanRepository) GetPlanByID(ctx context.Context, id uint) (*models.Plan, error) { +func (r *gormPlanRepository) GetPlanByID(ctx context.Context, id uint32) (*models.Plan, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPlanByID") var plan models.Plan @@ -236,7 +254,7 @@ func (r *gormPlanRepository) CreatePlanTx(ctx context.Context, tx *gorm.DB, plan // 如果是子计划类型,验证所有子计划是否存在且ID不为0 if plan.ContentType == models.PlanContentTypeSubPlans { - childIDsToValidate := make(map[uint]bool) + childIDsToValidate := make(map[uint32]bool) for _, subPlanLink := range plan.SubPlans { if subPlanLink.ChildPlanID == 0 { return ErrSubPlanIDIsZeroOnCreate @@ -244,7 +262,7 @@ func (r *gormPlanRepository) CreatePlanTx(ctx context.Context, tx *gorm.DB, plan childIDsToValidate[subPlanLink.ChildPlanID] = true } - var ids []uint + var ids []uint32 for id := range childIDsToValidate { ids = append(ids, id) } @@ -316,8 +334,8 @@ func (r *gormPlanRepository) validatePlanTree(ctx context.Context, tx *gorm.DB, } // 2. 递归验证所有子节点,并检测循环引用 - allIDs := make(map[uint]bool) - recursionStack := make(map[uint]bool) + allIDs := make(map[uint32]bool) + recursionStack := make(map[uint32]bool) if err := validateNodeAndDetectCycles(plan, allIDs, recursionStack); err != nil { return err } @@ -328,7 +346,7 @@ func (r *gormPlanRepository) validatePlanTree(ctx context.Context, tx *gorm.DB, } // 4. 一次性数据库存在性校验 - var idsToCheck []uint + var idsToCheck []uint32 for id := range allIDs { idsToCheck = append(idsToCheck, id) } @@ -346,7 +364,7 @@ func (r *gormPlanRepository) validatePlanTree(ctx context.Context, tx *gorm.DB, } // validateNodeAndDetectCycles 递归地验证节点有效性并检测循环引用 -func validateNodeAndDetectCycles(plan *models.Plan, allIDs, recursionStack map[uint]bool) error { +func validateNodeAndDetectCycles(plan *models.Plan, allIDs, recursionStack map[uint32]bool) error { if plan == nil { return nil } @@ -451,7 +469,7 @@ func (r *gormPlanRepository) reconcileSubPlans(ctx context.Context, tx *gorm.DB, return err } - existingLinkMap := make(map[uint]bool) + existingLinkMap := make(map[uint32]bool) for _, link := range existingLinks { existingLinkMap[link.ID] = true } @@ -471,7 +489,7 @@ func (r *gormPlanRepository) reconcileSubPlans(ctx context.Context, tx *gorm.DB, } } - var linksToDelete []uint + var linksToDelete []uint32 for id := range existingLinkMap { linksToDelete = append(linksToDelete, id) } @@ -485,7 +503,7 @@ func (r *gormPlanRepository) reconcileSubPlans(ctx context.Context, tx *gorm.DB, } // DeletePlan 根据ID删除计划,同时删除其关联的任务(非子任务)或子计划关联 -func (r *gormPlanRepository) DeletePlan(ctx context.Context, id uint) error { +func (r *gormPlanRepository) DeletePlan(ctx context.Context, id uint32) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePlan") return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { // 1. 检查该计划是否是其他计划的子计划 @@ -527,7 +545,7 @@ func (r *gormPlanRepository) DeletePlan(ctx context.Context, id uint) error { } // FlattenPlanTasks 递归展开计划,返回按执行顺序排列的所有任务列表 -func (r *gormPlanRepository) FlattenPlanTasks(ctx context.Context, planID uint) ([]models.Task, error) { +func (r *gormPlanRepository) FlattenPlanTasks(ctx context.Context, planID uint32) ([]models.Task, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "FlattenPlanTasks") plan, err := r.GetPlanByID(repoCtx, planID) if err != nil { @@ -627,7 +645,7 @@ func (r *gormPlanRepository) deleteTasksTx(ctx context.Context, tx *gorm.DB, ids } // FindPlanAnalysisTaskByParamsPlanID 根据Parameters中的ParamsPlanID字段值查找TaskPlanAnalysis类型的Task -func (r *gormPlanRepository) FindPlanAnalysisTaskByParamsPlanID(ctx context.Context, paramsPlanID uint) (*models.Task, error) { +func (r *gormPlanRepository) FindPlanAnalysisTaskByParamsPlanID(ctx context.Context, paramsPlanID uint32) (*models.Task, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPlanAnalysisTaskByParamsPlanID") return r.findPlanAnalysisTask(repoCtx, r.db, paramsPlanID) } @@ -704,7 +722,7 @@ func (r *gormPlanRepository) FindInactivePlans(ctx context.Context) ([]*models.P // findPlanAnalysisTask 是一个内部使用的、更高效的查找方法 // 关键修改:通过查询 parameters JSON 字段来查找 -func (r *gormPlanRepository) findPlanAnalysisTask(ctx context.Context, tx *gorm.DB, planID uint) (*models.Task, error) { +func (r *gormPlanRepository) findPlanAnalysisTask(ctx context.Context, tx *gorm.DB, planID uint32) (*models.Task, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "findPlanAnalysisTask") var task models.Task err := tx.WithContext(repoCtx).Where("type = ? AND parameters->>'plan_id' = ?", models.TaskPlanAnalysis, fmt.Sprintf("%d", planID)).First(&task).Error @@ -716,7 +734,7 @@ func (r *gormPlanRepository) findPlanAnalysisTask(ctx context.Context, tx *gorm. // FindPlanAnalysisTaskByPlanID 是暴露给外部的公共方法 // 关键修改:通过查询 parameters JSON 字段来查找 -func (r *gormPlanRepository) FindPlanAnalysisTaskByPlanID(ctx context.Context, planID uint) (*models.Task, error) { +func (r *gormPlanRepository) FindPlanAnalysisTaskByPlanID(ctx context.Context, planID uint32) (*models.Task, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPlanAnalysisTaskByPlanID") return r.findPlanAnalysisTask(repoCtx, r.db, planID) } @@ -751,7 +769,7 @@ func (r *gormPlanRepository) FindPlansWithPendingTasks(ctx context.Context) ([]* } // StopPlanTransactionally 停止一个计划的执行,包括更新状态、移除待执行任务和更新执行日志。 -func (r *gormPlanRepository) StopPlanTransactionally(ctx context.Context, planID uint) error { +func (r *gormPlanRepository) StopPlanTransactionally(ctx context.Context, planID uint32) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "StopPlanTransactionally") return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { // 使用事务创建新的仓库实例,确保所有操作都在同一个事务中 @@ -782,7 +800,7 @@ func (r *gormPlanRepository) StopPlanTransactionally(ctx context.Context, planID } if len(taskLogs) > 0 { - var taskLogIDs []uint + var taskLogIDs []uint32 for _, tl := range taskLogs { taskLogIDs = append(taskLogIDs, tl.ID) } @@ -799,7 +817,7 @@ func (r *gormPlanRepository) StopPlanTransactionally(ctx context.Context, planID } if len(pendingTasks) > 0 { - var pendingTaskIDs []uint + var pendingTaskIDs []uint32 for _, pt := range pendingTasks { pendingTaskIDs = append(pendingTaskIDs, pt.ID) } @@ -819,7 +837,7 @@ func (r *gormPlanRepository) StopPlanTransactionally(ctx context.Context, planID } // UpdatePlanStatus 更新指定计划的状态 -func (r *gormPlanRepository) UpdatePlanStatus(ctx context.Context, id uint, status models.PlanStatus) error { +func (r *gormPlanRepository) UpdatePlanStatus(ctx context.Context, id uint32, status models.PlanStatus) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanStatus") result := r.db.WithContext(repoCtx).Model(&models.Plan{}).Where("id = ?", id).Update("status", status) if result.Error != nil { @@ -831,7 +849,7 @@ func (r *gormPlanRepository) UpdatePlanStatus(ctx context.Context, id uint, stat return nil } -func (r *gormPlanRepository) UpdatePlanStateAfterExecution(ctx context.Context, planID uint, newCount uint, newStatus models.PlanStatus) error { +func (r *gormPlanRepository) UpdatePlanStateAfterExecution(ctx context.Context, planID uint32, newCount uint32, newStatus models.PlanStatus) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanStateAfterExecution") return r.db.WithContext(repoCtx).Model(&models.Plan{}).Where("id = ?", planID).Updates(map[string]interface{}{ "execute_count": newCount, @@ -840,7 +858,7 @@ func (r *gormPlanRepository) UpdatePlanStateAfterExecution(ctx context.Context, } // UpdateExecuteCount 更新指定计划的执行计数 -func (r *gormPlanRepository) UpdateExecuteCount(ctx context.Context, id uint, count uint) error { +func (r *gormPlanRepository) UpdateExecuteCount(ctx context.Context, id uint32, count uint32) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateExecuteCount") result := r.db.WithContext(repoCtx).Model(&models.Plan{}).Where("id = ?", id).Update("execute_count", count) if result.Error != nil { @@ -851,3 +869,30 @@ func (r *gormPlanRepository) UpdateExecuteCount(ctx context.Context, id uint, co } return nil } + +// FindTaskByID 根据ID获取任务的基本信息 +func (r *gormPlanRepository) FindTaskByID(ctx context.Context, id int) (*models.Task, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "FindTaskByID") + var task models.Task + result := r.db.WithContext(repoCtx).First(&task, id) + if result.Error != nil { + return nil, result.Error + } + return &task, nil +} + +func (r *gormPlanRepository) ListTasksByDeviceID(ctx context.Context, deviceID uint32) ([]*models.Task, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "ListTasksByDeviceID") + tasks := []*models.Task{} + // 使用 Joins 方法来连接 tasks 表和 device_tasks 关联表, + // 然后通过 Where 子句筛选出与指定 deviceID 关联的所有任务。 + err := r.db.WithContext(repoCtx).Joins("JOIN device_tasks ON device_tasks.task_id = tasks.id"). + Where("device_tasks.device_id = ?", deviceID). + Find(&tasks).Error + + if err != nil { + return nil, err + } + + return tasks, nil +} diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index ba6a757..2795167 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -12,7 +12,7 @@ import ( // RawMaterialPurchaseListOptions 定义了查询原料采购记录时的可选参数 type RawMaterialPurchaseListOptions struct { - RawMaterialID *uint + RawMaterialID *uint32 Supplier *string StartTime *time.Time // 基于 purchase_date 字段 EndTime *time.Time // 基于 purchase_date 字段 @@ -21,9 +21,9 @@ type RawMaterialPurchaseListOptions struct { // RawMaterialStockLogListOptions 定义了查询原料库存日志时的可选参数 type RawMaterialStockLogListOptions struct { - RawMaterialID *uint + RawMaterialID *uint32 SourceType *models.StockLogSourceType - SourceID *uint + SourceID *uint32 StartTime *time.Time // 基于 happened_at 字段 EndTime *time.Time // 基于 happened_at 字段 OrderBy string // 例如 "happened_at asc" @@ -31,9 +31,9 @@ type RawMaterialStockLogListOptions struct { // FeedUsageRecordListOptions 定义了查询饲料使用记录时的可选参数 type FeedUsageRecordListOptions struct { - PenID *uint - FeedFormulaID *uint - OperatorID *uint + PenID *uint32 + FeedFormulaID *uint32 + OperatorID *uint32 StartTime *time.Time // 基于 recorded_at 字段 EndTime *time.Time // 基于 recorded_at 字段 OrderBy string // 例如 "recorded_at asc" diff --git a/internal/infra/repository/sensor_data_repository.go b/internal/infra/repository/sensor_data_repository.go index 70c8810..88f1aea 100644 --- a/internal/infra/repository/sensor_data_repository.go +++ b/internal/infra/repository/sensor_data_repository.go @@ -12,7 +12,7 @@ import ( // SensorDataListOptions 定义了查询传感器数据列表时的可选参数 type SensorDataListOptions struct { - DeviceID *uint + DeviceID *uint32 SensorType *models.SensorType StartTime *time.Time EndTime *time.Time @@ -22,7 +22,7 @@ type SensorDataListOptions struct { // SensorDataRepository 定义了与传感器数据相关的数据库操作接口。 type SensorDataRepository interface { Create(ctx context.Context, sensorData *models.SensorData) error - GetLatestSensorDataByDeviceIDAndSensorType(ctx context.Context, deviceID uint, sensorType models.SensorType) (*models.SensorData, error) + GetLatestSensorDataByDeviceIDAndSensorType(ctx context.Context, deviceID uint32, sensorType models.SensorType) (*models.SensorData, error) // List 支持分页和过滤的列表查询 List(ctx context.Context, opts SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) } @@ -45,7 +45,7 @@ func (r *gormSensorDataRepository) Create(ctx context.Context, sensorData *model } // GetLatestSensorDataByDeviceIDAndSensorType 根据设备ID和传感器类型查询最新的传感器数据。 -func (r *gormSensorDataRepository) GetLatestSensorDataByDeviceIDAndSensorType(ctx context.Context, deviceID uint, sensorType models.SensorType) (*models.SensorData, error) { +func (r *gormSensorDataRepository) GetLatestSensorDataByDeviceIDAndSensorType(ctx context.Context, deviceID uint32, sensorType models.SensorType) (*models.SensorData, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLatestSensorDataByDeviceIDAndSensorType") var sensorData models.SensorData // 增加一个时间范围来缩小查询范围, 从而加快查找速度, 当使用时序数据库时时间范围可以让数据库忽略时间靠前的分片 diff --git a/internal/infra/repository/user_action_log_repository.go b/internal/infra/repository/user_action_log_repository.go index e0102e9..fcd4f32 100644 --- a/internal/infra/repository/user_action_log_repository.go +++ b/internal/infra/repository/user_action_log_repository.go @@ -12,7 +12,7 @@ import ( // UserActionLogListOptions 定义了查询用户操作日志时的可选参数 type UserActionLogListOptions struct { - UserID *uint + UserID *uint32 Username *string ActionType *string Status *models.AuditStatus diff --git a/internal/infra/repository/user_repository.go b/internal/infra/repository/user_repository.go index 97b8bb2..66f6c87 100644 --- a/internal/infra/repository/user_repository.go +++ b/internal/infra/repository/user_repository.go @@ -15,7 +15,7 @@ import ( type UserRepository interface { Create(ctx context.Context, user *models.User) error FindByUsername(ctx context.Context, username string) (*models.User, error) - FindByID(ctx context.Context, id uint) (*models.User, error) + FindByID(ctx context.Context, id uint32) (*models.User, error) FindUserForLogin(ctx context.Context, identifier string) (*models.User, error) FindAll(ctx context.Context) ([]*models.User, error) } @@ -66,7 +66,7 @@ func (r *gormUserRepository) FindUserForLogin(ctx context.Context, identifier st } // FindByID 根据 ID 查找用户 -func (r *gormUserRepository) FindByID(ctx context.Context, id uint) (*models.User, error) { +func (r *gormUserRepository) FindByID(ctx context.Context, id uint32) (*models.User, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID") var user models.User if err := r.db.WithContext(repoCtx).First(&user, id).Error; err != nil { diff --git a/internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go b/internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go index bd51957..a302e48 100644 --- a/internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go +++ b/internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go @@ -406,13 +406,13 @@ func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(ctx context.Con logger.Infof("成功解析采集响应 (CorrelationID: %s),包含 %d 个值。", correlationID, len(collectResp.Values)) // 3. 查找区域主控 (注意:LoRa Mesh 的 SourceAddr 对应于区域主控的 NetworkID) - regionalController, err := t.areaControllerRepo.FindByNetworkID(loraCtx, msg.SourceAddr) + areaController, err := t.areaControllerRepo.FindByNetworkID(loraCtx, msg.SourceAddr) if err != nil { logger.Errorf("处理上行消息失败:无法通过源地址 '%s' 找到区域主控设备: %v", msg.SourceAddr, err) return } - if err := regionalController.SelfCheck(); err != nil { - logger.Errorf("处理上行消息失败:区域主控 %v(ID: %d) 未通过自检: %v", regionalController.Name, regionalController.ID, err) + if err := areaController.SelfCheck(); err != nil { + logger.Errorf("处理上行消息失败:区域主控 %v(ID: %d) 未通过自检: %v", areaController.Name, areaController.ID, err) return } @@ -474,7 +474,7 @@ func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(ctx context.Con } valueDescriptor := valueDescriptors[0] - parsedValue := float64(rawSensorValue)*valueDescriptor.Multiplier + valueDescriptor.Offset + parsedValue := rawSensorValue*valueDescriptor.Multiplier + valueDescriptor.Offset var dataToRecord interface{} switch valueDescriptor.Type { @@ -486,10 +486,10 @@ func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(ctx context.Con dataToRecord = models.WeightData{WeightKilograms: parsedValue} default: logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type) - dataToRecord = map[string]float64{"value": parsedValue} + dataToRecord = map[string]float32{"value": parsedValue} } - t.recordSensorData(loraCtx, regionalController.ID, dev.ID, time.Now(), valueDescriptor.Type, dataToRecord) + t.recordSensorData(loraCtx, areaController.ID, dev.ID, time.Now(), valueDescriptor.Type, dataToRecord) logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 原始值=%f, 解析值=%.2f", dev.ID, valueDescriptor.Type, rawSensorValue, parsedValue) } @@ -502,7 +502,7 @@ func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(ctx context.Con } // recordSensorData 是一个通用方法,用于将传感器数据存入数据库。 -func (t *LoRaMeshUartPassthroughTransport) recordSensorData(ctx context.Context, regionalControllerID uint, sensorDeviceID uint, eventTime time.Time, sensorType models.SensorType, data interface{}) { +func (t *LoRaMeshUartPassthroughTransport) recordSensorData(ctx context.Context, areaControllerID uint32, sensorDeviceID uint32, eventTime time.Time, sensorType models.SensorType, data interface{}) { loraCtx, logger := logs.Trace(ctx, t.ctx, "recordSensorData") jsonData, err := json.Marshal(data) @@ -512,11 +512,11 @@ func (t *LoRaMeshUartPassthroughTransport) recordSensorData(ctx context.Context, } sensorData := &models.SensorData{ - Time: eventTime, - DeviceID: sensorDeviceID, - RegionalControllerID: regionalControllerID, - SensorType: sensorType, - Data: datatypes.JSON(jsonData), + Time: eventTime, + DeviceID: sensorDeviceID, + AreaControllerID: areaControllerID, + SensorType: sensorType, + Data: datatypes.JSON(jsonData), } if err := t.sensorDataRepo.Create(loraCtx, sensorData); err != nil { diff --git a/internal/infra/utils/command_generater/modbus_rtu.go b/internal/infra/utils/command_generater/modbus_rtu.go index f9afbe1..457f095 100644 --- a/internal/infra/utils/command_generater/modbus_rtu.go +++ b/internal/infra/utils/command_generater/modbus_rtu.go @@ -3,37 +3,8 @@ package command_generater import ( "encoding/binary" "fmt" -) -// 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 + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" ) // GenerateModbusRTUReadCommand 生成Modbus RTU读取指令 @@ -52,7 +23,7 @@ const ( // // []byte: 完整的Modbus RTU指令字节切片。 // error: 如果参数无效或生成过程中出现错误,则返回错误信息。 -func GenerateModbusRTUReadCommand(slaveAddress uint8, functionCode ModbusFunctionCode, startAddress uint16, quantity uint16) ([]byte, error) { +func GenerateModbusRTUReadCommand(slaveAddress uint8, functionCode models.ModbusFunctionCode, startAddress uint16, quantity uint16) ([]byte, error) { // 1. 校验输入参数 if slaveAddress == 0 || slaveAddress > 247 { return nil, fmt.Errorf("从站地址无效: %d, 必须在1-247之间", slaveAddress) @@ -60,9 +31,9 @@ func GenerateModbusRTUReadCommand(slaveAddress uint8, functionCode ModbusFunctio // 校验功能码是否为读取类型 switch functionCode { - case ReadCoils, ReadDiscreteInputs, ReadHoldingRegisters, ReadInputRegisters: + case models.ReadCoils, models.ReadDiscreteInputs, models.ReadHoldingRegisters, models.ReadInputRegisters: // 这些是支持的读取功能码 - case WriteSingleCoil, WriteSingleRegister, WriteMultipleCoils, WriteMultipleRegisters: + case models.WriteSingleCoil, models.WriteSingleRegister, models.WriteMultipleCoils, models.WriteMultipleRegisters: return nil, fmt.Errorf("功能码 %X 是写入操作,请使用 GenerateModbusRTUWriteCoilCommand 或其他写入函数", functionCode) default: return nil, fmt.Errorf("不支持的功能码: %X", functionCode) @@ -130,7 +101,7 @@ func GenerateModbusRTUSwitchCommand(slaveAddress uint8, coilAddress uint16, onOf // 2. 构建PDU (协议数据单元) for WriteSingleCoil (0x05) // PDU结构: 功能码 (1字节) + 线圈地址 (2字节) + 写入值 (2字节) pdu := make([]byte, 5) - pdu[0] = byte(WriteSingleCoil) + pdu[0] = byte(models.WriteSingleCoil) // Modbus协议中,地址和值都是大端字节序 (高位在前) binary.BigEndian.PutUint16(pdu[1:3], coilAddress) binary.BigEndian.PutUint16(pdu[3:5], writeValue) diff --git a/internal/infra/utils/token/token_service.go b/internal/infra/utils/token/token_service.go index 69dcbcc..8311e2e 100644 --- a/internal/infra/utils/token/token_service.go +++ b/internal/infra/utils/token/token_service.go @@ -9,13 +9,13 @@ import ( // Claims 定义了 JWT 的声明结构 type Claims struct { - UserID uint `json:"user_id"` + UserID uint32 `json:"user_id"` jwt.RegisteredClaims } // Generator 定义了 token 操作的接口 type Generator interface { - GenerateToken(userID uint) (string, error) + GenerateToken(userID uint32) (string, error) ParseToken(tokenString string) (*Claims, error) } @@ -30,7 +30,7 @@ func NewTokenGenerator(secret []byte) Generator { } // GenerateToken 生成一个新的 JWT token -func (s *tokenGenerator) GenerateToken(userID uint) (string, error) { +func (s *tokenGenerator) GenerateToken(userID uint32) (string, error) { nowTime := time.Now() expireTime := nowTime.Add(24 * time.Hour) // Token 有效期为 24 小时 diff --git a/project_structure.txt b/project_structure.txt index d7b9dd9..a01e2b7 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -10,26 +10,29 @@ RELAY_API.md TODO-List.txt config.example.yml config.yml -design/archive/2025-11-3-verification-before-device-deletion/add_get_device_id_configs_to_task.md -design/archive/2025-11-3-verification-before-device-deletion/check_before_device_deletion.md -design/archive/2025-11-3-verification-before-device-deletion/device_task_association_maintenance.md -design/archive/2025-11-3-verification-before-device-deletion/device_task_many_to_many_design.md -design/archive/2025-11-3-verification-before-device-deletion/index.md -design/archive/2025-11-3-verification-before-device-deletion/plan_service_refactor.md -design/archive/2025-11-3-verification-before-device-deletion/plan_service_refactor_to_domain.md -design/archive/2025-11-3-verification-before-device-deletion/refactor_deletion_check.md -design/archive/2025-11-3-verification-before-device-deletion/refactor_id_conversion.md -design/provide-logger-with-mothed/implementation.md -design/provide-logger-with-mothed/index.md -design/provide-logger-with-mothed/task-api.md -design/provide-logger-with-mothed/task-controller.md -design/provide-logger-with-mothed/task-domain.md -design/provide-logger-with-mothed/task-infra.md -design/provide-logger-with-mothed/task-list.md -design/provide-logger-with-mothed/task-middleware.md -design/provide-logger-with-mothed/task-repository.md -design/provide-logger-with-mothed/task-service.md -design/provide-logger-with-mothed/task-webhook.md +design/archive/2025-11-03-verification-before-device-deletion/add_get_device_id_configs_to_task.md +design/archive/2025-11-03-verification-before-device-deletion/check_before_device_deletion.md +design/archive/2025-11-03-verification-before-device-deletion/device_task_association_maintenance.md +design/archive/2025-11-03-verification-before-device-deletion/device_task_many_to_many_design.md +design/archive/2025-11-03-verification-before-device-deletion/index.md +design/archive/2025-11-03-verification-before-device-deletion/plan_service_refactor.md +design/archive/2025-11-03-verification-before-device-deletion/plan_service_refactor_to_domain.md +design/archive/2025-11-03-verification-before-device-deletion/refactor_deletion_check.md +design/archive/2025-11-03-verification-before-device-deletion/refactor_id_conversion.md +design/archive/2025-11-05-provide-logger-with-mothed/implementation.md +design/archive/2025-11-05-provide-logger-with-mothed/index.md +design/archive/2025-11-05-provide-logger-with-mothed/task-api.md +design/archive/2025-11-05-provide-logger-with-mothed/task-controller.md +design/archive/2025-11-05-provide-logger-with-mothed/task-domain.md +design/archive/2025-11-05-provide-logger-with-mothed/task-infra.md +design/archive/2025-11-05-provide-logger-with-mothed/task-list.md +design/archive/2025-11-05-provide-logger-with-mothed/task-middleware.md +design/archive/2025-11-05-provide-logger-with-mothed/task-repository.md +design/archive/2025-11-05-provide-logger-with-mothed/task-service.md +design/archive/2025-11-05-provide-logger-with-mothed/task-webhook.md +design/archive/2025-11-06-health-check-routing/index.md +design/archive/2025-11-06-system-plan-continuously-triggered/index.md +design/exceeding-threshold-alarm/index.md docs/docs.go docs/swagger.json docs/swagger.yaml @@ -37,8 +40,10 @@ go.mod go.sum internal/app/api/api.go internal/app/api/router.go +internal/app/controller/alarm/threshold_alarm_controller.go internal/app/controller/auth_utils.go internal/app/controller/device/device_controller.go +internal/app/controller/health/health_controller.go internal/app/controller/management/controller_helpers.go internal/app/controller/management/pig_batch_controller.go internal/app/controller/management/pig_batch_health_controller.go @@ -49,8 +54,11 @@ internal/app/controller/monitor/monitor_controller.go internal/app/controller/plan/plan_controller.go internal/app/controller/response.go internal/app/controller/user/user_controller.go +internal/app/dto/alarm_converter.go +internal/app/dto/alarm_dto.go internal/app/dto/device_converter.go internal/app/dto/device_dto.go +internal/app/dto/dto.go internal/app/dto/monitor_converter.go internal/app/dto/monitor_dto.go internal/app/dto/notification_converter.go @@ -69,6 +77,7 @@ internal/app/service/pig_batch_service.go internal/app/service/pig_farm_service.go internal/app/service/pig_service.go internal/app/service/plan_service.go +internal/app/service/threshold_alarm_service.go internal/app/service/user_service.go internal/app/webhook/chirp_stack.go internal/app/webhook/chirp_stack_types.go @@ -77,6 +86,7 @@ internal/app/webhook/transport.go internal/core/application.go internal/core/component_initializers.go internal/core/data_initializer.go +internal/domain/alarm/alarm_service.go internal/domain/device/device_service.go internal/domain/device/general_device_service.go internal/domain/notify/notify.go @@ -92,7 +102,10 @@ internal/domain/plan/analysis_plan_task_manager.go internal/domain/plan/plan_execution_manager.go internal/domain/plan/plan_service.go internal/domain/plan/task.go +internal/domain/task/alarm_notification_task.go +internal/domain/task/area_threshold_check_task.go internal/domain/task/delay_task.go +internal/domain/task/device_threshold_check_task.go internal/domain/task/full_collection_task.go internal/domain/task/release_feed_weight_task.go internal/domain/task/task.go @@ -102,6 +115,7 @@ internal/infra/database/storage.go internal/infra/logs/context.go internal/infra/logs/encoder.go internal/infra/logs/logs.go +internal/infra/models/alarm.go internal/infra/models/device.go internal/infra/models/device_template.go internal/infra/models/execution.go @@ -123,6 +137,7 @@ internal/infra/notify/log_notifier.go internal/infra/notify/notify.go internal/infra/notify/smtp.go internal/infra/notify/wechat.go +internal/infra/repository/alarm_repository.go internal/infra/repository/area_controller_repository.go internal/infra/repository/device_command_log_repository.go internal/infra/repository/device_repository.go