package asset import ( "context" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" pollingsvc "github.com/break/junhong_cmp_fiber/internal/service/polling" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // ResolvePriorityPolling 读取资产或卡的优先轮询状态只读投影,落在既有资产解析端点内。 // // 口径:卡取自身,设备取其在用绑定卡;存在待执行或执行中的优先项时「是否处于优先轮询中」为真, // 最近触发场景取该资产最近一条事实的触发类型,最近轮询结果取该条事实的结局, // 最近轮询时间取最近一次执行时间(未执行过时退回最近触发时间), // 失败原因取最近一条非空的可安全展示原因。无任何优先轮询事实时以否或空值返回, // MUST NOT 用普通轮询结果填充。 // // 只读:只做查询,不创建、不合并、不推进、不出队任何优先项,也不触发上游调用。 // 数据范围:资产本身的可读性由资产解析用例保证;此处再按优先项事实的店铺快照下推一次, // 读侧鉴权不通过(企业账号或范围缺失)时返回空投影,不因投影阻断资产详情,也不泄露范围外事实。 func (q *ExchangeTraceQuery) ResolvePriorityPolling(ctx context.Context, assetType string, assetID uint) (*dto.AssetPriorityPollingProjection, error) { projection := &dto.AssetPriorityPollingProjection{} if q == nil || q.db == nil || assetID == 0 { return projection, nil } cardIDs, err := q.assetCardIDs(ctx, assetType, assetID) if err != nil { return nil, err } if len(cardIDs) == 0 { return projection, nil } scope, scopeErr := pollingsvc.PriorityPollingReadScope(ctx) if scopeErr != nil { return projection, nil } summaries, err := postgres.NewPollingPriorityItemStore(q.db).ProjectPriorityPolling(ctx, cardIDs, scope) if err != nil { return nil, err } return projectPriorityPolling(summaries), nil } // assetCardIDs 返回该资产对应的轮询对象卡集合:卡为自身,设备为绑定状态有效的在用卡。 func (q *ExchangeTraceQuery) assetCardIDs(ctx context.Context, assetType string, assetID uint) ([]uint, error) { switch assetType { case constants.AssetResolveTypeCard: return []uint{assetID}, nil case constants.AssetTypeDevice: var cardIDs []uint if err := q.db.WithContext(ctx). Model(&model.DeviceSimBinding{}). Where("device_id = ? AND bind_status = ?", assetID, constants.BindStatusBound). Order("slot_position ASC"). Pluck("iot_card_id", &cardIDs).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备绑定的卡失败") } return cardIDs, nil default: // 其他资产类型没有轮询对象,投影按无事实返回。 return nil, nil } } // projectPriorityPolling 把逐卡摘要聚合为资产级投影。 // 多卡资产以最近创建的一条事实决定最近触发场景、结果与轮询时间; // 失败原因单独取最近一条非空原因,避免最近一条事实无原因时丢失可展示的失败说明。 func projectPriorityPolling(summaries map[uint]postgres.PriorityPollingCardProjection) *dto.AssetPriorityPollingProjection { projection := &dto.AssetPriorityPollingProjection{} var latest *model.PollingPriorityItem var latestFailed *model.PollingPriorityItem for _, summary := range summaries { if summary.HasActive { projection.InPriorityPolling = true } item := summary.Latest if item == nil { continue } if latest == nil || item.ID > latest.ID { latest = item } if item.FailureReason != "" && (latestFailed == nil || item.ID > latestFailed.ID) { latestFailed = item } } if latest == nil { return projection } projection.LastTriggerType = latest.TriggerType projection.LastTriggerName = constants.PollingPriorityTriggerName(latest.TriggerType) projection.LastResult = constants.PollingPriorityStatusOutcome(latest.Status) projection.LastResultName = constants.PollingPriorityOutcomeName(projection.LastResult) projection.LastPollingAt = priorityPollingLastPolledAt(latest) if latest.FailureReason != "" { projection.FailureReason = latest.FailureReason } else if latestFailed != nil { projection.FailureReason = latestFailed.FailureReason } return projection } // priorityPollingLastPolledAt 返回最近轮询时间:优先取执行时间,未执行过时取最近触发时间。 func priorityPollingLastPolledAt(item *model.PollingPriorityItem) *time.Time { if item == nil { return nil } switch { case item.FinishedAt != nil: return item.FinishedAt case item.StartedAt != nil: return item.StartedAt default: triggeredAt := item.LastTriggeredAt return &triggeredAt } }