package usage import ( "context" "errors" "fmt" "sort" "time" ) type Preset string const ( PresetToday Preset = "today" Preset7Days Preset = "7d" Preset30Days Preset = "30d" PresetMonth Preset = "month" ) var ErrForbidden = errors.New("usage scope forbidden") var ErrInvalidDateRange = errors.New("invalid usage date range") var ErrDateRangeTooLong = errors.New("usage date range exceeds 3660 days") const UnassignedOrganizationID = "__unassigned__" var shanghai = time.FixedZone("Asia/Shanghai", 8*60*60) type DateRange struct { From string `json:"from"` To string `json:"to"` StartDate string `json:"startDate"` EndDate string `json:"endDate"` Label string `json:"label"` DayCount int `json:"dayCount"` } func PresetRange(preset Preset, now time.Time) (DateRange, error) { today := now.In(shanghai) start := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, shanghai) switch preset { case PresetToday: case Preset7Days: start = start.AddDate(0, 0, -6) case Preset30Days: start = start.AddDate(0, 0, -29) case PresetMonth: start = time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, shanghai) default: return DateRange{}, fmt.Errorf("unknown usage preset %q", preset) } end := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, shanghai) labels := map[Preset]string{PresetToday: "今天", Preset7Days: "近 7 天", Preset30Days: "近 30 天", PresetMonth: "本月"} return DateRange{From: start.Format(time.RFC3339), To: end.AddDate(0, 0, 1).Format(time.RFC3339), StartDate: start.Format("2006-01-02"), EndDate: end.Format("2006-01-02"), Label: labels[preset], DayCount: int(end.Sub(start).Hours()/24) + 1}, nil } type Event struct { ID, OwnerID, JobID, Source, Capability, Provider, ReqKey, AccountUsername, AccountDisplayName, TenantID, OrganizationID, OrganizationName, EstimatedUnit, Currency, CreatedAt string Quantity int ChargedAmountFen *int64 } type Filters struct{ OwnerID, OrganizationID, Capability, Provider, From, To string } type Requester struct{ AccountID, OrganizationID, Role string } type Repository interface { Insert(Event) (Event, bool, error) List(Filters) ([]Event, error) } type OrganizationOptionSource interface { ListOrganizationOptions(context.Context, Requester) ([]Option, error) } type OrganizationOptionSourceFunc func(context.Context, Requester) ([]Option, error) func (fn OrganizationOptionSourceFunc) ListOrganizationOptions(ctx context.Context, requester Requester) ([]Option, error) { return fn(ctx, requester) } type Service struct { Repository Repository OrganizationOptions OrganizationOptionSource } type PersonalRequest struct { AccountID string Preset Preset Now time.Time } type AdminRequest struct { Requester StartDate, EndDate, OwnerID, Capability, Provider string Now time.Time RedactAccounts bool } type CountItem struct { Key string `json:"key"` Label string `json:"label"` Count int `json:"count"` } type Record struct { ID string `json:"id"` JobID string `json:"jobId"` OwnerID string `json:"ownerId"` AccountName string `json:"accountName"` AccountUsername string `json:"accountUsername,omitempty"` OrganizationID string `json:"organizationId"` OrganizationName string `json:"organizationName"` Capability string `json:"capability"` CapabilityLabel string `json:"capabilityLabel"` Provider string `json:"provider,omitempty"` ProviderLabel string `json:"providerLabel"` ReqKey string `json:"reqKey,omitempty"` CreatedAt string `json:"createdAt"` } type TrendPoint struct { Date string `json:"date"` Label string `json:"label"` Count int `json:"count"` } type Summary struct { Total int `json:"total"` ActiveAccounts int `json:"activeAccounts"` ActiveOrganizations int `json:"activeOrganizations"` AveragePerDay float64 `json:"averagePerDay"` } type organizationRow struct { OrganizationID string `json:"organizationId"` OrganizationName string `json:"organizationName"` Count int `json:"count"` AccountCount int `json:"accountCount"` LastUsedAt string `json:"lastUsedAt,omitempty"` accounts map[string]bool } type accountRow struct { OwnerID string `json:"ownerId"` AccountName string `json:"accountName"` AccountUsername string `json:"accountUsername,omitempty"` OrganizationID string `json:"organizationId"` OrganizationName string `json:"organizationName"` Count int `json:"count"` LastUsedAt string `json:"lastUsedAt,omitempty"` } type Options struct { Organizations []Option `json:"organizations"` Accounts []Option `json:"accounts"` Capabilities []Option `json:"capabilities"` Providers []Option `json:"providers"` } type Option struct { Value string `json:"value"` Label string `json:"label"` } type PersonalReport struct { Preset Preset `json:"preset"` Range DateRange `json:"range"` Total int `json:"total"` ByCapability []CountItem `json:"byCapability"` Recent []Record `json:"recent"` } type AdminReport struct { Range DateRange `json:"range"` Summary Summary `json:"summary"` Trend []TrendPoint `json:"trend"` ByCapability []CountItem `json:"byCapability"` ByProvider []CountItem `json:"byProvider"` Organizations any `json:"organizations"` Accounts any `json:"accounts"` Recent any `json:"recent"` Options Options `json:"options"` } type Reporter interface { Personal(context.Context, PersonalRequest) (PersonalReport, error) Admin(context.Context, AdminRequest) (AdminReport, error) } func (s Service) Record(event Event) (*Event, error) { if event.Source == "api" || event.Provider == "mock" { return nil, nil } saved, _, err := s.Repository.Insert(event) if err != nil { return nil, err } return &saved, nil } func (s Service) Report(who Requester, filters Filters) ([]Event, error) { switch who.Role { case "super_admin": case "organization_admin": if filters.OrganizationID != "" && filters.OrganizationID != who.OrganizationID { return nil, ErrForbidden } filters.OrganizationID = who.OrganizationID case "user": filters.OwnerID = who.AccountID default: filters.OwnerID = who.AccountID } events, err := s.Repository.List(filters) if err != nil { return nil, err } seen := map[string]bool{} out := make([]Event, 0, len(events)) for _, e := range events { if e.Source == "api" || e.Provider == "mock" || seen[e.JobID] || filters.OwnerID != "" && e.OwnerID != filters.OwnerID || filters.OrganizationID != "" && e.OrganizationID != filters.OrganizationID || filters.Capability != "" && e.Capability != filters.Capability || filters.Provider != "" && e.Provider != filters.Provider { continue } seen[e.JobID] = true if who.Role == "organization_admin" { e.AccountUsername = "" } out = append(out, e) } sort.SliceStable(out, func(i, j int) bool { return out[i].CreatedAt > out[j].CreatedAt }) return out, nil }