package admin import ( "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" "go.uber.org/zap" "gorm.io/gorm" "openteam/server/internal/billing" "openteam/server/internal/pkg/httpx" "openteam/server/internal/store" "openteam/server/internal/user" ) type usageHandler struct { db *gorm.DB bill *billing.Service log *zap.Logger } // Overview handles GET /api/v1/admin/stats/overview. func (h *usageHandler) Overview(c *gin.Context) { now := time.Now() today := now.Format("2006-01-02") month := now.Format("2006-01-02") var totals struct { Requests int Cost decimal.Decimal } h.db.Model(&store.UsageDaily{}). Select("COALESCE(SUM(requests),0) as requests, COALESCE(SUM(cost),0) as cost"). Scan(&totals) var todayAgg struct { Requests int Cost decimal.Decimal InputTokens int64 OutputTokens int64 } h.db.Model(&store.UsageDaily{}).Where("date = ?", today). Select("COALESCE(SUM(requests),0) as requests, COALESCE(SUM(cost),0) as cost, COALESCE(SUM(input_tokens),0) as input_tokens, COALESCE(SUM(output_tokens),0) as output_tokens"). Scan(&todayAgg) var monthAgg struct { Requests int Cost decimal.Decimal } h.db.Model(&store.UsageDaily{}).Where("date >= ? AND date <= ?", month[:7]+"-01", today). Select("COALESCE(SUM(requests),0) as requests, COALESCE(SUM(cost),0) as cost"). Scan(&monthAgg) var userCount, channelCount, modelCount int64 h.db.Model(&store.User{}).Count(&userCount) h.db.Model(&store.Channel{}).Count(&channelCount) h.db.Model(&store.Model{}).Count(&modelCount) httpx.OK(c, gin.H{ "total": gin.H{ "requests": totals.Requests, "cost": totals.Cost.String(), "users": userCount, "channels": channelCount, "models": modelCount, }, "today": gin.H{ "requests": todayAgg.Requests, "cost": todayAgg.Cost.String(), "inputTokens": todayAgg.InputTokens, "outputTokens": todayAgg.OutputTokens, }, "month": gin.H{ "requests": monthAgg.Requests, "cost": monthAgg.Cost.String(), }, }) } // Usage handles GET /api/v1/admin/usage?from&to&userId&model&group=day|model|user. func (h *usageHandler) Usage(c *gin.Context) { from := c.Query("from") to := c.Query("to") group := c.DefaultQuery("group", "day") q := h.db.Model(&store.UsageDaily{}) if from != "" { q = q.Where("date >= ?", from) } if to != "" { q = q.Where("date <= ?", to) } if uid := c.Query("userId"); uid != "" { q = q.Where("user_id = ?", uid) } if m := c.Query("model"); m != "" { q = q.Where("model_id = ?", m) } var rows []struct { Key string `gorm:"column:g"` Requests int InputTokens int64 OutputTokens int64 Cost decimal.Decimal } switch group { case "model": q = q.Joins("JOIN models ON models.id = usage_dailies.model_id"). Select("models.name as g, COALESCE(SUM(requests),0) as requests, COALESCE(SUM(input_tokens),0) as input_tokens, COALESCE(SUM(output_tokens),0) as output_tokens, COALESCE(SUM(cost),0) as cost"). Group("models.name") case "user": q = q.Joins("JOIN users ON users.id = usage_dailies.user_id"). Select("users.username as g, COALESCE(SUM(requests),0) as requests, COALESCE(SUM(input_tokens),0) as input_tokens, COALESCE(SUM(output_tokens),0) as output_tokens, COALESCE(SUM(cost),0) as cost"). Group("users.username") default: q = q.Select("date as g, COALESCE(SUM(requests),0) as requests, COALESCE(SUM(input_tokens),0) as input_tokens, COALESCE(SUM(output_tokens),0) as output_tokens, COALESCE(SUM(cost),0) as cost"). Group("date").Order("date ASC") } if err := q.Scan(&rows).Error; err != nil { h.log.Warn("admin usage query failed", zap.Error(err)) httpx.Fail(c, http.StatusInternalServerError, "usage query failed") return } out := make([]gin.H, 0, len(rows)) for _, r := range rows { out = append(out, gin.H{ "key": r.Key, "requests": r.Requests, "inputTokens": r.InputTokens, "outputTokens": r.OutputTokens, "cost": r.Cost.String(), }) } httpx.OK(c, out) } // Recharges handles GET /api/v1/admin/recharges. func (h *usageHandler) Recharges(c *gin.Context) { status := c.Query("status") q := h.db.Preload("User").Model(&store.RechargeOrder{}) if status != "" { q = q.Where("status = ?", status) } var orders []store.RechargeOrder if err := q.Order("id DESC").Limit(100).Find(&orders).Error; err != nil { h.log.Warn("list recharges failed", zap.Error(err)) httpx.Fail(c, http.StatusInternalServerError, "list recharges failed") return } out := make([]gin.H, 0, len(orders)) for i := range orders { o := &orders[i] out = append(out, gin.H{ "id": o.ID, "userId": o.UserID, "username": o.User.Username, "amount": o.Amount.String(), "status": o.Status, "method": o.Method, "remark": o.Remark, "createdAt": o.CreatedAt, }) } httpx.OK(c, out) } // ApproveRecharge handles POST /api/v1/admin/recharges/:id/approve. func (h *usageHandler) ApproveRecharge(c *gin.Context) { h.decideRecharge(c, "approve") } // RejectRecharge handles POST /api/v1/admin/recharges/:id/reject. func (h *usageHandler) RejectRecharge(c *gin.Context) { h.decideRecharge(c, "reject") } func (h *usageHandler) decideRecharge(c *gin.Context, action string) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { httpx.Fail(c, http.StatusBadRequest, "invalid order id") return } admin := user.Current(c) var order store.RechargeOrder if err := h.db.First(&order, id).Error; err != nil { httpx.Fail(c, http.StatusNotFound, "order not found") return } if order.Status != "pending" { httpx.Fail(c, http.StatusBadRequest, "order already processed") return } var in struct { Remark string `json:"remark"` } _ = c.ShouldBindJSON(&in) now := time.Now() if action == "approve" { err = h.db.Transaction(func(tx *gorm.DB) error { if _, cerr := h.bill.Credit(order.UserID, order.Amount, "recharge", orderIDStr(order.ID)); cerr != nil { return cerr } return tx.Model(&order).Updates(map[string]any{ "status": "credited", "reviewed_by": admin.ID, "reviewed_at": now, "remark": in.Remark, }).Error }) } else { err = h.db.Model(&order).Updates(map[string]any{ "status": "rejected", "reviewed_by": admin.ID, "reviewed_at": now, "remark": in.Remark, }).Error } if err != nil { h.log.Warn("recharge decision failed", zap.Error(err)) httpx.Fail(c, http.StatusInternalServerError, "recharge decision failed") return } httpx.OK(c, gin.H{"ok": true, "status": order.Status}) } func orderIDStr(id int64) string { return strconv.FormatInt(id, 10) }