issue_56 #58

Merged
huang merged 17 commits from issue_56 into main 2025-11-05 23:56:50 +08:00
28 changed files with 943 additions and 793 deletions
Showing only changes of commit 07d8c719ac - Show all commits

View File

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

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"
@@ -42,8 +41,8 @@ type API struct {
echo *echo.Echo // Echo 引擎实例,用于处理 HTTP 请求
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 // 用户控制器实例
@@ -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 引擎实例
@@ -84,13 +83,13 @@ func NewAPI(cfg config.ServerConfig,
// 初始化 API 结构体
baseCtx := context.Background()
api := &API{
echo: e,
Ctx: ctx,
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(baseCtx, userService),
// 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员

View File

@@ -54,8 +54,8 @@ func (a *API) setupRoutes() {
// --- Authenticated Routes ---
// 所有在此注册的路由都需要通过 JWT 身份验证
authGroup := a.echo.Group("/api/v1")
authGroup.Use(middleware.AuthMiddleware(logs.AddCompName(context.Background(), "AuthMiddleware"), a.tokenService, a.userRepo)) // 1. 身份认证中间件
authGroup.Use(middleware.AuditLogMiddleware(logs.AddCompName(context.Background(), "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")

View File

@@ -6,10 +6,10 @@ import (
"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"
)
@@ -23,24 +23,24 @@ type UserService interface {
// userService 实现了 UserService 接口
type userService struct {
ctx context.Context
userRepo repository.UserRepository
tokenService token.Service
notifyService domain_notify.Service
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,
) UserService {
return &userService{
ctx: ctx,
userRepo: userRepo,
tokenService: tokenService,
notifyService: notifyService,
ctx: ctx,
userRepo: userRepo,
tokenGenerator: tokenGenerator,
notifyService: notifyService,
}
}

View File

@@ -57,8 +57,8 @@ func NewApplication(configPath string) (*Application, error) {
appServices.deviceService,
appServices.planService,
appServices.userService,
infra.tokenService,
appServices.auditService,
infra.tokenGenerator,
infra.lora.listenHandler,
)
@@ -91,7 +91,7 @@ func (app *Application) Start() error {
}
// 3. 启动后台工作协程
app.Domain.planService.Start()
app.Domain.planService.Start(startCtx)
// 4. 启动 API 服务器
app.API.Start()
@@ -107,14 +107,14 @@ func (app *Application) Start() error {
// Stop 优雅地关闭应用的所有组件。
func (app *Application) Stop() error {
logger := logs.TraceLogger(app.Ctx, app.Ctx, "Stop")
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 {

View File

@@ -12,7 +12,6 @@ import (
"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,16 +20,17 @@ 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 初始化所有基础设施层组件。
@@ -52,14 +52,14 @@ func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructur
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
}
@@ -139,8 +139,14 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
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)
pigBatchDomain := pig.NewPigBatchService(
logs.AddCompName(baseCtx, "PigBatchDomain"),
infra.repos.pigBatchRepo,
infra.repos.pigBatchLogRepo,
infra.repos.unitOfWork,
pigPenTransferManager,
pigTradeManager, pigSickManager,
)
// 通用设备服务
generalDeviceService := device.NewGeneralDeviceService(
@@ -238,7 +244,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
)
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.tokenService, infra.notifyService)
userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, infra.notifyService)
return &AppServices{
pigFarmService: pigFarmService,

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

@@ -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

@@ -13,24 +13,24 @@ type Claims struct {
jwt.RegisteredClaims
}
// Service 定义了 token 操作的接口
type Service interface {
// Generator 定义了 token 操作的接口
type Generator interface {
GenerateToken(userID uint) (string, error)
ParseToken(tokenString string) (*Claims, error)
}
// tokenService 是 Service 接口的实现
type tokenService struct {
// tokenGenerator 是 Generator 接口的实现
type tokenGenerator struct {
secret []byte
}
// NewTokenService 创建并返回一个新的 Service 实例
func NewTokenService(secret []byte) Service {
return &tokenService{secret: secret}
// NewTokenGenerator 创建并返回一个新的 Generator 实例
func NewTokenGenerator(secret []byte) Generator {
return &tokenGenerator{secret: secret}
}
// GenerateToken 生成一个新的 JWT token
func (s *tokenService) GenerateToken(userID uint) (string, error) {
func (s *tokenGenerator) GenerateToken(userID uint) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(24 * time.Hour) // Token 有效期为 24 小时
@@ -48,7 +48,7 @@ func (s *tokenService) GenerateToken(userID uint) (string, error) {
}
// ParseToken 解析并验证 JWT token
func (s *tokenService) ParseToken(tokenString string) (*Claims, error) {
func (s *tokenGenerator) ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return s.secret, nil
})