package handler import ( "encoding/json" "net/http" "strconv" "sync" "time" "soul-api/internal/database" "soul-api/internal/model" "soul-api/internal/wechat" "github.com/gin-gonic/gin" "gorm.io/gorm" ) var paidStatuses = []string{"paid", "completed", "success"} // AdminDashboardStats GET /api/admin/dashboard/stats // 轻量聚合:总用户、付费订单数、付费用户数、总营收、转化率(无订单/用户明细) func AdminDashboardStats(c *gin.Context) { db := database.DB() var ( totalUsers int64 paidOrderCount int64 totalRevenue float64 paidUserCount int64 ) var wg sync.WaitGroup wg.Add(4) go func() { defer wg.Done(); db.Model(&model.User{}).Count(&totalUsers) }() go func() { defer wg.Done(); db.Model(&model.Order{}).Where("status IN ?", paidStatuses).Count(&paidOrderCount) }() go func() { defer wg.Done() db.Model(&model.Order{}).Where("status IN ?", paidStatuses). Select("COALESCE(SUM(amount), 0)").Scan(&totalRevenue) }() go func() { defer wg.Done() db.Table("orders").Where("status IN ?", paidStatuses). Select("COUNT(DISTINCT user_id)").Scan(&paidUserCount) }() wg.Wait() conversionRate := 0.0 if totalUsers > 0 && paidUserCount > 0 { conversionRate = float64(paidUserCount) / float64(totalUsers) * 100 } c.JSON(http.StatusOK, gin.H{ "success": true, "totalUsers": totalUsers, "paidOrderCount": paidOrderCount, "paidUserCount": paidUserCount, "totalRevenue": totalRevenue, "conversionRate": conversionRate, }) } // AdminDashboardRecentOrders GET /api/admin/dashboard/recent-orders?limit=10 func AdminDashboardRecentOrders(c *gin.Context) { db := database.DB() limit := 5 if l := c.Query("limit"); l != "" { if n, err := strconv.Atoi(l); err == nil && n >= 1 && n <= 20 { limit = n } } var recentOrders []model.Order db.Where("status IN ?", paidStatuses).Order("created_at DESC").Limit(limit).Find(&recentOrders) c.JSON(http.StatusOK, gin.H{"success": true, "recentOrders": buildRecentOrdersOut(db, recentOrders)}) } // AdminDashboardNewUsers GET /api/admin/dashboard/new-users func AdminDashboardNewUsers(c *gin.Context) { db := database.DB() var newUsers []model.User db.Model(&model.User{}).Order("created_at DESC").Limit(10).Find(&newUsers) c.JSON(http.StatusOK, gin.H{"success": true, "newUsers": buildNewUsersOut(newUsers)}) } // AdminDashboardOverview GET /api/admin/dashboard/overview // 数据概览:总用户、付费订单数、付费用户数、总营收、转化率、最近订单、新用户 // 优化:6 组查询并行执行,减少总耗时 func AdminDashboardOverview(c *gin.Context) { db := database.DB() var ( totalUsers int64 paidOrderCount int64 totalRevenue float64 paidUserCount int64 recentOrders []model.Order newUsers []model.User ) var wg sync.WaitGroup wg.Add(6) go func() { defer wg.Done() db.Model(&model.User{}).Count(&totalUsers) }() go func() { defer wg.Done() db.Model(&model.Order{}).Where("status IN ?", paidStatuses).Count(&paidOrderCount) }() go func() { defer wg.Done() db.Model(&model.Order{}).Where("status IN ?", paidStatuses). Select("COALESCE(SUM(amount), 0)").Scan(&totalRevenue) }() go func() { defer wg.Done() db.Table("orders").Where("status IN ?", paidStatuses). Select("COUNT(DISTINCT user_id)").Scan(&paidUserCount) }() go func() { defer wg.Done() db.Where("status IN ?", paidStatuses). Order("created_at DESC").Limit(5).Find(&recentOrders) }() go func() { defer wg.Done() db.Model(&model.User{}).Order("created_at DESC").Limit(10).Find(&newUsers) }() wg.Wait() conversionRate := 0.0 if totalUsers > 0 && paidUserCount > 0 { conversionRate = float64(paidUserCount) / float64(totalUsers) * 100 } recentOut := buildRecentOrdersOut(db, recentOrders) newOut := buildNewUsersOut(newUsers) c.JSON(http.StatusOK, gin.H{ "success": true, "totalUsers": totalUsers, "paidOrderCount": paidOrderCount, "paidUserCount": paidUserCount, "totalRevenue": totalRevenue, "conversionRate": conversionRate, "recentOrders": recentOut, "newUsers": newOut, }) } func dashStr(s *string) string { if s == nil || *s == "" { return "" } return *s } func buildRecentOrdersOut(db *gorm.DB, recentOrders []model.Order) []gin.H { if len(recentOrders) == 0 { return nil } userIDs := make(map[string]bool) for _, o := range recentOrders { if o.UserID != "" { userIDs[o.UserID] = true } } ids := make([]string, 0, len(userIDs)) for id := range userIDs { ids = append(ids, id) } var users []model.User db.Where("id IN ?", ids).Find(&users) userMap := make(map[string]*model.User) for i := range users { userMap[users[i].ID] = &users[i] } out := make([]gin.H, 0, len(recentOrders)) for _, o := range recentOrders { b, _ := json.Marshal(o) var m map[string]interface{} _ = json.Unmarshal(b, &m) if u := userMap[o.UserID]; u != nil { m["userNickname"] = dashStr(u.Nickname) m["userAvatar"] = resolveAvatarURL(dashStr(u.Avatar)) } else { m["userNickname"] = "" m["userAvatar"] = "" } out = append(out, m) } return out } // AdminTrackStats GET /api/admin/track/stats?period=today|week|month|all // 埋点统计:按 extra_data->module 分组,按 action+target 聚合 count func AdminTrackStats(c *gin.Context) { period := c.DefaultQuery("period", "week") if period != "today" && period != "week" && period != "month" && period != "all" { period = "week" } now := time.Now() var start time.Time switch period { case "today": start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) case "week": weekday := int(now.Weekday()) if weekday == 0 { weekday = 7 } start = time.Date(now.Year(), now.Month(), now.Day()-weekday+1, 0, 0, 0, 0, now.Location()) case "month": start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) case "all": start = time.Time{} } db := database.DB() var tracks []model.UserTrack q := db.Model(&model.UserTrack{}) if !start.IsZero() { q = q.Where("created_at >= ?", start) } if err := q.Find(&tracks).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } // byModule: module -> map[key] -> count, key = action + "|" + target type item struct { Action string `json:"action"` Target string `json:"target"` Module string `json:"module"` Page string `json:"page"` Count int `json:"count"` } byModule := make(map[string]map[string]*item) total := 0 for _, t := range tracks { total++ module := "other" page := "" if len(t.ExtraData) > 0 { var extra map[string]interface{} if err := json.Unmarshal(t.ExtraData, &extra); err == nil { if m, ok := extra["module"].(string); ok && m != "" { module = m } if p, ok := extra["page"].(string); ok { page = p } } } target := "" if t.Target != nil { target = *t.Target } key := t.Action + "|" + target if byModule[module] == nil { byModule[module] = make(map[string]*item) } if byModule[module][key] == nil { byModule[module][key] = &item{Action: t.Action, Target: target, Module: module, Page: page, Count: 0} } byModule[module][key].Count++ } // 转为前端期望格式:byModule[module] = [{action,target,module,page,count},...] out := make(map[string][]gin.H) for mod, m := range byModule { list := make([]gin.H, 0, len(m)) for _, v := range m { list = append(list, gin.H{ "action": v.Action, "target": v.Target, "module": v.Module, "page": v.Page, "count": v.Count, }) } out[mod] = list } c.JSON(http.StatusOK, gin.H{"success": true, "total": total, "byModule": out}) } // AdminBalanceSummary GET /api/admin/balance/summary // 汇总代付金额(product_type 为 gift_pay 或 gift_pay_batch 的已支付订单),用于 Dashboard 显示「含代付 ¥xx」 func AdminBalanceSummary(c *gin.Context) { db := database.DB() var totalGifted float64 db.Model(&model.Order{}).Where("product_type IN ? AND status IN ?", []string{"gift_pay", "gift_pay_batch"}, paidStatuses). Select("COALESCE(SUM(amount), 0)").Scan(&totalGifted) c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalGifted": totalGifted}}) } // AdminDashboardMerchantBalance GET /api/admin/dashboard/merchant-balance // 查询微信商户号实时余额(可用余额、待结算余额),用于看板展示 // 注意:普通商户可能需向微信申请开通权限,未开通时返回 error func AdminDashboardMerchantBalance(c *gin.Context) { bal, err := wechat.QueryMerchantBalance("BASIC") if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": err.Error(), "message": "查询商户余额失败,可能未开通权限(请联系微信支付运营申请)", }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "availableAmount": bal.AvailableAmount, // 单位:分 "pendingAmount": bal.PendingAmount, // 单位:分 }) } func buildNewUsersOut(newUsers []model.User) []gin.H { out := make([]gin.H, 0, len(newUsers)) for _, u := range newUsers { out = append(out, gin.H{ "id": u.ID, "nickname": dashStr(u.Nickname), "phone": dashStr(u.Phone), "referralCode": dashStr(u.ReferralCode), "createdAt": u.CreatedAt, }) } return out }