244 lines
7.8 KiB
Go
244 lines
7.8 KiB
Go
package app
|
|
|
|
import (
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"yingce/backend/internal/model"
|
|
)
|
|
|
|
type AnalyticsQuery struct {
|
|
From string
|
|
To string
|
|
UserID string
|
|
Model string
|
|
ChannelID string
|
|
Capability string
|
|
}
|
|
|
|
type AnalyticsOverview struct {
|
|
From time.Time `json:"from"`
|
|
To time.Time `json:"to"`
|
|
KPI AnalyticsKPI `json:"kpi"`
|
|
Trend []AnalyticsTrendPoint `json:"trend"`
|
|
Models []AnalyticsModelRow `json:"models"`
|
|
Users []AnalyticsUserRow `json:"users"`
|
|
Failures []AnalyticsFailureRow `json:"failures"`
|
|
}
|
|
|
|
type AnalyticsKPI struct {
|
|
ActiveUsers int `json:"activeUsers"`
|
|
DAU int `json:"dau"`
|
|
WAU int `json:"wau"`
|
|
MAU int `json:"mau"`
|
|
GenerationTasks int `json:"generationTasks"`
|
|
UpstreamRequests int `json:"upstreamRequests"`
|
|
SuccessRate float64 `json:"successRate"`
|
|
P95DurationMs int64 `json:"p95DurationMs"`
|
|
CurrentQueuedTasks int64 `json:"currentQueuedTasks"`
|
|
Finance AnalyticsFinance `json:"finance"`
|
|
}
|
|
|
|
type AnalyticsTrendPoint struct {
|
|
Day string `json:"day"`
|
|
Tasks int `json:"tasks"`
|
|
Requests int `json:"requests"`
|
|
ActiveUsers int `json:"activeUsers"`
|
|
RequestSuccessRate float64 `json:"requestSuccessRate"`
|
|
}
|
|
|
|
type AnalyticsModelRow struct {
|
|
Model string `json:"model"`
|
|
Capability string `json:"capability"`
|
|
Tasks int `json:"tasks"`
|
|
Requests int `json:"requests"`
|
|
UniqueUsers int `json:"uniqueUsers"`
|
|
TaskSuccessRate float64 `json:"taskSuccessRate"`
|
|
RequestSuccessRate float64 `json:"requestSuccessRate"`
|
|
P50DurationMs int64 `json:"p50DurationMs"`
|
|
P95DurationMs int64 `json:"p95DurationMs"`
|
|
InputTokens int64 `json:"inputTokens"`
|
|
OutputTokens int64 `json:"outputTokens"`
|
|
CachedTokens int64 `json:"cachedTokens"`
|
|
UsageAvailable bool `json:"usageAvailable"`
|
|
MediaCount int `json:"mediaCount"`
|
|
VideoSeconds int `json:"videoSeconds"`
|
|
Finance AnalyticsFinance `json:"finance"`
|
|
}
|
|
|
|
type AnalyticsUserRow struct {
|
|
UserID string `json:"userId"`
|
|
Name string `json:"name"`
|
|
ActiveDays int `json:"activeDays"`
|
|
Tasks int `json:"tasks"`
|
|
AgentMessages int `json:"agentMessages"`
|
|
CanvasDays int `json:"canvasDays"`
|
|
Assets int `json:"assets"`
|
|
Resources int `json:"resources"`
|
|
CommonModel string `json:"commonModel"`
|
|
}
|
|
|
|
type AnalyticsFailureRow struct {
|
|
Type string `json:"type"`
|
|
Model string `json:"model"`
|
|
Count int `json:"count"`
|
|
LastError string `json:"lastError"`
|
|
LastSeenAt time.Time `json:"lastSeenAt"`
|
|
}
|
|
|
|
type APICallLogQuery struct {
|
|
AnalyticsQuery
|
|
RecordType string
|
|
Keyword string
|
|
Status string
|
|
IDs []string
|
|
Page int
|
|
Limit int
|
|
}
|
|
|
|
type APICallLogPage struct {
|
|
Logs []model.ApiCallLog `json:"logs"`
|
|
Total int64 `json:"total"`
|
|
Page int `json:"page"`
|
|
Limit int `json:"pageSize"`
|
|
}
|
|
|
|
type ModelPricingRequest struct {
|
|
ChannelID string `json:"channelId"`
|
|
Model string `json:"model"`
|
|
Capability string `json:"capability"`
|
|
Currency string `json:"currency"`
|
|
InputPerMillionMicros int64 `json:"inputPerMillionMicros"`
|
|
OutputPerMillionMicros int64 `json:"outputPerMillionMicros"`
|
|
CachedPerMillionMicros int64 `json:"cachedPerMillionMicros"`
|
|
PerRequestMicros int64 `json:"perRequestMicros"`
|
|
PerMediaMicros int64 `json:"perMediaMicros"`
|
|
PerVideoSecondMicros int64 `json:"perVideoSecondMicros"`
|
|
}
|
|
|
|
func (s *Service) AdminAnalytics(actor *model.User, query AnalyticsQuery) (*AnalyticsOverview, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
filter := normalizeAnalyticsFilter(query)
|
|
tasks, err := s.repo.AnalyticsTasks(filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
logs, err := s.repo.AnalyticsAPICallLogs(filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if filter.ChannelID != "" {
|
|
tasks = tasksWithLoggedRequests(tasks, logs)
|
|
}
|
|
activityFilter := filter
|
|
rollingFrom := filter.To.AddDate(0, 0, -30)
|
|
if rollingFrom.Before(activityFilter.From) {
|
|
activityFilter.From = rollingFrom
|
|
}
|
|
activities, err := s.repo.AnalyticsActivities(activityFilter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rollingTasks := tasks
|
|
rollingLogs := logs
|
|
if activityFilter.From.Before(filter.From) {
|
|
rollingTasks, err = s.repo.AnalyticsTasks(activityFilter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hasCreationDimensionFilter(filter) {
|
|
rollingLogs, err = s.repo.AnalyticsAPICallLogs(activityFilter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if filter.ChannelID != "" {
|
|
rollingTasks = tasksWithLoggedRequests(rollingTasks, rollingLogs)
|
|
}
|
|
}
|
|
users, err := s.repo.Users()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
queued, err := s.repo.CurrentQueuedTaskCount()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := buildAnalyticsOverview(filter, tasks, rollingTasks, rollingLogs, logs, activities, users)
|
|
records, err := s.analyticsFinancialRecords(logs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
applyAnalyticsFinance(result, records)
|
|
result.KPI.CurrentQueuedTasks = queued
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Service) AdminModelPricings(actor *model.User) ([]model.ModelPricing, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.ModelPricings()
|
|
}
|
|
|
|
func (s *Service) SaveModelPricing(actor *model.User, id string, req ModelPricingRequest) (*model.ModelPricing, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
req.Model = strings.TrimSpace(req.Model)
|
|
req.Capability = normalizeCapability(req.Capability)
|
|
req.Currency = strings.ToUpper(strings.TrimSpace(req.Currency))
|
|
if req.Model == "" || req.Capability == "" {
|
|
return nil, BadAuthRequest("请填写模型并选择能力类型")
|
|
}
|
|
if req.Currency == "" {
|
|
req.Currency = "USD"
|
|
}
|
|
if len(req.Currency) > 12 || hasNegativePricing(req) {
|
|
return nil, BadAuthRequest("价格配置格式无效")
|
|
}
|
|
pricing := &model.ModelPricing{ID: newID(), CreatedAt: time.Now()}
|
|
if id != "" {
|
|
current, err := s.repo.ModelPricingByID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pricing = current
|
|
}
|
|
pricing.ChannelID = strings.TrimSpace(req.ChannelID)
|
|
pricing.Model = req.Model
|
|
pricing.Capability = req.Capability
|
|
pricing.Currency = req.Currency
|
|
pricing.InputPerMillionMicros = req.InputPerMillionMicros
|
|
pricing.OutputPerMillionMicros = req.OutputPerMillionMicros
|
|
pricing.CachedPerMillionMicros = req.CachedPerMillionMicros
|
|
pricing.PerRequestMicros = req.PerRequestMicros
|
|
pricing.PerMediaMicros = req.PerMediaMicros
|
|
pricing.PerVideoSecondMicros = req.PerVideoSecondMicros
|
|
pricing.UpdatedAt = time.Now()
|
|
if err := s.repo.Save(pricing); err != nil {
|
|
return nil, err
|
|
}
|
|
return pricing, nil
|
|
}
|
|
|
|
func (s *Service) DeleteModelPricing(actor *model.User, id string) error {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return err
|
|
}
|
|
return s.repo.DeleteModelPricing(id)
|
|
}
|
|
|
|
func hasNegativePricing(req ModelPricingRequest) bool {
|
|
return req.InputPerMillionMicros < 0 || req.OutputPerMillionMicros < 0 || req.CachedPerMillionMicros < 0 || req.PerRequestMicros < 0 || req.PerMediaMicros < 0 || req.PerVideoSecondMicros < 0
|
|
}
|
|
|
|
func (s *Service) recordActivity(userID string, event string, count int) {
|
|
if err := s.repo.RecordUserActivity(userID, event, count, time.Now()); err != nil {
|
|
log.Printf("record user activity failed: event=%s count=%d error=%v", event, count, err)
|
|
}
|
|
}
|