Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/paymentauditlog"
|
||||
"github.com/Wei-Shaw/sub2api/ent/paymentorder"
|
||||
)
|
||||
|
||||
// --- Dashboard & Analytics ---
|
||||
|
||||
func (s *PaymentService) GetDashboardStats(ctx context.Context, days int) (*DashboardStats, error) {
|
||||
if days <= 0 {
|
||||
days = 30
|
||||
}
|
||||
now := time.Now()
|
||||
since := now.AddDate(0, 0, -days)
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
paidStatuses := []string{OrderStatusCompleted, OrderStatusPaid, OrderStatusRecharging}
|
||||
|
||||
orders, err := s.entClient.PaymentOrder.Query().
|
||||
Where(
|
||||
paymentorder.StatusIn(paidStatuses...),
|
||||
paymentorder.PaidAtGTE(since),
|
||||
).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st := &DashboardStats{}
|
||||
computeBasicStats(st, orders, todayStart)
|
||||
|
||||
st.PendingOrders, err = s.entClient.PaymentOrder.Query().
|
||||
Where(paymentorder.StatusEQ(OrderStatusPending)).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st.DailySeries = buildDailySeries(orders, since, days)
|
||||
st.PaymentMethods = buildMethodDistribution(orders)
|
||||
st.TopUsers = buildTopUsers(orders)
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func computeBasicStats(st *DashboardStats, orders []*dbent.PaymentOrder, todayStart time.Time) {
|
||||
st.TotalAmount = make(CurrencyAmounts)
|
||||
st.TodayAmount = make(CurrencyAmounts)
|
||||
st.AvgAmount = make(CurrencyAmounts)
|
||||
currencyCounts := make(map[string]int)
|
||||
var todayCount int
|
||||
for _, o := range orders {
|
||||
currency := PaymentOrderCurrency(o)
|
||||
st.TotalAmount[currency] += o.PayAmount
|
||||
currencyCounts[currency]++
|
||||
if o.PaidAt != nil && !o.PaidAt.Before(todayStart) {
|
||||
st.TodayAmount[currency] += o.PayAmount
|
||||
todayCount++
|
||||
}
|
||||
}
|
||||
st.TotalCount = len(orders)
|
||||
st.TodayCount = todayCount
|
||||
for currency, totalAmount := range st.TotalAmount {
|
||||
st.AvgAmount[currency] = roundAmount(totalAmount / float64(currencyCounts[currency]))
|
||||
}
|
||||
roundCurrencyAmounts(st.TotalAmount)
|
||||
roundCurrencyAmounts(st.TodayAmount)
|
||||
}
|
||||
|
||||
func buildDailySeries(orders []*dbent.PaymentOrder, since time.Time, days int) []DailyStats {
|
||||
dailyMap := make(map[string]*DailyStats)
|
||||
for _, o := range orders {
|
||||
if o.PaidAt == nil {
|
||||
continue
|
||||
}
|
||||
date := o.PaidAt.Format("2006-01-02")
|
||||
ds, ok := dailyMap[date]
|
||||
if !ok {
|
||||
ds = &DailyStats{Date: date, Amount: make(CurrencyAmounts)}
|
||||
dailyMap[date] = ds
|
||||
}
|
||||
ds.Amount[PaymentOrderCurrency(o)] += o.PayAmount
|
||||
ds.Count++
|
||||
}
|
||||
series := make([]DailyStats, 0, days)
|
||||
for i := 0; i < days; i++ {
|
||||
date := since.AddDate(0, 0, i+1).Format("2006-01-02")
|
||||
if ds, ok := dailyMap[date]; ok {
|
||||
roundCurrencyAmounts(ds.Amount)
|
||||
series = append(series, *ds)
|
||||
} else {
|
||||
series = append(series, DailyStats{Date: date, Amount: make(CurrencyAmounts)})
|
||||
}
|
||||
}
|
||||
return series
|
||||
}
|
||||
|
||||
func buildMethodDistribution(orders []*dbent.PaymentOrder) []PaymentMethodStat {
|
||||
methodMap := make(map[string]*PaymentMethodStat)
|
||||
for _, o := range orders {
|
||||
ms, ok := methodMap[o.PaymentType]
|
||||
if !ok {
|
||||
ms = &PaymentMethodStat{Type: o.PaymentType, Amount: make(CurrencyAmounts)}
|
||||
methodMap[o.PaymentType] = ms
|
||||
}
|
||||
ms.Amount[PaymentOrderCurrency(o)] += o.PayAmount
|
||||
ms.Count++
|
||||
}
|
||||
methods := make([]PaymentMethodStat, 0, len(methodMap))
|
||||
for _, ms := range methodMap {
|
||||
roundCurrencyAmounts(ms.Amount)
|
||||
methods = append(methods, *ms)
|
||||
}
|
||||
sort.Slice(methods, func(i, j int) bool {
|
||||
return methods[i].Type < methods[j].Type
|
||||
})
|
||||
return methods
|
||||
}
|
||||
|
||||
func buildTopUsers(orders []*dbent.PaymentOrder) TopUsersByCurrency {
|
||||
userMap := make(map[string]map[int64]*TopUserStat)
|
||||
for _, o := range orders {
|
||||
currency := PaymentOrderCurrency(o)
|
||||
users, ok := userMap[currency]
|
||||
if !ok {
|
||||
users = make(map[int64]*TopUserStat)
|
||||
userMap[currency] = users
|
||||
}
|
||||
us, ok := users[o.UserID]
|
||||
if !ok {
|
||||
us = &TopUserStat{UserID: o.UserID, Email: o.UserEmail}
|
||||
users[o.UserID] = us
|
||||
}
|
||||
us.Amount += o.PayAmount
|
||||
}
|
||||
result := make(TopUsersByCurrency, len(userMap))
|
||||
for currency, users := range userMap {
|
||||
userList := make([]*TopUserStat, 0, len(users))
|
||||
for _, us := range users {
|
||||
us.Amount = roundAmount(us.Amount)
|
||||
userList = append(userList, us)
|
||||
}
|
||||
sort.Slice(userList, func(i, j int) bool {
|
||||
return userList[i].Amount > userList[j].Amount
|
||||
})
|
||||
limit := topUsersLimit
|
||||
if len(userList) < limit {
|
||||
limit = len(userList)
|
||||
}
|
||||
result[currency] = make([]TopUserStat, 0, limit)
|
||||
for i := 0; i < limit; i++ {
|
||||
result[currency] = append(result[currency], *userList[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func roundCurrencyAmounts(amounts CurrencyAmounts) {
|
||||
for currency, amount := range amounts {
|
||||
amounts[currency] = roundAmount(amount)
|
||||
}
|
||||
}
|
||||
|
||||
func roundAmount(amount float64) float64 {
|
||||
return math.Round(amount*100) / 100
|
||||
}
|
||||
|
||||
// --- Audit Logs ---
|
||||
|
||||
func (s *PaymentService) writeAuditLog(ctx context.Context, oid int64, action, op string, detail map[string]any) {
|
||||
dj, _ := json.Marshal(detail)
|
||||
_, err := s.entClient.PaymentAuditLog.Create().SetOrderID(strconv.FormatInt(oid, 10)).SetAction(action).SetDetail(string(dj)).SetOperator(op).Save(ctx)
|
||||
if err != nil {
|
||||
slog.Error("audit log failed", "orderID", oid, "action", action, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PaymentService) GetOrderAuditLogs(ctx context.Context, oid int64) ([]*dbent.PaymentAuditLog, error) {
|
||||
return s.entClient.PaymentAuditLog.Query().Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(oid, 10))).Order(paymentauditlog.ByCreatedAt()).All(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user