Files
sub2api/backend/internal/service/payment_stats.go
erio 63d1860dc0 feat(payment): add complete payment system with multi-provider support
Add a full payment and subscription system supporting EasyPay (Alipay/WeChat),
Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
2026-04-11 13:16:35 +08:00

164 lines
4.4 KiB
Go

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) {
var totalAmount, todayAmount float64
var todayCount int
for _, o := range orders {
totalAmount += o.PayAmount
if o.PaidAt != nil && !o.PaidAt.Before(todayStart) {
todayAmount += o.PayAmount
todayCount++
}
}
st.TotalAmount = math.Round(totalAmount*100) / 100
st.TodayAmount = math.Round(todayAmount*100) / 100
st.TotalCount = len(orders)
st.TodayCount = todayCount
if st.TotalCount > 0 {
st.AvgAmount = math.Round(totalAmount/float64(st.TotalCount)*100) / 100
}
}
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}
dailyMap[date] = ds
}
ds.Amount += 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 {
ds.Amount = math.Round(ds.Amount*100) / 100
series = append(series, *ds)
} else {
series = append(series, DailyStats{Date: date})
}
}
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}
methodMap[o.PaymentType] = ms
}
ms.Amount += o.PayAmount
ms.Count++
}
methods := make([]PaymentMethodStat, 0, len(methodMap))
for _, ms := range methodMap {
ms.Amount = math.Round(ms.Amount*100) / 100
methods = append(methods, *ms)
}
return methods
}
func buildTopUsers(orders []*dbent.PaymentOrder) []TopUserStat {
userMap := make(map[int64]*TopUserStat)
for _, o := range orders {
us, ok := userMap[o.UserID]
if !ok {
us = &TopUserStat{UserID: o.UserID, Email: o.UserEmail}
userMap[o.UserID] = us
}
us.Amount += o.PayAmount
}
userList := make([]*TopUserStat, 0, len(userMap))
for _, us := range userMap {
us.Amount = math.Round(us.Amount*100) / 100
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 := make([]TopUserStat, 0, limit)
for i := 0; i < limit; i++ {
result = append(result, *userList[i])
}
return result
}
// --- 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)
}