package usage import ( "context" "crypto/sha256" "fmt" "math" "sort" "time" "golang.org/x/text/collate" "golang.org/x/text/language" ) type ContextRepository interface { ListContext(context.Context, Filters) ([]Event, error) } func (s Service) Personal(ctx context.Context, request PersonalRequest) (PersonalReport, error) { rangeValue, err := PresetRange(request.Preset, request.Now) if err != nil { return PersonalReport{}, err } events, err := s.list(ctx, Filters{OwnerID: request.AccountID, From: rangeValue.From, To: rangeValue.To}) if err != nil { return PersonalReport{}, err } events = eligible(events) recent := records(events) if len(recent) > 5 { recent = recent[:5] } return PersonalReport{Preset: request.Preset, Range: rangeValue, Total: len(events), ByCapability: capabilityCounts(events), Recent: recent}, nil } func (s Service) Admin(ctx context.Context, request AdminRequest) (AdminReport, error) { rangeValue, err := adminRange(request) if err != nil { return AdminReport{}, err } baseEvents, err := s.list(ctx, Filters{From: rangeValue.From, To: rangeValue.To}) if err != nil { return AdminReport{}, err } baseEvents = eligible(baseEvents) injectedOrganizations, err := s.organizationOptions(ctx, request.Requester) if err != nil { return AdminReport{}, err } baseViews := recordsWithOrganizations(baseEvents, injectedOrganizations) views := filterRecords(baseViews, request) events := eventsForRecords(baseEvents, views) recent := views if len(recent) > 100 { recent = recent[:100] } optionViews := baseViews if request.OrganizationID != "" { optionViews = filterRecords(baseViews, AdminRequest{Requester: Requester{OrganizationID: request.OrganizationID}}) } accounts := map[string]bool{} organizations := map[string]bool{} for _, e := range events { accounts[e.OwnerID] = true if e.OrganizationID != "" { organizations[e.OrganizationID] = true } } return AdminReport{Range: rangeValue, Summary: Summary{Total: len(events), ActiveAccounts: len(accounts), ActiveOrganizations: len(organizations), AveragePerDay: math.Round(float64(len(events))/float64(rangeValue.DayCount)*10) / 10}, Trend: trend(events, rangeValue), ByCapability: capabilityCounts(events), ByProvider: providerCounts(events), Organizations: organizationRows(views), Accounts: accountRows(views), Recent: recent, Options: Options{Organizations: mergeOrganizationOptions(injectedOrganizations, optionViews), Accounts: accountOptions(optionViews), Capabilities: []Option{{"image.generate", "图片生成"}, {"video.generate", "视频生成"}}, Providers: providerOptions(baseEvents)}}, nil } func (s Service) organizationOptions(ctx context.Context, requester Requester) ([]Option, error) { if s.OrganizationOptions == nil { return []Option{}, nil } options, err := s.OrganizationOptions.ListOrganizationOptions(ctx, requester) if options == nil { options = []Option{} } return options, err } func recordsWithOrganizations(events []Event, organizations []Option) []Record { names := make(map[string]string, len(organizations)) for _, option := range organizations { names[option.Value] = option.Label } views := records(events) for i := range views { if events[i].OrganizationName == "" { if name := names[views[i].OrganizationID]; name != "" { views[i].OrganizationName = name } } } return views } func filterRecords(records []Record, request AdminRequest) []Record { out := make([]Record, 0, len(records)) for _, record := range records { if request.OrganizationID != "" && record.OrganizationID != request.OrganizationID || request.OwnerID != "" && record.OwnerID != request.OwnerID || request.Capability != "" && record.Capability != request.Capability || request.Provider != "" && record.Provider != request.Provider { continue } out = append(out, record) } return out } func eventsForRecords(events []Event, records []Record) []Event { byID := make(map[string]Event, len(events)) for _, event := range events { byID[event.ID] = event } out := make([]Event, 0, len(records)) for _, record := range records { if event, ok := byID[record.ID]; ok { out = append(out, event) } } return out } func (s Service) list(ctx context.Context, filters Filters) ([]Event, error) { if contextual, ok := s.Repository.(ContextRepository); ok { return contextual.ListContext(ctx, filters) } return s.Repository.List(filters) } func eligible(events []Event) []Event { seen := map[string]bool{} out := make([]Event, 0, len(events)) for _, e := range events { if e.Source == "api" || e.Provider == "mock" || seen[e.JobID] { continue } seen[e.JobID] = true out = append(out, e) } sort.SliceStable(out, func(i, j int) bool { return out[i].CreatedAt > out[j].CreatedAt }) return out } func records(events []Event) []Record { out := make([]Record, 0, len(events)) for _, e := range events { name := e.AccountDisplayName if name == "" { name = e.AccountUsername } if name == "" { sum := sha256.Sum256([]byte(e.OwnerID)) name = fmt.Sprintf("历史账号 %X", sum[:4]) } orgName := e.OrganizationName organizationID := e.OrganizationID if organizationID == "" { organizationID = UnassignedOrganizationID } if orgName == "" { orgName = "未归属组织" } out = append(out, Record{ID: e.ID, JobID: e.JobID, OwnerID: e.OwnerID, AccountName: name, AccountUsername: e.AccountUsername, OrganizationID: organizationID, OrganizationName: orgName, Capability: e.Capability, CapabilityLabel: capabilityLabel(e.Capability), Provider: e.Provider, ProviderLabel: providerLabel(e.Provider), ReqKey: e.ReqKey, CreatedAt: e.CreatedAt}) } return out } func capabilityCounts(events []Event) []CountItem { counts := map[string]int{} for _, e := range events { counts[e.Capability]++ } return []CountItem{{"image.generate", "图片生成", counts["image.generate"]}, {"video.generate", "视频生成", counts["video.generate"]}} } func providerCounts(events []Event) []CountItem { counts := map[string]int{} for _, e := range events { key := e.Provider if key == "" { key = "unknown" } counts[key]++ } out := make([]CountItem, 0, len(counts)) for key, count := range counts { out = append(out, CountItem{key, providerLabel(key), count}) } sort.Slice(out, func(i, j int) bool { if out[i].Count != out[j].Count { return out[i].Count > out[j].Count } return out[i].Key < out[j].Key }) return out } func providerOptions(events []Event) []Option { seen := map[string]bool{} out := []Option{} for _, e := range events { if e.Provider != "" && !seen[e.Provider] { seen[e.Provider] = true out = append(out, Option{e.Provider, providerLabel(e.Provider)}) } } sort.Slice(out, func(i, j int) bool { return out[i].Label < out[j].Label }) return out } func mergeOrganizationOptions(injected []Option, records []Record) []Option { labels := make(map[string]string, len(injected)+len(records)) for _, option := range injected { if option.Value != "" { labels[option.Value] = option.Label } } for _, record := range records { id := record.OrganizationID if id == "" { id = UnassignedOrganizationID } labels[id] = record.OrganizationName } out := make([]Option, 0, len(labels)) for value, label := range labels { if label == "" { label = value } out = append(out, Option{Value: value, Label: label}) } sort.Slice(out, func(i, j int) bool { if out[i].Value == UnassignedOrganizationID { return false } if out[j].Value == UnassignedOrganizationID { return true } if out[i].Label != out[j].Label { return out[i].Label < out[j].Label } return out[i].Value < out[j].Value }) return out } func accountOptions(records []Record) []Option { labels := make(map[string]string, len(records)) for _, record := range records { if _, exists := labels[record.OwnerID]; exists { continue } label := record.AccountName if record.AccountUsername != "" { label += "(" + record.AccountUsername + ")" } labels[record.OwnerID] = label } out := make([]Option, 0, len(labels)) for value, label := range labels { out = append(out, Option{Value: value, Label: label}) } sort.Slice(out, func(i, j int) bool { if out[i].Label != out[j].Label { return out[i].Label < out[j].Label } return out[i].Value < out[j].Value }) return out } func trend(events []Event, r DateRange) []TrendPoint { counts := map[string]int{} monthly := r.DayCount > 62 for _, e := range events { if parsed, err := time.Parse(time.RFC3339, e.CreatedAt); err == nil { layout := "2006-01-02" if monthly { layout = "2006-01" } counts[parsed.In(shanghai).Format(layout)]++ } } out := []TrendPoint{} if monthly { for key, count := range counts { out = append(out, TrendPoint{key, key, count}) } sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date }) return out } start, _ := time.ParseInLocation("2006-01-02", r.StartDate, shanghai) end, _ := time.ParseInLocation("2006-01-02", r.EndDate, shanghai) for day := start; !day.After(end); day = day.AddDate(0, 0, 1) { key := day.Format("2006-01-02") out = append(out, TrendPoint{key, key[5:], counts[key]}) } return out } func organizationRows(records []Record) any { by := map[string]*organizationRow{} for _, v := range records { id := v.OrganizationID if id == "" { id = UnassignedOrganizationID } item := by[id] if item == nil { item = &organizationRow{OrganizationID: id, OrganizationName: v.OrganizationName, accounts: map[string]bool{}} by[id] = item } item.Count++ item.accounts[v.OwnerID] = true item.AccountCount = len(item.accounts) if v.CreatedAt > item.LastUsedAt { item.LastUsedAt = v.CreatedAt } } out := make([]organizationRow, 0, len(by)) for _, v := range by { out = append(out, *v) } zhCN := collate.New(language.Chinese) sort.SliceStable(out, func(i, j int) bool { if out[i].Count != out[j].Count { return out[i].Count > out[j].Count } if out[i].OrganizationID == UnassignedOrganizationID { return false } if out[j].OrganizationID == UnassignedOrganizationID { return true } return zhCN.CompareString(out[i].OrganizationName, out[j].OrganizationName) < 0 }) return out } func accountRows(records []Record) any { by := map[string]*accountRow{} order := make([]string, 0, len(records)) for _, v := range records { item := by[v.OwnerID] if item == nil { item = &accountRow{OwnerID: v.OwnerID, AccountName: v.AccountName, AccountUsername: v.AccountUsername, OrganizationID: v.OrganizationID, OrganizationName: v.OrganizationName} by[v.OwnerID] = item order = append(order, v.OwnerID) } item.Count++ if v.CreatedAt > item.LastUsedAt { item.LastUsedAt = v.CreatedAt } } out := make([]accountRow, 0, len(by)) for _, id := range order { out = append(out, *by[id]) } sort.SliceStable(out, func(i, j int) bool { return out[i].Count > out[j].Count }) return out } func adminRange(request AdminRequest) (DateRange, error) { if request.StartDate == "" && request.EndDate == "" { return PresetRange(PresetMonth, request.Now) } fallback, _ := PresetRange(PresetMonth, request.Now) start, end := request.StartDate, request.EndDate if start == "" { start = fallback.StartDate } if end == "" { end = fallback.EndDate } from, err := time.ParseInLocation("2006-01-02", start, shanghai) if err != nil { return DateRange{}, err } to, err := time.ParseInLocation("2006-01-02", end, shanghai) if err != nil || to.Before(from) { return DateRange{}, ErrInvalidDateRange } dayCount := int(to.Sub(from).Hours()/24) + 1 if dayCount > 3660 { return DateRange{}, ErrDateRangeTooLong } label := start if start != end { label = start + " 至 " + end } return DateRange{From: from.Format(time.RFC3339), To: to.AddDate(0, 0, 1).Format(time.RFC3339), StartDate: start, EndDate: end, Label: label, DayCount: dayCount}, nil } func capabilityLabel(value string) string { if value == "video.generate" { return "视频生成" } return "图片生成" } func providerLabel(value string) string { switch value { case "volcengine-visual": return "即梦图片" case "evolink": return "EvoLink" case "seedance": return "Seedance" case "bailian": return "阿里云百炼" case "mock": return "系统" default: return "未知服务商" } } var _ Reporter = Service{}