feat(admin): 运营数据 - 增加余额消耗统计、通知排序与用户积分汇总

This commit is contained in:
ddcat666
2026-10-10 07:16:22 +08:00
parent 0cba2ae6b5
commit 18d47d7852
14 changed files with 84 additions and 27 deletions

View File

@@ -28,24 +28,27 @@ type AnalyticsOverview struct {
}
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"`
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"`
TotalBalanceMicrocredits int64 `json:"totalBalanceMicrocredits"`
TotalConsumedMicrocredits int64 `json:"totalConsumedMicrocredits"`
}
type AnalyticsTrendPoint struct {
Day string `json:"day"`
Tasks int `json:"tasks"`
Requests int `json:"requests"`
ActiveUsers int `json:"activeUsers"`
RequestSuccessRate float64 `json:"requestSuccessRate"`
Day string `json:"day"`
Tasks int `json:"tasks"`
Requests int `json:"requests"`
ActiveUsers int `json:"activeUsers"`
RequestSuccessRate float64 `json:"requestSuccessRate"`
ConsumedMicrocredits int64 `json:"consumedMicrocredits"`
}
type AnalyticsModelRow struct {
@@ -163,11 +166,21 @@ func (s *Service) AdminAnalytics(actor *model.User, query AnalyticsQuery) (*Anal
if err != nil {
return nil, err
}
totalBalance, err := s.repo.TotalCreditBalance()
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)
result.KPI.TotalBalanceMicrocredits = totalBalance
for _, log := range logs {
if log.BillingAmount > 0 {
result.KPI.TotalConsumedMicrocredits += log.BillingAmount
}
}
records, err := s.analyticsFinancialRecords(logs)
if err != nil {
return nil, err

View File

@@ -117,6 +117,9 @@ func buildAnalyticsTrend(filter repository.AnalyticsFilter, tasks []model.Task,
key := log.CreatedAt.UTC().Format("2006-01-02")
if point := points[key]; point != nil {
point.Requests++
if log.BillingAmount > 0 {
point.ConsumedMicrocredits += log.BillingAmount
}
requestTotals[key]++
if log.Status == model.ApiCallStatusSucceeded {
requestSuccess[key]++

View File

@@ -520,6 +520,7 @@ type CreateBannerAnnouncementRequest struct {
NoticeType string `json:"noticeType"`
Link string `json:"link"`
Status string `json:"status"` // "active" | "disabled"
SortOrder int `json:"sortOrder"`
StartsAt *time.Time `json:"startsAt"`
EndsAt *time.Time `json:"endsAt"`
}
@@ -763,6 +764,7 @@ func (s *Service) CreateBannerAnnouncement(actor *model.User, req CreateBannerAn
NoticeType: noticeType,
Link: link,
Status: status,
SortOrder: req.SortOrder,
StartsAt: req.StartsAt,
EndsAt: req.EndsAt,
CreatedBy: actor.ID,
@@ -810,6 +812,7 @@ func (s *Service) UpdateBannerAnnouncement(actor *model.User, id string, req Upd
banner.NoticeType = noticeType
banner.Link = link
banner.Status = status
banner.SortOrder = req.SortOrder
banner.StartsAt = req.StartsAt
banner.EndsAt = req.EndsAt
banner.UpdatedAt = time.Now()

View File

@@ -11,7 +11,7 @@ import (
"gorm.io/gorm"
)
const CurrentSchemaVersion int64 = 48
const CurrentSchemaVersion int64 = 49
const baselineSchemaChecksum = "sha256:open-ai-canvas-schema-v1-20260830"
const schemaMigrationAppliedAtIndexChecksum = "sha256:schema-migrations-applied-at-index-v2-20260830"
@@ -157,6 +157,12 @@ var schemaMigrations = []migration{
}
return nil
}},
{version: 49, name: "banner_sort_order", checksum: "sha256:banner-sort-order-v49-20261010", apply: func(tx *gorm.DB) error {
if !tx.Migrator().HasColumn(&model.BannerAnnouncement{}, "SortOrder") {
return tx.Migrator().AddColumn(&model.BannerAnnouncement{}, "SortOrder")
}
return nil
}},
}
func migratePrefixedIDSequenceReconcile(tx *gorm.DB) error {

View File

@@ -476,6 +476,7 @@ type BannerAnnouncement struct {
NoticeType string `json:"noticeType" gorm:"size:24"`
Link string `json:"link" gorm:"size:500"` // 点击跳转目标:http(s) 外链或 / 开头的站内路径;空表示不可点击
Status string `json:"status" gorm:"size:24;index"` // "active" | "disabled"
SortOrder int `json:"sortOrder" gorm:"index"`
StartsAt *time.Time `json:"startsAt,omitempty" gorm:"index"`
EndsAt *time.Time `json:"endsAt,omitempty" gorm:"index"`
CreatedBy string `json:"createdBy" gorm:"size:36"`

View File

@@ -17,10 +17,12 @@ var (
)
type AdminUserCounts struct {
LedgerEntries int64 `json:"ledgerEntries"`
Tasks int64 `json:"tasks"`
APICalls int64 `json:"apiCalls"`
AuditEvents int64 `json:"auditEvents"`
LedgerEntries int64 `json:"ledgerEntries"`
Tasks int64 `json:"tasks"`
APICalls int64 `json:"apiCalls"`
AuditEvents int64 `json:"auditEvents"`
RechargeMicrocredits int64 `json:"rechargeMicrocredits"`
CheckinMicrocredits int64 `json:"checkinMicrocredits"`
}
func (r *Repository) AppendAdminAudit(event *model.AdminAuditEvent) error {
@@ -112,6 +114,14 @@ func (r *Repository) AdminUserCounts(userID string) (AdminUserCounts, error) {
return AdminUserCounts{}, err
}
}
for entryType, target := range map[model.CreditLedgerType]*int64{
model.CreditLedgerPaymentTopup: &counts.RechargeMicrocredits,
model.CreditLedgerCheckinBonus: &counts.CheckinMicrocredits,
} {
if err := r.db.Model(&model.CreditLedgerEntry{}).Where("user_id = ? AND type = ?", userID, entryType).Select("COALESCE(SUM(amount_microcredits), 0)").Scan(target).Error; err != nil {
return AdminUserCounts{}, err
}
}
return counts, nil
}

View File

@@ -85,7 +85,7 @@ func (r *Repository) ActiveBannerAnnouncements(now time.Time) ([]model.BannerAnn
Where("status = ?", "active").
Where("starts_at IS NULL OR starts_at <= ?", now).
Where("ends_at IS NULL OR ends_at >= ?", now).
Order("created_at desc").
Order("sort_order asc, created_at desc").
Find(&banners).Error
decodeBannerTitleRuns(banners)
return banners, err
@@ -104,7 +104,7 @@ func (r *Repository) AdminBannerAnnouncements(keyword string, status string, lim
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at desc").Limit(limit).Offset(offset).Find(&banners).Error; err != nil {
if err := query.Order("sort_order asc, created_at desc").Limit(limit).Offset(offset).Find(&banners).Error; err != nil {
return nil, 0, err
}
decodeBannerTitleRuns(banners)

View File

@@ -63,6 +63,12 @@ func (r *Repository) Users() ([]model.User, error) {
return users, err
}
func (r *Repository) TotalCreditBalance() (int64, error) {
var total int64
err := r.db.Model(&model.CreditAccount{}).Select("COALESCE(SUM(available_microcredits), 0)").Scan(&total).Error
return total, err
}
func (r *Repository) AdminUsers(keyword string, role model.UserRole, status model.UserStatus, limit int, offset int) ([]model.User, int64, error) {
var users []model.User
var total int64