Merge pull request 'issue_56' (#58) from issue_56 into main

Reviewed-on: #58
This commit is contained in:
2025-11-05 23:56:50 +08:00
105 changed files with 4702 additions and 2421 deletions

View File

@@ -0,0 +1,89 @@
# 实现方案:纯 Context 驱动的调用链追踪
本方案旨在提供一个绝对安全、符合 Go 语言习惯且对业务代码侵入性最小的调用链追踪方案。其核心思想是:**调用链信息完全由标准的 `context.Context` 承载,并通过 `logs` 包提供的一系列无状态包级函数进行原子化、安全的操作。**
### 1. 核心设计原则
- **`Context` 是唯一载体**: 所有的调用链信息,包括组件名(对象名)和函数名(方法名),都只存储在标准的 `context.Context` 中。我们不再定义任何自定义的 `Context` 接口,以保证最大的兼容性。
- **纯粹的包级函数**: `logs` 包将提供一系列纯粹的、无状态的包级函数,作为与 `Context` 交互的唯一 API。这些函数负责向 `Context` 中添加信息,或从 `Context` 中生成 `Logger`
- **无状态的 `Logger`**: `Logger` 对象本身不再携带任何调用链信息。它在被 `logs.GetLogger(ctx)` 生成时,才被一次性地赋予包含完整调用链的配置。
### 2. 实现细节
#### a. Context 中存储的数据
`context.Context` 将通过 `context.WithValue` 在幕后存储两种核心信息,这两种信息都使用 `logs` 包内部的私有 `key` 类型,以避免与其他包的键冲突。
- **组件名 (`compNameKey`)**: 用于存储一个字符串,表示当前上下文环境属于哪个组件(例如 `\"组件1\"`)。
- **调用链 (`chainKey`)**: 用于存储一个字符串切片 (`[]string`),记录了从请求开始到当前位置的完整调用路径(例如 `[\"组件2.Create\", \"组件1.Create\"]`)。
#### b. `logs` 包提供的核心 API
`logs` 包需要对外提供以下四个核心的包级函数,以提供不同粒度的灵活性:
1. **`logs.AddCompName(ctx, compName) context.Context`**
- **职责**: 将一个组件名(对象名)存入 `Context`,并返回一个包含该信息的新 `Context`。这通常在依赖注入时完成,用于创建组件的“身份名牌” `selfCtx`
- **实现细节**: 该函数接收一个 `context.Context` 和一个 `compName` 字符串。它内部使用 `context.WithValue`,以私有的 `compNameKey` 为键,将 `compName` 字符串存入 `Context`,然后返回这个全新的 `Context`
2. **`logs.AddFuncName(upstreamCtx, selfCtx, funcName) context.Context`**
- **职责**: 这是构建调用链的核心原子操作。它智能地合并上游的调用链和当前组件的信息,生成并返回一个包含更新后调用链和当前组件名的 **新 `Context`**。此函数用于只需要传递调用链而不需要立即打印日志的场景。
- **实现细节**:
1. 函数接收 `upstreamCtx`, `selfCtx`, `funcName`
2. **获取上游调用链**: 从 `upstreamCtx` 中,通过 `chainKey` 读取出已经存在的调用链(`oldChain []string`)。
3. **获取当前组件名**: 从 `selfCtx` 中,通过 `compNameKey` 读取出当前组件的名称(`compName string`)。
4. **构建新节点**: 将 `compName``funcName` 拼接成一个新节点(例如 `\"组件2.Create\"`)。
5. **生成新调用链**: 将这个新节点追加到 `oldChain` 的末尾,形成 `newChain []string`
6. **创建新的 Context**:
- 使用 `context.WithValue`,以 `chainKey` 为键,将 `newChain` 存入一个新的 `Context`,我们称之为 `tmpCtx`
- 接着,**(关键修正)** 基于 `tmpCtx`,再次调用 `context.WithValue`,以 `compNameKey` 为键,将从 `selfCtx` 中获取的 `compName` 存入,得到最终的 `newCtx`。这确保了传向下游的 `Context` 正确地标识了当前组件。
7. **返回**: 返回 `newCtx`
3. **`logs.GetLogger(ctx) *Logger`**
- **职责**: 从 `Context` 中生成最终的、可用于打印的 `Logger` 实例。
- **实现细节**:
1. 函数接收一个 `context.Context`
2. 它从 `ctx` 中,通过 `chainKey` 读取出完整的调用链 `[]string`
3. 如果调用链不存在或为空,它就生成一个不带 `trace` 字段的普通 `Logger` 实例并返回。
4. 如果调用链存在,它就用 `->` 符号将切片中的所有节点拼接成一个完整的 `trace` 字符串。
5. 最后,它创建一个**一次性**的 `Logger` 实例,将这个 `trace` 字符串和底层的 `zap` 配置传给它,然后返回这个准备就绪的 `Logger`
4. **`logs.Trace(upstreamCtx, selfCtx, funcName) (context.Context, *Logger)`**
- **职责**: 作为 `AddFuncName``GetLogger` 的便捷封装,一步到位地完成调用链构建和 `Logger` 生成。用于需要立即打印日志的场景。
- **实现细节**:
1. 内部调用 `newCtx := logs.AddFuncName(upstreamCtx, selfCtx, funcName)`
2. 内部调用 `logger := logs.GetLogger(newCtx)`
3. 返回 `newCtx``logger`
### 3. 最终使用模式
#### a. 依赖注入阶段
(保持不变)在应用启动和组装依赖时,我们不再注入 `Logger` 对象,而是为每个组件创建一个包含其自身名称的专属 `Context`,并将这个 `Context` 注入到组件实例中。
- **流程**:
1. 在依赖注入的根源处,创建一个全局的、初始的 `context.Background()`
2. 对于需要被追踪的组件(例如 `组件1`),调用 `logs.AddCompName(ctx, \"组件1\")` 来创建一个 `ctxForC1`
3. 在创建 `组件1` 的实例时,将这个 `ctxForC1` 作为其成员变量(例如 `selfCtx`)保存起来。这个 `selfCtx` 就成了 `组件1` 的“身份名牌”。
#### b. 请求处理阶段
开发者可以根据需求灵活选择 API。
- **场景一:需要立即打印日志 (推荐)**:
1. 在方法入口处,立即调用 `ctx, logger := logs.Trace(upstreamCtx, z.selfCtx, \"Create\")`
2. 使用这个 `logger` 打印日志:`logger.Info(\"创建组件2\")`
3. 当需要调用下游方法时,将返回的 **新 `Context` (`ctx`)** 传递下去。
- **场景二:只需要传递调用链**:
1. 在方法入口处,调用 `ctx := logs.AddFuncName(upstreamCtx, z.selfCtx, \"Create\")`
2. 这个方法本身不打印日志,但在调用下游方法时,将这个包含了更新后调用链的 **新 `Context` (`ctx`)** 传递下去。
- **场景三:在方法中间打印日志**:
1. 一个方法可能在执行了一部分逻辑后才需要打印日志。
2. `ctx := logs.AddFuncName(upstreamCtx, z.selfCtx, \"Create\")` // 先在入口更新调用链
3. // ... 执行一些业务逻辑 ...
4. `logger := logs.GetLogger(ctx)` // 在需要时,基于更新后的 ctx 获取 logger
5. `logger.Info(\"业务逻辑执行到一半\")`
6. // ... 继续执行并传递 ctx ...

View File

@@ -0,0 +1,21 @@
# 需求
为日志系统提供一种机制,能够记录和追踪跨越不同对象和方法的调用链,以增强日志的可读性和可观测性。
## issue
http://git.huangwc.com/pig/pig-farm-controller/issues/56
## 描述
### 初始问题
在复杂的业务流程中,一个请求可能会流经多个服务或控制器。为了追踪一个完整的操作流程,开发者不得不在每个方法的日志中手动添加上下文信息(如 `actionType`),这非常繁琐且容易出错。
### 期望的演进
1. **初步设想**: 提供一个 `logger.With()` 方法,调用后返回一个携带预设信息的日志记录器副本,后续日志自动附带此信息。
2. **解决上下文覆盖**: 简单的 `With("action", "some_action")` 会导致同名字段被覆盖。因此,期望能将调用链信息存储在数组中,并在打印时拼接,如 `action: "action1·action2"`
3. **最终目标 (可观测性)**: 进一步区分“对象”和“方法”,构建出更具表现力的调用链。例如,当 `userController``Create` 方法调用 `userService``Create` 方法时,日志应能清晰地展示 `trace: "userController.Create->userService.Create"` 这样的调用关系。
## 实现方案
[实现方案](./implementation.md)

View File

@@ -0,0 +1,9 @@
- **`internal/app/api/api.go` (`API`)**:
- [x] 修改 `NewAPI` 函数,移除 `logger` 参数,改为接收 `ctx context.Context`
- [x] 移除 `API` 结构体中的 `logger` 成员,改为保存 `Ctx context.Context`
- [x] 为 `API` 组件本身创建 `Ctx``Ctx := logs.AddCompName(ctx, 'API')`,并传递给所有 `Controller`
的构造函数。
- [x] 改造 `Start` 方法,从 `a.Ctx` 获取 `logger` 实例进行日志记录。
- [x] 改造 `Stop` 方法,从 `a.Ctx` 获取 `logger` 实例进行日志记录。
- **`internal/app/api/router.go`
- [x] 改造 `setupRoutes` 方法,从 `a.Ctx` 获取 `logger` 实例进行日志记录。

View File

@@ -0,0 +1,113 @@
- **`internal/app/controller/user/user_controller.go` (`user.Controller`)**
- **结构体改造**:
- [x] 移除 `Controller` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `Ctx context.Context` 成员。
- **构造函数改造 (`NewController`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 将这个 `Ctx` 赋值给 `Controller` 结构体的 `Ctx` 成员。
- **公共方法改造 (`CreateUser`, `Login`, `SendTestNotification`)**:
- [x] 在每个方法入口处,从 `echo.Context` 中提取 `request.Context()` 作为 `upstreamCtx`
- [x] 使用 `newCtx, logger := logs.Trace(upstreamCtx, c.Ctx, "MethodName")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有对 `c.logger.Errorf``c.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `c.userService` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/controller/device/device_controller.go` (`device.Controller`)**
- **结构体改造**:
- [x] 移除 `Controller` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `Ctx context.Context` 成员。
- **构造函数改造 (`NewController`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 将这个 `Ctx` 赋值给 `Controller` 结构体的 `Ctx` 成员。
- **公共方法改造 (`CreateDevice`, `GetDevice`, `ListDevices`, `UpdateDevice`, `DeleteDevice`, `ManualControl`,
`CreateAreaController`, `GetAreaController`, `ListAreaControllers`, `UpdateAreaController`,
`DeleteAreaController`, `CreateDeviceTemplate`, `GetDeviceTemplate`, `ListDeviceTemplates`,
`UpdateDeviceTemplate`, `DeleteDeviceTemplate`)**:
- [x] 在每个方法入口处,从 `echo.Context` 中提取 `request.Context()` 作为 `upstreamCtx`
- [x] 使用 `newCtx, logger := logs.Trace(upstreamCtx, c.Ctx, "MethodName")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有对 `c.logger.Errorf`, `c.logger.Warnf`, `c.logger.Infof` 的调用替换为 `logger.Errorf`,
`logger.Warnf`, `logger.Infof`
- [x] 确保所有对 `c.deviceService` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/controller/plan/plan_controller.go` (`plan.Controller`)**
- **结构体改造**:
- [x] 移除 `Controller` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `Ctx context.Context` 成员。
- **构造函数改造 (`NewController`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 将这个 `Ctx` 赋值给 `Controller` 结构体的 `Ctx` 成员。
- **公共方法改造 (`CreatePlan`, `GetPlan`, `ListPlans`, `UpdatePlan`, `DeletePlan`, `StartPlan`, `StopPlan`)**:
- [x] 在每个方法入口处,从 `echo.Context` 中提取 `request.Context()` 作为 `upstreamCtx`
- [x] 使用 `newCtx, logger := logs.Trace(upstreamCtx, c.Ctx, "MethodName")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有对 `c.logger.Errorf``c.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `c.planService` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/controller/management/pig_farm_controller.go` (`management.PigFarmController`)**
- **结构体改造**:
- [x] 移除 `PigFarmController` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `Ctx context.Context` 成员。
- **构造函数改造 (`NewPigFarmController`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 将这个 `Ctx` 赋值给 `PigFarmController` 结构体的 `Ctx` 成员。
- **公共方法改造 (`CreatePigHouse`, `GetPigHouse`, `ListPigHouses`, `UpdatePigHouse`, `DeletePigHouse`, `CreatePen`,
`GetPen`, `ListPens`, `UpdatePen`, `DeletePen`, `UpdatePenStatus`)**:
- [x] 在每个方法入口处,从 `echo.Context` 中提取 `request.Context()` 作为 `upstreamCtx`
- [x] 使用 `newCtx, logger := logs.Trace(upstreamCtx, c.Ctx, "MethodName")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有对 `c.logger.Errorf``c.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `c.service` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/controller/management/pig_batch_controller.go` (`management.PigBatchController`)**
- **结构体改造**:
- [x] 移除 `PigBatchController` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `Ctx context.Context` 成员。
- **构造函数改造 (`NewPigBatchController`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 将这个 `Ctx` 赋值给 `PigBatchController` 结构体的 `Ctx` 成员。
- **公共方法改造 (
所有方法,例如 `CreatePigBatch`, `GetPigBatch`, `UpdatePigBatch`, `DeletePigBatch`, `ListPigBatches`,
`AssignEmptyPensToBatch`, `ReclassifyPenToNewBatch`, `RemoveEmptyPenFromBatch`, `MovePigsIntoPen`)**:
- [x] 这些方法通过调用 `controller_helpers.go` 中的 `handleAPIRequest...` 系列函数来处理请求。
- [x] 确保传递给 `handleAPIRequest...` 系列函数的 `serviceExecutor` 匿名函数,其签名与 `controller_helpers.go`
改造后期望的签名一致(即接收 `context.Context` 作为第一个参数)。
- [x] 在 `serviceExecutor` 匿名函数内部,将接收到的 `context.Context` 传递给 `c.service` 的相应方法。
- **`internal/app/controller/management/controller_helpers.go`**
- [x] 修改 `mapAndSendError` 函数中的 `c.logger.Errorf` 调用,改为从 `echo.Context` 中提取 `context.Context`,并使用
`logs.Trace``logs.GetLogger` 获取 `logger` 实例进行日志记录。
- [x] 检查其他函数是否直接或间接依赖 `*PigBatchController``logger` 成员,并进行相应改造。
- **`internal/app/controller/management/pig_batch_trade_controller.go` (`management.PigBatchController`)**
- [x] 修改 `NewController` 函数,移除 `logger` 参数,改为接收 `Ctx context.Context`
- [x] 移除 `Controller` 结构体中的 `logger` 成员,改为保存 `Ctx`
- [x] **所有公共方法**: 接收 `echo.Context` 的方法,需要从中提取 `request.Context()` 作为 `upstreamCtx`
- **`internal/app/controller/management/pig_batch_health_controller.go` (`management.PigBatchController`)**
- [x] 修改 `NewController` 函数,移除 `logger` 参数,改为接收 `Ctx context.Context`
- [x] 移除 `Controller` 结构体中的 `logger` 成员,改为保存 `Ctx`
- [x] **所有公共方法**: 接收 `echo.Context` 的方法,需要从中提取 `request.Context()` 作为 `upstreamCtx`
- **`internal/app/controller/management/pig_batch_transfer_controller.go` (`management.PigBatchController`)**
- [x] 修改 `NewController` 函数,移除 `logger` 参数,改为接收 `Ctx context.Context`
- [x] 移除 `Controller` 结构体中的 `logger` 成员,改为保存 `Ctx`
- [x] **所有公共方法**: 接收 `echo.Context` 的方法,需要从中提取 `request.Context()` 作为 `upstreamCtx`
- **`internal/app/controller/monitor/monitor_controller.go` (`monitor.Controller`)**
- **结构体改造**:
- [x] 移除 `Controller` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `Ctx context.Context` 成员。
- **构造函数改造 (`NewController`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 将这个 `Ctx` 赋值给 `Controller` 结构体的 `Ctx` 成员。
- **公共方法改造 (
所有方法,例如 `ListSensorData`, `ListDeviceCommandLogs`, `ListPlanExecutionLogs`, `ListTaskExecutionLogs`,
`ListPendingCollections`, `ListUserActionLogs`, `ListRawMaterialPurchases`, `ListRawMaterialStockLogs`,
`ListFeedUsageRecords`, `ListMedicationLogs`, `ListPigBatchLogs`, `ListWeighingBatches`, `ListWeighingRecords`,
`ListPigTransferLogs`, `ListPigSickLogs`, `ListPigPurchases`, `ListPigSales`, `ListNotifications`)**:
- [x] 在每个方法入口处,从 `echo.Context` 中提取 `request.Context()` 作为 `upstreamCtx`
- [x] 使用 `newCtx, logger := logs.Trace(upstreamCtx, c.Ctx, actionType)` 获取新的 `context.Context`
`logger` 实例(`actionType` 为方法内部定义的常量)。
- [x] 将所有对 `c.logger.Errorf`, `c.logger.Warnf`, `c.logger.Infof` 的调用替换为 `logger.Errorf`,
`logger.Warnf`, `logger.Infof`
- [x] 确保所有对 `c.monitorService` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/controller/response.go`**
- [x] 修改 `SendSuccessWithAudit``SendErrorWithAudit` 函数签名,使其接收 `ctx context.Context` 作为第一个参数。
- [x] 在 `setAuditDetails` 函数中,如果需要,从传入的 `context.Context` 中提取调用链信息,并将其添加到审计信息中。
- **`internal/app/controller/auth_utils.go`**
- [x] 检查 `GetOperatorIDFromContext``GetOperatorFromContext` 函数,确保它们能够正确地从 `echo.Context` 中获取
`request.Context()`,并从中提取用户信息。

View File

@@ -0,0 +1,272 @@
- **`internal/domain/audit/service.go` (`audit.Service`)**
- **结构体改造**:
- [x] 移除 `service` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `audit.Service` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "AuditService")`
- [x] 将这个 `selfCtx` 赋值给 `service` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`LogAction`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "LogAction")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有对 `s.logger.Warnw``s.logger.Errorw` 的调用替换为 `logger.Warnw``logger.Errorw`
- [x] 确保所有对 `s.userActionLogRepository.Create` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/device/general_device_service.go` (`device.Service`)**
- **结构体改造**:
- [x] 移除 `GeneralDeviceService` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGeneralDeviceService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `GeneralDeviceService` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "GeneralDeviceService")`
- [x] 将这个 `selfCtx` 赋值给 `GeneralDeviceService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Switch`, `Collect`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, g.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `g.logger.Errorf`, `g.logger.Infof`, `g.logger.Warnf`, `g.logger.Debugf`, `g.logger.DPanicf`
的调用替换为 `logger.Errorf`, `logger.Infof`, `logger.Warnf`, `logger.Debugf`, `logger.DPanicf`
- [x] 确保所有对 `g.deviceRepo`, `g.deviceCommandLogRepo`, `g.pendingCollectionRepo`, `g.comm` 等依赖的调用都将
`newCtx` 作为第一个参数传递。
- **`internal/domain/notify/notify.go` (`domain_notify.Service` - `failoverService` 实现)**
- **结构体改造**:
- [x] 移除 `failoverService` 结构体中的 `log *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewFailoverService`)**:
- [x] 修改函数签名,移除 `log *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `failoverService` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "FailoverService")`
- [x] 将这个 `selfCtx` 赋值给 `failoverService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`SendBatchAlarm`, `BroadcastAlarm`, `SendTestMessage`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.log.Infow`, `s.log.Errorw`, `s.log.Warnw` 的调用替换为 `logger.Infow`, `logger.Errorw`,
`logger.Warnw`
- [x] 确保所有对 `s.userRepo`, `s.primaryNotifier.Send`, `s.notifiers`, `s.notificationRepo` 等依赖的调用都将
`newCtx` 作为第一个参数传递。
- **内部辅助方法改造 (`sendAlarmToUser`, `recordNotificationAttempt`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.log.Errorw`, `s.log.Infow`, `s.log.Warnw` 的调用替换为 `logger.Errorw`, `logger.Infow`,
`logger.Warnw`
- [x] 确保所有对 `s.userRepo`, `s.primaryNotifier.Send`, `s.notifiers`, `s.notificationRepo` 等依赖的调用都将
`newCtx` 作为第一个参数传递。
- **`internal/domain/pig/pen_transfer_manager.go` (`pig.PigPenTransferManager`)**
- **结构体改造**:
- [x] 移除 `pigPenTransferManager` 结构体中可能存在的 `logger *logs.Logger` 成员(如果未来添加)。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPigPenTransferManager`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `pigPenTransferManager` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigPenTransferManager")`
- [x] 将这个 `selfCtx` 赋值给 `pigPenTransferManager` 结构体的 `selfCtx` 成员。
- **公共方法改造 (
所有方法,例如 `LogTransfer`, `GetPenByID`, `GetPensByBatchID`, `UpdatePenFields`, `GetCurrentPigsInPen`,
`GetTotalPigsInPensForBatchTx`, `ReleasePen`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数(在 `tx *gorm.DB` 之前)。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf``s.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `s.penRepo`, `s.logRepo`, `s.pigBatchRepo` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/pig/pig_trade_manager.go` (`pig.PigTradeManager`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPigTradeManager`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `pigTradeManager` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigTradeManager")`
- [x] 将这个 `selfCtx` 赋值给 `pigTradeManager` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`SellPig`, `BuyPig`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数(在 `tx *gorm.DB` 之前)。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 确保所有对 `s.tradeRepo` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/pig/pig_sick_manager.go` (`pig.SickPigManager`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewSickPigManager`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `sickPigManager` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "SickPigManager")`
- [x] 将这个 `selfCtx` 赋值给 `sickPigManager` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`ProcessSickPigLog`, `GetCurrentSickPigCount`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数(在 `tx *gorm.DB` 之前)。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 确保所有对 `s.sickLogRepo`, `s.medicationLogRepo` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/pig/pig_batch_service.go` (`pig.PigBatchService`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPigBatchService`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `pigBatchService` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigBatchService")`
- [x] 将这个 `selfCtx` 赋值给 `pigBatchService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (
所有方法,例如 `CreatePigBatch`, `GetPigBatch`, `UpdatePigBatch`, `DeletePigBatch`, `ListPigBatches`,
`AssignEmptyPensToBatch`, `MovePigsIntoPen`, `ReclassifyPenToNewBatch`, `RemoveEmptyPenFromBatch`,
`GetCurrentPigQuantity`, `GetCurrentPigsInPen`, `GetTotalPigsInPensForBatch`, `UpdatePigBatchQuantity`,
`SellPigs`, `BuyPigs`, `TransferPigsAcrossBatches`, `TransferPigsWithinBatch`, `RecordSickPigs`,
`RecordSickPigRecovery`, `RecordSickPigDeath`, `RecordSickPigCull`, `RecordDeath`, `RecordCull`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 确保所有对 `s.pigBatchRepo`, `s.pigBatchLogRepo`, `s.uow`, `s.transferSvc`, `s.tradeSvc`, `s.sickSvc`
等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/plan/analysis_plan_task_manager.go` (`plan.AnalysisPlanTaskManager`)**
- **结构体改造**:
- [x] 移除 `analysisPlanTaskManagerImpl` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewAnalysisPlanTaskManager`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `analysisPlanTaskManagerImpl` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "AnalysisPlanTaskManager")`
- [x] 将这个 `selfCtx` 赋值给 `analysisPlanTaskManagerImpl` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Refresh`, `CreateOrUpdateTrigger`, `EnsureAnalysisTaskDefinition`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, m.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `m.logger.Info`, `m.logger.Errorf`, `m.logger.Warnf` 的调用替换为 `logger.Info`,
`logger.Errorf`, `logger.Warnf`
- [x] 确保所有对 `m.planRepo`, `m.pendingTaskRepo`, `m.executionLogRepo` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **内部辅助方法改造 (`getRefreshData`, `cleanupInvalidTasks`, `addOrUpdateTriggers`, `createTriggerTask`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, m.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `m.logger.Info`, `m.logger.Errorf`, `m.logger.Warnf` 的调用替换为 `logger.Info`,
`logger.Errorf`, `logger.Warnf`
- [x] 确保所有对 `m.planRepo`, `m.pendingTaskRepo`, `m.executionLogRepo` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/plan/plan_execution_manager.go` (`plan.ExecutionManager`)**
- **结构体改造**:
- [x] 移除 `planExecutionManagerImpl` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPlanExecutionManager`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `planExecutionManagerImpl` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PlanExecutionManager")`
- [x] 将这个 `selfCtx` 赋值给 `planExecutionManagerImpl` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Start`, `Stop`)**:
- [x] 在 `Start` 方法中,使用 `newCtx, logger := logs.Trace(context.Background(), s.selfCtx, "Start")` 获取
`logger` 实例进行日志记录。
- [x] 在 `Stop` 方法中,使用 `newCtx, logger := logs.Trace(context.Background(), s.selfCtx, "Stop")` 获取
`logger` 实例进行日志记录。
- **内部辅助方法改造 (`run`, `claimAndSubmit`, `handleRequeue`, `processTask`, `runTask`, `analysisPlan`,
`updateTaskExecutionLogStatus`, `handlePlanTermination`, `handlePlanCompletion`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数(如果方法内部需要传递上下文)。
- [x] 在每个方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf`, `s.logger.Warnf`, `s.logger.Infof`, `s.logger.DPanicf` 的调用替换为
`logger.Errorf`, `logger.Warnf`, `logger.Infof`, `logger.DPanicf`
- [x] 确保所有对 `s.pendingTaskRepo`, `s.executionLogRepo`, `s.deviceRepo`, `s.sensorDataRepo`, `s.planRepo`,
`s.analysisPlanTaskManager`, `s.taskFactory`, `s.deviceService` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/plan/plan_service.go` (`plan.Service`)**
- **结构体改造**:
- [x] 移除 `planServiceImpl` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPlanService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `planServiceImpl` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PlanService")`
- [x] 将这个 `selfCtx` 赋值给 `planServiceImpl` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Start`, `Stop`, `RefreshPlanTriggers`, `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`,
`DeletePlan`, `StartPlan`, `StopPlan`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf`, `s.logger.Infof`, `s.logger.Warnf` 的调用替换为 `logger.Errorf`,
`logger.Infof`, `logger.Warnf`
- [x] 确保所有对 `s.executionManager`, `s.taskManager`, `s.planRepo`, `s.deviceRepo`, `s.unitOfWork`,
`s.taskFactory` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/task/task.go` (`task.TaskFactory`)**
- **结构体改造**:
- [x] 移除 `taskFactory` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewTaskFactory`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `taskFactory` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "TaskFactory")`
- [x] 将这个 `selfCtx` 赋值给 `taskFactory` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Production`, `CreateTaskFromModel`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法内部,使用 `newCtx, logger := logs.Trace(ctx, t.selfCtx, "MethodName")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有对 `t.logger.Panicf` 的调用替换为 `logger.Panicf`
- [x] 将所有对 `NewDelayTask`, `NewReleaseFeedWeightTask`, `NewFullCollectionTask` 的调用,传递 `newCtx`
- **`internal/domain/task/release_feed_weight_task.go`**
- **结构体改造**:
- [x] 移除 `ReleaseFeedWeightTask` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewReleaseFeedWeightTask`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `ReleaseFeedWeightTask` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "ReleaseFeedWeightTask")`
- [x] 将这个 `selfCtx` 赋值给 `ReleaseFeedWeightTask` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Execute`, `OnFailure`, `ResolveDeviceIDs`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, r.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `r.logger.Infof`, `r.logger.Errorf`, `r.logger.Warnf`, `r.logger.Debugf` 的调用替换为
`logger.Infof`, `logger.Errorf`, `logger.Warnf`, `logger.Debugf`
- [x] 确保所有对 `r.deviceRepo`, `r.sensorDataRepo`, `r.feedPort` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **内部辅助方法改造 (`getNowWeight`, `parseParameters`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, r.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `r.logger.Errorf`, `r.logger.Warnf` 的调用替换为 `logger.Errorf`, `logger.Warnf`
- [x] 确保所有对 `r.sensorDataRepo`, `r.deviceRepo` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/task/full_collection_task.go`**
- **结构体改造**:
- [x] 移除 `FullCollectionTask` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewFullCollectionTask`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `FullCollectionTask` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "FullCollectionTask")`
- [x] 将这个 `selfCtx` 赋值给 `FullCollectionTask` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Execute`, `OnFailure`, `ResolveDeviceIDs`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, t.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `t.logger.Infow`, `t.logger.Errorw` 的调用替换为 `logger.Infow`, `logger.Errorw`
- [x] 确保所有对 `t.deviceRepo`, `t.deviceService` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/domain/task/delay_task.go`**
- **结构体改造**:
- [x] 移除 `DelayTask` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewDelayTask`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `DelayTask` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "DelayTask")`
- [x] 将这个 `selfCtx` 赋值给 `DelayTask` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Execute`, `OnFailure`, `ResolveDeviceIDs`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, d.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `d.logger.Infof``d.logger.Errorf` 的调用替换为 `logger.Infof``logger.Errorf`
- **内部辅助方法改造 (`parseParameters`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, d.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `d.logger.Errorf` 的调用替换为 `logger.Errorf`
- **`internal/domain/token/token_service.go` (`token.Service`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewTokenService`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `tokenService` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "TokenService")`
- [x] 将这个 `selfCtx` 赋值给 `tokenService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`GenerateToken`, `ParseToken`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.*` 的调用替换为 `logger.*` (如果存在)。

View File

@@ -0,0 +1,163 @@
- **`internal/infra/database/storage.go` (`database.Storage`)**
- **工厂函数改造 (`NewStorage`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,将接收到的 `ctx` 作为第一个参数传递给 `NewPostgresStorage` 函数。
- **`internal/infra/database/postgres.go`**
- **结构体改造**:
- [x] 移除 `PostgresStorage` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPostgresStorage`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `PostgresStorage` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PostgresStorage")`
- [x] 将这个 `selfCtx` 赋值给 `PostgresStorage` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Connect`, `Disconnect`, `Migrate`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, ps.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `ps.logger.Info`, `ps.logger.Errorw`, `ps.logger.Debugw` 的调用替换为 `logger.Info`,
`logger.Errorw`, `logger.Debugw`
- [x] 在 `Connect` 方法中,调用 `logs.NewGormLogger` 时,将 `newCtx` 传递给它。
- [x] 在 `Connect` 方法中,调用 `gorm.Open` 时,使用
`ps.db, err = gorm.Open(postgres.Open(ps.connectionString), &gorm.Config{Logger: logs.NewGormLogger(newCtx)})`
- [x] 在 `Migrate` 方法中,确保所有对 `ps.db.AutoMigrate``ps.db.Exec` 的调用都使用 `newCtx`
- **内部辅助方法改造 (`setupTimescaleDB`, `creatingHyperTable`, `applyCompressionPolicies`, `creatingIndex`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在每个方法入口处,使用 `newCtx, logger := logs.Trace(ctx, ps.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `ps.logger.Info`, `ps.logger.Errorw`, `ps.logger.Debugw`, `ps.logger.Warnw`, `ps.logger.Debug`
的调用替换为 `logger.Info`, `logger.Errorw`, `logger.Debugw`, `logger.Warnw`, `logger.Debug`
- [x] 确保所有对 `ps.db.Exec` 的调用都使用 `newCtx`
- **`internal/infra/notify/log_notifier.go` (`notify.LogNotifier`)**
- **结构体改造**:
- [x] 移除 `logNotifier` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewLogNotifier`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `logNotifier` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "LogNotifier")`
- [x] 将这个 `selfCtx` 赋值给 `logNotifier` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Send`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, l.selfCtx, "Send")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有对 `l.logger.Infow` 的调用替换为 `logger.Infow`
- **公共方法 (`Type`)**:
- [x] 此方法不涉及日志或上下文传递,无需改造。
- **`internal/infra/notify/lark.go` (`notify.LarkNotifier`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewLarkNotifier`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `larkNotifier` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "LarkNotifier")`
- [x] 将这个 `selfCtx` 赋值给 `larkNotifier` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Send`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, l.selfCtx, "Send")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有内部的错误日志(例如 `fmt.Errorf` 返回的错误,以及 `getAccessToken` 中的错误)通过 `logger.Errorf`
记录。
- **内部辅助方法改造 (`getAccessToken`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, l.selfCtx, "getAccessToken")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有内部的错误日志通过 `logger.Errorf` 记录。
- **`internal/infra/notify/smtp.go` (`notify.SMTPNotifier`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewSMTPNotifier`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `smtpNotifier` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "SMTPNotifier")`
- [x] 将这个 `selfCtx` 赋值给 `smtpNotifier` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Send`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "Send")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有内部的错误日志(例如 `fmt.Errorf` 返回的错误)通过 `logger.Errorf` 记录。
- **`internal/infra/notify/wechat.go` (`notify.WechatNotifier`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewWechatNotifier`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `wechatNotifier` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "WechatNotifier")`
- [x] 将这个 `selfCtx` 赋值给 `wechatNotifier` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Send`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, w.selfCtx, "Send")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有内部的错误日志(例如 `fmt.Errorf` 返回的错误,以及 `getAccessToken` 中的错误)通过 `logger.Errorf`
记录。
- **内部辅助方法改造 (`getAccessToken`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, w.selfCtx, "getAccessToken")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有内部的错误日志通过 `logger.Errorf` 记录。
- **`internal/infra/transport/lora/chirp_stack.go` (`lora.ChirpStackTransport`)**
- **结构体改造**:
- [x] 移除 `ChirpStackTransport` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewChirpStackTransport`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `ChirpStackTransport` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "ChirpStackTransport")`
- [x] 将这个 `selfCtx` 赋值给 `ChirpStackTransport` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Send`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, c.selfCtx, "Send")` 获取新的 `context.Context`
`logger` 实例。
- [x] 将所有对 `c.logger.Errorf``c.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 在调用 `c.client.DeviceService.DeviceServiceEnqueue` 时,确保将 `newCtx` 传递给
`params.WithContext(newCtx)`,以便 ChirpStack 客户端内部的 HTTP 请求也能携带上下文。
- **`internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go` (`lora.LoRaMeshUartPassthroughTransport`)**
- **结构体改造**:
- [x] 移除 `LoRaMeshUartPassthroughTransport` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewLoRaMeshUartPassthroughTransport`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `LoRaMeshUartPassthroughTransport` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "LoRaMeshUartPassthroughTransport")`
- [x] 将这个 `selfCtx` 赋值给 `LoRaMeshUartPassthroughTransport` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Listen`, `Send`, `Stop`)**:
- [x] 修改 `Listen` 方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 修改 `Send` 方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 修改 `Stop` 方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, t.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `t.logger.Info`, `t.logger.Errorf` 的调用替换为 `logger.Info`, `logger.Errorf`
- [x] 在 `Send` 方法中,确保 `t.sendChan <- req` 传递的 `req` 包含了 `newCtx`
- **内部辅助方法改造 (`workerLoop`, `runIdleState`, `runReceivingState`, `executeSend`, `handleFrame`,
`handleUpstreamMessage`, `recordSensorData`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在每个方法入口处,使用 `newCtx, logger := logs.Trace(ctx, t.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `t.logger.Info`, `t.logger.Errorf`, `t.logger.Warnf`, `t.logger.Infof`, `t.logger.Debugf`
的调用替换为 `logger.Info`, `logger.Errorf`, `logger.Warnf`, `logger.Infof`, `logger.Debugf`
- [x] 确保所有对 `t.port`, `t.areaControllerRepo`, `t.pendingCollectionRepo`, `t.deviceRepo`,
`t.sensorDataRepo` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/infra/transport/lora/placeholder_transport.go` (`lora.PlaceholderTransport`)**
- **结构体改造**:
- [x] 移除 `PlaceholderTransport` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPlaceholderTransport`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `PlaceholderTransport` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PlaceholderTransport")`
- [x] 将这个 `selfCtx` 赋值给 `PlaceholderTransport` 结构体的 `selfCtx` 成员。
- [x] 使用 `newCtx, logger := logs.Trace(ctx, selfCtx, "NewPlaceholderTransport")` 获取 `logger` 实例,并替换
`logger.Info` 调用。
- **公共方法改造 (`Listen`, `Stop`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, p.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `p.logger.Warnf` 的调用替换为 `logger.Warnf`

View File

@@ -0,0 +1,82 @@
### 重构任务清单:纯 Context 驱动的调用链追踪
---
#### 1. 核心改造 (`logs` 包)
- **`internal/infra/logs/logs.go` (`logs.Logger` - 内部实现,非注入)**
- **核心包级函数实现 (根据 `implementation.md` 描述)**:
- [x] 实现 `AddCompName(ctx context.Context, compName string) context.Context`
- [x] 实现
`AddFuncName(upstreamCtx context.Context, selfCtx context.Context, funcName string) context.Context`
- [x] 实现 `GetLogger(ctx context.Context) *Logger`
- [x] 实现
`Trace(upstreamCtx context.Context, selfCtx context.Context, funcName string) (context.Context, *Logger)`
- **`Logger` 结构体改造**:
- [x] 无
- **`GormLogger` 改造**:
- [x] 修改 `GormLogger.Info` 方法,从传入的 `ctx` 中获取 `logger` 实例,并使用该实例进行日志记录。
- [x] 修改 `GormLogger.Warn` 方法,从传入的 `ctx` 中获取 `logger` 实例,并使用该实例进行日志记录。
- [x] 修改 `GormLogger.Error` 方法,从传入的 `ctx` 中获取 `logger` 实例,并使用该实例进行日志记录。
- [x] 修改 `GormLogger.Trace` 方法,从传入的 `ctx` 中获取 `logger` 实例,并使用该实例进行日志记录。特别是
`With(fields...)` 的调用需要调整。
---
#### 2. 依赖注入与结构体改造
- **`internal/core/application.go`**:
- [x] 移除 `Application` 结构体中的 `Logger *logs.Logger` 成员。
- [x] 修改 `NewApplication` 函数,使其不再创建 `logger`,而是创建根 `context.Background()`
- [x] 调整 `NewApplication`,将根 `context` 传递给 `initInfrastructure`, `initDomainServices`, `initAppServices`
`api.NewAPI`
- [x] 移除 `Application` 结构体中所有对 `app.Logger` 的直接调用,改为通过 `context` 获取 `Logger`
- **`internal/core/component_initializers.go`**:
- [x] **修改所有组件结构体定义**: 遍历所有相关组件Controllers, Services, Repositories 等),将其结构体中的
`logger *logs.Logger` 成员变量替换为 `selfCtx context.Context`
- [x] **重构所有 `init...` 函数**:
- 移除所有 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- 在每个 `init...` 函数内部,为即将创建的组件生成其专属的 `selfCtx`。例如:
`selfCtx := logs.AddCompName(ctx, 'ComponentName')`
- 将这个 `selfCtx` 注入到组件的构造函数中,并由组件保存为 `selfCtx` 成员。
---
#### 3. 全局方法签名改造:传递 `context.Context`
**以下所有列出的组件,其所有公共方法都需要进行签名改造,将 `ctx context.Context` 作为第一个参数。**
##### 3.1. API 层
[api](./task-api.md)
[controller](./task-controller.md)
[middleware](./task-middleware.md)
[webhook](./task-webhook.md)
##### 3.2. 应用层 (Application Services)
[service](./task-service.md)
##### 3.3. 领域层 (Domain Services)
[domain](./task-domain.md)
##### 3.4. 基础设施层 (Infrastructure)
[repository](./task-repository.md)
[infra-other](./task-infra.md)
---
#### 4. 日志调用点及方法内部逻辑改造
- [x] **遍历所有业务方法** (针对上述所有列出的组件的公共方法):
- [x] **定位旧日志**: 搜索所有对旧 `z.logger.*` 成员的调用。
- [x] **改造方法入口** (对于非 Controller 方法):
1. 在方法开始处,使用作为参数传入的 `ctx` (作为 `upstreamCtx`) 和组件自身持有的 `z.selfCtx`,调用 `logs.Trace`
- `newCtx, logger := logs.Trace(ctx, z.selfCtx, 'MethodName')`
2. 将所有旧的 `z.logger.*(...)` 调用,替换为使用新获取的 `logger.*(...)`
- [x] **改造下游调用**:
1. 在方法内部,当需要调用其他组件的方法时(如下游服务),**必须传递 `newCtx`**。
- `err := z.downstreamService.DoSomething(newCtx, data)`

View File

@@ -0,0 +1,25 @@
- **`internal/app/middleware/auth.go`**
- **中间件函数改造 (`AuthMiddleware`)**:
- [x] 在 `AuthMiddleware` 返回的 `echo.HandlerFunc` 内部,获取 `echo.Context``request.Context()` 作为
`upstreamCtx`
- [x] 使用 `newCtx, logger := logs.Trace(upstreamCtx, context.Background(), "AuthMiddleware")` 来创建 `newCtx`
`logger` 实例。
- [x] 使用 `c.SetRequest(c.Request().WithContext(newCtx))` 将更新后的 `newCtx` 写入 `echo.Context`,以便后续处理链使用。
- [x] 将所有对 `controller.SendErrorWithStatus` 的调用替换为 `controller.SendErrorWithAudit`
- [x] 确保 `controller.SendErrorWithAudit` 接收 `newCtx` 作为第一个参数,并提供适当的 `actionType`,
`description`, `targetResource`
- 例如,对于“请求未包含授权标头”的错误,`actionType` 可以是“认证失败”,`description` 是“请求未包含授权标头”,
`targetResource``nil`
- 对于“无效的Token”错误`actionType` 可以是“认证失败”,`description` 是“无效的Token”`targetResource`
`tokenString`
- [x] 在 `AuthMiddleware` 内部,如果需要日志记录(例如 `tokenService.ParseToken` 失败或 `userRepo.FindByID`
失败),使用新创建的 `logger` 实例进行日志输出。
- [x] **关键**: 使用 `c.SetRequest(c.Request().WithContext(newCtx))` 将更新后的 `Context` 写回 `echo.Context`
,以便传递给后续的中间件和 `Controller`
- **`internal/app/middleware/audit.go`**
- **改造动作**:
- [x] 检查并重构所有日志记录中间件。
- [x] 中间件应该从请求的 `c.Request().Context()` 中提取 `upstreamCtx`
- [x] 使用 `logs.Trace``logs.AddFuncName` 创建新的 `Context``Logger`
- [x] **关键**: 使用 `c.SetRequest(c.Request().WithContext(newCtx))` 将更新后的 `Context` 写回 `echo.Context`
,以便传递给后续的中间件和 `Controller`

View File

@@ -0,0 +1,396 @@
- **`internal/infra/repository/unit_of_work.go` (`repository.UnitOfWork` - `gormUnitOfWork` 实现)**
- **接口改造 (`UnitOfWork`)**:
- [x] 修改 `UnitOfWork` 接口中的 `ExecuteInTransaction` 方法签名,使其接收 `ctx context.Context` 作为第一个参数:
`ExecuteInTransaction(ctx context.Context, fn func(tx *gorm.DB) error) error`
- **结构体改造 (`gormUnitOfWork`)**:
- [x] 移除 `gormUnitOfWork` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormUnitOfWork`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `gormUnitOfWork` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "GormUnitOfWork")`
- [x] 将这个 `selfCtx` 赋值给 `gormUnitOfWork` 结构体的 `selfCtx` 成员。
- **方法改造 (`ExecuteInTransaction`)**:
- [x] 修改方法签名,使其接收 `ctx context.Context` 作为第一个参数:
`(u *gormUnitOfWork) ExecuteInTransaction(ctx context.Context, fn func(tx *gorm.DB) error) error`
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, u.selfCtx, "ExecuteInTransaction")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `u.logger.Errorf` 的调用替换为 `logger.Errorf`
- [x] 在开启事务时,使用 `tx := u.db.WithContext(newCtx).Begin()`,确保事务 `tx` 携带了正确的上下文。
- **`internal/infra/repository/plan_repository.go` (`repository.PlanRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPlanRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPlanRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PlanRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPlanRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/user_repository.go` (`repository.UserRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormUserRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormUserRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "UserRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormUserRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/device_repository.go` (`repository.DeviceRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormDeviceRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormDeviceRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "DeviceRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormDeviceRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_pen_repository.go` (`repository.PigPenRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigPenRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigPenRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigPenRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigPenRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_farm_repository.go` (`repository.PigFarmRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigFarmRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigFarmRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigFarmRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigFarmRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_sick_repository.go` (`repository.PigSickLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigSickLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigSickLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigSickLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigSickLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_batch_repository.go` (`repository.PigBatchRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigBatchRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigBatchRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigBatchRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigBatchRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_trade_repository.go` (`repository.PigTradeRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigTradeRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigTradeRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigTradeRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigTradeRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/sensor_data_repository.go` (`repository.SensorDataRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormSensorDataRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormSensorDataRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "SensorDataRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormSensorDataRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/notification_repository.go` (`repository.NotificationRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormNotificationRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormNotificationRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "NotificationRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormNotificationRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pending_task_repository.go` (`repository.PendingTaskRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPendingTaskRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPendingTaskRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PendingTaskRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPendingTaskRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/raw_material_repository.go` (`repository.RawMaterialRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormRawMaterialRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormRawMaterialRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "RawMaterialRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormRawMaterialRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/execution_log_repository.go` (`repository.ExecutionLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormExecutionLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormExecutionLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "ExecutionLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormExecutionLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_batch_log_repository.go` (`repository.PigBatchLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigBatchLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigBatchLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigBatchLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigBatchLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/medication_log_repository.go` (`repository.MedicationLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormMedicationLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormMedicationLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "MedicationLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormMedicationLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/area_controller_repository.go` (`repository.AreaControllerRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormAreaControllerRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormAreaControllerRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "AreaControllerRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormAreaControllerRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/device_template_repository.go` (`repository.DeviceTemplateRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormDeviceTemplateRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormDeviceTemplateRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "DeviceTemplateRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormDeviceTemplateRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/user_action_log_repository.go` (`repository.UserActionLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormUserActionLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormUserActionLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "UserActionLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormUserActionLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_transfer_log_repository.go` (`repository.PigTransferLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigTransferLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigTransferLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigTransferLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigTransferLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/device_command_log_repository.go` (`repository.DeviceCommandLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormDeviceCommandLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormDeviceCommandLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "DeviceCommandLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormDeviceCommandLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pending_collection_repository.go` (`repository.PendingCollectionRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPendingCollectionRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPendingCollectionRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PendingCollectionRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPendingCollectionRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/raw_material_repository.go` (`repository.RawMaterialRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormRawMaterialRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormRawMaterialRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "RawMaterialRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormRawMaterialRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/execution_log_repository.go` (`repository.ExecutionLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormExecutionLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormExecutionLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "ExecutionLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormExecutionLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_batch_log_repository.go` (`repository.PigBatchLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigBatchLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigBatchLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigBatchLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigBatchLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/medication_log_repository.go` (`repository.MedicationLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormMedicationLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormMedicationLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "MedicationLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormMedicationLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/area_controller_repository.go` (`repository.AreaControllerRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormAreaControllerRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormAreaControllerRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "AreaControllerRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormAreaControllerRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/device_template_repository.go` (`repository.DeviceTemplateRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormDeviceTemplateRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormDeviceTemplateRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "DeviceTemplateRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormDeviceTemplateRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/user_action_log_repository.go` (`repository.UserActionLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormUserActionLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormUserActionLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "UserActionLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormUserActionLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)``tx.WithContext(newCtx)`
- **`internal/infra/repository/pig_transfer_log_repository.go` (`repository.PigTransferLogRepository`)**
- **结构体改造**:
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewGormPigTransferLogRepository`)**:
- [x] 修改函数签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在函数内部,为 `gormPigTransferLogRepository` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigTransferLogRepository")`
- [x] 将这个 `selfCtx` 赋值给 `gormPigTransferLogRepository` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有公共方法)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx := logs.AddFuncName(ctx, r.selfCtx, "MethodName")`
- [x] 确保所有对 `r.db``tx` 的调用都使用 `r.db.WithContext(newCtx)` 或 `tx.WithContext(new

View File

@@ -0,0 +1,113 @@
- **`internal/app/service/pig_farm_service.go` (`service.PigFarmService`)**
- **结构体改造**:
- [x] 移除 `PigFarmService` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- [x] 移除 `PigFarmService` 结构体中的 `repo repository.PigFarmRepository` 成员,改为
`repo repository.PigFarmRepository`
- **构造函数改造 (`NewPigFarmService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `PigFarmService` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigFarmService")`
- [x] 将这个 `selfCtx` 赋值给 `PigFarmService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (
所有方法,例如 `CreatePigHouse`, `GetPigHouseByID`, `ListPigHouses`, `UpdatePigHouse`, `DeletePigHouse`,
`CreatePen`, `GetPenByID`, `ListPens`, `UpdatePen`, `DeletePen`, `UpdatePenStatus`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf``s.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `s.repo` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/service/pig_batch_service.go` (`service.PigBatchService`)**
- **结构体改造**:
- [x] 移除 `PigBatchService` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPigBatchService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `PigBatchService` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PigBatchService")`
- [x] 将这个 `selfCtx` 赋值给 `PigBatchService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (
所有方法,例如 `CreatePigBatch`, `GetPigBatch`, `UpdatePigBatch`, `DeletePigBatch`, `ListPigBatches`,
`AssignEmptyPensToBatch`, `ReclassifyPenToNewBatch`, `RemoveEmptyPenFromBatch`, `MovePigsIntoPen`, `SellPigs`,
`BuyPigs`, `RecordSickPigs`, `RecordSickPigRecovery`, `RecordSickPigDeath`, `RecordSickPigCull`, `RecordDeath`,
`RecordCull`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf``s.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `s.pigBatchRepo`, `s.pigBatchLogRepo`, `s.uow`, `s.transferSvc`, `s.tradeSvc`, `s.sickSvc`
等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/service/monitor_service.go` (`service.MonitorService`)**
- **结构体改造**:
- [x] 移除 `MonitorService` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewMonitorService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `MonitorService` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "MonitorService")`
- [x] 将这个 `selfCtx` 赋值给 `MonitorService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (
所有方法,例如 `ListSensorData`, `ListDeviceCommandLogs`, `ListPlanExecutionLogs`, `ListTaskExecutionLogs`,
`ListPendingCollections`, `ListUserActionLogs`, `ListRawMaterialPurchases`, `ListRawMaterialStockLogs`,
`ListFeedUsageRecords`, `ListMedicationLogs`, `ListPigBatchLogs`, `ListWeighingBatches`, `ListWeighingRecords`,
`ListPigTransferLogs`, `ListPigSickLogs`, `ListPigPurchases`, `ListPigSales`, `ListNotifications`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf``s.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `s.repo` 的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/service/device_service.go` (`service.DeviceService`)**
- **结构体改造**:
- [x] 移除 `DeviceService` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewDeviceService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `DeviceService` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "DeviceService")`
- [x] 将这个 `selfCtx` 赋值给 `DeviceService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (
所有方法,例如 `CreateDevice`, `GetDevice`, `ListDevices`, `UpdateDevice`, `DeleteDevice`, `ManualControl`,
`CreateAreaController`, `GetAreaController`, `ListAreaControllers`, `UpdateAreaController`,
`DeleteAreaController`, `CreateDeviceTemplate`, `GetDeviceTemplate`, `ListDeviceTemplates`,
`UpdateDeviceTemplate`, `DeleteDeviceTemplate`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf`, `s.logger.Warnf`, `s.logger.Infof` 的调用替换为 `logger.Errorf`,
`logger.Warnf`, `logger.Infof`
- [x] 确保所有对 `s.repo`, `s.generalDeviceService` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/service/plan_service.go` (`service.PlanService`)**
- **结构体改造**:
- [x] 移除 `PlanService` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPlanService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `PlanService` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "PlanService")`
- [x] 将这个 `selfCtx` 赋值给 `PlanService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (
所有方法,例如 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, `StartPlan`, `StopPlan`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf``s.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `s.repo`, `s.planExecutionManager`, `s.analysisPlanTaskManager` 等依赖的调用都将 `newCtx`
作为第一个参数传递。
- **`internal/app/service/user_service.go` (`service.UserService`)**
- **结构体改造**:
- [x] 移除 `UserService` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewUserService`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `UserService` 创建其专属的 `selfCtx``selfCtx := logs.AddCompName(ctx, "UserService")`
- [x] 将这个 `selfCtx` 赋值给 `UserService` 结构体的 `selfCtx` 成员。
- **公共方法改造 (所有方法,例如 `CreateUser`, `Login`, `SendTestNotification`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在方法入口处,使用 `newCtx, logger := logs.Trace(ctx, s.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `s.logger.Errorf``s.logger.Infof` 的调用替换为 `logger.Errorf``logger.Infof`
- [x] 确保所有对 `s.repo`, `s.tokenService`, `s.notifier` 等依赖的调用都将 `newCtx` 作为第一个参数传递。

View File

@@ -0,0 +1,39 @@
- **`internal/app/webhook/chirp_stack.go` (`webhook.ChirpStackListener`)**
- **结构体改造**:
- [x] 移除 `ChirpStackListener` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewChirpStackListener`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `ChirpStackListener` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "ChirpStackListener")`
- [x] 将这个 `selfCtx` 赋值给 `ChirpStackListener` 结构体的 `selfCtx` 成员。
- **公共方法改造 (`Handler`)**:
- [x] `Handler` 方法返回 `http.HandlerFunc`。在返回的 `http.HandlerFunc` 内部,获取 `r.Context()` 作为
`upstreamCtx`
- [x] 在 `go c.handler(b, event)` 调用之前,将 `upstreamCtx` 传递给 `c.handler` 方法,即
`go c.handler(upstreamCtx, b, event)`
- [x] 将所有对 `c.logger.Errorf` 的调用替换为 `logger.Errorf` (在 `handler` 方法中处理)。
- **内部辅助方法改造 (`handler`, `handleUpEvent`, `handleStatusEvent`, `handleAckEvent`, `handleLogEvent`,
`handleJoinEvent`, `handleTxAckEvent`, `handleLocationEvent`, `handleIntegrationEvent`, `recordSensorData`)**:
- [x] 修改方法签名,添加 `ctx context.Context` 作为第一个参数。
- [x] 在每个方法入口处,使用 `newCtx, logger := logs.Trace(ctx, c.selfCtx, "MethodName")` 获取新的
`context.Context``logger` 实例。
- [x] 将所有对 `c.logger.Errorf`, `c.logger.Infof`, `c.logger.Warnf` 的调用替换为 `logger.Errorf`,
`logger.Infof`, `logger.Warnf`
- [x] 确保所有对 `c.sensorDataRepo`, `c.deviceRepo`, `c.areaControllerRepo`, `c.deviceCommandLogRepo`,
`c.pendingCollectionRepo` 等依赖的调用都将 `newCtx` 作为第一个参数传递。
- **`internal/app/webhook/placeholder_listener.go` (`webhook.PlaceholderListener`)**
- **结构体改造**:
- [x] 移除 `PlaceholderListener` 结构体中的 `logger *logs.Logger` 成员。
- [x] 新增 `selfCtx context.Context` 成员。
- **构造函数改造 (`NewPlaceholderListener`)**:
- [x] 修改函数签名,移除 `logger *logs.Logger` 参数,改为接收 `ctx context.Context`
- [x] 在函数内部,为 `PlaceholderListener` 创建其专属的 `selfCtx`
`selfCtx := logs.AddCompName(ctx, "PlaceholderListener")`
- [x] 将这个 `selfCtx` 赋值给 `PlaceholderListener` 结构体的 `selfCtx` 成员。
- [x] 使用 `newCtx, logger := logs.Trace(ctx, selfCtx, "NewPlaceholderListener")` 获取 `logger` 实例,并替换
`logger.Info` 调用。
- **公共方法改造 (`Handler`)**:
- [x] 在 `Handler` 方法返回的 `http.HandlerFunc` 内部,获取 `r.Context()` 作为 `upstreamCtx`
- [x] 使用 `newCtx, logger := logs.Trace(upstreamCtx, p.selfCtx, "Handler")` 获取 `logger` 实例。
- [x] 将所有对 `p.logger.Warn` 的调用替换为 `logger.Warn`

View File

@@ -998,6 +998,7 @@ const docTemplate = `{
},
{
"enum": [
7,
-1,
0,
1,
@@ -1007,12 +1008,12 @@ const docTemplate = `{
5,
-1,
5,
6,
7
6
],
"type": "integer",
"format": "int32",
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -1022,8 +1023,7 @@ const docTemplate = `{
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel",
"_numLevels"
"InvalidLevel"
],
"name": "level",
"in": "query"
@@ -6859,6 +6859,7 @@ const docTemplate = `{
"type": "integer",
"format": "int32",
"enum": [
7,
-1,
0,
1,
@@ -6868,10 +6869,10 @@ const docTemplate = `{
5,
-1,
5,
6,
7
6
],
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -6881,8 +6882,7 @@ const docTemplate = `{
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel",
"_numLevels"
"InvalidLevel"
]
}
},

View File

@@ -990,6 +990,7 @@
},
{
"enum": [
7,
-1,
0,
1,
@@ -999,12 +1000,12 @@
5,
-1,
5,
6,
7
6
],
"type": "integer",
"format": "int32",
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -1014,8 +1015,7 @@
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel",
"_numLevels"
"InvalidLevel"
],
"name": "level",
"in": "query"
@@ -6851,6 +6851,7 @@
"type": "integer",
"format": "int32",
"enum": [
7,
-1,
0,
1,
@@ -6860,10 +6861,10 @@
5,
-1,
5,
6,
7
6
],
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -6873,8 +6874,7 @@
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel",
"_numLevels"
"InvalidLevel"
]
}
},

View File

@@ -1950,6 +1950,7 @@ definitions:
- PlanTypeFilterSystem
zapcore.Level:
enum:
- 7
- -1
- 0
- 1
@@ -1960,10 +1961,10 @@ definitions:
- -1
- 5
- 6
- 7
format: int32
type: integer
x-enum-varnames:
- _numLevels
- DebugLevel
- InfoLevel
- WarnLevel
@@ -1974,7 +1975,6 @@ definitions:
- _minLevel
- _maxLevel
- InvalidLevel
- _numLevels
info:
contact:
email: divano@example.com
@@ -2546,6 +2546,7 @@ paths:
name: end_time
type: string
- enum:
- 7
- -1
- 0
- 1
@@ -2556,12 +2557,12 @@ paths:
- -1
- 5
- 6
- 7
format: int32
in: query
name: level
type: integer
x-enum-varnames:
- _numLevels
- DebugLevel
- InfoLevel
- WarnLevel
@@ -2572,7 +2573,6 @@ paths:
- _minLevel
- _maxLevel
- InvalidLevel
- _numLevels
- enum:
- 邮件
- 企业微信

View File

@@ -26,12 +26,11 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
domain_plan "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@@ -40,10 +39,10 @@ import (
// API 结构体定义了 HTTP 服务器及其依赖
type API struct {
echo *echo.Echo // Echo 引擎实例,用于处理 HTTP 请求
logger *logs.Logger // 日志记录器,用于输出日志信息
Ctx context.Context // API 组件的上下文,包含日志记录器
userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作
tokenService token.Service // Token 服务接口,用于 JWT token 的生成和解析
auditService audit.Service // 审计服务,用于记录用户操作
tokenGenerator token.Generator // Token 服务接口,用于 JWT token 的生成和解析
auditService service.AuditService // 审计服务,用于记录用户操作
httpServer *http.Server // 标准库的 HTTP 服务器实例,用于启动和停止服务
config config.ServerConfig // API 服务器的配置,使用 infra/config 包中的 ServerConfig
userController *user.Controller // 用户控制器实例
@@ -59,7 +58,7 @@ type API struct {
// NewAPI 创建并返回一个新的 API 实例
// 负责初始化 Echo 引擎、设置全局中间件,并注入所有必要的依赖。
func NewAPI(cfg config.ServerConfig,
logger *logs.Logger,
ctx context.Context,
userRepo repository.UserRepository,
pigFarmService service.PigFarmService,
pigBatchService service.PigBatchService,
@@ -67,8 +66,8 @@ func NewAPI(cfg config.ServerConfig,
deviceService service.DeviceService,
planService service.PlanService,
userService service.UserService,
tokenService token.Service,
auditService audit.Service,
auditService service.AuditService,
tokenGenerator token.Generator,
listenHandler webhook.ListenHandler,
) *API {
// 使用 echo.New() 创建一个 Echo 引擎实例
@@ -82,26 +81,27 @@ func NewAPI(cfg config.ServerConfig,
e.Use(middleware.Recover())
// 初始化 API 结构体
baseCtx := context.Background()
api := &API{
echo: e,
logger: logger,
userRepo: userRepo,
tokenService: tokenService,
auditService: auditService,
config: cfg,
listenHandler: listenHandler,
echo: e,
Ctx: ctx,
userRepo: userRepo,
tokenGenerator: tokenGenerator,
auditService: auditService,
config: cfg,
listenHandler: listenHandler,
// 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员
userController: user.NewController(userService, logger),
userController: user.NewController(logs.AddCompName(baseCtx, "UserController"), userService),
// 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员
deviceController: device.NewController(deviceService, logger),
deviceController: device.NewController(logs.AddCompName(baseCtx, "DeviceController"), deviceService),
// 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员
planController: plan.NewController(logger, planService),
planController: plan.NewController(logs.AddCompName(baseCtx, "PlanController"), planService),
// 在 NewAPI 中初始化猪场管理控制器
pigFarmController: management.NewPigFarmController(logger, pigFarmService),
pigFarmController: management.NewPigFarmController(logs.AddCompName(baseCtx, "PigFarmController"), pigFarmService),
// 在 NewAPI 中初始化猪群控制器
pigBatchController: management.NewPigBatchController(logger, pigBatchService),
pigBatchController: management.NewPigBatchController(logs.AddCompName(baseCtx, "PigBatchController"), pigBatchService),
// 在 NewAPI 中初始化数据监控控制器
monitorController: monitor.NewController(monitorService, logger),
monitorController: monitor.NewController(logs.AddCompName(baseCtx, "MonitorController"), monitorService),
}
api.setupRoutes() // 设置所有路由
@@ -112,6 +112,7 @@ func NewAPI(cfg config.ServerConfig,
// 接收一个地址字符串 (例如 ":8080"),并在一个新的 goroutine 中启动服务器,
// 以便主线程可以继续执行其他任务(例如监听操作系统信号)。
func (a *API) Start() {
logger := logs.TraceLogger(a.Ctx, a.Ctx, "Start")
// 构建监听地址字符串
addr := fmt.Sprintf(":%d", a.config.Port)
@@ -126,18 +127,19 @@ func (a *API) Start() {
go func() {
// 启动服务器并检查错误。http.ErrServerClosed 是正常关闭时的错误,无需处理。
if err := a.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
a.logger.Fatalf("HTTP 服务器监听失败: %s", err)
logger.Fatalf("HTTP 服务器监听失败: %s", err)
}
}()
// 记录服务器已启动的信息
a.logger.Infof("HTTP 服务器正在监听 %s", addr)
logger.Infof("HTTP 服务器正在监听 %s", addr)
}
// Stop 优雅地停止 HTTP 服务器
// 在停止服务器时,会给一个超时时间,确保正在处理的请求能够完成。
func (a *API) Stop() {
logger := logs.TraceLogger(a.Ctx, a.Ctx, "Stop")
// 记录服务器正在关闭的信息
a.logger.Info("正在关闭 HTTP 服务器...")
logger.Info("正在关闭 HTTP 服务器...")
// 创建一个带有 5 秒超时时间的上下文
// 在此时间内,服务器会尝试完成所有活跃的连接。
@@ -148,8 +150,8 @@ func (a *API) Stop() {
// 如果在超时时间内未能关闭Shutdown 会返回错误。
if err := a.httpServer.Shutdown(ctx); err != nil {
// 如果关闭失败,记录致命错误并退出
a.logger.Fatalf("HTTP 服务器关闭失败: %s", err)
logger.Fatalf("HTTP 服务器关闭失败: %s", err)
}
// 记录服务器已停止的信息
a.logger.Info("HTTP 服务器已停止。")
logger.Info("HTTP 服务器已停止。")
}

View File

@@ -1,10 +1,13 @@
package api
import (
"context"
"net/http"
"net/http/pprof"
"git.huangwc.com/pig/pig-farm-controller/internal/app/middleware"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
echoSwagger "github.com/swaggo/echo-swagger"
)
@@ -12,7 +15,7 @@ import (
// setupRoutes 设置所有 API 路由
// 在此方法中,使用已初始化的控制器实例将其路由注册到 Echo 引擎中。
func (a *API) setupRoutes() {
a.logger.Info("开始初始化所有 API 路由")
logger := logs.TraceLogger(a.Ctx, a.Ctx, "SetupRoutes")
// --- Public Routes ---
// 这些路由不需要身份验证
@@ -20,7 +23,7 @@ func (a *API) setupRoutes() {
// 用户注册和登录
a.echo.POST("/api/v1/users", a.userController.CreateUser) // 注册新用户
a.echo.POST("/api/v1/users/login", a.userController.Login) // 用户登录
a.logger.Debug("公开接口注册成功:用户注册、登录")
logger.Debug("公开接口注册成功:用户注册、登录")
// 注册 pprof 路由
pprofGroup := a.echo.Group("/debug/pprof")
@@ -38,28 +41,28 @@ func (a *API) setupRoutes() {
pprofGroup.GET("/mutex", echo.WrapHandler(pprof.Handler("mutex"))) // pprof 互斥锁
pprofGroup.GET("/threadcreate", echo.WrapHandler(pprof.Handler("threadcreate")))
}
a.logger.Debug("pprof 接口注册成功")
logger.Debug("pprof 接口注册成功")
// 上行事件监听路由
a.echo.POST("/upstream", echo.WrapHandler(a.listenHandler.Handler())) // 处理设备上行事件
a.logger.Debug("上行事件监听接口注册成功")
logger.Debug("上行事件监听接口注册成功")
// 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到
a.echo.GET("/swagger/*any", echoSwagger.WrapHandler) // Swagger UI 接口
a.logger.Debug("Swagger UI 接口注册成功")
logger.Debug("Swagger UI 接口注册成功")
// --- Authenticated Routes ---
// 所有在此注册的路由都需要通过 JWT 身份验证
authGroup := a.echo.Group("/api/v1")
authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证中间件
authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志中间件
authGroup.Use(middleware.AuthMiddleware(logs.AddCompName(context.Background(), "AuthMiddleware"), a.tokenGenerator, a.userRepo)) // 1. 身份认证中间件
authGroup.Use(middleware.AuditLogMiddleware(logs.AddCompName(context.Background(), "AuditLogMiddleware"), a.auditService)) // 2. 审计日志中间件
{
// 用户相关路由组
userGroup := authGroup.Group("/users")
{
userGroup.POST("/:id/notifications/test", a.userController.SendTestNotification)
}
a.logger.Debug("用户相关接口注册成功 (需要认证和审计)")
logger.Debug("用户相关接口注册成功 (需要认证和审计)")
// 设备相关路由组
deviceGroup := authGroup.Group("/devices")
@@ -71,7 +74,7 @@ func (a *API) setupRoutes() {
deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) // 删除设备
deviceGroup.POST("/manual-control/:id", a.deviceController.ManualControl) // 手动控制设备
}
a.logger.Debug("设备相关接口注册成功 (需要认证和审计)")
logger.Debug("设备相关接口注册成功 (需要认证和审计)")
// 区域主控相关路由组
areaControllerGroup := authGroup.Group("/area-controllers")
@@ -82,7 +85,7 @@ func (a *API) setupRoutes() {
areaControllerGroup.PUT("/:id", a.deviceController.UpdateAreaController) // 更新区域主控
areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController) // 删除区域主控
}
a.logger.Debug("区域主控相关接口注册成功 (需要认证和审计)")
logger.Debug("区域主控相关接口注册成功 (需要认证和审计)")
// 设备模板相关路由组
deviceTemplateGroup := authGroup.Group("/device-templates")
@@ -93,7 +96,7 @@ func (a *API) setupRoutes() {
deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate) // 更新设备模板
deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate) // 删除设备模板
}
a.logger.Debug("设备模板相关接口注册成功 (需要认证和审计)")
logger.Debug("设备模板相关接口注册成功 (需要认证和审计)")
// 计划相关路由组
planGroup := authGroup.Group("/plans")
@@ -106,7 +109,7 @@ func (a *API) setupRoutes() {
planGroup.POST("/:id/start", a.planController.StartPlan) // 启动计划
planGroup.POST("/:id/stop", a.planController.StopPlan) // 停止计划
}
a.logger.Debug("计划相关接口注册成功 (需要认证和审计)")
logger.Debug("计划相关接口注册成功 (需要认证和审计)")
// 猪舍相关路由组
pigHouseGroup := authGroup.Group("/pig-houses")
@@ -117,7 +120,7 @@ func (a *API) setupRoutes() {
pigHouseGroup.PUT("/:id", a.pigFarmController.UpdatePigHouse) // 更新猪舍
pigHouseGroup.DELETE("/:id", a.pigFarmController.DeletePigHouse) // 删除猪舍
}
a.logger.Debug("猪舍相关接口注册成功 (需要认证和审计)")
logger.Debug("猪舍相关接口注册成功 (需要认证和审计)")
// 猪圈相关路由组
penGroup := authGroup.Group("/pens")
@@ -129,7 +132,7 @@ func (a *API) setupRoutes() {
penGroup.DELETE("/:id", a.pigFarmController.DeletePen) // 删除猪圈
penGroup.PUT("/:id/status", a.pigFarmController.UpdatePenStatus) // 更新猪圈状态
}
a.logger.Debug("猪圈相关接口注册成功 (需要认证和审计)")
logger.Debug("猪圈相关接口注册成功 (需要认证和审计)")
// 猪群相关路由组
pigBatchGroup := authGroup.Group("/pig-batches")
@@ -154,7 +157,7 @@ func (a *API) setupRoutes() {
pigBatchGroup.POST("/record-death/:id", a.pigBatchController.RecordDeath) // 记录正常猪只死亡事件
pigBatchGroup.POST("/record-cull/:id", a.pigBatchController.RecordCull) // 记录正常猪只淘汰事件
}
a.logger.Debug("猪群相关接口注册成功 (需要认证和审计)")
logger.Debug("猪群相关接口注册成功 (需要认证和审计)")
// 数据监控相关路由组
monitorGroup := authGroup.Group("/monitor")
@@ -178,8 +181,8 @@ func (a *API) setupRoutes() {
monitorGroup.GET("/pig-sales", a.monitorController.ListPigSales)
monitorGroup.GET("/notifications", a.monitorController.ListNotifications)
}
a.logger.Debug("数据监控相关接口注册成功 (需要认证和审计)")
logger.Debug("数据监控相关接口注册成功 (需要认证和审计)")
}
a.logger.Debug("所有接口注册成功")
logger.Debug("所有接口注册成功")
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/labstack/echo/v4"
)

View File

@@ -1,6 +1,7 @@
package device
import (
"context"
"errors"
"strconv"
@@ -8,24 +9,25 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
// Controller 设备控制器,封装了所有与设备和区域主控相关的业务逻辑
type Controller struct {
ctx context.Context
deviceService service.DeviceService
logger *logs.Logger
}
// NewController 创建一个新的设备控制器实例
func NewController(
ctx context.Context,
deviceService service.DeviceService,
logger *logs.Logger,
) *Controller {
return &Controller{
ctx: ctx,
deviceService: deviceService,
logger: logger,
}
}
@@ -42,20 +44,22 @@ func NewController(
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
// @Router /api/v1/devices [post]
func (c *Controller) CreateDevice(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreateDevice")
const actionType = "创建设备"
var req dto.CreateDeviceRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.deviceService.CreateDevice(&req)
resp, err := c.deviceService.CreateDevice(reqCtx, &req)
if err != nil {
c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备失败: "+err.Error(), actionType, "服务层创建失败", req)
}
c.logger.Infof("%s: 设备创建成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 设备创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备创建成功", resp, actionType, "设备创建成功", resp)
}
@@ -69,26 +73,28 @@ func (c *Controller) CreateDevice(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
// @Router /api/v1/devices/{id} [get]
func (c *Controller) GetDevice(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetDevice")
const actionType = "获取设备"
deviceID := ctx.Param("id")
id, err := strconv.ParseUint(deviceID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
}
resp, err := c.deviceService.GetDevice(uint(id))
resp, err := c.deviceService.GetDevice(reqCtx, uint(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
}
c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, deviceID)
logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: "+err.Error(), actionType, "服务层获取失败", deviceID)
}
c.logger.Infof("%s: 获取设备信息成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 获取设备信息成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备信息成功", resp, actionType, "获取设备信息成功", resp)
}
@@ -101,14 +107,16 @@ func (c *Controller) GetDevice(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=[]dto.DeviceResponse}
// @Router /api/v1/devices [get]
func (c *Controller) ListDevices(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListDevices")
const actionType = "获取设备列表"
resp, err := c.deviceService.ListDevices()
resp, err := c.deviceService.ListDevices(reqCtx)
if err != nil {
c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil)
}
c.logger.Infof("%s: 获取设备列表成功, 数量: %d", actionType, len(resp))
logger.Infof("%s: 获取设备列表成功, 数量: %d", actionType, len(resp))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备列表成功", resp, actionType, "获取设备列表成功", resp)
}
@@ -124,32 +132,34 @@ func (c *Controller) ListDevices(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.DeviceResponse}
// @Router /api/v1/devices/{id} [put]
func (c *Controller) UpdateDevice(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdateDevice")
const actionType = "更新设备"
deviceID := ctx.Param("id")
var req dto.UpdateDeviceRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
id, err := strconv.ParseUint(deviceID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
}
resp, err := c.deviceService.UpdateDevice(uint(id), &req)
resp, err := c.deviceService.UpdateDevice(reqCtx, uint(id), &req)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
}
c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, deviceID)
logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "服务层更新失败", deviceID)
}
c.logger.Infof("%s: 设备更新成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 设备更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备更新成功", resp, actionType, "设备更新成功", resp)
}
@@ -163,31 +173,33 @@ func (c *Controller) UpdateDevice(ctx echo.Context) error {
// @Success 200 {object} controller.Response
// @Router /api/v1/devices/{id} [delete]
func (c *Controller) DeleteDevice(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeleteDevice")
const actionType = "删除设备"
deviceID := ctx.Param("id")
id, err := strconv.ParseUint(deviceID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
}
if err := c.deviceService.DeleteDevice(uint(id)); err != nil {
if err := c.deviceService.DeleteDevice(reqCtx, uint(id)); err != nil {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
case errors.Is(err, service.ErrDeviceInUse):
c.logger.Warnf("%s: 尝试删除正在被使用的设备, ID: %s", actionType, deviceID)
logger.Warnf("%s: 尝试删除正在被使用的设备, ID: %s", actionType, deviceID)
// 返回 409 Conflict 状态码,表示请求与服务器当前状态冲突
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "设备正在被使用", deviceID)
default:
c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, deviceID)
logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "服务层删除失败", deviceID)
}
}
c.logger.Infof("%s: 设备删除成功, ID: %s", actionType, deviceID)
logger.Infof("%s: 设备删除成功, ID: %s", actionType, deviceID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备删除成功", nil, actionType, "设备删除成功", deviceID)
}
@@ -203,27 +215,29 @@ func (c *Controller) DeleteDevice(ctx echo.Context) error {
// @Success 200 {object} controller.Response
// @Router /api/v1/devices/manual-control/{id} [post]
func (c *Controller) ManualControl(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ManualControlDevice")
const actionType = "手动控制设备"
deviceID := ctx.Param("id")
var req dto.ManualControlDeviceRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
id, err := strconv.ParseUint(deviceID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
logger.Errorf("%s: 无效的ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+deviceID, actionType, "无效的ID", deviceID)
}
if err := c.deviceService.ManualControl(uint(id), &req); err != nil {
if err := c.deviceService.ManualControl(reqCtx, uint(id), &req); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID)
}
c.logger.Errorf("%s: 服务层手动控制失败: %v, ID: %s", actionType, err, deviceID)
logger.Errorf("%s: 服务层手动控制失败: %v, ID: %s", actionType, err, deviceID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "手动控制失败: "+err.Error(), actionType, "服务层手动控制失败", deviceID)
}
@@ -243,20 +257,22 @@ func (c *Controller) ManualControl(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
// @Router /api/v1/area-controllers [post]
func (c *Controller) CreateAreaController(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreateAreaController")
const actionType = "创建区域主控"
var req dto.CreateAreaControllerRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.deviceService.CreateAreaController(&req)
resp, err := c.deviceService.CreateAreaController(reqCtx, &req)
if err != nil {
c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建区域主控失败: "+err.Error(), actionType, "服务层创建失败", req)
}
c.logger.Infof("%s: 区域主控创建成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 区域主控创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "区域主控创建成功", resp, actionType, "区域主控创建成功", resp)
}
@@ -270,26 +286,28 @@ func (c *Controller) CreateAreaController(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
// @Router /api/v1/area-controllers/{id} [get]
func (c *Controller) GetAreaController(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetAreaController")
const actionType = "获取区域主控"
acID := ctx.Param("id")
id, err := strconv.ParseUint(acID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, acID)
logger.Errorf("%s: 无效的ID: %s", actionType, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
}
resp, err := c.deviceService.GetAreaController(uint(id))
resp, err := c.deviceService.GetAreaController(reqCtx, uint(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
}
c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, acID)
logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: "+err.Error(), actionType, "服务层获取失败", acID)
}
c.logger.Infof("%s: 获取区域主控信息成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 获取区域主控信息成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控信息成功", resp, actionType, "获取区域主控信息成功", resp)
}
@@ -302,14 +320,16 @@ func (c *Controller) GetAreaController(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=[]dto.AreaControllerResponse}
// @Router /api/v1/area-controllers [get]
func (c *Controller) ListAreaControllers(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListAreaControllers")
const actionType = "获取区域主控列表"
resp, err := c.deviceService.ListAreaControllers()
resp, err := c.deviceService.ListAreaControllers(reqCtx)
if err != nil {
c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil)
}
c.logger.Infof("%s: 获取区域主控列表成功, 数量: %d", actionType, len(resp))
logger.Infof("%s: 获取区域主控列表成功, 数量: %d", actionType, len(resp))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控列表成功", resp, actionType, "获取区域主控列表成功", resp)
}
@@ -325,31 +345,33 @@ func (c *Controller) ListAreaControllers(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse}
// @Router /api/v1/area-controllers/{id} [put]
func (c *Controller) UpdateAreaController(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdateAreaController")
const actionType = "更新区域主控"
acID := ctx.Param("id")
var req dto.UpdateAreaControllerRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
id, err := strconv.ParseUint(acID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, acID)
logger.Errorf("%s: 无效的ID: %s", actionType, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
}
resp, err := c.deviceService.UpdateAreaController(uint(id), &req)
resp, err := c.deviceService.UpdateAreaController(reqCtx, uint(id), &req)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
}
c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, acID)
logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "服务层更新失败", acID)
}
c.logger.Infof("%s: 区域主控更新成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 区域主控更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控更新成功", resp, actionType, "区域主控更新成功", resp)
}
@@ -363,30 +385,32 @@ func (c *Controller) UpdateAreaController(ctx echo.Context) error {
// @Success 200 {object} controller.Response
// @Router /api/v1/area-controllers/{id} [delete]
func (c *Controller) DeleteAreaController(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeleteAreaController")
const actionType = "删除区域主控"
acID := ctx.Param("id")
id, err := strconv.ParseUint(acID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, acID)
logger.Errorf("%s: 无效的ID: %s", actionType, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+acID, actionType, "无效的ID", acID)
}
if err := c.deviceService.DeleteAreaController(uint(id)); err != nil {
if err := c.deviceService.DeleteAreaController(reqCtx, uint(id)); err != nil {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID)
case errors.Is(err, service.ErrAreaControllerInUse):
c.logger.Warnf("%s: 尝试删除正在被使用的主控, ID: %s", actionType, acID)
logger.Warnf("%s: 尝试删除正在被使用的主控, ID: %s", actionType, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "主控正在被使用", acID)
default:
c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, acID)
logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, acID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "服务层删除失败", acID)
}
}
c.logger.Infof("%s: 区域主控删除成功, ID: %s", actionType, acID)
logger.Infof("%s: 区域主控删除成功, ID: %s", actionType, acID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控删除成功", nil, actionType, "区域主控删除成功", acID)
}
@@ -403,20 +427,22 @@ func (c *Controller) DeleteAreaController(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
// @Router /api/v1/device-templates [post]
func (c *Controller) CreateDeviceTemplate(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreateDeviceTemplate")
const actionType = "创建设备模板"
var req dto.CreateDeviceTemplateRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.deviceService.CreateDeviceTemplate(&req)
resp, err := c.deviceService.CreateDeviceTemplate(reqCtx, &req)
if err != nil {
c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
logger.Errorf("%s: 服务层创建失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备模板失败: "+err.Error(), actionType, "服务层创建失败", req)
}
c.logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备模板创建成功", resp, actionType, "设备模板创建成功", resp)
}
@@ -430,26 +456,28 @@ func (c *Controller) CreateDeviceTemplate(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
// @Router /api/v1/device-templates/{id} [get]
func (c *Controller) GetDeviceTemplate(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetDeviceTemplate")
const actionType = "获取设备模板"
dtID := ctx.Param("id")
id, err := strconv.ParseUint(dtID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
}
resp, err := c.deviceService.GetDeviceTemplate(uint(id))
resp, err := c.deviceService.GetDeviceTemplate(reqCtx, uint(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
}
c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, dtID)
logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: "+err.Error(), actionType, "服务层获取失败", dtID)
}
c.logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板信息成功", resp, actionType, "获取设备模板信息成功", resp)
}
@@ -462,14 +490,16 @@ func (c *Controller) GetDeviceTemplate(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=[]dto.DeviceTemplateResponse}
// @Router /api/v1/device-templates [get]
func (c *Controller) ListDeviceTemplates(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListDeviceTemplates")
const actionType = "获取设备模板列表"
resp, err := c.deviceService.ListDeviceTemplates()
resp, err := c.deviceService.ListDeviceTemplates(reqCtx)
if err != nil {
c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil)
}
c.logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(resp))
logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(resp))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板列表成功", resp, actionType, "获取设备模板列表成功", resp)
}
@@ -485,32 +515,34 @@ func (c *Controller) ListDeviceTemplates(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse}
// @Router /api/v1/device-templates/{id} [put]
func (c *Controller) UpdateDeviceTemplate(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdateDeviceTemplate")
const actionType = "更新设备模板"
dtID := ctx.Param("id")
var req dto.UpdateDeviceTemplateRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
id, err := strconv.ParseUint(dtID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
}
resp, err := c.deviceService.UpdateDeviceTemplate(uint(id), &req)
resp, err := c.deviceService.UpdateDeviceTemplate(reqCtx, uint(id), &req)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
}
c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, dtID)
logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "服务层更新失败", dtID)
}
c.logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板更新成功", resp, actionType, "设备模板更新成功", resp)
}
@@ -524,29 +556,31 @@ func (c *Controller) UpdateDeviceTemplate(ctx echo.Context) error {
// @Success 200 {object} controller.Response
// @Router /api/v1/device-templates/{id} [delete]
func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeleteDeviceTemplate")
const actionType = "删除设备模板"
dtID := ctx.Param("id")
id, err := strconv.ParseUint(dtID, 10, 64)
if err != nil {
c.logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
logger.Errorf("%s: 无效的ID: %s", actionType, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID: "+dtID, actionType, "无效的ID", dtID)
}
if err := c.deviceService.DeleteDeviceTemplate(uint(id)); err != nil {
if err := c.deviceService.DeleteDeviceTemplate(reqCtx, uint(id)); err != nil {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID)
case errors.Is(err, service.ErrDeviceTemplateInUse):
c.logger.Warnf("%s: 尝试删除正在被使用的模板, ID: %s", actionType, dtID)
logger.Warnf("%s: 尝试删除正在被使用的模板, ID: %s", actionType, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "模板正在被使用", dtID)
default:
c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, dtID)
logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, dtID)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "服务层删除失败", dtID)
}
}
c.logger.Infof("%s: 设备模板删除成功, ID: %s", actionType, dtID)
logger.Infof("%s: 设备模板删除成功, ID: %s", actionType, dtID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板删除成功", nil, actionType, "设备模板删除成功", dtID)
}

View File

@@ -1,18 +1,21 @@
package management
import (
"context"
"errors"
"fmt"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// mapAndSendError 统一映射服务层错误并发送响应。
// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。
func mapAndSendError(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 uint) error {
if errors.Is(err, service.ErrPigBatchNotFound) ||
errors.Is(err, service.ErrPenNotFound) ||
errors.Is(err, service.ErrPenNotAssociatedWithBatch) {
@@ -25,7 +28,7 @@ func mapAndSendError(c *PigBatchController, ctx echo.Context, action string, err
errors.Is(err, service.ErrPenNotEmpty) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
} else {
c.logger.Errorf("操作[%s]业务逻辑失败: %v", action, err)
logs.GetLogger(reqContext).Errorf("操作[%s]业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, fmt.Sprintf("操作失败: %v", err), action, err.Error(), id)
}
}
@@ -85,6 +88,7 @@ func extractOperatorAndPrimaryID(
// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。
// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。
func handleAPIRequest[Req any](
reqContext context.Context,
c *PigBatchController,
ctx echo.Context,
action string,
@@ -107,7 +111,7 @@ func handleAPIRequest[Req any](
// 3. 执行服务层逻辑
err = serviceExecutor(ctx, operatorID, primaryID, reqDTO)
if err != nil {
return mapAndSendError(c, ctx, action, err, primaryID)
return mapAndSendError(reqContext, c, ctx, action, err, primaryID)
}
// 4. 发送成功响应
@@ -116,6 +120,7 @@ func handleAPIRequest[Req any](
// handleNoBodyAPIRequest 封装了处理不带请求体但有路径参数和操作员ID的API请求的通用逻辑。
func handleNoBodyAPIRequest(
reqContext context.Context,
c *PigBatchController,
ctx echo.Context,
action string,
@@ -132,7 +137,7 @@ func handleNoBodyAPIRequest(
// 2. 执行服务层逻辑
err = serviceExecutor(ctx, operatorID, primaryID)
if err != nil {
return mapAndSendError(c, ctx, action, err, primaryID)
return mapAndSendError(reqContext, c, ctx, action, err, primaryID)
}
// 3. 发送成功响应
@@ -141,6 +146,7 @@ func handleNoBodyAPIRequest(
// handleAPIRequestWithResponse 封装了控制器中处理带有请求体、路径参数并返回响应DTO的API请求的通用逻辑。
func handleAPIRequestWithResponse[Req any, Resp any](
reqContext context.Context,
c *PigBatchController,
ctx echo.Context,
action string,
@@ -163,7 +169,7 @@ func handleAPIRequestWithResponse[Req any, Resp any](
// 3. 执行服务层逻辑
respDTO, err := serviceExecutor(ctx, operatorID, primaryID, reqDTO)
if err != nil {
return mapAndSendError(c, ctx, action, err, primaryID)
return mapAndSendError(reqContext, c, ctx, action, err, primaryID)
}
// 4. 发送成功响应
@@ -172,6 +178,7 @@ func handleAPIRequestWithResponse[Req any, Resp any](
// handleNoBodyAPIRequestWithResponse 封装了处理不带请求体但有路径参数和操作员ID并返回响应DTO的API请求的通用逻辑。
func handleNoBodyAPIRequestWithResponse[Resp any](
reqContext context.Context,
c *PigBatchController,
ctx echo.Context,
action string,
@@ -188,7 +195,7 @@ func handleNoBodyAPIRequestWithResponse[Resp any](
// 2. 执行服务层逻辑
respDTO, err := serviceExecutor(ctx, operatorID, primaryID)
if err != nil {
return mapAndSendError(c, ctx, action, err, primaryID)
return mapAndSendError(reqContext, c, ctx, action, err, primaryID)
}
// 3. 发送成功响应
@@ -197,6 +204,7 @@ func handleNoBodyAPIRequestWithResponse[Resp any](
// handleQueryAPIRequestWithResponse 封装了处理带有查询参数并返回响应DTO的API请求的通用逻辑。
func handleQueryAPIRequestWithResponse[Query any, Resp any](
reqContext context.Context,
c *PigBatchController,
ctx echo.Context,
action string,
@@ -219,7 +227,7 @@ func handleQueryAPIRequestWithResponse[Query any, Resp any](
respDTO, err := serviceExecutor(ctx, operatorID, queryDTO)
if err != nil {
// 对于列表查询通常没有primaryID所以传递0
return mapAndSendError(c, ctx, action, err, 0)
return mapAndSendError(reqContext, c, ctx, action, err, 0)
}
// 4. 发送成功响应

View File

@@ -1,6 +1,7 @@
package management
import (
"context"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
@@ -12,14 +13,14 @@ import (
// PigBatchController 负责处理猪批次相关的API请求
type PigBatchController struct {
logger *logs.Logger
ctx context.Context
service service.PigBatchService
}
// NewPigBatchController 创建一个新的 PigBatchController 实例
func NewPigBatchController(logger *logs.Logger, service service.PigBatchService) *PigBatchController {
func NewPigBatchController(ctx context.Context, service service.PigBatchService) *PigBatchController {
return &PigBatchController{
logger: logger,
ctx: ctx,
service: service,
}
}
@@ -35,14 +36,16 @@ func NewPigBatchController(logger *logs.Logger, service service.PigBatchService)
// @Success 201 {object} controller.Response{data=dto.PigBatchResponseDTO} "创建成功"
// @Router /api/v1/pig-batches [post]
func (c *PigBatchController) CreatePigBatch(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "CreatePigBatch")
const action = "创建猪批次"
var req dto.PigBatchCreateDTO
return handleAPIRequestWithResponse(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
// 对于创建操作primaryID通常不从路径中获取而是由服务层生成
return c.service.CreatePigBatch(operatorID, req)
return c.service.CreatePigBatch(reqCtx, operatorID, req)
},
"创建成功",
nil, // 无需自定义ID提取器primaryID将为0
@@ -59,12 +62,14 @@ func (c *PigBatchController) CreatePigBatch(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功"
// @Router /api/v1/pig-batches/{id} [get]
func (c *PigBatchController) GetPigBatch(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "GetPigBatch")
const action = "获取猪批次"
return handleNoBodyAPIRequestWithResponse(
c, ctx, action,
reqCtx, c, ctx, action,
func(ctx echo.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) {
return c.service.GetPigBatch(primaryID)
return c.service.GetPigBatch(reqCtx, primaryID)
},
"获取成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -83,13 +88,15 @@ func (c *PigBatchController) GetPigBatch(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "更新成功"
// @Router /api/v1/pig-batches/{id} [put]
func (c *PigBatchController) UpdatePigBatch(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "UpdatePigBatch")
const action = "更新猪批次"
var req dto.PigBatchUpdateDTO
return handleAPIRequestWithResponse(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
return c.service.UpdatePigBatch(primaryID, req)
return c.service.UpdatePigBatch(reqCtx, primaryID, req)
},
"更新成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -106,12 +113,14 @@ func (c *PigBatchController) UpdatePigBatch(ctx echo.Context) error {
// @Success 200 {object} controller.Response "删除成功"
// @Router /api/v1/pig-batches/{id} [delete]
func (c *PigBatchController) DeletePigBatch(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "DeletePigBatch")
const action = "删除猪批次"
return handleNoBodyAPIRequest(
c, ctx, action,
reqCtx, c, ctx, action,
func(ctx echo.Context, operatorID uint, primaryID uint) error {
return c.service.DeletePigBatch(primaryID)
return c.service.DeletePigBatch(reqCtx, primaryID)
},
"删除成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -128,13 +137,15 @@ func (c *PigBatchController) DeletePigBatch(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功"
// @Router /api/v1/pig-batches [get]
func (c *PigBatchController) ListPigBatches(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "ListPigBatches")
const action = "获取猪批次列表"
var query dto.PigBatchQueryDTO
return handleQueryAPIRequestWithResponse(
c, ctx, action, &query,
reqCtx, c, ctx, action, &query,
func(ctx echo.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) {
return c.service.ListPigBatches(query.IsActive)
return c.service.ListPigBatches(reqCtx, query.IsActive)
},
"获取成功",
)
@@ -152,13 +163,15 @@ func (c *PigBatchController) ListPigBatches(ctx echo.Context) error {
// @Success 200 {object} controller.Response "分配成功"
// @Router /api/v1/pig-batches/assign-pens/{id} [post]
func (c *PigBatchController) AssignEmptyPensToBatch(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "AssignEmptyPensToBatch")
const action = "为猪批次分配空栏"
var req dto.AssignEmptyPensToBatchRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error {
return c.service.AssignEmptyPensToBatch(primaryID, req.PenIDs, operatorID)
return c.service.AssignEmptyPensToBatch(reqCtx, primaryID, req.PenIDs, operatorID)
},
"分配成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -177,14 +190,16 @@ func (c *PigBatchController) AssignEmptyPensToBatch(ctx echo.Context) error {
// @Success 200 {object} controller.Response "划拨成功"
// @Router /api/v1/pig-batches/reclassify-pen/{fromBatchID} [post]
func (c *PigBatchController) ReclassifyPenToNewBatch(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "ReclassifyPenToNewBatch")
const action = "划拨猪栏到新批次"
var req dto.ReclassifyPenToNewBatchRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error {
// primaryID 在这里是 fromBatchID
return c.service.ReclassifyPenToNewBatch(primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks)
return c.service.ReclassifyPenToNewBatch(reqCtx, primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks)
},
"划拨成功",
func(ctx echo.Context) (uint, error) { // 自定义ID提取器从 ":fromBatchID" 路径参数提取
@@ -209,10 +224,12 @@ func (c *PigBatchController) ReclassifyPenToNewBatch(ctx echo.Context) error {
// @Success 200 {object} controller.Response "移除成功"
// @Router /api/v1/pig-batches/remove-pen/{penID}/{batchID} [delete]
func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "RemoveEmptyPenFromBatch")
const action = "从猪批次移除空栏"
return handleNoBodyAPIRequest(
c, ctx, action,
reqCtx, c, ctx, action,
func(ctx echo.Context, operatorID uint, primaryID uint) error {
// primaryID 在这里是 batchID
penIDParam := ctx.Param("penID")
@@ -220,7 +237,7 @@ func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx echo.Context) error {
if err != nil {
return err // 返回错误,因为 penID 格式无效
}
return c.service.RemoveEmptyPenFromBatch(primaryID, uint(parsedPenID))
return c.service.RemoveEmptyPenFromBatch(reqCtx, primaryID, uint(parsedPenID))
},
"移除成功",
func(ctx echo.Context) (uint, error) { // 自定义ID提取器从 ":batchID" 路径参数提取
@@ -246,13 +263,15 @@ func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx echo.Context) error {
// @Success 200 {object} controller.Response "移入成功"
// @Router /api/v1/pig-batches/move-pigs-into-pen/{id} [post]
func (c *PigBatchController) MovePigsIntoPen(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "MovePigsIntoPen")
const action = "将猪只移入猪栏"
var req dto.MovePigsIntoPenRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error {
return c.service.MovePigsIntoPen(primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
return c.service.MovePigsIntoPen(reqCtx, primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
},
"移入成功",
nil, // 默认从 ":id" 路径参数提取ID

View File

@@ -2,6 +2,8 @@ package management
import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
@@ -17,13 +19,15 @@ import (
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/record-sick-pigs/{id} [post]
func (c *PigBatchController) RecordSickPigs(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "RecordSickPigs")
const action = "记录新增病猪事件"
var req dto.RecordSickPigsRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error {
return c.service.RecordSickPigs(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
return c.service.RecordSickPigs(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -42,13 +46,15 @@ func (c *PigBatchController) RecordSickPigs(ctx echo.Context) error {
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/record-sick-pig-recovery/{id} [post]
func (c *PigBatchController) RecordSickPigRecovery(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "RecordSickPigRecovery")
const action = "记录病猪康复事件"
var req dto.RecordSickPigRecoveryRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error {
return c.service.RecordSickPigRecovery(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
return c.service.RecordSickPigRecovery(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -67,13 +73,15 @@ func (c *PigBatchController) RecordSickPigRecovery(ctx echo.Context) error {
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/record-sick-pig-death/{id} [post]
func (c *PigBatchController) RecordSickPigDeath(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "RecordSickPigDeath")
const action = "记录病猪死亡事件"
var req dto.RecordSickPigDeathRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error {
return c.service.RecordSickPigDeath(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
return c.service.RecordSickPigDeath(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -92,13 +100,15 @@ func (c *PigBatchController) RecordSickPigDeath(ctx echo.Context) error {
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/record-sick-pig-cull/{id} [post]
func (c *PigBatchController) RecordSickPigCull(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "RecordSickPigCull")
const action = "记录病猪淘汰事件"
var req dto.RecordSickPigCullRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error {
return c.service.RecordSickPigCull(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
return c.service.RecordSickPigCull(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -117,13 +127,15 @@ func (c *PigBatchController) RecordSickPigCull(ctx echo.Context) error {
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/record-death/{id} [post]
func (c *PigBatchController) RecordDeath(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "RecordDeath")
const action = "记录正常猪只死亡事件"
var req dto.RecordDeathRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error {
return c.service.RecordDeath(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
return c.service.RecordDeath(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -142,13 +154,15 @@ func (c *PigBatchController) RecordDeath(ctx echo.Context) error {
// @Success 200 {object} controller.Response "记录成功"
// @Router /api/v1/pig-batches/record-cull/{id} [post]
func (c *PigBatchController) RecordCull(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "RecordCull")
const action = "记录正常猪只淘汰事件"
var req dto.RecordCullRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error {
return c.service.RecordCull(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
return c.service.RecordCull(reqCtx, operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks)
},
"记录成功",
nil, // 默认从 ":id" 路径参数提取ID

View File

@@ -2,6 +2,8 @@ package management
import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
@@ -17,13 +19,14 @@ import (
// @Success 200 {object} controller.Response "卖猪成功"
// @Router /api/v1/pig-batches/sell-pigs/{id} [post]
func (c *PigBatchController) SellPigs(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "SellPigs")
const action = "卖猪"
var req dto.SellPigsRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error {
return c.service.SellPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
return c.service.SellPigs(reqCtx, primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
},
"卖猪成功",
nil, // 默认从 ":id" 路径参数提取ID
@@ -42,13 +45,14 @@ func (c *PigBatchController) SellPigs(ctx echo.Context) error {
// @Success 200 {object} controller.Response "买猪成功"
// @Router /api/v1/pig-batches/buy-pigs/{id} [post]
func (c *PigBatchController) BuyPigs(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "BuyPigs")
const action = "买猪"
var req dto.BuyPigsRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error {
return c.service.BuyPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
return c.service.BuyPigs(reqCtx, primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID)
},
"买猪成功",
nil, // 默认从 ":id" 路径参数提取ID

View File

@@ -4,6 +4,8 @@ import (
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
@@ -19,14 +21,16 @@ import (
// @Success 200 {object} controller.Response "调栏成功"
// @Router /api/v1/pig-batches/transfer-across-batches/{sourceBatchID} [post]
func (c *PigBatchController) TransferPigsAcrossBatches(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "TransferPigsAcrossBatches")
const action = "跨猪群调栏"
var req dto.TransferPigsAcrossBatchesRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error {
// primaryID 在这里是 sourceBatchID
return c.service.TransferPigsAcrossBatches(primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
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" 路径参数提取
@@ -52,14 +56,16 @@ func (c *PigBatchController) TransferPigsAcrossBatches(ctx echo.Context) error {
// @Success 200 {object} controller.Response "调栏成功"
// @Router /api/v1/pig-batches/transfer-within-batch/{id} [post]
func (c *PigBatchController) TransferPigsWithinBatch(ctx echo.Context) error {
reqCtx := logs.AddFuncName(ctx.Request().Context(), c.ctx, "TransferPigsWithinBatch")
const action = "群内调栏"
var req dto.TransferPigsWithinBatchRequest
return handleAPIRequest(
c, ctx, action, &req,
reqCtx, c, ctx, action, &req,
func(ctx echo.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error {
// primaryID 在这里是 batchID
return c.service.TransferPigsWithinBatch(primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
return c.service.TransferPigsWithinBatch(reqCtx, primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks)
},
"调栏成功",
nil, // 默认从 ":id" 路径参数提取ID

View File

@@ -1,6 +1,7 @@
package management
import (
"context"
"errors"
"strconv"
@@ -8,6 +9,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
@@ -15,14 +17,14 @@ import (
// PigFarmController 负责处理猪舍和猪栏相关的API请求
type PigFarmController struct {
logger *logs.Logger
ctx context.Context
service service.PigFarmService
}
// NewPigFarmController 创建一个新的 PigFarmController 实例
func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *PigFarmController {
func NewPigFarmController(ctx context.Context, service service.PigFarmService) *PigFarmController {
return &PigFarmController{
logger: logger,
ctx: ctx,
service: service,
}
}
@@ -40,16 +42,17 @@ func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *
// @Success 201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功"
// @Router /api/v1/pig-houses [post]
func (c *PigFarmController) CreatePigHouse(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreatePigHouse")
const action = "创建猪舍"
var req dto.CreatePigHouseRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", action, err)
logger.Errorf("%s: 参数绑定失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
}
house, err := c.service.CreatePigHouse(req.Name, req.Description)
house, err := c.service.CreatePigHouse(reqCtx, req.Name, req.Description)
if err != nil {
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req)
}
@@ -66,18 +69,19 @@ func (c *PigFarmController) CreatePigHouse(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功"
// @Router /api/v1/pig-houses/{id} [get]
func (c *PigFarmController) GetPigHouse(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetPigHouse")
const action = "获取猪舍"
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
}
house, err := c.service.GetPigHouseByID(uint(id))
house, err := c.service.GetPigHouseByID(reqCtx, uint(id))
if err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id)
}
@@ -93,10 +97,11 @@ func (c *PigFarmController) GetPigHouse(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功"
// @Router /api/v1/pig-houses [get]
func (c *PigFarmController) ListPigHouses(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigHouses")
const action = "获取猪舍列表"
houses, err := c.service.ListPigHouses()
houses, err := c.service.ListPigHouses(reqCtx)
if err != nil {
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
}
@@ -115,6 +120,7 @@ func (c *PigFarmController) ListPigHouses(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功"
// @Router /api/v1/pig-houses/{id} [put]
func (c *PigFarmController) UpdatePigHouse(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdatePigHouse")
const action = "更新猪舍"
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
@@ -126,12 +132,12 @@ func (c *PigFarmController) UpdatePigHouse(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
}
house, err := c.service.UpdatePigHouse(uint(id), req.Name, req.Description)
house, err := c.service.UpdatePigHouse(reqCtx, uint(id), req.Name, req.Description)
if err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
}
@@ -148,13 +154,14 @@ func (c *PigFarmController) UpdatePigHouse(ctx echo.Context) error {
// @Success 200 {object} controller.Response "删除成功"
// @Router /api/v1/pig-houses/{id} [delete]
func (c *PigFarmController) DeletePigHouse(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeletePigHouse")
const action = "删除猪舍"
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
}
if err := c.service.DeletePigHouse(uint(id)); err != nil {
if err := c.service.DeletePigHouse(reqCtx, uint(id)); err != nil {
if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id)
}
@@ -162,7 +169,7 @@ func (c *PigFarmController) DeletePigHouse(ctx echo.Context) error {
if errors.Is(err, service.ErrHouseContainsPens) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
}
@@ -182,19 +189,20 @@ func (c *PigFarmController) DeletePigHouse(ctx echo.Context) error {
// @Success 201 {object} controller.Response{data=dto.PenResponse} "创建成功"
// @Router /api/v1/pens [post]
func (c *PigFarmController) CreatePen(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreatePen")
const action = "创建猪栏"
var req dto.CreatePenRequest
if err := ctx.Bind(&req); err != nil {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
}
pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity)
pen, err := c.service.CreatePen(reqCtx, req.PenNumber, req.HouseID, req.Capacity)
if err != nil {
// 检查是否是业务逻辑错误
if errors.Is(err, service.ErrHouseNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), req)
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req)
}
@@ -211,18 +219,19 @@ func (c *PigFarmController) CreatePen(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PenResponse} "获取成功"
// @Router /api/v1/pens/{id} [get]
func (c *PigFarmController) GetPen(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetPen")
const action = "获取猪栏"
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
}
pen, err := c.service.GetPenByID(uint(id))
pen, err := c.service.GetPenByID(reqCtx, uint(id))
if err != nil {
if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id)
}
@@ -238,10 +247,11 @@ func (c *PigFarmController) GetPen(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=[]dto.PenResponse} "获取成功"
// @Router /api/v1/pens [get]
func (c *PigFarmController) ListPens(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPens")
const action = "获取猪栏列表"
pens, err := c.service.ListPens()
pens, err := c.service.ListPens(reqCtx)
if err != nil {
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil)
}
@@ -260,6 +270,7 @@ func (c *PigFarmController) ListPens(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
// @Router /api/v1/pens/{id} [put]
func (c *PigFarmController) UpdatePen(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdatePen")
const action = "更新猪栏"
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
@@ -271,13 +282,13 @@ func (c *PigFarmController) UpdatePen(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
}
pen, err := c.service.UpdatePen(uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status)
pen, err := c.service.UpdatePen(reqCtx, uint(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)
}
// 其他业务逻辑错误可以在这里添加处理
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req)
}
@@ -294,13 +305,14 @@ func (c *PigFarmController) UpdatePen(ctx echo.Context) error {
// @Success 200 {object} controller.Response "删除成功"
// @Router /api/v1/pens/{id} [delete]
func (c *PigFarmController) DeletePen(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeletePen")
const action = "删除猪栏"
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id"))
}
if err := c.service.DeletePen(uint(id)); err != nil {
if err := c.service.DeletePen(reqCtx, uint(id)); err != nil {
if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id)
}
@@ -308,7 +320,7 @@ func (c *PigFarmController) DeletePen(ctx echo.Context) error {
if errors.Is(err, service.ErrPenInUse) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id)
}
@@ -327,6 +339,7 @@ func (c *PigFarmController) DeletePen(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功"
// @Router /api/v1/pens/{id}/status [put]
func (c *PigFarmController) UpdatePenStatus(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdatePenStatus")
const action = "更新猪栏状态"
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
@@ -338,14 +351,14 @@ func (c *PigFarmController) UpdatePenStatus(ctx echo.Context) error {
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req)
}
pen, err := c.service.UpdatePenStatus(uint(id), req.Status)
pen, err := c.service.UpdatePenStatus(reqCtx, uint(id), req.Status)
if err != nil {
if errors.Is(err, service.ErrPenNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id)
} else if errors.Is(err, service.ErrPenStatusInvalidForOccupiedPen) || errors.Is(err, service.ErrPenStatusInvalidForUnoccupiedPen) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id)
}
c.logger.Errorf("%s: 业务逻辑失败: %v", action, err)
logger.Errorf("%s: 业务逻辑失败: %v", action, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id)
}

View File

@@ -1,6 +1,7 @@
package monitor
import (
"context"
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
@@ -8,20 +9,21 @@ import (
"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"
)
// Controller 监控控制器,封装了所有与数据监控相关的业务逻辑
type Controller struct {
ctx context.Context
monitorService service.MonitorService
logger *logs.Logger
}
// NewController 创建一个新的监控控制器实例
func NewController(monitorService service.MonitorService, logger *logs.Logger) *Controller {
func NewController(ctx context.Context, monitorService service.MonitorService) *Controller {
return &Controller{
ctx: ctx,
monitorService: monitorService,
logger: logger,
}
}
@@ -35,26 +37,27 @@ func NewController(monitorService service.MonitorService, logger *logs.Logger) *
// @Success 200 {object} controller.Response{data=dto.ListSensorDataResponse}
// @Router /api/v1/monitor/sensor-data [get]
func (c *Controller) ListSensorData(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListSensorData")
const actionType = "获取传感器数据列表"
var req dto.ListSensorDataRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListSensorData(&req)
resp, err := c.monitorService.ListSensorData(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取传感器数据失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取传感器数据成功", resp, actionType, "获取传感器数据成功", req)
}
@@ -68,26 +71,27 @@ func (c *Controller) ListSensorData(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListDeviceCommandLogResponse}
// @Router /api/v1/monitor/device-command-logs [get]
func (c *Controller) ListDeviceCommandLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListDeviceCommandLogs")
const actionType = "获取设备命令日志列表"
var req dto.ListDeviceCommandLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListDeviceCommandLogs(&req)
resp, err := c.monitorService.ListDeviceCommandLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备命令日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备命令日志成功", resp, actionType, "获取设备命令日志成功", req)
}
@@ -101,26 +105,27 @@ func (c *Controller) ListDeviceCommandLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListPlanExecutionLogResponse}
// @Router /api/v1/monitor/plan-execution-logs [get]
func (c *Controller) ListPlanExecutionLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPlanExecutionLogs")
const actionType = "获取计划执行日志列表"
var req dto.ListPlanExecutionLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListPlanExecutionLogs(&req)
resp, err := c.monitorService.ListPlanExecutionLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划执行日志成功", resp, actionType, "获取计划执行日志成功", req)
}
@@ -134,26 +139,27 @@ func (c *Controller) ListPlanExecutionLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListTaskExecutionLogResponse}
// @Router /api/v1/monitor/task-execution-logs [get]
func (c *Controller) ListTaskExecutionLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListTaskExecutionLogs")
const actionType = "获取任务执行日志列表"
var req dto.ListTaskExecutionLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListTaskExecutionLogs(&req)
resp, err := c.monitorService.ListTaskExecutionLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取任务执行日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取任务执行日志成功", resp, actionType, "获取任务执行日志成功", req)
}
@@ -167,26 +173,27 @@ func (c *Controller) ListTaskExecutionLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListPendingCollectionResponse}
// @Router /api/v1/monitor/pending-collections [get]
func (c *Controller) ListPendingCollections(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPendingCollections")
const actionType = "获取待采集请求列表"
var req dto.ListPendingCollectionRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListPendingCollections(&req)
resp, err := c.monitorService.ListPendingCollections(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取待采集请求失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取待采集请求成功", resp, actionType, "获取待采集请求成功", req)
}
@@ -200,26 +207,27 @@ func (c *Controller) ListPendingCollections(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListUserActionLogResponse}
// @Router /api/v1/monitor/user-action-logs [get]
func (c *Controller) ListUserActionLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListUserActionLogs")
const actionType = "获取用户操作日志列表"
var req dto.ListUserActionLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListUserActionLogs(&req)
resp, err := c.monitorService.ListUserActionLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户操作日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作日志成功", resp, actionType, "获取用户操作日志成功", req)
}
@@ -233,26 +241,27 @@ func (c *Controller) ListUserActionLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialPurchaseResponse}
// @Router /api/v1/monitor/raw-material-purchases [get]
func (c *Controller) ListRawMaterialPurchases(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRawMaterialPurchases")
const actionType = "获取原料采购记录列表"
var req dto.ListRawMaterialPurchaseRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListRawMaterialPurchases(&req)
resp, err := c.monitorService.ListRawMaterialPurchases(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料采购记录成功", resp, actionType, "获取原料采购记录成功", req)
}
@@ -266,26 +275,27 @@ func (c *Controller) ListRawMaterialPurchases(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialStockLogResponse}
// @Router /api/v1/monitor/raw-material-stock-logs [get]
func (c *Controller) ListRawMaterialStockLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRawMaterialStockLogs")
const actionType = "获取原料库存日志列表"
var req dto.ListRawMaterialStockLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListRawMaterialStockLogs(&req)
resp, err := c.monitorService.ListRawMaterialStockLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料库存日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料库存日志成功", resp, actionType, "获取原料库存日志成功", req)
}
@@ -299,26 +309,27 @@ func (c *Controller) ListRawMaterialStockLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListFeedUsageRecordResponse}
// @Router /api/v1/monitor/feed-usage-records [get]
func (c *Controller) ListFeedUsageRecords(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListFeedUsageRecords")
const actionType = "获取饲料使用记录列表"
var req dto.ListFeedUsageRecordRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListFeedUsageRecords(&req)
resp, err := c.monitorService.ListFeedUsageRecords(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取饲料使用记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取饲料使用记录成功", resp, actionType, "获取饲料使用记录成功", req)
}
@@ -332,26 +343,27 @@ func (c *Controller) ListFeedUsageRecords(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListMedicationLogResponse}
// @Router /api/v1/monitor/medication-logs [get]
func (c *Controller) ListMedicationLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListMedicationLogs")
const actionType = "获取用药记录列表"
var req dto.ListMedicationLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListMedicationLogs(&req)
resp, err := c.monitorService.ListMedicationLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用药记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用药记录成功", resp, actionType, "获取用药记录成功", req)
}
@@ -365,26 +377,27 @@ func (c *Controller) ListMedicationLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListPigBatchLogResponse}
// @Router /api/v1/monitor/pig-batch-logs [get]
func (c *Controller) ListPigBatchLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigBatchLogs")
const actionType = "获取猪批次日志列表"
var req dto.ListPigBatchLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListPigBatchLogs(&req)
resp, err := c.monitorService.ListPigBatchLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪批次日志成功", resp, actionType, "获取猪批次日志成功", req)
}
@@ -398,26 +411,27 @@ func (c *Controller) ListPigBatchLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListWeighingBatchResponse}
// @Router /api/v1/monitor/weighing-batches [get]
func (c *Controller) ListWeighingBatches(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListWeighingBatches")
const actionType = "获取批次称重记录列表"
var req dto.ListWeighingBatchRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListWeighingBatches(&req)
resp, err := c.monitorService.ListWeighingBatches(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取批次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取批次称重记录成功", resp, actionType, "获取批次称重记录成功", req)
}
@@ -431,26 +445,27 @@ func (c *Controller) ListWeighingBatches(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListWeighingRecordResponse}
// @Router /api/v1/monitor/weighing-records [get]
func (c *Controller) ListWeighingRecords(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListWeighingRecords")
const actionType = "获取单次称重记录列表"
var req dto.ListWeighingRecordRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListWeighingRecords(&req)
resp, err := c.monitorService.ListWeighingRecords(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取单次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取单次称重记录成功", resp, actionType, "获取单次称重记录成功", req)
}
@@ -464,26 +479,27 @@ func (c *Controller) ListWeighingRecords(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListPigTransferLogResponse}
// @Router /api/v1/monitor/pig-transfer-logs [get]
func (c *Controller) ListPigTransferLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigTransferLogs")
const actionType = "获取猪只迁移日志列表"
var req dto.ListPigTransferLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListPigTransferLogs(&req)
resp, err := c.monitorService.ListPigTransferLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只迁移日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只迁移日志成功", resp, actionType, "获取猪只迁移日志成功", req)
}
@@ -497,26 +513,27 @@ func (c *Controller) ListPigTransferLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListPigSickLogResponse}
// @Router /api/v1/monitor/pig-sick-logs [get]
func (c *Controller) ListPigSickLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigSickLogs")
const actionType = "获取病猪日志列表"
var req dto.ListPigSickLogRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListPigSickLogs(&req)
resp, err := c.monitorService.ListPigSickLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取病猪日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取病猪日志成功", resp, actionType, "获取病猪日志成功", req)
}
@@ -530,26 +547,27 @@ func (c *Controller) ListPigSickLogs(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListPigPurchaseResponse}
// @Router /api/v1/monitor/pig-purchases [get]
func (c *Controller) ListPigPurchases(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigPurchases")
const actionType = "获取猪只采购记录列表"
var req dto.ListPigPurchaseRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListPigPurchases(&req)
resp, err := c.monitorService.ListPigPurchases(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只采购记录成功", resp, actionType, "获取猪只采购记录成功", req)
}
@@ -563,26 +581,27 @@ func (c *Controller) ListPigPurchases(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListPigSaleResponse}
// @Router /api/v1/monitor/pig-sales [get]
func (c *Controller) ListPigSales(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigSales")
const actionType = "获取猪只售卖记录列表"
var req dto.ListPigSaleRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListPigSales(&req)
resp, err := c.monitorService.ListPigSales(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只售卖记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只售卖记录成功", resp, actionType, "获取猪只售卖记录成功", req)
}
@@ -596,25 +615,26 @@ func (c *Controller) ListPigSales(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListNotificationResponse}
// @Router /api/v1/monitor/notifications [get]
func (c *Controller) ListNotifications(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListNotifications")
const actionType = "批量查询通知"
var req dto.ListNotificationRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListNotifications(&req)
resp, err := c.monitorService.ListNotifications(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "批量查询通知失败: "+err.Error(), actionType, "服务层查询失败", req)
}
c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "批量查询通知成功", resp, actionType, "批量查询通知成功", req)
}

View File

@@ -1,6 +1,7 @@
package plan
import (
"context"
"errors"
"strconv"
@@ -9,6 +10,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
@@ -16,14 +18,14 @@ import (
// Controller 定义了计划相关的控制器
type Controller struct {
logger *logs.Logger
ctx context.Context
planService service.PlanService
}
// NewController 创建一个新的 Controller 实例
func NewController(logger *logs.Logger, planService service.PlanService) *Controller {
func NewController(ctx context.Context, planService service.PlanService) *Controller {
return &Controller{
logger: logger,
ctx: ctx,
planService: planService,
}
}
@@ -41,17 +43,18 @@ func NewController(logger *logs.Logger, planService service.PlanService) *Contro
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为201代表创建成功"
// @Router /api/v1/plans [post]
func (c *Controller) CreatePlan(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreatePlan")
var req dto.CreatePlanRequest
const actionType = "创建计划"
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
// 调用服务层创建计划
resp, err := c.planService.CreatePlan(&req)
resp, err := c.planService.CreatePlan(reqCtx, &req)
if err != nil {
c.logger.Errorf("%s: 服务层创建计划失败: %v", actionType, err)
logger.Errorf("%s: 服务层创建计划失败: %v", actionType, err)
// 根据服务层返回的错误类型转换为相应的HTTP状态码
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划数据校验失败或关联计划不存在", req)
@@ -60,7 +63,7 @@ func (c *Controller) CreatePlan(ctx echo.Context) error {
}
// 使用统一的成功响应函数
c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 计划创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "计划创建成功", resp, actionType, "计划创建成功", resp)
}
@@ -74,19 +77,20 @@ func (c *Controller) CreatePlan(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表成功获取"
// @Router /api/v1/plans/{id} [get]
func (c *Controller) GetPlan(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetPlan")
const actionType = "获取计划详情"
// 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
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(uint(id))
resp, err := c.planService.GetPlanByID(reqCtx, uint(id))
if err != nil {
c.logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
}
@@ -94,7 +98,7 @@ func (c *Controller) GetPlan(ctx echo.Context) error {
}
// 4. 发送成功响应
c.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划详情成功", resp, actionType, "获取计划详情成功", resp)
}
@@ -108,21 +112,22 @@ func (c *Controller) GetPlan(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.ListPlansResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/plans [get]
func (c *Controller) ListPlans(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPlans")
const actionType = "获取计划列表"
var query dto.ListPlansQuery
if err := ctx.Bind(&query); err != nil {
c.logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", query)
}
// 调用服务层获取计划列表
resp, err := c.planService.ListPlans(&query)
resp, err := c.planService.ListPlans(reqCtx, &query)
if err != nil {
c.logger.Errorf("%s: 服务层获取计划列表失败: %v", actionType, err)
logger.Errorf("%s: 服务层获取计划列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: "+err.Error(), actionType, "服务层获取计划列表失败", nil)
}
c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(resp.Plans))
logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(resp.Plans))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划列表成功", resp, actionType, "获取计划列表成功", resp)
}
@@ -138,26 +143,27 @@ func (c *Controller) ListPlans(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表更新成功"
// @Router /api/v1/plans/{id} [put]
func (c *Controller) UpdatePlan(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdatePlan")
const actionType = "更新计划"
// 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
}
// 2. 绑定请求体
var req dto.UpdatePlanRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
// 调用服务层更新计划
resp, err := c.planService.UpdatePlan(uint(id), &req)
resp, err := c.planService.UpdatePlan(reqCtx, uint(id), &req)
if err != nil {
c.logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
} else if errors.Is(err, plan.ErrPlanCannotBeModified) { // 修改为 plan.ErrPlanCannotBeModified
@@ -167,7 +173,7 @@ func (c *Controller) UpdatePlan(ctx echo.Context) error {
}
// 9. 发送成功响应
c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, resp.ID)
logger.Infof("%s: 计划更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划更新成功", resp, actionType, "计划更新成功", resp)
}
@@ -181,19 +187,20 @@ func (c *Controller) UpdatePlan(ctx echo.Context) error {
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
// @Router /api/v1/plans/{id} [delete]
func (c *Controller) DeletePlan(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeletePlan")
const actionType = "删除计划"
// 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
}
// 调用服务层删除计划
err = c.planService.DeletePlan(uint(id))
err = c.planService.DeletePlan(reqCtx, uint(id))
if err != nil {
c.logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
} else if errors.Is(err, plan.ErrPlanCannotBeDeleted) { // 修改为 plan.ErrPlanCannotBeDeleted
@@ -203,7 +210,7 @@ func (c *Controller) DeletePlan(ctx echo.Context) error {
}
// 6. 发送成功响应
c.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划删除成功", nil, actionType, "计划删除成功", id)
}
@@ -217,19 +224,20 @@ func (c *Controller) DeletePlan(ctx echo.Context) error {
// @Success 200 {object} controller.Response "业务码为200代表成功启动计划"
// @Router /api/v1/plans/{id}/start [post]
func (c *Controller) StartPlan(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "StartPlan")
const actionType = "启动计划"
// 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
}
// 调用服务层启动计划
err = c.planService.StartPlan(uint(id))
err = c.planService.StartPlan(reqCtx, uint(id))
if err != nil {
c.logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
} else if errors.Is(err, plan.ErrPlanCannotBeStarted) { // 修改为 plan.ErrPlanCannotBeStarted
@@ -241,7 +249,7 @@ func (c *Controller) StartPlan(ctx echo.Context) error {
}
// 6. 发送成功响应
c.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功启动", nil, actionType, "计划已成功启动", id)
}
@@ -255,19 +263,20 @@ func (c *Controller) StartPlan(ctx echo.Context) error {
// @Success 200 {object} controller.Response "业务码为200代表成功停止计划"
// @Router /api/v1/plans/{id}/stop [post]
func (c *Controller) StopPlan(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "StopPlan")
const actionType = "停止计划"
// 1. 从 URL 路径中获取 ID
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
logger.Errorf("%s: 计划ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr)
}
// 调用服务层停止计划
err = c.planService.StopPlan(uint(id))
err = c.planService.StopPlan(reqCtx, uint(id))
if err != nil {
c.logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, plan.ErrPlanNotFound) { // 修改为 plan.ErrPlanNotFound
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id)
} else if errors.Is(err, plan.ErrPlanCannotBeStopped) { // 修改为 plan.ErrPlanCannotBeStopped
@@ -279,6 +288,6 @@ func (c *Controller) StopPlan(ctx echo.Context) error {
}
// 6. 发送成功响应
c.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划已成功停止", nil, actionType, "计划已成功停止", id)
}

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/labstack/echo/v4"
)

View File

@@ -1,29 +1,31 @@
package user
import (
"context"
"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"
"github.com/labstack/echo/v4"
)
// Controller 用户控制器
type Controller struct {
ctx context.Context
userService service.UserService
logger *logs.Logger
}
// NewController 创建用户控制器实例
func NewController(
ctx context.Context,
userService service.UserService,
logger *logs.Logger,
) *Controller {
return &Controller{
ctx: ctx,
userService: userService,
logger: logger,
}
}
@@ -39,15 +41,17 @@ func NewController(
// @Success 200 {object} controller.Response{data=dto.CreateUserResponse} "业务码为201代表创建成功"
// @Router /api/v1/users [post]
func (c *Controller) CreateUser(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreateUser")
var req dto.CreateUserRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("创建用户: 参数绑定失败: %v", err)
logger.Errorf("创建用户: 参数绑定失败: %v", err)
return controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
}
resp, err := c.userService.CreateUser(&req)
resp, err := c.userService.CreateUser(reqCtx, &req)
if err != nil {
c.logger.Errorf("创建用户: 服务层调用失败: %v", err)
logger.Errorf("创建用户: 服务层调用失败: %v", err)
return controller.SendErrorResponse(ctx, controller.CodeInternalError, err.Error())
}
@@ -64,15 +68,17 @@ func (c *Controller) CreateUser(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=dto.LoginResponse} "业务码为200代表登录成功"
// @Router /api/v1/users/login [post]
func (c *Controller) Login(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "Login")
var req dto.LoginRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("登录: 参数绑定失败: %v", err)
logger.Errorf("登录: 参数绑定失败: %v", err)
return controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error())
}
resp, err := c.userService.Login(&req)
resp, err := c.userService.Login(reqCtx, &req)
if err != nil {
c.logger.Errorf("登录: 服务层调用失败: %v", err)
logger.Errorf("登录: 服务层调用失败: %v", err)
return controller.SendErrorResponse(ctx, controller.CodeUnauthorized, err.Error())
}
@@ -91,30 +97,31 @@ func (c *Controller) Login(ctx echo.Context) error {
// @Success 200 {object} controller.Response{data=string} "成功响应"
// @Router /api/v1/users/{id}/notifications/test [post]
func (c *Controller) SendTestNotification(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "SendTestNotification")
const actionType = "发送测试通知"
// 1. 从 URL 中获取用户 ID
userID, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
c.logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err)
logger.Errorf("%s: 无效的用户ID格式: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", ctx.Param("id"))
}
// 2. 从请求体 (JSON Body) 中获取要测试的通知类型
var req dto.SendTestNotificationRequest
if err := ctx.Bind(&req); err != nil {
c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "请求体格式错误或缺少 'type' 字段: "+err.Error(), actionType, "请求体绑定失败", req)
}
// 3. 调用服务层
err = c.userService.SendTestNotification(uint(userID), &req)
err = c.userService.SendTestNotification(reqCtx, uint(userID), &req)
if err != nil {
c.logger.Errorf("%s: 服务层调用失败: %v", actionType, err)
logger.Errorf("%s: 服务层调用失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", map[string]interface{}{"userID": userID, "type": req.Type})
}
// 4. 返回成功响应
c.logger.Infof("%s: 成功为用户 %d 发送类型为 %s 的测试消息", actionType, userID, req.Type)
logger.Infof("%s: 成功为用户 %d 发送类型为 %s 的测试消息", actionType, userID, req.Type)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "测试消息已发送,请检查您的接收端。", nil, actionType, "测试消息发送成功", map[string]interface{}{"userID": userID, "type": req.Type})
}

View File

@@ -2,6 +2,7 @@ package dto
import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"go.uber.org/zap/zapcore"
)

View File

@@ -5,6 +5,7 @@ import (
"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"
)

View File

@@ -1,16 +1,22 @@
package middleware
import (
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/labstack/echo/v4"
)
// AuditLogMiddleware 创建一个Echo中间件用于在请求结束后记录用户操作审计日志。
// 它依赖于控制器通过调用 SendSuccessWithAudit 或 SendErrorWithAudit 在上下文中设置的审计信息。
func AuditLogMiddleware(auditService audit.Service) echo.MiddlewareFunc {
func AuditLogMiddleware(ctx context.Context, auditService service.AuditService) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
newCtx := logs.AddFuncName(c.Request().Context(), ctx, "AuditLogMiddleware")
// 首先执行请求链中的后续处理程序(即业务控制器)
err := next(c)
@@ -29,8 +35,8 @@ func AuditLogMiddleware(auditService audit.Service) echo.MiddlewareFunc {
user, _ = userCtx.(*models.User)
}
// 构建 RequestContext
reqCtx := audit.RequestContext{
// 构建 AuditRequestContext
reqCtx := service.AuditRequestContext{
ClientIP: c.RealIP(),
HTTPPath: c.Request().URL.Path,
HTTPMethod: c.Request().Method,
@@ -42,8 +48,12 @@ func AuditLogMiddleware(auditService audit.Service) echo.MiddlewareFunc {
status, _ := c.Get(models.ContextAuditStatus.String()).(models.AuditStatus)
resultDetails, _ := c.Get(models.ContextAuditResultDetails.String()).(string)
// 为异步任务创建一个分离的 Context以防止原始请求的 Context 被取消
detachedCtx := logs.DetachContext(newCtx)
// 调用审计服务记录日志(异步)
auditService.LogAction(
detachedCtx,
user,
reqCtx,
actionType,

View File

@@ -2,23 +2,28 @@
package middleware
import (
"context"
"errors"
"net/http"
"strings"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
// AuthMiddleware 创建一个Echo中间件用于JWT身份验证
// 它依赖于 TokenService 来解析和验证 token并使用 UserRepository 来获取完整的用户信息
func AuthMiddleware(tokenService token.Service, userRepo repository.UserRepository) echo.MiddlewareFunc {
func AuthMiddleware(ctx context.Context, tokenGenerator token.Generator, userRepo repository.UserRepository) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
reqCtx := logs.AddFuncName(c.Request().Context(), ctx, "AuthMiddleware")
// 从 Authorization header 获取 token
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" {
@@ -34,13 +39,13 @@ func AuthMiddleware(tokenService token.Service, userRepo repository.UserReposito
tokenString := parts[1]
// 解析和验证 token
claims, err := tokenService.ParseToken(tokenString)
claims, err := tokenGenerator.ParseToken(tokenString)
if err != nil {
return controller.SendErrorWithStatus(c, http.StatusUnauthorized, controller.CodeUnauthorized, "无效的Token")
}
// 根据 token 中的用户ID从数据库中获取完整的用户信息
user, err := userRepo.FindByID(claims.UserID)
user, err := userRepo.FindByID(reqCtx, claims.UserID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Token有效但对应的用户已不存在

View File

@@ -0,0 +1,74 @@
// Package audit 提供了用户操作审计相关的功能
package service
import (
"context"
"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"
)
// AuditRequestContext 封装了审计日志所需的请求上下文信息
type AuditRequestContext struct {
ClientIP string
HTTPPath string
HTTPMethod string
}
// AuditService 定义了审计服务的接口
type AuditService interface {
LogAction(ctx context.Context, user *models.User, reqCtx AuditRequestContext, actionType, description string, targetResource interface{}, status models.AuditStatus, resultDetails string)
}
// auditService 是 AuditService 接口的实现
type auditService struct {
ctx context.Context
userActionLogRepository repository.UserActionLogRepository
}
// NewAuditService 创建一个新的审计服务实例
func NewAuditService(ctx context.Context, repo repository.UserActionLogRepository) AuditService {
return &auditService{
ctx: ctx,
userActionLogRepository: repo,
}
}
// LogAction 记录一个用户操作。它在一个新的 goroutine 中异步执行,以避免阻塞主请求。
func (s *auditService) LogAction(ctx context.Context, user *models.User, reqCtx AuditRequestContext, actionType, description string, targetResource interface{}, status models.AuditStatus, resultDetails string) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "LogAction")
// 不再从 context 中获取用户信息,直接使用传入的 user 对象
if user == nil {
logger.Warnw("无法记录审计日志:传入的用户对象为 nil")
return
}
log := &models.UserActionLog{
Time: time.Now(),
UserID: user.ID,
Username: user.Username, // 用户名快照
SourceIP: reqCtx.ClientIP,
ActionType: actionType,
Description: description,
Status: status,
HTTPPath: reqCtx.HTTPPath,
HTTPMethod: reqCtx.HTTPMethod,
ResultDetails: resultDetails,
}
// 使用模型提供的方法来设置 TargetResource
if err := log.SetTargetResource(targetResource); err != nil {
logger.Errorw("无法记录审计日志:序列化 targetResource 失败", "error", err)
// 即使序列化失败,我们可能仍然希望记录操作本身,所以不在此处 return
}
// 异步写入数据库,不阻塞当前请求
go func() {
if err := s.userActionLogRepository.Create(serviceCtx, log); err != nil {
logger.Errorw("异步保存审计日志失败", "error", err)
}
}()
}

View File

@@ -1,12 +1,14 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
)
@@ -25,42 +27,45 @@ var (
// DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。
// DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。
type DeviceService interface {
CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error)
GetDevice(id uint) (*dto.DeviceResponse, error)
ListDevices() ([]*dto.DeviceResponse, error)
UpdateDevice(id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error)
DeleteDevice(id uint) error
ManualControl(id uint, req *dto.ManualControlDeviceRequest) error
CreateDevice(ctx context.Context, req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error)
GetDevice(ctx context.Context, id uint) (*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
CreateAreaController(req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error)
GetAreaController(id uint) (*dto.AreaControllerResponse, error)
ListAreaControllers() ([]*dto.AreaControllerResponse, error)
UpdateAreaController(id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error)
DeleteAreaController(id uint) error
CreateAreaController(ctx context.Context, req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error)
GetAreaController(ctx context.Context, id uint) (*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
CreateDeviceTemplate(req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
GetDeviceTemplate(id uint) (*dto.DeviceTemplateResponse, error)
ListDeviceTemplates() ([]*dto.DeviceTemplateResponse, error)
UpdateDeviceTemplate(id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
DeleteDeviceTemplate(id uint) error
CreateDeviceTemplate(ctx context.Context, req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error)
GetDeviceTemplate(ctx context.Context, id uint) (*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
}
// deviceService 是 DeviceService 接口的具体实现。
type deviceService struct {
ctx context.Context
deviceRepo repository.DeviceRepository
areaControllerRepo repository.AreaControllerRepository
deviceTemplateRepo repository.DeviceTemplateRepository
deviceDomainSvc device.Service // 依赖领域服务
deviceDomainSvc device.Service
}
// NewDeviceService 创建一个新的 DeviceService 实例。
func NewDeviceService(
ctx context.Context,
deviceRepo repository.DeviceRepository,
areaControllerRepo repository.AreaControllerRepository,
deviceTemplateRepo repository.DeviceTemplateRepository,
deviceDomainSvc device.Service,
) DeviceService {
return &deviceService{
ctx: ctx,
deviceRepo: deviceRepo,
areaControllerRepo: areaControllerRepo,
deviceTemplateRepo: deviceTemplateRepo,
@@ -70,7 +75,8 @@ func NewDeviceService(
// --- Devices ---
func (s *deviceService) CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) {
func (s *deviceService) CreateDevice(ctx context.Context, req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateDevice")
propertiesJSON, err := json.Marshal(req.Properties)
if err != nil {
return nil, err // Consider wrapping this error for better context
@@ -88,11 +94,11 @@ func (s *deviceService) CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceR
return nil, err
}
if err := s.deviceRepo.Create(device); err != nil {
if err := s.deviceRepo.Create(serviceCtx, device); err != nil {
return nil, err
}
createdDevice, err := s.deviceRepo.FindByID(device.ID)
createdDevice, err := s.deviceRepo.FindByID(serviceCtx, device.ID)
if err != nil {
return nil, err
}
@@ -100,24 +106,27 @@ func (s *deviceService) CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceR
return dto.NewDeviceResponse(createdDevice)
}
func (s *deviceService) GetDevice(id uint) (*dto.DeviceResponse, error) {
device, err := s.deviceRepo.FindByID(id)
func (s *deviceService) GetDevice(ctx context.Context, id uint) (*dto.DeviceResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDevice")
device, err := s.deviceRepo.FindByID(serviceCtx, id)
if err != nil {
return nil, err
}
return dto.NewDeviceResponse(device)
}
func (s *deviceService) ListDevices() ([]*dto.DeviceResponse, error) {
devices, err := s.deviceRepo.ListAll()
func (s *deviceService) ListDevices(ctx context.Context) ([]*dto.DeviceResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListDevices")
devices, err := s.deviceRepo.ListAll(serviceCtx)
if err != nil {
return nil, err
}
return dto.NewListDeviceResponse(devices)
}
func (s *deviceService) UpdateDevice(id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) {
existingDevice, err := s.deviceRepo.FindByID(id)
func (s *deviceService) UpdateDevice(ctx context.Context, id uint, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateDevice")
existingDevice, err := s.deviceRepo.FindByID(serviceCtx, id)
if err != nil {
return nil, err
}
@@ -137,11 +146,11 @@ func (s *deviceService) UpdateDevice(id uint, req *dto.UpdateDeviceRequest) (*dt
return nil, err
}
if err := s.deviceRepo.Update(existingDevice); err != nil {
if err := s.deviceRepo.Update(serviceCtx, existingDevice); err != nil {
return nil, err
}
updatedDevice, err := s.deviceRepo.FindByID(existingDevice.ID)
updatedDevice, err := s.deviceRepo.FindByID(serviceCtx, existingDevice.ID)
if err != nil {
return nil, err
}
@@ -149,16 +158,17 @@ func (s *deviceService) UpdateDevice(id uint, req *dto.UpdateDeviceRequest) (*dt
return dto.NewDeviceResponse(updatedDevice)
}
func (s *deviceService) DeleteDevice(id uint) error {
func (s *deviceService) DeleteDevice(ctx context.Context, id uint) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteDevice")
// 检查设备是否存在
_, err := s.deviceRepo.FindByID(id)
_, err := s.deviceRepo.FindByID(serviceCtx, id)
if err != nil {
return err // 如果未找到,会返回 gorm.ErrRecordNotFound
}
// 在删除前检查设备是否被任务使用
inUse, err := s.deviceRepo.IsDeviceInUse(id)
inUse, err := s.deviceRepo.IsDeviceInUse(serviceCtx, id)
if err != nil {
// 如果检查过程中发生数据库错误,则返回错误
return fmt.Errorf("检查设备使用情况失败: %w", err)
@@ -169,17 +179,18 @@ func (s *deviceService) DeleteDevice(id uint) error {
}
// 只有在未被使用时,才执行删除操作
return s.deviceRepo.Delete(id)
return s.deviceRepo.Delete(serviceCtx, id)
}
func (s *deviceService) ManualControl(id uint, req *dto.ManualControlDeviceRequest) error {
dev, err := s.deviceRepo.FindByID(id)
func (s *deviceService) ManualControl(ctx context.Context, id uint, req *dto.ManualControlDeviceRequest) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ManualControl")
dev, err := s.deviceRepo.FindByID(serviceCtx, id)
if err != nil {
return err
}
if req.Action == nil {
return s.deviceDomainSvc.Collect(dev.AreaControllerID, []*models.Device{dev})
return s.deviceDomainSvc.Collect(serviceCtx, dev.AreaControllerID, []*models.Device{dev})
} else {
action := device.DeviceActionStart
switch *req.Action {
@@ -190,13 +201,14 @@ func (s *deviceService) ManualControl(id uint, req *dto.ManualControlDeviceReque
default:
return errors.New("invalid action")
}
return s.deviceDomainSvc.Switch(dev, action)
return s.deviceDomainSvc.Switch(serviceCtx, dev, action)
}
}
// --- Area Controllers ---
func (s *deviceService) CreateAreaController(req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error) {
func (s *deviceService) CreateAreaController(ctx context.Context, req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateAreaController")
propertiesJSON, err := json.Marshal(req.Properties)
if err != nil {
return nil, err
@@ -213,31 +225,34 @@ func (s *deviceService) CreateAreaController(req *dto.CreateAreaControllerReques
return nil, err
}
if err := s.areaControllerRepo.Create(ac); err != nil {
if err := s.areaControllerRepo.Create(serviceCtx, ac); err != nil {
return nil, err
}
return dto.NewAreaControllerResponse(ac)
}
func (s *deviceService) GetAreaController(id uint) (*dto.AreaControllerResponse, error) {
ac, err := s.areaControllerRepo.FindByID(id)
func (s *deviceService) GetAreaController(ctx context.Context, id uint) (*dto.AreaControllerResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetAreaController")
ac, err := s.areaControllerRepo.FindByID(serviceCtx, id)
if err != nil {
return nil, err
}
return dto.NewAreaControllerResponse(ac)
}
func (s *deviceService) ListAreaControllers() ([]*dto.AreaControllerResponse, error) {
acs, err := s.areaControllerRepo.ListAll()
func (s *deviceService) ListAreaControllers(ctx context.Context) ([]*dto.AreaControllerResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListAreaControllers")
acs, err := s.areaControllerRepo.ListAll(serviceCtx)
if err != nil {
return nil, err
}
return dto.NewListAreaControllerResponse(acs)
}
func (s *deviceService) UpdateAreaController(id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) {
existingAC, err := s.areaControllerRepo.FindByID(id)
func (s *deviceService) UpdateAreaController(ctx context.Context, id uint, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateAreaController")
existingAC, err := s.areaControllerRepo.FindByID(serviceCtx, id)
if err != nil {
return nil, err
}
@@ -256,23 +271,24 @@ func (s *deviceService) UpdateAreaController(id uint, req *dto.UpdateAreaControl
return nil, err
}
if err := s.areaControllerRepo.Update(existingAC); err != nil {
if err := s.areaControllerRepo.Update(serviceCtx, existingAC); err != nil {
return nil, err
}
return dto.NewAreaControllerResponse(existingAC)
}
func (s *deviceService) DeleteAreaController(id uint) error {
func (s *deviceService) DeleteAreaController(ctx context.Context, id uint) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteAreaController")
// 1. 检查是否存在
_, err := s.areaControllerRepo.FindByID(id)
_, err := s.areaControllerRepo.FindByID(serviceCtx, id)
if err != nil {
return err // 如果未找到gorm会返回 ErrRecordNotFound
}
// 2. 检查是否被使用(业务逻辑)
inUse, err := s.deviceRepo.IsAreaControllerInUse(id)
inUse, err := s.deviceRepo.IsAreaControllerInUse(serviceCtx, id)
if err != nil {
return err // 返回数据库检查错误
}
@@ -281,12 +297,13 @@ func (s *deviceService) DeleteAreaController(id uint) error {
}
// 3. 执行删除
return s.areaControllerRepo.Delete(id)
return s.areaControllerRepo.Delete(serviceCtx, id)
}
// --- Device Templates ---
func (s *deviceService) CreateDeviceTemplate(req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) {
func (s *deviceService) CreateDeviceTemplate(ctx context.Context, req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateDeviceTemplate")
commandsJSON, err := json.Marshal(req.Commands)
if err != nil {
return nil, err
@@ -310,31 +327,34 @@ func (s *deviceService) CreateDeviceTemplate(req *dto.CreateDeviceTemplateReques
return nil, err
}
if err := s.deviceTemplateRepo.Create(deviceTemplate); err != nil {
if err := s.deviceTemplateRepo.Create(serviceCtx, deviceTemplate); err != nil {
return nil, err
}
return dto.NewDeviceTemplateResponse(deviceTemplate)
}
func (s *deviceService) GetDeviceTemplate(id uint) (*dto.DeviceTemplateResponse, error) {
deviceTemplate, err := s.deviceTemplateRepo.FindByID(id)
func (s *deviceService) GetDeviceTemplate(ctx context.Context, id uint) (*dto.DeviceTemplateResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetDeviceTemplate")
deviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id)
if err != nil {
return nil, err
}
return dto.NewDeviceTemplateResponse(deviceTemplate)
}
func (s *deviceService) ListDeviceTemplates() ([]*dto.DeviceTemplateResponse, error) {
deviceTemplates, err := s.deviceTemplateRepo.ListAll()
func (s *deviceService) ListDeviceTemplates(ctx context.Context) ([]*dto.DeviceTemplateResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListDeviceTemplates")
deviceTemplates, err := s.deviceTemplateRepo.ListAll(serviceCtx)
if err != nil {
return nil, err
}
return dto.NewListDeviceTemplateResponse(deviceTemplates)
}
func (s *deviceService) UpdateDeviceTemplate(id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) {
existingDeviceTemplate, err := s.deviceTemplateRepo.FindByID(id)
func (s *deviceService) UpdateDeviceTemplate(ctx context.Context, id uint, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateDeviceTemplate")
existingDeviceTemplate, err := s.deviceTemplateRepo.FindByID(serviceCtx, id)
if err != nil {
return nil, err
}
@@ -360,23 +380,24 @@ func (s *deviceService) UpdateDeviceTemplate(id uint, req *dto.UpdateDeviceTempl
return nil, err
}
if err := s.deviceTemplateRepo.Update(existingDeviceTemplate); err != nil {
if err := s.deviceTemplateRepo.Update(serviceCtx, existingDeviceTemplate); err != nil {
return nil, err
}
return dto.NewDeviceTemplateResponse(existingDeviceTemplate)
}
func (s *deviceService) DeleteDeviceTemplate(id uint) error {
func (s *deviceService) DeleteDeviceTemplate(ctx context.Context, id uint) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteDeviceTemplate")
// 1. 检查是否存在
_, err := s.deviceTemplateRepo.FindByID(id)
_, err := s.deviceTemplateRepo.FindByID(serviceCtx, id)
if err != nil {
return err
}
// 2. 检查是否被使用(业务逻辑)
inUse, err := s.deviceTemplateRepo.IsInUse(id)
inUse, err := s.deviceTemplateRepo.IsInUse(serviceCtx, id)
if err != nil {
return err
}
@@ -385,5 +406,5 @@ func (s *deviceService) DeleteDeviceTemplate(id uint) error {
}
// 3. 执行删除
return s.deviceTemplateRepo.Delete(id)
return s.deviceTemplateRepo.Delete(serviceCtx, id)
}

View File

@@ -1,35 +1,39 @@
package service
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"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"
)
// MonitorService 定义了监控相关的业务逻辑服务接口
type MonitorService interface {
ListSensorData(req *dto.ListSensorDataRequest) (*dto.ListSensorDataResponse, error)
ListDeviceCommandLogs(req *dto.ListDeviceCommandLogRequest) (*dto.ListDeviceCommandLogResponse, error)
ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequest) (*dto.ListPlanExecutionLogResponse, error)
ListTaskExecutionLogs(req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error)
ListPendingCollections(req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error)
ListUserActionLogs(req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error)
ListRawMaterialPurchases(req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error)
ListRawMaterialStockLogs(req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error)
ListFeedUsageRecords(req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error)
ListMedicationLogs(req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error)
ListPigBatchLogs(req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error)
ListWeighingBatches(req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error)
ListWeighingRecords(req *dto.ListWeighingRecordRequest) (*dto.ListWeighingRecordResponse, error)
ListPigTransferLogs(req *dto.ListPigTransferLogRequest) (*dto.ListPigTransferLogResponse, error)
ListPigSickLogs(req *dto.ListPigSickLogRequest) (*dto.ListPigSickLogResponse, error)
ListPigPurchases(req *dto.ListPigPurchaseRequest) (*dto.ListPigPurchaseResponse, error)
ListPigSales(req *dto.ListPigSaleRequest) (*dto.ListPigSaleResponse, error)
ListNotifications(req *dto.ListNotificationRequest) (*dto.ListNotificationResponse, error)
ListSensorData(ctx context.Context, req *dto.ListSensorDataRequest) (*dto.ListSensorDataResponse, error)
ListDeviceCommandLogs(ctx context.Context, req *dto.ListDeviceCommandLogRequest) (*dto.ListDeviceCommandLogResponse, error)
ListPlanExecutionLogs(ctx context.Context, req *dto.ListPlanExecutionLogRequest) (*dto.ListPlanExecutionLogResponse, error)
ListTaskExecutionLogs(ctx context.Context, req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error)
ListPendingCollections(ctx context.Context, req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error)
ListUserActionLogs(ctx context.Context, req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error)
ListRawMaterialPurchases(ctx context.Context, req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error)
ListRawMaterialStockLogs(ctx context.Context, req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error)
ListFeedUsageRecords(ctx context.Context, req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error)
ListMedicationLogs(ctx context.Context, req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error)
ListPigBatchLogs(ctx context.Context, req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error)
ListWeighingBatches(ctx context.Context, req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error)
ListWeighingRecords(ctx context.Context, req *dto.ListWeighingRecordRequest) (*dto.ListWeighingRecordResponse, error)
ListPigTransferLogs(ctx context.Context, req *dto.ListPigTransferLogRequest) (*dto.ListPigTransferLogResponse, error)
ListPigSickLogs(ctx context.Context, req *dto.ListPigSickLogRequest) (*dto.ListPigSickLogResponse, error)
ListPigPurchases(ctx context.Context, req *dto.ListPigPurchaseRequest) (*dto.ListPigPurchaseResponse, error)
ListPigSales(ctx context.Context, req *dto.ListPigSaleRequest) (*dto.ListPigSaleResponse, error)
ListNotifications(ctx context.Context, req *dto.ListNotificationRequest) (*dto.ListNotificationResponse, error)
}
// monitorService 是 MonitorService 接口的具体实现
type monitorService struct {
ctx context.Context
sensorDataRepo repository.SensorDataRepository
deviceCommandLogRepo repository.DeviceCommandLogRepository
executionLogRepo repository.ExecutionLogRepository
@@ -48,6 +52,7 @@ type monitorService struct {
// NewMonitorService 创建一个新的 MonitorService 实例
func NewMonitorService(
ctx context.Context,
sensorDataRepo repository.SensorDataRepository,
deviceCommandLogRepo repository.DeviceCommandLogRepository,
executionLogRepo repository.ExecutionLogRepository,
@@ -64,6 +69,7 @@ func NewMonitorService(
notificationRepo repository.NotificationRepository,
) MonitorService {
return &monitorService{
ctx: ctx,
sensorDataRepo: sensorDataRepo,
deviceCommandLogRepo: deviceCommandLogRepo,
executionLogRepo: executionLogRepo,
@@ -82,7 +88,8 @@ func NewMonitorService(
}
// ListSensorData 负责处理查询传感器数据列表的业务逻辑
func (s *monitorService) ListSensorData(req *dto.ListSensorDataRequest) (*dto.ListSensorDataResponse, error) {
func (s *monitorService) ListSensorData(ctx context.Context, req *dto.ListSensorDataRequest) (*dto.ListSensorDataResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListSensorData")
opts := repository.SensorDataListOptions{
DeviceID: req.DeviceID,
OrderBy: req.OrderBy,
@@ -94,7 +101,7 @@ func (s *monitorService) ListSensorData(req *dto.ListSensorDataRequest) (*dto.Li
opts.SensorType = &sensorType
}
data, total, err := s.sensorDataRepo.List(opts, req.Page, req.PageSize)
data, total, err := s.sensorDataRepo.List(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -103,7 +110,8 @@ func (s *monitorService) ListSensorData(req *dto.ListSensorDataRequest) (*dto.Li
}
// ListDeviceCommandLogs 负责处理查询设备命令日志列表的业务逻辑
func (s *monitorService) ListDeviceCommandLogs(req *dto.ListDeviceCommandLogRequest) (*dto.ListDeviceCommandLogResponse, error) {
func (s *monitorService) ListDeviceCommandLogs(ctx context.Context, req *dto.ListDeviceCommandLogRequest) (*dto.ListDeviceCommandLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListDeviceCommandLogs")
opts := repository.DeviceCommandLogListOptions{
DeviceID: req.DeviceID,
ReceivedSuccess: req.ReceivedSuccess,
@@ -112,7 +120,7 @@ func (s *monitorService) ListDeviceCommandLogs(req *dto.ListDeviceCommandLogRequ
EndTime: req.EndTime,
}
data, total, err := s.deviceCommandLogRepo.List(opts, req.Page, req.PageSize)
data, total, err := s.deviceCommandLogRepo.List(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -121,7 +129,8 @@ func (s *monitorService) ListDeviceCommandLogs(req *dto.ListDeviceCommandLogRequ
}
// ListPlanExecutionLogs 负责处理查询计划执行日志列表的业务逻辑
func (s *monitorService) ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequest) (*dto.ListPlanExecutionLogResponse, error) {
func (s *monitorService) ListPlanExecutionLogs(ctx context.Context, req *dto.ListPlanExecutionLogRequest) (*dto.ListPlanExecutionLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPlanExecutionLogs")
opts := repository.PlanExecutionLogListOptions{
PlanID: req.PlanID,
OrderBy: req.OrderBy,
@@ -133,7 +142,7 @@ func (s *monitorService) ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequ
opts.Status = &status
}
planLogs, total, err := s.executionLogRepo.ListPlanExecutionLogs(opts, req.Page, req.PageSize)
planLogs, total, err := s.executionLogRepo.ListPlanExecutionLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -151,7 +160,7 @@ func (s *monitorService) ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequ
planIds = append(planIds, datum.PlanID)
}
}
plans, err := s.planRepository.GetPlansByIDs(planIds)
plans, err := s.planRepository.GetPlansByIDs(serviceCtx, planIds)
if err != nil {
return nil, err
}
@@ -159,7 +168,8 @@ func (s *monitorService) ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequ
}
// ListTaskExecutionLogs 负责处理查询任务执行日志列表的业务逻辑
func (s *monitorService) ListTaskExecutionLogs(req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error) {
func (s *monitorService) ListTaskExecutionLogs(ctx context.Context, req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListTaskExecutionLogs")
opts := repository.TaskExecutionLogListOptions{
PlanExecutionLogID: req.PlanExecutionLogID,
TaskID: req.TaskID,
@@ -172,7 +182,7 @@ func (s *monitorService) ListTaskExecutionLogs(req *dto.ListTaskExecutionLogRequ
opts.Status = &status
}
data, total, err := s.executionLogRepo.ListTaskExecutionLogs(opts, req.Page, req.PageSize)
data, total, err := s.executionLogRepo.ListTaskExecutionLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -181,7 +191,8 @@ func (s *monitorService) ListTaskExecutionLogs(req *dto.ListTaskExecutionLogRequ
}
// ListPendingCollections 负责处理查询待采集请求列表的业务逻辑
func (s *monitorService) ListPendingCollections(req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error) {
func (s *monitorService) ListPendingCollections(ctx context.Context, req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPendingCollections")
opts := repository.PendingCollectionListOptions{
DeviceID: req.DeviceID,
OrderBy: req.OrderBy,
@@ -193,7 +204,7 @@ func (s *monitorService) ListPendingCollections(req *dto.ListPendingCollectionRe
opts.Status = &status
}
data, total, err := s.pendingCollectionRepo.List(opts, req.Page, req.PageSize)
data, total, err := s.pendingCollectionRepo.List(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -202,7 +213,8 @@ func (s *monitorService) ListPendingCollections(req *dto.ListPendingCollectionRe
}
// ListUserActionLogs 负责处理查询用户操作日志列表的业务逻辑
func (s *monitorService) ListUserActionLogs(req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error) {
func (s *monitorService) ListUserActionLogs(ctx context.Context, req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListUserActionLogs")
opts := repository.UserActionLogListOptions{
UserID: req.UserID,
Username: req.Username,
@@ -216,7 +228,7 @@ func (s *monitorService) ListUserActionLogs(req *dto.ListUserActionLogRequest) (
opts.Status = &status
}
data, total, err := s.userActionLogRepo.List(opts, req.Page, req.PageSize)
data, total, err := s.userActionLogRepo.List(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -225,7 +237,8 @@ func (s *monitorService) ListUserActionLogs(req *dto.ListUserActionLogRequest) (
}
// ListRawMaterialPurchases 负责处理查询原料采购记录列表的业务逻辑
func (s *monitorService) ListRawMaterialPurchases(req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error) {
func (s *monitorService) ListRawMaterialPurchases(ctx context.Context, req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterialPurchases")
opts := repository.RawMaterialPurchaseListOptions{
RawMaterialID: req.RawMaterialID,
Supplier: req.Supplier,
@@ -234,7 +247,7 @@ func (s *monitorService) ListRawMaterialPurchases(req *dto.ListRawMaterialPurcha
EndTime: req.EndTime,
}
data, total, err := s.rawMaterialRepo.ListRawMaterialPurchases(opts, req.Page, req.PageSize)
data, total, err := s.rawMaterialRepo.ListRawMaterialPurchases(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -243,7 +256,8 @@ func (s *monitorService) ListRawMaterialPurchases(req *dto.ListRawMaterialPurcha
}
// ListRawMaterialStockLogs 负责处理查询原料库存日志列表的业务逻辑
func (s *monitorService) ListRawMaterialStockLogs(req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error) {
func (s *monitorService) ListRawMaterialStockLogs(ctx context.Context, req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterialStockLogs")
opts := repository.RawMaterialStockLogListOptions{
RawMaterialID: req.RawMaterialID,
SourceID: req.SourceID,
@@ -256,7 +270,7 @@ func (s *monitorService) ListRawMaterialStockLogs(req *dto.ListRawMaterialStockL
opts.SourceType = &sourceType
}
data, total, err := s.rawMaterialRepo.ListRawMaterialStockLogs(opts, req.Page, req.PageSize)
data, total, err := s.rawMaterialRepo.ListRawMaterialStockLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -265,7 +279,8 @@ func (s *monitorService) ListRawMaterialStockLogs(req *dto.ListRawMaterialStockL
}
// ListFeedUsageRecords 负责处理查询饲料使用记录列表的业务逻辑
func (s *monitorService) ListFeedUsageRecords(req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error) {
func (s *monitorService) ListFeedUsageRecords(ctx context.Context, req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListFeedUsageRecords")
opts := repository.FeedUsageRecordListOptions{
PenID: req.PenID,
FeedFormulaID: req.FeedFormulaID,
@@ -275,7 +290,7 @@ func (s *monitorService) ListFeedUsageRecords(req *dto.ListFeedUsageRecordReques
EndTime: req.EndTime,
}
data, total, err := s.rawMaterialRepo.ListFeedUsageRecords(opts, req.Page, req.PageSize)
data, total, err := s.rawMaterialRepo.ListFeedUsageRecords(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -284,7 +299,8 @@ func (s *monitorService) ListFeedUsageRecords(req *dto.ListFeedUsageRecordReques
}
// ListMedicationLogs 负责处理查询用药记录列表的业务逻辑
func (s *monitorService) ListMedicationLogs(req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error) {
func (s *monitorService) ListMedicationLogs(ctx context.Context, req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListMedicationLogs")
opts := repository.MedicationLogListOptions{
PigBatchID: req.PigBatchID,
MedicationID: req.MedicationID,
@@ -298,7 +314,7 @@ func (s *monitorService) ListMedicationLogs(req *dto.ListMedicationLogRequest) (
opts.Reason = &reason
}
data, total, err := s.medicationRepo.ListMedicationLogs(opts, req.Page, req.PageSize)
data, total, err := s.medicationRepo.ListMedicationLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -307,7 +323,8 @@ func (s *monitorService) ListMedicationLogs(req *dto.ListMedicationLogRequest) (
}
// ListPigBatchLogs 负责处理查询猪批次日志列表的业务逻辑
func (s *monitorService) ListPigBatchLogs(req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error) {
func (s *monitorService) ListPigBatchLogs(ctx context.Context, req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigBatchLogs")
opts := repository.PigBatchLogListOptions{
PigBatchID: req.PigBatchID,
OperatorID: req.OperatorID,
@@ -320,7 +337,7 @@ func (s *monitorService) ListPigBatchLogs(req *dto.ListPigBatchLogRequest) (*dto
opts.ChangeType = &changeType
}
data, total, err := s.pigBatchLogRepo.List(opts, req.Page, req.PageSize)
data, total, err := s.pigBatchLogRepo.List(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -329,7 +346,8 @@ func (s *monitorService) ListPigBatchLogs(req *dto.ListPigBatchLogRequest) (*dto
}
// ListWeighingBatches 负责处理查询批次称重记录列表的业务逻辑
func (s *monitorService) ListWeighingBatches(req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error) {
func (s *monitorService) ListWeighingBatches(ctx context.Context, req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListWeighingBatches")
opts := repository.WeighingBatchListOptions{
PigBatchID: req.PigBatchID,
OrderBy: req.OrderBy,
@@ -337,7 +355,7 @@ func (s *monitorService) ListWeighingBatches(req *dto.ListWeighingBatchRequest)
EndTime: req.EndTime,
}
data, total, err := s.pigBatchRepo.ListWeighingBatches(opts, req.Page, req.PageSize)
data, total, err := s.pigBatchRepo.ListWeighingBatches(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -346,7 +364,8 @@ func (s *monitorService) ListWeighingBatches(req *dto.ListWeighingBatchRequest)
}
// ListWeighingRecords 负责处理查询单次称重记录列表的业务逻辑
func (s *monitorService) ListWeighingRecords(req *dto.ListWeighingRecordRequest) (*dto.ListWeighingRecordResponse, error) {
func (s *monitorService) ListWeighingRecords(ctx context.Context, req *dto.ListWeighingRecordRequest) (*dto.ListWeighingRecordResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListWeighingRecords")
opts := repository.WeighingRecordListOptions{
WeighingBatchID: req.WeighingBatchID,
PenID: req.PenID,
@@ -356,7 +375,7 @@ func (s *monitorService) ListWeighingRecords(req *dto.ListWeighingRecordRequest)
EndTime: req.EndTime,
}
data, total, err := s.pigBatchRepo.ListWeighingRecords(opts, req.Page, req.PageSize)
data, total, err := s.pigBatchRepo.ListWeighingRecords(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -365,7 +384,8 @@ func (s *monitorService) ListWeighingRecords(req *dto.ListWeighingRecordRequest)
}
// ListPigTransferLogs 负责处理查询猪只迁移日志列表的业务逻辑
func (s *monitorService) ListPigTransferLogs(req *dto.ListPigTransferLogRequest) (*dto.ListPigTransferLogResponse, error) {
func (s *monitorService) ListPigTransferLogs(ctx context.Context, req *dto.ListPigTransferLogRequest) (*dto.ListPigTransferLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigTransferLogs")
opts := repository.PigTransferLogListOptions{
PigBatchID: req.PigBatchID,
PenID: req.PenID,
@@ -380,7 +400,7 @@ func (s *monitorService) ListPigTransferLogs(req *dto.ListPigTransferLogRequest)
opts.TransferType = &transferType
}
data, total, err := s.pigTransferLogRepo.ListPigTransferLogs(opts, req.Page, req.PageSize)
data, total, err := s.pigTransferLogRepo.ListPigTransferLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -389,7 +409,8 @@ func (s *monitorService) ListPigTransferLogs(req *dto.ListPigTransferLogRequest)
}
// ListPigSickLogs 负责处理查询病猪日志列表的业务逻辑
func (s *monitorService) ListPigSickLogs(req *dto.ListPigSickLogRequest) (*dto.ListPigSickLogResponse, error) {
func (s *monitorService) ListPigSickLogs(ctx context.Context, req *dto.ListPigSickLogRequest) (*dto.ListPigSickLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigSickLogs")
opts := repository.PigSickLogListOptions{
PigBatchID: req.PigBatchID,
PenID: req.PenID,
@@ -407,7 +428,7 @@ func (s *monitorService) ListPigSickLogs(req *dto.ListPigSickLogRequest) (*dto.L
opts.TreatmentLocation = &treatmentLocation
}
data, total, err := s.pigSickLogRepo.ListPigSickLogs(opts, req.Page, req.PageSize)
data, total, err := s.pigSickLogRepo.ListPigSickLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -416,7 +437,8 @@ func (s *monitorService) ListPigSickLogs(req *dto.ListPigSickLogRequest) (*dto.L
}
// ListPigPurchases 负责处理查询猪只采购记录列表的业务逻辑
func (s *monitorService) ListPigPurchases(req *dto.ListPigPurchaseRequest) (*dto.ListPigPurchaseResponse, error) {
func (s *monitorService) ListPigPurchases(ctx context.Context, req *dto.ListPigPurchaseRequest) (*dto.ListPigPurchaseResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigPurchases")
opts := repository.PigPurchaseListOptions{
PigBatchID: req.PigBatchID,
Supplier: req.Supplier,
@@ -426,7 +448,7 @@ func (s *monitorService) ListPigPurchases(req *dto.ListPigPurchaseRequest) (*dto
EndTime: req.EndTime,
}
data, total, err := s.pigTradeRepo.ListPigPurchases(opts, req.Page, req.PageSize)
data, total, err := s.pigTradeRepo.ListPigPurchases(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -435,7 +457,8 @@ func (s *monitorService) ListPigPurchases(req *dto.ListPigPurchaseRequest) (*dto
}
// ListPigSales 负责处理查询猪只销售记录列表的业务逻辑
func (s *monitorService) ListPigSales(req *dto.ListPigSaleRequest) (*dto.ListPigSaleResponse, error) {
func (s *monitorService) ListPigSales(ctx context.Context, req *dto.ListPigSaleRequest) (*dto.ListPigSaleResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigSales")
opts := repository.PigSaleListOptions{
PigBatchID: req.PigBatchID,
Buyer: req.Buyer,
@@ -445,7 +468,7 @@ func (s *monitorService) ListPigSales(req *dto.ListPigSaleRequest) (*dto.ListPig
EndTime: req.EndTime,
}
data, total, err := s.pigTradeRepo.ListPigSales(opts, req.Page, req.PageSize)
data, total, err := s.pigTradeRepo.ListPigSales(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
@@ -454,7 +477,8 @@ func (s *monitorService) ListPigSales(req *dto.ListPigSaleRequest) (*dto.ListPig
}
// ListNotifications 负责处理查询通知列表的业务逻辑
func (s *monitorService) ListNotifications(req *dto.ListNotificationRequest) (*dto.ListNotificationResponse, error) {
func (s *monitorService) ListNotifications(ctx context.Context, req *dto.ListNotificationRequest) (*dto.ListNotificationResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListNotifications")
opts := repository.NotificationListOptions{
UserID: req.UserID,
NotifierType: req.NotifierType,
@@ -465,7 +489,7 @@ func (s *monitorService) ListNotifications(req *dto.ListNotificationRequest) (*d
Status: req.Status,
}
data, total, err := s.notificationRepo.List(opts, req.Page, req.PageSize)
data, total, err := s.notificationRepo.List(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,7 @@
package service
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
@@ -11,47 +12,47 @@ import (
// PigBatchService 接口定义保持不变,继续作为应用层对外的契约。
type PigBatchService interface {
CreatePigBatch(operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error)
GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error)
UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error)
DeletePigBatch(id uint) error
ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error)
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
ListPigBatches(ctx context.Context, isActive *bool) ([]*dto.PigBatchResponseDTO, error)
// Pig Pen Management
AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error
ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
RemoveEmptyPenFromBatch(batchID uint, penID uint) error
MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
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
// Trade Sub-service
SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
BuyPigs(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 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
// Transfer Sub-service
TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) 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
// Sick Pig Management
RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
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
// Normal Pig Management
RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
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
}
// pigBatchService 的实现现在依赖于领域服务接口。
type pigBatchService struct {
logger *logs.Logger
domainService domain_pig.PigBatchService // 依赖注入领域服务
ctx context.Context
domainService domain_pig.PigBatchService
}
// NewPigBatchService 构造函数被修改,以注入领域服务。
func NewPigBatchService(domainService domain_pig.PigBatchService, logger *logs.Logger) PigBatchService {
func NewPigBatchService(ctx context.Context, domainService domain_pig.PigBatchService) PigBatchService {
return &pigBatchService{
logger: logger,
ctx: ctx,
domainService: domainService,
}
}
@@ -78,7 +79,8 @@ func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch, currentT
}
// CreatePigBatch 现在将请求委托给领域服务处理。
func (s *pigBatchService) CreatePigBatch(operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreatePigBatch")
// 1. DTO -> 领域模型
batch := &models.PigBatch{
BatchNumber: dto.BatchNumber,
@@ -89,9 +91,9 @@ func (s *pigBatchService) CreatePigBatch(operatorID uint, dto *dto.PigBatchCreat
}
// 2. 调用领域服务
createdBatch, err := s.domainService.CreatePigBatch(operatorID, batch)
createdBatch, err := s.domainService.CreatePigBatch(serviceCtx, operatorID, batch)
if err != nil {
s.logger.Errorf("应用层: 创建猪批次失败: %v", err)
logger.Errorf("应用层: 创建猪批次失败: %v", err)
return nil, MapDomainError(err)
}
@@ -100,31 +102,33 @@ func (s *pigBatchService) CreatePigBatch(operatorID uint, dto *dto.PigBatchCreat
}
// GetPigBatch 从领域服务获取数据并转换为DTO同时处理错误转换。
func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) {
batch, err := s.domainService.GetPigBatch(id)
func (s *pigBatchService) GetPigBatch(ctx context.Context, id uint) (*dto.PigBatchResponseDTO, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPigBatch")
batch, err := s.domainService.GetPigBatch(serviceCtx, id)
if err != nil {
s.logger.Warnf("应用层: 获取猪批次失败, ID: %d, 错误: %v", id, err)
logger.Warnf("应用层: 获取猪批次失败, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
currentTotalQuantity, err := s.domainService.GetCurrentPigQuantity(id)
currentTotalQuantity, err := s.domainService.GetCurrentPigQuantity(serviceCtx, id)
if err != nil {
s.logger.Warnf("应用层: 获取猪批次总数失败, ID: %d, 错误: %v", id, err)
logger.Warnf("应用层: 获取猪批次总数失败, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
currentTotalPigsInPens, err := s.domainService.GetTotalPigsInPensForBatch(id)
currentTotalPigsInPens, err := s.domainService.GetTotalPigsInPensForBatch(serviceCtx, id)
if err != nil {
s.logger.Warnf("应用层: 获取猪批次存栏总数失败, ID: %d, 错误: %v", id, err)
logger.Warnf("应用层: 获取猪批次存栏总数失败, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
return s.toPigBatchResponseDTO(batch, currentTotalQuantity, currentTotalPigsInPens), nil
}
// UpdatePigBatch 协调获取、更新和保存的流程,并处理错误转换。
func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
func (s *pigBatchService) UpdatePigBatch(ctx context.Context, id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePigBatch")
// 1. 先获取最新的领域模型
existingBatch, err := s.domainService.GetPigBatch(id)
existingBatch, err := s.domainService.GetPigBatch(serviceCtx, id)
if err != nil {
s.logger.Warnf("应用层: 更新猪批次失败,获取原批次信息错误, ID: %d, 错误: %v", id, err)
logger.Warnf("应用层: 更新猪批次失败,获取原批次信息错误, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
@@ -149,21 +153,21 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*
}
// 3. 调用领域服务执行更新
updatedBatch, err := s.domainService.UpdatePigBatch(existingBatch)
updatedBatch, err := s.domainService.UpdatePigBatch(serviceCtx, existingBatch)
if err != nil {
s.logger.Errorf("应用层: 更新猪批次失败, ID: %d, 错误: %v", id, err)
logger.Errorf("应用层: 更新猪批次失败, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
// 4. 填充猪群信息
currentTotalQuantity, err := s.domainService.GetCurrentPigQuantity(id)
currentTotalQuantity, err := s.domainService.GetCurrentPigQuantity(serviceCtx, id)
if err != nil {
s.logger.Warnf("应用层: 获取猪批次总数失败, ID: %d, 错误: %v", id, err)
logger.Warnf("应用层: 获取猪批次总数失败, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
currentTotalPigsInPens, err := s.domainService.GetTotalPigsInPensForBatch(id)
currentTotalPigsInPens, err := s.domainService.GetTotalPigsInPensForBatch(serviceCtx, id)
if err != nil {
s.logger.Warnf("应用层: 获取猪批次存栏总数失败, ID: %d, 错误: %v", id, err)
logger.Warnf("应用层: 获取猪批次存栏总数失败, ID: %d, 错误: %v", id, err)
return nil, MapDomainError(err)
}
@@ -172,33 +176,35 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*
}
// DeletePigBatch 将删除操作委托给领域服务,并转换领域错误为应用层错误。
func (s *pigBatchService) DeletePigBatch(id uint) error {
err := s.domainService.DeletePigBatch(id)
func (s *pigBatchService) DeletePigBatch(ctx context.Context, id uint) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeletePigBatch")
err := s.domainService.DeletePigBatch(serviceCtx, id)
if err != nil {
s.logger.Errorf("应用层: 删除猪批次失败, ID: %d, 错误: %v", id, err)
logger.Errorf("应用层: 删除猪批次失败, ID: %d, 错误: %v", id, err)
return MapDomainError(err)
}
return nil
}
// ListPigBatches 从领域服务获取列表并进行转换。
func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) {
batches, err := s.domainService.ListPigBatches(isActive)
func (s *pigBatchService) ListPigBatches(ctx context.Context, isActive *bool) ([]*dto.PigBatchResponseDTO, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "ListPigBatches")
batches, err := s.domainService.ListPigBatches(serviceCtx, isActive)
if err != nil {
s.logger.Errorf("应用层: 批量查询猪批次失败: %v", err)
logger.Errorf("应用层: 批量查询猪批次失败: %v", err)
return nil, MapDomainError(err)
}
var responseDTOs []*dto.PigBatchResponseDTO
for _, batch := range batches {
currentTotalQuantity, err := s.domainService.GetCurrentPigQuantity(batch.ID)
currentTotalQuantity, err := s.domainService.GetCurrentPigQuantity(serviceCtx, batch.ID)
if err != nil {
s.logger.Warnf("应用层: 获取猪批次总数失败, ID: %d, 错误: %v", batch.ID, err)
logger.Warnf("应用层: 获取猪批次总数失败, ID: %d, 错误: %v", batch.ID, err)
return nil, MapDomainError(err)
}
currentTotalPigsInPens, err := s.domainService.GetTotalPigsInPensForBatch(batch.ID)
currentTotalPigsInPens, err := s.domainService.GetTotalPigsInPensForBatch(serviceCtx, batch.ID)
if err != nil {
s.logger.Warnf("应用层: 获取猪批次存栏总数失败, ID: %d, 错误: %v", batch.ID, err)
logger.Warnf("应用层: 获取猪批次存栏总数失败, ID: %d, 错误: %v", batch.ID, err)
return nil, MapDomainError(err)
}
responseDTOs = append(responseDTOs, s.toPigBatchResponseDTO(batch, currentTotalQuantity, currentTotalPigsInPens))
@@ -208,140 +214,154 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchRespons
}
// AssignEmptyPensToBatch 委托给领域服务
func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error {
err := s.domainService.AssignEmptyPensToBatch(batchID, penIDs, operatorID)
func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID uint, penIDs []uint, operatorID uint) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "AssignEmptyPensToBatch")
err := s.domainService.AssignEmptyPensToBatch(serviceCtx, batchID, penIDs, operatorID)
if err != nil {
s.logger.Errorf("应用层: 为猪批次分配空栏失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 为猪批次分配空栏失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// ReclassifyPenToNewBatch 委托给领域服务
func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
err := s.domainService.ReclassifyPenToNewBatch(fromBatchID, toBatchID, penID, operatorID, remarks)
func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "ReclassifyPenToNewBatch")
err := s.domainService.ReclassifyPenToNewBatch(serviceCtx, fromBatchID, toBatchID, penID, operatorID, remarks)
if err != nil {
s.logger.Errorf("应用层: 划拨猪栏到新批次失败, 源批次ID: %d, 错误: %v", fromBatchID, err)
logger.Errorf("应用层: 划拨猪栏到新批次失败, 源批次ID: %d, 错误: %v", fromBatchID, err)
return MapDomainError(err)
}
return nil
}
// RemoveEmptyPenFromBatch 委托给领域服务
func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) error {
err := s.domainService.RemoveEmptyPenFromBatch(batchID, penID)
func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID uint, penID uint) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "RemoveEmptyPenFromBatch")
err := s.domainService.RemoveEmptyPenFromBatch(serviceCtx, batchID, penID)
if err != nil {
s.logger.Errorf("应用层: 从猪批次移除空栏失败, 批次ID: %d, 猪栏ID: %d, 错误: %v", batchID, penID, err)
logger.Errorf("应用层: 从猪批次移除空栏失败, 批次ID: %d, 猪栏ID: %d, 错误: %v", batchID, penID, err)
return MapDomainError(err)
}
return nil
}
// MovePigsIntoPen 委托给领域服务
func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
err := s.domainService.MovePigsIntoPen(batchID, toPenID, quantity, operatorID, remarks)
func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "MovePigsIntoPen")
err := s.domainService.MovePigsIntoPen(serviceCtx, batchID, toPenID, quantity, operatorID, remarks)
if err != nil {
s.logger.Errorf("应用层: 将猪只移入猪栏失败, 批次ID: %d, 目标猪栏ID: %d, 错误: %v", batchID, toPenID, err)
logger.Errorf("应用层: 将猪只移入猪栏失败, 批次ID: %d, 目标猪栏ID: %d, 错误: %v", batchID, toPenID, err)
return MapDomainError(err)
}
return nil
}
// SellPigs 委托给领域服务
func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
err := s.domainService.SellPigs(batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
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 {
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 {
s.logger.Errorf("应用层: 卖猪失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 卖猪失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// BuyPigs 委托给领域服务
func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
err := s.domainService.BuyPigs(batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID)
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 {
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 {
s.logger.Errorf("应用层: 买猪失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 买猪失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// TransferPigsAcrossBatches 委托给领域服务
func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
err := s.domainService.TransferPigsAcrossBatches(sourceBatchID, destBatchID, fromPenID, toPenID, quantity, operatorID, remarks)
func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, 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 {
s.logger.Errorf("应用层: 跨群调栏失败, 源批次ID: %d, 错误: %v", sourceBatchID, err)
logger.Errorf("应用层: 跨群调栏失败, 源批次ID: %d, 错误: %v", sourceBatchID, err)
return MapDomainError(err)
}
return nil
}
// TransferPigsWithinBatch 委托给领域服务
func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
err := s.domainService.TransferPigsWithinBatch(batchID, fromPenID, toPenID, quantity, operatorID, remarks)
func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, 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 {
s.logger.Errorf("应用层: 群内调栏失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 群内调栏失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordSickPigs 委托给领域服务
func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordSickPigs(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
func (s *pigBatchService) RecordSickPigs(ctx context.Context, operatorID uint, batchID uint, penID uint, 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 {
s.logger.Errorf("应用层: 记录病猪事件失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 记录病猪事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordSickPigRecovery 委托给领域服务
func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordSickPigRecovery(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
func (s *pigBatchService) RecordSickPigRecovery(ctx context.Context, operatorID uint, batchID uint, penID uint, 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 {
s.logger.Errorf("应用层: 记录病猪康复事件失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 记录病猪康复事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordSickPigDeath 委托给领域服务
func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordSickPigDeath(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
func (s *pigBatchService) RecordSickPigDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, 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 {
s.logger.Errorf("应用层: 记录病猪死亡事件失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 记录病猪死亡事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordSickPigCull 委托给领域服务
func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordSickPigCull(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks)
func (s *pigBatchService) RecordSickPigCull(ctx context.Context, operatorID uint, batchID uint, penID uint, 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 {
s.logger.Errorf("应用层: 记录病猪淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 记录病猪淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordDeath 委托给领域服务
func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordDeath(operatorID, batchID, penID, quantity, happenedAt, remarks)
func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, 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 {
s.logger.Errorf("应用层: 记录正常猪只死亡事件失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 记录正常猪只死亡事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil
}
// RecordCull 委托给领域服务
func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
err := s.domainService.RecordCull(operatorID, batchID, penID, quantity, happenedAt, remarks)
func (s *pigBatchService) RecordCull(ctx context.Context, operatorID uint, batchID uint, penID uint, 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 {
s.logger.Errorf("应用层: 记录正常猪只淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err)
logger.Errorf("应用层: 记录正常猪只淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err)
return MapDomainError(err)
}
return nil

View File

@@ -1,6 +1,7 @@
package service
import (
"context"
"errors"
"fmt"
@@ -16,40 +17,41 @@ import (
// PigFarmService 提供了猪场资产管理的业务逻辑
type PigFarmService interface {
// PigHouse methods
CreatePigHouse(name, description string) (*dto.PigHouseResponse, error)
GetPigHouseByID(id uint) (*dto.PigHouseResponse, error)
ListPigHouses() ([]dto.PigHouseResponse, error)
UpdatePigHouse(id uint, name, description string) (*dto.PigHouseResponse, error)
DeletePigHouse(id uint) error
CreatePigHouse(ctx context.Context, name, description string) (*dto.PigHouseResponse, error)
GetPigHouseByID(ctx context.Context, id uint) (*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
// Pen methods
CreatePen(penNumber string, houseID uint, capacity int) (*dto.PenResponse, error)
GetPenByID(id uint) (*dto.PenResponse, error)
ListPens() ([]*dto.PenResponse, error)
UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error)
DeletePen(id uint) error
CreatePen(ctx context.Context, penNumber string, houseID uint, capacity int) (*dto.PenResponse, error)
GetPenByID(ctx context.Context, id uint) (*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
// UpdatePenStatus 更新猪栏状态
UpdatePenStatus(id uint, newStatus models.PenStatus) (*dto.PenResponse, error)
UpdatePenStatus(ctx context.Context, id uint, newStatus models.PenStatus) (*dto.PenResponse, error)
}
type pigFarmService struct {
logger *logs.Logger
ctx context.Context
farmRepository repository.PigFarmRepository
penRepository repository.PigPenRepository
batchRepository repository.PigBatchRepository
pigBatchService domain_pig.PigBatchService // Add domain PigBatchService dependency
uow repository.UnitOfWork // 工作单元,用于事务管理
pigBatchService domain_pig.PigBatchService
uow repository.UnitOfWork // 工作单元,用于事务管理
}
// NewPigFarmService 创建一个新的 PigFarmService 实例
func NewPigFarmService(farmRepository repository.PigFarmRepository,
func NewPigFarmService(ctx context.Context,
farmRepository repository.PigFarmRepository,
penRepository repository.PigPenRepository,
batchRepository repository.PigBatchRepository,
pigBatchService domain_pig.PigBatchService,
uow repository.UnitOfWork,
logger *logs.Logger) PigFarmService {
) PigFarmService {
return &pigFarmService{
logger: logger,
ctx: ctx,
farmRepository: farmRepository,
penRepository: penRepository,
batchRepository: batchRepository,
@@ -60,12 +62,13 @@ func NewPigFarmService(farmRepository repository.PigFarmRepository,
// --- PigHouse Implementation ---
func (s *pigFarmService) CreatePigHouse(name, description string) (*dto.PigHouseResponse, error) {
func (s *pigFarmService) CreatePigHouse(ctx context.Context, name, description string) (*dto.PigHouseResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigHouse")
house := &models.PigHouse{
Name: name,
Description: description,
}
err := s.farmRepository.CreatePigHouse(house)
err := s.farmRepository.CreatePigHouse(serviceCtx, house)
if err != nil {
return nil, err
}
@@ -76,8 +79,9 @@ func (s *pigFarmService) CreatePigHouse(name, description string) (*dto.PigHouse
}, nil
}
func (s *pigFarmService) GetPigHouseByID(id uint) (*dto.PigHouseResponse, error) {
house, err := s.farmRepository.GetPigHouseByID(id)
func (s *pigFarmService) GetPigHouseByID(ctx context.Context, id uint) (*dto.PigHouseResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigHouseByID")
house, err := s.farmRepository.GetPigHouseByID(serviceCtx, id)
if err != nil {
return nil, err
}
@@ -88,8 +92,9 @@ func (s *pigFarmService) GetPigHouseByID(id uint) (*dto.PigHouseResponse, error)
}, nil
}
func (s *pigFarmService) ListPigHouses() ([]dto.PigHouseResponse, error) {
houses, err := s.farmRepository.ListPigHouses()
func (s *pigFarmService) ListPigHouses(ctx context.Context) ([]dto.PigHouseResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigHouses")
houses, err := s.farmRepository.ListPigHouses(serviceCtx)
if err != nil {
return nil, err
}
@@ -104,13 +109,14 @@ func (s *pigFarmService) ListPigHouses() ([]dto.PigHouseResponse, error) {
return resp, nil
}
func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*dto.PigHouseResponse, error) {
func (s *pigFarmService) UpdatePigHouse(ctx context.Context, id uint, name, description string) (*dto.PigHouseResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigHouse")
house := &models.PigHouse{
Model: gorm.Model{ID: id},
Name: name,
Description: description,
}
rowsAffected, err := s.farmRepository.UpdatePigHouse(house)
rowsAffected, err := s.farmRepository.UpdatePigHouse(serviceCtx, house)
if err != nil {
return nil, err
}
@@ -118,7 +124,7 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*dto
return nil, ErrHouseNotFound
}
// 返回更新后的完整信息
updatedHouse, err := s.farmRepository.GetPigHouseByID(id)
updatedHouse, err := s.farmRepository.GetPigHouseByID(serviceCtx, id)
if err != nil {
return nil, err
}
@@ -129,9 +135,10 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*dto
}, nil
}
func (s *pigFarmService) DeletePigHouse(id uint) error {
func (s *pigFarmService) DeletePigHouse(ctx context.Context, id uint) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigHouse")
// 业务逻辑:检查猪舍是否包含猪栏
penCount, err := s.farmRepository.CountPensInHouse(id)
penCount, err := s.farmRepository.CountPensInHouse(serviceCtx, id)
if err != nil {
return err
}
@@ -140,7 +147,7 @@ func (s *pigFarmService) DeletePigHouse(id uint) error {
}
// 调用仓库层进行删除
rowsAffected, err := s.farmRepository.DeletePigHouse(id)
rowsAffected, err := s.farmRepository.DeletePigHouse(serviceCtx, id)
if err != nil {
return err
}
@@ -152,9 +159,10 @@ func (s *pigFarmService) DeletePigHouse(id uint) error {
// --- Pen Implementation ---
func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*dto.PenResponse, error) {
func (s *pigFarmService) CreatePen(ctx context.Context, penNumber string, houseID uint, capacity int) (*dto.PenResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePen")
// 业务逻辑:验证所属猪舍是否存在
_, err := s.farmRepository.GetPigHouseByID(houseID)
_, err := s.farmRepository.GetPigHouseByID(serviceCtx, houseID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrHouseNotFound
@@ -168,7 +176,7 @@ func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int)
Capacity: capacity,
Status: models.PenStatusEmpty,
}
err = s.penRepository.CreatePen(pen)
err = s.penRepository.CreatePen(serviceCtx, pen)
if err != nil {
return nil, err
}
@@ -181,15 +189,16 @@ func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int)
}, nil
}
func (s *pigFarmService) GetPenByID(id uint) (*dto.PenResponse, error) {
pen, err := s.penRepository.GetPenByID(id)
func (s *pigFarmService) GetPenByID(ctx context.Context, id uint) (*dto.PenResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPenByID")
pen, err := s.penRepository.GetPenByID(serviceCtx, id)
if err != nil {
return nil, err
}
currentPigCount, err := s.pigBatchService.GetCurrentPigsInPen(id)
currentPigCount, err := s.pigBatchService.GetCurrentPigsInPen(serviceCtx, id)
if err != nil {
s.logger.Errorf("获取猪栏 %d 存栏量失败: %v", id, err)
logger.Errorf("获取猪栏 %d 存栏量失败: %v", id, err)
currentPigCount = 0 // 如果获取计数时出错则默认为0
}
@@ -209,17 +218,18 @@ func (s *pigFarmService) GetPenByID(id uint) (*dto.PenResponse, error) {
return response, nil
}
func (s *pigFarmService) ListPens() ([]*dto.PenResponse, error) {
pens, err := s.penRepository.ListPens()
func (s *pigFarmService) ListPens(ctx context.Context) ([]*dto.PenResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "ListPens")
pens, err := s.penRepository.ListPens(serviceCtx)
if err != nil {
return nil, err
}
var response []*dto.PenResponse
for _, pen := range pens {
currentPigCount, err := s.pigBatchService.GetCurrentPigsInPen(pen.ID)
currentPigCount, err := s.pigBatchService.GetCurrentPigsInPen(serviceCtx, pen.ID)
if err != nil {
s.logger.Errorf("获取猪栏 %d 存栏量失败: %v", pen.ID, err)
logger.Errorf("获取猪栏 %d 存栏量失败: %v", pen.ID, err)
currentPigCount = 0 // 如果获取计数时出错则默认为0
}
@@ -241,9 +251,10 @@ func (s *pigFarmService) ListPens() ([]*dto.PenResponse, error) {
return response, nil
}
func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error) {
func (s *pigFarmService) UpdatePen(ctx context.Context, id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePen")
// 业务逻辑:验证所属猪舍是否存在
_, err := s.farmRepository.GetPigHouseByID(houseID)
_, err := s.farmRepository.GetPigHouseByID(serviceCtx, houseID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrHouseNotFound
@@ -258,7 +269,7 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa
Capacity: capacity,
Status: status,
}
rowsAffected, err := s.penRepository.UpdatePen(pen)
rowsAffected, err := s.penRepository.UpdatePen(serviceCtx, pen)
if err != nil {
return nil, err
}
@@ -266,7 +277,7 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa
return nil, ErrPenNotFound
}
// 返回更新后的完整信息
updatedPen, err := s.penRepository.GetPenByID(id)
updatedPen, err := s.penRepository.GetPenByID(serviceCtx, id)
if err != nil {
return nil, err
}
@@ -280,9 +291,10 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa
}, nil
}
func (s *pigFarmService) DeletePen(id uint) error {
func (s *pigFarmService) DeletePen(ctx context.Context, id uint) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePen")
// 业务逻辑:检查猪栏是否被活跃批次使用
pen, err := s.penRepository.GetPenByID(id)
pen, err := s.penRepository.GetPenByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound // 猪栏不存在
@@ -293,7 +305,7 @@ func (s *pigFarmService) DeletePen(id uint) error {
// 检查猪栏是否关联了活跃批次
// 注意pen.PigBatchID 是指针类型,需要检查是否为 nil
if pen.PigBatchID != nil && *pen.PigBatchID != 0 {
pigBatch, err := s.batchRepository.GetPigBatchByID(*pen.PigBatchID)
pigBatch, err := s.batchRepository.GetPigBatchByID(serviceCtx, *pen.PigBatchID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
@@ -304,7 +316,7 @@ func (s *pigFarmService) DeletePen(id uint) error {
}
// 调用仓库层进行删除
rowsAffected, err := s.penRepository.DeletePen(id)
rowsAffected, err := s.penRepository.DeletePen(serviceCtx, id)
if err != nil {
return err
}
@@ -315,15 +327,16 @@ func (s *pigFarmService) DeletePen(id uint) error {
}
// UpdatePenStatus 更新猪栏状态
func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*dto.PenResponse, error) {
func (s *pigFarmService) UpdatePenStatus(ctx context.Context, id uint, newStatus models.PenStatus) (*dto.PenResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePenStatus")
var updatedPen *models.Pen
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
pen, err := s.penRepository.GetPenByIDTx(tx, id)
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
pen, err := s.penRepository.GetPenByIDTx(serviceCtx, tx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
}
s.logger.Errorf("更新猪栏状态失败: 获取猪栏 %d 信息错误: %v", id, err)
logger.Errorf("更新猪栏状态失败: 获取猪栏 %d 信息错误: %v", id, err)
return fmt.Errorf("获取猪栏 %d 信息失败: %w", id, err)
}
@@ -348,15 +361,15 @@ func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*
"status": newStatus,
}
if err := s.penRepository.UpdatePenFieldsTx(tx, id, updates); err != nil {
s.logger.Errorf("更新猪栏 %d 状态失败: %v", id, err)
if err := s.penRepository.UpdatePenFieldsTx(serviceCtx, tx, id, updates); err != nil {
logger.Errorf("更新猪栏 %d 状态失败: %v", id, err)
return fmt.Errorf("更新猪栏 %d 状态失败: %w", id, err)
}
// 获取更新后的猪栏信息
updatedPen, err = s.penRepository.GetPenByIDTx(tx, id)
updatedPen, err = s.penRepository.GetPenByIDTx(serviceCtx, tx, id)
if err != nil {
s.logger.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %v", id, err)
logger.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %v", id, err)
return fmt.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %w", id, err)
}
return nil

View File

@@ -1,6 +1,7 @@
package service
import (
"context"
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
@@ -12,100 +13,103 @@ import (
// PlanService 定义了计划相关的应用服务接口
type PlanService interface {
// CreatePlan 创建一个新的计划
CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error)
CreatePlan(ctx context.Context, req *dto.CreatePlanRequest) (*dto.PlanResponse, error)
// GetPlanByID 根据ID获取计划详情
GetPlanByID(id uint) (*dto.PlanResponse, error)
GetPlanByID(ctx context.Context, id uint) (*dto.PlanResponse, error)
// ListPlans 获取计划列表,支持过滤和分页
ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error)
ListPlans(ctx context.Context, query *dto.ListPlansQuery) (*dto.ListPlansResponse, error)
// UpdatePlan 更新计划
UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error)
UpdatePlan(ctx context.Context, id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error)
// DeletePlan 删除计划(软删除)
DeletePlan(id uint) error
DeletePlan(ctx context.Context, id uint) error
// StartPlan 启动计划
StartPlan(id uint) error
StartPlan(ctx context.Context, id uint) error
// StopPlan 停止计划
StopPlan(id uint) error
StopPlan(ctx context.Context, id uint) error
}
// planService 是 PlanService 接口的实现
type planService struct {
logger *logs.Logger
domainPlanService plan.Service // 替换为领域层的服务接口
ctx context.Context
domainPlanService plan.Service
}
// NewPlanService 创建一个新的 PlanService 实例
func NewPlanService(
logger *logs.Logger,
domainPlanService plan.Service, // 接收领域层服务
ctx context.Context,
domainPlanService plan.Service,
) PlanService {
return &planService{
logger: logger,
domainPlanService: domainPlanService, // 注入领域层服务
ctx: ctx,
domainPlanService: domainPlanService,
}
}
// CreatePlan 创建一个新的计划
func (s *planService) CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error) {
func (s *planService) CreatePlan(ctx context.Context, req *dto.CreatePlanRequest) (*dto.PlanResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreatePlan")
const actionType = "应用服务层:创建计划"
// 使用 DTO 转换函数将请求转换为领域实体
planToCreate, err := dto.NewPlanFromCreateRequest(req)
if err != nil {
s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
return nil, err
}
// 调用领域服务创建计划
createdPlan, err := s.domainPlanService.CreatePlan(planToCreate)
createdPlan, err := s.domainPlanService.CreatePlan(serviceCtx, planToCreate)
if err != nil {
s.logger.Errorf("%s: 领域服务创建计划失败: %v", actionType, err)
logger.Errorf("%s: 领域服务创建计划失败: %v", actionType, err)
return nil, err // 直接返回领域层错误
}
// 将领域实体转换为响应 DTO
resp, err := dto.NewPlanToResponse(createdPlan)
if err != nil {
s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, createdPlan)
logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, createdPlan)
return nil, errors.New("计划创建成功,但响应生成失败")
}
s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, createdPlan.ID)
logger.Infof("%s: 计划创建成功, ID: %d", actionType, createdPlan.ID)
return resp, nil
}
// GetPlanByID 根据ID获取计划详情
func (s *planService) GetPlanByID(id uint) (*dto.PlanResponse, error) {
func (s *planService) GetPlanByID(ctx context.Context, id uint) (*dto.PlanResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "GetPlanByID")
const actionType = "应用服务层:获取计划详情"
// 调用领域服务获取计划
plan, err := s.domainPlanService.GetPlanByID(id)
plan, err := s.domainPlanService.GetPlanByID(serviceCtx, id)
if err != nil {
s.logger.Errorf("%s: 领域服务获取计划详情失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 领域服务获取计划详情失败: %v, ID: %d", actionType, err, id)
return nil, err // 直接返回领域层错误
}
// 将领域实体转换为响应 DTO
resp, err := dto.NewPlanToResponse(plan)
if err != nil {
s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan)
return nil, errors.New("获取计划详情失败: 内部数据格式错误")
}
s.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
return resp, nil
}
// ListPlans 获取计划列表,支持过滤和分页
func (s *planService) ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error) {
func (s *planService) ListPlans(ctx context.Context, query *dto.ListPlansQuery) (*dto.ListPlansResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "ListPlans")
const actionType = "应用服务层:获取计划列表"
// 将 DTO 查询参数转换为领域层可接受的选项
opts := repository.ListPlansOptions{PlanType: query.PlanType}
// 调用领域服务获取计划列表
plans, total, err := s.domainPlanService.ListPlans(opts, query.Page, query.PageSize)
plans, total, err := s.domainPlanService.ListPlans(serviceCtx, opts, query.Page, query.PageSize)
if err != nil {
s.logger.Errorf("%s: 领域服务获取计划列表失败: %v", actionType, err)
logger.Errorf("%s: 领域服务获取计划列表失败: %v", actionType, err)
return nil, err // 直接返回领域层错误
}
@@ -114,7 +118,7 @@ func (s *planService) ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansRespon
for _, p := range plans {
resp, err := dto.NewPlanToResponse(&p)
if err != nil {
s.logger.Errorf("%s: 序列化单个计划响应失败: %v, Plan: %+v", actionType, err, p)
logger.Errorf("%s: 序列化单个计划响应失败: %v, Plan: %+v", actionType, err, p)
// 这里选择跳过有问题的计划,并记录错误,而不是中断整个列表的返回
continue
}
@@ -125,81 +129,85 @@ func (s *planService) ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansRespon
Plans: planResponses,
Total: total,
}
s.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses))
logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses))
return resp, nil
}
// UpdatePlan 更新计划
func (s *planService) UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error) {
func (s *planService) UpdatePlan(ctx context.Context, id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePlan")
const actionType = "应用服务层:更新计划"
// 使用 DTO 转换函数将请求转换为领域实体
planToUpdate, err := dto.NewPlanFromUpdateRequest(req)
if err != nil {
s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
logger.Errorf("%s: 计划数据校验失败: %v", actionType, err)
return nil, err
}
planToUpdate.ID = id // 确保ID被设置
// 调用领域服务更新计划
updatedPlan, err := s.domainPlanService.UpdatePlan(planToUpdate)
updatedPlan, err := s.domainPlanService.UpdatePlan(serviceCtx, planToUpdate)
if err != nil {
s.logger.Errorf("%s: 领域服务更新计划失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 领域服务更新计划失败: %v, ID: %d", actionType, err, id)
return nil, err // 直接返回领域层错误
}
// 将领域实体转换为响应 DTO
resp, err := dto.NewPlanToResponse(updatedPlan)
if err != nil {
s.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan)
return nil, errors.New("计划更新成功,但响应生成失败")
}
s.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
return resp, nil
}
// DeletePlan 删除计划(软删除)
func (s *planService) DeletePlan(id uint) error {
func (s *planService) DeletePlan(ctx context.Context, id uint) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "DeletePlan")
const actionType = "应用服务层:删除计划"
// 调用领域服务删除计划
err := s.domainPlanService.DeletePlan(id)
err := s.domainPlanService.DeletePlan(serviceCtx, id)
if err != nil {
s.logger.Errorf("%s: 领域服务删除计划失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 领域服务删除计划失败: %v, ID: %d", actionType, err, id)
return err // 直接返回领域层错误
}
s.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
return nil
}
// StartPlan 启动计划
func (s *planService) StartPlan(id uint) error {
func (s *planService) StartPlan(ctx context.Context, id uint) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "StartPlan")
const actionType = "应用服务层:启动计划"
// 调用领域服务启动计划
err := s.domainPlanService.StartPlan(id)
err := s.domainPlanService.StartPlan(serviceCtx, id)
if err != nil {
s.logger.Errorf("%s: 领域服务启动计划失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 领域服务启动计划失败: %v, ID: %d", actionType, err, id)
return err // 直接返回领域层错误
}
s.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
return nil
}
// StopPlan 停止计划
func (s *planService) StopPlan(id uint) error {
func (s *planService) StopPlan(ctx context.Context, id uint) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "StopPlan")
const actionType = "应用服务层:停止计划"
// 调用领域服务停止计划
err := s.domainPlanService.StopPlan(id)
err := s.domainPlanService.StopPlan(serviceCtx, id)
if err != nil {
s.logger.Errorf("%s: 领域服务停止计划失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 领域服务停止计划失败: %v, ID: %d", actionType, err, id)
return err // 直接返回领域层错误
}
s.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
return nil
}

View File

@@ -1,59 +1,62 @@
package service
import (
"context"
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token"
"gorm.io/gorm"
)
// UserService 定义用户服务接口
type UserService interface {
CreateUser(req *dto.CreateUserRequest) (*dto.CreateUserResponse, error)
Login(req *dto.LoginRequest) (*dto.LoginResponse, error)
SendTestNotification(userID uint, req *dto.SendTestNotificationRequest) error
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
}
// userService 实现了 UserService 接口
type userService struct {
userRepo repository.UserRepository
tokenService token.Service
notifyService domain_notify.Service
logger *logs.Logger
ctx context.Context
userRepo repository.UserRepository
tokenGenerator token.Generator
notifyService domain_notify.Service
}
// NewUserService 创建并返回一个新的 UserService 实例
func NewUserService(
ctx context.Context,
userRepo repository.UserRepository,
tokenService token.Service,
tokenGenerator token.Generator,
notifyService domain_notify.Service,
logger *logs.Logger,
) UserService {
return &userService{
userRepo: userRepo,
tokenService: tokenService,
notifyService: notifyService,
logger: logger,
ctx: ctx,
userRepo: userRepo,
tokenGenerator: tokenGenerator,
notifyService: notifyService,
}
}
// CreateUser 创建新用户
func (s *userService) CreateUser(req *dto.CreateUserRequest) (*dto.CreateUserResponse, error) {
func (s *userService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*dto.CreateUserResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "CreateUser")
user := &models.User{
Username: req.Username,
Password: req.Password, // 密码会在 BeforeSave 钩子中哈希
}
if err := s.userRepo.Create(user); err != nil {
s.logger.Errorf("创建用户: 创建用户失败: %v", err)
if err := s.userRepo.Create(serviceCtx, user); err != nil {
logger.Errorf("创建用户: 创建用户失败: %v", err)
// 尝试查询用户,以判断是否是用户名重复导致的错误
_, findErr := s.userRepo.FindByUsername(req.Username)
_, findErr := s.userRepo.FindByUsername(serviceCtx, req.Username)
if findErr == nil { // 如果能找到用户,说明是用户名重复
return nil, errors.New("用户名已存在")
}
@@ -69,14 +72,15 @@ func (s *userService) CreateUser(req *dto.CreateUserRequest) (*dto.CreateUserRes
}
// Login 用户登录
func (s *userService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
func (s *userService) Login(ctx context.Context, req *dto.LoginRequest) (*dto.LoginResponse, error) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "Login")
// 使用新的方法,通过唯一标识符(用户名、邮箱等)查找用户
user, err := s.userRepo.FindUserForLogin(req.Identifier)
user, err := s.userRepo.FindUserForLogin(serviceCtx, req.Identifier)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("登录凭证不正确")
}
s.logger.Errorf("登录: 查询用户失败: %v", err)
logger.Errorf("登录: 查询用户失败: %v", err)
return nil, errors.New("登录失败")
}
@@ -85,9 +89,9 @@ func (s *userService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
}
// 登录成功,生成 JWT token
tokenString, err := s.tokenService.GenerateToken(user.ID)
tokenString, err := s.tokenGenerator.GenerateToken(user.ID)
if err != nil {
s.logger.Errorf("登录: 生成令牌失败: %v", err)
logger.Errorf("登录: 生成令牌失败: %v", err)
return nil, errors.New("登录失败,无法生成认证信息")
}
@@ -99,12 +103,13 @@ func (s *userService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
}
// SendTestNotification 发送测试通知
func (s *userService) SendTestNotification(userID uint, req *dto.SendTestNotificationRequest) error {
err := s.notifyService.SendTestMessage(userID, req.Type)
func (s *userService) SendTestNotification(ctx context.Context, userID uint, req *dto.SendTestNotificationRequest) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendTestNotification")
err := s.notifyService.SendTestMessage(serviceCtx, userID, req.Type)
if err != nil {
s.logger.Errorf("发送测试通知: 服务层调用失败: %v", err)
logger.Errorf("发送测试通知: 服务层调用失败: %v", err)
return errors.New("发送测试消息失败: " + err.Error())
}
s.logger.Infof("发送测试通知: 成功为用户 %d 发送类型为 %s 的测试消息", userID, req.Type)
logger.Infof("发送测试通知: 成功为用户 %d 发送类型为 %s 的测试消息", userID, req.Type)
return nil
}

View File

@@ -1,6 +1,7 @@
package webhook
import (
"context"
"encoding/base64"
"encoding/json"
"io"
@@ -30,7 +31,7 @@ const (
// ChirpStackListener 是一个监听器, 用于监听ChirpStack反馈的设备上行事件
type ChirpStackListener struct {
logger *logs.Logger
ctx context.Context
sensorDataRepo repository.SensorDataRepository
deviceRepo repository.DeviceRepository
areaControllerRepo repository.AreaControllerRepository
@@ -40,15 +41,15 @@ type ChirpStackListener struct {
// NewChirpStackListener 创建一个新的 ChirpStackListener 实例
func NewChirpStackListener(
logger *logs.Logger,
ctx context.Context,
sensorDataRepo repository.SensorDataRepository,
deviceRepo repository.DeviceRepository,
areaControllerRepo repository.AreaControllerRepository,
deviceCommandLogRepo repository.DeviceCommandLogRepository,
pendingCollectionRepo repository.PendingCollectionRepository,
) ListenHandler { // 返回接口类型
) ListenHandler {
return &ChirpStackListener{
logger: logger,
ctx: ctx,
sensorDataRepo: sensorDataRepo,
deviceRepo: deviceRepo,
areaControllerRepo: areaControllerRepo,
@@ -60,11 +61,13 @@ func NewChirpStackListener(
// Handler 监听ChirpStack反馈的事件, 因为这是个Webhook, 所以直接回复掉再慢慢处理信息
func (c *ChirpStackListener) Handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, logger := logs.Trace(r.Context(), c.ctx, "ChirpStackListener")
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
c.logger.Errorf("读取请求体失败: %v", err)
logger.Errorf("读取请求体失败: %v", err)
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
@@ -74,100 +77,102 @@ func (c *ChirpStackListener) Handler() http.HandlerFunc {
w.WriteHeader(http.StatusOK)
// 将异步处理逻辑委托给 handler 方法
go c.handler(b, event)
go c.handler(ctx, b, event)
}
}
// handler 用于处理 ChirpStack 发送的事件
func (c *ChirpStackListener) handler(data []byte, eventType string) {
func (c *ChirpStackListener) handler(ctx context.Context, data []byte, eventType string) {
reqCtx, logger := logs.Trace(ctx, c.ctx, "ChirpStackListener.handler")
switch eventType {
case eventTypeUp:
var msg UpEvent
if err := json.Unmarshal(data, &msg); err != nil {
c.logger.Errorf("解析 'up' 事件失败: %v, data: %s", err, string(data))
logger.Errorf("解析 'up' 事件失败: %v, data: %s", err, string(data))
return
}
c.handleUpEvent(&msg)
c.handleUpEvent(reqCtx, &msg)
case eventTypeJoin:
var msg JoinEvent
if err := json.Unmarshal(data, &msg); err != nil {
c.logger.Errorf("解析 'join' 事件失败: %v, data: %s", err, string(data))
logger.Errorf("解析 'join' 事件失败: %v, data: %s", err, string(data))
return
}
c.handleJoinEvent(&msg)
c.handleJoinEvent(reqCtx, &msg)
case eventTypeAck:
var msg AckEvent
if err := json.Unmarshal(data, &msg); err != nil {
c.logger.Errorf("解析 'ack' 事件失败: %v, data: %s", err, string(data))
logger.Errorf("解析 'ack' 事件失败: %v, data: %s", err, string(data))
return
}
c.handleAckEvent(&msg)
c.handleAckEvent(reqCtx, &msg)
case eventTypeTxAck:
var msg TxAckEvent
if err := json.Unmarshal(data, &msg); err != nil {
c.logger.Errorf("解析 'txack' 事件失败: %v, data: %s", err, string(data))
logger.Errorf("解析 'txack' 事件失败: %v, data: %s", err, string(data))
return
}
c.handleTxAckEvent(&msg)
c.handleTxAckEvent(reqCtx, &msg)
case eventTypeStatus:
var msg StatusEvent
if err := json.Unmarshal(data, &msg); err != nil {
c.logger.Errorf("解析 'status' 事件失败: %v, data: %s", err, string(data))
logger.Errorf("解析 'status' 事件失败: %v, data: %s", err, string(data))
return
}
c.handleStatusEvent(&msg)
c.handleStatusEvent(reqCtx, &msg)
case eventTypeLog:
var msg LogEvent
if err := json.Unmarshal(data, &msg); err != nil {
c.logger.Errorf("解析 'log' 事件失败: %v, data: %s", err, string(data))
logger.Errorf("解析 'log' 事件失败: %v, data: %s", err, string(data))
return
}
c.handleLogEvent(&msg)
c.handleLogEvent(reqCtx, &msg)
case eventTypeLocation:
var msg LocationEvent
if err := json.Unmarshal(data, &msg); err != nil {
c.logger.Errorf("解析 'location' 事件失败: %v, data: %s", err, string(data))
logger.Errorf("解析 'location' 事件失败: %v, data: %s", err, string(data))
return
}
c.handleLocationEvent(&msg)
c.handleLocationEvent(reqCtx, &msg)
case eventTypeIntegration:
var msg IntegrationEvent
if err := json.Unmarshal(data, &msg); err != nil {
c.logger.Errorf("解析 'integration' 事件失败: %v, data: %s", err, string(data))
logger.Errorf("解析 'integration' 事件失败: %v, data: %s", err, string(data))
return
}
c.handleIntegrationEvent(&msg)
c.handleIntegrationEvent(reqCtx, &msg)
default:
c.logger.Errorf("未知的ChirpStack事件: %s, data: %s", eventType, string(data))
logger.Errorf("未知的ChirpStack事件: %s, data: %s", eventType, string(data))
}
}
// --- 业务处理函数 ---
// handleUpEvent 处理上行数据事件
func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
c.logger.Infof("开始处理 'up' 事件, DevEui: %s", event.DeviceInfo.DevEui)
func (c *ChirpStackListener) handleUpEvent(ctx context.Context, event *UpEvent) {
reqCtx, logger := logs.Trace(ctx, c.ctx, "ChirpStackListener.handleUpEvent")
logger.Infof("开始处理 'up' 事件, DevEui: %s", event.DeviceInfo.DevEui)
// 1. 查找区域主控设备
regionalController, err := c.areaControllerRepo.FindByNetworkID(event.DeviceInfo.DevEui)
regionalController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui)
if err != nil {
c.logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
return
}
// 依赖 SelfCheck 确保区域主控有效
if err := regionalController.SelfCheck(); err != nil {
c.logger.Errorf("处理 'up' 事件失败:区域主控 %v(ID: %d) 未通过自检: %v", regionalController.Name, regionalController.ID, err)
logger.Errorf("处理 'up' 事件失败:区域主控 %v(ID: %d) 未通过自检: %v", regionalController.Name, regionalController.ID, err)
return
}
c.logger.Infof("找到区域主控: %s (ID: %d)", regionalController.Name, regionalController.ID)
logger.Infof("找到区域主控: %s (ID: %d)", regionalController.Name, regionalController.ID)
// 2. 记录区域主控的信号强度 (如果存在)
if len(event.RxInfo) > 0 {
@@ -182,29 +187,29 @@ func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
}
// 记录信号强度
c.recordSensorData(regionalController.ID, regionalController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
c.logger.Infof("已记录区域主控 (ID: %d) 的信号强度: RSSI=%d, SNR=%.2f", regionalController.ID, rx.Rssi, rx.Snr)
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)
} else {
c.logger.Warnf("处理 'up' 事件时未找到 RxInfo无法记录信号数据。DevEui: %s", event.DeviceInfo.DevEui)
logger.Warnf("处理 'up' 事件时未找到 RxInfo无法记录信号数据。DevEui: %s", event.DeviceInfo.DevEui)
}
// 3. 处理上报的传感器数据
if event.Data == "" {
c.logger.Warnf("处理 'up' 事件时 Data 字段为空无需记录上行数据。DevEui: %s", event.DeviceInfo.DevEui)
logger.Warnf("处理 'up' 事件时 Data 字段为空无需记录上行数据。DevEui: %s", event.DeviceInfo.DevEui)
return
}
// 3.1 Base64 解码
decodedData, err := base64.StdEncoding.DecodeString(event.Data)
if err != nil {
c.logger.Errorf("Base64 解码 'up' 事件的 Data 失败: %v, Data: %s", err, event.Data)
logger.Errorf("Base64 解码 'up' 事件的 Data 失败: %v, Data: %s", err, event.Data)
return
}
// 3.2 解析外层 "信封"
var instruction proto.Instruction
if err := gproto.Unmarshal(decodedData, &instruction); err != nil {
c.logger.Errorf("解析上行 Instruction Protobuf 失败: %v, Decoded Data: %x", err, decodedData)
logger.Errorf("解析上行 Instruction Protobuf 失败: %v, Decoded Data: %x", err, decodedData)
return
}
@@ -215,29 +220,29 @@ func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
collectResp = p.CollectResult
default:
// 如果上行的数据不是采集结果,记录日志并忽略
c.logger.Infof("收到一个非采集响应的上行指令 (Type: %T),无需处理。", p)
logger.Infof("收到一个非采集响应的上行指令 (Type: %T),无需处理。", p)
return
}
// 检查 collectResp 是否为 nil虽然在 type switch 成功的情况下不太可能
if collectResp == nil {
c.logger.Errorf("从 Instruction 中提取的 CollectResult 为 nil")
logger.Errorf("从 Instruction 中提取的 CollectResult 为 nil")
return
}
correlationID := collectResp.CorrelationId
c.logger.Infof("成功解析采集响应 (CorrelationID: %s),包含 %d 个值。", correlationID, len(collectResp.Values))
logger.Infof("成功解析采集响应 (CorrelationID: %s),包含 %d 个值。", correlationID, len(collectResp.Values))
// 4. 根据 CorrelationID 查找待处理请求
pendingReq, err := c.pendingCollectionRepo.FindByCorrelationID(correlationID)
pendingReq, err := c.pendingCollectionRepo.FindByCorrelationID(reqCtx, correlationID)
if err != nil {
c.logger.Errorf("处理采集响应失败:无法找到待处理请求 (CorrelationID: %s): %v", correlationID, err)
logger.Errorf("处理采集响应失败:无法找到待处理请求 (CorrelationID: %s): %v", correlationID, err)
return
}
// 检查状态,防止重复处理
if pendingReq.Status != models.PendingStatusPending && pendingReq.Status != models.PendingStatusTimedOut {
c.logger.Warnf("收到一个已处理过的采集响应 (CorrelationID: %s, Status: %s),将忽略。", correlationID, pendingReq.Status)
logger.Warnf("收到一个已处理过的采集响应 (CorrelationID: %s, Status: %s),将忽略。", correlationID, pendingReq.Status)
return
}
@@ -245,11 +250,11 @@ func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
deviceIDs := pendingReq.CommandMetadata
values := collectResp.Values
if len(deviceIDs) != len(values) {
c.logger.Errorf("数据不匹配:下行指令要求采集 %d 个设备,但上行响应包含 %d 个值 (CorrelationID: %s)", len(deviceIDs), len(values), correlationID)
logger.Errorf("数据不匹配:下行指令要求采集 %d 个设备,但上行响应包含 %d 个值 (CorrelationID: %s)", len(deviceIDs), len(values), correlationID)
// 即使数量不匹配,也更新状态为完成,以防止请求永远 pending
err = c.pendingCollectionRepo.UpdateStatusToFulfilled(correlationID, event.Time)
err = c.pendingCollectionRepo.UpdateStatusToFulfilled(reqCtx, correlationID, event.Time)
if err != nil {
c.logger.Errorf("处理采集响应失败:无法更新待处理请求 (CorrelationID: %s) 的状态为完成: %v", correlationID, err)
logger.Errorf("处理采集响应失败:无法更新待处理请求 (CorrelationID: %s) 的状态为完成: %v", correlationID, err)
}
return
}
@@ -259,35 +264,35 @@ func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
// 检查设备上报的值是否为 NaN (Not a Number),如果是则跳过
if math.IsNaN(float64(rawSensorValue)) {
c.logger.Warnf("设备 (ID: %d) 上报了一个无效的 NaN 值,已跳过当前值的记录。", deviceID)
logger.Warnf("设备 (ID: %d) 上报了一个无效的 NaN 值,已跳过当前值的记录。", deviceID)
continue
}
// 5.1 获取设备及其模板
dev, err := c.deviceRepo.FindByID(deviceID)
dev, err := c.deviceRepo.FindByID(reqCtx, deviceID)
if err != nil {
c.logger.Errorf("处理采集数据失败:无法找到设备 (ID: %d): %v", deviceID, err)
logger.Errorf("处理采集数据失败:无法找到设备 (ID: %d): %v", deviceID, err)
continue
}
// 依赖 SelfCheck 确保设备和模板有效
if err := dev.SelfCheck(); err != nil {
c.logger.Warnf("跳过设备 %d因其未通过自检: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其未通过自检: %v", dev.ID, err)
continue
}
if err := dev.DeviceTemplate.SelfCheck(); err != nil {
c.logger.Warnf("跳过设备 %d因其设备模板未通过自检: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其设备模板未通过自检: %v", dev.ID, err)
continue
}
// 5.2 从设备模板中解析 ValueDescriptor
var valueDescriptors []*models.ValueDescriptor
if err := dev.DeviceTemplate.ParseValues(&valueDescriptors); err != nil {
c.logger.Warnf("跳过设备 %d因其设备模板的 Values 属性解析失败: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其设备模板的 Values 属性解析失败: %v", dev.ID, err)
continue
}
// 根据 DeviceTemplate.SelfCheck这里应该只有一个 ValueDescriptor
if len(valueDescriptors) == 0 {
c.logger.Warnf("跳过设备 %d因其设备模板缺少 ValueDescriptor 定义", dev.ID)
logger.Warnf("跳过设备 %d因其设备模板缺少 ValueDescriptor 定义", dev.ID)
continue
}
valueDescriptor := valueDescriptors[0]
@@ -306,31 +311,32 @@ func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
dataToRecord = models.WeightData{WeightKilograms: parsedValue}
default:
// TODO 未知传感器的数据需要记录吗
c.logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type)
logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type)
dataToRecord = map[string]float64{"value": parsedValue}
}
// 5.5 记录传感器数据
c.recordSensorData(regionalController.ID, dev.ID, event.Time, valueDescriptor.Type, dataToRecord)
c.logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 原始值=%f, 解析值=%.2f", dev.ID, valueDescriptor.Type, rawSensorValue, parsedValue)
c.recordSensorData(reqCtx, regionalController.ID, dev.ID, event.Time, valueDescriptor.Type, dataToRecord)
logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 原始值=%f, 解析值=%.2f", dev.ID, valueDescriptor.Type, rawSensorValue, parsedValue)
}
// 6. 更新请求状态为“已完成”
if err := c.pendingCollectionRepo.UpdateStatusToFulfilled(correlationID, event.Time); err != nil {
c.logger.Errorf("更新待采集请求状态为 'fulfilled' 失败 (CorrelationID: %s): %v", correlationID, err)
if err := c.pendingCollectionRepo.UpdateStatusToFulfilled(reqCtx, correlationID, event.Time); err != nil {
logger.Errorf("更新待采集请求状态为 'fulfilled' 失败 (CorrelationID: %s): %v", correlationID, err)
} else {
c.logger.Infof("成功完成并关闭采集请求 (CorrelationID: %s)", correlationID)
logger.Infof("成功完成并关闭采集请求 (CorrelationID: %s)", correlationID)
}
}
// handleStatusEvent 处理设备状态事件
func (c *ChirpStackListener) handleStatusEvent(event *StatusEvent) {
c.logger.Infof("处接收到理 'status' 事件: %+v", event)
func (c *ChirpStackListener) handleStatusEvent(ctx context.Context, event *StatusEvent) {
reqCtx, logger := logs.Trace(ctx, c.ctx, "handleStatusEvent")
logger.Infof("处接收到理 'status' 事件: %+v", event)
// 查找区域主控设备
regionalController, err := c.areaControllerRepo.FindByNetworkID(event.DeviceInfo.DevEui)
regionalController, err := c.areaControllerRepo.FindByNetworkID(reqCtx, event.DeviceInfo.DevEui)
if err != nil {
c.logger.Errorf("处理 'status' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
logger.Errorf("处理 'status' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
return
}
@@ -338,8 +344,8 @@ func (c *ChirpStackListener) handleStatusEvent(event *StatusEvent) {
signalMetrics := models.SignalMetrics{
MarginDb: event.Margin,
}
c.recordSensorData(regionalController.ID, regionalController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
c.logger.Infof("已记录区域主控 (ID: %d) 的信号状态: %+v", regionalController.ID, signalMetrics)
c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeSignalMetrics, signalMetrics)
logger.Infof("已记录区域主控 (ID: %d) 的信号状态: %+v", regionalController.ID, signalMetrics)
// 记录电量
batteryLevel := models.BatteryLevel{
@@ -347,68 +353,74 @@ func (c *ChirpStackListener) handleStatusEvent(event *StatusEvent) {
BatteryLevelUnavailable: event.BatteryLevelUnavailable,
ExternalPower: event.ExternalPower,
}
c.recordSensorData(regionalController.ID, regionalController.ID, event.Time, models.SensorTypeBatteryLevel, batteryLevel)
c.logger.Infof("已记录区域主控 (ID: %d) 的电池状态: %+v", regionalController.ID, batteryLevel)
c.recordSensorData(reqCtx, regionalController.ID, regionalController.ID, event.Time, models.SensorTypeBatteryLevel, batteryLevel)
logger.Infof("已记录区域主控 (ID: %d) 的电池状态: %+v", regionalController.ID, batteryLevel)
}
// handleAckEvent 处理下行确认事件
func (c *ChirpStackListener) handleAckEvent(event *AckEvent) {
c.logger.Infof("接收到 'ack' 事件: %+v", event)
func (c *ChirpStackListener) handleAckEvent(ctx context.Context, event *AckEvent) {
reqCtx, logger := logs.Trace(ctx, c.ctx, "handleAckEvent")
logger.Infof("接收到 'ack' 事件: %+v", event)
// 更新下行任务记录的确认时间及接收成功状态
err := c.deviceCommandLogRepo.UpdateAcknowledgedAt(event.DeduplicationID, event.Time, event.Acknowledged)
err := c.deviceCommandLogRepo.UpdateAcknowledgedAt(reqCtx, event.DeduplicationID, event.Time, event.Acknowledged)
if err != nil {
c.logger.Errorf("更新下行任务记录的确认时间及接收成功状态失败 (MessageID: %s, DevEui: %s, Acknowledged: %t): %v",
logger.Errorf("更新下行任务记录的确认时间及接收成功状态失败 (MessageID: %s, DevEui: %s, Acknowledged: %t): %v",
event.DeduplicationID, event.DeviceInfo.DevEui, event.Acknowledged, err)
return
}
c.logger.Infof("成功更新下行任务记录确认时间及接收成功状态 (MessageID: %s, DevEui: %s, Acknowledged: %t, AcknowledgedAt: %s)",
logger.Infof("成功更新下行任务记录确认时间及接收成功状态 (MessageID: %s, DevEui: %s, Acknowledged: %t, AcknowledgedAt: %s)",
event.DeduplicationID, event.DeviceInfo.DevEui, event.Acknowledged, event.Time.Format(time.RFC3339))
}
// handleLogEvent 处理日志事件
func (c *ChirpStackListener) handleLogEvent(event *LogEvent) {
func (c *ChirpStackListener) handleLogEvent(ctx context.Context, event *LogEvent) {
logger := logs.TraceLogger(ctx, c.ctx, "handleLogEvent")
// 首先,打印完整的事件结构体,用于详细排查
c.logger.Infof("接收到 'log' 事件的完整内容: %+v", event)
logger.Infof("接收到 'log' 事件的完整内容: %+v", event)
// 接着,根据 ChirpStack 日志的级别,使用我们自己的 logger 对应级别来打印核心信息
logMessage := "ChirpStack 日志: [%s] %s (DevEui: %s)"
switch event.Level {
case "INFO":
c.logger.Infof(logMessage, event.Code, event.Description, event.DeviceInfo.DevEui)
logger.Infof(logMessage, event.Code, event.Description, event.DeviceInfo.DevEui)
case "WARNING":
c.logger.Warnf(logMessage, event.Code, event.Description, event.DeviceInfo.DevEui)
logger.Warnf(logMessage, event.Code, event.Description, event.DeviceInfo.DevEui)
case "ERROR":
c.logger.Errorf(logMessage, event.Code, event.Description, event.DeviceInfo.DevEui)
logger.Errorf(logMessage, event.Code, event.Description, event.DeviceInfo.DevEui)
default:
// 对于未知级别,使用 Warn 级别打印,并明确指出级别未知
c.logger.Warnf("ChirpStack 日志: [未知级别: %s] %s %s (DevEui: %s)",
logger.Warnf("ChirpStack 日志: [未知级别: %s] %s %s (DevEui: %s)",
event.Level, event.Code, event.Description, event.DeviceInfo.DevEui)
}
}
// handleJoinEvent 处理入网事件
func (c *ChirpStackListener) handleJoinEvent(event *JoinEvent) {
c.logger.Infof("接收到 'join' 事件: %+v", event)
func (c *ChirpStackListener) handleJoinEvent(ctx context.Context, event *JoinEvent) {
logger := logs.TraceLogger(ctx, c.ctx, "handleJoinEvent")
logger.Infof("接收到 'join' 事件: %+v", event)
// 在这里添加您的业务逻辑
}
// handleTxAckEvent 处理网关发送确认事件
func (c *ChirpStackListener) handleTxAckEvent(event *TxAckEvent) {
c.logger.Infof("接收到 'txack' 事件: %+v", event)
func (c *ChirpStackListener) handleTxAckEvent(ctx context.Context, event *TxAckEvent) {
logger := logs.TraceLogger(ctx, c.ctx, "handleTxAckEvent")
logger.Infof("接收到 'txack' 事件: %+v", event)
// 在这里添加您的业务逻辑
}
// handleLocationEvent 处理位置事件
func (c *ChirpStackListener) handleLocationEvent(event *LocationEvent) {
c.logger.Infof("接收到 'location' 事件: %+v", event)
func (c *ChirpStackListener) handleLocationEvent(ctx context.Context, event *LocationEvent) {
logger := logs.TraceLogger(ctx, c.ctx, "handleLocationEvent")
logger.Infof("接收到 'location' 事件: %+v", event)
// 在这里添加您的业务逻辑
}
// handleIntegrationEvent 处理集成事件
func (c *ChirpStackListener) handleIntegrationEvent(event *IntegrationEvent) {
c.logger.Infof("接收到 'integration' 事件: %+v", event)
func (c *ChirpStackListener) handleIntegrationEvent(ctx context.Context, event *IntegrationEvent) {
logger := logs.TraceLogger(ctx, c.ctx, "handleIntegrationEvent")
logger.Infof("接收到 'integration' 事件: %+v", event)
// 在这里添加您的业务逻辑
}
@@ -417,11 +429,12 @@ func (c *ChirpStackListener) handleIntegrationEvent(event *IntegrationEvent) {
// sensorDeviceID: 实际产生传感器数据的普通设备的ID
// sensorType: 传感器值的类型 (例如 models.SensorTypeTemperature)
// data: 具体的传感器数据结构体实例 (例如 models.TemperatureData)
func (c *ChirpStackListener) recordSensorData(regionalControllerID uint, sensorDeviceID uint, eventTime time.Time, sensorType models.SensorType, data interface{}) {
func (c *ChirpStackListener) recordSensorData(ctx context.Context, regionalControllerID uint, sensorDeviceID uint, eventTime time.Time, sensorType models.SensorType, data interface{}) {
reqCtx, logger := logs.Trace(ctx, c.ctx, "recordSensorData")
// 1. 将传入的结构体序列化为 JSON
jsonData, err := json.Marshal(data)
if err != nil {
c.logger.Errorf("记录传感器数据失败:序列化数据为 JSON 时出错: %v", err)
logger.Errorf("记录传感器数据失败:序列化数据为 JSON 时出错: %v", err)
return
}
@@ -435,7 +448,7 @@ func (c *ChirpStackListener) recordSensorData(regionalControllerID uint, sensorD
}
// 3. 调用仓库创建记录
if err := c.sensorDataRepo.Create(sensorData); err != nil {
c.logger.Errorf("记录传感器数据失败:存入数据库时出错: %v", err)
if err := c.sensorDataRepo.Create(reqCtx, sensorData); err != nil {
logger.Errorf("记录传感器数据失败:存入数据库时出错: %v", err)
}
}

View File

@@ -1,6 +1,7 @@
package webhook
import (
"context"
"net/http"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
@@ -8,15 +9,14 @@ import (
// PlaceholderListener 是一个占位符, 用于在非 LoRaWAN 配置下满足 ListenHandler 接口
type PlaceholderListener struct {
logger *logs.Logger
ctx context.Context
}
// NewPlaceholderListener 创建一个新的 PlaceholderListener 实例
// 它只打印一条日志, 表明 ChirpStack webhook 未被激活
func NewPlaceholderListener(logger *logs.Logger) ListenHandler {
logger.Info("当前配置非 LoRaWAN, ChirpStack webhook 监听器未激活。")
func NewPlaceholderListener(ctx context.Context) ListenHandler {
return &PlaceholderListener{
logger: logger,
ctx: ctx,
}
}
@@ -24,7 +24,8 @@ func NewPlaceholderListener(logger *logs.Logger) ListenHandler {
// 理论上, 在占位符生效的模式下, 这个 Handler 不应该被调用
func (p *PlaceholderListener) Handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
p.logger.Warn("PlaceholderListener 的 Handler 被调用, 这通常是意料之外的。")
logger := logs.TraceLogger(r.Context(), p.ctx, "PlaceholderListener")
logger.Warn("PlaceholderListener 的 Handler 被调用, 这通常是意料之外的。")
w.WriteHeader(http.StatusNotImplemented)
}
}

View File

@@ -1,6 +1,7 @@
package core
import (
"context"
"fmt"
"os"
"os/signal"
@@ -14,7 +15,7 @@ import (
// Application 是整个应用的核心,封装了所有组件和生命周期。
type Application struct {
Config *config.Config
Logger *logs.Logger
Ctx context.Context
API *api.API
Infra *Infrastructure
@@ -30,20 +31,25 @@ func NewApplication(configPath string) (*Application, error) {
if err := cfg.Load(configPath); err != nil {
return nil, fmt.Errorf("无法加载配置: %w", err)
}
logger := logs.NewLogger(cfg.Log)
// 初始化全局日志记录器
logs.InitDefaultLogger(cfg.Log)
// 为 Application 本身创建 Ctx
selfCtx := logs.AddCompName(context.Background(), "Application")
ctx := logs.AddFuncName(selfCtx, selfCtx, "NewApplication")
// 2. 初始化所有分层服务
infra, err := initInfrastructure(cfg, logger)
infra, err := initInfrastructure(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("初始化基础设施失败: %w", err)
}
domain := initDomainServices(cfg, infra, logger)
appServices := initAppServices(infra, domain, logger)
domain := initDomainServices(ctx, cfg, infra)
appServices := initAppServices(ctx, infra, domain)
// 3. 初始化 API 入口点
apiServer := api.NewAPI(
cfg.Server,
logger,
logs.AddCompName(context.Background(), "API"),
infra.repos.userRepo,
appServices.pigFarmService,
appServices.pigBatchService,
@@ -51,15 +57,15 @@ func NewApplication(configPath string) (*Application, error) {
appServices.deviceService,
appServices.planService,
appServices.userService,
infra.tokenService,
appServices.auditService,
infra.tokenGenerator,
infra.lora.listenHandler,
)
// 4. 组装 Application 对象
app := &Application{
Config: cfg,
Logger: logger,
Ctx: selfCtx,
API: apiServer,
Infra: infra,
Domain: domain,
@@ -71,20 +77,21 @@ func NewApplication(configPath string) (*Application, error) {
// Start 启动应用的所有组件并阻塞,直到接收到关闭信号。
func (app *Application) Start() error {
app.Logger.Info("应用启动中...")
startCtx, logger := logs.Trace(app.Ctx, app.Ctx, "Start")
logger.Info("应用启动中...")
// 1. 启动底层监听器
if err := app.Infra.lora.loraListener.Listen(); err != nil {
if err := app.Infra.lora.loraListener.Listen(startCtx); err != nil {
return fmt.Errorf("启动 LoRa Mesh 监听器失败: %w", err)
}
// 2. 初始化应用状态 (清理、刷新任务等)
if err := app.initializeState(); err != nil {
if err := app.initializeState(startCtx); err != nil {
return fmt.Errorf("初始化应用状态失败: %w", err)
}
// 3. 启动后台工作协程
app.Domain.planService.Start()
app.Domain.planService.Start(startCtx)
// 4. 启动 API 服务器
app.API.Start()
@@ -100,27 +107,28 @@ func (app *Application) Start() error {
// Stop 优雅地关闭应用的所有组件。
func (app *Application) Stop() error {
app.Logger.Info("应用关闭中...")
stopCtx, logger := logs.Trace(app.Ctx, app.Ctx, "Stop")
logger.Info("应用关闭中...")
// 关闭 API 服务器
app.API.Stop()
// 关闭任务执行器
app.Domain.planService.Stop()
app.Domain.planService.Stop(stopCtx)
// 断开数据库连接
if err := app.Infra.storage.Disconnect(); err != nil {
app.Logger.Errorw("数据库连接断开失败", "error", err)
if err := app.Infra.storage.Disconnect(stopCtx); err != nil {
logger.Errorw("数据库连接断开失败", "error", err)
}
// 关闭 LoRa Mesh 监听器
if err := app.Infra.lora.loraListener.Stop(); err != nil {
app.Logger.Errorw("LoRa Mesh 监听器关闭失败", "error", err)
if err := app.Infra.lora.loraListener.Stop(stopCtx); err != nil {
logger.Errorw("LoRa Mesh 监听器关闭失败", "error", err)
}
// 刷新日志缓冲区
_ = app.Logger.Sync()
_ = logger.Sync()
app.Logger.Info("应用已成功关闭")
logger.Info("应用已成功关闭")
return nil
}

View File

@@ -1,18 +1,17 @@
package core
import (
"context"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/token"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/database"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
@@ -21,45 +20,46 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"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"
)
// Infrastructure 聚合了所有基础设施层的组件。
type Infrastructure struct {
storage database.Storage
repos *Repositories
lora *LoraComponents
notifyService domain_notify.Service
tokenService token.Service
storage database.Storage
repos *Repositories
lora *LoraComponents
notifyService domain_notify.Service
tokenGenerator token.Generator
}
// initInfrastructure 初始化所有基础设施层组件。
func initInfrastructure(cfg *config.Config, logger *logs.Logger) (*Infrastructure, error) {
storage, err := initStorage(cfg.Database, logger)
func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructure, error) {
storage, err := initStorage(ctx, cfg.Database)
if err != nil {
return nil, err
}
repos := initRepositories(storage.GetDB(), logger)
repos := initRepositories(ctx, storage.GetDB(ctx))
lora, err := initLora(cfg, logger, repos)
lora, err := initLora(ctx, cfg, repos)
if err != nil {
return nil, err
}
notifyService, err := initNotifyService(cfg.Notify, logger, repos.userRepo, repos.notificationRepo)
notifyService, err := initNotifyService(ctx, cfg.Notify, repos.userRepo, repos.notificationRepo)
if err != nil {
return nil, fmt.Errorf("初始化通知服务失败: %w", err)
}
tokenService := token.NewTokenService([]byte(cfg.App.JWTSecret))
tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret))
return &Infrastructure{
storage: storage,
repos: repos,
lora: lora,
notifyService: notifyService,
tokenService: tokenService,
storage: storage,
repos: repos,
lora: lora,
notifyService: notifyService,
tokenGenerator: tokenGenerator,
}, nil
}
@@ -90,30 +90,31 @@ type Repositories struct {
}
// initRepositories 初始化所有的仓库。
func initRepositories(db *gorm.DB, logger *logs.Logger) *Repositories {
func initRepositories(ctx context.Context, db *gorm.DB) *Repositories {
baseCtx := context.Background()
return &Repositories{
userRepo: repository.NewGormUserRepository(db),
deviceRepo: repository.NewGormDeviceRepository(db),
areaControllerRepo: repository.NewGormAreaControllerRepository(db),
deviceTemplateRepo: repository.NewGormDeviceTemplateRepository(db),
planRepo: repository.NewGormPlanRepository(db),
pendingTaskRepo: repository.NewGormPendingTaskRepository(db),
executionLogRepo: repository.NewGormExecutionLogRepository(db),
sensorDataRepo: repository.NewGormSensorDataRepository(db),
deviceCommandLogRepo: repository.NewGormDeviceCommandLogRepository(db),
pendingCollectionRepo: repository.NewGormPendingCollectionRepository(db),
userActionLogRepo: repository.NewGormUserActionLogRepository(db),
pigBatchRepo: repository.NewGormPigBatchRepository(db),
pigBatchLogRepo: repository.NewGormPigBatchLogRepository(db),
pigFarmRepo: repository.NewGormPigFarmRepository(db),
pigPenRepo: repository.NewGormPigPenRepository(db),
pigTransferLogRepo: repository.NewGormPigTransferLogRepository(db),
pigTradeRepo: repository.NewGormPigTradeRepository(db),
pigSickPigLogRepo: repository.NewGormPigSickLogRepository(db),
medicationLogRepo: repository.NewGormMedicationLogRepository(db),
rawMaterialRepo: repository.NewGormRawMaterialRepository(db),
notificationRepo: repository.NewGormNotificationRepository(db),
unitOfWork: repository.NewGormUnitOfWork(db, logger),
userRepo: repository.NewGormUserRepository(logs.AddCompName(baseCtx, "UserRepo"), db),
deviceRepo: repository.NewGormDeviceRepository(logs.AddCompName(baseCtx, "DeviceRepo"), db),
areaControllerRepo: repository.NewGormAreaControllerRepository(logs.AddCompName(baseCtx, "AreaControllerRepo"), db),
deviceTemplateRepo: repository.NewGormDeviceTemplateRepository(logs.AddCompName(baseCtx, "DeviceTemplateRepo"), db),
planRepo: repository.NewGormPlanRepository(logs.AddCompName(baseCtx, "PlanRepo"), db),
pendingTaskRepo: repository.NewGormPendingTaskRepository(logs.AddCompName(baseCtx, "PendingTaskRepo"), db),
executionLogRepo: repository.NewGormExecutionLogRepository(logs.AddCompName(baseCtx, "ExecutionLogRepo"), db),
sensorDataRepo: repository.NewGormSensorDataRepository(logs.AddCompName(baseCtx, "SensorDataRepo"), db),
deviceCommandLogRepo: repository.NewGormDeviceCommandLogRepository(logs.AddCompName(baseCtx, "DeviceCommandLogRepo"), db),
pendingCollectionRepo: repository.NewGormPendingCollectionRepository(logs.AddCompName(baseCtx, "PendingCollectionRepo"), db),
userActionLogRepo: repository.NewGormUserActionLogRepository(logs.AddCompName(baseCtx, "UserActionLogRepo"), db),
pigBatchRepo: repository.NewGormPigBatchRepository(logs.AddCompName(baseCtx, "PigBatchRepo"), db),
pigBatchLogRepo: repository.NewGormPigBatchLogRepository(logs.AddCompName(baseCtx, "PigBatchLogRepo"), db),
pigFarmRepo: repository.NewGormPigFarmRepository(logs.AddCompName(baseCtx, "PigFarmRepo"), db),
pigPenRepo: repository.NewGormPigPenRepository(logs.AddCompName(baseCtx, "PigPenRepo"), db),
pigTransferLogRepo: repository.NewGormPigTransferLogRepository(logs.AddCompName(baseCtx, "PigTransferLogRepo"), db),
pigTradeRepo: repository.NewGormPigTradeRepository(logs.AddCompName(baseCtx, "PigTradeRepo"), db),
pigSickPigLogRepo: repository.NewGormPigSickLogRepository(logs.AddCompName(baseCtx, "PigSickPigLogRepo"), db),
medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), db),
rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db),
notificationRepo: repository.NewGormNotificationRepository(logs.AddCompName(baseCtx, "NotificationRepo"), db),
unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db),
}
}
@@ -131,31 +132,40 @@ type DomainServices struct {
}
// initDomainServices 初始化所有的领域服务。
func initDomainServices(cfg *config.Config, infra *Infrastructure, logger *logs.Logger) *DomainServices {
func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastructure) *DomainServices {
baseCtx := context.Background()
// 猪群管理相关
pigPenTransferManager := pig.NewPigPenTransferManager(infra.repos.pigPenRepo, infra.repos.pigTransferLogRepo, infra.repos.pigBatchRepo)
pigTradeManager := pig.NewPigTradeManager(infra.repos.pigTradeRepo)
pigSickManager := pig.NewSickPigManager(infra.repos.pigSickPigLogRepo, infra.repos.medicationLogRepo)
pigBatchDomain := pig.NewPigBatchService(infra.repos.pigBatchRepo, infra.repos.pigBatchLogRepo, infra.repos.unitOfWork,
pigPenTransferManager, pigTradeManager, pigSickManager)
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)
pigSickManager := pig.NewSickPigManager(logs.AddCompName(baseCtx, "PigSickManager"), infra.repos.pigSickPigLogRepo, infra.repos.medicationLogRepo)
pigBatchDomain := pig.NewPigBatchService(
logs.AddCompName(baseCtx, "PigBatchDomain"),
infra.repos.pigBatchRepo,
infra.repos.pigBatchLogRepo,
infra.repos.unitOfWork,
pigPenTransferManager,
pigTradeManager, pigSickManager,
)
// 通用设备服务
generalDeviceService := device.NewGeneralDeviceService(
logs.AddCompName(baseCtx, "GeneralDeviceService"),
infra.repos.deviceRepo,
infra.repos.deviceCommandLogRepo,
infra.repos.pendingCollectionRepo,
logger,
infra.lora.comm,
)
// 任务工厂
taskFactory := task.NewTaskFactory(logger, infra.repos.sensorDataRepo, infra.repos.deviceRepo, generalDeviceService)
taskFactory := task.NewTaskFactory(logs.AddCompName(baseCtx, "TaskFactory"), infra.repos.sensorDataRepo, infra.repos.deviceRepo, generalDeviceService)
// 计划任务管理器
analysisPlanTaskManager := plan.NewAnalysisPlanTaskManager(infra.repos.planRepo, infra.repos.pendingTaskRepo, infra.repos.executionLogRepo, logger)
analysisPlanTaskManager := plan.NewAnalysisPlanTaskManager(logs.AddCompName(baseCtx, "AnalysisPlanTaskManager"), infra.repos.planRepo, infra.repos.pendingTaskRepo, infra.repos.executionLogRepo)
// 任务执行器
planExecutionManager := plan.NewPlanExecutionManager(
logs.AddCompName(baseCtx, "PlanExecutionManager"),
infra.repos.pendingTaskRepo,
infra.repos.executionLogRepo,
infra.repos.deviceRepo,
@@ -163,7 +173,6 @@ func initDomainServices(cfg *config.Config, infra *Infrastructure, logger *logs.
infra.repos.planRepo,
analysisPlanTaskManager,
taskFactory,
logger,
generalDeviceService,
time.Duration(cfg.Task.Interval)*time.Second,
cfg.Task.NumWorkers,
@@ -171,13 +180,14 @@ func initDomainServices(cfg *config.Config, infra *Infrastructure, logger *logs.
// 计划管理器
planService := plan.NewPlanService(
logs.AddCompName(baseCtx, "PlanService"),
planExecutionManager,
analysisPlanTaskManager,
infra.repos.planRepo,
infra.repos.deviceRepo,
infra.repos.unitOfWork,
taskFactory,
logger)
)
return &DomainServices{
pigPenTransferManager: pigPenTransferManager,
@@ -200,14 +210,16 @@ type AppServices struct {
deviceService service.DeviceService
planService service.PlanService
userService service.UserService
auditService audit.Service
auditService service.AuditService
}
// initAppServices 初始化所有的应用服务。
func initAppServices(infra *Infrastructure, domainServices *DomainServices, logger *logs.Logger) *AppServices {
pigFarmService := service.NewPigFarmService(infra.repos.pigFarmRepo, infra.repos.pigPenRepo, infra.repos.pigBatchRepo, domainServices.pigBatchDomain, infra.repos.unitOfWork, logger)
pigBatchService := service.NewPigBatchService(domainServices.pigBatchDomain, logger)
func initAppServices(ctx context.Context, infra *Infrastructure, domainServices *DomainServices) *AppServices {
baseCtx := context.Background()
pigFarmService := service.NewPigFarmService(logs.AddCompName(baseCtx, "PigFarmService"), infra.repos.pigFarmRepo, infra.repos.pigPenRepo, infra.repos.pigBatchRepo, domainServices.pigBatchDomain, infra.repos.unitOfWork)
pigBatchService := service.NewPigBatchService(logs.AddCompName(baseCtx, "PigBatchService"), domainServices.pigBatchDomain)
monitorService := service.NewMonitorService(
logs.AddCompName(baseCtx, "MonitorService"),
infra.repos.sensorDataRepo,
infra.repos.deviceCommandLogRepo,
infra.repos.executionLogRepo,
@@ -224,14 +236,15 @@ func initAppServices(infra *Infrastructure, domainServices *DomainServices, logg
infra.repos.notificationRepo,
)
deviceService := service.NewDeviceService(
logs.AddCompName(baseCtx, "DeviceService"),
infra.repos.deviceRepo,
infra.repos.areaControllerRepo,
infra.repos.deviceTemplateRepo,
domainServices.generalDeviceService,
)
auditService := audit.NewService(infra.repos.userActionLogRepo, logger)
planService := service.NewPlanService(logger, domainServices.planService)
userService := service.NewUserService(infra.repos.userRepo, infra.tokenService, infra.notifyService, logger)
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)
return &AppServices{
pigFarmService: pigFarmService,
@@ -253,23 +266,25 @@ type LoraComponents struct {
// initLora 根据配置初始化 LoRa 相关组件。
func initLora(
ctx context.Context,
cfg *config.Config,
logger *logs.Logger,
repos *Repositories,
) (*LoraComponents, error) {
var listenHandler webhook.ListenHandler
var comm transport.Communicator
var loraListener transport.Listener
baseCtx := context.Background()
logger := logs.GetLogger(ctx)
if cfg.Lora.Mode == config.LoraMode_LoRaWAN {
logger.Info("当前运行模式: lora_wan。初始化 ChirpStack 监听器和传输层。")
listenHandler = webhook.NewChirpStackListener(logger, repos.sensorDataRepo, repos.deviceRepo, repos.areaControllerRepo, repos.deviceCommandLogRepo, repos.pendingCollectionRepo)
comm = lora.NewChirpStackTransport(cfg.ChirpStack, logger)
loraListener = lora.NewPlaceholderTransport(logger)
listenHandler = webhook.NewChirpStackListener(logs.AddCompName(baseCtx, "ChirpStackListener"), repos.sensorDataRepo, repos.deviceRepo, repos.areaControllerRepo, repos.deviceCommandLogRepo, repos.pendingCollectionRepo)
comm = lora.NewChirpStackTransport(logs.AddCompName(baseCtx, "ChirpStackTransport"), cfg.ChirpStack)
loraListener = lora.NewPlaceholderTransport(logs.AddCompName(baseCtx, "PlaceholderTransport"))
} else {
logger.Info("当前运行模式: lora_mesh。初始化 LoRa Mesh 传输层和占位符监听器。")
listenHandler = webhook.NewPlaceholderListener(logger)
tp, err := lora.NewLoRaMeshUartPassthroughTransport(cfg.LoraMesh, logger, repos.areaControllerRepo, repos.pendingCollectionRepo, repos.deviceRepo, repos.sensorDataRepo)
listenHandler = webhook.NewPlaceholderListener(logs.AddCompName(baseCtx, "PlaceholderListener"))
tp, err := lora.NewLoRaMeshUartPassthroughTransport(logs.AddCompName(baseCtx, "LoRaMeshTransport"), cfg.LoraMesh, repos.areaControllerRepo, repos.pendingCollectionRepo, repos.deviceRepo, repos.sensorDataRepo)
if err != nil {
return nil, fmt.Errorf("无法初始化 LoRa Mesh 模块: %w", err)
}
@@ -287,21 +302,24 @@ func initLora(
// initNotifyService 根据配置初始化并返回一个通知领域服务。
// 它确保至少有一个 LogNotifier 总是可用,并根据配置启用其他通知器。
func initNotifyService(
ctx context.Context,
cfg config.NotifyConfig,
log *logs.Logger,
userRepo repository.UserRepository,
notificationRepo repository.NotificationRepository,
) (domain_notify.Service, error) {
var availableNotifiers []notify.Notifier
logger := logs.GetLogger(ctx)
baseCtx := context.Background()
// 1. 总是创建 LogNotifier 作为所有告警的最终记录渠道
logNotifier := notify.NewLogNotifier(log)
logNotifier := notify.NewLogNotifier(logs.AddCompName(baseCtx, "LogNotifier"))
availableNotifiers = append(availableNotifiers, logNotifier)
log.Info("Log通知器已启用 (作为所有告警的最终记录渠道)")
logger.Info("Log通知器已启用 (作为所有告警的最终记录渠道)")
// 2. 根据配置,按需创建并收集所有启用的其他 Notifier 实例
if cfg.SMTP.Enabled {
smtpNotifier := notify.NewSMTPNotifier(
logs.AddCompName(baseCtx, "SMTPNotifier"),
cfg.SMTP.Host,
cfg.SMTP.Port,
cfg.SMTP.Username,
@@ -309,26 +327,28 @@ func initNotifyService(
cfg.SMTP.Sender,
)
availableNotifiers = append(availableNotifiers, smtpNotifier)
log.Info("SMTP通知器已启用")
logger.Info("SMTP通知器已启用")
}
if cfg.WeChat.Enabled {
wechatNotifier := notify.NewWechatNotifier(
logs.AddCompName(baseCtx, "WechatNotifier"),
cfg.WeChat.CorpID,
cfg.WeChat.AgentID,
cfg.WeChat.Secret,
)
availableNotifiers = append(availableNotifiers, wechatNotifier)
log.Info("企业微信通知器已启用")
logger.Info("企业微信通知器已启用")
}
if cfg.Lark.Enabled {
larkNotifier := notify.NewLarkNotifier(
logs.AddCompName(baseCtx, "LarkNotifier"),
cfg.Lark.AppID,
cfg.Lark.AppSecret,
)
availableNotifiers = append(availableNotifiers, larkNotifier)
log.Info("飞书通知器已启用")
logger.Info("飞书通知器已启用")
}
// 3. 动态确定首选通知器
@@ -346,12 +366,12 @@ func initNotifyService(
// 如果用户指定的主渠道未启用或未指定,则自动选择第一个可用的 (这将是 LogNotifier如果其他都未启用)
if primaryNotifier == nil {
primaryNotifier = availableNotifiers[0] // 确保总能找到一个,因为 LogNotifier 总是存在的
log.Warnf("配置的首选渠道 '%s' 未启用或未指定,已自动降级使用 '%s' 作为首选渠道。", cfg.Primary, primaryNotifier.Type())
logger.Warnf("配置的首选渠道 '%s' 未启用或未指定,已自动降级使用 '%s' 作为首选渠道。", cfg.Primary, primaryNotifier.Type())
}
// 4. 使用创建的 Notifier 列表和 notificationRepo 来组装领域服务
notifyService, err := domain_notify.NewFailoverService(
log,
logs.AddCompName(baseCtx, "FailoverNotifyService"),
userRepo,
availableNotifiers,
primaryNotifier.Type(),
@@ -362,24 +382,24 @@ func initNotifyService(
return nil, fmt.Errorf("创建故障转移通知服务失败: %w", err)
}
log.Infof("通知服务初始化成功,首选渠道: %s, 故障阈值: %d", primaryNotifier.Type(), cfg.FailureThreshold)
logger.Infof("通知服务初始化成功,首选渠道: %s, 故障阈值: %d", primaryNotifier.Type(), cfg.FailureThreshold)
return notifyService, nil
}
// initStorage 封装了数据库的初始化、连接和迁移逻辑。
func initStorage(cfg config.DatabaseConfig, logger *logs.Logger) (database.Storage, error) {
func initStorage(ctx context.Context, cfg config.DatabaseConfig) (database.Storage, error) {
// 创建存储实例
storage := database.NewStorage(cfg, logger)
if err := storage.Connect(); err != nil {
storage := database.NewStorage(logs.AddCompName(context.Background(), "Storage"), cfg)
if err := storage.Connect(ctx); err != nil {
// 错误已在 Connect 内部被记录,这里只需包装并返回
return nil, fmt.Errorf("数据库连接失败: %w", err)
}
// 执行数据库迁移
if err := storage.Migrate(models.GetAllModels()...); err != nil {
if err := storage.Migrate(ctx, models.GetAllModels()...); err != nil {
return nil, fmt.Errorf("数据库迁移失败: %w", err)
}
logger.Info("数据库初始化完成。")
logs.GetLogger(ctx).Info("数据库初始化完成。")
return storage, nil
}

View File

@@ -1,8 +1,10 @@
package core
import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
)
@@ -14,19 +16,20 @@ const (
// initializeState 在应用启动时准备其初始数据状态。
// 这包括清理任何因上次异常关闭而留下的悬空任务或请求。
func (app *Application) initializeState() error {
func (app *Application) initializeState(ctx context.Context) error {
appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializeState")
// 初始化预定义系统计划 (致命错误)
if err := app.initializeSystemPlans(); err != nil {
if err := app.initializeSystemPlans(ctx); err != nil {
return fmt.Errorf("初始化预定义系统计划失败: %w", err)
}
// 清理待采集任务 (非致命错误)
if err := app.initializePendingCollections(); err != nil {
app.Logger.Errorw("清理待采集任务时发生非致命错误", "error", err)
if err := app.initializePendingCollections(appCtx); err != nil {
logger.Errorw("清理待采集任务时发生非致命错误", "error", err)
}
// 初始化待执行任务列表 (致命错误)
if err := app.initializePendingTasks(); err != nil {
if err := app.initializePendingTasks(appCtx); err != nil {
return fmt.Errorf("初始化待执行任务列表失败: %w", err)
}
@@ -34,14 +37,15 @@ func (app *Application) initializeState() error {
}
// initializeSystemPlans 确保预定义的系统计划在数据库中存在并保持最新。
func (app *Application) initializeSystemPlans() error {
app.Logger.Info("开始检查并更新预定义的系统计划...")
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(repository.ListPlansOptions{
existingPlans, _, err := app.Infra.repos.planRepo.ListPlans(appCtx, repository.ListPlansOptions{
PlanType: repository.PlanTypeFilterSystem,
}, 1, 99999) // 使用一个较大的 pageSize 来获取所有系统计划
if err != nil {
@@ -60,29 +64,29 @@ func (app *Application) initializeSystemPlans() error {
if foundExistingPlan, ok := existingPlanMap[predefinedPlan.Name]; ok {
// 如果计划存在,则进行无差别更新
app.Logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name)
logger.Infof("预定义计划 '%s' 已存在,正在进行无差别更新...", predefinedPlan.Name)
// 将数据库中已存在的计划的ID和运行时状态字段赋值给预定义计划
predefinedPlan.ID = foundExistingPlan.ID
predefinedPlan.ExecuteCount = foundExistingPlan.ExecuteCount
if err := app.Infra.repos.planRepo.UpdatePlan(predefinedPlan); err != nil {
if err := app.Infra.repos.planRepo.UpdatePlan(appCtx, predefinedPlan); err != nil {
return fmt.Errorf("更新预定义计划 '%s' 失败: %w", predefinedPlan.Name, err)
} else {
app.Logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name)
logger.Infof("成功更新预定义计划 '%s'。", predefinedPlan.Name)
}
} else {
// 如果计划不存在, 则创建
app.Logger.Infof("预定义计划 '%s' 不存在,正在创建...", predefinedPlan.Name)
if err := app.Infra.repos.planRepo.CreatePlan(predefinedPlan); err != nil {
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 {
app.Logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name)
logger.Infof("成功创建预定义计划 '%s'。", predefinedPlan.Name)
}
}
}
app.Logger.Info("预定义系统计划检查完成。")
logger.Info("预定义系统计划检查完成。")
return nil
}
@@ -119,25 +123,26 @@ func (app *Application) getPredefinedSystemPlans() []models.Plan {
// initializePendingCollections 在应用启动时处理所有未完成的采集请求。
// 我们的策略是:任何在程序重启前仍处于“待处理”状态的请求,都应被视为已失败。
// 这保证了系统在每次启动时都处于一个干净、确定的状态。
func (app *Application) initializePendingCollections() error {
app.Logger.Info("开始清理所有未完成的采集请求...")
func (app *Application) initializePendingCollections(ctx context.Context) error {
appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializePendingCollections")
logger.Info("开始清理所有未完成的采集请求...")
// 直接将所有 'pending' 状态的请求更新为 'timed_out'。
count, err := app.Infra.repos.pendingCollectionRepo.MarkAllPendingAsTimedOut()
count, err := app.Infra.repos.pendingCollectionRepo.MarkAllPendingAsTimedOut(appCtx)
if err != nil {
return fmt.Errorf("清理未完成的采集请求失败: %v", err)
} else if count > 0 {
app.Logger.Infof("成功将 %d 个未完成的采集请求标记为超时。", count)
logger.Infof("成功将 %d 个未完成的采集请求标记为超时。", count)
} else {
app.Logger.Info("没有需要清理的采集请求。")
logger.Info("没有需要清理的采集请求。")
}
return nil
}
// initializePendingTasks 在应用启动时清理并刷新待执行任务列表。
func (app *Application) initializePendingTasks() error {
logger := app.Logger
func (app *Application) initializePendingTasks(ctx context.Context) error {
appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializePendingTasks")
planRepo := app.Infra.repos.planRepo
pendingTaskRepo := app.Infra.repos.pendingTaskRepo
executionLogRepo := app.Infra.repos.executionLogRepo
@@ -147,7 +152,7 @@ func (app *Application) initializePendingTasks() error {
// 阶段一:修正因崩溃导致状态不一致的固定次数计划
logger.Info("阶段一:开始修正因崩溃导致状态不一致的固定次数计划...")
plansToCorrect, err := planRepo.FindPlansWithPendingTasks()
plansToCorrect, err := planRepo.FindPlansWithPendingTasks(appCtx)
if err != nil {
return fmt.Errorf("查找需要修正的计划失败: %w", err)
}
@@ -167,7 +172,7 @@ func (app *Application) initializePendingTasks() error {
}
// 保存更新后的计划
if err := planRepo.UpdatePlan(plan); err != nil {
if err := planRepo.UpdatePlan(appCtx, plan); err != nil {
logger.Errorf("修正计划 #%d 状态失败: %v", plan.ID, err)
// 这是一个非阻塞性错误,继续处理其他计划
}
@@ -179,7 +184,7 @@ func (app *Application) initializePendingTasks() error {
// --- 新增逻辑:处理因崩溃导致状态不一致的计划主表状态 ---
// 1. 查找所有未完成的计划执行日志 (状态为 Started 或 Waiting)
incompletePlanLogs, err := executionLogRepo.FindIncompletePlanExecutionLogs()
incompletePlanLogs, err := executionLogRepo.FindIncompletePlanExecutionLogs(appCtx)
if err != nil {
return fmt.Errorf("查找未完成的计划执行日志失败: %w", err)
}
@@ -193,7 +198,7 @@ func (app *Application) initializePendingTasks() error {
// 3. 对于每个受影响的 PlanID重置其 execute_count 并将其状态设置为 Failed, 系统计划不受此影响
for planID := range affectedPlanIDs {
// 首先,获取计划的详细信息以判断其类型
plan, err := planRepo.GetBasicPlanByID(planID)
plan, err := planRepo.GetBasicPlanByID(appCtx, planID)
if err != nil {
logger.Errorf("在尝试修正计划状态时,获取计划 #%d 的基本信息失败: %v", planID, err)
continue // 获取失败,跳过此计划
@@ -208,7 +213,7 @@ func (app *Application) initializePendingTasks() error {
// 对于非系统计划,执行原有的失败标记逻辑
logger.Warnf("检测到计划 #%d 在应用崩溃前处于未完成状态,将重置其计数并标记为失败。", planID)
// 使用 UpdatePlanStateAfterExecution 来更新主表状态,避免影响关联数据
if err := planRepo.UpdatePlanStateAfterExecution(planID, 0, models.PlanStatusFailed); err != nil {
if err := planRepo.UpdatePlanStateAfterExecution(appCtx, planID, 0, models.PlanStatusFailed); err != nil {
logger.Errorf("重置计划 #%d 计数并标记为失败时出错: %v", planID, err)
// 这是一个非阻塞性错误,继续处理其他计划
}
@@ -216,26 +221,26 @@ func (app *Application) initializePendingTasks() error {
logger.Info("阶段二:计划主表状态修正完成。")
// 直接调用新的方法来更新计划执行日志状态为失败
if err := executionLogRepo.FailAllIncompletePlanExecutionLogs(); err != nil {
if err := executionLogRepo.FailAllIncompletePlanExecutionLogs(appCtx); err != nil {
logger.Errorf("更新所有未完成计划执行日志状态为失败失败: %v", err)
// 这是一个非阻塞性错误,继续执行
}
// 直接调用新的方法来更新任务执行日志状态为取消
if err := executionLogRepo.CancelAllIncompleteTaskExecutionLogs(); err != nil {
if err := executionLogRepo.CancelAllIncompleteTaskExecutionLogs(appCtx); err != nil {
logger.Errorf("更新所有未完成任务执行日志状态为取消失败: %v", err)
// 这是一个非阻塞性错误,继续执行
}
// 清空待执行列表
if err := pendingTaskRepo.ClearAllPendingTasks(); err != nil {
if err := pendingTaskRepo.ClearAllPendingTasks(appCtx); err != nil {
return fmt.Errorf("清空待执行任务列表失败: %w", err)
}
logger.Info("阶段二:待执行任务和相关日志清理完成。")
// 阶段三:初始刷新
logger.Info("阶段三:开始刷新待执行列表...")
if err := planService.RefreshPlanTriggers(); err != nil {
if err := planService.RefreshPlanTriggers(appCtx); err != nil {
return fmt.Errorf("刷新待执行任务列表失败: %w", err)
}
logger.Info("阶段三:待执行任务列表初始化完成。")

View File

@@ -1,69 +0,0 @@
// Package audit 提供了用户操作审计相关的功能
package audit
import (
"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"
// 移除对 "github.com/gin-gonic/gin" 的直接依赖
)
// RequestContext 封装了审计日志所需的请求上下文信息
type RequestContext struct {
ClientIP string
HTTPPath string
HTTPMethod string
}
// Service 定义了审计服务的接口
type Service interface {
LogAction(user *models.User, reqCtx RequestContext, actionType, description string, targetResource interface{}, status models.AuditStatus, resultDetails string)
}
// service 是 Service 接口的实现
type service struct {
userActionLogRepository repository.UserActionLogRepository
logger *logs.Logger
}
// NewService 创建一个新的审计服务实例
func NewService(repo repository.UserActionLogRepository, logger *logs.Logger) Service {
return &service{userActionLogRepository: repo, logger: logger}
}
// LogAction 记录一个用户操作。它在一个新的 goroutine 中异步执行,以避免阻塞主请求。
func (s *service) LogAction(user *models.User, reqCtx RequestContext, actionType, description string, targetResource interface{}, status models.AuditStatus, resultDetails string) {
// 不再从 context 中获取用户信息,直接使用传入的 user 对象
if user == nil {
s.logger.Warnw("无法记录审计日志:传入的用户对象为 nil")
return
}
log := &models.UserActionLog{
Time: time.Now(),
UserID: user.ID,
Username: user.Username, // 用户名快照
SourceIP: reqCtx.ClientIP,
ActionType: actionType,
Description: description,
Status: status,
HTTPPath: reqCtx.HTTPPath,
HTTPMethod: reqCtx.HTTPMethod,
ResultDetails: resultDetails,
}
// 使用模型提供的方法来设置 TargetResource
if err := log.SetTargetResource(targetResource); err != nil {
s.logger.Errorw("无法记录审计日志:序列化 targetResource 失败", "error", err)
// 即使序列化失败,我们可能仍然希望记录操作本身,所以不在此处 return
}
// 异步写入数据库,不阻塞当前请求
go func() {
if err := s.userActionLogRepository.Create(log); err != nil {
s.logger.Errorw("异步保存审计日志失败", "error", err)
}
}()
}

View File

@@ -1,6 +1,10 @@
package device
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// 设备行为
type DeviceAction string
@@ -21,10 +25,10 @@ var (
type Service interface {
// Switch 用于切换指定设备的状态, 比如启动和停止
Switch(device *models.Device, action DeviceAction) error
Switch(ctx context.Context, device *models.Device, action DeviceAction) error
// Collect 用于发起对指定区域主控下的多个设备的批量采集请求。
Collect(regionalControllerID uint, devicesToCollect []*models.Device) error
Collect(ctx context.Context, regionalControllerID uint, devicesToCollect []*models.Device) error
}
// 设备操作指令通用结构(最外层)

View File

@@ -1,6 +1,7 @@
package device
import (
"context"
"errors"
"fmt"
"time"
@@ -17,31 +18,32 @@ import (
)
type GeneralDeviceService struct {
ctx context.Context
deviceRepo repository.DeviceRepository
deviceCommandLogRepo repository.DeviceCommandLogRepository
pendingCollectionRepo repository.PendingCollectionRepository
logger *logs.Logger
comm transport.Communicator
}
// NewGeneralDeviceService 创建一个通用设备服务
func NewGeneralDeviceService(
ctx context.Context,
deviceRepo repository.DeviceRepository,
deviceCommandLogRepo repository.DeviceCommandLogRepository,
pendingCollectionRepo repository.PendingCollectionRepository,
logger *logs.Logger,
comm transport.Communicator,
) Service {
return &GeneralDeviceService{
ctx: ctx,
deviceRepo: deviceRepo,
deviceCommandLogRepo: deviceCommandLogRepo,
pendingCollectionRepo: pendingCollectionRepo,
logger: logger,
comm: comm,
}
}
func (g *GeneralDeviceService) Switch(device *models.Device, action DeviceAction) error {
func (g *GeneralDeviceService) Switch(ctx context.Context, device *models.Device, action DeviceAction) error {
serviceCtx, logger := logs.Trace(ctx, g.ctx, "Switch")
// 1. 依赖模型自身的 SelfCheck 进行全面校验
if err := device.SelfCheck(); err != nil {
return fmt.Errorf("设备 %v(id=%v) 未通过自检: %w", device.Name, device.ID, err)
@@ -102,7 +104,7 @@ func (g *GeneralDeviceService) Switch(device *models.Device, action DeviceAction
// 7. 发送指令
networkID := areaController.NetworkID
sendResult, err := g.comm.Send(networkID, message)
sendResult, err := g.comm.Send(serviceCtx, networkID, message)
if err != nil {
return fmt.Errorf("发送指令到 %s 失败: %w", networkID, err)
}
@@ -120,20 +122,21 @@ func (g *GeneralDeviceService) Switch(device *models.Device, action DeviceAction
logRecord.ReceivedSuccess = *sendResult.ReceivedSuccess
}
if err := g.deviceCommandLogRepo.Create(logRecord); err != nil {
if err := g.deviceCommandLogRepo.Create(serviceCtx, logRecord); err != nil {
// 记录日志失败是一个需要关注的问题,但可能不应该中断主流程。
// 我们记录一个错误日志,然后成功返回。
g.logger.Errorf("创建指令日志失败 (MessageID: %s): %v", sendResult.MessageID, err)
logger.Errorf("创建指令日志失败 (MessageID: %s): %v", sendResult.MessageID, err)
}
g.logger.Infof("成功发送指令到 %s 并创建日志 (MessageID: %s)", networkID, sendResult.MessageID)
logger.Infof("成功发送指令到 %s 并创建日志 (MessageID: %s)", networkID, sendResult.MessageID)
return nil
}
// Collect 实现了 Service 接口,用于发起对指定区域主控下的多个设备的批量采集请求。
func (g *GeneralDeviceService) Collect(regionalControllerID uint, devicesToCollect []*models.Device) error {
func (g *GeneralDeviceService) Collect(ctx context.Context, regionalControllerID uint, devicesToCollect []*models.Device) error {
serviceCtx, logger := logs.Trace(ctx, g.ctx, "Collect")
if len(devicesToCollect) == 0 {
g.logger.Info("待采集设备列表为空,无需执行采集任务。")
logger.Info("待采集设备列表为空,无需执行采集任务。")
return nil
}
@@ -153,25 +156,25 @@ func (g *GeneralDeviceService) Collect(regionalControllerID uint, devicesToColle
for _, dev := range devicesToCollect {
// 依赖模型自身的 SelfCheck 进行全面校验
if err := dev.SelfCheck(); err != nil {
g.logger.Warnf("跳过设备 %d因其未通过自检: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其未通过自检: %v", dev.ID, err)
continue
}
if err := dev.DeviceTemplate.SelfCheck(); err != nil {
g.logger.Warnf("跳过设备 %d因其设备模板未通过自检: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其设备模板未通过自检: %v", dev.ID, err)
continue
}
// 使用模板的 ParseCommands 方法获取传感器指令参数
var sensorCmd models.SensorCommands
if err := dev.DeviceTemplate.ParseCommands(&sensorCmd); err != nil {
g.logger.Warnf("跳过设备 %d因其模板指令无法解析为 SensorCommands: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其模板指令无法解析为 SensorCommands: %v", dev.ID, err)
continue
}
// 使用模型层预定义的 Bus485Properties 结构体解析设备属性
var deviceProps models.Bus485Properties
if err := dev.ParseProperties(&deviceProps); err != nil {
g.logger.Warnf("跳过设备 %d因其属性解析失败: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其属性解析失败: %v", dev.ID, err)
continue
}
@@ -183,10 +186,10 @@ func (g *GeneralDeviceService) Collect(regionalControllerID uint, devicesToColle
sensorCmd.ModbusQuantity,
)
if err != nil {
g.logger.Warnf("跳过设备 %d因生成Modbus RTU读取指令失败: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因生成Modbus RTU读取指令失败: %v", dev.ID, err)
continue
}
g.logger.Debugf("生成485指令: %v", modbusCommandBytes)
logger.Debugf("生成485指令: %v", modbusCommandBytes)
// 构建 Raw485Command包含总线号
raw485Cmd := &proto.Raw485Command{
@@ -216,11 +219,11 @@ func (g *GeneralDeviceService) Collect(regionalControllerID uint, devicesToColle
Status: models.PendingStatusPending,
CreatedAt: time.Now(),
}
if err := g.pendingCollectionRepo.Create(pendingReq); err != nil {
g.logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err)
if err := g.pendingCollectionRepo.Create(serviceCtx, pendingReq); err != nil {
logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err)
return err
}
g.logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, regionalController.ID)
logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, regionalController.ID)
// 5. 构建最终的空中载荷
batchCmd := &proto.BatchCollectCommand{
@@ -234,15 +237,15 @@ func (g *GeneralDeviceService) Collect(regionalControllerID uint, devicesToColle
}
payload, err := gproto.Marshal(instruction)
if err != nil {
g.logger.Errorf("序列化采集指令失败 (CorrelationID: %s): %v", correlationID, err)
logger.Errorf("序列化采集指令失败 (CorrelationID: %s): %v", correlationID, err)
return err
}
g.logger.Infof("构造空中载荷成功: networkID: %v, payload: %v", networkID, instruction)
if _, err := g.comm.Send(networkID, payload); err != nil {
g.logger.DPanicf("待采集请求 (CorrelationID: %s) 已创建,但发送到设备失败: %v。数据可能不一致", correlationID, err)
logger.Infof("构造空中载荷成功: networkID: %v, payload: %v", networkID, instruction)
if _, err := g.comm.Send(serviceCtx, networkID, payload); err != nil {
logger.DPanicf("待采集请求 (CorrelationID: %s) 已创建,但发送到设备失败: %v。数据可能不一致", correlationID, err)
return err
}
g.logger.Infof("成功将采集请求 (CorrelationID: %s) 发送到设备 %s", correlationID, networkID)
logger.Infof("成功将采集请求 (CorrelationID: %s) 发送到设备 %s", correlationID, networkID)
return nil
}

View File

@@ -1,6 +1,7 @@
package notify
import (
"context"
"fmt"
"strings"
"sync"
@@ -10,24 +11,25 @@ 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(userIDs []uint, content notify.AlarmContent) error
SendBatchAlarm(ctx context.Context, userIDs []uint, content notify.AlarmContent) error
// BroadcastAlarm 向所有用户发送告警通知。它会并发地为每个用户执行带故障转移的发送逻辑。
BroadcastAlarm(content notify.AlarmContent) error
BroadcastAlarm(ctx context.Context, content notify.AlarmContent) error
// SendTestMessage 向指定用户发送一条测试消息,用于手动验证特定通知渠道的配置。
SendTestMessage(userID uint, notifierType notify.NotifierType) error
SendTestMessage(ctx context.Context, userID uint, notifierType notify.NotifierType) error
}
// failoverService 是 Service 接口的实现,提供了故障转移功能
type failoverService struct {
log *logs.Logger
ctx context.Context
userRepo repository.UserRepository
notifiers map[notify.NotifierType]notify.Notifier
primaryNotifier notify.Notifier
@@ -38,7 +40,7 @@ type failoverService struct {
// NewFailoverService 创建一个新的故障转移通知服务
func NewFailoverService(
log *logs.Logger,
ctx context.Context,
userRepo repository.UserRepository,
notifiers []notify.Notifier,
primaryNotifierType notify.NotifierType,
@@ -56,7 +58,7 @@ func NewFailoverService(
}
return &failoverService{
log: log,
ctx: ctx,
userRepo: userRepo,
notifiers: notifierMap,
primaryNotifier: primaryNotifier,
@@ -67,18 +69,19 @@ func NewFailoverService(
}
// SendBatchAlarm 实现了向多个用户并发发送告警的功能
func (s *failoverService) SendBatchAlarm(userIDs []uint, content notify.AlarmContent) error {
func (s *failoverService) SendBatchAlarm(ctx context.Context, userIDs []uint, content notify.AlarmContent) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendBatchAlarm")
var wg sync.WaitGroup
var mu sync.Mutex
var allErrors []string
s.log.Infow("开始批量发送告警...", "userCount", len(userIDs))
logger.Infow("开始批量发送告警...", "userCount", len(userIDs))
for _, userID := range userIDs {
wg.Add(1)
go func(id uint) {
defer wg.Done()
if err := s.sendAlarmToUser(id, content); err != nil {
if err := s.sendAlarmToUser(serviceCtx, id, content); err != nil {
mu.Lock()
allErrors = append(allErrors, fmt.Sprintf("发送失败 (用户ID: %d): %v", id, err))
mu.Unlock()
@@ -90,19 +93,20 @@ func (s *failoverService) SendBatchAlarm(userIDs []uint, content notify.AlarmCon
if len(allErrors) > 0 {
finalError := fmt.Errorf("批量告警发送完成,但有 %d 个用户发送失败:\n%s", len(allErrors), strings.Join(allErrors, "\n"))
s.log.Error(finalError.Error())
logger.Error(finalError.Error())
return finalError
}
s.log.Info("批量发送告警成功完成,所有用户均已通知。")
logger.Info("批量发送告警成功完成,所有用户均已通知。")
return nil
}
// BroadcastAlarm 实现了向所有用户发送告警的功能
func (s *failoverService) BroadcastAlarm(content notify.AlarmContent) error {
users, err := s.userRepo.FindAll()
func (s *failoverService) BroadcastAlarm(ctx context.Context, content notify.AlarmContent) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "BroadcastAlarm")
users, err := s.userRepo.FindAll(serviceCtx)
if err != nil {
s.log.Errorw("广播告警失败:查找所有用户时出错", "error", err)
logger.Errorw("广播告警失败:查找所有用户时出错", "error", err)
return fmt.Errorf("广播告警失败:查找所有用户时出错: %w", err)
}
@@ -111,16 +115,17 @@ func (s *failoverService) BroadcastAlarm(content notify.AlarmContent) error {
userIDs = append(userIDs, user.ID)
}
s.log.Infow("开始广播告警给所有用户", "totalUsers", len(userIDs))
logger.Infow("开始广播告警给所有用户", "totalUsers", len(userIDs))
// 复用 SendBatchAlarm 的逻辑进行并发发送和错误处理
return s.SendBatchAlarm(userIDs, content)
return s.SendBatchAlarm(serviceCtx, userIDs, content)
}
// sendAlarmToUser 是为单个用户发送告警的内部方法,包含了完整的故障转移逻辑
func (s *failoverService) sendAlarmToUser(userID uint, content notify.AlarmContent) error {
user, err := s.userRepo.FindByID(userID)
func (s *failoverService) sendAlarmToUser(ctx context.Context, userID uint, content notify.AlarmContent) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "sendAlarmToUser")
user, err := s.userRepo.FindByID(serviceCtx, userID)
if err != nil {
s.log.Errorw("发送告警失败:查找用户时出错", "userID", userID, "error", err)
logger.Errorw("发送告警失败:查找用户时出错", "userID", userID, "error", err)
return fmt.Errorf("查找用户失败: %w", err)
}
@@ -132,50 +137,50 @@ func (s *failoverService) sendAlarmToUser(userID uint, content notify.AlarmConte
addr := getAddressForNotifier(primaryType, user.Contact)
if addr == "" {
// 记录跳过通知
s.recordNotificationAttempt(userID, primaryType, content, "", models.NotificationStatusSkipped, fmt.Errorf("用户未配置首选通知方式 '%s' 的地址", primaryType))
s.recordNotificationAttempt(serviceCtx, userID, primaryType, content, "", models.NotificationStatusSkipped, fmt.Errorf("用户未配置首选通知方式 '%s' 的地址", primaryType))
return fmt.Errorf("用户未配置首选通知方式 '%s' 的地址", primaryType)
}
err = s.primaryNotifier.Send(content, addr)
err = s.primaryNotifier.Send(serviceCtx, content, addr)
if err == nil {
// 记录成功通知
s.recordNotificationAttempt(userID, primaryType, content, addr, models.NotificationStatusSuccess, nil)
s.recordNotificationAttempt(serviceCtx, userID, primaryType, content, addr, models.NotificationStatusSuccess, nil)
if failureCount > 0 {
s.log.Infow("首选渠道发送恢复正常", "userID", userID, "notifierType", primaryType)
logger.Infow("首选渠道发送恢复正常", "userID", userID, "notifierType", primaryType)
s.failureCounters.Store(userID, 0)
}
return nil
}
// 记录失败通知
s.recordNotificationAttempt(userID, primaryType, content, addr, models.NotificationStatusFailed, err)
s.recordNotificationAttempt(serviceCtx, userID, primaryType, content, addr, models.NotificationStatusFailed, err)
newFailureCount := failureCount + 1
s.failureCounters.Store(userID, newFailureCount)
s.log.Warnw("首选渠道发送失败", "userID", userID, "notifierType", primaryType, "error", err, "failureCount", newFailureCount)
logger.Warnw("首选渠道发送失败", "userID", userID, "notifierType", primaryType, "error", err, "failureCount", newFailureCount)
failureCount = newFailureCount
}
if failureCount >= s.failureThreshold {
s.log.Warnw("故障转移阈值已达到,开始广播通知", "userID", userID, "threshold", s.failureThreshold)
logger.Warnw("故障转移阈值已达到,开始广播通知", "userID", userID, "threshold", s.failureThreshold)
var lastErr error
for _, notifier := range s.notifiers {
addr := getAddressForNotifier(notifier.Type(), user.Contact)
if addr == "" {
// 记录跳过通知
s.recordNotificationAttempt(userID, notifier.Type(), content, "", models.NotificationStatusSkipped, fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifier.Type()))
s.recordNotificationAttempt(serviceCtx, userID, notifier.Type(), content, "", models.NotificationStatusSkipped, fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifier.Type()))
continue
}
if err := notifier.Send(content, addr); err == nil {
if err := notifier.Send(serviceCtx, content, addr); err == nil {
// 记录成功通知
s.recordNotificationAttempt(userID, notifier.Type(), content, addr, models.NotificationStatusSuccess, nil)
s.log.Infow("广播通知成功", "userID", userID, "notifierType", notifier.Type())
s.recordNotificationAttempt(serviceCtx, userID, notifier.Type(), content, addr, models.NotificationStatusSuccess, nil)
logger.Infow("广播通知成功", "userID", userID, "notifierType", notifier.Type())
s.failureCounters.Store(userID, 0)
return nil
}
// 记录失败通知
s.recordNotificationAttempt(userID, notifier.Type(), content, addr, models.NotificationStatusFailed, err)
s.recordNotificationAttempt(serviceCtx, userID, notifier.Type(), content, addr, models.NotificationStatusFailed, err)
lastErr = err
s.log.Warnw("广播通知:渠道发送失败", "userID", userID, "notifierType", notifier.Type(), "error", err)
logger.Warnw("广播通知:渠道发送失败", "userID", userID, "notifierType", notifier.Type(), "error", err)
}
return fmt.Errorf("所有渠道均发送失败,最后一个错误: %w", lastErr)
}
@@ -184,24 +189,25 @@ func (s *failoverService) sendAlarmToUser(userID uint, content notify.AlarmConte
}
// SendTestMessage 实现了手动发送测试消息的功能
func (s *failoverService) SendTestMessage(userID uint, notifierType notify.NotifierType) error {
user, err := s.userRepo.FindByID(userID)
func (s *failoverService) SendTestMessage(ctx context.Context, userID uint, notifierType notify.NotifierType) error {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "SendTestMessage")
user, err := s.userRepo.FindByID(serviceCtx, userID)
if err != nil {
s.log.Errorw("发送测试消息失败:查找用户时出错", "userID", userID, "error", err)
logger.Errorw("发送测试消息失败:查找用户时出错", "userID", userID, "error", err)
return fmt.Errorf("查找用户失败: %w", err)
}
notifier, ok := s.notifiers[notifierType]
if !ok {
s.log.Errorw("发送测试消息失败:通知器类型不存在", "userID", userID, "notifierType", notifierType)
logger.Errorw("发送测试消息失败:通知器类型不存在", "userID", userID, "notifierType", notifierType)
return fmt.Errorf("指定的通知器类型 '%s' 不存在", notifierType)
}
addr := getAddressForNotifier(notifierType, user.Contact)
if addr == "" {
s.log.Warnw("发送测试消息失败:缺少地址", "userID", userID, "notifierType", notifierType)
logger.Warnw("发送测试消息失败:缺少地址", "userID", userID, "notifierType", notifierType)
// 记录跳过通知
s.recordNotificationAttempt(userID, notifierType, notify.AlarmContent{
s.recordNotificationAttempt(serviceCtx, userID, notifierType, notify.AlarmContent{
Title: "通知服务测试",
Message: fmt.Sprintf("这是一条来自【%s】渠道的测试消息。如果您收到此消息说明您的配置正确。", notifierType),
Level: zap.InfoLevel,
@@ -217,18 +223,18 @@ func (s *failoverService) SendTestMessage(userID uint, notifierType notify.Notif
Timestamp: time.Now(),
}
s.log.Infow("正在发送测试消息...", "userID", userID, "notifierType", notifierType, "address", addr)
err = notifier.Send(testContent, addr)
logger.Infow("正在发送测试消息...", "userID", userID, "notifierType", notifierType, "address", addr)
err = notifier.Send(serviceCtx, testContent, addr)
if err != nil {
s.log.Errorw("发送测试消息失败", "userID", userID, "notifierType", notifierType, "error", err)
logger.Errorw("发送测试消息失败", "userID", userID, "notifierType", notifierType, "error", err)
// 记录失败通知
s.recordNotificationAttempt(userID, notifierType, testContent, addr, models.NotificationStatusFailed, err)
s.recordNotificationAttempt(serviceCtx, userID, notifierType, testContent, addr, models.NotificationStatusFailed, err)
return err
}
s.log.Infow("发送测试消息成功", "userID", userID, "notifierType", notifierType)
logger.Infow("发送测试消息成功", "userID", userID, "notifierType", notifierType)
// 记录成功通知
s.recordNotificationAttempt(userID, notifierType, testContent, addr, models.NotificationStatusSuccess, nil)
s.recordNotificationAttempt(serviceCtx, userID, notifierType, testContent, addr, models.NotificationStatusSuccess, nil)
return nil
}
@@ -256,6 +262,7 @@ func getAddressForNotifier(notifierType notify.NotifierType, contact models.Cont
// status: 发送尝试的状态 (成功、失败、跳过)
// err: 如果发送失败,记录的错误信息
func (s *failoverService) recordNotificationAttempt(
ctx context.Context,
userID uint,
notifierType notify.NotifierType,
content notify.AlarmContent,
@@ -263,6 +270,7 @@ func (s *failoverService) recordNotificationAttempt(
status models.NotificationStatus,
err error,
) {
serviceCtx, logger := logs.Trace(ctx, s.ctx, "recordNotificationAttempt")
errorMessage := ""
if err != nil {
errorMessage = err.Error()
@@ -280,8 +288,8 @@ func (s *failoverService) recordNotificationAttempt(
ErrorMessage: errorMessage,
}
if saveErr := s.notificationRepo.Create(notification); saveErr != nil {
s.log.Errorw("无法保存通知发送记录到数据库",
if saveErr := s.notificationRepo.Create(serviceCtx, notification); saveErr != nil {
logger.Errorw("无法保存通知发送记录到数据库",
"userID", userID,
"notifierType", notifierType,
"status", status,

View File

@@ -1,11 +1,14 @@
package pig
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"gorm.io/gorm"
)
@@ -13,38 +16,40 @@ import (
// 它是一个内部服务,被主服务 PigBatchService 调用。
type PigPenTransferManager interface {
// LogTransfer 在数据库中创建一条猪只迁移日志。
LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error
LogTransfer(ctx context.Context, tx *gorm.DB, log *models.PigTransferLog) error
// GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。
GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error)
GetPenByID(ctx context.Context, tx *gorm.DB, penID uint) (*models.Pen, error)
// GetPensByBatchID 获取一个猪群当前关联的所有猪栏。
GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error)
GetPensByBatchID(ctx context.Context, tx *gorm.DB, batchID uint) ([]*models.Pen, error)
// UpdatePenFields 更新一个猪栏的指定字段。
UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error
UpdatePenFields(ctx context.Context, tx *gorm.DB, penID uint, updates map[string]interface{}) error
// GetCurrentPigsInPen 通过汇总猪只迁移日志,计算给定猪栏中的当前猪只数量。
GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error)
GetCurrentPigsInPen(ctx context.Context, tx *gorm.DB, penID uint) (int, error)
// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error)
GetTotalPigsInPensForBatchTx(ctx context.Context, tx *gorm.DB, batchID uint) (int, error)
// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。
ReleasePen(tx *gorm.DB, penID uint) error
ReleasePen(ctx context.Context, tx *gorm.DB, penID uint) error
}
// pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。
// 它作为调栏管理器,处理底层的数据库交互。
type pigPenTransferManager struct {
ctx context.Context
penRepo repository.PigPenRepository
logRepo repository.PigTransferLogRepository
pigBatchRepo repository.PigBatchRepository
}
// NewPigPenTransferManager 是 pigPenTransferManager 的构造函数。
func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository, pigBatchRepo repository.PigBatchRepository) PigPenTransferManager {
func NewPigPenTransferManager(ctx context.Context, penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository, pigBatchRepo repository.PigBatchRepository) PigPenTransferManager {
return &pigPenTransferManager{
ctx: ctx,
penRepo: penRepo,
logRepo: logRepo,
pigBatchRepo: pigBatchRepo,
@@ -52,29 +57,34 @@ func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repos
}
// LogTransfer 实现了在数据库中创建迁移日志的逻辑。
func (s *pigPenTransferManager) LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error {
return s.logRepo.CreatePigTransferLog(tx, log)
func (s *pigPenTransferManager) LogTransfer(ctx context.Context, tx *gorm.DB, log *models.PigTransferLog) error {
managerCtx := logs.AddFuncName(ctx, s.ctx, "LogTransfer")
return s.logRepo.CreatePigTransferLog(managerCtx, tx, log)
}
// GetPenByID 实现了获取猪栏信息的逻辑。
func (s *pigPenTransferManager) GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) {
return s.penRepo.GetPenByIDTx(tx, penID)
func (s *pigPenTransferManager) GetPenByID(ctx context.Context, tx *gorm.DB, penID uint) (*models.Pen, error) {
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetPenByID")
return s.penRepo.GetPenByIDTx(managerCtx, tx, penID)
}
// GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。
func (s *pigPenTransferManager) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
return s.penRepo.GetPensByBatchIDTx(tx, batchID)
func (s *pigPenTransferManager) GetPensByBatchID(ctx context.Context, tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetPensByBatchID")
return s.penRepo.GetPensByBatchIDTx(managerCtx, tx, batchID)
}
// UpdatePenFields 实现了更新猪栏字段的逻辑。
func (s *pigPenTransferManager) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error {
return s.penRepo.UpdatePenFieldsTx(tx, penID, updates)
func (s *pigPenTransferManager) UpdatePenFields(ctx context.Context, tx *gorm.DB, penID uint, 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(tx *gorm.DB, penID uint) (int, error) {
func (s *pigPenTransferManager) GetCurrentPigsInPen(ctx context.Context, tx *gorm.DB, penID uint) (int, error) {
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigsInPen")
// 1. 通过猪栏ID查出所属猪群信息
pen, err := s.penRepo.GetPenByIDTx(tx, penID)
pen, err := s.penRepo.GetPenByIDTx(managerCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, ErrPenNotFound
@@ -89,7 +99,7 @@ func (s *pigPenTransferManager) GetCurrentPigsInPen(tx *gorm.DB, penID uint) (in
currentBatchID := *pen.PigBatchID
// 2. 根据猪群ID获取猪群的起始日期
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, currentBatchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(managerCtx, tx, currentBatchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, ErrPigBatchNotFound
@@ -99,7 +109,7 @@ func (s *pigPenTransferManager) GetCurrentPigsInPen(tx *gorm.DB, penID uint) (in
batchStartDate := batch.StartDate
// 3. 调用仓库方法,获取从猪群开始至今,该猪栏的所有倒序日志
logs, err := s.logRepo.GetLogsForPenSince(tx, penID, batchStartDate)
logs, err := s.logRepo.GetLogsForPenSince(managerCtx, tx, penID, batchStartDate)
if err != nil {
return 0, err
}
@@ -127,9 +137,10 @@ func (s *pigPenTransferManager) GetCurrentPigsInPen(tx *gorm.DB, penID uint) (in
// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数
// 该方法通过遍历猪群下的每个猪栏,并调用 GetCurrentPigsInPen 来累加存栏数。
func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error) {
func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(ctx context.Context, tx *gorm.DB, batchID uint) (int, error) {
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetTotalPigsInPensForBatchTx")
// 1. 获取该批次下所有猪栏的列表
pensInBatch, err := s.GetPensByBatchID(tx, batchID)
pensInBatch, err := s.GetPensByBatchID(managerCtx, tx, batchID)
if err != nil {
return 0, fmt.Errorf("获取猪群 %d 下属猪栏失败: %w", batchID, err)
}
@@ -137,7 +148,7 @@ func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchI
totalPigs := 0
// 2. 遍历每个猪栏,累加其存栏数
for _, pen := range pensInBatch {
pigsInPen, err := s.GetCurrentPigsInPen(tx, pen.ID)
pigsInPen, err := s.GetCurrentPigsInPen(managerCtx, tx, pen.ID)
if err != nil {
return 0, fmt.Errorf("获取猪栏 %d 存栏数失败: %w", pen.ID, err)
}
@@ -149,9 +160,10 @@ func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchI
// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。
// 此操作通常在猪栏被清空后调用。
func (s *pigPenTransferManager) ReleasePen(tx *gorm.DB, penID uint) error {
func (s *pigPenTransferManager) ReleasePen(ctx context.Context, tx *gorm.DB, penID uint) error {
managerCtx := logs.AddFuncName(ctx, s.ctx, "ReleasePen")
// 1. 获取猪栏信息
pen, err := s.penRepo.GetPenByIDTx(tx, penID)
pen, err := s.penRepo.GetPenByIDTx(managerCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
@@ -167,7 +179,7 @@ func (s *pigPenTransferManager) ReleasePen(tx *gorm.DB, penID uint) error {
"status": models.PenStatusEmpty,
}
if err := s.penRepo.UpdatePenFieldsTx(tx, penID, updates); err != nil {
if err := s.penRepo.UpdatePenFieldsTx(managerCtx, tx, penID, updates); err != nil {
return fmt.Errorf("释放猪栏 %v 失败: %w", pen.PenNumber, err)
}

View File

@@ -1,6 +1,7 @@
package pig
import (
"context"
"errors"
"time"
@@ -37,63 +38,64 @@ var (
// 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。
type PigBatchService interface {
// CreatePigBatch 创建猪批次,并记录初始日志。
CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error)
CreatePigBatch(ctx context.Context, operatorID uint, batch *models.PigBatch) (*models.PigBatch, error)
// GetPigBatch 获取单个猪批次。
GetPigBatch(id uint) (*models.PigBatch, error)
GetPigBatch(ctx context.Context, id uint) (*models.PigBatch, error)
// UpdatePigBatch 更新猪批次信息。
UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error)
UpdatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, error)
// DeletePigBatch 删除猪批次,包含业务规则校验。
DeletePigBatch(id uint) error
DeletePigBatch(ctx context.Context, id uint) error
// ListPigBatches 批量查询猪批次。
ListPigBatches(isActive *bool) ([]*models.PigBatch, error)
ListPigBatches(ctx context.Context, isActive *bool) ([]*models.PigBatch, error)
// AssignEmptyPensToBatch 为猪群分配空栏
AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error
AssignEmptyPensToBatch(ctx context.Context, batchID uint, penIDs []uint, operatorID uint) error
// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏
MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
MovePigsIntoPen(ctx context.Context, batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error
// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群
ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error
// RemoveEmptyPenFromBatch 将一个猪栏移除出猪群,此方法需要在猪栏为空的情况下执行。
RemoveEmptyPenFromBatch(batchID uint, penID uint) error
RemoveEmptyPenFromBatch(ctx context.Context, batchID uint, penID uint) error
// GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。
GetCurrentPigQuantity(batchID uint) (int, error)
GetCurrentPigQuantity(ctx context.Context, batchID uint) (int, error)
// GetCurrentPigsInPen 获取指定猪栏的当前存栏量。
GetCurrentPigsInPen(penID uint) (int, error)
GetCurrentPigsInPen(ctx context.Context, penID uint) (int, error)
// GetTotalPigsInPensForBatch 获取指定猪群下所有猪栏的当前总存栏数
GetTotalPigsInPensForBatch(batchID uint) (int, error)
GetTotalPigsInPensForBatch(ctx context.Context, batchID uint) (int, error)
UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error
UpdatePigBatchQuantity(ctx context.Context, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error
// ---交易子服务---
// SellPigs 处理卖猪的业务逻辑。
SellPigs(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 uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error
// BuyPigs 处理买猪的业务逻辑。
BuyPigs(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
// ---调栏子服务 ---
TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error
TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) 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
// --- 病猪管理相关方法 ---
// RecordSickPigs 记录新增病猪事件。
RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
RecordSickPigs(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error
// RecordSickPigRecovery 记录病猪康复事件。
RecordSickPigRecovery(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 记录病猪死亡事件。
RecordSickPigDeath(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 记录病猪淘汰事件。
RecordSickPigCull(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
// --- 正常猪只管理相关方法 ---
// RecordDeath 记录正常猪只死亡事件。
RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
RecordDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error
// RecordCull 记录正常猪只淘汰事件。
RecordCull(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
}
// pigBatchService 是 PigBatchService 接口的具体实现。
// 它作为猪群领域的主服务,封装了所有业务逻辑。
type pigBatchService struct {
ctx context.Context
pigBatchRepo repository.PigBatchRepository // 猪批次仓库
pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库
uow repository.UnitOfWork // 工作单元,用于管理事务
@@ -105,6 +107,7 @@ type pigBatchService struct {
// NewPigBatchService 是 pigBatchService 的构造函数。
// 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。
func NewPigBatchService(
ctx context.Context,
pigBatchRepo repository.PigBatchRepository,
pigBatchLogRepo repository.PigBatchLogRepository,
uow repository.UnitOfWork,
@@ -113,6 +116,7 @@ func NewPigBatchService(
sickSvc SickPigManager,
) PigBatchService {
return &pigBatchService{
ctx: ctx,
pigBatchRepo: pigBatchRepo,
pigBatchLogRepo: pigBatchLogRepo,
uow: uow,

View File

@@ -1,26 +1,30 @@
package pig
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"
)
// --- 领域服务实现 ---
// CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。
func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) {
func (s *pigBatchService) CreatePigBatch(ctx context.Context, operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigBatch")
// 业务规则可以在这里添加,例如检查批次号是否唯一等
var createdBatch *models.PigBatch
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 创建猪批次
// 注意: 此处依赖一个假设存在的 pigBatchRepo.CreatePigBatchTx 方法
var err error
createdBatch, err = s.pigBatchRepo.CreatePigBatchTx(tx, batch)
createdBatch, err = s.pigBatchRepo.CreatePigBatchTx(serviceCtx, tx, batch)
if err != nil {
return fmt.Errorf("创建猪批次失败: %w", err)
}
@@ -38,7 +42,7 @@ func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch
}
// 3. 记录批次日志
if err := s.pigBatchLogRepo.CreateTx(tx, initialLog); err != nil {
if err := s.pigBatchLogRepo.CreateTx(serviceCtx, tx, initialLog); err != nil {
return fmt.Errorf("记录初始批次日志失败: %w", err)
}
@@ -53,8 +57,9 @@ func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch
}
// GetPigBatch 实现了获取单个猪批次的逻辑。
func (s *pigBatchService) GetPigBatch(id uint) (*models.PigBatch, error) {
batch, err := s.pigBatchRepo.GetPigBatchByID(id)
func (s *pigBatchService) GetPigBatch(ctx context.Context, id uint) (*models.PigBatch, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigBatch")
batch, err := s.pigBatchRepo.GetPigBatchByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrPigBatchNotFound
@@ -65,9 +70,10 @@ func (s *pigBatchService) GetPigBatch(id uint) (*models.PigBatch, error) {
}
// UpdatePigBatch 实现了更新猪批次的逻辑。
func (s *pigBatchService) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) {
func (s *pigBatchService) UpdatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigBatch")
// 可以在这里添加更新前的业务校验
updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(batch)
updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(serviceCtx, batch)
if err != nil {
return nil, err
}
@@ -78,10 +84,11 @@ func (s *pigBatchService) UpdatePigBatch(batch *models.PigBatch) (*models.PigBat
}
// DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。
func (s *pigBatchService) DeletePigBatch(id uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
func (s *pigBatchService) DeletePigBatch(ctx context.Context, id uint) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigBatch")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 获取猪批次信息
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, id) // 使用事务内方法
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, id) // 使用事务内方法
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -96,20 +103,20 @@ func (s *pigBatchService) DeletePigBatch(id uint) error {
// 3. 释放所有关联的猪栏
// 获取该批次下所有猪栏
pensInBatch, err := s.transferSvc.GetPensByBatchID(tx, id)
pensInBatch, err := s.transferSvc.GetPensByBatchID(serviceCtx, tx, id)
if err != nil {
return fmt.Errorf("获取猪批次 %d 关联猪栏失败: %w", id, err)
}
// 逐一释放猪栏
for _, pen := range pensInBatch {
if err := s.transferSvc.ReleasePen(tx, pen.ID); err != nil {
if err := s.transferSvc.ReleasePen(serviceCtx, tx, pen.ID); err != nil {
return fmt.Errorf("释放猪栏 %d 失败: %w", pen.ID, err)
}
}
// 4. 执行删除猪批次
rowsAffected, err := s.pigBatchRepo.DeletePigBatchTx(tx, id)
rowsAffected, err := s.pigBatchRepo.DeletePigBatchTx(serviceCtx, tx, id)
if err != nil {
return err
}
@@ -122,16 +129,18 @@ func (s *pigBatchService) DeletePigBatch(id uint) error {
}
// ListPigBatches 实现了批量查询猪批次的逻辑。
func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) {
return s.pigBatchRepo.ListPigBatches(isActive)
func (s *pigBatchService) ListPigBatches(ctx context.Context, isActive *bool) ([]*models.PigBatch, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigBatches")
return s.pigBatchRepo.ListPigBatches(serviceCtx, isActive)
}
// GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。
func (s *pigBatchService) GetCurrentPigQuantity(batchID uint) (int, error) {
func (s *pigBatchService) GetCurrentPigQuantity(ctx context.Context, batchID uint) (int, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigQuantity")
var getErr error
var quantity int
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
quantity, getErr = s.getCurrentPigQuantityTx(tx, batchID)
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
quantity, getErr = s.getCurrentPigQuantityTx(serviceCtx, tx, batchID)
return getErr
})
if err != nil {
@@ -141,9 +150,10 @@ func (s *pigBatchService) GetCurrentPigQuantity(batchID uint) (int, error) {
}
// getCurrentPigQuantityTx 实现了获取指定猪批次的当前猪只数量的逻辑。
func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (int, error) {
func (s *pigBatchService) getCurrentPigQuantityTx(ctx context.Context, tx *gorm.DB, batchID uint) (int, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "getCurrentPigQuantityTx")
// 1. 获取猪批次初始信息
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, ErrPigBatchNotFound
@@ -152,7 +162,7 @@ func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (in
}
// 2. 尝试获取该批次的最后一条日志记录
lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID)
lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 如果没有找到任何日志记录(除了初始创建),则当前数量就是初始数量
@@ -165,14 +175,16 @@ func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (in
return lastLog.AfterCount, nil
}
func (s *pigBatchService) UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
return s.updatePigBatchQuantityTx(tx, operatorID, batchID, changeType, changeAmount, changeReason, happenedAt)
func (s *pigBatchService) UpdatePigBatchQuantity(ctx context.Context, operatorID uint, batchID uint, 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(tx *gorm.DB, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error {
lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID)
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 {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "updatePigBatchQuantityTx")
lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(serviceCtx, tx, batchID)
if err != nil {
return err
}
@@ -192,5 +204,5 @@ func (s *pigBatchService) updatePigBatchQuantityTx(tx *gorm.DB, operatorID uint,
OperatorID: operatorID,
HappenedAt: happenedAt,
}
return s.pigBatchLogRepo.CreateTx(tx, pigBatchLog)
return s.pigBatchLogRepo.CreateTx(serviceCtx, tx, pigBatchLog)
}

View File

@@ -1,20 +1,25 @@
package pig
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"
"github.com/google/uuid"
"gorm.io/gorm"
)
// executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。
func (s *pigBatchService) executeTransferAndLog(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 uint, quantity int, transferType models.PigTransferType, operatorID uint, remarks string) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "executeTransferAndLog")
// 通用校验:任何调出操作都不能超过源猪栏的当前存栏数
if quantity < 0 { // 当调出时才需要检查
currentPigsInFromPen, err := s.transferSvc.GetCurrentPigsInPen(tx, fromPenID)
currentPigsInFromPen, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, fromPenID)
if err != nil {
return fmt.Errorf("获取源猪栏 %d 当前猪只数失败: %w", fromPenID, err)
}
@@ -51,10 +56,10 @@ func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatc
}
// 4. 调用子服务记录日志
if err := s.transferSvc.LogTransfer(tx, logOut); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, logOut); err != nil {
return fmt.Errorf("记录调出日志失败: %w", err)
}
if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, logIn); err != nil {
return fmt.Errorf("记录调入日志失败: %w", err)
}
@@ -62,7 +67,8 @@ func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatc
}
// TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。
func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
func (s *pigBatchService) TransferPigsWithinBatch(ctx context.Context, batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "TransferPigsWithinBatch")
if fromPenID == toPenID {
return errors.New("源猪栏和目标猪栏不能相同")
}
@@ -70,13 +76,13 @@ func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint,
return errors.New("迁移数量不能为零")
}
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 核心业务规则校验
fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID)
fromPen, err := s.transferSvc.GetPenByID(serviceCtx, tx, fromPenID)
if err != nil {
return fmt.Errorf("获取源猪栏信息失败: %w", err)
}
toPen, err := s.transferSvc.GetPenByID(tx, toPenID)
toPen, err := s.transferSvc.GetPenByID(serviceCtx, tx, toPenID)
if err != nil {
return fmt.Errorf("获取目标猪栏信息失败: %w", err)
}
@@ -89,7 +95,7 @@ func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint,
}
// 2. 调用通用辅助方法执行日志记录
err = s.executeTransferAndLog(tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏", operatorID, remarks)
err = s.executeTransferAndLog(serviceCtx, tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏", operatorID, remarks)
if err != nil {
return err
}
@@ -100,7 +106,8 @@ func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint,
}
// TransferPigsAcrossBatches 实现了跨猪群的调栏业务。
func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
func (s *pigBatchService) TransferPigsAcrossBatches(ctx context.Context, sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "TransferPigsAcrossBatches")
if sourceBatchID == destBatchID {
return errors.New("源猪群和目标猪群不能相同")
}
@@ -108,16 +115,16 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc
return errors.New("迁移数量不能为零")
}
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 核心业务规则校验
// 1.1 校验猪群存在
if _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, sourceBatchID); err != nil {
if _, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, sourceBatchID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("源猪群 %d 不存在", sourceBatchID)
}
return fmt.Errorf("获取源猪群信息失败: %w", err)
}
if _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, destBatchID); err != nil {
if _, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, destBatchID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("目标猪群 %d 不存在", destBatchID)
}
@@ -125,7 +132,7 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc
}
// 1.2 校验猪栏归属
fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID)
fromPen, err := s.transferSvc.GetPenByID(serviceCtx, tx, fromPenID)
if err != nil {
return fmt.Errorf("获取源猪栏信息失败: %w", err)
}
@@ -134,7 +141,7 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc
}
// 2. 调用通用辅助方法执行猪只物理转移的日志记录
err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏", operatorID, remarks)
err = s.executeTransferAndLog(serviceCtx, tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏", operatorID, remarks)
if err != nil {
return err
}
@@ -143,14 +150,14 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc
now := time.Now()
// 3.1 记录源猪群数量减少
reasonOut := fmt.Sprintf("跨群调栏: %d头猪从批次 %d 调出至批次 %d。备注: %s", quantity, sourceBatchID, destBatchID, remarks)
err = s.updatePigBatchQuantityTx(tx, operatorID, sourceBatchID, models.ChangeTypeTransferOut, -int(quantity), reasonOut, now)
err = s.updatePigBatchQuantityTx(serviceCtx, tx, operatorID, sourceBatchID, models.ChangeTypeTransferOut, -int(quantity), reasonOut, now)
if err != nil {
return fmt.Errorf("更新源猪群 %d 数量失败: %w", sourceBatchID, err)
}
// 3.2 记录目标猪群数量增加
reasonIn := fmt.Sprintf("跨群调栏: %d头猪从批次 %d 调入。备注: %s", quantity, sourceBatchID, remarks)
err = s.updatePigBatchQuantityTx(tx, operatorID, destBatchID, models.ChangeTypeTransferIn, int(quantity), reasonIn, now)
err = s.updatePigBatchQuantityTx(serviceCtx, tx, operatorID, destBatchID, models.ChangeTypeTransferIn, int(quantity), reasonIn, now)
if err != nil {
return fmt.Errorf("更新目标猪群 %d 数量失败: %w", destBatchID, err)
}
@@ -160,10 +167,11 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc
}
// AssignEmptyPensToBatch 为猪群分配空栏
func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
func (s *pigBatchService) AssignEmptyPensToBatch(ctx context.Context, batchID uint, penIDs []uint, operatorID uint) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "AssignEmptyPensToBatch")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 验证猪批次是否存在且活跃
pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -176,7 +184,7 @@ func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, op
// 2. 遍历并校验每一个待分配的猪栏
for _, penID := range penIDs {
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
@@ -197,7 +205,7 @@ func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, op
"pig_batch_id": &batchID,
"status": models.PenStatusOccupied,
}
if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil {
if err := s.transferSvc.UpdatePenFields(serviceCtx, tx, penID, updates); err != nil {
return fmt.Errorf("分配猪栏 %d 失败: %w", penID, err)
}
}
@@ -207,14 +215,15 @@ func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, op
}
// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏
func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
func (s *pigBatchService) MovePigsIntoPen(ctx context.Context, batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "MovePigsIntoPen")
if quantity <= 0 {
return errors.New("迁移数量必须大于零")
}
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 验证猪批次是否存在且活跃
pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -226,7 +235,7 @@ func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity i
}
// 2. 校验目标猪栏
toPen, err := s.transferSvc.GetPenByID(tx, toPenID)
toPen, err := s.transferSvc.GetPenByID(serviceCtx, tx, toPenID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("目标猪栏 %d 不存在: %w", toPenID, ErrPenNotFound)
@@ -243,13 +252,13 @@ func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity i
}
// 3. 校验猪群中有足够的“未分配”猪只
currentBatchTotal, err := s.getCurrentPigQuantityTx(tx, batchID)
currentBatchTotal, err := s.getCurrentPigQuantityTx(serviceCtx, tx, batchID)
if err != nil {
return fmt.Errorf("获取猪群 %d 当前总数量失败: %w", batchID, err)
}
// 获取该批次下所有猪栏的当前总存栏数
totalPigsInPens, err := s.transferSvc.GetTotalPigsInPensForBatchTx(tx, batchID)
totalPigsInPens, err := s.transferSvc.GetTotalPigsInPensForBatchTx(serviceCtx, tx, batchID)
if err != nil {
return fmt.Errorf("计算猪群 %d 下属猪栏总存栏失败: %w", batchID, err)
}
@@ -269,7 +278,7 @@ func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity i
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, logIn); err != nil {
return fmt.Errorf("记录入栏日志失败: %w", err)
}
@@ -278,22 +287,23 @@ func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity i
}
// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群
func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
func (s *pigBatchService) ReclassifyPenToNewBatch(ctx context.Context, fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ReclassifyPenToNewBatch")
if fromBatchID == toBatchID {
return errors.New("源猪群和目标猪群不能相同")
}
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 核心业务规则校验
// 1.1 校验猪群存在
fromBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, fromBatchID)
fromBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, fromBatchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("源猪群 %d 不存在", fromBatchID)
}
return fmt.Errorf("获取源猪群信息失败: %w", err)
}
toBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, toBatchID)
toBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, toBatchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("目标猪群 %d 不存在", toBatchID)
@@ -302,7 +312,7 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID ui
}
// 1.2 校验猪栏归属
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound)
@@ -314,7 +324,7 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID ui
}
// 2. 获取猪栏当前存栏数
quantity, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
quantity, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %v 存栏数失败: %w", pen.PenNumber, err)
}
@@ -323,7 +333,7 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID ui
updates := map[string]interface{}{
"pig_batch_id": &toBatchID,
}
if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil {
if err := s.transferSvc.UpdatePenFields(serviceCtx, tx, penID, updates); err != nil {
return fmt.Errorf("更新猪栏 %v 归属失败: %w", pen.PenNumber, err)
}
// 如果猪栏是空的,则只进行归属变更,不影响猪群数量
@@ -343,7 +353,7 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID ui
OperatorID: operatorID,
Remarks: fmt.Sprintf("整栏划拨迁出: %d头猪从批次 %v 随猪栏 %v 划拨至批次 %v。备注: %s", quantity, fromBatch.BatchNumber, pen.PenNumber, toBatch.BatchNumber, remarks),
}
if err := s.transferSvc.LogTransfer(tx, logOut); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, logOut); err != nil {
return fmt.Errorf("记录猪栏 %d 迁出日志失败: %w", penID, err)
}
@@ -358,7 +368,7 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID ui
OperatorID: operatorID,
Remarks: fmt.Sprintf("整栏划拨迁入: %v头猪随猪栏 %v 从批次 %v 划拨入。备注: %s", quantity, fromBatch.BatchNumber, pen.PenNumber, remarks),
}
if err := s.transferSvc.LogTransfer(tx, logIn); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, logIn); err != nil {
return fmt.Errorf("记录猪栏 %d 迁入日志失败: %w", penID, err)
}
@@ -366,14 +376,14 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID ui
now := time.Now()
// 7.1 记录源猪群数量减少
reasonOutBatch := fmt.Sprintf("整栏划拨: %d头猪随猪栏 %v 从批次 %v 划拨至批次 %v。备注: %s", quantity, pen.PenNumber, fromBatch.BatchNumber, toBatchID, remarks)
err = s.updatePigBatchQuantityTx(tx, operatorID, fromBatchID, models.ChangeTypeTransferOut, -quantity, reasonOutBatch, now)
err = s.updatePigBatchQuantityTx(serviceCtx, tx, operatorID, fromBatchID, models.ChangeTypeTransferOut, -quantity, reasonOutBatch, now)
if err != nil {
return fmt.Errorf("更新源猪群 %v 数量失败: %w", fromBatch.BatchNumber, err)
}
// 7.2 记录目标猪群数量增加
reasonInBatch := fmt.Sprintf("整栏划拨: %v头猪随猪栏 %v 从批次 %v 划拨入。备注: %s", quantity, pen.PenNumber, fromBatch.BatchNumber, remarks)
err = s.updatePigBatchQuantityTx(tx, operatorID, toBatchID, models.ChangeTypeTransferIn, quantity, reasonInBatch, now)
err = s.updatePigBatchQuantityTx(serviceCtx, tx, operatorID, toBatchID, models.ChangeTypeTransferIn, quantity, reasonInBatch, now)
if err != nil {
return fmt.Errorf("更新目标猪群 %v 数量失败: %w", toBatch.BatchNumber, err)
}
@@ -382,10 +392,11 @@ func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID ui
})
}
func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
func (s *pigBatchService) RemoveEmptyPenFromBatch(ctx context.Context, batchID uint, penID uint) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RemoveEmptyPenFromBatch")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 检查猪批次是否存在且活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -397,7 +408,7 @@ func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) erro
}
// 2. 检查猪栏是否存在
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -411,7 +422,7 @@ func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) erro
}
// 4. 检查猪栏是否为空
pigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
pigsInPen, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return err
}
@@ -420,17 +431,18 @@ func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) erro
}
// 5. 释放猪栏 (将 pig_batch_id 设置为 nil状态设置为空闲)
if err := s.transferSvc.ReleasePen(tx, penID); err != nil {
if err := s.transferSvc.ReleasePen(serviceCtx, tx, penID); err != nil {
return err
}
return nil
})
}
func (s *pigBatchService) GetCurrentPigsInPen(penID uint) (int, error) {
func (s *pigBatchService) GetCurrentPigsInPen(ctx context.Context, penID uint) (int, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentPigsInPen")
var currentPigs int
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
pigs, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
pigs, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return err
}
@@ -441,10 +453,11 @@ func (s *pigBatchService) GetCurrentPigsInPen(penID uint) (int, error) {
}
// GetTotalPigsInPensForBatch 实现了获取指定猪群下所有猪栏的当前总存栏数的逻辑。
func (s *pigBatchService) GetTotalPigsInPensForBatch(batchID uint) (int, error) {
func (s *pigBatchService) GetTotalPigsInPensForBatch(ctx context.Context, batchID uint) (int, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetTotalPigsInPensForBatch")
var totalPigs int
err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
pigs, err := s.transferSvc.GetTotalPigsInPensForBatchTx(tx, batchID)
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
pigs, err := s.transferSvc.GetTotalPigsInPensForBatchTx(serviceCtx, tx, batchID)
if err != nil {
return err
}

View File

@@ -1,25 +1,29 @@
package pig
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"
)
// RecordSickPigs 记录新增病猪事件。
func (s *pigBatchService) RecordSickPigs(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 uint, batchID uint, penID uint, 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")
}
var err error
// 1. 开启事务
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
err = s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1.1 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -31,7 +35,7 @@ func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID ui
}
// 1.2 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -43,12 +47,12 @@ func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID ui
}
// 1.3 检查剩余健康猪不能少于即将转化的病猪数量
totalPigsInBatch, err := s.getCurrentPigQuantityTx(tx, batchID)
totalPigsInBatch, err := s.getCurrentPigQuantityTx(serviceCtx, tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 总猪只数量失败: %w", batchID, err)
}
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(serviceCtx, tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
}
@@ -70,7 +74,7 @@ func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID ui
HappenedAt: happenedAt,
}
if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
if err := s.sickSvc.ProcessSickPigLog(serviceCtx, tx, sickLog); err != nil {
return fmt.Errorf("处理病猪日志失败: %w", err)
}
@@ -85,15 +89,16 @@ func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID ui
}
// RecordSickPigRecovery 记录病猪康复事件。
func (s *pigBatchService) RecordSickPigRecovery(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 uint, batchID uint, penID uint, 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")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
err = s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -105,7 +110,7 @@ func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, p
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -117,7 +122,7 @@ func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, p
}
// 3. 检查当前病猪数量是否足够康复
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(serviceCtx, tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
}
@@ -138,7 +143,7 @@ func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, p
HappenedAt: happenedAt,
}
if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
if err := s.sickSvc.ProcessSickPigLog(serviceCtx, tx, sickLog); err != nil {
return fmt.Errorf("处理病猪康复日志失败: %w", err)
}
@@ -153,15 +158,16 @@ func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, p
}
// RecordSickPigDeath 记录病猪死亡事件。
func (s *pigBatchService) RecordSickPigDeath(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 uint, batchID uint, penID uint, 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")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
err = s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -173,7 +179,7 @@ func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penI
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -185,7 +191,7 @@ func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penI
}
// 3. 检查当前病猪数量是否足够死亡
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(serviceCtx, tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
}
@@ -195,7 +201,7 @@ func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penI
}
// 4. 检查猪栏内猪只数量是否足够死亡
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
}
@@ -214,12 +220,12 @@ func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penI
OperatorID: operatorID,
HappenedAt: happenedAt,
}
if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
if err := s.sickSvc.ProcessSickPigLog(serviceCtx, tx, sickLog); err != nil {
return fmt.Errorf("处理病猪死亡日志失败: %w", err)
}
// 6. 更新批次总猪只数量 (减少批次总数)
if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil {
if err := s.UpdatePigBatchQuantity(serviceCtx, operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil {
return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
}
@@ -233,7 +239,7 @@ func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penI
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, transferLog); err != nil {
return fmt.Errorf("记录猪只死亡转移日志失败: %w", err)
}
@@ -248,15 +254,16 @@ func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penI
}
// RecordSickPigCull 记录病猪淘汰事件。
func (s *pigBatchService) RecordSickPigCull(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 uint, batchID uint, penID uint, 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")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
err = s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -268,7 +275,7 @@ func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -280,7 +287,7 @@ func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID
}
// 3. 检查当前病猪数量是否足够淘汰
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID)
currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(serviceCtx, tx, batchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err)
}
@@ -290,7 +297,7 @@ func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID
}
// 4. 检查猪栏内猪只数量是否足够淘汰
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
}
@@ -309,12 +316,12 @@ func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID
OperatorID: operatorID,
HappenedAt: happenedAt,
}
if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil {
if err := s.sickSvc.ProcessSickPigLog(serviceCtx, tx, sickLog); err != nil {
return fmt.Errorf("处理病猪淘汰日志失败: %w", err)
}
// 6. 更新批次总猪只数量 (减少批次总数)
if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil {
if err := s.UpdatePigBatchQuantity(serviceCtx, operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil {
return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
}
@@ -328,7 +335,7 @@ func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, transferLog); err != nil {
return fmt.Errorf("记录猪只淘汰转移日志失败: %w", err)
}
@@ -343,15 +350,16 @@ func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID
}
// RecordDeath 记录正常猪只死亡事件。
func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
func (s *pigBatchService) RecordDeath(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordDeath")
if quantity <= 0 {
return errors.New("死亡猪只数量必须大于0")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
err = s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -363,7 +371,7 @@ func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint,
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -375,7 +383,7 @@ func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint,
}
// 3. 检查猪栏内猪只数量是否足够死亡
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
}
@@ -384,7 +392,7 @@ func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint,
}
// 4. 更新批次总猪只数量 (减少批次总数)
if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil {
if err := s.UpdatePigBatchQuantity(serviceCtx, operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil {
return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
}
@@ -398,7 +406,7 @@ func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint,
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, transferLog); err != nil {
return fmt.Errorf("记录猪只死亡转移日志失败: %w", err)
}
@@ -413,15 +421,16 @@ func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint,
}
// RecordCull 记录正常猪只淘汰事件。
func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
func (s *pigBatchService) RecordCull(ctx context.Context, operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "RecordCull")
if quantity <= 0 {
return errors.New("淘汰猪只数量必须大于0")
}
var err error
err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
err = s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 1. 检查批次是否活跃
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID)
batch, err := s.pigBatchRepo.GetPigBatchByIDTx(serviceCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBatchNotFound
@@ -433,7 +442,7 @@ func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint,
}
// 2. 检查猪栏是否关联
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -445,7 +454,7 @@ func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint,
}
// 3. 检查猪栏内猪只数量是否足够淘汰
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err)
}
@@ -454,7 +463,7 @@ func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint,
}
// 4. 更新批次总猪只数量 (减少批次总数)
if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil {
if err := s.UpdatePigBatchQuantity(serviceCtx, operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil {
return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err)
}
@@ -468,7 +477,7 @@ func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint,
OperatorID: operatorID,
Remarks: remarks,
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, transferLog); err != nil {
return fmt.Errorf("记录猪只淘汰转移日志失败: %w", err)
}

View File

@@ -1,23 +1,27 @@
package pig
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"
)
// SellPigs 处理批量销售猪的业务逻辑。
func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
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 {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "SellPigs")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
if quantity <= 0 {
return errors.New("销售数量必须大于0")
}
// 1. 校验猪栏信息
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -31,7 +35,7 @@ func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitP
}
// 2. 业务校验:检查销售数量是否超过猪栏当前猪只数
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err)
}
@@ -51,7 +55,7 @@ func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitP
Remarks: remarks,
OperatorID: operatorID,
}
if err := s.tradeSvc.SellPig(tx, sale); err != nil {
if err := s.tradeSvc.SellPig(serviceCtx, tx, sale); err != nil {
return fmt.Errorf("记录销售交易失败: %w", err)
}
@@ -65,12 +69,12 @@ func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitP
OperatorID: operatorID,
Remarks: fmt.Sprintf("销售给 %s", traderName),
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, transferLog); err != nil {
return fmt.Errorf("创建猪只转移日志失败: %w", err)
}
// 5. 记录批次数量变更日志 (逻辑)
if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeSale, -quantity,
if err := s.updatePigBatchQuantityTx(serviceCtx, tx, operatorID, batchID, models.ChangeTypeSale, -quantity,
fmt.Sprintf("猪批次 %d 从猪栏 %d 销售 %d 头猪给 %s", batchID, penID, quantity, traderName),
tradeDate); err != nil {
return fmt.Errorf("更新猪批次数量失败: %w", err)
@@ -81,14 +85,15 @@ func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitP
}
// BuyPigs 处理批量购买猪的业务逻辑。
func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error {
return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error {
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 {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "BuyPigs")
return s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
if quantity <= 0 {
return errors.New("采购数量必须大于0")
}
// 1. 校验猪栏信息
pen, err := s.transferSvc.GetPenByID(tx, penID)
pen, err := s.transferSvc.GetPenByID(serviceCtx, tx, penID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPenNotFound
@@ -102,7 +107,7 @@ func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPr
}
// 2. 业务校验:检查猪栏容量,如果超出,在备注中记录警告
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID)
currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(serviceCtx, tx, penID)
if err != nil {
return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err)
}
@@ -124,7 +129,7 @@ func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPr
Remarks: remarks, // 用户传入的备注
OperatorID: operatorID,
}
if err := s.tradeSvc.BuyPig(tx, purchase); err != nil {
if err := s.tradeSvc.BuyPig(serviceCtx, tx, purchase); err != nil {
return fmt.Errorf("记录采购交易失败: %w", err)
}
@@ -138,12 +143,12 @@ func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPr
OperatorID: operatorID,
Remarks: transferRemarks, // 包含系统生成的备注和潜在的警告
}
if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil {
if err := s.transferSvc.LogTransfer(serviceCtx, tx, transferLog); err != nil {
return fmt.Errorf("创建猪只转移日志失败: %w", err)
}
// 5. 记录批次数量变更日志 (逻辑)
if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeBuy, quantity,
if err := s.updatePigBatchQuantityTx(serviceCtx, tx, operatorID, batchID, models.ChangeTypeBuy, quantity,
fmt.Sprintf("猪批次 %d 在猪栏 %d 采购 %d 头猪从 %s", batchID, penID, quantity, traderName),
tradeDate); err != nil {
return fmt.Errorf("更新猪批次数量失败: %w", err)

View File

@@ -1,11 +1,14 @@
package pig
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"gorm.io/gorm"
)
@@ -15,31 +18,35 @@ type SickPigManager interface {
// ProcessSickPigLog 处理病猪相关的日志事件。
// log 包含事件的基本信息,如 PigBatchID, PenID, PigIDs, ChangeCount, Reason, TreatmentLocation, Remarks, OperatorID, HappenedAt。
// Manager 内部会计算并填充 BeforeCount 和 AfterCount并进行必要的业务校验和副作用处理。
ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog) error
ProcessSickPigLog(ctx context.Context, tx *gorm.DB, log *models.PigSickLog) error
// GetCurrentSickPigCount 获取指定批次当前患病猪只的总数
GetCurrentSickPigCount(tx *gorm.DB, batchID uint) (int, error)
GetCurrentSickPigCount(ctx context.Context, tx *gorm.DB, batchID uint) (int, error)
}
// sickPigManager 是 SickPigManager 接口的具体实现。
// 它依赖于仓库接口来执行数据持久化操作。
type sickPigManager struct {
ctx context.Context
sickLogRepo repository.PigSickLogRepository
medicationLogRepo repository.MedicationLogRepository
}
// NewSickPigManager 是 sickPigManager 的构造函数。
func NewSickPigManager(
ctx context.Context,
sickLogRepo repository.PigSickLogRepository,
medicationLogRepo repository.MedicationLogRepository,
) SickPigManager {
return &sickPigManager{
ctx: ctx,
sickLogRepo: sickLogRepo,
medicationLogRepo: medicationLogRepo,
}
}
func (s *sickPigManager) ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog) error {
func (s *sickPigManager) ProcessSickPigLog(ctx context.Context, tx *gorm.DB, log *models.PigSickLog) error {
managerCtx := logs.AddFuncName(ctx, s.ctx, "ProcessSickPigLog")
// 1. 输入校验
if log == nil {
return errors.New("病猪日志不能为空")
@@ -93,7 +100,7 @@ func (s *sickPigManager) ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog)
}
// 2. 获取当前病猪数量 (BeforeCount)
beforeCount, err := s.GetCurrentSickPigCount(tx, log.PigBatchID)
beforeCount, err := s.GetCurrentSickPigCount(managerCtx, tx, log.PigBatchID)
if err != nil {
return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", log.PigBatchID, err)
}
@@ -108,15 +115,16 @@ func (s *sickPigManager) ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog)
}
// 5. 持久化 PigSickLog
if err := s.sickLogRepo.CreatePigSickLogTx(tx, log); err != nil {
if err := s.sickLogRepo.CreatePigSickLogTx(managerCtx, tx, log); err != nil {
return fmt.Errorf("创建 PigSickLog 失败: %w", err)
}
return nil
}
func (s *sickPigManager) GetCurrentSickPigCount(tx *gorm.DB, batchID uint) (int, error) {
lastLog, err := s.sickLogRepo.GetLastLogByBatchTx(tx, batchID)
func (s *sickPigManager) GetCurrentSickPigCount(ctx context.Context, tx *gorm.DB, batchID uint) (int, error) {
managerCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentSickPigCount")
lastLog, err := s.sickLogRepo.GetLastLogByBatchTx(managerCtx, tx, batchID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil // 如果没有找到任何日志表示当前病猪数量为0

View File

@@ -1,8 +1,12 @@
package pig
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" // 引入基础设施层的仓库接口
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"gorm.io/gorm"
)
@@ -10,37 +14,41 @@ import (
// 这是一个领域服务,负责协调业务逻辑。
type PigTradeManager interface {
// SellPig 处理卖猪的业务逻辑,通过仓库接口创建 PigSale 记录。
SellPig(tx *gorm.DB, sale *models.PigSale) error
SellPig(ctx context.Context, tx *gorm.DB, sale *models.PigSale) error
// BuyPig 处理买猪的业务逻辑,通过仓库接口创建 PigPurchase 记录。
BuyPig(tx *gorm.DB, purchase *models.PigPurchase) error
BuyPig(ctx context.Context, tx *gorm.DB, purchase *models.PigPurchase) error
}
// pigTradeManager 是 PigTradeManager 接口的具体实现。
// 它依赖于 repository.PigTradeRepository 接口来执行数据持久化操作。
type pigTradeManager struct {
tradeRepo repository.PigTradeRepository // 依赖于基础设施层定义的仓库接口
ctx context.Context
tradeRepo repository.PigTradeRepository
}
// NewPigTradeManager 是 pigTradeManager 的构造函数。
func NewPigTradeManager(tradeRepo repository.PigTradeRepository) PigTradeManager {
func NewPigTradeManager(ctx context.Context, tradeRepo repository.PigTradeRepository) PigTradeManager {
return &pigTradeManager{
ctx: ctx,
tradeRepo: tradeRepo,
}
}
// SellPig 实现了卖猪的逻辑。
// 它通过调用 tradeRepo 来持久化销售记录。
func (s *pigTradeManager) SellPig(tx *gorm.DB, sale *models.PigSale) error {
func (s *pigTradeManager) SellPig(ctx context.Context, tx *gorm.DB, sale *models.PigSale) error {
managerCtx := logs.AddFuncName(ctx, s.ctx, "SellPig")
// 在此处可以添加更复杂的卖猪前置校验或业务逻辑
// 例如:检查猪只库存、更新猪只状态等。
return s.tradeRepo.CreatePigSaleTx(tx, sale)
return s.tradeRepo.CreatePigSaleTx(managerCtx, tx, sale)
}
// BuyPig 实现了买猪的逻辑。
// 它通过调用 tradeRepo 来持久化采购记录。
func (s *pigTradeManager) BuyPig(tx *gorm.DB, purchase *models.PigPurchase) error {
func (s *pigTradeManager) BuyPig(ctx context.Context, tx *gorm.DB, purchase *models.PigPurchase) error {
managerCtx := logs.AddFuncName(ctx, s.ctx, "BuyPig")
// 在此处可以添加更复杂的买猪前置校验或业务逻辑
// 例如:检查资金、更新猪只状态等。
return s.tradeRepo.CreatePigPurchaseTx(tx, purchase)
return s.tradeRepo.CreatePigPurchaseTx(managerCtx, tx, purchase)
}

View File

@@ -1,6 +1,7 @@
package plan
import (
"context"
"fmt"
"sync"
"time"
@@ -14,78 +15,80 @@ import (
// AnalysisPlanTaskManager 定义了分析计划任务管理器的接口。
type AnalysisPlanTaskManager interface {
// Refresh 同步数据库中的计划状态和待执行队列中的触发器任务。
Refresh() error
Refresh(ctx context.Context) error
// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。
// 如果触发器已存在,会根据计划类型更新其执行时间。
CreateOrUpdateTrigger(planID uint) error
CreateOrUpdateTrigger(ctx context.Context, planID uint) error
// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。
// 如果不存在,则会自动创建。此方法不涉及待执行队列。
EnsureAnalysisTaskDefinition(planID uint) error
EnsureAnalysisTaskDefinition(ctx context.Context, planID uint) error
}
// analysisPlanTaskManagerImpl 负责管理分析计划的触发器任务。
// 它确保数据库中可执行的计划在待执行队列中有对应的触发器,并移除无效的触发器。
// 这是一个有状态的组件,包含一个互斥锁以确保并发安全。
type analysisPlanTaskManagerImpl struct {
ctx context.Context
planRepo repository.PlanRepository
pendingTaskRepo repository.PendingTaskRepository
executionLogRepo repository.ExecutionLogRepository
logger *logs.Logger
mu sync.Mutex
}
// NewAnalysisPlanTaskManager 是 analysisPlanTaskManagerImpl 的构造函数。
func NewAnalysisPlanTaskManager(
ctx context.Context,
planRepo repository.PlanRepository,
pendingTaskRepo repository.PendingTaskRepository,
executionLogRepo repository.ExecutionLogRepository,
logger *logs.Logger,
) AnalysisPlanTaskManager {
return &analysisPlanTaskManagerImpl{
ctx: ctx,
planRepo: planRepo,
pendingTaskRepo: pendingTaskRepo,
executionLogRepo: executionLogRepo,
logger: logger,
}
}
// Refresh 同步数据库中的计划状态和待执行队列中的触发器任务。
// 这是一个编排方法,将复杂的逻辑分解到多个内部方法中。
func (m *analysisPlanTaskManagerImpl) Refresh() error {
func (m *analysisPlanTaskManagerImpl) Refresh(ctx context.Context) error {
managerCtx, logger := logs.Trace(ctx, m.ctx, "Refresh")
m.mu.Lock()
defer m.mu.Unlock()
m.logger.Info("开始同步计划任务管理器...")
logger.Info("开始同步计划任务管理器...")
// 1. 一次性获取所有需要的数据
runnablePlans, invalidPlanIDs, pendingTasks, err := m.getRefreshData()
runnablePlans, invalidPlanIDs, pendingTasks, err := m.getRefreshData(managerCtx)
if err != nil {
return fmt.Errorf("获取刷新数据失败: %w", err)
}
// 2. 清理所有与失效计划相关的待执行任务
if err := m.cleanupInvalidTasks(invalidPlanIDs, pendingTasks); err != nil {
if err := m.cleanupInvalidTasks(managerCtx, invalidPlanIDs, pendingTasks); err != nil {
// 仅记录错误,清理失败不应阻止新任务的添加
m.logger.Errorf("清理无效任务时出错: %v", err)
logger.Errorf("清理无效任务时出错: %v", err)
}
// 3. 添加或更新触发器
if err := m.addOrUpdateTriggers(runnablePlans, pendingTasks); err != nil {
if err := m.addOrUpdateTriggers(managerCtx, runnablePlans, pendingTasks); err != nil {
return fmt.Errorf("添加或更新触发器时出错: %w", err)
}
m.logger.Info("计划任务管理器同步完成.")
logger.Info("计划任务管理器同步完成.")
return nil
}
// CreateOrUpdateTrigger 为给定的 planID 创建其关联的触发任务。
// 如果触发器已存在,会根据计划类型更新其执行时间。
func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(planID uint) error {
func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(ctx context.Context, planID uint) error {
managerCtx, logger := logs.Trace(ctx, m.ctx, "CreateOrUpdateTrigger")
m.mu.Lock()
defer m.mu.Unlock()
// 检查计划是否可执行
plan, err := m.planRepo.GetBasicPlanByID(planID)
plan, err := m.planRepo.GetBasicPlanByID(managerCtx, planID)
if err != nil {
return fmt.Errorf("获取计划基本信息失败: %w", err)
}
@@ -94,7 +97,7 @@ func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(planID uint) error {
}
// 查找现有触发器
existingTrigger, err := m.pendingTaskRepo.FindPendingTriggerByPlanID(planID)
existingTrigger, err := m.pendingTaskRepo.FindPendingTriggerByPlanID(managerCtx, planID)
if err != nil {
return fmt.Errorf("查找现有触发器失败: %w", err)
}
@@ -109,7 +112,7 @@ func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(planID uint) error {
// 自动计划,根据 Cron 表达式计算下一次执行时间
next, err := utils.GetNextCronTime(plan.CronExpression)
if err != nil {
m.logger.Errorf("为计划 #%d 解析Cron表达式失败无法更新触发器: %v", plan.ID, err)
logger.Errorf("为计划 #%d 解析Cron表达式失败无法更新触发器: %v", plan.ID, err)
return fmt.Errorf("解析 Cron 表达式失败: %w", err)
}
expectedExecuteAt = next
@@ -117,47 +120,48 @@ func (m *analysisPlanTaskManagerImpl) CreateOrUpdateTrigger(planID uint) error {
// 如果计算出的执行时间与当前待执行任务的时间不一致,则更新
if !existingTrigger.ExecuteAt.Equal(expectedExecuteAt) {
m.logger.Infof("计划 #%d 的执行时间已变更,正在更新触发器 #%d 的执行时间从 %v 到 %v...", plan.ID, existingTrigger.ID, existingTrigger.ExecuteAt, expectedExecuteAt)
if err := m.pendingTaskRepo.UpdatePendingTaskExecuteAt(existingTrigger.ID, expectedExecuteAt); err != nil {
m.logger.Errorf("更新触发器 #%d 的执行时间失败: %v", existingTrigger.ID, err)
logger.Infof("计划 #%d 的执行时间已变更,正在更新触发器 #%d 的执行时间从 %v 到 %v...", plan.ID, existingTrigger.ID, existingTrigger.ExecuteAt, expectedExecuteAt)
if err := m.pendingTaskRepo.UpdatePendingTaskExecuteAt(managerCtx, existingTrigger.ID, expectedExecuteAt); err != nil {
logger.Errorf("更新触发器 #%d 的执行时间失败: %v", existingTrigger.ID, err)
return fmt.Errorf("更新触发器执行时间失败: %w", err)
}
} else {
m.logger.Infof("计划 #%d 的触发器已存在且执行时间无需更新。", plan.ID)
logger.Infof("计划 #%d 的触发器已存在且执行时间无需更新。", plan.ID)
}
return nil // 触发器已存在且已处理更新,直接返回
}
// 如果触发器不存在,则创建新的触发器
m.logger.Infof("为计划 #%d 创建新的触发器...", planID)
return m.createTriggerTask(plan)
logger.Infof("为计划 #%d 创建新的触发器...", planID)
return m.createTriggerTask(managerCtx, plan)
}
// EnsureAnalysisTaskDefinition 确保计划的分析任务定义存在于 tasks 表中。
// 如果不存在,则会自动创建。此方法不涉及待执行队列。
func (m *analysisPlanTaskManagerImpl) EnsureAnalysisTaskDefinition(planID uint) error {
func (m *analysisPlanTaskManagerImpl) EnsureAnalysisTaskDefinition(ctx context.Context, planID uint) error {
managerCtx, logger := logs.Trace(ctx, m.ctx, "EnsureAnalysisTaskDefinition")
m.mu.Lock()
defer m.mu.Unlock()
plan, err := m.planRepo.GetBasicPlanByID(planID)
plan, err := m.planRepo.GetBasicPlanByID(managerCtx, planID)
if err != nil {
return fmt.Errorf("确保分析任务定义失败:获取计划 #%d 基本信息时出错: %w", planID, err)
}
analysisTask, err := m.planRepo.FindPlanAnalysisTaskByPlanID(plan.ID)
analysisTask, err := m.planRepo.FindPlanAnalysisTaskByPlanID(managerCtx, plan.ID)
if err != nil {
return fmt.Errorf("确保分析任务定义失败:查找计划 #%d 的分析任务时出错: %w", plan.ID, err)
}
if analysisTask == nil {
m.logger.Infof("未找到计划 #%d 关联的 'plan_analysis' 任务定义,将自动创建...", plan.ID)
_, err := m.planRepo.CreatePlanAnalysisTask(plan) // CreatePlanAnalysisTask returns *models.Task, error
logger.Infof("未找到计划 #%d 关联的 'plan_analysis' 任务定义,将自动创建...", plan.ID)
_, err := m.planRepo.CreatePlanAnalysisTask(managerCtx, plan) // CreatePlanAnalysisTask returns *models.Task, error
if err != nil {
return fmt.Errorf("自动创建 'plan_analysis' 任务定义失败: %w", err)
}
m.logger.Infof("已成功为计划 #%d 创建 'plan_analysis' 任务定义。", plan.ID)
logger.Infof("已成功为计划 #%d 创建 'plan_analysis' 任务定义。", plan.ID)
} else {
m.logger.Infof("计划 #%d 的 'plan_analysis' 任务定义已存在。", plan.ID)
logger.Infof("计划 #%d 的 'plan_analysis' 任务定义已存在。", plan.ID)
}
return nil
@@ -166,16 +170,17 @@ func (m *analysisPlanTaskManagerImpl) EnsureAnalysisTaskDefinition(planID uint)
// --- 内部私有方法 ---
// getRefreshData 从数据库获取刷新所需的所有数据。
func (m *analysisPlanTaskManagerImpl) getRefreshData() (runnablePlans []*models.Plan, invalidPlanIDs []uint, pendingTasks []models.PendingTask, err error) {
runnablePlans, err = m.planRepo.FindRunnablePlans()
func (m *analysisPlanTaskManagerImpl) getRefreshData(ctx context.Context) (runnablePlans []*models.Plan, invalidPlanIDs []uint, pendingTasks []models.PendingTask, err error) {
managerCtx, logger := logs.Trace(ctx, m.ctx, "getRefreshData")
runnablePlans, err = m.planRepo.FindRunnablePlans(managerCtx)
if err != nil {
m.logger.Errorf("获取可执行计划列表失败: %v", err)
logger.Errorf("获取可执行计划列表失败: %v", err)
return
}
invalidPlans, err := m.planRepo.FindInactivePlans()
invalidPlans, err := m.planRepo.FindInactivePlans(managerCtx)
if err != nil {
m.logger.Errorf("获取失效计划列表失败: %v", err)
logger.Errorf("获取失效计划列表失败: %v", err)
return
}
invalidPlanIDs = make([]uint, len(invalidPlans))
@@ -183,16 +188,17 @@ func (m *analysisPlanTaskManagerImpl) getRefreshData() (runnablePlans []*models.
invalidPlanIDs[i] = p.ID
}
pendingTasks, err = m.pendingTaskRepo.FindAllPendingTasks()
pendingTasks, err = m.pendingTaskRepo.FindAllPendingTasks(managerCtx)
if err != nil {
m.logger.Errorf("获取所有待执行任务失败: %v", err)
logger.Errorf("获取所有待执行任务失败: %v", err)
return
}
return
}
// cleanupInvalidTasks 清理所有与失效计划相关的待执行任务。
func (m *analysisPlanTaskManagerImpl) cleanupInvalidTasks(invalidPlanIDs []uint, allPendingTasks []models.PendingTask) error {
func (m *analysisPlanTaskManagerImpl) cleanupInvalidTasks(ctx context.Context, invalidPlanIDs []uint, allPendingTasks []models.PendingTask) error {
managerCtx, logger := logs.Trace(ctx, m.ctx, "cleanupInvalidTasks")
if len(invalidPlanIDs) == 0 {
return nil // 没有需要清理的计划
}
@@ -219,24 +225,25 @@ func (m *analysisPlanTaskManagerImpl) cleanupInvalidTasks(invalidPlanIDs []uint,
return nil // 没有找到需要清理的任务
}
m.logger.Infof("准备从待执行队列中清理 %d 个与失效计划相关的任务...", len(tasksToDeleteIDs))
logger.Infof("准备从待执行队列中清理 %d 个与失效计划相关的任务...", len(tasksToDeleteIDs))
// 批量删除待执行任务
if err := m.pendingTaskRepo.DeletePendingTasksByIDs(tasksToDeleteIDs); err != nil {
if err := m.pendingTaskRepo.DeletePendingTasksByIDs(managerCtx, tasksToDeleteIDs); err != nil {
return fmt.Errorf("批量删除待执行任务失败: %w", err)
}
// 批量更新相关执行日志状态为“已取消”
if err := m.executionLogRepo.UpdateTaskExecutionLogStatusByIDs(logsToCancelIDs, models.ExecutionStatusCancelled); err != nil {
if err := m.executionLogRepo.UpdateTaskExecutionLogStatusByIDs(managerCtx, logsToCancelIDs, models.ExecutionStatusCancelled); err != nil {
// 这是一个非关键性错误,只记录日志
m.logger.Warnf("批量更新日志状态为 'Cancelled' 失败: %v", err)
logger.Warnf("批量更新日志状态为 'Cancelled' 失败: %v", err)
}
return nil
}
// addOrUpdateTriggers 检查、更新或创建触发器。
func (m *analysisPlanTaskManagerImpl) addOrUpdateTriggers(runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error {
func (m *analysisPlanTaskManagerImpl) addOrUpdateTriggers(ctx context.Context, runnablePlans []*models.Plan, allPendingTasks []models.PendingTask) error {
managerCtx, logger := logs.Trace(ctx, m.ctx, "addOrUpdateTriggers")
// 创建一个映射,存放所有已在队列中的计划触发器
pendingTriggersMap := make(map[uint]models.PendingTask)
for _, pt := range allPendingTasks {
@@ -254,22 +261,22 @@ func (m *analysisPlanTaskManagerImpl) addOrUpdateTriggers(runnablePlans []*model
if plan.ExecutionType == models.PlanExecutionTypeAutomatic {
next, err := utils.GetNextCronTime(plan.CronExpression)
if err != nil {
m.logger.Errorf("为计划 #%d 解析Cron表达式失败跳过更新: %v", plan.ID, err)
logger.Errorf("为计划 #%d 解析Cron表达式失败跳过更新: %v", plan.ID, err)
continue
}
// 如果数据库中记录的执行时间与根据当前Cron表达式计算出的下一次时间不一致则更新
if !existingTrigger.ExecuteAt.Equal(next) {
m.logger.Infof("计划 #%d 的执行时间已变更,正在更新触发器 #%d 的执行时间从 %v 到 %v...", plan.ID, existingTrigger.ID, existingTrigger.ExecuteAt, next)
if err := m.pendingTaskRepo.UpdatePendingTaskExecuteAt(existingTrigger.ID, next); err != nil {
m.logger.Errorf("更新触发器 #%d 的执行时间失败: %v", existingTrigger.ID, err)
logger.Infof("计划 #%d 的执行时间已变更,正在更新触发器 #%d 的执行时间从 %v 到 %v...", plan.ID, existingTrigger.ID, existingTrigger.ExecuteAt, next)
if err := m.pendingTaskRepo.UpdatePendingTaskExecuteAt(managerCtx, existingTrigger.ID, next); err != nil {
logger.Errorf("更新触发器 #%d 的执行时间失败: %v", existingTrigger.ID, err)
}
}
}
} else {
// --- 原有逻辑:为缺失的计划创建新触发器 ---
m.logger.Infof("发现应执行但队列中缺失的计划 #%d正在为其创建触发器...", plan.ID)
if err := m.createTriggerTask(plan); err != nil {
m.logger.Errorf("为计划 #%d 创建触发器失败: %v", plan.ID, err)
logger.Infof("发现应执行但队列中缺失的计划 #%d正在为其创建触发器...", plan.ID)
if err := m.createTriggerTask(managerCtx, plan); err != nil {
logger.Errorf("为计划 #%d 创建触发器失败: %v", plan.ID, err)
// 继续处理下一个,不因单点失败而中断
}
}
@@ -278,21 +285,22 @@ func (m *analysisPlanTaskManagerImpl) addOrUpdateTriggers(runnablePlans []*model
}
// createTriggerTask 是创建触发器任务的内部核心逻辑。
func (m *analysisPlanTaskManagerImpl) createTriggerTask(plan *models.Plan) error {
analysisTask, err := m.planRepo.FindPlanAnalysisTaskByPlanID(plan.ID)
func (m *analysisPlanTaskManagerImpl) createTriggerTask(ctx context.Context, plan *models.Plan) error {
managerCtx, logger := logs.Trace(ctx, m.ctx, "createTriggerTask")
analysisTask, err := m.planRepo.FindPlanAnalysisTaskByPlanID(managerCtx, plan.ID)
if err != nil {
return fmt.Errorf("查找计划分析任务失败: %w", err)
}
// --- 如果触发器任务定义不存在,则自动创建 ---
if analysisTask == nil {
m.logger.Warnf("未找到计划 #%d 关联的 'plan_analysis' 任务定义,将自动创建...", plan.ID)
newAnalysisTask, err := m.planRepo.CreatePlanAnalysisTask(plan)
logger.Warnf("未找到计划 #%d 关联的 'plan_analysis' 任务定义,将自动创建...", plan.ID)
newAnalysisTask, err := m.planRepo.CreatePlanAnalysisTask(managerCtx, plan)
if err != nil {
return fmt.Errorf("自动创建 'plan_analysis' 任务定义失败: %w", err)
}
analysisTask = newAnalysisTask
m.logger.Infof("已成功为计划 #%d 创建 'plan_analysis' 任务定义 (ID: %d)", plan.ID, analysisTask.ID)
logger.Infof("已成功为计划 #%d 创建 'plan_analysis' 任务定义 (ID: %d)", plan.ID, analysisTask.ID)
}
var executeAt time.Time
@@ -310,7 +318,7 @@ func (m *analysisPlanTaskManagerImpl) createTriggerTask(plan *models.Plan) error
TaskID: analysisTask.ID,
Status: models.ExecutionStatusWaiting,
}
if err := m.executionLogRepo.CreateTaskExecutionLog(taskLog); err != nil {
if err := m.executionLogRepo.CreateTaskExecutionLog(managerCtx, taskLog); err != nil {
return fmt.Errorf("创建任务执行日志失败: %w", err)
}
@@ -319,10 +327,10 @@ func (m *analysisPlanTaskManagerImpl) createTriggerTask(plan *models.Plan) error
ExecuteAt: executeAt,
TaskExecutionLogID: taskLog.ID,
}
if err := m.pendingTaskRepo.CreatePendingTask(pendingTask); err != nil {
if err := m.pendingTaskRepo.CreatePendingTask(managerCtx, pendingTask); err != nil {
return fmt.Errorf("创建待执行任务失败: %w", err)
}
m.logger.Infof("成功为计划 #%d 创建触发器 (任务ID: %d),执行时间: %v", plan.ID, analysisTask.ID, executeAt)
logger.Infof("成功为计划 #%d 创建触发器 (任务ID: %d),执行时间: %v", plan.ID, analysisTask.ID, executeAt)
return nil
}

View File

@@ -1 +0,0 @@
package plan

View File

@@ -1,6 +1,7 @@
package plan
import (
"context"
"errors"
"sync"
"time"
@@ -16,9 +17,9 @@ import (
// ExecutionManager 定义了计划执行管理器的接口。
type ExecutionManager interface {
// Start 启动计划执行管理器。
Start()
Start(ctx context.Context)
// Stop 优雅地停止计划执行管理器。
Stop()
Stop(ctx context.Context)
}
// ProgressTracker 仅用于在内存中提供计划执行的并发锁
@@ -83,7 +84,7 @@ func (t *ProgressTracker) GetRunningPlanIDs() []uint {
// planExecutionManagerImpl 是核心的、持久化的任务调度器
type planExecutionManagerImpl struct {
logger *logs.Logger
ctx context.Context
pollingInterval time.Duration
workers int
pendingTaskRepo repository.PendingTaskRepository
@@ -103,6 +104,7 @@ type planExecutionManagerImpl struct {
// NewPlanExecutionManager 创建一个新的调度器实例
func NewPlanExecutionManager(
ctx context.Context,
pendingTaskRepo repository.PendingTaskRepository,
executionLogRepo repository.ExecutionLogRepository,
deviceRepo repository.DeviceRepository,
@@ -110,12 +112,12 @@ func NewPlanExecutionManager(
planRepo repository.PlanRepository,
analysisPlanTaskManager AnalysisPlanTaskManager,
taskFactory TaskFactory,
logger *logs.Logger,
deviceService device.Service,
interval time.Duration,
numWorkers int,
) ExecutionManager {
return &planExecutionManagerImpl{
ctx: ctx,
pendingTaskRepo: pendingTaskRepo,
executionLogRepo: executionLogRepo,
deviceRepo: deviceRepo,
@@ -123,7 +125,6 @@ func NewPlanExecutionManager(
planRepo: planRepo,
analysisPlanTaskManager: analysisPlanTaskManager,
taskFactory: taskFactory,
logger: logger,
deviceService: deviceService,
pollingInterval: interval,
workers: numWorkers,
@@ -133,10 +134,11 @@ func NewPlanExecutionManager(
}
// Start 启动调度器,包括初始化协程池和启动主轮询循环
func (s *planExecutionManagerImpl) Start() {
s.logger.Warnf("任务调度器正在启动,工作协程数: %d...", s.workers)
func (s *planExecutionManagerImpl) Start(ctx context.Context) {
managerCtx, logger := logs.Trace(ctx, s.ctx, "Start")
logger.Warnf("任务调度器正在启动,工作协程数: %d...", s.workers)
pool, err := ants.NewPool(s.workers, ants.WithPanicHandler(func(err interface{}) {
s.logger.Errorf("[严重] 任务执行时发生 panic: %v", err)
logger.Errorf("[严重] 任务执行时发生 panic: %v", err)
}))
if err != nil {
panic("初始化协程池失败: " + err.Error())
@@ -144,21 +146,23 @@ func (s *planExecutionManagerImpl) Start() {
s.pool = pool
s.wg.Add(1)
go s.run()
s.logger.Warnf("任务调度器已成功启动")
go s.run(managerCtx)
logger.Warnf("任务调度器已成功启动")
}
// Stop 优雅地停止调度器
func (s *planExecutionManagerImpl) Stop() {
s.logger.Warnf("正在停止任务调度器...")
func (s *planExecutionManagerImpl) Stop(ctx context.Context) {
logger := logs.TraceLogger(ctx, s.ctx, "Stop")
logger.Warnf("正在停止任务调度器...")
close(s.stopChan) // 1. 发出停止信号,停止主循环
s.wg.Wait() // 2. 等待主循环完成
s.pool.Release() // 3. 释放 ants 池 (等待所有已提交的任务执行完毕)
s.logger.Warnf("任务调度器已安全停止")
logger.Warnf("任务调度器已安全停止")
}
// run 是主轮询循环,负责从数据库认领任务并提交到协程池
func (s *planExecutionManagerImpl) run() {
func (s *planExecutionManagerImpl) run(ctx context.Context) {
managerCtx := logs.AddFuncName(ctx, s.ctx, "run")
defer s.wg.Done()
ticker := time.NewTicker(s.pollingInterval)
defer ticker.Stop()
@@ -170,19 +174,20 @@ func (s *planExecutionManagerImpl) run() {
return
case <-ticker.C:
// 定时触发任务认领和提交
go s.claimAndSubmit()
go s.claimAndSubmit(managerCtx)
}
}
}
// claimAndSubmit 实现了最终的“认领-锁定-执行 或 等待-放回”的健壮逻辑
func (s *planExecutionManagerImpl) claimAndSubmit() {
func (s *planExecutionManagerImpl) claimAndSubmit(ctx context.Context) {
managerCtx, logger := logs.Trace(ctx, s.ctx, "claimAndSubmit")
runningPlanIDs := s.progressTracker.GetRunningPlanIDs()
claimedLog, pendingTask, err := s.pendingTaskRepo.ClaimNextAvailableTask(runningPlanIDs)
claimedLog, pendingTask, err := s.pendingTaskRepo.ClaimNextAvailableTask(managerCtx, runningPlanIDs)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Errorf("认领任务时发生错误: %v", err)
logger.Errorf("认领任务时发生错误: %v", err)
}
// gorm.ErrRecordNotFound 说明没任务要执行
return
@@ -193,100 +198,103 @@ func (s *planExecutionManagerImpl) claimAndSubmit() {
// 成功获取锁,正常派发任务
err = s.pool.Submit(func() {
defer s.progressTracker.Unlock(claimedLog.PlanExecutionLogID)
s.processTask(claimedLog)
s.processTask(managerCtx, claimedLog)
})
if err != nil {
s.logger.Errorf("向协程池提交任务失败: %v", err)
logger.Errorf("向协程池提交任务失败: %v", err)
// 提交失败,必须释放刚刚获取的锁
s.progressTracker.Unlock(claimedLog.PlanExecutionLogID)
// 同样需要将任务安全放回
s.handleRequeue(claimedLog.PlanExecutionLogID, pendingTask)
s.handleRequeue(managerCtx, claimedLog.PlanExecutionLogID, pendingTask)
}
} else {
// 获取锁失败,说明有“兄弟”任务正在执行。执行“锁定并安全放回”逻辑。
s.handleRequeue(claimedLog.PlanExecutionLogID, pendingTask)
s.handleRequeue(managerCtx, claimedLog.PlanExecutionLogID, pendingTask)
}
}
// handleRequeue 同步地、安全地将一个无法立即执行的任务放回队列。
func (s *planExecutionManagerImpl) handleRequeue(planExecutionLogID uint, taskToRequeue *models.PendingTask) {
s.logger.Warnf("计划 %d 正在执行,任务 %d (TaskID: %d) 将等待并重新入队...", planExecutionLogID, taskToRequeue.ID, taskToRequeue.TaskID)
func (s *planExecutionManagerImpl) handleRequeue(ctx context.Context, planExecutionLogID uint, taskToRequeue *models.PendingTask) {
managerCtx, logger := logs.Trace(ctx, s.ctx, "handleRequeue")
logger.Warnf("计划 %d 正在执行,任务 %d (TaskID: %d) 将等待并重新入队...", planExecutionLogID, taskToRequeue.ID, taskToRequeue.TaskID)
// 1. 阻塞式地等待,直到可以获取到该计划的锁。
s.progressTracker.Lock(planExecutionLogID)
defer s.progressTracker.Unlock(planExecutionLogID)
// 2. 在持有锁的情况下,将任务安全地放回队列。
if err := s.pendingTaskRepo.RequeueTask(taskToRequeue); err != nil {
s.logger.Errorf("[严重] 任务重新入队失败, 原始PendingTaskID: %d, 错误: %v", taskToRequeue.ID, err)
if err := s.pendingTaskRepo.RequeueTask(managerCtx, taskToRequeue); err != nil {
logger.Errorf("[严重] 任务重新入队失败, 原始PendingTaskID: %d, 错误: %v", taskToRequeue.ID, err)
return
}
s.logger.Warnf("任务 (原始ID: %d) 已成功重新入队,并已释放计划 %d 的锁。", taskToRequeue.ID, planExecutionLogID)
logger.Warnf("任务 (原始ID: %d) 已成功重新入队,并已释放计划 %d 的锁。", taskToRequeue.ID, planExecutionLogID)
}
// processTask 处理单个任务的逻辑
func (s *planExecutionManagerImpl) processTask(claimedLog *models.TaskExecutionLog) {
s.logger.Warnf("开始处理任务, 日志ID: %d, 任务ID: %d, 任务名称: %s, 描述: %s",
func (s *planExecutionManagerImpl) processTask(ctx context.Context, claimedLog *models.TaskExecutionLog) {
managerCtx, logger := logs.Trace(ctx, s.ctx, "processTask")
logger.Warnf("开始处理任务, 日志ID: %d, 任务ID: %d, 任务名称: %s, 描述: %s",
claimedLog.ID, claimedLog.TaskID, claimedLog.Task.Name, claimedLog.Task.Description)
claimedLog.StartedAt = time.Now()
claimedLog.Status = models.ExecutionStatusCompleted // 先乐观假定任务成功, 后续失败了再改
defer s.updateTaskExecutionLogStatus(claimedLog)
defer s.updateTaskExecutionLogStatus(managerCtx, claimedLog)
// 执行任务
err := s.runTask(claimedLog)
err := s.runTask(managerCtx, claimedLog)
if err != nil {
claimedLog.Status = models.ExecutionStatusFailed
claimedLog.Output = err.Error()
// 任务失败时,调用统一的终止服务
s.handlePlanTermination(claimedLog.PlanExecutionLogID, "子任务执行失败: "+err.Error())
s.handlePlanTermination(managerCtx, claimedLog.PlanExecutionLogID, "子任务执行失败: "+err.Error())
return
}
// 如果是计划分析任务,它的职责是解析和分发任务,到此即完成,不参与后续的计划完成度检查。
if claimedLog.Task.Type == models.TaskPlanAnalysis {
s.logger.Warnf("完成计划分析任务, 日志ID: %d", claimedLog.ID)
logger.Warnf("完成计划分析任务, 日志ID: %d", claimedLog.ID)
return
}
// --- 以下是常规任务的完成逻辑 ---
s.logger.Warnf("完成任务, 日志ID: %d", claimedLog.ID)
logger.Warnf("完成任务, 日志ID: %d", claimedLog.ID)
// 检查是否是最后一个任务
incompleteCount, err := s.executionLogRepo.CountIncompleteTasksByPlanLogID(claimedLog.PlanExecutionLogID)
incompleteCount, err := s.executionLogRepo.CountIncompleteTasksByPlanLogID(managerCtx, claimedLog.PlanExecutionLogID)
if err != nil {
s.logger.Errorf("检查计划 %d 的未完成任务数时出错: %v", claimedLog.PlanExecutionLogID, err)
logger.Errorf("检查计划 %d 的未完成任务数时出错: %v", claimedLog.PlanExecutionLogID, err)
return
}
// 如果此计划执行中,未完成的任务只剩下当前这一个(因为当前任务的状态此时在数据库中仍为 'started'
// 则认为整个计划已完成。
if incompleteCount == 1 {
s.handlePlanCompletion(claimedLog.PlanExecutionLogID)
s.handlePlanCompletion(managerCtx, claimedLog.PlanExecutionLogID)
}
}
// runTask 用于执行具体任务
func (s *planExecutionManagerImpl) runTask(claimedLog *models.TaskExecutionLog) error {
func (s *planExecutionManagerImpl) runTask(ctx context.Context, claimedLog *models.TaskExecutionLog) error {
managerCtx, logger := logs.Trace(ctx, s.ctx, "runTask")
// 这是个特殊任务, 用于解析Plan并将解析出的任务队列添加到待执行队列中
if claimedLog.Task.Type == models.TaskPlanAnalysis {
// 解析plan
err := s.analysisPlan(claimedLog)
err := s.analysisPlan(managerCtx, claimedLog)
if err != nil {
// TODO 这里要处理一下, 比如再插一个新的触发器回去
s.logger.Errorf("[严重] 计划解析失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
logger.Errorf("[严重] 计划解析失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
return err
}
} else {
// 执行普通任务
task := s.taskFactory.Production(claimedLog)
task := s.taskFactory.Production(managerCtx, claimedLog)
if err := task.Execute(); err != nil {
s.logger.Errorf("[严重] 任务执行失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
task.OnFailure(err)
if err := task.Execute(managerCtx); err != nil {
logger.Errorf("[严重] 任务执行失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
task.OnFailure(managerCtx, err)
return err
}
@@ -295,14 +303,15 @@ func (s *planExecutionManagerImpl) runTask(claimedLog *models.TaskExecutionLog)
}
// analysisPlan 解析Plan并将解析出的Task列表插入待执行队列中
func (s *planExecutionManagerImpl) analysisPlan(claimedLog *models.TaskExecutionLog) error {
func (s *planExecutionManagerImpl) analysisPlan(ctx context.Context, claimedLog *models.TaskExecutionLog) error {
managerCtx, logger := logs.Trace(ctx, s.ctx, "analysisPlan")
// 创建Plan执行记录
// 从任务的 Parameters 中解析出真实的 PlanID
var params struct {
PlanID uint `json:"plan_id"`
}
if err := claimedLog.Task.ParseParameters(&params); err != nil {
s.logger.Errorf("解析任务参数中的计划ID失败日志ID: %d, 错误: %v", claimedLog.ID, err)
logger.Errorf("解析任务参数中的计划ID失败日志ID: %d, 错误: %v", claimedLog.ID, err)
return err
}
realPlanID := params.PlanID
@@ -312,15 +321,15 @@ func (s *planExecutionManagerImpl) analysisPlan(claimedLog *models.TaskExecution
Status: models.ExecutionStatusStarted,
StartedAt: time.Now(),
}
if err := s.executionLogRepo.CreatePlanExecutionLog(planLog); err != nil {
s.logger.Errorf("[严重] 创建计划执行日志失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
if err := s.executionLogRepo.CreatePlanExecutionLog(managerCtx, planLog); err != nil {
logger.Errorf("[严重] 创建计划执行日志失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
return err
}
// 解析出Task列表
tasks, err := s.planRepo.FlattenPlanTasks(realPlanID)
tasks, err := s.planRepo.FlattenPlanTasks(managerCtx, realPlanID)
if err != nil {
s.logger.Errorf("[严重] 解析计划失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
logger.Errorf("[严重] 解析计划失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
return err
}
@@ -334,9 +343,9 @@ func (s *planExecutionManagerImpl) analysisPlan(claimedLog *models.TaskExecution
}
}
err = s.executionLogRepo.CreateTaskExecutionLogsInBatch(taskLogs)
err = s.executionLogRepo.CreateTaskExecutionLogsInBatch(managerCtx, taskLogs)
if err != nil {
s.logger.Errorf("[严重] 写入执行历史, 日志ID: %d, 错误: %v", claimedLog.ID, err)
logger.Errorf("[严重] 写入执行历史, 日志ID: %d, 错误: %v", claimedLog.ID, err)
return err
}
@@ -351,9 +360,9 @@ func (s *planExecutionManagerImpl) analysisPlan(claimedLog *models.TaskExecution
ExecuteAt: time.Now().Add(time.Duration(i) * time.Second),
}
}
err = s.pendingTaskRepo.CreatePendingTasksInBatch(pendingTasks)
err = s.pendingTaskRepo.CreatePendingTasksInBatch(managerCtx, pendingTasks)
if err != nil {
s.logger.Errorf("[严重] 写入待执行队列, 日志ID: %d, 错误: %v", claimedLog.ID, err)
logger.Errorf("[严重] 写入待执行队列, 日志ID: %d, 错误: %v", claimedLog.ID, err)
return err
}
@@ -361,18 +370,19 @@ func (s *planExecutionManagerImpl) analysisPlan(claimedLog *models.TaskExecution
// 如果一个计划被解析后,发现其任务列表为空,
// 那么它实际上已经“执行”完毕了,我们需要在这里手动为它创建下一次的触发器。
if len(tasks) == 0 {
s.handlePlanCompletion(planLog.ID)
s.handlePlanCompletion(managerCtx, planLog.ID)
}
return nil
}
// updateTaskExecutionLogStatus 修改任务历史中的执行状态
func (s *planExecutionManagerImpl) updateTaskExecutionLogStatus(claimedLog *models.TaskExecutionLog) error {
func (s *planExecutionManagerImpl) updateTaskExecutionLogStatus(ctx context.Context, claimedLog *models.TaskExecutionLog) error {
managerCtx, logger := logs.Trace(ctx, s.ctx, "updateTaskExecutionLogStatus")
claimedLog.EndedAt = time.Now()
if err := s.executionLogRepo.UpdateTaskExecutionLog(claimedLog); err != nil {
s.logger.Errorf("[严重] 更新任务执行日志失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
if err := s.executionLogRepo.UpdateTaskExecutionLog(managerCtx, claimedLog); err != nil {
logger.Errorf("[严重] 更新任务执行日志失败, 日志ID: %d, 错误: %v", claimedLog.ID, err)
return err
}
@@ -380,64 +390,66 @@ func (s *planExecutionManagerImpl) updateTaskExecutionLogStatus(claimedLog *mode
}
// handlePlanTermination 集中处理计划的终止逻辑(失败或取消)
func (s *planExecutionManagerImpl) handlePlanTermination(planLogID uint, reason string) {
func (s *planExecutionManagerImpl) handlePlanTermination(ctx context.Context, planLogID uint, reason string) {
managerCtx, logger := logs.Trace(ctx, s.ctx, "handlePlanTermination")
// 1. 从待执行队列中删除所有相关的子任务
if err := s.pendingTaskRepo.DeletePendingTasksByPlanLogID(planLogID); err != nil {
s.logger.Errorf("从待执行队列中删除计划 %d 的后续任务时出错: %v", planLogID, err)
if err := s.pendingTaskRepo.DeletePendingTasksByPlanLogID(managerCtx, planLogID); err != nil {
logger.Errorf("从待执行队列中删除计划 %d 的后续任务时出错: %v", planLogID, err)
}
// 2. 将父计划的执行日志标记为失败
if err := s.executionLogRepo.FailPlanExecution(planLogID, reason); err != nil {
s.logger.Errorf("标记计划执行日志 %d 为失败时出错: %v", planLogID, err)
if err := s.executionLogRepo.FailPlanExecution(managerCtx, planLogID, reason); err != nil {
logger.Errorf("标记计划执行日志 %d 为失败时出错: %v", planLogID, err)
}
// 3. 将所有未完成的子任务日志标记为已取消
if err := s.executionLogRepo.CancelIncompleteTasksByPlanLogID(planLogID, "父计划失败或被取消"); err != nil {
s.logger.Errorf("取消计划 %d 的后续任务日志时出错: %v", planLogID, err)
if err := s.executionLogRepo.CancelIncompleteTasksByPlanLogID(managerCtx, planLogID, "父计划失败或被取消"); err != nil {
logger.Errorf("取消计划 %d 的后续任务日志时出错: %v", planLogID, err)
}
// 4. 获取计划执行日志以获取顶层 PlanID
planLog, err := s.executionLogRepo.FindPlanExecutionLogByID(planLogID)
planLog, err := s.executionLogRepo.FindPlanExecutionLogByID(managerCtx, planLogID)
if err != nil {
s.logger.Errorf("无法找到计划执行日志 %d 以更新父计划状态: %v", planLogID, err)
logger.Errorf("无法找到计划执行日志 %d 以更新父计划状态: %v", planLogID, err)
return
}
// 5. 获取顶层计划的详细信息,以检查其类型
topLevelPlan, err := s.planRepo.GetBasicPlanByID(planLog.PlanID)
topLevelPlan, err := s.planRepo.GetBasicPlanByID(managerCtx, planLog.PlanID)
if err != nil {
s.logger.Errorf("获取顶层计划 %d 的基本信息失败: %v", planLog.PlanID, err)
logger.Errorf("获取顶层计划 %d 的基本信息失败: %v", planLog.PlanID, err)
return
}
// 6. 如果是系统任务,则不修改计划状态
if topLevelPlan.PlanType == models.PlanTypeSystem {
s.logger.Warnf("系统任务 %d (日志ID: %d) 执行失败,但根据策略不修改其计划状态。", topLevelPlan.ID, planLogID)
logger.Warnf("系统任务 %d (日志ID: %d) 执行失败,但根据策略不修改其计划状态。", topLevelPlan.ID, planLogID)
return
}
// 7. 将计划本身的状态更新为失败 (仅对非系统任务执行)
if err := s.planRepo.UpdatePlanStatus(planLog.PlanID, models.PlanStatusFailed); err != nil {
s.logger.Errorf("更新计划 %d 状态为 '失败' 时出错: %v", planLog.PlanID, err)
if err := s.planRepo.UpdatePlanStatus(managerCtx, planLog.PlanID, models.PlanStatusFailed); err != nil {
logger.Errorf("更新计划 %d 状态为 '失败' 时出错: %v", planLog.PlanID, err)
}
}
// handlePlanCompletion 集中处理计划成功完成后的所有逻辑
func (s *planExecutionManagerImpl) handlePlanCompletion(planLogID uint) {
s.logger.Infof("计划执行 %d 的所有任务已完成,开始处理计划完成逻辑...", planLogID)
func (s *planExecutionManagerImpl) handlePlanCompletion(ctx context.Context, planLogID uint) {
managerCtx, logger := logs.Trace(ctx, s.ctx, "handlePlanCompletion")
logger.Infof("计划执行 %d 的所有任务已完成,开始处理计划完成逻辑...", planLogID)
// 1. 通过 PlanExecutionLog 反查正确的顶层 PlanID
planExecutionLog, err := s.executionLogRepo.FindPlanExecutionLogByID(planLogID)
planExecutionLog, err := s.executionLogRepo.FindPlanExecutionLogByID(managerCtx, planLogID)
if err != nil {
s.logger.Errorf("获取计划执行日志 %d 失败: %v", planLogID, err)
logger.Errorf("获取计划执行日志 %d 失败: %v", planLogID, err)
return
}
topLevelPlanID := planExecutionLog.PlanID // 这才是正确的顶层计划ID
// 2. 获取计划的最新数据,这里我们只需要基本信息来判断执行类型和次数
plan, err := s.planRepo.GetBasicPlanByID(topLevelPlanID)
plan, err := s.planRepo.GetBasicPlanByID(managerCtx, topLevelPlanID)
if err != nil {
s.logger.Errorf("获取计划 %d 的基本信息失败: %v", topLevelPlanID, err)
logger.Errorf("获取计划 %d 的基本信息失败: %v", topLevelPlanID, err)
return
}
@@ -448,27 +460,27 @@ func (s *planExecutionManagerImpl) handlePlanCompletion(planLogID uint) {
// 如果是自动计划且达到执行次数上限,或计划是手动类型,则更新计划状态为已停止
if (plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteNum > 0 && newExecuteCount >= plan.ExecuteNum) || plan.ExecutionType == models.PlanExecutionTypeManual {
newStatus = models.PlanStatusStopped
s.logger.Infof("计划 %d 已完成执行,状态更新为 '执行完毕'。", topLevelPlanID)
logger.Infof("计划 %d 已完成执行,状态更新为 '执行完毕'。", topLevelPlanID)
}
// 4. 使用专门的方法来原子性地更新计数值和状态
if err := s.planRepo.UpdatePlanStateAfterExecution(topLevelPlanID, newExecuteCount, newStatus); err != nil {
s.logger.Errorf("更新计划 %d 的执行后状态失败: %v", topLevelPlanID, err)
if err := s.planRepo.UpdatePlanStateAfterExecution(managerCtx, topLevelPlanID, newExecuteCount, newStatus); err != nil {
logger.Errorf("更新计划 %d 的执行后状态失败: %v", topLevelPlanID, err)
return
}
// 5. 更新计划执行日志状态为完成
if err := s.executionLogRepo.UpdatePlanExecutionLogStatus(planLogID, models.ExecutionStatusCompleted); err != nil {
s.logger.Errorf("更新计划执行日志 %d 状态为 '完成' 失败: %v", planLogID, err)
if err := s.executionLogRepo.UpdatePlanExecutionLogStatus(managerCtx, planLogID, models.ExecutionStatusCompleted); err != nil {
logger.Errorf("更新计划执行日志 %d 状态为 '完成' 失败: %v", planLogID, err)
}
// 6. 调用共享的 Manager 来处理触发器更新逻辑
// 只有当计划在本次执行后仍然是 Enabled 状态时,才需要创建下一次的触发器。
if newStatus == models.PlanStatusEnabled {
if err := s.analysisPlanTaskManager.CreateOrUpdateTrigger(topLevelPlanID); err != nil {
s.logger.Errorf("为计划 %d 创建/更新触发器失败: %v", topLevelPlanID, err)
if err := s.analysisPlanTaskManager.CreateOrUpdateTrigger(managerCtx, topLevelPlanID); err != nil {
logger.Errorf("为计划 %d 创建/更新触发器失败: %v", topLevelPlanID, err)
}
} else {
s.logger.Infof("计划 %d 状态为 '%d',无需创建下一次触发器。", topLevelPlanID, newStatus)
logger.Infof("计划 %d 状态为 '%d',无需创建下一次触发器。", topLevelPlanID, newStatus)
}
}

View File

@@ -1,12 +1,14 @@
package plan
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"gorm.io/gorm"
)
@@ -30,80 +32,84 @@ var (
// Service 定义了计划领域服务的接口。
type Service interface {
// Start 启动计划相关的后台服务,例如计划执行管理器。
Start()
Start(ctx context.Context)
// Stop 停止计划相关的后台服务,例如计划执行管理器。
Stop()
Stop(ctx context.Context)
// RefreshPlanTriggers 刷新计划触发器,同步数据库中的计划状态和待执行队列中的触发器任务。
RefreshPlanTriggers() error
RefreshPlanTriggers(ctx context.Context) error
// CreatePlan 创建一个新的计划
CreatePlan(plan *models.Plan) (*models.Plan, error)
CreatePlan(ctx context.Context, plan *models.Plan) (*models.Plan, error)
// GetPlanByID 根据ID获取计划详情
GetPlanByID(id uint) (*models.Plan, error)
GetPlanByID(ctx context.Context, id uint) (*models.Plan, error)
// ListPlans 获取计划列表,支持过滤和分页
ListPlans(opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)
ListPlans(ctx context.Context, opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)
// UpdatePlan 更新计划
UpdatePlan(plan *models.Plan) (*models.Plan, error)
UpdatePlan(ctx context.Context, plan *models.Plan) (*models.Plan, error)
// DeletePlan 删除计划(软删除)
DeletePlan(id uint) error
DeletePlan(ctx context.Context, id uint) error
// StartPlan 启动计划
StartPlan(id uint) error
StartPlan(ctx context.Context, id uint) error
// StopPlan 停止计划
StopPlan(id uint) error
StopPlan(ctx context.Context, id uint) error
}
// planServiceImpl 是 Service 接口的具体实现。
type planServiceImpl struct {
ctx context.Context
executionManager ExecutionManager
taskManager AnalysisPlanTaskManager
planRepo repository.PlanRepository
deviceRepo repository.DeviceRepository
unitOfWork repository.UnitOfWork
taskFactory TaskFactory
logger *logs.Logger
}
// NewPlanService 创建一个新的 Service 实例。
func NewPlanService(
ctx context.Context,
executionManager ExecutionManager,
taskManager AnalysisPlanTaskManager,
planRepo repository.PlanRepository,
deviceRepo repository.DeviceRepository,
unitOfWork repository.UnitOfWork,
taskFactory TaskFactory,
logger *logs.Logger,
) Service {
return &planServiceImpl{
ctx: ctx,
executionManager: executionManager,
taskManager: taskManager,
planRepo: planRepo,
deviceRepo: deviceRepo,
unitOfWork: unitOfWork,
taskFactory: taskFactory,
logger: logger,
}
}
// Start 启动计划相关的后台服务。
func (s *planServiceImpl) Start() {
s.logger.Infof("PlanService 正在启动...")
s.executionManager.Start()
func (s *planServiceImpl) Start(ctx context.Context) {
planCtx, logger := logs.Trace(ctx, s.ctx, "Start")
logger.Infof("PlanService 正在启动...")
s.executionManager.Start(planCtx)
}
// Stop 停止计划相关的后台服务。
func (s *planServiceImpl) Stop() {
s.logger.Infof("PlanService 正在停止...")
s.executionManager.Stop()
func (s *planServiceImpl) Stop(ctx context.Context) {
planCtx, logger := logs.Trace(ctx, s.ctx, "Stop")
logger.Infof("PlanService 正在停止...")
s.executionManager.Stop(planCtx)
}
// RefreshPlanTriggers 刷新计划触发器。
func (s *planServiceImpl) RefreshPlanTriggers() error {
s.logger.Infof("PlanService 正在刷新计划触发器...")
return s.taskManager.Refresh()
func (s *planServiceImpl) RefreshPlanTriggers(ctx context.Context) error {
planCtx, logger := logs.Trace(ctx, s.ctx, "RefreshPlanTriggers")
logger.Infof("PlanService 正在刷新计划触发器...")
return s.taskManager.Refresh(planCtx)
}
// CreatePlan 创建一个新的计划
func (s *planServiceImpl) CreatePlan(planToCreate *models.Plan) (*models.Plan, error) {
func (s *planServiceImpl) CreatePlan(ctx context.Context, planToCreate *models.Plan) (*models.Plan, error) {
planCtx, logger := logs.Trace(ctx, s.ctx, "CreatePlan")
const actionType = "领域层:创建计划"
// 1. 业务规则处理
@@ -119,7 +125,7 @@ func (s *planServiceImpl) CreatePlan(planToCreate *models.Plan) (*models.Plan, e
// 2. 验证和重排顺序 (领域逻辑)
if err := planToCreate.ValidateExecutionOrder(); err != nil {
s.logger.Errorf("%s: 计划 (ID: %d) 的执行顺序无效: %v", actionType, planToCreate.ID, err)
logger.Errorf("%s: 计划 (ID: %d) 的执行顺序无效: %v", actionType, planToCreate.ID, err)
return nil, err
}
planToCreate.ReorderSteps()
@@ -128,14 +134,14 @@ func (s *planServiceImpl) CreatePlan(planToCreate *models.Plan) (*models.Plan, e
for i := range planToCreate.Tasks {
taskModel := &planToCreate.Tasks[i]
// 使用工厂创建临时领域对象
taskResolver, err := s.taskFactory.CreateTaskFromModel(taskModel)
taskResolver, err := s.taskFactory.CreateTaskFromModel(planCtx, taskModel)
if err != nil {
// 如果一个任务类型不支持,我们可以选择跳过或报错
s.logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
continue
}
deviceIDs, err := taskResolver.ResolveDeviceIDs()
deviceIDs, err := taskResolver.ResolveDeviceIDs(planCtx)
if err != nil {
// 在事务外解析失败,直接返回错误
return nil, fmt.Errorf("为任务 '%s' 提取设备ID失败: %w", taskModel.Name, err)
@@ -151,71 +157,74 @@ func (s *planServiceImpl) CreatePlan(planToCreate *models.Plan) (*models.Plan, e
}
// 4. 调用仓库方法创建计划,该方法内部会处理事务
err := s.planRepo.CreatePlan(planToCreate)
err := s.planRepo.CreatePlan(planCtx, planToCreate)
if err != nil {
s.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
return nil, err
}
// 5. 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
if err := s.taskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil {
if err := s.taskManager.EnsureAnalysisTaskDefinition(planCtx, planToCreate.ID); err != nil {
// 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功
s.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err)
logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err)
}
s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID)
logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID)
return planToCreate, nil
}
// GetPlanByID 根据ID获取计划详情
func (s *planServiceImpl) GetPlanByID(id uint) (*models.Plan, error) {
func (s *planServiceImpl) GetPlanByID(ctx context.Context, id uint) (*models.Plan, error) {
planCtx, logger := logs.Trace(ctx, s.ctx, "GetPlanByID")
const actionType = "领域层:获取计划详情"
plan, err := s.planRepo.GetPlanByID(id)
plan, err := s.planRepo.GetPlanByID(planCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
return nil, ErrPlanNotFound
}
s.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id)
return nil, err
}
s.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id)
return plan, nil
}
// ListPlans 获取计划列表,支持过滤和分页
func (s *planServiceImpl) ListPlans(opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) {
func (s *planServiceImpl) ListPlans(ctx context.Context, opts repository.ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) {
planCtx, logger := logs.Trace(ctx, s.ctx, "ListPlans")
const actionType = "领域层:获取计划列表"
plans, total, err := s.planRepo.ListPlans(opts, page, pageSize)
plans, total, err := s.planRepo.ListPlans(planCtx, opts, page, pageSize)
if err != nil {
s.logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
logger.Errorf("%s: 数据库查询失败: %v", actionType, err)
return nil, 0, err
}
s.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(plans))
logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(plans))
return plans, total, nil
}
// UpdatePlan 更新计划
func (s *planServiceImpl) UpdatePlan(planToUpdate *models.Plan) (*models.Plan, error) {
func (s *planServiceImpl) UpdatePlan(ctx context.Context, planToUpdate *models.Plan) (*models.Plan, error) {
planCtx, logger := logs.Trace(ctx, s.ctx, "UpdatePlan")
const actionType = "领域层:更新计划"
existingPlan, err := s.planRepo.GetBasicPlanByID(planToUpdate.ID)
existingPlan, err := s.planRepo.GetBasicPlanByID(planCtx, planToUpdate.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, planToUpdate.ID)
logger.Warnf("%s: 计划不存在, ID: %d", actionType, planToUpdate.ID)
return nil, ErrPlanNotFound
}
s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, planToUpdate.ID)
logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, planToUpdate.ID)
return nil, err
}
// 系统计划不允许修改
if existingPlan.PlanType == models.PlanTypeSystem {
s.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, planToUpdate.ID)
logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, planToUpdate.ID)
return nil, ErrPlanCannotBeModified
}
@@ -228,25 +237,25 @@ func (s *planServiceImpl) UpdatePlan(planToUpdate *models.Plan) (*models.Plan, e
// 验证和重排顺序 (领域逻辑)
if err := planToUpdate.ValidateExecutionOrder(); err != nil {
s.logger.Errorf("%s: 计划 (ID: %d) 的执行顺序无效: %v", actionType, planToUpdate.ID, err)
logger.Errorf("%s: 计划 (ID: %d) 的执行顺序无效: %v", actionType, planToUpdate.ID, err)
return nil, err
}
planToUpdate.ReorderSteps()
// 只要是更新任务,就重置执行计数器
planToUpdate.ExecuteCount = 0
s.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID)
logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID)
// 在调用仓库前,准备好所有数据,包括设备关联
for i := range planToUpdate.Tasks {
taskModel := &planToUpdate.Tasks[i]
taskResolver, err := s.taskFactory.CreateTaskFromModel(taskModel)
taskResolver, err := s.taskFactory.CreateTaskFromModel(planCtx, taskModel)
if err != nil {
s.logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
continue
}
deviceIDs, err := taskResolver.ResolveDeviceIDs()
deviceIDs, err := taskResolver.ResolveDeviceIDs(planCtx)
if err != nil {
return nil, fmt.Errorf("为任务 '%s' 提取设备ID失败: %w", taskModel.Name, err)
}
@@ -261,85 +270,87 @@ func (s *planServiceImpl) UpdatePlan(planToUpdate *models.Plan) (*models.Plan, e
}
// 调用仓库方法更新计划,该方法内部会处理事务
err = s.planRepo.UpdatePlanMetadataAndStructure(planToUpdate)
err = s.planRepo.UpdatePlanMetadataAndStructure(planCtx, planToUpdate)
if err != nil {
s.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
return nil, err
}
if err := s.taskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil {
s.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err)
if err := s.taskManager.EnsureAnalysisTaskDefinition(planCtx, planToUpdate.ID); err != nil {
logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err)
}
updatedPlan, err := s.planRepo.GetPlanByID(planToUpdate.ID)
updatedPlan, err := s.planRepo.GetPlanByID(planCtx, planToUpdate.ID)
if err != nil {
s.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, planToUpdate.ID)
logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, planToUpdate.ID)
return nil, errors.New("获取更新后计划详情时发生内部错误")
}
s.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID)
return updatedPlan, nil
}
// DeletePlan 删除计划(软删除)
func (s *planServiceImpl) DeletePlan(id uint) error {
func (s *planServiceImpl) DeletePlan(ctx context.Context, id uint) error {
planCtx, logger := logs.Trace(ctx, s.ctx, "DeletePlan")
const actionType = "领域层:删除计划"
plan, err := s.planRepo.GetBasicPlanByID(id)
plan, err := s.planRepo.GetBasicPlanByID(planCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
return ErrPlanNotFound
}
s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
return err
}
// 系统计划不允许删除
if plan.PlanType == models.PlanTypeSystem {
s.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id)
logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id)
return ErrPlanCannotBeDeleted
}
// 如果计划处于启用状态,先停止它
if plan.Status == models.PlanStatusEnabled {
if err := s.planRepo.StopPlanTransactionally(id); err != nil {
s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
if err := s.planRepo.StopPlanTransactionally(planCtx, id); err != nil {
logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
return err
}
}
if err := s.planRepo.DeletePlan(id); err != nil {
s.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id)
if err := s.planRepo.DeletePlan(planCtx, id); err != nil {
logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id)
return err
}
s.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
logger.Infof("%s: 计划删除成功, ID: %d", actionType, id)
return nil
}
// StartPlan 启动计划
func (s *planServiceImpl) StartPlan(id uint) error {
func (s *planServiceImpl) StartPlan(ctx context.Context, id uint) error {
planCtx, logger := logs.Trace(ctx, s.ctx, "StartPlan")
const actionType = "领域层:启动计划"
plan, err := s.planRepo.GetBasicPlanByID(id)
plan, err := s.planRepo.GetBasicPlanByID(planCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
return ErrPlanNotFound
}
s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
return err
}
// 系统计划不允许手动启动
if plan.PlanType == models.PlanTypeSystem {
s.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id)
logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id)
return ErrPlanCannotBeStarted
}
// 计划已处于启动状态,无需重复操作
if plan.Status == models.PlanStatusEnabled {
s.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id)
logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id)
return ErrPlanAlreadyEnabled
}
@@ -347,63 +358,64 @@ func (s *planServiceImpl) StartPlan(id uint) error {
if plan.Status != models.PlanStatusEnabled {
// 如果执行计数器大于0重置为0
if plan.ExecuteCount > 0 {
if err := s.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil {
s.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID)
if err := s.planRepo.UpdateExecuteCount(planCtx, plan.ID, 0); err != nil {
logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID)
return err
}
s.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID)
logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID)
}
// 更新计划状态为启用
if err := s.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil {
s.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID)
if err := s.planRepo.UpdatePlanStatus(planCtx, plan.ID, models.PlanStatusEnabled); err != nil {
logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID)
return err
}
s.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID)
logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID)
}
// 创建或更新触发器
if err := s.taskManager.CreateOrUpdateTrigger(plan.ID); err != nil {
s.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID)
if err := s.taskManager.CreateOrUpdateTrigger(planCtx, plan.ID); err != nil {
logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID)
return err
}
s.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id)
return nil
}
// StopPlan 停止计划
func (s *planServiceImpl) StopPlan(id uint) error {
func (s *planServiceImpl) StopPlan(ctx context.Context, id uint) error {
planCtx, logger := logs.Trace(ctx, s.ctx, "StopPlan")
const actionType = "领域层:停止计划"
plan, err := s.planRepo.GetBasicPlanByID(id)
plan, err := s.planRepo.GetBasicPlanByID(planCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
logger.Warnf("%s: 计划不存在, ID: %d", actionType, id)
return ErrPlanNotFound
}
s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id)
return err
}
// 系统计划不允许停止
if plan.PlanType == models.PlanTypeSystem {
s.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id)
logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id)
return ErrPlanCannotBeStopped
}
// 计划当前不是启用状态
if plan.Status != models.PlanStatusEnabled {
s.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status)
logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status)
return ErrPlanNotEnabled
}
// 停止计划事务性操作
if err := s.planRepo.StopPlanTransactionally(id); err != nil {
s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
if err := s.planRepo.StopPlanTransactionally(planCtx, id); err != nil {
logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id)
return err
}
s.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id)
return nil
}

View File

@@ -1,6 +1,10 @@
package plan
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// Task 定义了所有可被调度器执行的任务必须实现的接口。
type Task interface {
@@ -8,12 +12,12 @@ type Task interface {
// ctx: 用于控制任务的超时或取消。
// log: 包含了当前任务执行的完整上下文信息,包括从数据库中加载的任务参数等。
// 返回的 error 表示任务是否执行成功。调度器会根据返回的 error 是否为 nil 来决定任务状态。
Execute() error
Execute(ctx context.Context) error
// OnFailure 定义了当 Execute 方法返回错误时,需要执行的回滚或清理逻辑。
// log: 任务执行的上下文。
// executeErr: 从 Execute 方法返回的原始错误。
OnFailure(executeErr error)
OnFailure(ctx context.Context, executeErr error)
TaskDeviceIDResolver
}
@@ -22,13 +26,13 @@ type Task interface {
type TaskDeviceIDResolver interface {
// ResolveDeviceIDs 从任务配置中解析并返回所有关联的设备ID列表
// 返回值: uint数组每个字符串代表一个设备ID
ResolveDeviceIDs() ([]uint, error)
ResolveDeviceIDs(ctx context.Context) ([]uint, error)
}
// TaskFactory 是一个工厂接口,用于根据任务执行日志创建任务实例。
type TaskFactory interface {
// Production 根据指定的任务执行日志创建一个任务实例。
Production(claimedLog *models.TaskExecutionLog) Task
Production(ctx context.Context, claimedLog *models.TaskExecutionLog) Task
// CreateTaskFromModel 仅根据任务模型创建一个任务实例,用于非执行场景(如参数解析)。
CreateTaskFromModel(taskModel *models.Task) (TaskDeviceIDResolver, error)
CreateTaskFromModel(ctx context.Context, taskModel *models.Task) (TaskDeviceIDResolver, error)
}

View File

@@ -1,6 +1,7 @@
package task
import (
"context"
"fmt"
"time"
@@ -15,45 +16,47 @@ type DelayTaskParams struct {
// DelayTask 是一个用于模拟延迟的 Task 实现
type DelayTask struct {
ctx context.Context
executionTask *models.TaskExecutionLog
duration time.Duration
logger *logs.Logger
}
func NewDelayTask(logger *logs.Logger, executionTask *models.TaskExecutionLog) plan.Task {
func NewDelayTask(ctx context.Context, executionTask *models.TaskExecutionLog) plan.Task {
return &DelayTask{
ctx: ctx,
executionTask: executionTask,
logger: logger,
}
}
// Execute 执行延迟任务,等待指定的时间
func (d *DelayTask) Execute() error {
if err := d.parseParameters(); err != nil {
func (d *DelayTask) Execute(ctx context.Context) error {
taskCtx, logger := logs.Trace(ctx, d.ctx, "Execute")
if err := d.parseParameters(taskCtx); err != nil {
return err
}
d.logger.Infof("任务 %v: 开始延迟 %v...", d.executionTask.TaskID, d.duration)
logger.Infof("任务 %v: 开始延迟 %v...", d.executionTask.TaskID, d.duration)
time.Sleep(d.duration)
d.logger.Infof("任务 %v: 延迟结束。", d.executionTask.TaskID)
logger.Infof("任务 %v: 延迟结束。", d.executionTask.TaskID)
return nil
}
func (d *DelayTask) parseParameters() error {
func (d *DelayTask) parseParameters(ctx context.Context) error {
logger := logs.TraceLogger(ctx, d.ctx, "parseParameters")
if d.executionTask.Task.Parameters == nil {
d.logger.Errorf("任务 %v: 缺少参数", d.executionTask.TaskID)
logger.Errorf("任务 %v: 缺少参数", d.executionTask.TaskID)
return fmt.Errorf("任务 %v: 参数不全", d.executionTask.TaskID)
}
var params DelayTaskParams
err := d.executionTask.Task.ParseParameters(&params)
if err != nil {
d.logger.Errorf("任务 %v: 解析参数失败: %v", d.executionTask.TaskID, err)
logger.Errorf("任务 %v: 解析参数失败: %v", d.executionTask.TaskID, err)
return fmt.Errorf("任务 %v: 解析参数失败: %v", d.executionTask.TaskID, err)
}
if params.DelayDuration <= 0 {
d.logger.Errorf("任务 %v: 参数 delay_duration 缺失或无效 (必须大于0)", d.executionTask.TaskID)
logger.Errorf("任务 %v: 参数 delay_duration 缺失或无效 (必须大于0)", d.executionTask.TaskID)
return fmt.Errorf("任务 %v: 参数 delay_duration 缺失或无效 (必须大于0)", d.executionTask.TaskID)
}
@@ -62,10 +65,11 @@ func (d *DelayTask) parseParameters() error {
return nil
}
func (d *DelayTask) OnFailure(executeErr error) {
d.logger.Errorf("任务 %v: 执行失败: %v", d.executionTask.TaskID, executeErr)
func (d *DelayTask) OnFailure(ctx context.Context, executeErr error) {
logger := logs.TraceLogger(ctx, d.ctx, "OnFailure")
logger.Errorf("任务 %v: 执行失败: %v", d.executionTask.TaskID, executeErr)
}
func (d *DelayTask) ResolveDeviceIDs() ([]uint, error) {
func (d *DelayTask) ResolveDeviceIDs(ctx context.Context) ([]uint, error) {
return []uint{}, nil
}

View File

@@ -1,6 +1,7 @@
package task
import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
@@ -12,38 +13,39 @@ import (
// FullCollectionTask 实现了 plan.Task 接口,用于执行一次全量的设备数据采集
type FullCollectionTask struct {
ctx context.Context
log *models.TaskExecutionLog
deviceRepo repository.DeviceRepository
deviceService device.Service
logger *logs.Logger
}
// NewFullCollectionTask 创建一个全量采集任务实例
func NewFullCollectionTask(
ctx context.Context,
log *models.TaskExecutionLog,
deviceRepo repository.DeviceRepository,
deviceService device.Service,
logger *logs.Logger,
) plan.Task {
return &FullCollectionTask{
ctx: ctx,
log: log,
deviceRepo: deviceRepo,
deviceService: deviceService,
logger: logger,
}
}
// Execute 是任务的核心执行逻辑
func (t *FullCollectionTask) Execute() error {
t.logger.Infow("开始执行全量采集任务", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
func (t *FullCollectionTask) Execute(ctx context.Context) error {
taskCtx, logger := logs.Trace(ctx, t.ctx, "Execute")
logger.Infow("开始执行全量采集任务", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
sensors, err := t.deviceRepo.ListAllSensors()
sensors, err := t.deviceRepo.ListAllSensors(taskCtx)
if err != nil {
return fmt.Errorf("全量采集任务: 从数据库获取所有传感器失败: %w", err)
}
if len(sensors) == 0 {
t.logger.Infow("全量采集任务: 未发现任何传感器设备,跳过本次采集", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
logger.Infow("全量采集任务: 未发现任何传感器设备,跳过本次采集", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
return nil
}
@@ -54,15 +56,15 @@ func (t *FullCollectionTask) Execute() error {
var firstError error
for controllerID, controllerSensors := range sensorsByController {
t.logger.Infow("全量采集任务: 准备为区域主控下的传感器下发采集指令",
logger.Infow("全量采集任务: 准备为区域主控下的传感器下发采集指令",
"task_id", t.log.TaskID,
"task_type", t.log.Task.Type,
"log_id", t.log.ID,
"controller_id", controllerID,
"sensor_count", len(controllerSensors),
)
if err := t.deviceService.Collect(controllerID, controllerSensors); err != nil {
t.logger.Errorw("全量采集任务: 为区域主控下发采集指令失败",
if err := t.deviceService.Collect(taskCtx, controllerID, controllerSensors); err != nil {
logger.Errorw("全量采集任务: 为区域主控下发采集指令失败",
"task_id", t.log.TaskID,
"task_type", t.log.Task.Type,
"log_id", t.log.ID,
@@ -79,13 +81,14 @@ func (t *FullCollectionTask) Execute() error {
return fmt.Errorf("全量采集任务执行期间发生错误: %w", firstError)
}
t.logger.Infow("全量采集任务执行完成", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
logger.Infow("全量采集任务执行完成", "task_id", t.log.TaskID, "task_type", t.log.Task.Type, "log_id", t.log.ID)
return nil
}
// OnFailure 定义了当 Execute 方法返回错误时,需要执行的回滚或清理逻辑
func (t *FullCollectionTask) OnFailure(executeErr error) {
t.logger.Errorw("全量采集任务执行失败",
func (t *FullCollectionTask) OnFailure(ctx context.Context, executeErr error) {
logger := logs.TraceLogger(ctx, t.ctx, "OnFailure")
logger.Errorw("全量采集任务执行失败",
"task_id", t.log.TaskID,
"task_type", t.log.Task.Type,
"log_id", t.log.ID,
@@ -94,7 +97,7 @@ func (t *FullCollectionTask) OnFailure(executeErr error) {
}
// ResolveDeviceIDs 获取当前任务需要使用的设备ID列表
func (t *FullCollectionTask) ResolveDeviceIDs() ([]uint, error) {
func (t *FullCollectionTask) ResolveDeviceIDs(ctx context.Context) ([]uint, error) {
// 全量采集任务不和任何设备绑定, 每轮采集都会重新获取全量传感器
return []uint{}, nil
}

View File

@@ -1,6 +1,7 @@
package task
import (
"context"
"encoding/json"
"fmt"
"sync"
@@ -22,6 +23,8 @@ type ReleaseFeedWeightTaskParams struct {
// ReleaseFeedWeightTask 是一个控制下料口释放指定重量的任务
type ReleaseFeedWeightTask struct {
ctx context.Context
deviceRepo repository.DeviceRepository
sensorDataRepo repository.SensorDataRepository
claimedLog *models.TaskExecutionLog
@@ -34,40 +37,39 @@ type ReleaseFeedWeightTask struct {
// onceParse 保证解析参数只执行一次
onceParse sync.Once
logger *logs.Logger
}
// NewReleaseFeedWeightTask 创建一个新的 ReleaseFeedWeightTask 实例
func NewReleaseFeedWeightTask(
ctx context.Context,
claimedLog *models.TaskExecutionLog,
sensorDataRepo repository.SensorDataRepository,
deviceRepo repository.DeviceRepository,
deviceService device.Service,
logger *logs.Logger,
) plan.Task {
return &ReleaseFeedWeightTask{
ctx: ctx,
claimedLog: claimedLog,
deviceRepo: deviceRepo,
sensorDataRepo: sensorDataRepo,
feedPort: deviceService,
logger: logger,
}
}
func (r *ReleaseFeedWeightTask) Execute() error {
r.logger.Infof("任务 %v: 开始执行, 日志ID: %v", r.claimedLog.TaskID, r.claimedLog.ID)
if err := r.parseParameters(); err != nil {
func (r *ReleaseFeedWeightTask) Execute(ctx context.Context) error {
taskCtx, logger := logs.Trace(ctx, r.ctx, "Execute")
logger.Infof("任务 %v: 开始执行, 日志ID: %v", r.claimedLog.TaskID, r.claimedLog.ID)
if err := r.parseParameters(taskCtx); err != nil {
return err
}
weight, err := r.getNowWeight()
weight, err := r.getNowWeight(taskCtx)
if err != nil {
return err
}
if err = r.feedPort.Switch(r.feedPortDevice, device.DeviceActionStart); err != nil {
r.logger.Errorf("启动下料口(id=%v)失败: %v , 日志ID: %v", r.feedPortDevice.ID, err, r.claimedLog.ID)
if err = r.feedPort.Switch(taskCtx, r.feedPortDevice, device.DeviceActionStart); err != nil {
logger.Errorf("启动下料口(id=%v)失败: %v , 日志ID: %v", r.feedPortDevice.ID, err, r.claimedLog.ID)
return err
}
@@ -76,33 +78,34 @@ func (r *ReleaseFeedWeightTask) Execute() error {
// TODO 这个判断有延迟, 尤其是LoRa通信本身延迟较高, 可以考虑根据信号质量或其他指标提前发送停止命令
for targetWeight <= weight {
weight, err = r.getNowWeight()
weight, err = r.getNowWeight(taskCtx)
if err != nil {
errCount++
if errCount > 3 { // 如果连续三次没成功采集到重量数据,则认为计划执行失败
r.logger.Errorf("获取当前计划执行日志(id=%v)的当前搅拌罐重量失败: %v, 任务结束", r.claimedLog.ID, err)
logger.Errorf("获取当前计划执行日志(id=%v)的当前搅拌罐重量失败: %v, 任务结束", r.claimedLog.ID, err)
return err
}
r.logger.Warnf("第%v次尝试获取当前计划执行日志(id=%v)的当前搅拌罐重量失败: %v", errCount, r.claimedLog.ID, err)
logger.Warnf("第%v次尝试获取当前计划执行日志(id=%v)的当前搅拌罐重量失败: %v", errCount, r.claimedLog.ID, err)
continue
}
time.Sleep(100 * time.Millisecond)
}
if err = r.feedPort.Switch(r.feedPortDevice, device.DeviceActionStop); err != nil {
r.logger.Errorf("关闭下料口(id=%v)失败: %v , 日志ID: %v", r.feedPortDevice.ID, err, r.claimedLog.ID)
if err = r.feedPort.Switch(taskCtx, r.feedPortDevice, device.DeviceActionStop); err != nil {
logger.Errorf("关闭下料口(id=%v)失败: %v , 日志ID: %v", r.feedPortDevice.ID, err, r.claimedLog.ID)
return err
}
r.logger.Infof("完成计划执行日志(id=%v)的当前计划, 完成下料 %vkg, 搅拌罐剩余重量 %vkg", r.claimedLog.ID, r.releaseWeight, weight)
logger.Infof("完成计划执行日志(id=%v)的当前计划, 完成下料 %vkg, 搅拌罐剩余重量 %vkg", r.claimedLog.ID, r.releaseWeight, weight)
return nil
}
// 获取当前搅拌罐重量
func (r *ReleaseFeedWeightTask) getNowWeight() (float64, error) {
sensorData, err := r.sensorDataRepo.GetLatestSensorDataByDeviceIDAndSensorType(r.mixingTankDeviceID, models.SensorTypeWeight)
func (r *ReleaseFeedWeightTask) getNowWeight(ctx context.Context) (float64, error) {
taskCtx, logger := logs.Trace(ctx, r.ctx, "getNowWeight")
sensorData, err := r.sensorDataRepo.GetLatestSensorDataByDeviceIDAndSensorType(taskCtx, r.mixingTankDeviceID, models.SensorTypeWeight)
if err != nil {
r.logger.Errorf("获取设备 %v 最新传感器数据失败: %v , 日志ID: %v", r.mixingTankDeviceID, err, r.claimedLog.ID)
logger.Errorf("获取设备 %v 最新传感器数据失败: %v , 日志ID: %v", r.mixingTankDeviceID, err, r.claimedLog.ID)
return 0, err
}
@@ -113,47 +116,48 @@ func (r *ReleaseFeedWeightTask) getNowWeight() (float64, error) {
wg := &models.WeightData{}
err = json.Unmarshal(sensorData.Data, wg)
if err != nil {
r.logger.Errorf("反序列化设备 %v 最新传感器数据失败: %v , 日志ID: %v", r.mixingTankDeviceID, err, r.claimedLog.ID)
logger.Errorf("反序列化设备 %v 最新传感器数据失败: %v , 日志ID: %v", r.mixingTankDeviceID, err, r.claimedLog.ID)
return 0, err
}
return wg.WeightKilograms, nil
}
func (r *ReleaseFeedWeightTask) parseParameters() error {
func (r *ReleaseFeedWeightTask) parseParameters(ctx context.Context) error {
taskCtx, logger := logs.Trace(ctx, r.ctx, "parseParameters")
var err error
r.onceParse.Do(func() {
if r.claimedLog.Task.Parameters == nil {
r.logger.Errorf("任务 %v: 缺少参数", r.claimedLog.TaskID)
logger.Errorf("任务 %v: 缺少参数", r.claimedLog.TaskID)
err = fmt.Errorf("任务 %v: 参数不全", r.claimedLog.TaskID)
}
var params ReleaseFeedWeightTaskParams
err := r.claimedLog.Task.ParseParameters(&params)
if err != nil {
r.logger.Errorf("任务 %v: 解析参数失败: %v", r.claimedLog.TaskID, err)
logger.Errorf("任务 %v: 解析参数失败: %v", r.claimedLog.TaskID, err)
err = fmt.Errorf("任务 %v: 解析参数失败: %v", r.claimedLog.TaskID, err)
}
// 校验参数是否存在
if params.ReleaseWeight == 0 {
r.logger.Errorf("任务 %v: 参数 release_weight 缺失或无效", r.claimedLog.TaskID)
logger.Errorf("任务 %v: 参数 release_weight 缺失或无效", r.claimedLog.TaskID)
err = fmt.Errorf("任务 %v: 参数 release_weight 缺失或无效", r.claimedLog.TaskID)
}
if params.FeedPortDeviceID == 0 {
r.logger.Errorf("任务 %v: 参数 feed_port_device_id 缺失或无效", r.claimedLog.TaskID)
logger.Errorf("任务 %v: 参数 feed_port_device_id 缺失或无效", r.claimedLog.TaskID)
err = fmt.Errorf("任务 %v: 参数 feed_port_device_id 缺失或无效", r.claimedLog.TaskID)
}
if params.MixingTankDeviceID == 0 {
r.logger.Errorf("任务 %v: 参数 mixing_tank_device_id 缺失或无效", r.claimedLog.TaskID)
logger.Errorf("任务 %v: 参数 mixing_tank_device_id 缺失或无效", r.claimedLog.TaskID)
err = fmt.Errorf("任务 %v: 参数 mixing_tank_device_id 缺失或无效", r.claimedLog.TaskID)
}
r.releaseWeight = params.ReleaseWeight
r.mixingTankDeviceID = params.MixingTankDeviceID
r.feedPortDevice, err = r.deviceRepo.FindByID(params.FeedPortDeviceID)
r.feedPortDevice, err = r.deviceRepo.FindByID(taskCtx, params.FeedPortDeviceID)
if err != nil {
r.logger.Errorf("任务 %v: 获取设备信息失败: %v", r.claimedLog.TaskID, err)
logger.Errorf("任务 %v: 获取设备信息失败: %v", r.claimedLog.TaskID, err)
err = fmt.Errorf("任务 %v: 获取设备信息失败: %v", r.claimedLog.TaskID, err)
}
@@ -161,21 +165,23 @@ func (r *ReleaseFeedWeightTask) parseParameters() error {
return err
}
func (r *ReleaseFeedWeightTask) OnFailure(executeErr error) {
r.logger.Errorf("开始善后处理, 日志ID:%v; 错误信息: %v", r.claimedLog.ID, executeErr)
func (r *ReleaseFeedWeightTask) OnFailure(ctx context.Context, executeErr error) {
taskCtx, logger := logs.Trace(ctx, r.ctx, "OnFailure")
logger.Errorf("开始善后处理, 日志ID:%v; 错误信息: %v", r.claimedLog.ID, executeErr)
if r.feedPort != nil {
err := r.feedPort.Switch(r.feedPortDevice, device.DeviceActionStop)
err := r.feedPort.Switch(taskCtx, r.feedPortDevice, device.DeviceActionStop)
if err != nil {
r.logger.Errorf("[严重] 下料口停止失败, 日志ID: %v, 错误: %v", r.claimedLog.ID, err)
logger.Errorf("[严重] 下料口停止失败, 日志ID: %v, 错误: %v", r.claimedLog.ID, err)
}
} else {
r.logger.Warnf("[警告] 下料口通信器尚未初始化, 不进行任何操作, 日志ID: %v", r.claimedLog.ID)
logger.Warnf("[警告] 下料口通信器尚未初始化, 不进行任何操作, 日志ID: %v", r.claimedLog.ID)
}
r.logger.Errorf("善后处理完成, 日志ID:%v", r.claimedLog.ID)
logger.Errorf("善后处理完成, 日志ID:%v", r.claimedLog.ID)
}
func (r *ReleaseFeedWeightTask) ResolveDeviceIDs() ([]uint, error) {
if err := r.parseParameters(); err != nil {
func (r *ReleaseFeedWeightTask) ResolveDeviceIDs(ctx context.Context) ([]uint, 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

View File

@@ -1,6 +1,7 @@
package task
import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
@@ -10,61 +11,70 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
)
const (
CompNameDelayTask = "DelayTask"
CompNameReleaseFeedWeight = "ReleaseFeedWeightTask"
CompNameFullCollectionTask = "FullCollectionTask"
)
type taskFactory struct {
logger *logs.Logger
ctx context.Context
sensorDataRepo repository.SensorDataRepository
deviceRepo repository.DeviceRepository
deviceService device.Service
}
func NewTaskFactory(
logger *logs.Logger,
ctx context.Context,
sensorDataRepo repository.SensorDataRepository,
deviceRepo repository.DeviceRepository,
deviceService device.Service,
) plan.TaskFactory {
return &taskFactory{
logger: logger,
ctx: ctx,
sensorDataRepo: sensorDataRepo,
deviceRepo: deviceRepo,
deviceService: deviceService,
}
}
func (t *taskFactory) Production(claimedLog *models.TaskExecutionLog) plan.Task {
func (t *taskFactory) Production(ctx context.Context, claimedLog *models.TaskExecutionLog) plan.Task {
logger := logs.TraceLogger(ctx, t.ctx, "Production")
baseCtx := context.Background()
switch claimedLog.Task.Type {
case models.TaskTypeWaiting:
return NewDelayTask(t.logger, claimedLog)
return NewDelayTask(logs.AddCompName(baseCtx, CompNameDelayTask), claimedLog)
case models.TaskTypeReleaseFeedWeight:
return NewReleaseFeedWeightTask(claimedLog, t.sensorDataRepo, t.deviceRepo, t.deviceService, t.logger)
return NewReleaseFeedWeightTask(logs.AddCompName(baseCtx, CompNameReleaseFeedWeight), claimedLog, t.sensorDataRepo, t.deviceRepo, t.deviceService)
case models.TaskTypeFullCollection:
return NewFullCollectionTask(claimedLog, t.deviceRepo, t.deviceService, t.logger)
return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), claimedLog, t.deviceRepo, t.deviceService)
default:
// TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型
t.logger.Panicf("不支持的任务类型: %s", claimedLog.Task.Type)
logger.Panicf("不支持的任务类型: %s", claimedLog.Task.Type)
panic("不支持的任务类型") // 显式panic防编译器报错
}
}
// CreateTaskFromModel 实现了 TaskFactory 接口,用于从模型创建任务实例。
func (t *taskFactory) CreateTaskFromModel(taskModel *models.Task) (plan.TaskDeviceIDResolver, error) {
func (t *taskFactory) CreateTaskFromModel(ctx context.Context, taskModel *models.Task) (plan.TaskDeviceIDResolver, error) {
// 这个方法不关心 claimedLog 的其他字段,所以可以构造一个临时的
// 它只用于访问那些不依赖于执行日志的方法,比如 ResolveDeviceIDs
tempLog := &models.TaskExecutionLog{Task: *taskModel}
baseCtx := context.Background()
switch taskModel.Type {
case models.TaskTypeWaiting:
return NewDelayTask(t.logger, tempLog), nil
return NewDelayTask(logs.AddCompName(baseCtx, CompNameDelayTask), tempLog), nil
case models.TaskTypeReleaseFeedWeight:
return NewReleaseFeedWeightTask(
logs.AddCompName(baseCtx, CompNameReleaseFeedWeight),
tempLog,
t.sensorDataRepo,
t.deviceRepo,
t.deviceService,
t.logger,
), nil
case models.TaskTypeFullCollection:
return NewFullCollectionTask(tempLog, t.deviceRepo, t.deviceService, t.logger), nil
return NewFullCollectionTask(logs.AddCompName(baseCtx, CompNameFullCollectionTask), tempLog, t.deviceRepo, t.deviceService), nil
default:
return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type)
}

View File

@@ -4,12 +4,13 @@
package database
import (
"context"
"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/logs"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
@@ -17,35 +18,36 @@ import (
// PostgresStorage 代表基于PostgreSQL的存储实现
// 使用GORM作为ORM库
type PostgresStorage struct {
ctx context.Context
db *gorm.DB
isTimescaleDB bool
connectionString string
maxOpenConns int
maxIdleConns int
connMaxLifetime int
logger *logs.Logger // 依赖注入的 logger
}
// NewPostgresStorage 创建并返回一个新的PostgreSQL存储实例
// 它接收一个 logger 实例,而不是自己创建
func NewPostgresStorage(connectionString string, isTimescaleDB bool, maxOpenConns, maxIdleConns, connMaxLifetime int, logger *logs.Logger) *PostgresStorage {
func NewPostgresStorage(ctx context.Context, connectionString string, isTimescaleDB bool, maxOpenConns, maxIdleConns, connMaxLifetime int) *PostgresStorage {
return &PostgresStorage{
ctx: ctx,
connectionString: connectionString,
isTimescaleDB: isTimescaleDB,
maxOpenConns: maxOpenConns,
maxIdleConns: maxIdleConns,
connMaxLifetime: connMaxLifetime,
logger: logger, // 注入 logger
}
}
// Connect 建立与PostgreSQL数据库的连接
// 使用GORM建立数据库连接并使用自定义的 logger 接管 GORM 日志
func (ps *PostgresStorage) Connect() error {
ps.logger.Info("正在连接PostgreSQL数据库")
func (ps *PostgresStorage) Connect(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "Connect")
logger.Info("正在连接PostgreSQL数据库")
// 创建 GORM 的 logger 适配器
gormLogger := logs.NewGormLogger(ps.logger)
gormLogger := logs.NewGormLogger(logs.GetLogger(logs.AddCompName(context.Background(), "GORM")))
var err error
// 在 gorm.Open 时传入我们自定义的 logger
@@ -53,19 +55,19 @@ func (ps *PostgresStorage) Connect() error {
Logger: gormLogger,
})
if err != nil {
ps.logger.Errorw("数据库连接失败", "error", err)
logger.Errorw("数据库连接失败", "error", err)
return fmt.Errorf("数据库连接失败: %w", err) // 使用 %w 进行错误包装
}
// 测试连接
sqlDB, err := ps.db.DB()
sqlDB, err := ps.db.WithContext(storageCtx).DB()
if err != nil {
ps.logger.Errorw("获取数据库实例失败", "error", err)
logger.Errorw("获取数据库实例失败", "error", err)
return fmt.Errorf("获取数据库实例失败: %w", err)
}
if err = sqlDB.Ping(); err != nil {
ps.logger.Errorw("数据库连接测试失败", "error", err)
logger.Errorw("数据库连接测试失败", "error", err)
return fmt.Errorf("数据库连接测试失败: %w", err)
}
@@ -77,59 +79,62 @@ func (ps *PostgresStorage) Connect() error {
// gorm会根据字段名自动创建外键约束, 但触发器Task的PlanID是不存在的, 所以需要关闭, 这个关闭对
ps.db.DisableForeignKeyConstraintWhenMigrating = true
ps.logger.Info("PostgreSQL数据库连接成功")
logger.Info("PostgreSQL数据库连接成功")
return nil
}
// Disconnect 断开与PostgreSQL数据库的连接
// 安全地关闭所有数据库连接
func (ps *PostgresStorage) Disconnect() error {
func (ps *PostgresStorage) Disconnect(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "Disconnect")
if ps.db != nil {
ps.logger.Info("正在断开PostgreSQL数据库连接")
logger.Info("正在断开PostgreSQL数据库连接")
sqlDB, err := ps.db.DB()
sqlDB, err := ps.db.WithContext(storageCtx).DB()
if err != nil {
ps.logger.Errorw("获取数据库实例失败", "error", err)
logger.Errorw("获取数据库实例失败", "error", err)
return fmt.Errorf("获取数据库实例失败: %w", err)
}
if err := sqlDB.Close(); err != nil {
ps.logger.Errorw("关闭数据库连接失败", "error", err)
logger.Errorw("关闭数据库连接失败", "error", err)
return fmt.Errorf("关闭数据库连接失败: %w", err)
}
ps.logger.Info("PostgreSQL数据库连接已断开")
logger.Info("PostgreSQL数据库连接已断开")
}
return nil
}
// GetDB 获取GORM数据库实例
// 用于执行具体的数据库操作
func (ps *PostgresStorage) GetDB() *gorm.DB {
return ps.db
func (ps *PostgresStorage) GetDB(ctx context.Context) *gorm.DB {
storageCtx := logs.AddFuncName(ctx, ps.ctx, "GetDB")
return ps.db.WithContext(storageCtx)
}
// Migrate 执行数据库迁移
func (ps *PostgresStorage) Migrate(models ...interface{}) error {
func (ps *PostgresStorage) Migrate(ctx context.Context, models ...interface{}) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "Migrate")
if len(models) == 0 {
ps.logger.Info("没有需要迁移的数据库模型,跳过迁移步骤")
logger.Info("没有需要迁移的数据库模型,跳过迁移步骤")
return nil
}
ps.logger.Info("正在自动迁移数据库表结构")
if err := ps.db.AutoMigrate(models...); err != nil {
ps.logger.Errorw("数据库表结构迁移失败", "error", err)
logger.Info("正在自动迁移数据库表结构")
if err := ps.db.WithContext(storageCtx).AutoMigrate(models...); err != nil {
logger.Errorw("数据库表结构迁移失败", "error", err)
return fmt.Errorf("数据库表结构迁移失败: %w", err)
}
ps.logger.Info("数据库表结构迁移完成")
logger.Info("数据库表结构迁移完成")
// -- 处理gorm做不到的初始化逻辑 --
if err := ps.creatingIndex(); err != nil {
if err := ps.creatingIndex(storageCtx); err != nil {
return err
}
// 如果是 TimescaleDB, 则将部分表转换为 hypertable
if ps.isTimescaleDB {
ps.logger.Info("检测到 TimescaleDB, 准备进行超表转换和压缩策略配置")
if err := ps.setupTimescaleDB(); err != nil {
logger.Info("检测到 TimescaleDB, 准备进行超表转换和压缩策略配置")
if err := ps.setupTimescaleDB(storageCtx); err != nil {
return err
}
}
@@ -137,18 +142,20 @@ func (ps *PostgresStorage) Migrate(models ...interface{}) error {
}
// setupTimescaleDB 统一处理所有 TimescaleDB 相关的设置
func (ps *PostgresStorage) setupTimescaleDB() error {
if err := ps.creatingHyperTable(); err != nil {
func (ps *PostgresStorage) setupTimescaleDB(ctx context.Context) error {
storageCtx := logs.AddFuncName(ctx, ps.ctx, "setupTimescaleDB")
if err := ps.creatingHyperTable(storageCtx); err != nil {
return err
}
if err := ps.applyCompressionPolicies(); err != nil {
if err := ps.applyCompressionPolicies(storageCtx); err != nil {
return err
}
return nil
}
// creatingHyperTable 用于在数据库是 TimescaleDB 时创建超表
func (ps *PostgresStorage) creatingHyperTable() error {
func (ps *PostgresStorage) creatingHyperTable(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingHyperTable")
// 定义一个辅助结构体来管理超表转换
tablesToConvert := []struct {
model interface{ TableName() string }
@@ -177,20 +184,21 @@ func (ps *PostgresStorage) creatingHyperTable() error {
for _, table := range tablesToConvert {
tableName := table.model.TableName()
chunkInterval := "1 days" // 统一设置为1天
ps.logger.Debugw("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval)
logger.Debugw("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval)
sql := fmt.Sprintf("SELECT create_hypertable('%s', '%s', chunk_time_interval => INTERVAL '%s', if_not_exists => TRUE);", tableName, table.timeColumn, chunkInterval)
if err := ps.db.Exec(sql).Error; err != nil {
ps.logger.Errorw("转换为超表失败", "table", tableName, "error", err)
if err := ps.db.WithContext(storageCtx).Exec(sql).Error; err != nil {
logger.Errorw("转换为超表失败", "table", tableName, "error", err)
return fmt.Errorf("将 %s 转换为超表失败: %w", tableName, err)
}
ps.logger.Debugw("成功将表转换为超表 (或已转换)", "table", tableName)
logger.Debugw("成功将表转换为超表 (或已转换)", "table", tableName)
}
return nil
}
// applyCompressionPolicies 为超表配置自动压缩策略
func (ps *PostgresStorage) applyCompressionPolicies() error {
func (ps *PostgresStorage) applyCompressionPolicies(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "applyCompressionPolicies")
policies := []struct {
model interface{ TableName() string }
segmentColumn string
@@ -220,50 +228,51 @@ func (ps *PostgresStorage) applyCompressionPolicies() error {
compressAfter := "3 days" // 统一设置为2天后即进入第3天开始压缩
// 1. 开启表的压缩设置,并指定分段列
ps.logger.Debugw("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn)
logger.Debugw("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn)
// 使用 + 而非Sprintf以规避goland静态检查报错
alterSQL := "ALTER TABLE" + " " + tableName + " SET (timescaledb.compress, timescaledb.compress_segmentby = '" + policy.segmentColumn + "');"
if err := ps.db.Exec(alterSQL).Error; err != nil {
if err := ps.db.WithContext(storageCtx).Exec(alterSQL).Error; err != nil {
// 忽略错误,因为这个设置可能是不可变的,重复执行会报错
ps.logger.Warnw("启用压缩设置时遇到问题 (可能已设置,可忽略)", "table", tableName, "error", err)
logger.Warnw("启用压缩设置时遇到问题 (可能已设置,可忽略)", "table", tableName, "error", err)
}
ps.logger.Debugw("成功为表启用压缩设置 (或已启用)", "table", tableName)
logger.Debugw("成功为表启用压缩设置 (或已启用)", "table", tableName)
// 2. 添加压缩策略
ps.logger.Debugw("为表添加压缩策略", "table", tableName, "compress_after", compressAfter)
logger.Debugw("为表添加压缩策略", "table", tableName, "compress_after", compressAfter)
policySQL := fmt.Sprintf("SELECT add_compression_policy('%s', INTERVAL '%s', if_not_exists => TRUE);", tableName, compressAfter)
if err := ps.db.Exec(policySQL).Error; err != nil {
ps.logger.Errorw("添加压缩策略失败", "table", tableName, "error", err)
if err := ps.db.WithContext(storageCtx).Exec(policySQL).Error; err != nil {
logger.Errorw("添加压缩策略失败", "table", tableName, "error", err)
return fmt.Errorf("为 %s 添加压缩策略失败: %w", tableName, err)
}
ps.logger.Debugw("成功为表添加压缩策略 (或已存在)", "table", tableName)
logger.Debugw("成功为表添加压缩策略 (或已存在)", "table", tableName)
}
return nil
}
// creatingIndex 用于创建gorm无法处理的索引, 如gin索引
func (ps *PostgresStorage) creatingIndex() error {
func (ps *PostgresStorage) creatingIndex(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingIndex")
// 使用 IF NOT EXISTS 保证幂等性
// 如果索引已存在,此命令不会报错
// 为 sensor_data 表的 data 字段创建 GIN 索引
ps.logger.Debug("正在为 sensor_data 表的 data 字段创建 GIN 索引")
logger.Debug("正在为 sensor_data 表的 data 字段创建 GIN 索引")
ginSensorDataIndexSQL := "CREATE INDEX IF NOT EXISTS idx_sensor_data_data_gin ON sensor_data USING GIN (data);"
if err := ps.db.Exec(ginSensorDataIndexSQL).Error; err != nil {
ps.logger.Errorw("为 sensor_data 的 data 字段创建 GIN 索引失败", "error", err)
if err := ps.db.WithContext(storageCtx).Exec(ginSensorDataIndexSQL).Error; err != nil {
logger.Errorw("为 sensor_data 的 data 字段创建 GIN 索引失败", "error", err)
return fmt.Errorf("为 sensor_data 的 data 字段创建 GIN 索引失败: %w", err)
}
ps.logger.Debug("成功为 sensor_data 的 data 字段创建 GIN 索引 (或已存在)")
logger.Debug("成功为 sensor_data 的 data 字段创建 GIN 索引 (或已存在)")
// 为 tasks.parameters 创建 GIN 索引
ps.logger.Debug("正在为 tasks 表的 parameters 字段创建 GIN 索引")
logger.Debug("正在为 tasks 表的 parameters 字段创建 GIN 索引")
taskGinIndexSQL := "CREATE INDEX IF NOT EXISTS idx_tasks_parameters_gin ON tasks USING GIN (parameters);"
if err := ps.db.Exec(taskGinIndexSQL).Error; err != nil {
ps.logger.Errorw("为 tasks 的 parameters 字段创建 GIN 索引失败", "error", err)
if err := ps.db.WithContext(storageCtx).Exec(taskGinIndexSQL).Error; err != nil {
logger.Errorw("为 tasks 的 parameters 字段创建 GIN 索引失败", "error", err)
return fmt.Errorf("为 tasks 的 parameters 字段创建 GIN 索引失败: %w", err)
}
ps.logger.Debug("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
logger.Debug("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
return nil
}

View File

@@ -4,10 +4,12 @@
package database
import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"gorm.io/gorm"
)
@@ -15,22 +17,22 @@ import (
// 所有存储实现都需要实现此接口定义的方法
type Storage interface {
// Connect 建立与存储后端的连接
Connect() error
Connect(ctx context.Context) error
// Disconnect 断开与存储后端的连接
Disconnect() error
Disconnect(ctx context.Context) error
// GetDB 获取数据库实例
GetDB() *gorm.DB
GetDB(ctx context.Context) *gorm.DB
// Migrate 执行数据库迁移
// 参数为需要迁移的 GORM 模型
Migrate(models ...interface{}) error
Migrate(ctx context.Context, models ...interface{}) error
}
// NewStorage 创建并返回一个存储实例
// 根据配置返回相应的存储实现
func NewStorage(cfg config.DatabaseConfig, logger *logs.Logger) Storage {
func NewStorage(ctx context.Context, cfg config.DatabaseConfig) Storage {
// 构建数据库连接字符串
connectionString := fmt.Sprintf(
"user=%s password=%s dbname=%s host=%s port=%d sslmode=%s",
@@ -45,11 +47,11 @@ func NewStorage(cfg config.DatabaseConfig, logger *logs.Logger) Storage {
// 当前默认返回PostgreSQL存储实现并将 logger 注入
// 当前默认返回PostgreSQL存储实现并将 logger 注入
return NewPostgresStorage(
logs.AddCompName(context.Background(), "PostgresStorage"),
connectionString,
cfg.IsTimescaleDB, // <--- 添加 IsTimescaleDB
cfg.IsTimescaleDB,
cfg.MaxOpenConns,
cfg.MaxIdleConns,
cfg.ConnMaxLifetime,
logger,
)
}

View File

@@ -0,0 +1,90 @@
package logs
import (
"context"
"fmt"
"strings"
)
// contextKey 是用于在 context.Context 中存储值的私有类型,避免键冲突。
type contextKey string
const (
// compNameKey 用于存储组件名称。
compNameKey contextKey = "compName"
// chainKey 用于存储调用链。
chainKey contextKey = "chain"
)
// AddCompName 将一个组件名(对象名)存入 Context并返回一个包含该信息的新 Context。
// 这通常在依赖注入时完成,用于创建组件的“身份名牌” selfCtx。
func AddCompName(ctx context.Context, compName string) context.Context {
return context.WithValue(ctx, compNameKey, compName)
}
// AddFuncName 这是构建调用链的核心原子操作。它智能地合并上游的调用链和当前组件的信息,
// 生成并返回一个包含更新后调用链和当前组件名的 新 Context。
// 此函数用于只需要传递调用链而不需要立即打印日志的场景。
func AddFuncName(upstreamCtx context.Context, selfCtx context.Context, funcName string) context.Context {
// 1. 获取上游调用链
var oldChain []string
if val := upstreamCtx.Value(chainKey); val != nil {
if chain, ok := val.([]string); ok {
oldChain = chain
}
}
// 2. 获取当前组件名
compName, ok := selfCtx.Value(compNameKey).(string)
if !ok {
// 如果 selfCtx 中没有 compName则无法构建节点直接返回 upstreamCtx
// 这种情况通常不应该发生,因为 selfCtx 应该在依赖注入时通过 AddCompName 初始化
return upstreamCtx
}
// 3. 构建新节点
newNode := fmt.Sprintf("%s.%s", compName, funcName)
// 4. 生成新调用链
// 创建一个新的切片,并将旧链和新节点复制进去
newChain := make([]string, len(oldChain)+1)
copy(newChain, oldChain)
newChain[len(oldChain)] = newNode
// 5. 创建新的 Context
// 使用 context.WithValue以 chainKey 为键,将 newChain 存入一个新的 Context我们称之为 tmpCtx。
tmpCtx := context.WithValue(upstreamCtx, chainKey, newChain)
// 接着,基于 tmpCtx再次调用 context.WithValue以 compNameKey 为键,
// 将从 selfCtx 中获取的 compName 存入,得到最终的 newCtx。
// 这确保了传向下游的 Context 正确地标识了当前组件。
newCtx := context.WithValue(tmpCtx, compNameKey, compName)
return newCtx
}
// DetachContext 创建一个“分离”的 Context。
// 新的 Context 会继承原始 Context 中的所有值(特别是用于日志追踪的 chainKey 和 compNameKey
// 但它会使用 context.Background() 作为其父级,从而“丢弃”原始 Context 的取消信号。
// 这对于需要在请求结束后继续执行的异步任务(如记录审计日志)至关重要,可以防止出现 "context canceled" 错误。
func DetachContext(ctx context.Context) context.Context {
detachedCtx := context.Background()
// 复制我们关心的、用于日志追踪的所有值
if val := ctx.Value(chainKey); val != nil {
detachedCtx = context.WithValue(detachedCtx, chainKey, val)
}
if val := ctx.Value(compNameKey); val != nil {
detachedCtx = context.WithValue(detachedCtx, compNameKey, val)
}
return detachedCtx
}
// 获取context中的调用链字符串
func GetTraceStr(ctx context.Context) string {
if trace, ok := ctx.Value(chainKey).([]string); ok {
return strings.Join(trace, "->")
}
return ""
}

View File

@@ -0,0 +1,102 @@
package logs
import (
"bytes"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
)
// traceKey 是日志中用于调用链的字段名,必须与 GetLogger 中使用的保持一致。
const traceKey = "Trace"
// coloredConsoleEncoder 是一个自定义的 zapcore.Encoder它实现了两个核心功能
// 1. 为控制台输出的日志行根据级别添加 ANSI 颜色。
// 2. 确保代表“调用链”的结构化字段总是出现在日志行的末尾。
type coloredConsoleEncoder struct {
zapcore.Encoder // 嵌入一个标准的 ConsoleEncoder复用其大部分能力。
}
// NewColoredConsoleEncoder 创建一个新的 coloredConsoleEncoder 实例。
func NewColoredConsoleEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
return &coloredConsoleEncoder{
Encoder: zapcore.NewConsoleEncoder(cfg),
}
}
// EncodeEntry 重写了核心的编码方法。
func (c *coloredConsoleEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
// 1. 从所有字段中分离出“调用链”字段和其他字段
var traceField zapcore.Field
otherFields := make([]zapcore.Field, 0, len(fields))
for _, field := range fields {
if field.Key == traceKey {
traceField = field
} else {
otherFields = append(otherFields, field)
}
}
// 2. 使用内嵌的 ConsoleEncoder 先编码日志条目和“其他”字段
// 这会生成不包含调用链的日志主体部分。
line, err := c.Encoder.EncodeEntry(entry, otherFields)
if err != nil {
return nil, err
}
// 3. 如果存在“调用链”字段,则将其手动编码并追加到末尾
if traceField.Key != "" {
// 为了保持格式一致,我们创建一个临时的 Encoder 来只编码这一个字段
// 注意:这里我们不能直接使用 c.Encoder因为它会添加不必要的前缀如时间、级别等
tempEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{})
traceLine, err := tempEncoder.EncodeEntry(zapcore.Entry{}, []zapcore.Field{traceField})
if err != nil {
return nil, err // 理论上不应失败
}
// 将编码后的调用链字段(例如 "逻辑调用链=xxx->yyy")追加到日志主体后
// TrimSpace 用于移除 tempEncoder 可能产生的额外换行符
line.AppendString("\t") // 使用制表符分隔
line.AppendString(string(bytes.TrimSpace(traceLine.Bytes())))
}
// 4. 为最终的日志行(不包括调用链)添加颜色
var color string
switch entry.Level {
case zapcore.DebugLevel:
color = blue
case zapcore.InfoLevel:
color = green
case zapcore.WarnLevel:
color = yellow
case zapcore.ErrorLevel:
color = red
case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
color = bold + red
default:
color = reset
}
// 创建一个新的 buffer将颜色、日志内容和重置代码包裹起来
finalBuf := buffer.NewPool().Get()
finalBuf.AppendString(color)
finalBuf.Write(line.Bytes()) // 写入已包含调用链的日志行
finalBuf.AppendString(reset)
// 如果原始日志行末尾没有换行符,则添加一个
if line.Bytes()[line.Len()-1] != '\n' {
finalBuf.AppendByte('\n')
}
line.Free() // 释放由 c.Encoder.EncodeEntry 创建的 buffer
return finalBuf, nil
}
// Clone 必须被重写,以确保克隆时返回的是我们自定义的 encoder 类型。
func (c *coloredConsoleEncoder) Clone() zapcore.Encoder {
// 克隆内嵌的 Encoder并用它来创建一个新的 coloredConsoleEncoder
return &coloredConsoleEncoder{
Encoder: c.Encoder.Clone(),
}
}

View File

@@ -9,11 +9,11 @@ import (
"io"
"os"
"strings"
"sync"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"go.uber.org/zap"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
gormlogger "gorm.io/gorm/logger"
@@ -32,6 +32,11 @@ const (
bold = "\033[1m"
)
var (
defaultLogger *Logger
once sync.Once
)
// Logger 是一个封装了 zap.SugaredLogger 的日志记录器。
// 它提供了结构化日志记录的各种方法,并实现了 io.Writer 接口以兼容 Gin。
type Logger struct {
@@ -64,10 +69,59 @@ func NewLogger(cfg config.LogConfig) *Logger {
return &Logger{zapLogger.Sugar()}
}
// InitDefaultLogger 初始化包级单例的 defaultLogger。
// 此函数应在应用程序启动时调用一次。
func InitDefaultLogger(cfg config.LogConfig) {
once.Do(func() {
defaultLogger = NewLogger(cfg)
})
}
// GetLogger 从 Context 中提取调用链信息,并返回一个携带这些信息的 Logger 副本。
// 如果 Context 中没有调用链信息,则直接返回默认的 Logger。
func GetLogger(ctx context.Context) *Logger {
if defaultLogger == nil {
// 在调用 InitDefaultLogger 之前,提供一个备用的、仅输出到控制台的 Logger
fallbackCfg := config.LogConfig{Level: "info", Format: "console"}
return NewLogger(fallbackCfg)
}
val := ctx.Value(chainKey)
if val == nil {
return defaultLogger
}
chain := GetTraceStr(ctx)
if chain == "" {
return defaultLogger
}
// 使用 With 方法创建带有 traceKey 字段的 Logger 副本
newSugaredLogger := defaultLogger.With(traceKey, chain)
return &Logger{newSugaredLogger}
}
// Trace 是构建和记录调用链的核心函数。
// 它首先使用 AddFuncName 创建一个包含新调用节点的新 Context
// 然后返回这个新 Context 和一个包含完整调用链的 Logger 实例。
func Trace(upstreamCtx context.Context, selfCtx context.Context, funcName string) (context.Context, *Logger) {
newCtx := AddFuncName(upstreamCtx, selfCtx, funcName)
logger := GetLogger(newCtx)
return newCtx, logger
}
// TraceLogger 类似于 Trace但只返回一个包含完整调用链的 Logger 实例,
// 而不返回更新后的 Context。适用于调用链的末端或不需要向下传递 Context 的场景。
func TraceLogger(upstreamCtx context.Context, selfCtx context.Context, funcName string) *Logger {
newCtx := AddFuncName(upstreamCtx, selfCtx, funcName)
logger := GetLogger(newCtx)
return logger
}
// GetEncoder 根据指定的格式返回一个 zapcore.Encoder。
func GetEncoder(format string) zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 时间格式: 2006-01-02T15:04:05.000Z0700
encoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder // 时间格式: 2006-01-02T15:04:05Z07:00
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 日志级别大写: INFO (由 coloredConsoleEncoder 处理颜色)
if format == "json" {
@@ -77,63 +131,6 @@ func GetEncoder(format string) zapcore.Encoder {
return NewColoredConsoleEncoder(encoderConfig)
}
// coloredConsoleEncoder 是一个自定义的 zapcore.Encoder用于为整个日志行添加颜色。
type coloredConsoleEncoder struct {
zapcore.Encoder // 嵌入默认的 ConsoleEncoder以便委托其大部分功能
cfg zapcore.EncoderConfig
}
// NewColoredConsoleEncoder 创建一个新的 coloredConsoleEncoder 实例。
func NewColoredConsoleEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
// 创建一个标准的 ConsoleEncoder 作为基础
baseEncoder := zapcore.NewConsoleEncoder(cfg)
return &coloredConsoleEncoder{
Encoder: baseEncoder,
cfg: cfg,
}
}
// EncodeEntry 重写 EncodeEntry 方法,在原始日志输出前后添加颜色代码。
func (c *coloredConsoleEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
// 首先,让嵌入的默认编码器生成原始的日志行
buf, err := c.Encoder.EncodeEntry(entry, fields)
if err != nil {
return nil, err
}
// 根据日志级别确定颜色
var color string
switch entry.Level {
case zapcore.DebugLevel:
color = blue
case zapcore.InfoLevel:
color = green
case zapcore.WarnLevel:
color = yellow
case zapcore.ErrorLevel:
color = red
case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
color = bold + red
default:
color = reset
}
// 创建一个新的 buffer 来存储带颜色的日志行
coloredBuf := buffer.NewPool().Get()
coloredBuf.AppendString(color)
coloredBuf.Write(buf.Bytes())
coloredBuf.AppendString(reset)
buf.Free() // 释放原始 buffer 回池中
return coloredBuf, nil
}
// Clone 方法也需要重写,以确保返回一个新的 coloredConsoleEncoder 实例。
func (c *coloredConsoleEncoder) Clone() zapcore.Encoder {
return NewColoredConsoleEncoder(c.cfg)
}
// getWriteSyncer 根据配置创建日志写入目标。
func getWriteSyncer(cfg config.LogConfig) zapcore.WriteSyncer {
writers := []zapcore.WriteSyncer{os.Stdout}
@@ -190,17 +187,17 @@ func (g *GormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
// Info 打印 Info 级别的日志。
func (g *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
g.ZapLogger.Infof(msg, data...)
GetLogger(ctx).Infof(msg, data...)
}
// Warn 打印 Warn 级别的日志。
func (g *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
g.ZapLogger.Warnf(msg, data...)
GetLogger(ctx).Warnf(msg, data...)
}
// Error 打印 Error 级别的日志。
func (g *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
g.ZapLogger.Errorf(msg, data...)
GetLogger(ctx).Errorf(msg, data...)
}
// Trace 打印 SQL 查询日志,这是 GORM 日志的核心。
@@ -214,25 +211,30 @@ func (g *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql
"elapsed", fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6),
}
// --- 逻辑修复开始 ---
// 附加调用链信息
chain := GetTraceStr(ctx)
if chain != "" {
fields = append(fields, traceKey, chain)
}
if err != nil {
// 如果是 "record not found" 错误且我们配置了跳过,则直接返回
if g.SkipErrRecordNotFound && strings.Contains(err.Error(), "record not found") {
return
}
// 否则,记录为错误日志
g.ZapLogger.With(fields...).Errorf("[GORM] error: %s", err)
defaultLogger.With(fields...).Errorf("[GORM] error: %s", err)
return
}
// 如果查询时间超过慢查询阈值,则记录警告
if g.SlowThreshold != 0 && elapsed > g.SlowThreshold {
g.ZapLogger.With(fields...).Warnf("[GORM] slow query")
defaultLogger.With(fields...).Warnf("[GORM] slow query")
return
}
// 正常情况,记录 Debug 级别的 SQL 查询
g.ZapLogger.With(fields...).Debugf("[GORM] trace")
defaultLogger.With(fields...).Debugf("[GORM] trace")
// --- 逻辑修复结束 ---
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/command_generater"
"gorm.io/datatypes"
"gorm.io/gorm"
)

View File

@@ -2,11 +2,14 @@ package notify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
)
const (
@@ -18,6 +21,8 @@ const (
// larkNotifier 实现了 Notifier 接口,用于通过飞书自建应用发送私聊消息。
type larkNotifier struct {
ctx context.Context
appID string // 应用 ID
appSecret string // 应用密钥
@@ -29,8 +34,9 @@ type larkNotifier struct {
// NewLarkNotifier 创建一个新的 larkNotifier 实例。
// 调用者需要注入飞书应用的 AppID 和 AppSecret。
func NewLarkNotifier(appID, appSecret string) Notifier {
func NewLarkNotifier(ctx context.Context, appID, appSecret string) Notifier {
return &larkNotifier{
ctx: ctx,
appID: appID,
appSecret: appSecret,
}
@@ -38,9 +44,10 @@ func NewLarkNotifier(appID, appSecret string) Notifier {
// Send 向指定用户发送一条飞书消息卡片。
// toAddr 参数是接收者的邮箱地址。
func (l *larkNotifier) Send(content AlarmContent, toAddr string) error {
func (l *larkNotifier) Send(ctx context.Context, content AlarmContent, toAddr string) error {
notifierCtx := logs.AddFuncName(ctx, l.ctx, "Send")
// 1. 获取有效的 tenant_access_token
token, err := l.getAccessToken()
token, err := l.getAccessToken(notifierCtx)
if err != nil {
return err
}
@@ -115,7 +122,7 @@ func (l *larkNotifier) Send(content AlarmContent, toAddr string) error {
}
// getAccessToken 获取并缓存 tenant_access_token处理了线程安全和自动刷新。
func (l *larkNotifier) getAccessToken() (string, error) {
func (l *larkNotifier) getAccessToken(ctx context.Context) (string, error) {
l.mu.Lock()
defer l.mu.Unlock()

View File

@@ -1,26 +1,29 @@
package notify
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
)
// logNotifier 实现了 Notifier 接口,用于将告警信息记录到日志中。
type logNotifier struct {
logger *logs.Logger
ctx context.Context
}
// NewLogNotifier 创建一个新的 logNotifier 实例。
// 它接收一个日志记录器,用于实际的日志输出。
func NewLogNotifier(logger *logs.Logger) Notifier {
func NewLogNotifier(ctx context.Context) Notifier {
return &logNotifier{
logger: logger,
ctx: ctx,
}
}
// Send 将告警内容以结构化的方式记录到日志中。
// toAddr 参数在这里表示告警的预期接收者地址,也会被记录。
func (l *logNotifier) Send(content AlarmContent, toAddr string) error {
l.logger.Infow("告警已记录到日志",
func (l *logNotifier) Send(ctx context.Context, content AlarmContent, toAddr string) error {
logger := logs.TraceLogger(ctx, l.ctx, "Send")
logger.Infow("告警已记录到日志",
"notifierType", NotifierTypeLog,
"title", content.Title,
"message", content.Message,

View File

@@ -1,6 +1,7 @@
package notify
import (
"context"
"time"
"go.uber.org/zap/zapcore"
@@ -38,7 +39,7 @@ type AlarmContent struct {
// Notifier 定义了通知发送器的接口
type Notifier interface {
// Send 发送通知
Send(content AlarmContent, toAddr string) error
Send(ctx context.Context, content AlarmContent, toAddr string) error
// Type 返回通知器的类型
Type() NotifierType
}

View File

@@ -1,6 +1,7 @@
package notify
import (
"context"
"fmt"
"net/smtp"
"strings"
@@ -8,6 +9,7 @@ import (
// smtpNotifier 实现了 Notifier 接口,用于通过 SMTP 发送邮件通知。
type smtpNotifier struct {
ctx context.Context
host string // SMTP 服务器地址
port int // SMTP 服务器端口
username string // 发件人邮箱地址
@@ -17,8 +19,9 @@ type smtpNotifier struct {
// NewSMTPNotifier 创建一个新的 smtpNotifier 实例。
// 调用者需要注入 SMTP 相关的配置。
func NewSMTPNotifier(host string, port int, username, password, sender string) Notifier {
func NewSMTPNotifier(ctx context.Context, host string, port int, username, password, sender string) Notifier {
return &smtpNotifier{
ctx: ctx,
host: host,
port: port,
username: username,
@@ -28,7 +31,7 @@ func NewSMTPNotifier(host string, port int, username, password, sender string) N
}
// Send 使用 net/smtp 包发送一封邮件。
func (s *smtpNotifier) Send(content AlarmContent, toAddr string) error {
func (s *smtpNotifier) Send(ctx context.Context, content AlarmContent, toAddr string) error {
// 1. 设置认证信息
auth := smtp.PlainAuth("", s.username, s.password, s.host)

View File

@@ -2,6 +2,7 @@ package notify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
@@ -19,6 +20,8 @@ const (
// wechatNotifier 实现了 Notifier 接口,用于通过企业微信自建应用发送私聊消息。
type wechatNotifier struct {
ctx context.Context
corpID string // 企业ID (CorpID)
agentID string // 应用ID (AgentID)
secret string // 应用密钥 (Secret)
@@ -31,8 +34,9 @@ type wechatNotifier struct {
// NewWechatNotifier 创建一个新的 wechatNotifier 实例。
// 调用者需要注入企业微信应用的 CorpID, AgentID 和 Secret。
func NewWechatNotifier(corpID, agentID, secret string) Notifier {
func NewWechatNotifier(ctx context.Context, corpID, agentID, secret string) Notifier {
return &wechatNotifier{
ctx: ctx,
corpID: corpID,
agentID: agentID,
secret: secret,
@@ -41,7 +45,7 @@ func NewWechatNotifier(corpID, agentID, secret string) Notifier {
// Send 向指定用户发送一条 markdown 格式的私聊消息。
// toAddr 参数是接收者的 UserID 列表,用逗号或竖线分隔。
func (w *wechatNotifier) Send(content AlarmContent, toAddr string) error {
func (w *wechatNotifier) Send(ctx context.Context, content AlarmContent, toAddr string) error {
// 1. 获取有效的 access_token
token, err := w.getAccessToken()
if err != nil {

View File

@@ -1,72 +1,85 @@
package repository
import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// AreaControllerRepository 定义了对 AreaController 模型的数据库操作接口
type AreaControllerRepository interface {
FindByID(id uint) (*models.AreaController, error)
FindByNetworkID(networkID string) (*models.AreaController, error)
Create(ac *models.AreaController) error
ListAll() ([]*models.AreaController, error)
Update(ac *models.AreaController) error
Delete(id uint) error
FindByID(ctx context.Context, id uint) (*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
}
// gormAreaControllerRepository 是 AreaControllerRepository 的 GORM 实现。
type gormAreaControllerRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormAreaControllerRepository 创建一个新的 AreaControllerRepository GORM 实现实例。
func NewGormAreaControllerRepository(db *gorm.DB) AreaControllerRepository {
return &gormAreaControllerRepository{db: db}
func NewGormAreaControllerRepository(ctx context.Context, db *gorm.DB) AreaControllerRepository {
return &gormAreaControllerRepository{
ctx: ctx,
db: db,
}
}
// Create 创建一个新的 AreaController 记录。
func (r *gormAreaControllerRepository) Create(ac *models.AreaController) error {
return r.db.Create(ac).Error
func (r *gormAreaControllerRepository) Create(ctx context.Context, ac *models.AreaController) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
return r.db.WithContext(repoCtx).Create(ac).Error
}
// ListAll 返回所有 AreaController 的列表。
func (r *gormAreaControllerRepository) ListAll() ([]*models.AreaController, error) {
func (r *gormAreaControllerRepository) ListAll(ctx context.Context) ([]*models.AreaController, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListAll")
var areaControllers []*models.AreaController
if err := r.db.Find(&areaControllers).Error; err != nil {
if err := r.db.WithContext(repoCtx).Find(&areaControllers).Error; err != nil {
return nil, err
}
return areaControllers, nil
}
// Update 更新一个已存在的 AreaController 记录。
func (r *gormAreaControllerRepository) Update(ac *models.AreaController) error {
return r.db.Save(ac).Error
func (r *gormAreaControllerRepository) Update(ctx context.Context, ac *models.AreaController) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Update")
return r.db.WithContext(repoCtx).Save(ac).Error
}
// Delete 删除一个 AreaController 记录。
func (r *gormAreaControllerRepository) Delete(id uint) error {
if err := r.db.Delete(&models.AreaController{}, id).Error; err != nil {
func (r *gormAreaControllerRepository) Delete(ctx context.Context, id uint) 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)
}
return nil
}
// FindByID 通过 ID 查找一个 AreaController。
func (r *gormAreaControllerRepository) FindByID(id uint) (*models.AreaController, error) {
func (r *gormAreaControllerRepository) FindByID(ctx context.Context, id uint) (*models.AreaController, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
var areaController models.AreaController
if err := r.db.First(&areaController, id).Error; err != nil {
if err := r.db.WithContext(repoCtx).First(&areaController, id).Error; err != nil {
return nil, err
}
return &areaController, nil
}
// FindByNetworkID 通过 NetworkID 查找一个 AreaController。
func (r *gormAreaControllerRepository) FindByNetworkID(networkID string) (*models.AreaController, error) {
func (r *gormAreaControllerRepository) FindByNetworkID(ctx context.Context, networkID string) (*models.AreaController, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByNetworkID")
var areaController models.AreaController
if err := r.db.Where("network_id = ?", networkID).First(&areaController).Error; err != nil {
if err := r.db.WithContext(repoCtx).Where("network_id = ?", networkID).First(&areaController).Error; err != nil {
return nil, err
}
return &areaController, nil

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -18,40 +21,44 @@ type DeviceCommandLogListOptions struct {
// DeviceCommandLogRepository 定义了设备下行命令历史记录的数据访问接口
type DeviceCommandLogRepository interface {
Create(record *models.DeviceCommandLog) error
FindByMessageID(messageID string) (*models.DeviceCommandLog, error)
UpdateAcknowledgedAt(messageID string, acknowledgedAt time.Time, receivedSuccess bool) error
Create(ctx context.Context, record *models.DeviceCommandLog) error
FindByMessageID(ctx context.Context, messageID string) (*models.DeviceCommandLog, error)
UpdateAcknowledgedAt(ctx context.Context, messageID string, acknowledgedAt time.Time, receivedSuccess bool) error
// List 支持分页和过滤的列表查询
List(opts DeviceCommandLogListOptions, page, pageSize int) ([]models.DeviceCommandLog, int64, error)
List(ctx context.Context, opts DeviceCommandLogListOptions, page, pageSize int) ([]models.DeviceCommandLog, int64, error)
}
// gormDeviceCommandLogRepository 是 DeviceCommandLogRepository 接口的 GORM 实现
type gormDeviceCommandLogRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormDeviceCommandLogRepository 创建一个新的 DeviceCommandLogRepository GORM 实现
func NewGormDeviceCommandLogRepository(db *gorm.DB) DeviceCommandLogRepository {
return &gormDeviceCommandLogRepository{db: db}
func NewGormDeviceCommandLogRepository(ctx context.Context, db *gorm.DB) DeviceCommandLogRepository {
return &gormDeviceCommandLogRepository{ctx: ctx, db: db}
}
// Create 实现 DeviceCommandLogRepository 接口的 Create 方法
func (r *gormDeviceCommandLogRepository) Create(record *models.DeviceCommandLog) error {
return r.db.Create(record).Error
func (r *gormDeviceCommandLogRepository) Create(ctx context.Context, record *models.DeviceCommandLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
return r.db.WithContext(repoCtx).Create(record).Error
}
// FindByMessageID 实现 DeviceCommandLogRepository 接口的 FindByMessageID 方法
func (r *gormDeviceCommandLogRepository) FindByMessageID(messageID string) (*models.DeviceCommandLog, error) {
func (r *gormDeviceCommandLogRepository) FindByMessageID(ctx context.Context, messageID string) (*models.DeviceCommandLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByMessageID")
var record models.DeviceCommandLog
if err := r.db.Where("message_id = ?", messageID).First(&record).Error; err != nil {
if err := r.db.WithContext(repoCtx).Where("message_id = ?", messageID).First(&record).Error; err != nil {
return nil, err
}
return &record, nil
}
// UpdateAcknowledgedAt 实现 DeviceCommandLogRepository 接口的 UpdateAcknowledgedAt 方法
func (r *gormDeviceCommandLogRepository) UpdateAcknowledgedAt(messageID string, acknowledgedAt time.Time, receivedSuccess bool) error {
return r.db.Model(&models.DeviceCommandLog{}).
func (r *gormDeviceCommandLogRepository) UpdateAcknowledgedAt(ctx context.Context, messageID string, acknowledgedAt time.Time, receivedSuccess bool) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateAcknowledgedAt")
return r.db.WithContext(repoCtx).Model(&models.DeviceCommandLog{}).
Where("message_id = ?", messageID).
Updates(map[string]interface{}{
"acknowledged_at": acknowledgedAt,
@@ -60,7 +67,8 @@ func (r *gormDeviceCommandLogRepository) UpdateAcknowledgedAt(messageID string,
}
// List 实现了分页和过滤查询设备命令日志的功能
func (r *gormDeviceCommandLogRepository) List(opts DeviceCommandLogListOptions, page, pageSize int) ([]models.DeviceCommandLog, int64, error) {
func (r *gormDeviceCommandLogRepository) List(ctx context.Context, opts DeviceCommandLogListOptions, page, pageSize int) ([]models.DeviceCommandLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "List")
// --- 校验分页参数 ---
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
@@ -69,7 +77,7 @@ func (r *gormDeviceCommandLogRepository) List(opts DeviceCommandLogListOptions,
var results []models.DeviceCommandLog
var total int64
query := r.db.Model(&models.DeviceCommandLog{})
query := r.db.WithContext(repoCtx).Model(&models.DeviceCommandLog{})
// --- 应用过滤条件 ---
if opts.DeviceID != nil {

View File

@@ -1,10 +1,13 @@
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"
"gorm.io/gorm"
)
@@ -12,83 +15,89 @@ import (
// 这一层抽象使得上层业务逻辑无需关心底层数据库的具体实现。
type DeviceRepository interface {
// Create 创建一个新设备记录
Create(device *models.Device) error
Create(ctx context.Context, device *models.Device) error
// FindByID 根据主键 ID 查找设备
FindByID(id uint) (*models.Device, error)
FindByID(ctx context.Context, id uint) (*models.Device, error)
// FindByIDString 根据字符串形式的主键 ID 查找设备
FindByIDString(id string) (*models.Device, error)
FindByIDString(ctx context.Context, id string) (*models.Device, error)
// ListAll 获取所有设备的列表
ListAll() ([]*models.Device, error)
ListAll(ctx context.Context) ([]*models.Device, error)
// ListAllSensors 获取所有传感器类型的设备列表
ListAllSensors() ([]*models.Device, error)
ListAllSensors(ctx context.Context) ([]*models.Device, error)
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备
ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error)
ListByAreaControllerID(ctx context.Context, areaControllerID uint) ([]*models.Device, error)
// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备
FindByDeviceTemplateID(deviceTemplateID uint) ([]*models.Device, error)
FindByDeviceTemplateID(ctx context.Context, deviceTemplateID uint) ([]*models.Device, error)
// Update 更新一个已有的设备信息
Update(device *models.Device) error
Update(ctx context.Context, device *models.Device) error
// Delete 根据主键 ID 删除一个设备
Delete(id uint) error
Delete(ctx context.Context, id uint) error
// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备
FindByAreaControllerAndPhysicalAddress(areaControllerID uint, busNumber int, busAddress int) (*models.Device, error)
FindByAreaControllerAndPhysicalAddress(ctx context.Context, areaControllerID uint, busNumber int, busAddress int) (*models.Device, error)
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error)
GetDevicesByIDsTx(ctx context.Context, tx *gorm.DB, ids []uint) ([]models.Device, error)
// IsDeviceInUse 检查设备是否被任何任务使用
IsDeviceInUse(deviceID uint) (bool, error)
IsDeviceInUse(ctx context.Context, deviceID uint) (bool, error)
// IsAreaControllerInUse 检查区域主控是否被任何设备使用
IsAreaControllerInUse(areaControllerID uint) (bool, error)
IsAreaControllerInUse(ctx context.Context, areaControllerID uint) (bool, error)
}
// gormDeviceRepository 是 DeviceRepository 的 GORM 实现
type gormDeviceRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormDeviceRepository 创建一个新的 DeviceRepository GORM 实现实例
func NewGormDeviceRepository(db *gorm.DB) DeviceRepository {
return &gormDeviceRepository{db: db}
func NewGormDeviceRepository(ctx context.Context, db *gorm.DB) DeviceRepository {
return &gormDeviceRepository{ctx: ctx, db: db}
}
// Create 创建一个新的设备记录
func (r *gormDeviceRepository) Create(device *models.Device) error {
return r.db.Create(device).Error
func (r *gormDeviceRepository) Create(ctx context.Context, device *models.Device) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
// BeforeSave 钩子会在这里被自动触发
return r.db.WithContext(repoCtx).Create(device).Error
}
// FindByID 根据 ID 查找设备
func (r *gormDeviceRepository) FindByID(id uint) (*models.Device, error) {
func (r *gormDeviceRepository) FindByID(ctx context.Context, id uint) (*models.Device, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
var device models.Device
if err := r.db.Preload("AreaController").Preload("DeviceTemplate").First(&device, id).Error; err != nil {
if err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").First(&device, id).Error; err != nil {
return nil, err
}
return &device, nil
}
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
func (r *gormDeviceRepository) GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error) {
func (r *gormDeviceRepository) GetDevicesByIDsTx(ctx context.Context, tx *gorm.DB, ids []uint) ([]models.Device, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetDevicesByIDsTx")
var devices []models.Device
if len(ids) == 0 {
return devices, nil
}
if err := tx.Where("id IN ?", ids).Find(&devices).Error; err != nil {
if err := tx.WithContext(repoCtx).Where("id IN ?", ids).Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}
// FindByIDString 根据字符串形式的主键 ID 查找设备
func (r *gormDeviceRepository) FindByIDString(id string) (*models.Device, error) {
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 {
@@ -96,22 +105,24 @@ func (r *gormDeviceRepository) FindByIDString(id string) (*models.Device, error)
return nil, fmt.Errorf("无效的设备ID格式: %w", err)
}
// 调用已有的 FindByID 方法
return r.FindByID(uint(idInt))
return r.FindByID(repoCtx, uint(idInt))
}
// ListAll 获取所有设备的列表
func (r *gormDeviceRepository) ListAll() ([]*models.Device, error) {
func (r *gormDeviceRepository) ListAll(ctx context.Context) ([]*models.Device, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListAll")
var devices []*models.Device
if err := r.db.Preload("AreaController").Preload("DeviceTemplate").Find(&devices).Error; err != nil {
if err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}
// ListAllSensors 检索归类为传感器的所有设备
func (r *gormDeviceRepository) ListAllSensors() ([]*models.Device, error) {
func (r *gormDeviceRepository) ListAllSensors(ctx context.Context) ([]*models.Device, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListAllSensors")
var sensors []*models.Device
err := r.db.Preload("AreaController").Preload("DeviceTemplate").
err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").
Joins("JOIN device_templates ON device_templates.id = devices.device_template_id").
Where("device_templates.category = ?", models.CategorySensor).
Find(&sensors).Error
@@ -122,9 +133,10 @@ func (r *gormDeviceRepository) ListAllSensors() ([]*models.Device, error) {
}
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备
func (r *gormDeviceRepository) ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error) {
func (r *gormDeviceRepository) ListByAreaControllerID(ctx context.Context, areaControllerID uint) ([]*models.Device, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListByAreaControllerID")
var devices []*models.Device
err := r.db.Preload("AreaController").Preload("DeviceTemplate").Where("area_controller_id = ?", areaControllerID).Find(&devices).Error
err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").Where("area_controller_id = ?", areaControllerID).Find(&devices).Error
if err != nil {
return nil, err
}
@@ -132,9 +144,10 @@ func (r *gormDeviceRepository) ListByAreaControllerID(areaControllerID uint) ([]
}
// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备
func (r *gormDeviceRepository) FindByDeviceTemplateID(deviceTemplateID uint) ([]*models.Device, error) {
func (r *gormDeviceRepository) FindByDeviceTemplateID(ctx context.Context, deviceTemplateID uint) ([]*models.Device, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByDeviceTemplateID")
var devices []*models.Device
err := r.db.Where("device_template_id = ?", deviceTemplateID).Find(&devices).Error
err := r.db.WithContext(repoCtx).Where("device_template_id = ?", deviceTemplateID).Find(&devices).Error
if err != nil {
return nil, fmt.Errorf("查询使用设备模板ID %d 的设备失败: %w", deviceTemplateID, err)
}
@@ -143,20 +156,23 @@ func (r *gormDeviceRepository) FindByDeviceTemplateID(deviceTemplateID uint) ([]
// Update 更新一个已有的设备信息
// GORM 的 Save 方法会自动处理主键存在时更新,不存在时创建的逻辑,但这里我们明确用于更新。
func (r *gormDeviceRepository) Update(device *models.Device) error {
return r.db.Save(device).Error
func (r *gormDeviceRepository) Update(ctx context.Context, device *models.Device) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Update")
return r.db.WithContext(repoCtx).Save(device).Error
}
// Delete 根据 ID 删除一个设备
// GORM 使用软删除,记录不会从数据库中物理移除,而是设置 DeletedAt 字段。
func (r *gormDeviceRepository) Delete(id uint) error {
return r.db.Delete(&models.Device{}, id).Error
func (r *gormDeviceRepository) Delete(ctx context.Context, id uint) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Delete")
return r.db.WithContext(repoCtx).Delete(&models.Device{}, id).Error
}
// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备
func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(areaControllerID uint, busNumber int, busAddress int) (*models.Device, error) {
func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(ctx context.Context, areaControllerID uint, busNumber int, busAddress int) (*models.Device, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByAreaControllerAndPhysicalAddress")
var device models.Device
err := r.db.Preload("AreaController").Preload("DeviceTemplate").
err := r.db.WithContext(repoCtx).Preload("AreaController").Preload("DeviceTemplate").
Where("area_controller_id = ?", areaControllerID).
Where("properties->>'bus_number' = ?", strconv.Itoa(busNumber)).
Where("properties->>'bus_address' = ?", strconv.Itoa(busAddress)).
@@ -169,10 +185,11 @@ func (r *gormDeviceRepository) FindByAreaControllerAndPhysicalAddress(areaContro
}
// IsDeviceInUse 检查设备是否被任何任务使用
func (r *gormDeviceRepository) IsDeviceInUse(deviceID uint) (bool, error) {
func (r *gormDeviceRepository) IsDeviceInUse(ctx context.Context, deviceID uint) (bool, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsDeviceInUse")
var count int64
// 直接对 device_tasks 关联表进行 COUNT 操作,性能最高
err := r.db.Model(&models.DeviceTask{}).Where("device_id = ?", deviceID).Count(&count).Error
err := r.db.WithContext(repoCtx).Model(&models.DeviceTask{}).Where("device_id = ?", deviceID).Count(&count).Error
if err != nil {
return false, fmt.Errorf("查询设备任务关联失败: %w", err)
}
@@ -180,9 +197,10 @@ func (r *gormDeviceRepository) IsDeviceInUse(deviceID uint) (bool, error) {
}
// IsAreaControllerInUse 检查区域主控是否被任何设备使用
func (r *gormDeviceRepository) IsAreaControllerInUse(areaControllerID uint) (bool, error) {
func (r *gormDeviceRepository) IsAreaControllerInUse(ctx context.Context, areaControllerID uint) (bool, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsAreaControllerInUse")
var count int64
if err := r.db.Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil {
if err := r.db.WithContext(repoCtx).Model(&models.Device{}).Where("area_controller_id = ?", areaControllerID).Count(&count).Error; err != nil {
return false, fmt.Errorf("检查区域主控使用情况失败: %w", err)
}
return count > 0, nil

View File

@@ -1,43 +1,49 @@
package repository
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// DeviceTemplateRepository 定义了设备模板数据访问的接口
type DeviceTemplateRepository interface {
Create(deviceTemplate *models.DeviceTemplate) error
FindByID(id uint) (*models.DeviceTemplate, error)
FindByName(name string) (*models.DeviceTemplate, error)
ListAll() ([]*models.DeviceTemplate, error)
Update(deviceTemplate *models.DeviceTemplate) error
Delete(id uint) error
IsInUse(id uint) (bool, error)
Create(ctx context.Context, deviceTemplate *models.DeviceTemplate) error
FindByID(ctx context.Context, id uint) (*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)
}
// gormDeviceTemplateRepository 是 DeviceTemplateRepository 的 GORM 实现
type gormDeviceTemplateRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormDeviceTemplateRepository 创建一个新的 gormDeviceTemplateRepository 实例
func NewGormDeviceTemplateRepository(db *gorm.DB) DeviceTemplateRepository {
return &gormDeviceTemplateRepository{db: db}
func NewGormDeviceTemplateRepository(ctx context.Context, db *gorm.DB) DeviceTemplateRepository {
return &gormDeviceTemplateRepository{ctx: ctx, db: db}
}
// Create 在数据库中创建一个新的设备模板
func (r *gormDeviceTemplateRepository) Create(deviceTemplate *models.DeviceTemplate) error {
return r.db.Create(deviceTemplate).Error
func (r *gormDeviceTemplateRepository) Create(ctx context.Context, deviceTemplate *models.DeviceTemplate) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
return r.db.WithContext(repoCtx).Create(deviceTemplate).Error
}
// FindByID 根据ID查找设备模板
func (r *gormDeviceTemplateRepository) FindByID(id uint) (*models.DeviceTemplate, error) {
func (r *gormDeviceTemplateRepository) FindByID(ctx context.Context, id uint) (*models.DeviceTemplate, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
var deviceTemplate models.DeviceTemplate
if err := r.db.First(&deviceTemplate, id).Error; err != nil {
if err := r.db.WithContext(repoCtx).First(&deviceTemplate, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("设备模板未找到: %w", err)
}
@@ -47,9 +53,10 @@ func (r *gormDeviceTemplateRepository) FindByID(id uint) (*models.DeviceTemplate
}
// FindByName 根据名称查找设备模板
func (r *gormDeviceTemplateRepository) FindByName(name string) (*models.DeviceTemplate, error) {
func (r *gormDeviceTemplateRepository) FindByName(ctx context.Context, name string) (*models.DeviceTemplate, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByName")
var deviceTemplate models.DeviceTemplate
if err := r.db.Where("name = ?", name).First(&deviceTemplate).Error; err != nil {
if err := r.db.WithContext(repoCtx).Where("name = ?", name).First(&deviceTemplate).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("设备模板未找到: %w", err)
}
@@ -59,31 +66,35 @@ func (r *gormDeviceTemplateRepository) FindByName(name string) (*models.DeviceTe
}
// ListAll 获取所有设备模板
func (r *gormDeviceTemplateRepository) ListAll() ([]*models.DeviceTemplate, error) {
func (r *gormDeviceTemplateRepository) ListAll(ctx context.Context) ([]*models.DeviceTemplate, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListAll")
var deviceTemplates []*models.DeviceTemplate
if err := r.db.Find(&deviceTemplates).Error; err != nil {
if err := r.db.WithContext(repoCtx).Find(&deviceTemplates).Error; err != nil {
return nil, fmt.Errorf("获取设备模板列表失败: %w", err)
}
return deviceTemplates, nil
}
// Update 更新数据库中的设备模板
func (r *gormDeviceTemplateRepository) Update(deviceTemplate *models.DeviceTemplate) error {
return r.db.Save(deviceTemplate).Error
func (r *gormDeviceTemplateRepository) Update(ctx context.Context, deviceTemplate *models.DeviceTemplate) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Update")
return r.db.WithContext(repoCtx).Save(deviceTemplate).Error
}
// IsInUse 检查设备模板是否正在被设备使用
func (r *gormDeviceTemplateRepository) IsInUse(id uint) (bool, error) {
func (r *gormDeviceTemplateRepository) IsInUse(ctx context.Context, id uint) (bool, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsInUse")
var count int64
if err := r.db.Model(&models.Device{}).Where("device_template_id = ?", id).Count(&count).Error; err != nil {
if err := r.db.WithContext(repoCtx).Model(&models.Device{}).Where("device_template_id = ?", id).Count(&count).Error; err != nil {
return false, fmt.Errorf("检查设备模板使用情况失败: %w", err)
}
return count > 0, nil
}
// Delete 软删除数据库中的设备模板
func (r *gormDeviceTemplateRepository) Delete(id uint) error {
if err := r.db.Delete(&models.DeviceTemplate{}, id).Error; err != nil {
func (r *gormDeviceTemplateRepository) Delete(ctx context.Context, id uint) 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)
}
return nil

View File

@@ -1,10 +1,13 @@
package repository
import (
"context"
"errors"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -30,61 +33,63 @@ type TaskExecutionLogListOptions struct {
// ExecutionLogRepository 定义了与执行日志交互的接口。
type ExecutionLogRepository interface {
// --- Existing methods ---
UpdateTaskExecutionLogStatusByIDs(logIDs []uint, status models.ExecutionStatus) error
UpdateTaskExecutionLogStatus(logID uint, status models.ExecutionStatus) error
CreateTaskExecutionLog(log *models.TaskExecutionLog) error
CreatePlanExecutionLog(log *models.PlanExecutionLog) error
UpdatePlanExecutionLog(log *models.PlanExecutionLog) error
CreateTaskExecutionLogsInBatch(logs []*models.TaskExecutionLog) error
UpdateTaskExecutionLog(log *models.TaskExecutionLog) error
FindTaskExecutionLogByID(id uint) (*models.TaskExecutionLog, error)
UpdateTaskExecutionLogStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error
UpdateTaskExecutionLogStatus(ctx context.Context, logID uint, 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)
// UpdatePlanExecutionLogStatus 更新计划执行日志的状态
UpdatePlanExecutionLogStatus(logID uint, status models.ExecutionStatus) error
UpdatePlanExecutionLogStatus(ctx context.Context, logID uint, status models.ExecutionStatus) error
// UpdatePlanExecutionLogsStatusByIDs 批量更新计划执行日志的状态
UpdatePlanExecutionLogsStatusByIDs(logIDs []uint, status models.ExecutionStatus) error
UpdatePlanExecutionLogsStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error
// FindIncompletePlanExecutionLogs 查找所有未完成的计划执行日志
FindIncompletePlanExecutionLogs() ([]models.PlanExecutionLog, error)
FindIncompletePlanExecutionLogs(ctx context.Context) ([]models.PlanExecutionLog, error)
// FindInProgressPlanExecutionLogByPlanID 根据 PlanID 查找正在进行的计划执行日志
FindInProgressPlanExecutionLogByPlanID(planID uint) (*models.PlanExecutionLog, error)
FindInProgressPlanExecutionLogByPlanID(ctx context.Context, planID uint) (*models.PlanExecutionLog, error)
// FindIncompleteTaskExecutionLogsByPlanLogID 根据计划日志ID查找所有未完成的任务日志
FindIncompleteTaskExecutionLogsByPlanLogID(planLogID uint) ([]models.TaskExecutionLog, error)
FindIncompleteTaskExecutionLogsByPlanLogID(ctx context.Context, planLogID uint) ([]models.TaskExecutionLog, error)
// FailAllIncompletePlanExecutionLogs 将所有状态为 ExecutionStatusStarted 和 ExecutionStatusWaiting 的计划状态都修改为 ExecutionStatusFailed
FailAllIncompletePlanExecutionLogs() error
FailAllIncompletePlanExecutionLogs(ctx context.Context) error
// CancelAllIncompleteTaskExecutionLogs 将所有状态为 ExecutionStatusStarted 和 ExecutionStatusWaiting 的任务状态修改为 ExecutionStatusCancelled
CancelAllIncompleteTaskExecutionLogs() error
CancelAllIncompleteTaskExecutionLogs(ctx context.Context) error
// FindPlanExecutionLogByID 根据ID查找计划执行日志
FindPlanExecutionLogByID(id uint) (*models.PlanExecutionLog, error)
FindPlanExecutionLogByID(ctx context.Context, id uint) (*models.PlanExecutionLog, error)
// CountIncompleteTasksByPlanLogID 计算一个计划执行中未完成的任务数量
CountIncompleteTasksByPlanLogID(planLogID uint) (int64, error)
CountIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint) (int64, error)
// FailPlanExecution 将指定的计划执行标记为失败
FailPlanExecution(planLogID uint, errorMessage string) error
FailPlanExecution(ctx context.Context, planLogID uint, errorMessage string) error
// CancelIncompleteTasksByPlanLogID 取消一个计划执行中的所有未完成任务
CancelIncompleteTasksByPlanLogID(planLogID uint, reason string) error
CancelIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint, reason string) error
// --- New methods ---
ListPlanExecutionLogs(opts PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, int64, error)
ListTaskExecutionLogs(opts TaskExecutionLogListOptions, page, pageSize int) ([]models.TaskExecutionLog, int64, error)
ListPlanExecutionLogs(ctx context.Context, opts PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, int64, error)
ListTaskExecutionLogs(ctx context.Context, opts TaskExecutionLogListOptions, page, pageSize int) ([]models.TaskExecutionLog, int64, error)
}
// gormExecutionLogRepository 是使用 GORM 的具体实现。
type gormExecutionLogRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormExecutionLogRepository 创建一个新的执行日志仓库。
func NewGormExecutionLogRepository(db *gorm.DB) ExecutionLogRepository {
return &gormExecutionLogRepository{db: db}
func NewGormExecutionLogRepository(ctx context.Context, db *gorm.DB) ExecutionLogRepository {
return &gormExecutionLogRepository{ctx: ctx, db: db}
}
// ListPlanExecutionLogs 实现了分页和过滤查询计划执行日志的功能
func (r *gormExecutionLogRepository) ListPlanExecutionLogs(opts PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, int64, error) {
func (r *gormExecutionLogRepository) ListPlanExecutionLogs(ctx context.Context, opts PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPlanExecutionLogs")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -92,7 +97,7 @@ func (r *gormExecutionLogRepository) ListPlanExecutionLogs(opts PlanExecutionLog
var results []models.PlanExecutionLog
var total int64
query := r.db.Model(&models.PlanExecutionLog{})
query := r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{})
if opts.PlanID != nil {
query = query.Where("plan_id = ?", *opts.PlanID)
@@ -124,7 +129,8 @@ func (r *gormExecutionLogRepository) ListPlanExecutionLogs(opts PlanExecutionLog
}
// ListTaskExecutionLogs 实现了分页和过滤查询任务执行日志的功能
func (r *gormExecutionLogRepository) ListTaskExecutionLogs(opts TaskExecutionLogListOptions, page, pageSize int) ([]models.TaskExecutionLog, int64, error) {
func (r *gormExecutionLogRepository) ListTaskExecutionLogs(ctx context.Context, opts TaskExecutionLogListOptions, page, pageSize int) ([]models.TaskExecutionLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListTaskExecutionLogs")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -132,7 +138,7 @@ func (r *gormExecutionLogRepository) ListTaskExecutionLogs(opts TaskExecutionLog
var results []models.TaskExecutionLog
var total int64
query := r.db.Model(&models.TaskExecutionLog{})
query := r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{})
if opts.PlanExecutionLogID != nil {
query = query.Where("plan_execution_log_id = ?", *opts.PlanExecutionLogID)
@@ -169,56 +175,64 @@ func (r *gormExecutionLogRepository) ListTaskExecutionLogs(opts TaskExecutionLog
// --- Existing method implementations ---
func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatusByIDs(logIDs []uint, status models.ExecutionStatus) error {
func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateTaskExecutionLogStatusByIDs")
if len(logIDs) == 0 {
return nil
}
return r.db.Model(&models.TaskExecutionLog{}).Where("id IN ?", logIDs).Update("status", status).Error
return r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).Where("id IN ?", logIDs).Update("status", status).Error
}
func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatus(logID uint, status models.ExecutionStatus) error {
return r.db.Model(&models.TaskExecutionLog{}).Where("id = ?", logID).Update("status", status).Error
func (r *gormExecutionLogRepository) UpdateTaskExecutionLogStatus(ctx context.Context, logID uint, 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
}
func (r *gormExecutionLogRepository) CreateTaskExecutionLog(log *models.TaskExecutionLog) error {
return r.db.Create(log).Error
func (r *gormExecutionLogRepository) CreateTaskExecutionLog(ctx context.Context, log *models.TaskExecutionLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateTaskExecutionLog")
return r.db.WithContext(repoCtx).Create(log).Error
}
// CreatePlanExecutionLog 为一次计划执行创建一条新的日志条目。
func (r *gormExecutionLogRepository) CreatePlanExecutionLog(log *models.PlanExecutionLog) error {
return r.db.Create(log).Error
func (r *gormExecutionLogRepository) CreatePlanExecutionLog(ctx context.Context, log *models.PlanExecutionLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePlanExecutionLog")
return r.db.WithContext(repoCtx).Create(log).Error
}
// UpdatePlanExecutionLog 使用 Updates 方法更新一个计划执行日志。
// GORM 的 Updates 传入 struct 时,只会更新非零值字段。
// 在这里,我们期望传入的对象一定包含一个有效的 ID。
func (r *gormExecutionLogRepository) UpdatePlanExecutionLog(log *models.PlanExecutionLog) error {
return r.db.Updates(log).Error
func (r *gormExecutionLogRepository) UpdatePlanExecutionLog(ctx context.Context, log *models.PlanExecutionLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanExecutionLog")
return r.db.WithContext(repoCtx).Updates(log).Error
}
// CreateTaskExecutionLogsInBatch 在一次数据库调用中创建多个任务执行日志条目。
// 这是“预写日志”步骤的关键。
func (r *gormExecutionLogRepository) CreateTaskExecutionLogsInBatch(logs []*models.TaskExecutionLog) error {
if len(logs) == 0 {
func (r *gormExecutionLogRepository) CreateTaskExecutionLogsInBatch(ctx context.Context, executionLogs []*models.TaskExecutionLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateTaskExecutionLogsInBatch")
if len(executionLogs) == 0 {
return nil
}
// GORM 的 CreateTx 传入一个切片指针会执行批量插入。
return r.db.Create(&logs).Error
return r.db.WithContext(repoCtx).Create(&executionLogs).Error
}
// UpdateTaskExecutionLog 使用 Updates 方法更新一个任务执行日志。
// GORM 的 Updates 传入 struct 时,只会更新非零值字段。
// 这种方式代码更直观,上层服务可以直接修改模型对象后进行保存。
func (r *gormExecutionLogRepository) UpdateTaskExecutionLog(log *models.TaskExecutionLog) error {
return r.db.Updates(log).Error
func (r *gormExecutionLogRepository) UpdateTaskExecutionLog(ctx context.Context, log *models.TaskExecutionLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateTaskExecutionLog")
return r.db.WithContext(repoCtx).Updates(log).Error
}
// FindTaskExecutionLogByID 根据 ID 查找单个任务执行日志。
// 它会预加载关联的 Task 信息。
func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(id uint) (*models.TaskExecutionLog, error) {
func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(ctx context.Context, id uint) (*models.TaskExecutionLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindTaskExecutionLogByID")
var log models.TaskExecutionLog
// 使用 Preload("Task") 来确保关联的任务信息被一并加载
err := r.db.Preload("Task").First(&log, id).Error
err := r.db.WithContext(repoCtx).Preload("Task").First(&log, id).Error
if err != nil {
return nil, err
}
@@ -226,29 +240,33 @@ func (r *gormExecutionLogRepository) FindTaskExecutionLogByID(id uint) (*models.
}
// UpdatePlanExecutionLogStatus 更新计划执行日志的状态
func (r *gormExecutionLogRepository) UpdatePlanExecutionLogStatus(logID uint, status models.ExecutionStatus) error {
return r.db.Model(&models.PlanExecutionLog{}).Where("id = ?", logID).Update("status", status).Error
func (r *gormExecutionLogRepository) UpdatePlanExecutionLogStatus(ctx context.Context, logID uint, 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(logIDs []uint, status models.ExecutionStatus) error {
func (r *gormExecutionLogRepository) UpdatePlanExecutionLogsStatusByIDs(ctx context.Context, logIDs []uint, status models.ExecutionStatus) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanExecutionLogsStatusByIDs")
if len(logIDs) == 0 {
return nil
}
return r.db.Model(&models.PlanExecutionLog{}).Where("id IN ?", logIDs).Update("status", status).Error
return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}).Where("id IN ?", logIDs).Update("status", status).Error
}
// FindIncompletePlanExecutionLogs 查找所有未完成的计划执行日志
func (r *gormExecutionLogRepository) FindIncompletePlanExecutionLogs() ([]models.PlanExecutionLog, error) {
func (r *gormExecutionLogRepository) FindIncompletePlanExecutionLogs(ctx context.Context) ([]models.PlanExecutionLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindIncompletePlanExecutionLogs")
var logs []models.PlanExecutionLog
err := r.db.Where("status = ? OR status = ?", models.ExecutionStatusStarted, models.ExecutionStatusWaiting).Find(&logs).Error
err := r.db.WithContext(repoCtx).Where("status = ? OR status = ?", models.ExecutionStatusStarted, models.ExecutionStatusWaiting).Find(&logs).Error
return logs, err
}
// FindInProgressPlanExecutionLogByPlanID 根据 PlanID 查找正在进行的计划执行日志
func (r *gormExecutionLogRepository) FindInProgressPlanExecutionLogByPlanID(planID uint) (*models.PlanExecutionLog, error) {
func (r *gormExecutionLogRepository) FindInProgressPlanExecutionLogByPlanID(ctx context.Context, planID uint) (*models.PlanExecutionLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindInProgressPlanExecutionLogByPlanID")
var log models.PlanExecutionLog
err := r.db.Where("plan_id = ? AND status = ?", planID, models.ExecutionStatusStarted).First(&log).Error
err := r.db.WithContext(repoCtx).Where("plan_id = ? AND status = ?", planID, models.ExecutionStatusStarted).First(&log).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 未找到不是一个需要上报的错误,代表计划当前没有在运行
@@ -261,31 +279,35 @@ func (r *gormExecutionLogRepository) FindInProgressPlanExecutionLogByPlanID(plan
}
// FindIncompleteTaskExecutionLogsByPlanLogID 根据计划日志ID查找所有未完成的任务日志
func (r *gormExecutionLogRepository) FindIncompleteTaskExecutionLogsByPlanLogID(planLogID uint) ([]models.TaskExecutionLog, error) {
func (r *gormExecutionLogRepository) FindIncompleteTaskExecutionLogsByPlanLogID(ctx context.Context, planLogID uint) ([]models.TaskExecutionLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindIncompleteTaskExecutionLogsByPlanLogID")
var logs []models.TaskExecutionLog
err := r.db.Where("plan_execution_log_id = ? AND (status = ? OR status = ?)",
err := r.db.WithContext(repoCtx).Where("plan_execution_log_id = ? AND (status = ? OR status = ?)",
planLogID, models.ExecutionStatusWaiting, models.ExecutionStatusStarted).Find(&logs).Error
return logs, err
}
// FailAllIncompletePlanExecutionLogs 将所有状态为 ExecutionStatusStarted 和 ExecutionStatusWaiting 的计划状态都修改为 ExecutionStatusFailed
func (r *gormExecutionLogRepository) FailAllIncompletePlanExecutionLogs() error {
return r.db.Model(&models.PlanExecutionLog{}).
func (r *gormExecutionLogRepository) FailAllIncompletePlanExecutionLogs(ctx context.Context) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FailAllIncompletePlanExecutionLogs")
return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}).
Where("status IN (?, ?)", models.ExecutionStatusStarted, models.ExecutionStatusWaiting).
Updates(map[string]interface{}{"status": models.ExecutionStatusFailed, "ended_at": time.Now(), "error": "系统中断"}).Error
}
// CancelAllIncompleteTaskExecutionLogs 将所有状态为 ExecutionStatusStarted 和 ExecutionStatusWaiting 的任务状态修改为 ExecutionStatusCancelled
func (r *gormExecutionLogRepository) CancelAllIncompleteTaskExecutionLogs() error {
return r.db.Model(&models.TaskExecutionLog{}).
func (r *gormExecutionLogRepository) CancelAllIncompleteTaskExecutionLogs(ctx context.Context) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CancelAllIncompleteTaskExecutionLogs")
return r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).
Where("status IN (?, ?)", models.ExecutionStatusStarted, models.ExecutionStatusWaiting).
Updates(map[string]interface{}{"status": models.ExecutionStatusCancelled, "ended_at": time.Now(), "output": "系统中断"}).Error
}
// FindPlanExecutionLogByID 根据ID查找计划执行日志
func (r *gormExecutionLogRepository) FindPlanExecutionLogByID(id uint) (*models.PlanExecutionLog, error) {
func (r *gormExecutionLogRepository) FindPlanExecutionLogByID(ctx context.Context, id uint) (*models.PlanExecutionLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPlanExecutionLogByID")
var log models.PlanExecutionLog
err := r.db.First(&log, id).Error
err := r.db.WithContext(repoCtx).First(&log, id).Error
if err != nil {
return nil, err
}
@@ -293,9 +315,10 @@ func (r *gormExecutionLogRepository) FindPlanExecutionLogByID(id uint) (*models.
}
// CountIncompleteTasksByPlanLogID 计算一个计划执行中未完成的任务数量
func (r *gormExecutionLogRepository) CountIncompleteTasksByPlanLogID(planLogID uint) (int64, error) {
func (r *gormExecutionLogRepository) CountIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CountIncompleteTasksByPlanLogID")
var count int64
err := r.db.Model(&models.TaskExecutionLog{}).
err := r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).
Where("plan_execution_log_id = ? AND status IN (?, ?)",
planLogID, models.ExecutionStatusWaiting, models.ExecutionStatusStarted).
Count(&count).Error
@@ -303,8 +326,9 @@ func (r *gormExecutionLogRepository) CountIncompleteTasksByPlanLogID(planLogID u
}
// FailPlanExecution 将指定的计划执行标记为失败
func (r *gormExecutionLogRepository) FailPlanExecution(planLogID uint, errorMessage string) error {
return r.db.Model(&models.PlanExecutionLog{}).
func (r *gormExecutionLogRepository) FailPlanExecution(ctx context.Context, planLogID uint, errorMessage string) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FailPlanExecution")
return r.db.WithContext(repoCtx).Model(&models.PlanExecutionLog{}).
Where("id = ?", planLogID).
Updates(map[string]interface{}{
"status": models.ExecutionStatusFailed,
@@ -314,8 +338,9 @@ func (r *gormExecutionLogRepository) FailPlanExecution(planLogID uint, errorMess
}
// CancelIncompleteTasksByPlanLogID 取消一个计划执行中的所有未完成任务
func (r *gormExecutionLogRepository) CancelIncompleteTasksByPlanLogID(planLogID uint, reason string) error {
return r.db.Model(&models.TaskExecutionLog{}).
func (r *gormExecutionLogRepository) CancelIncompleteTasksByPlanLogID(ctx context.Context, planLogID uint, 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 (?, ?)",
planLogID, models.ExecutionStatusWaiting, models.ExecutionStatusStarted).
Updates(map[string]interface{}{

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -20,27 +23,30 @@ type MedicationLogListOptions struct {
// MedicationLogRepository 定义了与群体用药日志模型相关的数据库操作接口。
type MedicationLogRepository interface {
CreateMedicationLog(log *models.MedicationLog) error
ListMedicationLogs(opts MedicationLogListOptions, page, pageSize int) ([]models.MedicationLog, int64, error)
CreateMedicationLog(ctx context.Context, log *models.MedicationLog) error
ListMedicationLogs(ctx context.Context, opts MedicationLogListOptions, page, pageSize int) ([]models.MedicationLog, int64, error)
}
// gormMedicationLogRepository 是 MedicationLogRepository 接口的 GORM 实现。
type gormMedicationLogRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormMedicationLogRepository 创建一个新的 MedicationLogRepository GORM 实现实例。
func NewGormMedicationLogRepository(db *gorm.DB) MedicationLogRepository {
return &gormMedicationLogRepository{db: db}
func NewGormMedicationLogRepository(ctx context.Context, db *gorm.DB) MedicationLogRepository {
return &gormMedicationLogRepository{ctx: ctx, db: db}
}
// CreateMedicationLog 创建一条新的群体用药日志记录
func (r *gormMedicationLogRepository) CreateMedicationLog(log *models.MedicationLog) error {
return r.db.Create(log).Error
func (r *gormMedicationLogRepository) CreateMedicationLog(ctx context.Context, log *models.MedicationLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateMedicationLog")
return r.db.WithContext(repoCtx).Create(log).Error
}
// ListMedicationLogs 实现了分页和过滤查询用药记录的功能
func (r *gormMedicationLogRepository) ListMedicationLogs(opts MedicationLogListOptions, page, pageSize int) ([]models.MedicationLog, int64, error) {
func (r *gormMedicationLogRepository) ListMedicationLogs(ctx context.Context, opts MedicationLogListOptions, page, pageSize int) ([]models.MedicationLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListMedicationLogs")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -48,7 +54,7 @@ func (r *gormMedicationLogRepository) ListMedicationLogs(opts MedicationLogListO
var results []models.MedicationLog
var total int64
query := r.db.Model(&models.MedicationLog{})
query := r.db.WithContext(repoCtx).Model(&models.MedicationLog{})
if opts.PigBatchID != nil {
query = query.Where("pig_batch_id = ?", *opts.PigBatchID)

View File

@@ -1,10 +1,13 @@
package repository
import (
"context"
"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/notify"
"go.uber.org/zap/zapcore"
"gorm.io/gorm"
)
@@ -23,44 +26,49 @@ type NotificationListOptions struct {
// NotificationRepository 定义了与通知记录相关的数据库操作接口。
type NotificationRepository interface {
// Create 将一条新的通知记录插入数据库。
Create(notification *models.Notification) error
Create(ctx context.Context, notification *models.Notification) error
// CreateInTx 在给定的事务中插入一条新的通知记录。
CreateInTx(tx *gorm.DB, notification *models.Notification) error
CreateInTx(ctx context.Context, tx *gorm.DB, notification *models.Notification) error
// BatchCreate 批量插入多条通知记录。
BatchCreate(notifications []*models.Notification) error
BatchCreate(ctx context.Context, notifications []*models.Notification) error
// List 支持分页和过滤的通知列表查询。
// 返回通知列表、总记录数和错误。
List(opts NotificationListOptions, page, pageSize int) ([]models.Notification, int64, error)
List(ctx context.Context, opts NotificationListOptions, page, pageSize int) ([]models.Notification, int64, error)
}
// gormNotificationRepository 是 NotificationRepository 的 GORM 实现。
type gormNotificationRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormNotificationRepository 创建一个新的 NotificationRepository GORM 实现实例。
func NewGormNotificationRepository(db *gorm.DB) NotificationRepository {
return &gormNotificationRepository{db: db}
func NewGormNotificationRepository(ctx context.Context, db *gorm.DB) NotificationRepository {
return &gormNotificationRepository{ctx: ctx, db: db}
}
// Create 将一条新的通知记录插入数据库。
func (r *gormNotificationRepository) Create(notification *models.Notification) error {
return r.db.Create(notification).Error
func (r *gormNotificationRepository) Create(ctx context.Context, notification *models.Notification) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
return r.db.WithContext(repoCtx).Create(notification).Error
}
// CreateInTx 在给定的事务中插入一条新的通知记录。
func (r *gormNotificationRepository) CreateInTx(tx *gorm.DB, notification *models.Notification) error {
return tx.Create(notification).Error
func (r *gormNotificationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, notification *models.Notification) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateInTx")
return tx.WithContext(repoCtx).Create(notification).Error
}
// BatchCreate 批量插入多条通知记录。
func (r *gormNotificationRepository) BatchCreate(notifications []*models.Notification) error {
func (r *gormNotificationRepository) BatchCreate(ctx context.Context, notifications []*models.Notification) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "BatchCreate")
// GORM 的 Create 方法在传入切片时会自动进行批量插入
return r.db.Create(&notifications).Error
return r.db.WithContext(repoCtx).Create(&notifications).Error
}
// List 实现了分页和过滤查询通知记录的功能
func (r *gormNotificationRepository) List(opts NotificationListOptions, page, pageSize int) ([]models.Notification, int64, error) {
func (r *gormNotificationRepository) List(ctx context.Context, opts NotificationListOptions, page, pageSize int) ([]models.Notification, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "List")
// --- 校验分页参数 ---
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination // 复用已定义的错误
@@ -69,7 +77,7 @@ func (r *gormNotificationRepository) List(opts NotificationListOptions, page, pa
var results []models.Notification
var total int64
query := r.db.Model(&models.Notification{})
query := r.db.WithContext(repoCtx).Model(&models.Notification{})
// --- 应用过滤条件 ---
if opts.UserID != nil {

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -19,48 +22,52 @@ type PendingCollectionListOptions struct {
// PendingCollectionRepository 定义了与待采集请求相关的数据库操作接口。
type PendingCollectionRepository interface {
// Create 创建一个新的待采集请求。
Create(req *models.PendingCollection) error
Create(ctx context.Context, req *models.PendingCollection) error
// FindByCorrelationID 根据关联ID查找一个待采集请求。
FindByCorrelationID(correlationID string) (*models.PendingCollection, error)
FindByCorrelationID(ctx context.Context, correlationID string) (*models.PendingCollection, error)
// UpdateStatusToFulfilled 将指定关联ID的请求状态更新为“已完成”。
UpdateStatusToFulfilled(correlationID string, fulfilledAt time.Time) error
UpdateStatusToFulfilled(ctx context.Context, correlationID string, fulfilledAt time.Time) error
// MarkAllPendingAsTimedOut 将所有“待处理”请求更新为“已超时”。
MarkAllPendingAsTimedOut() (int64, error)
MarkAllPendingAsTimedOut(ctx context.Context) (int64, error)
// List 支持分页和过滤的列表查询
List(opts PendingCollectionListOptions, page, pageSize int) ([]models.PendingCollection, int64, error)
List(ctx context.Context, opts PendingCollectionListOptions, page, pageSize int) ([]models.PendingCollection, int64, error)
}
// gormPendingCollectionRepository 是 PendingCollectionRepository 的 GORM 实现。
type gormPendingCollectionRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPendingCollectionRepository 创建一个新的 PendingCollectionRepository GORM 实现实例。
func NewGormPendingCollectionRepository(db *gorm.DB) PendingCollectionRepository {
return &gormPendingCollectionRepository{db: db}
func NewGormPendingCollectionRepository(ctx context.Context, db *gorm.DB) PendingCollectionRepository {
return &gormPendingCollectionRepository{ctx: ctx, db: db}
}
// Create 创建一个新的待采集请求。
func (r *gormPendingCollectionRepository) Create(req *models.PendingCollection) error {
return r.db.Create(req).Error
func (r *gormPendingCollectionRepository) Create(ctx context.Context, req *models.PendingCollection) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
return r.db.WithContext(repoCtx).Create(req).Error
}
// FindByCorrelationID 根据关联ID查找一个待采集请求。
func (r *gormPendingCollectionRepository) FindByCorrelationID(correlationID string) (*models.PendingCollection, error) {
func (r *gormPendingCollectionRepository) FindByCorrelationID(ctx context.Context, correlationID string) (*models.PendingCollection, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByCorrelationID")
var req models.PendingCollection
if err := r.db.First(&req, "correlation_id = ?", correlationID).Error; err != nil {
if err := r.db.WithContext(repoCtx).First(&req, "correlation_id = ?", correlationID).Error; err != nil {
return nil, err
}
return &req, nil
}
// UpdateStatusToFulfilled 将指定关联ID的请求状态更新为“已完成”。
func (r *gormPendingCollectionRepository) UpdateStatusToFulfilled(correlationID string, fulfilledAt time.Time) error {
return r.db.Model(&models.PendingCollection{}).
func (r *gormPendingCollectionRepository) UpdateStatusToFulfilled(ctx context.Context, correlationID string, fulfilledAt time.Time) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateStatusToFulfilled")
return r.db.WithContext(repoCtx).Model(&models.PendingCollection{}).
Where("correlation_id = ?", correlationID).
Updates(map[string]interface{}{
"status": models.PendingStatusFulfilled,
@@ -70,8 +77,9 @@ func (r *gormPendingCollectionRepository) UpdateStatusToFulfilled(correlationID
// MarkAllPendingAsTimedOut 将所有状态为 'pending' 的记录更新为 'timed_out'。
// 返回被更新的记录数量和错误。
func (r *gormPendingCollectionRepository) MarkAllPendingAsTimedOut() (int64, error) {
result := r.db.Model(&models.PendingCollection{}).
func (r *gormPendingCollectionRepository) MarkAllPendingAsTimedOut(ctx context.Context) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "MarkAllPendingAsTimedOut")
result := r.db.WithContext(repoCtx).Model(&models.PendingCollection{}).
Where("status = ?", models.PendingStatusPending).
Update("status", models.PendingStatusTimedOut)
@@ -79,7 +87,8 @@ func (r *gormPendingCollectionRepository) MarkAllPendingAsTimedOut() (int64, err
}
// List 实现了分页和过滤查询待采集请求的功能
func (r *gormPendingCollectionRepository) List(opts PendingCollectionListOptions, page, pageSize int) ([]models.PendingCollection, int64, error) {
func (r *gormPendingCollectionRepository) List(ctx context.Context, opts PendingCollectionListOptions, page, pageSize int) ([]models.PendingCollection, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "List")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -87,7 +96,7 @@ func (r *gormPendingCollectionRepository) List(opts PendingCollectionListOptions
var results []models.PendingCollection
var total int64
query := r.db.Model(&models.PendingCollection{})
query := r.db.WithContext(repoCtx).Model(&models.PendingCollection{})
if opts.DeviceID != nil {
query = query.Where("device_id = ?", *opts.DeviceID)

View File

@@ -1,63 +1,69 @@
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"
"gorm.io/gorm/clause"
)
// PendingTaskRepository 定义了与待执行任务队列交互的接口。
type PendingTaskRepository interface {
FindAllPendingTasks() ([]models.PendingTask, error)
FindPendingTriggerByPlanID(planID uint) (*models.PendingTask, error)
DeletePendingTasksByIDs(ids []uint) error
CreatePendingTask(task *models.PendingTask) error
CreatePendingTasksInBatch(tasks []*models.PendingTask) error
FindAllPendingTasks(ctx context.Context) ([]models.PendingTask, error)
FindPendingTriggerByPlanID(ctx context.Context, planID uint) (*models.PendingTask, error)
DeletePendingTasksByIDs(ctx context.Context, ids []uint) error
CreatePendingTask(ctx context.Context, task *models.PendingTask) error
CreatePendingTasksInBatch(ctx context.Context, tasks []*models.PendingTask) error
// UpdatePendingTaskExecuteAt 更新指定待执行任务的执行时间
UpdatePendingTaskExecuteAt(id uint, executeAt time.Time) error
UpdatePendingTaskExecuteAt(ctx context.Context, id uint, executeAt time.Time) error
// ClearAllPendingTasks 清空所有待执行任务
ClearAllPendingTasks() error
ClearAllPendingTasks(ctx context.Context) error
// ClaimNextAvailableTask 原子地认领下一个可用的任务。
// 它会同时返回被认领任务对应的日志对象,以及被删除的待办任务对象的内存副本。
ClaimNextAvailableTask(excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error)
ClaimNextAvailableTask(ctx context.Context, excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error)
// RequeueTask 安全地将一个任务重新放回队列。
RequeueTask(originalPendingTask *models.PendingTask) error
RequeueTask(ctx context.Context, originalPendingTask *models.PendingTask) error
// FindPendingTasksByTaskLogIDs 根据 TaskExecutionLogID 列表查找对应的待执行任务
FindPendingTasksByTaskLogIDs(taskLogIDs []uint) ([]models.PendingTask, error)
FindPendingTasksByTaskLogIDs(ctx context.Context, taskLogIDs []uint) ([]models.PendingTask, error)
// DeletePendingTasksByPlanLogID 删除与指定计划执行日志ID相关的所有待执行任务
DeletePendingTasksByPlanLogID(planLogID uint) error
DeletePendingTasksByPlanLogID(ctx context.Context, planLogID uint) error
}
// gormPendingTaskRepository 是使用 GORM 的具体实现。
type gormPendingTaskRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPendingTaskRepository 创建一个新的待执行任务队列仓库。
func NewGormPendingTaskRepository(db *gorm.DB) PendingTaskRepository {
return &gormPendingTaskRepository{db: db}
func NewGormPendingTaskRepository(ctx context.Context, db *gorm.DB) PendingTaskRepository {
return &gormPendingTaskRepository{ctx: ctx, db: db}
}
func (r *gormPendingTaskRepository) FindAllPendingTasks() ([]models.PendingTask, error) {
func (r *gormPendingTaskRepository) FindAllPendingTasks(ctx context.Context) ([]models.PendingTask, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindAllPendingTasks")
var tasks []models.PendingTask
// 预加载 Task 以便后续访问 Task.PlanID
// 预加载 TaskExecutionLog 以便后续访问 PlanExecutionLogID
err := r.db.Preload("Task").Preload("TaskExecutionLog").Find(&tasks).Error
err := r.db.WithContext(repoCtx).Preload("Task").Preload("TaskExecutionLog").Find(&tasks).Error
return tasks, err
}
func (r *gormPendingTaskRepository) FindPendingTriggerByPlanID(planID uint) (*models.PendingTask, error) {
func (r *gormPendingTaskRepository) FindPendingTriggerByPlanID(ctx context.Context, planID uint) (*models.PendingTask, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPendingTriggerByPlanID")
var pendingTask models.PendingTask
// 关键修改:通过 JOIN tasks 表并查询 parameters JSON 字段来查找触发器,而不是依赖 task.plan_id
err := r.db.
err := r.db.WithContext(repoCtx).
Joins("JOIN tasks ON tasks.id = pending_tasks.task_id").
Where("tasks.type = ? AND tasks.parameters->>'plan_id' = ?", models.TaskPlanAnalysis, fmt.Sprintf("%d", planID)).
First(&pendingTask).Error
@@ -67,42 +73,48 @@ func (r *gormPendingTaskRepository) FindPendingTriggerByPlanID(planID uint) (*mo
return &pendingTask, err
}
func (r *gormPendingTaskRepository) DeletePendingTasksByIDs(ids []uint) error {
func (r *gormPendingTaskRepository) DeletePendingTasksByIDs(ctx context.Context, ids []uint) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePendingTasksByIDs")
if len(ids) == 0 {
return nil
}
return r.db.Where("id IN ?", ids).Delete(&models.PendingTask{}).Error
return r.db.WithContext(repoCtx).Where("id IN ?", ids).Delete(&models.PendingTask{}).Error
}
func (r *gormPendingTaskRepository) CreatePendingTask(task *models.PendingTask) error {
return r.db.Create(task).Error
func (r *gormPendingTaskRepository) CreatePendingTask(ctx context.Context, task *models.PendingTask) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePendingTask")
return r.db.WithContext(repoCtx).Create(task).Error
}
// CreatePendingTasksInBatch 在一次数据库调用中创建多个待执行任务条目。
func (r *gormPendingTaskRepository) CreatePendingTasksInBatch(tasks []*models.PendingTask) error {
func (r *gormPendingTaskRepository) CreatePendingTasksInBatch(ctx context.Context, tasks []*models.PendingTask) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePendingTasksInBatch")
if len(tasks) == 0 {
return nil
}
return r.db.Create(&tasks).Error
return r.db.WithContext(repoCtx).Create(&tasks).Error
}
// UpdatePendingTaskExecuteAt 更新指定待执行任务的执行时间
func (r *gormPendingTaskRepository) UpdatePendingTaskExecuteAt(id uint, executeAt time.Time) error {
return r.db.Model(&models.PendingTask{}).Where("id = ?", id).Update("execute_at", executeAt).Error
func (r *gormPendingTaskRepository) UpdatePendingTaskExecuteAt(ctx context.Context, id uint, 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
}
// ClearAllPendingTasks 清空所有待执行任务
func (r *gormPendingTaskRepository) ClearAllPendingTasks() error {
return r.db.Where("1 = 1").Delete(&models.PendingTask{}).Error
func (r *gormPendingTaskRepository) ClearAllPendingTasks(ctx context.Context) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ClearAllPendingTasks")
return r.db.WithContext(repoCtx).Where("1 = 1").Delete(&models.PendingTask{}).Error
}
// ClaimNextAvailableTask 以原子方式认领下一个可用的任务。
func (r *gormPendingTaskRepository) ClaimNextAvailableTask(excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error) {
func (r *gormPendingTaskRepository) ClaimNextAvailableTask(ctx context.Context, excludePlanIDs []uint) (*models.TaskExecutionLog, *models.PendingTask, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ClaimNextAvailableTask")
var log models.TaskExecutionLog
var pendingTask models.PendingTask
err := r.db.Transaction(func(tx *gorm.DB) error {
query := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
err := r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
query := tx.WithContext(repoCtx).Clauses(clause.Locking{Strength: "UPDATE"}).
Where("execute_at <= ?", time.Now()).
Order("execute_at ASC")
@@ -114,7 +126,7 @@ func (r *gormPendingTaskRepository) ClaimNextAvailableTask(excludePlanIDs []uint
return err
}
if err := tx.Unscoped().Delete(&pendingTask).Error; err != nil {
if err := tx.WithContext(repoCtx).Unscoped().Delete(&pendingTask).Error; err != nil {
return err
}
@@ -122,12 +134,12 @@ func (r *gormPendingTaskRepository) ClaimNextAvailableTask(excludePlanIDs []uint
"status": models.ExecutionStatusStarted,
"started_at": time.Now(),
}
if err := tx.Model(&models.TaskExecutionLog{}).Where("id = ?", pendingTask.TaskExecutionLogID).Updates(updates).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).Where("id = ?", pendingTask.TaskExecutionLogID).Updates(updates).Error; err != nil {
return err
}
// 关键修改:在 Preload("Task") 时,使用 Unscoped() 来忽略 Task 的软删除状态
if err := tx.Preload("Task", func(db *gorm.DB) *gorm.DB {
if err := tx.WithContext(repoCtx).Preload("Task", func(db *gorm.DB) *gorm.DB {
return db.Unscoped()
}).First(&log, pendingTask.TaskExecutionLogID).Error; err != nil {
return err
@@ -145,10 +157,11 @@ func (r *gormPendingTaskRepository) ClaimNextAvailableTask(excludePlanIDs []uint
// RequeueTask 安全地将一个任务重新放回队列。
// 它通过将原始 PendingTask 的 ID 重置为 0并重新创建它来实现。
func (r *gormPendingTaskRepository) RequeueTask(originalPendingTask *models.PendingTask) error {
return r.db.Transaction(func(tx *gorm.DB) error {
func (r *gormPendingTaskRepository) RequeueTask(ctx context.Context, originalPendingTask *models.PendingTask) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "RequeueTask")
return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
// 1. 将日志状态恢复为 waiting
if err := tx.Model(&models.TaskExecutionLog{}).Where("id = ?", originalPendingTask.TaskExecutionLogID).Update("status", models.ExecutionStatusWaiting).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).Where("id = ?", originalPendingTask.TaskExecutionLogID).Update("status", models.ExecutionStatusWaiting).Error; err != nil {
return err
}
@@ -157,25 +170,27 @@ func (r *gormPendingTaskRepository) RequeueTask(originalPendingTask *models.Pend
originalPendingTask.ID = 0
// 3. 重新创建待办任务。GORM 会忽略掉已被重置的 ID并让数据库生成一个新的主键。
return tx.Create(originalPendingTask).Error
return tx.WithContext(repoCtx).Create(originalPendingTask).Error
})
}
// FindPendingTasksByTaskLogIDs 根据 TaskExecutionLogID 列表查找对应的待执行任务
func (r *gormPendingTaskRepository) FindPendingTasksByTaskLogIDs(taskLogIDs []uint) ([]models.PendingTask, error) {
func (r *gormPendingTaskRepository) FindPendingTasksByTaskLogIDs(ctx context.Context, taskLogIDs []uint) ([]models.PendingTask, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPendingTasksByTaskLogIDs")
if len(taskLogIDs) == 0 {
return []models.PendingTask{}, nil
}
var pendingTasks []models.PendingTask
err := r.db.Where("task_execution_log_id IN ?", taskLogIDs).Find(&pendingTasks).Error
err := r.db.WithContext(repoCtx).Where("task_execution_log_id IN ?", taskLogIDs).Find(&pendingTasks).Error
return pendingTasks, err
}
// DeletePendingTasksByPlanLogID 删除与指定计划执行日志ID相关的所有待执行任务
func (r *gormPendingTaskRepository) DeletePendingTasksByPlanLogID(planLogID uint) error {
func (r *gormPendingTaskRepository) DeletePendingTasksByPlanLogID(ctx context.Context, planLogID uint) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePendingTasksByPlanLogID")
// 使用子查询找到所有与 planLogID 相关的 task_execution_log_id
subQuery := r.db.Model(&models.TaskExecutionLog{}).Select("id").Where("plan_execution_log_id = ?", planLogID)
subQuery := r.db.WithContext(repoCtx).Model(&models.TaskExecutionLog{}).Select("id").Where("plan_execution_log_id = ?", planLogID)
// 使用子查询的结果来删除待执行任务
return r.db.Where("task_execution_log_id IN (?)", subQuery).Delete(&models.PendingTask{}).Error
return r.db.WithContext(repoCtx).Where("task_execution_log_id IN (?)", subQuery).Delete(&models.PendingTask{}).Error
}

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -20,37 +23,40 @@ type PigBatchLogListOptions struct {
// PigBatchLogRepository 定义了与猪批次日志相关的数据库操作接口。
type PigBatchLogRepository interface {
// CreateTx 在指定的事务中创建一条新的猪批次日志。
CreateTx(tx *gorm.DB, log *models.PigBatchLog) error
CreateTx(ctx context.Context, tx *gorm.DB, log *models.PigBatchLog) error
// GetLogsByBatchIDAndDateRangeTx 在指定的事务中,获取指定批次在特定时间范围内的所有日志记录。
GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error)
GetLogsByBatchIDAndDateRangeTx(ctx context.Context, tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error)
// GetLastLogByBatchIDTx 在指定的事务中,获取某批次的最后一条日志记录。
GetLastLogByBatchIDTx(tx *gorm.DB, batchID uint) (*models.PigBatchLog, error)
GetLastLogByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint) (*models.PigBatchLog, error)
// List 支持分页和过滤的列表查询
List(opts PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error)
List(ctx context.Context, opts PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error)
}
// gormPigBatchLogRepository 是 PigBatchLogRepository 的 GORM 实现。
type gormPigBatchLogRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPigBatchLogRepository 创建一个新的 PigBatchLogRepository 实例。
func NewGormPigBatchLogRepository(db *gorm.DB) PigBatchLogRepository {
return &gormPigBatchLogRepository{db: db}
func NewGormPigBatchLogRepository(ctx context.Context, db *gorm.DB) PigBatchLogRepository {
return &gormPigBatchLogRepository{ctx: ctx, db: db}
}
// CreateTx 实现了在事务中创建猪批次日志的逻辑。
func (r *gormPigBatchLogRepository) CreateTx(tx *gorm.DB, log *models.PigBatchLog) error {
return tx.Create(log).Error
func (r *gormPigBatchLogRepository) CreateTx(ctx context.Context, tx *gorm.DB, log *models.PigBatchLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateTx")
return tx.WithContext(repoCtx).Create(log).Error
}
// GetLogsByBatchIDAndDateRangeTx 实现了在指定的事务中,获取指定批次在特定时间范围内的所有日志记录的逻辑。
func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) {
func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(ctx context.Context, tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLogsByBatchIDAndDateRangeTx")
var logs []*models.PigBatchLog
err := tx.Where("pig_batch_id = ? AND created_at >= ? AND created_at <= ?", batchID, startDate, endDate).Find(&logs).Error
err := tx.WithContext(repoCtx).Where("pig_batch_id = ? AND created_at >= ? AND created_at <= ?", batchID, startDate, endDate).Find(&logs).Error
if err != nil {
return nil, err
}
@@ -58,9 +64,10 @@ func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB,
}
// GetLastLogByBatchIDTx 实现了在指定的事务中,获取某批次的最后一条日志记录的逻辑。
func (r *gormPigBatchLogRepository) GetLastLogByBatchIDTx(tx *gorm.DB, batchID uint) (*models.PigBatchLog, error) {
func (r *gormPigBatchLogRepository) GetLastLogByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint) (*models.PigBatchLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLastLogByBatchIDTx")
var log models.PigBatchLog
err := tx.Where("pig_batch_id = ?", batchID).Order("id DESC").First(&log).Error
err := tx.WithContext(repoCtx).Where("pig_batch_id = ?", batchID).Order("id DESC").First(&log).Error
if err != nil {
return nil, err
}
@@ -68,7 +75,8 @@ func (r *gormPigBatchLogRepository) GetLastLogByBatchIDTx(tx *gorm.DB, batchID u
}
// List 实现了分页和过滤查询猪批次日志的功能
func (r *gormPigBatchLogRepository) List(opts PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error) {
func (r *gormPigBatchLogRepository) List(ctx context.Context, opts PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "List")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -76,7 +84,7 @@ func (r *gormPigBatchLogRepository) List(opts PigBatchLogListOptions, page, page
var results []models.PigBatchLog
var total int64
query := r.db.Model(&models.PigBatchLog{})
query := r.db.WithContext(repoCtx).Model(&models.PigBatchLog{})
if opts.PigBatchID != nil {
query = query.Where("pig_batch_id = ?", *opts.PigBatchID)

View File

@@ -1,30 +1,33 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigBatchRepository 定义了与猪批次相关的数据库操作接口
type PigBatchRepository interface {
CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error)
CreatePigBatchTx(tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error)
GetPigBatchByID(id uint) (*models.PigBatch, error)
GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error)
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)
// UpdatePigBatch 更新一个猪批次,返回更新后的批次、受影响的行数和错误
UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error)
UpdatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, int64, error)
// DeletePigBatch 根据ID删除一个猪批次返回受影响的行数和错误
DeletePigBatch(id uint) (int64, error)
DeletePigBatchTx(tx *gorm.DB, id uint) (int64, error)
ListPigBatches(isActive *bool) ([]*models.PigBatch, error)
DeletePigBatch(ctx context.Context, id uint) (int64, error)
DeletePigBatchTx(ctx context.Context, tx *gorm.DB, id uint) (int64, error)
ListPigBatches(ctx context.Context, isActive *bool) ([]*models.PigBatch, error)
// ListWeighingBatches 支持分页和过滤的批次称重列表查询
ListWeighingBatches(opts WeighingBatchListOptions, page, pageSize int) ([]models.WeighingBatch, int64, error)
ListWeighingBatches(ctx context.Context, opts WeighingBatchListOptions, page, pageSize int) ([]models.WeighingBatch, int64, error)
// ListWeighingRecords 支持分页和过滤的单次称重记录列表查询
ListWeighingRecords(opts WeighingRecordListOptions, page, pageSize int) ([]models.WeighingRecord, int64, error)
ListWeighingRecords(ctx context.Context, opts WeighingRecordListOptions, page, pageSize int) ([]models.WeighingRecord, int64, error)
}
// WeighingBatchListOptions 定义了查询批次称重记录时的可选参数
@@ -47,35 +50,40 @@ type WeighingRecordListOptions struct {
// gormPigBatchRepository 是 PigBatchRepository 的 GORM 实现
type gormPigBatchRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPigBatchRepository 创建一个新的 PigBatchRepository GORM 实现实例
func NewGormPigBatchRepository(db *gorm.DB) PigBatchRepository {
return &gormPigBatchRepository{db: db}
func NewGormPigBatchRepository(ctx context.Context, db *gorm.DB) PigBatchRepository {
return &gormPigBatchRepository{ctx: ctx, db: db}
}
// CreatePigBatch 创建一个新的猪批次
func (r *gormPigBatchRepository) CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) {
return r.CreatePigBatchTx(r.db, batch)
func (r *gormPigBatchRepository) CreatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigBatch")
return r.CreatePigBatchTx(repoCtx, r.db, batch)
}
// CreatePigBatchTx 在指定的事务中,创建一个新的猪批次
func (r *gormPigBatchRepository) CreatePigBatchTx(tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error) {
if err := tx.Create(batch).Error; err != nil {
func (r *gormPigBatchRepository) CreatePigBatchTx(ctx context.Context, tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigBatchTx")
if err := tx.WithContext(repoCtx).Create(batch).Error; err != nil {
return nil, err
}
return batch, nil
}
// GetPigBatchByID 根据ID获取单个猪批次
func (r *gormPigBatchRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) {
return r.GetPigBatchByIDTx(r.db, id)
func (r *gormPigBatchRepository) GetPigBatchByID(ctx context.Context, id uint) (*models.PigBatch, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigBatchByID")
return r.GetPigBatchByIDTx(repoCtx, r.db, id)
}
// UpdatePigBatch 更新一个猪批次
func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error) {
result := r.db.Model(&models.PigBatch{}).Where("id = ?", batch.ID).Updates(batch)
func (r *gormPigBatchRepository) UpdatePigBatch(ctx context.Context, batch *models.PigBatch) (*models.PigBatch, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePigBatch")
result := r.db.WithContext(repoCtx).Model(&models.PigBatch{}).Where("id = ?", batch.ID).Updates(batch)
if result.Error != nil {
return nil, 0, result.Error
}
@@ -84,12 +92,14 @@ func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models
}
// DeletePigBatch 根据ID删除一个猪批次 (GORM 会执行软删除)
func (r *gormPigBatchRepository) DeletePigBatch(id uint) (int64, error) {
return r.DeletePigBatchTx(r.db, id)
func (r *gormPigBatchRepository) DeletePigBatch(ctx context.Context, id uint) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigBatch")
return r.DeletePigBatchTx(repoCtx, r.db, id)
}
func (r *gormPigBatchRepository) DeletePigBatchTx(tx *gorm.DB, id uint) (int64, error) {
result := tx.Delete(&models.PigBatch{}, id)
func (r *gormPigBatchRepository) DeletePigBatchTx(ctx context.Context, tx *gorm.DB, id uint) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigBatchTx")
result := tx.WithContext(repoCtx).Delete(&models.PigBatch{}, id)
if result.Error != nil {
return 0, result.Error
}
@@ -98,9 +108,10 @@ func (r *gormPigBatchRepository) DeletePigBatchTx(tx *gorm.DB, id uint) (int64,
}
// ListPigBatches 批量查询猪批次,支持根据 IsActive 筛选
func (r *gormPigBatchRepository) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) {
func (r *gormPigBatchRepository) ListPigBatches(ctx context.Context, isActive *bool) ([]*models.PigBatch, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigBatches")
var batches []*models.PigBatch
query := r.db.Model(&models.PigBatch{})
query := r.db.WithContext(repoCtx).Model(&models.PigBatch{})
if isActive != nil {
if *isActive {
@@ -119,16 +130,18 @@ func (r *gormPigBatchRepository) ListPigBatches(isActive *bool) ([]*models.PigBa
}
// GetPigBatchByIDTx 在指定的事务中通过ID获取单个猪批次
func (r *gormPigBatchRepository) GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) {
func (r *gormPigBatchRepository) GetPigBatchByIDTx(ctx context.Context, tx *gorm.DB, id uint) (*models.PigBatch, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigBatchByIDTx")
var batch models.PigBatch
if err := tx.First(&batch, id).Error; err != nil {
if err := tx.WithContext(repoCtx).First(&batch, id).Error; err != nil {
return nil, err
}
return &batch, nil
}
// ListWeighingBatches 实现了分页和过滤查询批次称重记录的功能
func (r *gormPigBatchRepository) ListWeighingBatches(opts WeighingBatchListOptions, page, pageSize int) ([]models.WeighingBatch, int64, error) {
func (r *gormPigBatchRepository) ListWeighingBatches(ctx context.Context, opts WeighingBatchListOptions, page, pageSize int) ([]models.WeighingBatch, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListWeighingBatches")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -136,7 +149,7 @@ func (r *gormPigBatchRepository) ListWeighingBatches(opts WeighingBatchListOptio
var results []models.WeighingBatch
var total int64
query := r.db.Model(&models.WeighingBatch{})
query := r.db.WithContext(repoCtx).Model(&models.WeighingBatch{})
if opts.PigBatchID != nil {
query = query.Where("pig_batch_id = ?", *opts.PigBatchID)
@@ -165,7 +178,8 @@ func (r *gormPigBatchRepository) ListWeighingBatches(opts WeighingBatchListOptio
}
// ListWeighingRecords 实现了分页和过滤查询单次称重记录的功能
func (r *gormPigBatchRepository) ListWeighingRecords(opts WeighingRecordListOptions, page, pageSize int) ([]models.WeighingRecord, int64, error) {
func (r *gormPigBatchRepository) ListWeighingRecords(ctx context.Context, opts WeighingRecordListOptions, page, pageSize int) ([]models.WeighingRecord, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListWeighingRecords")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -173,7 +187,7 @@ func (r *gormPigBatchRepository) ListWeighingRecords(opts WeighingRecordListOpti
var results []models.WeighingRecord
var total int64
query := r.db.Model(&models.WeighingRecord{})
query := r.db.WithContext(repoCtx).Model(&models.WeighingRecord{})
if opts.WeighingBatchID != nil {
query = query.Where("weighing_batch_id = ?", *opts.WeighingBatchID)

View File

@@ -1,61 +1,70 @@
package repository
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigFarmRepository 定义了与猪场资产(猪舍、猪栏)相关的数据库操作接口
type PigFarmRepository interface {
// PigHouse methods
CreatePigHouse(house *models.PigHouse) error
GetPigHouseByID(id uint) (*models.PigHouse, error)
ListPigHouses() ([]models.PigHouse, error)
CreatePigHouse(ctx context.Context, house *models.PigHouse) error
GetPigHouseByID(ctx context.Context, id uint) (*models.PigHouse, error)
ListPigHouses(ctx context.Context) ([]models.PigHouse, error)
// UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误
UpdatePigHouse(house *models.PigHouse) (int64, error)
UpdatePigHouse(ctx context.Context, house *models.PigHouse) (int64, error)
// DeletePigHouse 根据ID删除一个猪舍返回受影响的行数和错误
DeletePigHouse(id uint) (int64, error)
CountPensInHouse(houseID uint) (int64, error)
DeletePigHouse(ctx context.Context, id uint) (int64, error)
CountPensInHouse(ctx context.Context, houseID uint) (int64, error)
}
// gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现
type gormPigFarmRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPigFarmRepository 创建一个新的 PigFarmRepository GORM 实现实例
func NewGormPigFarmRepository(db *gorm.DB) PigFarmRepository {
return &gormPigFarmRepository{db: db}
func NewGormPigFarmRepository(ctx context.Context, db *gorm.DB) PigFarmRepository {
return &gormPigFarmRepository{ctx: ctx, db: db}
}
// --- PigHouse Implementation ---
// CreatePigHouse 创建一个新的猪舍
func (r *gormPigFarmRepository) CreatePigHouse(house *models.PigHouse) error {
return r.db.Create(house).Error
func (r *gormPigFarmRepository) CreatePigHouse(ctx context.Context, house *models.PigHouse) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigHouse")
return r.db.WithContext(repoCtx).Create(house).Error
}
// GetPigHouseByID 根据ID获取单个猪舍
func (r *gormPigFarmRepository) GetPigHouseByID(id uint) (*models.PigHouse, error) {
func (r *gormPigFarmRepository) GetPigHouseByID(ctx context.Context, id uint) (*models.PigHouse, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigHouseByID")
var house models.PigHouse
if err := r.db.First(&house, id).Error; err != nil {
if err := r.db.WithContext(repoCtx).First(&house, id).Error; err != nil {
return nil, err
}
return &house, nil
}
// ListPigHouses 列出所有猪舍
func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) {
func (r *gormPigFarmRepository) ListPigHouses(ctx context.Context) ([]models.PigHouse, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigHouses")
var houses []models.PigHouse
if err := r.db.Find(&houses).Error; err != nil {
if err := r.db.WithContext(repoCtx).Find(&houses).Error; err != nil {
return nil, err
}
return houses, nil
}
// UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误
func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) (int64, error) {
result := r.db.Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house)
func (r *gormPigFarmRepository) UpdatePigHouse(ctx context.Context, house *models.PigHouse) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePigHouse")
result := r.db.WithContext(repoCtx).Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house)
if result.Error != nil {
return 0, result.Error
}
@@ -63,8 +72,9 @@ func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) (int64, e
}
// DeletePigHouse 根据ID删除一个猪舍返回受影响的行数和错误
func (r *gormPigFarmRepository) DeletePigHouse(id uint) (int64, error) {
result := r.db.Delete(&models.PigHouse{}, id)
func (r *gormPigFarmRepository) DeletePigHouse(ctx context.Context, id uint) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigHouse")
result := r.db.WithContext(repoCtx).Delete(&models.PigHouse{}, id)
if result.Error != nil {
return 0, result.Error
}
@@ -72,8 +82,9 @@ func (r *gormPigFarmRepository) DeletePigHouse(id uint) (int64, error) {
}
// CountPensInHouse 统计猪舍中的猪栏数量
func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) {
func (r *gormPigFarmRepository) CountPensInHouse(ctx context.Context, houseID uint) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CountPensInHouse")
var count int64
err := r.db.Model(&models.Pen{}).Where("house_id = ?", houseID).Count(&count).Error
err := r.db.WithContext(repoCtx).Model(&models.Pen{}).Where("house_id = ?", houseID).Count(&count).Error
return count, err
}

View File

@@ -1,69 +1,79 @@
package repository
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigPenRepository 定义了与猪栏模型相关的数据库操作接口。
type PigPenRepository interface {
CreatePen(pen *models.Pen) error
CreatePen(ctx context.Context, pen *models.Pen) error
// GetPenByID 根据ID获取单个猪栏 (非事务性)
GetPenByID(id uint) (*models.Pen, error)
GetPenByID(ctx context.Context, id uint) (*models.Pen, error)
// GetPenByIDTx 根据ID获取单个猪栏 (事务性)
GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error)
ListPens() ([]models.Pen, error)
GetPenByIDTx(ctx context.Context, tx *gorm.DB, id uint) (*models.Pen, error)
ListPens(ctx context.Context) ([]models.Pen, error)
// UpdatePen 更新一个猪栏,返回受影响的行数和错误
UpdatePen(pen *models.Pen) (int64, error)
UpdatePen(ctx context.Context, pen *models.Pen) (int64, error)
// DeletePen 根据ID删除一个猪栏返回受影响的行数和错误
DeletePen(id uint) (int64, error)
DeletePen(ctx context.Context, id uint) (int64, error)
// GetPensByBatchIDTx 根据批次ID获取所有关联的猪栏 (事务性)
GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error)
GetPensByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint) ([]*models.Pen, error)
// UpdatePenFieldsTx 更新猪栏的指定字段 (事务性)
UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error
UpdatePenFieldsTx(ctx context.Context, tx *gorm.DB, penID uint, updates map[string]interface{}) error
}
// gormPigPenRepository 是 PigPenRepository 接口的 GORM 实现。
type gormPigPenRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPigPenRepository 创建一个新的 PigPenRepository GORM 实现实例。
func NewGormPigPenRepository(db *gorm.DB) PigPenRepository {
return &gormPigPenRepository{db: db}
func NewGormPigPenRepository(ctx context.Context, db *gorm.DB) PigPenRepository {
return &gormPigPenRepository{ctx: ctx, db: db}
}
// CreatePen 创建一个新的猪栏
func (r *gormPigPenRepository) CreatePen(pen *models.Pen) error {
return r.db.Create(pen).Error
func (r *gormPigPenRepository) CreatePen(ctx context.Context, pen *models.Pen) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePen")
return r.db.WithContext(repoCtx).Create(pen).Error
}
// GetPenByID 根据ID获取单个猪栏 (非事务性)
func (r *gormPigPenRepository) GetPenByID(id uint) (*models.Pen, error) {
return r.GetPenByIDTx(r.db, id) // 非Tx方法直接调用Tx方法
func (r *gormPigPenRepository) GetPenByID(ctx context.Context, id uint) (*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(tx *gorm.DB, id uint) (*models.Pen, error) {
func (r *gormPigPenRepository) GetPenByIDTx(ctx context.Context, tx *gorm.DB, id uint) (*models.Pen, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPenByIDTx")
var pen models.Pen
if err := tx.First(&pen, id).Error; err != nil {
if err := tx.WithContext(repoCtx).First(&pen, id).Error; err != nil {
return nil, err
}
return &pen, nil
}
// ListPens 列出所有猪栏
func (r *gormPigPenRepository) ListPens() ([]models.Pen, error) {
func (r *gormPigPenRepository) ListPens(ctx context.Context) ([]models.Pen, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPens")
var pens []models.Pen
if err := r.db.Find(&pens).Error; err != nil {
if err := r.db.WithContext(repoCtx).Find(&pens).Error; err != nil {
return nil, err
}
return pens, nil
}
// UpdatePen 更新一个猪栏,返回受影响的行数和错误
func (r *gormPigPenRepository) UpdatePen(pen *models.Pen) (int64, error) {
result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen)
func (r *gormPigPenRepository) UpdatePen(ctx context.Context, pen *models.Pen) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePen")
result := r.db.WithContext(repoCtx).Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen)
if result.Error != nil {
return 0, result.Error
}
@@ -71,8 +81,9 @@ func (r *gormPigPenRepository) UpdatePen(pen *models.Pen) (int64, error) {
}
// DeletePen 根据ID删除一个猪栏返回受影响的行数和错误
func (r *gormPigPenRepository) DeletePen(id uint) (int64, error) {
result := r.db.Delete(&models.Pen{}, id)
func (r *gormPigPenRepository) DeletePen(ctx context.Context, id uint) (int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePen")
result := r.db.WithContext(repoCtx).Delete(&models.Pen{}, id)
if result.Error != nil {
return 0, result.Error
}
@@ -80,10 +91,11 @@ func (r *gormPigPenRepository) DeletePen(id uint) (int64, error) {
}
// GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。
func (r *gormPigPenRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
func (r *gormPigPenRepository) GetPensByBatchIDTx(ctx context.Context, tx *gorm.DB, batchID uint) ([]*models.Pen, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPensByBatchIDTx")
var pens []*models.Pen
// 注意PigBatchID 是指针类型,需要处理 nil 值
result := tx.Where("pig_batch_id = ?", batchID).Find(&pens)
result := tx.WithContext(repoCtx).Where("pig_batch_id = ?", batchID).Find(&pens)
if result.Error != nil {
return nil, result.Error
}
@@ -91,7 +103,8 @@ func (r *gormPigPenRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]
}
// UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。
func (r *gormPigPenRepository) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error {
result := tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates)
func (r *gormPigPenRepository) UpdatePenFieldsTx(ctx context.Context, tx *gorm.DB, penID uint, 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
}

View File

@@ -1,10 +1,13 @@
package repository
import (
"context"
"errors"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -23,38 +26,42 @@ type PigSickLogListOptions struct {
// PigSickLogRepository 定义了与病猪日志模型相关的数据库操作接口。
type PigSickLogRepository interface {
// CreatePigSickLog 创建一条新的病猪日志记录
CreatePigSickLog(log *models.PigSickLog) error
CreatePigSickLogTx(tx *gorm.DB, log *models.PigSickLog) error
CreatePigSickLog(ctx context.Context, log *models.PigSickLog) error
CreatePigSickLogTx(ctx context.Context, tx *gorm.DB, log *models.PigSickLog) error
// GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录
GetLastLogByBatchTx(tx *gorm.DB, batchID uint) (*models.PigSickLog, error)
GetLastLogByBatchTx(ctx context.Context, tx *gorm.DB, batchID uint) (*models.PigSickLog, error)
// ListPigSickLogs 支持分页和过滤的病猪日志列表查询
ListPigSickLogs(opts PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error)
ListPigSickLogs(ctx context.Context, opts PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error)
}
// gormPigSickLogRepository 是 PigSickLogRepository 接口的 GORM 实现。
type gormPigSickLogRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPigSickLogRepository 创建一个新的 PigSickLogRepository GORM 实现实例。
func NewGormPigSickLogRepository(db *gorm.DB) PigSickLogRepository {
return &gormPigSickLogRepository{db: db}
func NewGormPigSickLogRepository(ctx context.Context, db *gorm.DB) PigSickLogRepository {
return &gormPigSickLogRepository{ctx: ctx, db: db}
}
// CreatePigSickLog 创建一条新的病猪日志记录
func (r *gormPigSickLogRepository) CreatePigSickLog(log *models.PigSickLog) error {
return r.CreatePigSickLogTx(r.db, log)
func (r *gormPigSickLogRepository) CreatePigSickLog(ctx context.Context, log *models.PigSickLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigSickLog")
return r.CreatePigSickLogTx(repoCtx, r.db, log)
}
func (r *gormPigSickLogRepository) CreatePigSickLogTx(tx *gorm.DB, log *models.PigSickLog) error {
return tx.Create(log).Error
func (r *gormPigSickLogRepository) CreatePigSickLogTx(ctx context.Context, tx *gorm.DB, log *models.PigSickLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigSickLogTx")
return tx.WithContext(repoCtx).Create(log).Error
}
// GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录
func (r *gormPigSickLogRepository) GetLastLogByBatchTx(tx *gorm.DB, batchID uint) (*models.PigSickLog, error) {
func (r *gormPigSickLogRepository) GetLastLogByBatchTx(ctx context.Context, tx *gorm.DB, batchID uint) (*models.PigSickLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLastLogByBatchTx")
var lastLog models.PigSickLog
err := tx.
err := tx.WithContext(repoCtx).
Where("pig_batch_id = ?", batchID).
Order("happened_at DESC"). // 按时间降序排列
First(&lastLog).Error // 获取第一条记录 (即最新一条)
@@ -69,7 +76,8 @@ func (r *gormPigSickLogRepository) GetLastLogByBatchTx(tx *gorm.DB, batchID uint
}
// ListPigSickLogs 实现了分页和过滤查询病猪日志的功能
func (r *gormPigSickLogRepository) ListPigSickLogs(opts PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error) {
func (r *gormPigSickLogRepository) ListPigSickLogs(ctx context.Context, opts PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigSickLogs")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -77,7 +85,7 @@ func (r *gormPigSickLogRepository) ListPigSickLogs(opts PigSickLogListOptions, p
var results []models.PigSickLog
var total int64
query := r.db.Model(&models.PigSickLog{})
query := r.db.WithContext(repoCtx).Model(&models.PigSickLog{})
if opts.PigBatchID != nil {
query = query.Where("pig_batch_id = ?", *opts.PigBatchID)

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -31,40 +34,44 @@ type PigSaleListOptions struct {
// 领域服务通过此接口与数据层交互,实现解耦。
type PigTradeRepository interface {
// CreatePigSaleTx 在数据库中创建一条猪只销售记录。
CreatePigSaleTx(tx *gorm.DB, sale *models.PigSale) error
CreatePigSaleTx(ctx context.Context, tx *gorm.DB, sale *models.PigSale) error
// CreatePigPurchaseTx 在数据库中创建一条猪只采购记录。
CreatePigPurchaseTx(tx *gorm.DB, purchase *models.PigPurchase) error
CreatePigPurchaseTx(ctx context.Context, tx *gorm.DB, purchase *models.PigPurchase) error
// ListPigPurchases 支持分页和过滤的猪只采购记录列表查询
ListPigPurchases(opts PigPurchaseListOptions, page, pageSize int) ([]models.PigPurchase, int64, error)
ListPigPurchases(ctx context.Context, opts PigPurchaseListOptions, page, pageSize int) ([]models.PigPurchase, int64, error)
// ListPigSales 支持分页和过滤的猪只销售记录列表查询
ListPigSales(opts PigSaleListOptions, page, pageSize int) ([]models.PigSale, int64, error)
ListPigSales(ctx context.Context, opts PigSaleListOptions, page, pageSize int) ([]models.PigSale, int64, error)
}
// gormPigTradeRepository 是 PigTradeRepository 接口的 GORM 实现。
type gormPigTradeRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPigTradeRepository 创建一个新的 PigTradeRepository GORM 实现实例。
func NewGormPigTradeRepository(db *gorm.DB) PigTradeRepository {
return &gormPigTradeRepository{db: db}
func NewGormPigTradeRepository(ctx context.Context, db *gorm.DB) PigTradeRepository {
return &gormPigTradeRepository{ctx: ctx, db: db}
}
// CreatePigSaleTx 实现了在数据库中创建猪只销售记录的逻辑。
func (r *gormPigTradeRepository) CreatePigSaleTx(tx *gorm.DB, sale *models.PigSale) error {
return tx.Create(sale).Error
func (r *gormPigTradeRepository) CreatePigSaleTx(ctx context.Context, tx *gorm.DB, sale *models.PigSale) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigSaleTx")
return tx.WithContext(repoCtx).Create(sale).Error
}
// CreatePigPurchaseTx 实现了在数据库中创建猪只采购记录的逻辑。
func (r *gormPigTradeRepository) CreatePigPurchaseTx(tx *gorm.DB, purchase *models.PigPurchase) error {
return tx.Create(purchase).Error
func (r *gormPigTradeRepository) CreatePigPurchaseTx(ctx context.Context, tx *gorm.DB, purchase *models.PigPurchase) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigPurchaseTx")
return tx.WithContext(repoCtx).Create(purchase).Error
}
// ListPigPurchases 实现了分页和过滤查询猪只采购记录的功能
func (r *gormPigTradeRepository) ListPigPurchases(opts PigPurchaseListOptions, page, pageSize int) ([]models.PigPurchase, int64, error) {
func (r *gormPigTradeRepository) ListPigPurchases(ctx context.Context, opts PigPurchaseListOptions, page, pageSize int) ([]models.PigPurchase, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigPurchases")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -72,7 +79,7 @@ func (r *gormPigTradeRepository) ListPigPurchases(opts PigPurchaseListOptions, p
var results []models.PigPurchase
var total int64
query := r.db.Model(&models.PigPurchase{})
query := r.db.WithContext(repoCtx).Model(&models.PigPurchase{})
if opts.PigBatchID != nil {
query = query.Where("pig_batch_id = ?", *opts.PigBatchID)
@@ -107,7 +114,8 @@ func (r *gormPigTradeRepository) ListPigPurchases(opts PigPurchaseListOptions, p
}
// ListPigSales 实现了分页和过滤查询猪只销售记录的功能
func (r *gormPigTradeRepository) ListPigSales(opts PigSaleListOptions, page, pageSize int) ([]models.PigSale, int64, error) {
func (r *gormPigTradeRepository) ListPigSales(ctx context.Context, opts PigSaleListOptions, page, pageSize int) ([]models.PigSale, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigSales")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -115,7 +123,7 @@ func (r *gormPigTradeRepository) ListPigSales(opts PigSaleListOptions, page, pag
var results []models.PigSale
var total int64
query := r.db.Model(&models.PigSale{})
query := r.db.WithContext(repoCtx).Model(&models.PigSale{})
if opts.PigBatchID != nil {
query = query.Where("pig_batch_id = ?", *opts.PigBatchID)

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -22,39 +25,43 @@ type PigTransferLogListOptions struct {
// PigTransferLogRepository 定义了猪只迁移日志数据持久化的接口。
type PigTransferLogRepository interface {
// CreatePigTransferLog 在数据库中创建一条猪只迁移日志记录。
CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error
CreatePigTransferLog(ctx context.Context, tx *gorm.DB, log *models.PigTransferLog) error
// GetLogsForPenSince 获取指定猪栏自特定时间点以来的所有迁移日志,按时间倒序排列。
GetLogsForPenSince(tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error)
GetLogsForPenSince(ctx context.Context, tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error)
// ListPigTransferLogs 支持分页和过滤的猪只迁移日志列表查询
ListPigTransferLogs(opts PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error)
ListPigTransferLogs(ctx context.Context, opts PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error)
}
// gormPigTransferLogRepository 是 PigTransferLogRepository 接口的 GORM 实现。
type gormPigTransferLogRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPigTransferLogRepository 创建一个新的 PigTransferLogRepository GORM 实现实例。
func NewGormPigTransferLogRepository(db *gorm.DB) PigTransferLogRepository {
return &gormPigTransferLogRepository{db: db}
func NewGormPigTransferLogRepository(ctx context.Context, db *gorm.DB) PigTransferLogRepository {
return &gormPigTransferLogRepository{ctx: ctx, db: db}
}
// CreatePigTransferLog 实现了在数据库中创建猪只迁移日志记录的逻辑。
func (r *gormPigTransferLogRepository) CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error {
return tx.Create(log).Error
func (r *gormPigTransferLogRepository) CreatePigTransferLog(ctx context.Context, tx *gorm.DB, log *models.PigTransferLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigTransferLog")
return tx.WithContext(repoCtx).Create(log).Error
}
// GetLogsForPenSince 实现了获取猪栏自特定时间点以来所有迁移日志的逻辑。
func (r *gormPigTransferLogRepository) GetLogsForPenSince(tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error) {
func (r *gormPigTransferLogRepository) GetLogsForPenSince(ctx context.Context, tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLogsForPenSince")
var logs []*models.PigTransferLog
err := tx.Where("pen_id = ? AND transfer_time >= ?", penID, since).Order("transfer_time DESC").Find(&logs).Error
err := tx.WithContext(repoCtx).Where("pen_id = ? AND transfer_time >= ?", penID, since).Order("transfer_time DESC").Find(&logs).Error
return logs, err
}
// ListPigTransferLogs 实现了分页和过滤查询猪只迁移日志的功能
func (r *gormPigTransferLogRepository) ListPigTransferLogs(opts PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error) {
func (r *gormPigTransferLogRepository) ListPigTransferLogs(ctx context.Context, opts PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigTransferLogs")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -62,7 +69,7 @@ func (r *gormPigTransferLogRepository) ListPigTransferLogs(opts PigTransferLogLi
var results []models.PigTransferLog
var total int64
query := r.db.Model(&models.PigTransferLog{})
query := r.db.WithContext(repoCtx).Model(&models.PigTransferLog{})
if opts.PigBatchID != nil {
query = query.Where("pig_batch_id = ?", *opts.PigBatchID)

View File

@@ -1,11 +1,14 @@
package repository
import (
"context"
"encoding/json"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -39,67 +42,71 @@ type ListPlansOptions struct {
// 这是为了让业务逻辑层依赖于抽象,而不是具体的数据库实现
type PlanRepository interface {
// ListPlans 获取计划列表,支持过滤和分页
ListPlans(opts ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)
ListPlans(ctx context.Context, opts ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error)
// GetBasicPlanByID 根据ID获取计划的基本信息不包含子计划和任务详情
GetBasicPlanByID(id uint) (*models.Plan, error)
GetBasicPlanByID(ctx context.Context, id uint) (*models.Plan, error)
// GetPlanByID 根据ID获取计划包含子计划和任务详情
GetPlanByID(id uint) (*models.Plan, error)
GetPlanByID(ctx context.Context, id uint) (*models.Plan, error)
// GetPlansByIDs 根据ID列表获取计划不包含子计划和任务详情
GetPlansByIDs(ids []uint) ([]models.Plan, error)
GetPlansByIDs(ctx context.Context, ids []uint) ([]models.Plan, error)
// CreatePlan 创建一个新的计划
CreatePlan(plan *models.Plan) error
CreatePlan(ctx context.Context, plan *models.Plan) error
// CreatePlanTx 在指定事务中创建一个新的计划
CreatePlanTx(tx *gorm.DB, plan *models.Plan) error
CreatePlanTx(ctx context.Context, tx *gorm.DB, plan *models.Plan) error
// UpdatePlanMetadataAndStructure 更新计划的元数据和结构,但不包括状态等运行时信息
UpdatePlanMetadataAndStructure(plan *models.Plan) error
UpdatePlanMetadataAndStructure(ctx context.Context, plan *models.Plan) error
// UpdatePlan 更新计划的所有字段
UpdatePlan(plan *models.Plan) error
UpdatePlan(ctx context.Context, plan *models.Plan) error
// UpdatePlanStatus 更新指定计划的状态
UpdatePlanStatus(id uint, status models.PlanStatus) error
UpdatePlanStatus(ctx context.Context, id uint, status models.PlanStatus) error
// UpdateExecuteCount 更新指定计划的执行计数
UpdateExecuteCount(id uint, count uint) error
UpdateExecuteCount(ctx context.Context, id uint, count uint) error
// DeletePlan 根据ID删除计划同时删除其关联的任务非子任务或子计划关联
DeletePlan(id uint) error
DeletePlan(ctx context.Context, id uint) error
// FlattenPlanTasks 递归展开计划,返回按执行顺序排列的所有任务列表
FlattenPlanTasks(planID uint) ([]models.Task, error)
FlattenPlanTasks(ctx context.Context, planID uint) ([]models.Task, error)
// DeleteTask 根据ID删除任务
DeleteTask(id int) error
DeleteTask(ctx context.Context, id int) error
// FindPlanAnalysisTaskByParamsPlanID 根据Parameters中的ParamsPlanID字段值查找TaskPlanAnalysis类型的Task
FindPlanAnalysisTaskByParamsPlanID(paramsPlanID uint) (*models.Task, error)
FindPlanAnalysisTaskByParamsPlanID(ctx context.Context, paramsPlanID uint) (*models.Task, error)
// FindRunnablePlans 获取所有应执行的计划
FindRunnablePlans() ([]*models.Plan, error)
FindRunnablePlans(ctx context.Context) ([]*models.Plan, error)
// FindInactivePlans 获取所有已禁用或已停止的计划
FindInactivePlans() ([]*models.Plan, error)
FindInactivePlans(ctx context.Context) ([]*models.Plan, error)
// FindPlanAnalysisTaskByPlanID 根据 PlanID 找到其关联的 'plan_analysis' 任务
FindPlanAnalysisTaskByPlanID(planID uint) (*models.Task, error)
FindPlanAnalysisTaskByPlanID(ctx context.Context, planID uint) (*models.Task, error)
// CreatePlanAnalysisTask 创建一个 plan_analysis 类型的任务并返回它
CreatePlanAnalysisTask(plan *models.Plan) (*models.Task, error)
CreatePlanAnalysisTask(ctx context.Context, plan *models.Plan) (*models.Task, error)
// FindPlansWithPendingTasks 查找所有正在执行的计划
FindPlansWithPendingTasks() ([]*models.Plan, error)
FindPlansWithPendingTasks(ctx context.Context) ([]*models.Plan, error)
// StopPlanTransactionally 停止一个计划的执行,包括更新状态、移除待执行任务和更新执行日志
StopPlanTransactionally(planID uint) error
StopPlanTransactionally(ctx context.Context, planID uint) error
// UpdatePlanStateAfterExecution 更新计划执行后的状态(计数和状态)
UpdatePlanStateAfterExecution(planID uint, newCount uint, newStatus models.PlanStatus) error
UpdatePlanStateAfterExecution(ctx context.Context, planID uint, newCount uint, newStatus models.PlanStatus) error
}
// gormPlanRepository 是 PlanRepository 的 GORM 实现
type gormPlanRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormPlanRepository 创建一个新的 PlanRepository GORM 实现实例
func NewGormPlanRepository(db *gorm.DB) PlanRepository {
func NewGormPlanRepository(ctx context.Context, db *gorm.DB) PlanRepository {
return &gormPlanRepository{
db: db,
ctx: ctx,
db: db,
}
}
// ListPlans 获取计划列表,支持过滤和分页
func (r *gormPlanRepository) ListPlans(opts ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) {
func (r *gormPlanRepository) ListPlans(ctx context.Context, opts ListPlansOptions, page, pageSize int) ([]models.Plan, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPlans")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -107,7 +114,7 @@ func (r *gormPlanRepository) ListPlans(opts ListPlansOptions, page, pageSize int
var plans []models.Plan
var total int64
query := r.db.Model(&models.Plan{})
query := r.db.WithContext(repoCtx).Model(&models.Plan{})
switch opts.PlanType {
case PlanTypeFilterCustom:
@@ -132,10 +139,11 @@ func (r *gormPlanRepository) ListPlans(opts ListPlansOptions, page, pageSize int
}
// GetBasicPlanByID 根据ID获取计划的基本信息不包含子计划和任务详情
func (r *gormPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) {
func (r *gormPlanRepository) GetBasicPlanByID(ctx context.Context, id uint) (*models.Plan, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetBasicPlanByID")
var plan models.Plan
// GORM 默认不会加载关联,除非使用 Preload所以直接 First 即可满足要求
result := r.db.First(&plan, id)
result := r.db.WithContext(repoCtx).First(&plan, id)
if result.Error != nil {
return nil, result.Error
}
@@ -143,12 +151,13 @@ func (r *gormPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) {
}
// GetPlansByIDs 根据ID列表获取计划不包含子计划和任务详情
func (r *gormPlanRepository) GetPlansByIDs(ids []uint) ([]models.Plan, error) {
func (r *gormPlanRepository) GetPlansByIDs(ctx context.Context, ids []uint) ([]models.Plan, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPlansByIDs")
var plans []models.Plan
if len(ids) == 0 {
return plans, nil
}
err := r.db.Where("id IN ?", ids).Find(&plans).Error
err := r.db.WithContext(repoCtx).Where("id IN ?", ids).Find(&plans).Error
if err != nil {
return nil, err
}
@@ -156,11 +165,12 @@ func (r *gormPlanRepository) GetPlansByIDs(ids []uint) ([]models.Plan, error) {
}
// GetPlanByID 根据ID获取计划包含子计划和任务详情
func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
func (r *gormPlanRepository) GetPlanByID(ctx context.Context, id uint) (*models.Plan, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPlanByID")
var plan models.Plan
// 先获取基本计划信息
result := r.db.First(&plan, id)
result := r.db.WithContext(repoCtx).First(&plan, id)
if result.Error != nil {
return nil, result.Error
}
@@ -170,14 +180,14 @@ func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
case models.PlanContentTypeSubPlans:
// 加载子计划引用
var subPlans []models.SubPlan
result = r.db.Where("parent_plan_id = ?", plan.ID).Order("execution_order").Find(&subPlans)
result = r.db.WithContext(repoCtx).Where("parent_plan_id = ?", plan.ID).Order("execution_order").Find(&subPlans)
if result.Error != nil {
return nil, result.Error
}
// 递归加载每个子计划的完整信息
for i := range subPlans {
childPlan, err := r.GetPlanByID(subPlans[i].ChildPlanID)
childPlan, err := r.GetPlanByID(repoCtx, subPlans[i].ChildPlanID)
if err != nil {
return nil, err
}
@@ -187,7 +197,7 @@ func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
plan.SubPlans = subPlans
case models.PlanContentTypeTasks:
// 加载任务
result = r.db.Preload("Tasks", func(taskDB *gorm.DB) *gorm.DB {
result = r.db.WithContext(repoCtx).Preload("Tasks", func(taskDB *gorm.DB) *gorm.DB {
return taskDB.Order("execution_order")
}).First(&plan, id)
if result.Error != nil {
@@ -201,12 +211,14 @@ func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
}
// CreatePlan 创建一个新的计划
func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error {
return r.CreatePlanTx(r.db, plan)
func (r *gormPlanRepository) CreatePlan(ctx context.Context, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePlan")
return r.CreatePlanTx(repoCtx, r.db, plan)
}
// CreatePlanTx 在指定事务中创建一个新的计划
func (r *gormPlanRepository) CreatePlanTx(tx *gorm.DB, plan *models.Plan) error {
func (r *gormPlanRepository) CreatePlanTx(ctx context.Context, tx *gorm.DB, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePlanTx")
// 1. 前置校验
if plan.ID != 0 {
return ErrCreateWithNonZeroID
@@ -239,7 +251,7 @@ func (r *gormPlanRepository) CreatePlanTx(tx *gorm.DB, plan *models.Plan) error
if len(ids) > 0 {
var count int64
if err := tx.Model(&models.Plan{}).Where("id IN ?", ids).Count(&count).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(&models.Plan{}).Where("id IN ?", ids).Count(&count).Error; err != nil {
return fmt.Errorf("验证子计划存在性失败: %w", err)
}
if int(count) != len(ids) {
@@ -251,13 +263,13 @@ func (r *gormPlanRepository) CreatePlanTx(tx *gorm.DB, plan *models.Plan) error
// 2. 创建根计划
// GORM 会自动处理关联的 Tasks (如果 ContentType 是 tasks 且 Task.ID 为 0),
// 以及 Tasks 内部已经填充好的 Devices 关联。
if err := tx.Create(plan).Error; err != nil {
if err := tx.WithContext(repoCtx).Create(plan).Error; err != nil {
return err
}
// 3. 创建触发器Task
// 关键修改:调用 createPlanAnalysisTask 并处理其返回的 Task 对象
_, err := r.createPlanAnalysisTask(tx, plan)
_, err := r.createPlanAnalysisTask(repoCtx, tx, plan)
if err != nil {
return err
}
@@ -265,32 +277,36 @@ func (r *gormPlanRepository) CreatePlanTx(tx *gorm.DB, plan *models.Plan) error
}
// UpdatePlan 更新计划
func (r *gormPlanRepository) UpdatePlan(plan *models.Plan) error {
return r.db.Save(plan).Error
func (r *gormPlanRepository) UpdatePlan(ctx context.Context, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlan")
return r.db.WithContext(repoCtx).Save(plan).Error
}
// UpdatePlanMetadataAndStructure 是更新计划元数据和结构的公共入口点
func (r *gormPlanRepository) UpdatePlanMetadataAndStructure(plan *models.Plan) error {
return r.db.Transaction(func(tx *gorm.DB) error {
return r.updatePlanMetadataAndStructureTx(tx, plan)
func (r *gormPlanRepository) UpdatePlanMetadataAndStructure(ctx context.Context, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePlanMetadataAndStructure")
return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
return r.updatePlanMetadataAndStructureTx(repoCtx, tx, plan)
})
}
// updatePlanMetadataAndStructureTx 在事务中协调整个更新过程
func (r *gormPlanRepository) updatePlanMetadataAndStructureTx(tx *gorm.DB, plan *models.Plan) error {
if err := r.validatePlanTree(tx, plan); err != nil {
func (r *gormPlanRepository) updatePlanMetadataAndStructureTx(ctx context.Context, tx *gorm.DB, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "updatePlanMetadataAndStructureTx")
if err := r.validatePlanTree(repoCtx, tx, plan); err != nil {
return err
}
if err := r.reconcilePlanNode(tx, plan); err != nil {
if err := r.reconcilePlanNode(repoCtx, tx, plan); err != nil {
return err
}
// 更新Plan触发器
return r.updatePlanAnalysisTask(tx, plan)
return r.updatePlanAnalysisTask(repoCtx, tx, plan)
}
// validatePlanTree 对整个计划树进行全面的只读健康检查
func (r *gormPlanRepository) validatePlanTree(tx *gorm.DB, plan *models.Plan) error {
func (r *gormPlanRepository) validatePlanTree(ctx context.Context, tx *gorm.DB, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "validatePlanTree")
// 1. 检查根节点
if plan == nil || plan.ID == 0 {
return ErrUpdateWithInvalidRoot
@@ -319,7 +335,7 @@ func (r *gormPlanRepository) validatePlanTree(tx *gorm.DB, plan *models.Plan) er
if len(idsToCheck) > 0 {
var count int64
if err := tx.Model(&models.Plan{}).Where("id IN ?", idsToCheck).Count(&count).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(&models.Plan{}).Where("id IN ?", idsToCheck).Count(&count).Error; err != nil {
return fmt.Errorf("检查计划存在性时出错: %w", err)
}
if int(count) != len(idsToCheck) {
@@ -358,12 +374,13 @@ func validateNodeAndDetectCycles(plan *models.Plan, allIDs, recursionStack map[u
}
// reconcilePlanNode 递归地同步数据库状态以匹配给定的计划节点
func (r *gormPlanRepository) reconcilePlanNode(tx *gorm.DB, plan *models.Plan) error {
func (r *gormPlanRepository) reconcilePlanNode(ctx context.Context, tx *gorm.DB, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "reconcilePlanNode")
if plan == nil {
return nil
}
// 1. 更新节点本身的基础字段
if err := tx.Model(plan).Select("Name", "Description", "ExecutionType", "ExecuteNum", "CronExpression", "ContentType").Updates(plan).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(plan).Select("Name", "Description", "ExecutionType", "ExecuteNum", "CronExpression", "ContentType").Updates(plan).Error; err != nil {
return err
}
@@ -371,26 +388,27 @@ func (r *gormPlanRepository) reconcilePlanNode(tx *gorm.DB, plan *models.Plan) e
switch plan.ContentType {
case models.PlanContentTypeTasks:
// 清理旧的子计划关联
if err := tx.Where("parent_plan_id = ?", plan.ID).Delete(&models.SubPlan{}).Error; err != nil {
if err := tx.WithContext(repoCtx).Where("parent_plan_id = ?", plan.ID).Delete(&models.SubPlan{}).Error; err != nil {
return fmt.Errorf("更新时清理旧的子计划关联失败: %w", err)
}
// 协调任务列表
return r.reconcileTasks(tx, plan)
return r.reconcileTasks(repoCtx, tx, plan)
case models.PlanContentTypeSubPlans:
// 清理旧的任务
if err := tx.Where("plan_id = ?", plan.ID).Delete(&models.Task{}).Error; err != nil {
if err := tx.WithContext(repoCtx).Where("plan_id = ?", plan.ID).Delete(&models.Task{}).Error; err != nil {
return fmt.Errorf("更新时清理旧的任务失败: %w", err)
}
// 协调子计划关联
return r.reconcileSubPlans(tx, plan)
return r.reconcileSubPlans(repoCtx, tx, plan)
}
return nil
}
// reconcileTasks 精确同步任务列表
func (r *gormPlanRepository) reconcileTasks(tx *gorm.DB, plan *models.Plan) error {
func (r *gormPlanRepository) reconcileTasks(ctx context.Context, tx *gorm.DB, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "reconcileTasks")
var existingTasks []models.Task
if err := tx.Where("plan_id = ?", plan.ID).Find(&existingTasks).Error; err != nil {
if err := tx.WithContext(repoCtx).Where("plan_id = ?", plan.ID).Find(&existingTasks).Error; err != nil {
return err
}
@@ -403,12 +421,12 @@ func (r *gormPlanRepository) reconcileTasks(tx *gorm.DB, plan *models.Plan) erro
task := &plan.Tasks[i]
if task.ID == 0 {
task.PlanID = plan.ID
if err := tx.Create(task).Error; err != nil {
if err := tx.WithContext(repoCtx).Create(task).Error; err != nil {
return err
}
} else {
delete(existingTaskMap, task.ID) // 从待删除map中移除
if err := tx.Model(task).Updates(task).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(task).Updates(task).Error; err != nil {
return err
}
}
@@ -420,15 +438,16 @@ func (r *gormPlanRepository) reconcileTasks(tx *gorm.DB, plan *models.Plan) erro
}
if len(tasksToDelete) > 0 {
return r.deleteTasksTx(tx, tasksToDelete)
return r.deleteTasksTx(repoCtx, tx, tasksToDelete)
}
return nil
}
// reconcileSubPlans 精确同步子计划关联并递归
func (r *gormPlanRepository) reconcileSubPlans(tx *gorm.DB, plan *models.Plan) error {
func (r *gormPlanRepository) reconcileSubPlans(ctx context.Context, tx *gorm.DB, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "reconcileSubPlans")
var existingLinks []models.SubPlan
if err := tx.Where("parent_plan_id = ?", plan.ID).Find(&existingLinks).Error; err != nil {
if err := tx.WithContext(repoCtx).Where("parent_plan_id = ?", plan.ID).Find(&existingLinks).Error; err != nil {
return err
}
@@ -441,12 +460,12 @@ func (r *gormPlanRepository) reconcileSubPlans(tx *gorm.DB, plan *models.Plan) e
link := &plan.SubPlans[i]
link.ParentPlanID = plan.ID
if link.ID == 0 {
if err := tx.Create(link).Error; err != nil {
if err := tx.WithContext(repoCtx).Create(link).Error; err != nil {
return err
}
} else {
delete(existingLinkMap, link.ID) // 从待删除map中移除
if err := tx.Model(link).Updates(link).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(link).Updates(link).Error; err != nil {
return err
}
}
@@ -458,7 +477,7 @@ func (r *gormPlanRepository) reconcileSubPlans(tx *gorm.DB, plan *models.Plan) e
}
if len(linksToDelete) > 0 {
if err := tx.Delete(&models.SubPlan{}, linksToDelete).Error; err != nil {
if err := tx.WithContext(repoCtx).Delete(&models.SubPlan{}, linksToDelete).Error; err != nil {
return err
}
}
@@ -466,11 +485,12 @@ func (r *gormPlanRepository) reconcileSubPlans(tx *gorm.DB, plan *models.Plan) e
}
// DeletePlan 根据ID删除计划同时删除其关联的任务非子任务或子计划关联
func (r *gormPlanRepository) DeletePlan(id uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
func (r *gormPlanRepository) DeletePlan(ctx context.Context, id uint) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePlan")
return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
// 1. 检查该计划是否是其他计划的子计划
var count int64
if err := tx.Model(&models.SubPlan{}).Where("child_plan_id = ?", id).Count(&count).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(&models.SubPlan{}).Where("child_plan_id = ?", id).Count(&count).Error; err != nil {
return fmt.Errorf("检查计划是否为子计划失败: %w", err)
}
if count > 0 {
@@ -479,7 +499,7 @@ func (r *gormPlanRepository) DeletePlan(id uint) error {
var plan models.Plan
// 2. 获取计划以确定其内容类型
if err := tx.First(&plan, id).Error; err != nil {
if err := tx.WithContext(repoCtx).First(&plan, id).Error; err != nil {
return err
}
@@ -487,18 +507,18 @@ func (r *gormPlanRepository) DeletePlan(id uint) error {
switch plan.ContentType {
case models.PlanContentTypeTasks:
// 删除与此计划关联的所有非子任务
if err := tx.Where("plan_id = ?", id).Delete(&models.Task{}).Error; err != nil {
if err := tx.WithContext(repoCtx).Where("plan_id = ?", id).Delete(&models.Task{}).Error; err != nil {
return fmt.Errorf("删除计划ID %d 的任务失败: %w", id, err)
}
case models.PlanContentTypeSubPlans:
// 删除与此计划关联的所有子计划链接
if err := tx.Where("parent_plan_id = ?", id).Delete(&models.SubPlan{}).Error; err != nil {
if err := tx.WithContext(repoCtx).Where("parent_plan_id = ?", id).Delete(&models.SubPlan{}).Error; err != nil {
return fmt.Errorf("删除计划ID %d 的子计划关联失败: %w", id, err)
}
}
// 4. 删除计划本身
if err := tx.Delete(&models.Plan{}, id).Error; err != nil {
if err := tx.WithContext(repoCtx).Delete(&models.Plan{}, id).Error; err != nil {
return fmt.Errorf("删除计划ID %d 失败: %w", id, err)
}
@@ -507,17 +527,19 @@ func (r *gormPlanRepository) DeletePlan(id uint) error {
}
// FlattenPlanTasks 递归展开计划,返回按执行顺序排列的所有任务列表
func (r *gormPlanRepository) FlattenPlanTasks(planID uint) ([]models.Task, error) {
plan, err := r.GetPlanByID(planID)
func (r *gormPlanRepository) FlattenPlanTasks(ctx context.Context, planID uint) ([]models.Task, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FlattenPlanTasks")
plan, err := r.GetPlanByID(repoCtx, planID)
if err != nil {
return nil, fmt.Errorf("获取计划(ID: %d)失败: %w", planID, err)
}
return r.flattenPlanTasksRecursive(plan)
return r.flattenPlanTasksRecursive(repoCtx, plan)
}
// flattenPlanTasksRecursive 递归展开计划的内部实现
func (r *gormPlanRepository) flattenPlanTasksRecursive(plan *models.Plan) ([]models.Task, error) {
func (r *gormPlanRepository) flattenPlanTasksRecursive(ctx context.Context, plan *models.Plan) ([]models.Task, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "flattenPlanTasksRecursive")
var tasks []models.Task
switch plan.ContentType {
@@ -535,10 +557,10 @@ func (r *gormPlanRepository) flattenPlanTasksRecursive(plan *models.Plan) ([]mod
// 确保子计划已经被加载
if subPlan.ChildPlan != nil {
subTasks, err = r.flattenPlanTasksRecursive(subPlan.ChildPlan)
subTasks, err = r.flattenPlanTasksRecursive(repoCtx, subPlan.ChildPlan)
} else {
// 如果子计划未加载,则从数据库获取并递归展开
subTasks, err = r.FlattenPlanTasks(subPlan.ChildPlanID)
subTasks, err = r.FlattenPlanTasks(repoCtx, subPlan.ChildPlanID)
}
if err != nil {
@@ -556,22 +578,24 @@ func (r *gormPlanRepository) flattenPlanTasksRecursive(plan *models.Plan) ([]mod
}
// DeleteTask 根据ID删除任务
func (r *gormPlanRepository) DeleteTask(id int) error {
func (r *gormPlanRepository) DeleteTask(ctx context.Context, id int) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteTask")
// 使用事务确保操作的原子性
return r.db.Transaction(func(tx *gorm.DB) error {
return r.deleteTasksTx(tx, []int{id})
return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
return r.deleteTasksTx(repoCtx, tx, []int{id})
})
}
// deleteTasksTx 在事务中批量软删除任务,并物理删除其在关联表中的记录
func (r *gormPlanRepository) deleteTasksTx(tx *gorm.DB, ids []int) error {
func (r *gormPlanRepository) deleteTasksTx(ctx context.Context, tx *gorm.DB, ids []int) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "deleteTasksTx")
if len(ids) == 0 {
return nil
}
// 检查是否有待执行任务引用了这些任务
var pendingTaskCount int64
if err := tx.Model(&models.PendingTask{}).Where("task_id IN ?", ids).Count(&pendingTaskCount).Error; err != nil {
if err := tx.WithContext(repoCtx).Model(&models.PendingTask{}).Where("task_id IN ?", ids).Count(&pendingTaskCount).Error; err != nil {
return fmt.Errorf("检查待执行任务时出错: %w", err)
}
@@ -584,12 +608,12 @@ func (r *gormPlanRepository) deleteTasksTx(tx *gorm.DB, ids []int) error {
// 1. 直接、高效地从关联表中物理删除所有相关记录
// 这是最关键的优化,避免了不必要的查询和循环
if err := tx.Where("task_id IN ?", ids).Delete(&models.DeviceTask{}).Error; err != nil {
if err := tx.WithContext(repoCtx).Where("task_id IN ?", ids).Delete(&models.DeviceTask{}).Error; err != nil {
return fmt.Errorf("清理任务的设备关联失败: %w", err)
}
// 2. 对任务本身进行软删除
result := tx.Delete(&models.Task{}, ids)
result := tx.WithContext(repoCtx).Delete(&models.Task{}, ids)
if result.Error != nil {
return result.Error
}
@@ -603,13 +627,15 @@ func (r *gormPlanRepository) deleteTasksTx(tx *gorm.DB, ids []int) error {
}
// FindPlanAnalysisTaskByParamsPlanID 根据Parameters中的ParamsPlanID字段值查找TaskPlanAnalysis类型的Task
func (r *gormPlanRepository) FindPlanAnalysisTaskByParamsPlanID(paramsPlanID uint) (*models.Task, error) {
return r.findPlanAnalysisTask(r.db, paramsPlanID)
func (r *gormPlanRepository) FindPlanAnalysisTaskByParamsPlanID(ctx context.Context, paramsPlanID uint) (*models.Task, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPlanAnalysisTaskByParamsPlanID")
return r.findPlanAnalysisTask(repoCtx, r.db, paramsPlanID)
}
// createPlanAnalysisTask 用于创建一个TaskPlanAnalysis类型的Task
// 关键修改Task.PlanID 设置为 0实际 PlanID 存储在 Parameters 中,并返回创建的 Task
func (r *gormPlanRepository) createPlanAnalysisTask(tx *gorm.DB, plan *models.Plan) (*models.Task, error) {
func (r *gormPlanRepository) createPlanAnalysisTask(ctx context.Context, tx *gorm.DB, plan *models.Plan) (*models.Task, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "createPlanAnalysisTask")
m := map[string]interface{}{
models.ParamsPlanID: plan.ID,
}
@@ -627,22 +653,23 @@ func (r *gormPlanRepository) createPlanAnalysisTask(tx *gorm.DB, plan *models.Pl
Parameters: datatypes.JSON(parameters),
}
if err := tx.Create(task).Error; err != nil {
if err := tx.WithContext(repoCtx).Create(task).Error; err != nil {
return nil, err
}
return task, nil
}
// updatePlanAnalysisTask 使用更安全的方式更新触发器任务
func (r *gormPlanRepository) updatePlanAnalysisTask(tx *gorm.DB, plan *models.Plan) error {
task, err := r.findPlanAnalysisTask(tx, plan.ID)
func (r *gormPlanRepository) updatePlanAnalysisTask(ctx context.Context, tx *gorm.DB, plan *models.Plan) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "updatePlanAnalysisTask")
task, err := r.findPlanAnalysisTask(repoCtx, tx, plan.ID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("查找现有计划分析任务失败: %w", err)
}
// 如果触发器任务不存在,则创建一个
if task == nil {
_, err := r.createPlanAnalysisTask(tx, plan)
_, err := r.createPlanAnalysisTask(repoCtx, tx, plan)
return err
}
@@ -650,24 +677,26 @@ func (r *gormPlanRepository) updatePlanAnalysisTask(tx *gorm.DB, plan *models.Pl
task.Name = fmt.Sprintf("'%s'计划触发器", plan.Name)
task.Description = fmt.Sprintf("计划名: %s, 计划ID: %d", plan.Name, plan.ID)
return tx.Save(task).Error
return tx.WithContext(repoCtx).Save(task).Error
}
func (r *gormPlanRepository) FindRunnablePlans() ([]*models.Plan, error) {
func (r *gormPlanRepository) FindRunnablePlans(ctx context.Context) ([]*models.Plan, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindRunnablePlans")
var plans []*models.Plan
err := r.db.
err := r.db.WithContext(repoCtx).
Where("status = ?", models.PlanStatusEnabled).
Where(
r.db.Where("execution_type = ?", models.PlanExecutionTypeManual).
r.db.WithContext(repoCtx).Where("execution_type = ?", models.PlanExecutionTypeManual).
Or("execution_type = ? AND (execute_num = 0 OR execute_count < execute_num)", models.PlanExecutionTypeAutomatic),
).
Find(&plans).Error
return plans, err
}
func (r *gormPlanRepository) FindInactivePlans() ([]*models.Plan, error) {
func (r *gormPlanRepository) FindInactivePlans(ctx context.Context) ([]*models.Plan, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindInactivePlans")
var plans []*models.Plan
err := r.db.
err := r.db.WithContext(repoCtx).
Where("status != ?", models.PlanStatusEnabled).
Find(&plans).Error
return plans, err
@@ -675,9 +704,10 @@ func (r *gormPlanRepository) FindInactivePlans() ([]*models.Plan, error) {
// findPlanAnalysisTask 是一个内部使用的、更高效的查找方法
// 关键修改:通过查询 parameters JSON 字段来查找
func (r *gormPlanRepository) findPlanAnalysisTask(tx *gorm.DB, planID uint) (*models.Task, error) {
func (r *gormPlanRepository) findPlanAnalysisTask(ctx context.Context, tx *gorm.DB, planID uint) (*models.Task, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "findPlanAnalysisTask")
var task models.Task
err := tx.Where("type = ? AND parameters->>'plan_id' = ?", models.TaskPlanAnalysis, fmt.Sprintf("%d", planID)).First(&task).Error
err := tx.WithContext(repoCtx).Where("type = ? AND parameters->>'plan_id' = ?", models.TaskPlanAnalysis, fmt.Sprintf("%d", planID)).First(&task).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 未找到不是错误返回nil, nil
}
@@ -686,28 +716,31 @@ func (r *gormPlanRepository) findPlanAnalysisTask(tx *gorm.DB, planID uint) (*mo
// FindPlanAnalysisTaskByPlanID 是暴露给外部的公共方法
// 关键修改:通过查询 parameters JSON 字段来查找
func (r *gormPlanRepository) FindPlanAnalysisTaskByPlanID(planID uint) (*models.Task, error) {
return r.findPlanAnalysisTask(r.db, planID)
func (r *gormPlanRepository) FindPlanAnalysisTaskByPlanID(ctx context.Context, planID uint) (*models.Task, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPlanAnalysisTaskByPlanID")
return r.findPlanAnalysisTask(repoCtx, r.db, planID)
}
// CreatePlanAnalysisTask 创建一个 plan_analysis 类型的任务并返回它
// 这个方法是公开的,主要由 TaskManager 在发现触发器任务定义丢失时调用。
func (r *gormPlanRepository) CreatePlanAnalysisTask(plan *models.Plan) (*models.Task, error) {
func (r *gormPlanRepository) CreatePlanAnalysisTask(ctx context.Context, plan *models.Plan) (*models.Task, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePlanAnalysisTask")
var createdTask *models.Task
err := r.db.Transaction(func(tx *gorm.DB) error {
err := r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
var err error
createdTask, err = r.createPlanAnalysisTask(tx, plan)
createdTask, err = r.createPlanAnalysisTask(repoCtx, tx, plan)
return err
})
return createdTask, err
}
// FindPlansWithPendingTasks 查找所有正在执行的计划
func (r *gormPlanRepository) FindPlansWithPendingTasks() ([]*models.Plan, error) {
func (r *gormPlanRepository) FindPlansWithPendingTasks(ctx context.Context) ([]*models.Plan, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindPlansWithPendingTasks")
var plans []*models.Plan
// 关联 pending_tasks, task_execution_logs, tasks 表来查找符合条件的计划
err := r.db.Table("plans").
err := r.db.WithContext(repoCtx).Table("plans").
Joins("JOIN tasks ON plans.id = tasks.plan_id").
Joins("JOIN task_execution_logs ON tasks.id = task_execution_logs.task_id").
Joins("JOIN pending_tasks ON task_execution_logs.id = pending_tasks.task_execution_log_id").
@@ -718,20 +751,21 @@ func (r *gormPlanRepository) FindPlansWithPendingTasks() ([]*models.Plan, error)
}
// StopPlanTransactionally 停止一个计划的执行,包括更新状态、移除待执行任务和更新执行日志。
func (r *gormPlanRepository) StopPlanTransactionally(planID uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
func (r *gormPlanRepository) StopPlanTransactionally(ctx context.Context, planID uint) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "StopPlanTransactionally")
return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
// 使用事务创建新的仓库实例,确保所有操作都在同一个事务中
planRepoTx := NewGormPlanRepository(tx)
executionLogRepoTx := NewGormExecutionLogRepository(tx)
pendingTaskRepoTx := NewGormPendingTaskRepository(tx)
planRepoTx := NewGormPlanRepository(repoCtx, tx)
executionLogRepoTx := NewGormExecutionLogRepository(repoCtx, tx)
pendingTaskRepoTx := NewGormPendingTaskRepository(repoCtx, tx)
// 1. 更新计划状态为“已停止”
if err := planRepoTx.UpdatePlanStatus(planID, models.PlanStatusDisabled); err != nil {
if err := planRepoTx.UpdatePlanStatus(repoCtx, planID, models.PlanStatusDisabled); err != nil {
return fmt.Errorf("更新计划 #%d 状态为 '已停止' 失败: %w", planID, err)
}
// 2. 查找当前正在进行的计划执行日志
planLog, err := executionLogRepoTx.FindInProgressPlanExecutionLogByPlanID(planID)
planLog, err := executionLogRepoTx.FindInProgressPlanExecutionLogByPlanID(repoCtx, planID)
if err != nil {
return fmt.Errorf("查找计划 #%d 正在进行的执行日志失败: %w", planID, err)
}
@@ -742,7 +776,7 @@ func (r *gormPlanRepository) StopPlanTransactionally(planID uint) error {
}
// 3. 查找所有需要被取消的任务执行日志
taskLogs, err := executionLogRepoTx.FindIncompleteTaskExecutionLogsByPlanLogID(planLog.ID)
taskLogs, err := executionLogRepoTx.FindIncompleteTaskExecutionLogsByPlanLogID(repoCtx, planLog.ID)
if err != nil {
return fmt.Errorf("查找计划执行日志 #%d 下未完成的任务日志失败: %w", planLog.ID, err)
}
@@ -754,12 +788,12 @@ func (r *gormPlanRepository) StopPlanTransactionally(planID uint) error {
}
// 3.1 批量更新任务执行日志状态为“已取消”
if err := executionLogRepoTx.UpdateTaskExecutionLogStatusByIDs(taskLogIDs, models.ExecutionStatusCancelled); err != nil {
if err := executionLogRepoTx.UpdateTaskExecutionLogStatusByIDs(repoCtx, taskLogIDs, models.ExecutionStatusCancelled); err != nil {
return fmt.Errorf("批量更新任务执行日志状态为 '已取消' 失败: %w", err)
}
// 3.2 查找并删除待执行队列中对应的任务
pendingTasks, err := pendingTaskRepoTx.FindPendingTasksByTaskLogIDs(taskLogIDs)
pendingTasks, err := pendingTaskRepoTx.FindPendingTasksByTaskLogIDs(repoCtx, taskLogIDs)
if err != nil {
return fmt.Errorf("查找计划执行日志 #%d 下对应的待执行任务失败: %w", planLog.ID, err)
}
@@ -769,14 +803,14 @@ func (r *gormPlanRepository) StopPlanTransactionally(planID uint) error {
for _, pt := range pendingTasks {
pendingTaskIDs = append(pendingTaskIDs, pt.ID)
}
if err := pendingTaskRepoTx.DeletePendingTasksByIDs(pendingTaskIDs); err != nil {
if err := pendingTaskRepoTx.DeletePendingTasksByIDs(repoCtx, pendingTaskIDs); err != nil {
return fmt.Errorf("批量删除待执行任务失败: %w", err)
}
}
}
// 4. 更新计划执行历史的总状态为“失败”
if err := executionLogRepoTx.UpdatePlanExecutionLogStatus(planLog.ID, models.ExecutionStatusFailed); err != nil {
if err := executionLogRepoTx.UpdatePlanExecutionLogStatus(repoCtx, planLog.ID, models.ExecutionStatusFailed); err != nil {
return fmt.Errorf("更新计划执行日志 #%d 状态为 '失败' 失败: %w", planLog.ID, err)
}
@@ -785,8 +819,9 @@ func (r *gormPlanRepository) StopPlanTransactionally(planID uint) error {
}
// UpdatePlanStatus 更新指定计划的状态
func (r *gormPlanRepository) UpdatePlanStatus(id uint, status models.PlanStatus) error {
result := r.db.Model(&models.Plan{}).Where("id = ?", id).Update("status", status)
func (r *gormPlanRepository) UpdatePlanStatus(ctx context.Context, id uint, 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 {
return result.Error
}
@@ -796,16 +831,18 @@ func (r *gormPlanRepository) UpdatePlanStatus(id uint, status models.PlanStatus)
return nil
}
func (r *gormPlanRepository) UpdatePlanStateAfterExecution(planID uint, newCount uint, newStatus models.PlanStatus) error {
return r.db.Model(&models.Plan{}).Where("id = ?", planID).Updates(map[string]interface{}{
func (r *gormPlanRepository) UpdatePlanStateAfterExecution(ctx context.Context, planID uint, newCount uint, 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,
"status": newStatus,
}).Error
}
// UpdateExecuteCount 更新指定计划的执行计数
func (r *gormPlanRepository) UpdateExecuteCount(id uint, count uint) error {
result := r.db.Model(&models.Plan{}).Where("id = ?", id).Update("execute_count", count)
func (r *gormPlanRepository) UpdateExecuteCount(ctx context.Context, id uint, count uint) 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 {
return result.Error
}

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -38,23 +41,25 @@ type FeedUsageRecordListOptions struct {
// RawMaterialRepository 定义了与原料相关的数据库操作接口
type RawMaterialRepository interface {
ListRawMaterialPurchases(opts RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error)
ListRawMaterialStockLogs(opts RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error)
ListFeedUsageRecords(opts FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error)
ListRawMaterialPurchases(ctx context.Context, opts RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error)
ListRawMaterialStockLogs(ctx context.Context, opts RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error)
ListFeedUsageRecords(ctx context.Context, opts FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error)
}
// gormRawMaterialRepository 是 RawMaterialRepository 的 GORM 实现
type gormRawMaterialRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormRawMaterialRepository 创建一个新的 RawMaterialRepository GORM 实现实例
func NewGormRawMaterialRepository(db *gorm.DB) RawMaterialRepository {
return &gormRawMaterialRepository{db: db}
func NewGormRawMaterialRepository(ctx context.Context, db *gorm.DB) RawMaterialRepository {
return &gormRawMaterialRepository{ctx: ctx, db: db}
}
// ListRawMaterialPurchases 实现了分页和过滤查询原料采购记录的功能
func (r *gormRawMaterialRepository) ListRawMaterialPurchases(opts RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error) {
func (r *gormRawMaterialRepository) ListRawMaterialPurchases(ctx context.Context, opts RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterialPurchases")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -62,7 +67,7 @@ func (r *gormRawMaterialRepository) ListRawMaterialPurchases(opts RawMaterialPur
var results []models.RawMaterialPurchase
var total int64
query := r.db.Model(&models.RawMaterialPurchase{})
query := r.db.WithContext(repoCtx).Model(&models.RawMaterialPurchase{})
if opts.RawMaterialID != nil {
query = query.Where("raw_material_id = ?", *opts.RawMaterialID)
@@ -94,7 +99,8 @@ func (r *gormRawMaterialRepository) ListRawMaterialPurchases(opts RawMaterialPur
}
// ListRawMaterialStockLogs 实现了分页和过滤查询原料库存日志的功能
func (r *gormRawMaterialRepository) ListRawMaterialStockLogs(opts RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) {
func (r *gormRawMaterialRepository) ListRawMaterialStockLogs(ctx context.Context, opts RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterialStockLogs")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -102,7 +108,7 @@ func (r *gormRawMaterialRepository) ListRawMaterialStockLogs(opts RawMaterialSto
var results []models.RawMaterialStockLog
var total int64
query := r.db.Model(&models.RawMaterialStockLog{})
query := r.db.WithContext(repoCtx).Model(&models.RawMaterialStockLog{})
if opts.RawMaterialID != nil {
query = query.Where("raw_material_id = ?", *opts.RawMaterialID)
@@ -137,7 +143,8 @@ func (r *gormRawMaterialRepository) ListRawMaterialStockLogs(opts RawMaterialSto
}
// ListFeedUsageRecords 实现了分页和过滤查询饲料使用记录的功能
func (r *gormRawMaterialRepository) ListFeedUsageRecords(opts FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error) {
func (r *gormRawMaterialRepository) ListFeedUsageRecords(ctx context.Context, opts FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListFeedUsageRecords")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -145,7 +152,7 @@ func (r *gormRawMaterialRepository) ListFeedUsageRecords(opts FeedUsageRecordLis
var results []models.FeedUsageRecord
var total int64
query := r.db.Model(&models.FeedUsageRecord{})
query := r.db.WithContext(repoCtx).Model(&models.FeedUsageRecord{})
if opts.PenID != nil {
query = query.Where("pen_id = ?", *opts.PenID)

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -18,39 +21,43 @@ type SensorDataListOptions struct {
// SensorDataRepository 定义了与传感器数据相关的数据库操作接口。
type SensorDataRepository interface {
Create(sensorData *models.SensorData) error
GetLatestSensorDataByDeviceIDAndSensorType(deviceID uint, sensorType models.SensorType) (*models.SensorData, error)
Create(ctx context.Context, sensorData *models.SensorData) error
GetLatestSensorDataByDeviceIDAndSensorType(ctx context.Context, deviceID uint, sensorType models.SensorType) (*models.SensorData, error)
// List 支持分页和过滤的列表查询
List(opts SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error)
List(ctx context.Context, opts SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error)
}
// gormSensorDataRepository 是 SensorDataRepository 的 GORM 实现。
type gormSensorDataRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormSensorDataRepository 创建一个新的 SensorDataRepository GORM 实现实例。
func NewGormSensorDataRepository(db *gorm.DB) SensorDataRepository {
return &gormSensorDataRepository{db: db}
func NewGormSensorDataRepository(ctx context.Context, db *gorm.DB) SensorDataRepository {
return &gormSensorDataRepository{ctx: ctx, db: db}
}
// Create 将一条新的传感器数据记录插入数据库。
func (r *gormSensorDataRepository) Create(sensorData *models.SensorData) error {
return r.db.Create(sensorData).Error
func (r *gormSensorDataRepository) Create(ctx context.Context, sensorData *models.SensorData) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
return r.db.WithContext(repoCtx).Create(sensorData).Error
}
// GetLatestSensorDataByDeviceIDAndSensorType 根据设备ID和传感器类型查询最新的传感器数据。
func (r *gormSensorDataRepository) GetLatestSensorDataByDeviceIDAndSensorType(deviceID uint, sensorType models.SensorType) (*models.SensorData, error) {
func (r *gormSensorDataRepository) GetLatestSensorDataByDeviceIDAndSensorType(ctx context.Context, deviceID uint, sensorType models.SensorType) (*models.SensorData, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLatestSensorDataByDeviceIDAndSensorType")
var sensorData models.SensorData
// 增加一个时间范围来缩小查询范围, 从而加快查找速度, 当使用时序数据库时时间范围可以让数据库忽略时间靠前的分片
err := r.db.Where("device_id = ? AND sensor_type = ? AND time >=?", deviceID, sensorType, time.Now().Add(-24*time.Hour)).
err := r.db.WithContext(repoCtx).Where("device_id = ? AND sensor_type = ? AND time >=?", deviceID, sensorType, time.Now().Add(-24*time.Hour)).
Order("time DESC").
First(&sensorData).Error
return &sensorData, err
}
// List 实现了分页和过滤查询传感器数据的功能
func (r *gormSensorDataRepository) List(opts SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) {
func (r *gormSensorDataRepository) List(ctx context.Context, opts SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "List")
// --- 校验分页参数 ---
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
@@ -59,7 +66,7 @@ func (r *gormSensorDataRepository) List(opts SensorDataListOptions, page, pageSi
var results []models.SensorData
var total int64
query := r.db.Model(&models.SensorData{})
query := r.db.WithContext(repoCtx).Model(&models.SensorData{})
// --- 应用过滤条件 ---
if opts.DeviceID != nil {

View File

@@ -1,9 +1,11 @@
package repository
import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"gorm.io/gorm"
)
@@ -12,35 +14,40 @@ type UnitOfWork interface {
// ExecuteInTransaction 在一个数据库事务中执行给定的函数。
// 如果函数返回错误,事务将被回滚;否则,事务将被提交。
// tx 参数是当前事务的 GORM DB 实例,应传递给所有仓库方法。
ExecuteInTransaction(fn func(tx *gorm.DB) error) error
ExecuteInTransaction(ctx context.Context, fn func(tx *gorm.DB) error) error
}
// gormUnitOfWork 是 UnitOfWork 接口的 GORM 实现
type gormUnitOfWork struct {
db *gorm.DB
logger *logs.Logger // 添加日志记录器
ctx context.Context
db *gorm.DB
}
// NewGormUnitOfWork 创建一个新的 gormUnitOfWork 实例
func NewGormUnitOfWork(db *gorm.DB, logger *logs.Logger) UnitOfWork {
return &gormUnitOfWork{db: db, logger: logger}
func NewGormUnitOfWork(ctx context.Context, db *gorm.DB) UnitOfWork {
return &gormUnitOfWork{
ctx: ctx,
db: db,
}
}
// ExecuteInTransaction 实现了 UnitOfWork 接口的事务执行逻辑
func (u *gormUnitOfWork) ExecuteInTransaction(fn func(tx *gorm.DB) error) error {
tx := u.db.Begin()
func (u *gormUnitOfWork) ExecuteInTransaction(ctx context.Context, fn func(tx *gorm.DB) error) error {
uowCtx, logger := logs.Trace(ctx, u.ctx, "ExecuteInTransaction")
tx := u.db.WithContext(uowCtx).Begin()
if tx.Error != nil {
u.logger.Errorf("开启数据库事务失败: %v", tx.Error) // 记录错误日志
logger.Errorf("开启数据库事务失败: %v", tx.Error) // 记录错误日志
return fmt.Errorf("开启事务失败: %w", tx.Error)
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
u.logger.Errorf("事务中发生 panic已回滚: %v", r) // 记录 panic 日志
logger.Errorf("事务中发生 panic已回滚: %v", r) // 记录 panic 日志
} else if tx.Error != nil { // 如果函数执行过程中返回错误,或者事务本身有错误,则回滚
tx.Rollback()
u.logger.Errorf("事务执行失败,已回滚: %v", tx.Error) // 记录错误日志
logger.Errorf("事务执行失败,已回滚: %v", tx.Error) // 记录错误日志
}
}()
@@ -52,7 +59,7 @@ func (u *gormUnitOfWork) ExecuteInTransaction(fn func(tx *gorm.DB) error) error
// 提交事务
if err := tx.Commit().Error; err != nil {
u.logger.Errorf("提交数据库事务失败: %v", err) // 记录错误日志
logger.Errorf("提交数据库事务失败: %v", err) // 记录错误日志
return fmt.Errorf("提交事务失败: %w", err)
}

View File

@@ -1,9 +1,12 @@
package repository
import (
"context"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
@@ -20,27 +23,30 @@ type UserActionLogListOptions struct {
// UserActionLogRepository 定义了与用户操作日志相关的数据库操作接口
type UserActionLogRepository interface {
Create(log *models.UserActionLog) error
List(opts UserActionLogListOptions, page, pageSize int) ([]models.UserActionLog, int64, error)
Create(ctx context.Context, log *models.UserActionLog) error
List(ctx context.Context, opts UserActionLogListOptions, page, pageSize int) ([]models.UserActionLog, int64, error)
}
// gormUserActionLogRepository 是 UserActionLogRepository 的 GORM 实现
type gormUserActionLogRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormUserActionLogRepository 创建一个新的 UserActionLogRepository GORM 实现实例
func NewGormUserActionLogRepository(db *gorm.DB) UserActionLogRepository {
return &gormUserActionLogRepository{db: db}
func NewGormUserActionLogRepository(ctx context.Context, db *gorm.DB) UserActionLogRepository {
return &gormUserActionLogRepository{ctx: ctx, db: db}
}
// Create 创建一条新的用户操作日志记录
func (r *gormUserActionLogRepository) Create(log *models.UserActionLog) error {
return r.db.Create(log).Error
func (r *gormUserActionLogRepository) Create(ctx context.Context, log *models.UserActionLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
return r.db.WithContext(repoCtx).Create(log).Error
}
// List 根据选项查询用户操作日志,并返回总数
func (r *gormUserActionLogRepository) List(opts UserActionLogListOptions, page, pageSize int) ([]models.UserActionLog, int64, error) {
func (r *gormUserActionLogRepository) List(ctx context.Context, opts UserActionLogListOptions, page, pageSize int) ([]models.UserActionLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "List")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
@@ -48,7 +54,7 @@ func (r *gormUserActionLogRepository) List(opts UserActionLogListOptions, page,
var logs []models.UserActionLog
var total int64
query := r.db.Model(&models.UserActionLog{})
query := r.db.WithContext(repoCtx).Model(&models.UserActionLog{})
if opts.UserID != nil {
query = query.Where("user_id = ?", *opts.UserID)

View File

@@ -2,40 +2,47 @@
package repository
import (
"context"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// UserRepository 定义了与用户模型相关的数据库操作接口
// 这是为了让业务逻辑层依赖于抽象,而不是具体的数据库实现
type UserRepository interface {
Create(user *models.User) error
FindByUsername(username string) (*models.User, error)
FindByID(id uint) (*models.User, error)
FindUserForLogin(identifier string) (*models.User, error)
FindAll() ([]*models.User, error)
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)
FindUserForLogin(ctx context.Context, identifier string) (*models.User, error)
FindAll(ctx context.Context) ([]*models.User, error)
}
// gormUserRepository 是 UserRepository 的 GORM 实现
type gormUserRepository struct {
db *gorm.DB
ctx context.Context
db *gorm.DB
}
// NewGormUserRepository 创建一个新的 UserRepository GORM 实现实例
func NewGormUserRepository(db *gorm.DB) UserRepository {
return &gormUserRepository{db: db}
func NewGormUserRepository(ctx context.Context, db *gorm.DB) UserRepository {
return &gormUserRepository{ctx: ctx, db: db}
}
// Create 创建一个新的用户记录
func (r *gormUserRepository) Create(user *models.User) error {
func (r *gormUserRepository) Create(ctx context.Context, user *models.User) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "Create")
// BeforeSave 钩子会在这里被自动触发
return r.db.Create(user).Error
return r.db.WithContext(repoCtx).Create(user).Error
}
// FindByUsername 根据用户名查找用户
func (r *gormUserRepository) FindByUsername(username string) (*models.User, error) {
func (r *gormUserRepository) FindByUsername(ctx context.Context, username string) (*models.User, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByUsername")
var user models.User
if err := r.db.Where("username = ?", username).First(&user).Error; err != nil {
if err := r.db.WithContext(repoCtx).Where("username = ?", username).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
@@ -43,10 +50,11 @@ func (r *gormUserRepository) FindByUsername(username string) (*models.User, erro
// FindUserForLogin 根据提供的标识符查找用户,可用于登录验证
// 标识符可以是用户名、邮箱、手机号、微信号或飞书账号
func (r *gormUserRepository) FindUserForLogin(identifier string) (*models.User, error) {
func (r *gormUserRepository) FindUserForLogin(ctx context.Context, identifier string) (*models.User, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindUserForLogin")
var user models.User
// 使用 ->> 操作符来查询 JSONB 字段中的文本值
err := r.db.Where(
err := r.db.WithContext(repoCtx).Where(
"username = ? OR contact ->> 'email' = ? OR contact ->> 'phone' = ? OR contact ->> 'wechat' = ? OR contact ->> 'feishu' = ?",
identifier, identifier, identifier, identifier, identifier,
).First(&user).Error
@@ -58,18 +66,20 @@ func (r *gormUserRepository) FindUserForLogin(identifier string) (*models.User,
}
// FindByID 根据 ID 查找用户
func (r *gormUserRepository) FindByID(id uint) (*models.User, error) {
func (r *gormUserRepository) FindByID(ctx context.Context, id uint) (*models.User, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindByID")
var user models.User
if err := r.db.First(&user, id).Error; err != nil {
if err := r.db.WithContext(repoCtx).First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
// FindAll 返回数据库中的所有用户
func (r *gormUserRepository) FindAll() ([]*models.User, error) {
func (r *gormUserRepository) FindAll(ctx context.Context) ([]*models.User, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "FindAll")
var users []*models.User
if err := r.db.Where("1 = 1").Find(&users).Error; err != nil {
if err := r.db.WithContext(repoCtx).Where("1 = 1").Find(&users).Error; err != nil {
return nil, err
}
return users, nil

View File

@@ -1,32 +1,33 @@
package lora
import (
"context"
"errors"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora/chirp_stack_proto/client"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora/chirp_stack_proto/client/device_service"
"github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora/chirp_stack_proto/client"
)
// ChirpStackTransport 是一个客户端,用于封装与 ChirpStack REST API 的交互。
type ChirpStackTransport struct {
ctx context.Context
client *client.ChirpStackRESTAPI
authInfo runtime.ClientAuthInfoWriter
config config.ChirpStackConfig
logger *logs.Logger
}
// NewChirpStackTransport 创建一个新的通信实例,用于与 ChirpStack 通信。
func NewChirpStackTransport(
ctx context.Context,
config config.ChirpStackConfig,
logger *logs.Logger,
) *ChirpStackTransport {
// 使用配置中的服务器地址创建一个 HTTP transport。
// 它会使用生成的客户端中定义的默认 base path 和 schemes。
@@ -39,14 +40,15 @@ func NewChirpStackTransport(
authInfo := httptransport.APIKeyAuth("grpc-metadata-authorization", "header", config.GenerateAPIKey())
return &ChirpStackTransport{
ctx: ctx,
client: apiClient,
authInfo: authInfo,
config: config,
logger: logger,
}
}
func (c *ChirpStackTransport) Send(address string, payload []byte) (*transport.SendResult, error) {
func (c *ChirpStackTransport) Send(ctx context.Context, address string, payload []byte) (*transport.SendResult, error) {
logger := logs.TraceLogger(ctx, c.ctx, "Send")
// 1. 构建 API 请求体。
// - Confirmed: true 表示确认消息, 设为false将不保证消息送达(但可以节约下行容量)。
// - Data: 经过 Base64 编码的数据。
@@ -72,18 +74,18 @@ func (c *ChirpStackTransport) Send(address string, payload []byte) (*transport.S
// c.authInfo 是您在 NewChirpStackTransport 中创建的认证信息。
resp, err := c.client.DeviceService.DeviceServiceEnqueue(params, c.authInfo)
if err != nil {
c.logger.Errorf("设备 %s 调用ChirpStack Enqueue失败: %v", address, err)
logger.Errorf("设备 %s 调用ChirpStack Enqueue失败: %v", address, err)
return nil, err
}
if resp == nil || resp.Payload == nil || resp.Payload.ID == "" {
// 这是一个需要明确处理的错误情况,因为调用方依赖 MessageID。
errMsg := "ChirpStack Enqueue 响应未包含 MessageID (ID)"
c.logger.Errorf(errMsg)
logger.Errorf(errMsg)
return nil, errors.New(errMsg)
}
c.logger.Infof("成功将 payload 发送到设备 %s 的队列 (MessageID: %s)", address, resp.Payload.ID)
logger.Infof("成功将 payload 发送到设备 %s 的队列 (MessageID: %s)", address, resp.Payload.ID)
// 将 MessageID 包装在 SendResult 中返回
result := &transport.SendResult{

View File

@@ -2,6 +2,7 @@ package lora
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"fmt"
@@ -17,6 +18,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/proto"
"github.com/google/uuid"
"github.com/tarm/serial"
gproto "google.golang.org/protobuf/proto"
@@ -41,8 +43,8 @@ type message struct {
// LoRaMeshUartPassthroughTransport 实现了 transport.Communicator 和 transport.Listener 接口
type LoRaMeshUartPassthroughTransport struct {
ctx context.Context
config config.LoraMeshConfig
logger *logs.Logger
port *serial.Port
mu sync.Mutex // 用于保护对外的公共方法如Send的并发调用
@@ -87,8 +89,8 @@ type reassemblyBuffer struct {
// NewLoRaMeshUartPassthroughTransport 创建一个新的 LoRaMeshUartPassthroughTransport 实例
func NewLoRaMeshUartPassthroughTransport(
ctx context.Context,
config config.LoraMeshConfig,
logger *logs.Logger,
areaControllerRepo repository.AreaControllerRepository,
pendingCollectionRepo repository.PendingCollectionRepository,
deviceRepo repository.DeviceRepository,
@@ -106,8 +108,8 @@ func NewLoRaMeshUartPassthroughTransport(
}
t := &LoRaMeshUartPassthroughTransport{
ctx: ctx,
config: config,
logger: logger,
port: port,
state: stateIdle,
stopChan: make(chan struct{}),
@@ -126,15 +128,16 @@ func NewLoRaMeshUartPassthroughTransport(
}
// Listen 启动后台监听协程(非阻塞)
func (t *LoRaMeshUartPassthroughTransport) Listen() error {
func (t *LoRaMeshUartPassthroughTransport) Listen(ctx context.Context) error {
loraCtx, logger := logs.Trace(ctx, t.ctx, "Listen")
t.wg.Add(1)
go t.workerLoop()
t.logger.Info("LoRa传输层工作协程已启动")
go t.workerLoop(loraCtx)
logger.Info("LoRa传输层工作协程已启动")
return nil
}
// Send 将发送任务提交给worker协程
func (t *LoRaMeshUartPassthroughTransport) Send(address string, payload []byte) (*transport.SendResult, error) {
func (t *LoRaMeshUartPassthroughTransport) Send(ctx context.Context, address string, payload []byte) (*transport.SendResult, error) {
t.mu.Lock()
defer t.mu.Unlock()
@@ -156,14 +159,16 @@ func (t *LoRaMeshUartPassthroughTransport) Send(address string, payload []byte)
}
// Stop 停止传输层
func (t *LoRaMeshUartPassthroughTransport) Stop() error {
func (t *LoRaMeshUartPassthroughTransport) Stop(ctx context.Context) error {
close(t.stopChan)
t.wg.Wait()
return t.port.Close()
}
// workerLoop 是核心的状态机和调度器
func (t *LoRaMeshUartPassthroughTransport) workerLoop() {
func (t *LoRaMeshUartPassthroughTransport) workerLoop(ctx context.Context) {
loraCtx, logger := logs.Trace(ctx, t.ctx, "workerLoop")
defer t.wg.Done()
readBuffer := make([]byte, 1024)
@@ -176,7 +181,7 @@ func (t *LoRaMeshUartPassthroughTransport) workerLoop() {
if t.reassemblyTimeout != nil {
t.reassemblyTimeout.Stop()
}
t.logger.Info("LoRa传输层工作协程已停止")
logger.Info("LoRa传输层工作协程已停止")
return
default:
}
@@ -188,7 +193,7 @@ func (t *LoRaMeshUartPassthroughTransport) workerLoop() {
}
if err != nil && err != io.EOF {
// 忽略预期的超时错误(io.EOF)只记录真正的IO错误
t.logger.Errorf("从串口读取数据时发生错误: %v", err)
logger.Errorf("从串口读取数据时发生错误: %v", err)
}
// 3. 循环解析缓冲区中的完整物理帧
@@ -197,27 +202,29 @@ func (t *LoRaMeshUartPassthroughTransport) workerLoop() {
if frame == nil {
break // 缓冲区中没有更多完整帧了
}
t.handleFrame(frame)
t.handleFrame(loraCtx, frame)
}
// 4. 根据当前状态执行主要逻辑
switch t.state {
case stateIdle:
t.runIdleState()
t.runIdleState(loraCtx)
case stateReceiving:
t.runReceivingState()
t.runReceivingState(loraCtx)
default:
}
}
}
// runIdleState 处理空闲状态下的逻辑,主要是检查并启动发送任务
func (t *LoRaMeshUartPassthroughTransport) runIdleState() {
func (t *LoRaMeshUartPassthroughTransport) runIdleState(ctx context.Context) {
loraCtx := logs.AddFuncName(ctx, t.ctx, "Listen")
select {
case req := <-t.sendChan:
t.state = stateSending
// 此处为阻塞式发送
result, err := t.executeSend(req)
result, err := t.executeSend(loraCtx, req)
req.result <- &sendResultTuple{result: result, err: err}
t.state = stateIdle
default:
@@ -226,10 +233,11 @@ func (t *LoRaMeshUartPassthroughTransport) runIdleState() {
}
// runReceivingState 处理接收状态下的逻辑,主要是检查超时
func (t *LoRaMeshUartPassthroughTransport) runReceivingState() {
func (t *LoRaMeshUartPassthroughTransport) runReceivingState(ctx context.Context) {
logger := logs.TraceLogger(ctx, t.ctx, "runReceivingState")
select {
case sourceAddr := <-t.reassemblyTimeoutCh:
t.logger.Warnf("接收来自 0x%04X 的消息超时", sourceAddr)
logger.Warnf("接收来自 0x%04X 的消息超时", sourceAddr)
delete(t.reassemblyBuffers, sourceAddr)
t.state = stateIdle
default:
@@ -238,7 +246,8 @@ func (t *LoRaMeshUartPassthroughTransport) runReceivingState() {
}
// executeSend 执行完整的发送流程(分片、构建、写入)
func (t *LoRaMeshUartPassthroughTransport) executeSend(req *sendRequest) (*transport.SendResult, error) {
func (t *LoRaMeshUartPassthroughTransport) executeSend(ctx context.Context, req *sendRequest) (*transport.SendResult, error) {
logger := logs.TraceLogger(ctx, t.ctx, "executeSend")
chunks := splitPayload(req.payload, t.config.MaxChunkSize)
totalChunks := uint8(len(chunks))
@@ -257,7 +266,7 @@ func (t *LoRaMeshUartPassthroughTransport) executeSend(req *sendRequest) (*trans
frame.WriteByte(currentChunk) // 当前包序号
frame.Write(chunk) // 数据块
t.logger.Infof("构建LoRa数据包: %v", frame.Bytes())
logger.Infof("构建LoRa数据包: %v", frame.Bytes())
_, err := t.port.Write(frame.Bytes())
if err != nil {
return nil, fmt.Errorf("写入串口失败: %w", err)
@@ -272,9 +281,10 @@ func (t *LoRaMeshUartPassthroughTransport) executeSend(req *sendRequest) (*trans
}
// handleFrame 处理一个从串口解析出的完整物理帧
func (t *LoRaMeshUartPassthroughTransport) handleFrame(frame []byte) {
func (t *LoRaMeshUartPassthroughTransport) handleFrame(ctx context.Context, frame []byte) {
loraCtx, logger := logs.Trace(ctx, t.ctx, "handleFrame")
if len(frame) < 8 {
t.logger.Warnf("收到了一个无效长度的帧: %d", len(frame))
logger.Warnf("收到了一个无效长度的帧: %d", len(frame))
return
}
@@ -291,7 +301,7 @@ func (t *LoRaMeshUartPassthroughTransport) handleFrame(frame []byte) {
DestAddr: fmt.Sprintf("%04X", destAddr),
Payload: chunkData,
}
go t.handleUpstreamMessage(msg)
go t.handleUpstreamMessage(loraCtx, msg)
return
}
@@ -316,18 +326,18 @@ func (t *LoRaMeshUartPassthroughTransport) handleFrame(frame []byte) {
t.reassemblyTimeoutCh <- sourceAddr
})
} else {
t.logger.Warnf("在空闲状态下收到了一个来自 0x%04X 的非首包分片,已忽略。", sourceAddr)
logger.Warnf("在空闲状态下收到了一个来自 0x%04X 的非首包分片,已忽略。", sourceAddr)
}
case stateReceiving:
if sourceAddr != t.currentRecvSource {
t.logger.Warnf("正在接收来自 0x%04X 的数据时,收到了另一个源 0x%04X 的分片,已忽略。", t.currentRecvSource, sourceAddr)
logger.Warnf("正在接收来自 0x%04X 的数据时,收到了另一个源 0x%04X 的分片,已忽略。", t.currentRecvSource, sourceAddr)
return
}
buffer, ok := t.reassemblyBuffers[sourceAddr]
if !ok {
t.logger.Errorf("内部错误: 处于接收状态,但没有为 0x%04X 找到缓冲区", sourceAddr)
logger.Errorf("内部错误: 处于接收状态,但没有为 0x%04X 找到缓冲区", sourceAddr)
t.state = stateIdle // 重置状态
return
}
@@ -352,23 +362,27 @@ func (t *LoRaMeshUartPassthroughTransport) handleFrame(frame []byte) {
DestAddr: fmt.Sprintf("%04X", destAddr),
Payload: fullPayload.Bytes(),
}
go t.handleUpstreamMessage(msg)
go t.handleUpstreamMessage(loraCtx, msg)
// 清理并返回空闲状态
delete(t.reassemblyBuffers, sourceAddr)
t.state = stateIdle
}
default:
logger.Errorf("内部错误: 状态机处于未知状态 %d", t.state)
}
}
// handleUpstreamMessage 在独立的协程中处理单个上行的、完整的消息。
func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(msg *message) {
t.logger.Infof("开始处理来自 %s 的上行消息", msg.SourceAddr)
func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(ctx context.Context, msg *message) {
loraCtx, logger := logs.Trace(ctx, t.ctx, "handleUpstreamMessage")
logger.Infof("开始处理来自 %s 的上行消息", msg.SourceAddr)
// 1. 解析外层 "信封"
var instruction proto.Instruction
if err := gproto.Unmarshal(msg.Payload, &instruction); err != nil {
t.logger.Errorf("解析上行 Instruction Protobuf 失败: %v, 源地址: %s, 原始数据: %x", err, msg.SourceAddr, msg.Payload)
logger.Errorf("解析上行 Instruction Protobuf 失败: %v, 源地址: %s, 原始数据: %x", err, msg.SourceAddr, msg.Payload)
return
}
@@ -379,39 +393,39 @@ func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(msg *message) {
collectResp = p.CollectResult
default:
// 如果上行的数据不是采集结果,记录日志并忽略
t.logger.Infof("收到一个非采集响应的上行指令 (类型: %T),无需处理。源地址: %s", p, msg.SourceAddr)
logger.Infof("收到一个非采集响应的上行指令 (类型: %T),无需处理。源地址: %s", p, msg.SourceAddr)
return
}
if collectResp == nil {
t.logger.Errorf("从 Instruction 中提取的 CollectResult 为 nil。源地址: %s", msg.SourceAddr)
logger.Errorf("从 Instruction 中提取的 CollectResult 为 nil。源地址: %s", msg.SourceAddr)
return
}
correlationID := collectResp.CorrelationId
t.logger.Infof("成功解析采集响应 (CorrelationID: %s),包含 %d 个值。", correlationID, len(collectResp.Values))
logger.Infof("成功解析采集响应 (CorrelationID: %s),包含 %d 个值。", correlationID, len(collectResp.Values))
// 3. 查找区域主控 (注意LoRa Mesh 的 SourceAddr 对应于区域主控的 NetworkID)
regionalController, err := t.areaControllerRepo.FindByNetworkID(msg.SourceAddr)
regionalController, err := t.areaControllerRepo.FindByNetworkID(loraCtx, msg.SourceAddr)
if err != nil {
t.logger.Errorf("处理上行消息失败:无法通过源地址 '%s' 找到区域主控设备: %v", msg.SourceAddr, err)
logger.Errorf("处理上行消息失败:无法通过源地址 '%s' 找到区域主控设备: %v", msg.SourceAddr, err)
return
}
if err := regionalController.SelfCheck(); err != nil {
t.logger.Errorf("处理上行消息失败:区域主控 %v(ID: %d) 未通过自检: %v", regionalController.Name, regionalController.ID, err)
logger.Errorf("处理上行消息失败:区域主控 %v(ID: %d) 未通过自检: %v", regionalController.Name, regionalController.ID, err)
return
}
// 4. 根据 CorrelationID 查找待处理请求
pendingReq, err := t.pendingCollectionRepo.FindByCorrelationID(correlationID)
pendingReq, err := t.pendingCollectionRepo.FindByCorrelationID(loraCtx, correlationID)
if err != nil {
t.logger.Errorf("处理采集响应失败:无法找到待处理请求 (CorrelationID: %s): %v", correlationID, err)
logger.Errorf("处理采集响应失败:无法找到待处理请求 (CorrelationID: %s): %v", correlationID, err)
return
}
// 检查状态,防止重复处理
if pendingReq.Status != models.PendingStatusPending && pendingReq.Status != models.PendingStatusTimedOut {
t.logger.Warnf("收到一个已处理过的采集响应 (CorrelationID: %s, Status: %s),将忽略。", correlationID, pendingReq.Status)
logger.Warnf("收到一个已处理过的采集响应 (CorrelationID: %s, Status: %s),将忽略。", correlationID, pendingReq.Status)
return
}
@@ -419,10 +433,10 @@ func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(msg *message) {
deviceIDs := pendingReq.CommandMetadata
values := collectResp.Values
if len(deviceIDs) != len(values) {
t.logger.Errorf("数据不匹配:下行指令要求采集 %d 个设备,但上行响应包含 %d 个值 (CorrelationID: %s)", len(deviceIDs), len(values), correlationID)
err = t.pendingCollectionRepo.UpdateStatusToFulfilled(correlationID, time.Now())
logger.Errorf("数据不匹配:下行指令要求采集 %d 个设备,但上行响应包含 %d 个值 (CorrelationID: %s)", len(deviceIDs), len(values), correlationID)
err = t.pendingCollectionRepo.UpdateStatusToFulfilled(loraCtx, correlationID, time.Now())
if err != nil {
t.logger.Errorf("处理采集响应失败:无法更新待处理请求 (CorrelationID: %s) 的状态为完成: %v", correlationID, err)
logger.Errorf("处理采集响应失败:无法更新待处理请求 (CorrelationID: %s) 的状态为完成: %v", correlationID, err)
}
return
}
@@ -431,31 +445,31 @@ func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(msg *message) {
rawSensorValue := values[i]
if math.IsNaN(float64(rawSensorValue)) {
t.logger.Warnf("设备 (ID: %d) 上报了一个无效的 NaN 值,已跳过当前值的记录。", deviceID)
logger.Warnf("设备 (ID: %d) 上报了一个无效的 NaN 值,已跳过当前值的记录。", deviceID)
continue
}
dev, err := t.deviceRepo.FindByID(deviceID)
dev, err := t.deviceRepo.FindByID(loraCtx, deviceID)
if err != nil {
t.logger.Errorf("处理采集数据失败:无法找到设备 (ID: %d): %v", deviceID, err)
logger.Errorf("处理采集数据失败:无法找到设备 (ID: %d): %v", deviceID, err)
continue
}
if err := dev.SelfCheck(); err != nil {
t.logger.Warnf("跳过设备 %d因其未通过自检: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其未通过自检: %v", dev.ID, err)
continue
}
if err := dev.DeviceTemplate.SelfCheck(); err != nil {
t.logger.Warnf("跳过设备 %d因其设备模板未通过自检: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其设备模板未通过自检: %v", dev.ID, err)
continue
}
var valueDescriptors []*models.ValueDescriptor
if err := dev.DeviceTemplate.ParseValues(&valueDescriptors); err != nil {
t.logger.Warnf("跳过设备 %d因其设备模板的 Values 属性解析失败: %v", dev.ID, err)
logger.Warnf("跳过设备 %d因其设备模板的 Values 属性解析失败: %v", dev.ID, err)
continue
}
if len(valueDescriptors) == 0 {
t.logger.Warnf("跳过设备 %d因其设备模板缺少 ValueDescriptor 定义", dev.ID)
logger.Warnf("跳过设备 %d因其设备模板缺少 ValueDescriptor 定义", dev.ID)
continue
}
valueDescriptor := valueDescriptors[0]
@@ -471,27 +485,29 @@ func (t *LoRaMeshUartPassthroughTransport) handleUpstreamMessage(msg *message) {
case models.SensorTypeWeight:
dataToRecord = models.WeightData{WeightKilograms: parsedValue}
default:
t.logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type)
logger.Warnf("未知的传感器类型 '%s',将使用通用格式记录", valueDescriptor.Type)
dataToRecord = map[string]float64{"value": parsedValue}
}
t.recordSensorData(regionalController.ID, dev.ID, time.Now(), valueDescriptor.Type, dataToRecord)
t.logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 原始值=%f, 解析值=%.2f", dev.ID, valueDescriptor.Type, rawSensorValue, parsedValue)
t.recordSensorData(loraCtx, regionalController.ID, dev.ID, time.Now(), valueDescriptor.Type, dataToRecord)
logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 原始值=%f, 解析值=%.2f", dev.ID, valueDescriptor.Type, rawSensorValue, parsedValue)
}
// 6. 更新请求状态为“已完成”
if err := t.pendingCollectionRepo.UpdateStatusToFulfilled(correlationID, time.Now()); err != nil {
t.logger.Errorf("更新待采集请求状态为 'fulfilled' 失败 (CorrelationID: %s): %v", correlationID, err)
if err := t.pendingCollectionRepo.UpdateStatusToFulfilled(loraCtx, correlationID, time.Now()); err != nil {
logger.Errorf("更新待采集请求状态为 'fulfilled' 失败 (CorrelationID: %s): %v", correlationID, err)
} else {
t.logger.Infof("成功完成并关闭采集请求 (CorrelationID: %s)", correlationID)
logger.Infof("成功完成并关闭采集请求 (CorrelationID: %s)", correlationID)
}
}
// recordSensorData 是一个通用方法,用于将传感器数据存入数据库。
func (t *LoRaMeshUartPassthroughTransport) recordSensorData(regionalControllerID uint, sensorDeviceID uint, eventTime time.Time, sensorType models.SensorType, data interface{}) {
func (t *LoRaMeshUartPassthroughTransport) recordSensorData(ctx context.Context, regionalControllerID uint, sensorDeviceID uint, eventTime time.Time, sensorType models.SensorType, data interface{}) {
loraCtx, logger := logs.Trace(ctx, t.ctx, "recordSensorData")
jsonData, err := json.Marshal(data)
if err != nil {
t.logger.Errorf("记录传感器数据失败:序列化数据为 JSON 时出错: %v", err)
logger.Errorf("记录传感器数据失败:序列化数据为 JSON 时出错: %v", err)
return
}
@@ -503,8 +519,8 @@ func (t *LoRaMeshUartPassthroughTransport) recordSensorData(regionalControllerID
Data: datatypes.JSON(jsonData),
}
if err := t.sensorDataRepo.Create(sensorData); err != nil {
t.logger.Errorf("记录传感器数据失败:存入数据库时出错: %v", err)
if err := t.sensorDataRepo.Create(loraCtx, sensorData); err != nil {
logger.Errorf("记录传感器数据失败:存入数据库时出错: %v", err)
}
}

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