Files
2026-09-11 15:33:18 +08:00

409 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 "seedream":
return "Seedream 5.0 Pro"
case "bailian":
return "阿里云百炼"
case "minimax":
return "MiniMax H3"
case "mock":
return "系统"
default:
return "未知服务商"
}
}
var _ Reporter = Service{}