Files
ttstd-infinite-canvas/backend/internal/app/model_availability.go

166 lines
5.1 KiB
Go

package app
import (
"time"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
)
const publicModelAvailabilityMinimumSamples = 10
type publicModelAvailabilityKey struct {
channelID string
modelKey string
}
type publicModelAvailabilityAccumulator struct {
total int
success int
latest time.Time
}
// publicModelAvailability builds a deliberately small public read model. The
// current binary Available field remains owned by the catalog/route-health
// logic; these values describe only recorded user-facing create outcomes.
func (s *Service) publicModelAvailability(channels []PublicChannelCatalog, now time.Time) (map[publicModelAvailabilityKey]*PublicModelAvailability, error) {
if now.IsZero() {
now = time.Now().UTC()
}
now = now.UTC()
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
trendFrom := dayStart.AddDate(0, 0, -6)
channelIDs := make([]string, 0, len(channels))
known := make(map[publicModelAvailabilityKey]struct{})
for _, channel := range channels {
if channel.ID == "" {
continue
}
channelIDs = append(channelIDs, channel.ID)
for _, item := range channel.Models {
known[publicModelAvailabilityKey{channelID: channel.ID, modelKey: item.ModelKey}] = struct{}{}
}
}
result := make(map[publicModelAvailabilityKey]*PublicModelAvailability, len(known))
if len(channelIDs) == 0 || len(known) == 0 {
return result, nil
}
from := trendFrom
logs, err := s.repo.PublicModelAvailabilityLogs(repository.PublicModelAvailabilityFilter{From: from, To: now, ChannelIDs: channelIDs})
if err != nil {
return nil, err
}
last24 := make(map[publicModelAvailabilityKey]*publicModelAvailabilityAccumulator)
trend := make([]map[publicModelAvailabilityKey]*publicModelAvailabilityAccumulator, 7)
for index := range trend {
trend[index] = make(map[publicModelAvailabilityKey]*publicModelAvailabilityAccumulator)
}
for _, log := range logs {
key := publicModelAvailabilityKey{channelID: log.ChannelID, modelKey: log.Model}
if _, ok := known[key]; !ok {
continue
}
if !log.CreatedAt.Before(now.Add(-24 * time.Hour)) {
addAvailabilityLog(last24, key, log)
}
segment := int(log.CreatedAt.Sub(from) / (24 * time.Hour))
if segment >= 0 && segment < len(trend) {
addAvailabilityLog(trend[segment], key, log)
}
}
for key := range known {
primary := last24[key]
item := &PublicModelAvailability{
Trend7d: make([]PublicModelAvailabilityDay, 0, 7),
DataState: publicAvailabilityState(sampleCount(primary), publicModelAvailabilityMinimumSamples),
ComputedAt: now,
}
if primary != nil {
item.SampleCount = primary.total
if primary.total >= publicModelAvailabilityMinimumSamples {
item.Rate24h = publicAvailabilityRate(primary)
}
if !primary.latest.IsZero() {
latest := primary.latest.UTC()
item.DataThrough = &latest
}
}
for index := range trend {
bucket := trend[index][key]
bucketStart := from.Add(time.Duration(index) * 24 * time.Hour)
day := PublicModelAvailabilityDay{
Day: bucketStart.UTC().Format("2006-01-02"),
SampleCount: sampleCount(bucket),
DataState: publicAvailabilityState(sampleCount(bucket), publicModelAvailabilityMinimumSamples),
}
if bucket != nil {
if bucket.total >= publicModelAvailabilityMinimumSamples {
day.Rate = publicAvailabilityRate(bucket)
}
if item.DataThrough == nil || bucket.latest.After(*item.DataThrough) {
latest := bucket.latest.UTC()
item.DataThrough = &latest
}
}
item.Trend7d = append(item.Trend7d, day)
}
result[key] = item
}
return result, nil
}
func addAvailabilityLog(target map[publicModelAvailabilityKey]*publicModelAvailabilityAccumulator, key publicModelAvailabilityKey, log model.ApiCallLog) {
item := target[key]
if item == nil {
item = &publicModelAvailabilityAccumulator{}
target[key] = item
}
item.total++
if log.Status == model.ApiCallStatusSucceeded {
item.success++
}
if log.CreatedAt.After(item.latest) {
item.latest = log.CreatedAt
}
}
func sampleCount(item *publicModelAvailabilityAccumulator) int {
if item == nil {
return 0
}
return item.total
}
func publicAvailabilityRate(item *publicModelAvailabilityAccumulator) *float64 {
if item == nil || item.total == 0 {
return nil
}
rate := float64(item.success) / float64(item.total)
return &rate
}
func publicAvailabilityState(samples int, minimum int) PublicModelAvailabilityDataState {
switch {
case samples == 0:
return PublicModelAvailabilityNoData
case samples < minimum:
return PublicModelAvailabilityInsufficient
default:
return PublicModelAvailabilityReady
}
}
func attachPublicModelAvailability(channels []PublicChannelCatalog, availability map[publicModelAvailabilityKey]*PublicModelAvailability) {
for channelIndex := range channels {
for modelIndex := range channels[channelIndex].Models {
item := channels[channelIndex].Models[modelIndex]
if value := availability[publicModelAvailabilityKey{channelID: channels[channelIndex].ID, modelKey: item.ModelKey}]; value != nil {
channels[channelIndex].Models[modelIndex].Availability = value
}
}
}
}