feat: complete remaining Go backend modules

This commit is contained in:
zn-admin committed 2026-08-14 00:02:43 +08:00
1 parent cea2751dc5
commit aef5a97165
145 files changed
+18376 -199

No files matched your search

+404
View File
@@ -0,0 +1,404 @@
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{}