| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- package service
- import (
- "context"
- "fmt"
- "log/slog"
- )
- // 渠道监控聚合层:把 latest + availability 拼成 admin/user 视图所需的 summary / detail。
- // 所有方法都遵守"失败仅日志,返回零值"的原则,避免 N+1 查询失败拖垮列表渲染。
- // BatchMonitorStatusSummary 批量聚合多个监控的 latest + 7d 可用率(admin/user list 用,消除 N+1)。
- // 失败时返回空 map,错误仅日志,不影响列表渲染。
- //
- // 参数:
- // - ids: 要聚合的 monitor ID 列表
- // - primaryByID: monitor ID -> primary model(用于读 7d 可用率与 latest 状态)
- // - extrasByID: monitor ID -> extra models 列表(用于读 latest 状态填充 ExtraModels)
- func (s *ChannelMonitorService) BatchMonitorStatusSummary(
- ctx context.Context,
- ids []int64,
- primaryByID map[int64]string,
- extrasByID map[int64][]string,
- ) map[int64]MonitorStatusSummary {
- out := make(map[int64]MonitorStatusSummary, len(ids))
- if len(ids) == 0 {
- return out
- }
- latestMap, err := s.repo.ListLatestForMonitorIDs(ctx, ids)
- if err != nil {
- slog.Warn("channel_monitor: batch load latest failed", "error", err)
- latestMap = map[int64][]*ChannelMonitorLatest{}
- }
- availMap, err := s.repo.ComputeAvailabilityForMonitors(ctx, ids, monitorAvailability7Days)
- if err != nil {
- slog.Warn("channel_monitor: batch compute availability failed", "error", err)
- availMap = map[int64][]*ChannelMonitorAvailability{}
- }
- for _, id := range ids {
- out[id] = buildStatusSummary(
- indexLatestByModel(latestMap[id]),
- indexAvailabilityByModel(availMap[id]),
- primaryByID[id],
- extrasByID[id],
- )
- }
- return out
- }
- // ListUserView 用户只读视图:列出所有 enabled 监控的概览。
- // 使用批量聚合接口避免 N+1:
- //
- // 1 次查 monitors;
- // 1 次批量 latest(含 ping_latency_ms);
- // 1 次批量 7d availability;
- // 1 次批量 timeline(主模型最近 N 条)。
- func (s *ChannelMonitorService) ListUserView(ctx context.Context) ([]*UserMonitorView, error) {
- monitors, err := s.repo.ListEnabled(ctx)
- if err != nil {
- return nil, fmt.Errorf("list enabled monitors: %w", err)
- }
- if len(monitors) == 0 {
- return []*UserMonitorView{}, nil
- }
- ids, primaryByID, extrasByID := collectMonitorIndexes(monitors)
- summaries := s.BatchMonitorStatusSummary(ctx, ids, primaryByID, extrasByID)
- latestMap := s.batchLatest(ctx, ids)
- timelineMap := s.batchTimeline(ctx, ids, primaryByID)
- views := make([]*UserMonitorView, 0, len(monitors))
- for _, m := range monitors {
- primaryLatest := pickLatest(latestMap[m.ID], m.PrimaryModel)
- views = append(views, buildUserViewFromSummary(m, summaries[m.ID], primaryLatest, timelineMap[m.ID]))
- }
- return views, nil
- }
- // collectMonitorIndexes 把 monitors 列表按 ID 展开为聚合查询所需的三个索引结构。
- func collectMonitorIndexes(monitors []*ChannelMonitor) ([]int64, map[int64]string, map[int64][]string) {
- ids := make([]int64, 0, len(monitors))
- primaryByID := make(map[int64]string, len(monitors))
- extrasByID := make(map[int64][]string, len(monitors))
- for _, m := range monitors {
- ids = append(ids, m.ID)
- primaryByID[m.ID] = m.PrimaryModel
- extrasByID[m.ID] = m.ExtraModels
- }
- return ids, primaryByID, extrasByID
- }
- // batchLatest 批量取 latest per model,失败仅日志(与现有 BatchMonitorStatusSummary 一致,不阻断列表渲染)。
- func (s *ChannelMonitorService) batchLatest(ctx context.Context, ids []int64) map[int64][]*ChannelMonitorLatest {
- latestMap, err := s.repo.ListLatestForMonitorIDs(ctx, ids)
- if err != nil {
- slog.Warn("channel_monitor: user view batch latest failed", "error", err)
- return map[int64][]*ChannelMonitorLatest{}
- }
- return latestMap
- }
- // batchTimeline 批量取每个 monitor 主模型最近 monitorTimelineMaxPoints 条历史。
- func (s *ChannelMonitorService) batchTimeline(
- ctx context.Context,
- ids []int64,
- primaryByID map[int64]string,
- ) map[int64][]*ChannelMonitorHistoryEntry {
- timelineMap, err := s.repo.ListRecentHistoryForMonitors(ctx, ids, primaryByID, monitorTimelineMaxPoints)
- if err != nil {
- slog.Warn("channel_monitor: user view batch timeline failed", "error", err)
- return map[int64][]*ChannelMonitorHistoryEntry{}
- }
- return timelineMap
- }
- // pickLatest 从 latest 切片中挑出指定 model 对应项,未命中返回 nil。
- func pickLatest(rows []*ChannelMonitorLatest, model string) *ChannelMonitorLatest {
- if model == "" {
- return nil
- }
- for _, r := range rows {
- if r.Model == model {
- return r
- }
- }
- return nil
- }
- // GetUserDetail 用户只读视图:单个监控详情(每个模型 7d/15d/30d 可用率与平均延迟)。
- // 不暴露 api_key。
- func (s *ChannelMonitorService) GetUserDetail(ctx context.Context, id int64) (*UserMonitorDetail, error) {
- m, err := s.repo.GetByID(ctx, id)
- if err != nil {
- return nil, err
- }
- if !m.Enabled {
- return nil, ErrChannelMonitorNotFound
- }
- latest, err := s.repo.ListLatestPerModel(ctx, id)
- if err != nil {
- return nil, fmt.Errorf("list latest per model: %w", err)
- }
- availMap, err := s.collectAvailabilityWindows(ctx, id)
- if err != nil {
- return nil, err
- }
- models := mergeModelDetails(m, latest, availMap)
- return &UserMonitorDetail{
- ID: m.ID,
- Name: m.Name,
- Provider: m.Provider,
- GroupName: m.GroupName,
- Models: models,
- }, nil
- }
- // collectAvailabilityWindows 一次性查询 7/15/30 天三个窗口,按模型组织。
- func (s *ChannelMonitorService) collectAvailabilityWindows(ctx context.Context, monitorID int64) (map[int]map[string]*ChannelMonitorAvailability, error) {
- out := make(map[int]map[string]*ChannelMonitorAvailability, 3)
- windows := []int{monitorAvailability7Days, monitorAvailability15Days, monitorAvailability30Days}
- for _, w := range windows {
- rows, err := s.repo.ComputeAvailability(ctx, monitorID, w)
- if err != nil {
- return nil, fmt.Errorf("compute availability %dd: %w", w, err)
- }
- out[w] = indexAvailabilityByModel(rows)
- }
- return out, nil
- }
- // ---------- 纯函数 helper(无 IO,可在 batch / 单 monitor / detail 路径复用)----------
- // indexLatestByModel 把 latest 切片按 model 索引(小工具,避免在 hot path 重复写)。
- func indexLatestByModel(rows []*ChannelMonitorLatest) map[string]*ChannelMonitorLatest {
- m := make(map[string]*ChannelMonitorLatest, len(rows))
- for _, r := range rows {
- m[r.Model] = r
- }
- return m
- }
- // indexAvailabilityByModel 把 availability 切片按 model 索引。
- func indexAvailabilityByModel(rows []*ChannelMonitorAvailability) map[string]*ChannelMonitorAvailability {
- m := make(map[string]*ChannelMonitorAvailability, len(rows))
- for _, r := range rows {
- m[r.Model] = r
- }
- return m
- }
- // buildStatusSummary 由 latest + availability 字典构造 MonitorStatusSummary。
- // 不做任何 IO,纯组装,便于在 batch 与单 monitor 路径复用。
- func buildStatusSummary(
- latestByModel map[string]*ChannelMonitorLatest,
- availByModel map[string]*ChannelMonitorAvailability,
- primary string,
- extras []string,
- ) MonitorStatusSummary {
- summary := MonitorStatusSummary{ExtraModels: make([]ExtraModelStatus, 0, len(extras))}
- if primary != "" {
- if l, ok := latestByModel[primary]; ok {
- summary.PrimaryStatus = l.Status
- summary.PrimaryLatencyMs = l.LatencyMs
- // 配额快照只挂主模型行(quota 模式唯一行 / quota_probe 的主行)。
- summary.LatestQuota = l.Quota
- }
- if a, ok := availByModel[primary]; ok {
- summary.Availability7d = a.AvailabilityPct
- }
- }
- for _, model := range extras {
- entry := ExtraModelStatus{Model: model}
- if l, ok := latestByModel[model]; ok {
- entry.Status = l.Status
- entry.LatencyMs = l.LatencyMs
- }
- summary.ExtraModels = append(summary.ExtraModels, entry)
- }
- return summary
- }
- // buildUserViewFromSummary 用预聚合好的 MonitorStatusSummary + 主模型 latest + timeline 装填 UserMonitorView(无 IO)。
- // primaryLatest 可能为 nil(该监控尚无历史);timelineEntries 可能为空。
- func buildUserViewFromSummary(
- m *ChannelMonitor,
- summary MonitorStatusSummary,
- primaryLatest *ChannelMonitorLatest,
- timelineEntries []*ChannelMonitorHistoryEntry,
- ) *UserMonitorView {
- view := &UserMonitorView{
- ID: m.ID,
- Name: m.Name,
- Provider: m.Provider,
- GroupName: m.GroupName,
- PrimaryModel: m.PrimaryModel,
- PrimaryStatus: summary.PrimaryStatus,
- PrimaryLatencyMs: summary.PrimaryLatencyMs,
- Availability7d: summary.Availability7d,
- ExtraModels: summary.ExtraModels,
- Timeline: buildTimelinePoints(timelineEntries),
- }
- if primaryLatest != nil {
- view.PrimaryPingLatencyMs = primaryLatest.PingLatencyMs
- view.LatestQuota = primaryLatest.Quota
- }
- return view
- }
- // buildTimelinePoints 把 history entry 裁剪为 timeline 点(去除 message/ID/Model,减小响应体)。
- func buildTimelinePoints(entries []*ChannelMonitorHistoryEntry) []UserMonitorTimelinePoint {
- out := make([]UserMonitorTimelinePoint, 0, len(entries))
- for _, e := range entries {
- out = append(out, UserMonitorTimelinePoint{
- Status: e.Status,
- LatencyMs: e.LatencyMs,
- PingLatencyMs: e.PingLatencyMs,
- CheckedAt: e.CheckedAt,
- })
- }
- return out
- }
- // mergeModelDetails 合并 latest + availability 三个窗口为 ModelDetail 列表。
- // 复用 indexLatestByModel,避免在多处重复写 build map 逻辑。
- func mergeModelDetails(
- m *ChannelMonitor,
- latest []*ChannelMonitorLatest,
- availMap map[int]map[string]*ChannelMonitorAvailability,
- ) []ModelDetail {
- all := append([]string{m.PrimaryModel}, m.ExtraModels...)
- latestByModel := indexLatestByModel(latest)
- out := make([]ModelDetail, 0, len(all))
- for _, model := range all {
- d := ModelDetail{Model: model}
- if l, ok := latestByModel[model]; ok {
- d.LatestStatus = l.Status
- d.LatestLatencyMs = l.LatencyMs
- }
- if a, ok := availMap[monitorAvailability7Days][model]; ok {
- d.Availability7d = a.AvailabilityPct
- d.AvgLatency7dMs = a.AvgLatencyMs
- }
- if a, ok := availMap[monitorAvailability15Days][model]; ok {
- d.Availability15d = a.AvailabilityPct
- }
- if a, ok := availMap[monitorAvailability30Days][model]; ok {
- d.Availability30d = a.AvailabilityPct
- }
- out = append(out, d)
- }
- return out
- }
|