feat(admin): 运营数据 - 增加余额消耗统计、通知排序与用户积分汇总
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]++
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user