package handler import ( "fmt" "net/http" "time" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" ) // AdminDistributionOverview GET /api/admin/distribution/overview(全部使用 GORM,无 Raw SQL) func AdminDistributionOverview(c *gin.Context) { now := time.Now() todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) todayEnd := todayStart.Add(24 * time.Hour) monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) db := database.DB() overview := gin.H{ "todayClicks": 0, "todayBindings": 0, "todayConversions": 0, "todayEarnings": 0, "monthClicks": 0, "monthBindings": 0, "monthConversions": 0, "monthEarnings": 0, "totalClicks": 0, "totalBindings": 0, "totalConversions": 0, "totalEarnings": 0, "expiringBindings": 0, "pendingWithdrawals": 0, "pendingWithdrawAmount": 0, "conversionRate": "0.00", "totalDistributors": 0, "activeDistributors": 0, } // 订单:仅用 Where + Count / Select(Sum) 参数化 var totalOrders int64 db.Model(&model.Order{}).Where("status = ?", "paid").Count(&totalOrders) var totalAmount float64 db.Model(&model.Order{}).Where("status = ?", "paid").Select("COALESCE(SUM(amount),0)").Scan(&totalAmount) var todayOrders int64 db.Model(&model.Order{}).Where("status = ? AND created_at >= ? AND created_at < ?", "paid", todayStart, todayEnd).Count(&todayOrders) var todayAmount float64 db.Model(&model.Order{}).Where("status = ? AND created_at >= ? AND created_at < ?", "paid", todayStart, todayEnd).Select("COALESCE(SUM(amount),0)").Scan(&todayAmount) var monthOrders int64 db.Model(&model.Order{}).Where("status = ? AND created_at >= ?", "paid", monthStart).Count(&monthOrders) var monthAmount float64 db.Model(&model.Order{}).Where("status = ? AND created_at >= ?", "paid", monthStart).Select("COALESCE(SUM(amount),0)").Scan(&monthAmount) overview["totalEarnings"] = totalAmount overview["todayEarnings"] = todayAmount overview["monthEarnings"] = monthAmount // 绑定:全部 GORM Where var totalBindings int64 db.Model(&model.ReferralBinding{}).Count(&totalBindings) var converted int64 db.Model(&model.ReferralBinding{}).Where("status = ?", "converted").Count(&converted) var todayBindings int64 db.Model(&model.ReferralBinding{}).Where("binding_date >= ? AND binding_date < ?", todayStart, todayEnd).Count(&todayBindings) var todayConv int64 db.Model(&model.ReferralBinding{}).Where("status = ? AND binding_date >= ? AND binding_date < ?", "converted", todayStart, todayEnd).Count(&todayConv) var monthBindings int64 db.Model(&model.ReferralBinding{}).Where("binding_date >= ?", monthStart).Count(&monthBindings) var monthConv int64 db.Model(&model.ReferralBinding{}).Where("status = ? AND binding_date >= ?", "converted", monthStart).Count(&monthConv) expiringEnd := now.Add(7 * 24 * time.Hour) var expiring int64 db.Model(&model.ReferralBinding{}).Where("status = ? AND expiry_date > ? AND expiry_date <= ?", "active", now, expiringEnd).Count(&expiring) overview["totalBindings"] = totalBindings overview["totalConversions"] = converted overview["todayBindings"] = todayBindings overview["todayConversions"] = todayConv overview["monthBindings"] = monthBindings overview["monthConversions"] = monthConv overview["expiringBindings"] = expiring // 访问数(点击量):总 / 今日 / 本月 var visitTotal int64 db.Model(&model.ReferralVisit{}).Count(&visitTotal) var todayClicks int64 db.Model(&model.ReferralVisit{}).Where("created_at >= ? AND created_at < ?", todayStart, todayEnd).Count(&todayClicks) var monthClicks int64 db.Model(&model.ReferralVisit{}).Where("created_at >= ?", monthStart).Count(&monthClicks) overview["totalClicks"] = visitTotal overview["todayClicks"] = todayClicks overview["monthClicks"] = monthClicks if visitTotal > 0 && converted > 0 { overview["conversionRate"] = formatPercent(float64(converted) / float64(visitTotal) * 100) } // 今日独立访客数(按 visitor_id 或 visitor_openid 去重,空则用 id 占位) var todayUniqueVisitors int64 db.Raw("SELECT COUNT(DISTINCT COALESCE(visitor_id, visitor_openid, CONCAT('_', id))) FROM referral_visits WHERE created_at >= ? AND created_at < ?", todayStart, todayEnd).Scan(&todayUniqueVisitors) overview["todayUniqueVisitors"] = todayUniqueVisitors // 今日总文章点击率:今日总点击 / 今日独立访客(人均点击),无访客时为 0 todayClickRate := 0.0 if todayUniqueVisitors > 0 && todayClicks > 0 { todayClickRate = float64(todayClicks) / float64(todayUniqueVisitors) } overview["todayClickRate"] = todayClickRate // 每篇文章今日点击量(按 page 分组) var pageClicks []struct { Page string `gorm:"column:page"` Clicks int64 `gorm:"column:clicks"` } db.Raw("SELECT COALESCE(page, '') AS page, COUNT(*) AS clicks FROM referral_visits WHERE created_at >= ? AND created_at < ? GROUP BY COALESCE(page, '')", todayStart, todayEnd).Scan(&pageClicks) byPage := make([]gin.H, 0, len(pageClicks)) for _, r := range pageClicks { byPage = append(byPage, gin.H{"page": r.Page, "clicks": r.Clicks}) } overview["todayClicksByPage"] = byPage // 提现待处理 var pendCount int64 db.Model(&model.Withdrawal{}).Where("status = ?", "pending").Count(&pendCount) var pendSum float64 db.Model(&model.Withdrawal{}).Where("status = ?", "pending").Select("COALESCE(SUM(amount),0)").Scan(&pendSum) overview["pendingWithdrawals"] = pendCount overview["pendingWithdrawAmount"] = pendSum // 推广佣金(referral_bindings.total_commission 累计) var totalCommission float64 db.Model(&model.ReferralBinding{}).Select("COALESCE(SUM(total_commission),0)").Scan(&totalCommission) var todayCommission float64 db.Model(&model.ReferralBinding{}).Where("binding_date >= ? AND binding_date < ?", todayStart, todayEnd).Select("COALESCE(SUM(total_commission),0)").Scan(&todayCommission) var monthCommission float64 db.Model(&model.ReferralBinding{}).Where("binding_date >= ?", monthStart).Select("COALESCE(SUM(total_commission),0)").Scan(&monthCommission) overview["totalCommission"] = totalCommission overview["todayCommission"] = todayCommission overview["monthCommission"] = monthCommission // 分销商 var distTotal int64 db.Model(&model.User{}).Where("referral_code IS NOT NULL AND referral_code != ?", "").Count(&distTotal) var distActive int64 db.Model(&model.User{}).Where("referral_code IS NOT NULL AND referral_code != ? AND earnings > ?", "", 0).Count(&distActive) overview["totalDistributors"] = distTotal overview["activeDistributors"] = distActive c.JSON(http.StatusOK, gin.H{"success": true, "overview": overview}) } // AdminDistributionRanking GET /api/admin/distribution/ranking?limit=20&sort=bindings func AdminDistributionRanking(c *gin.Context) { db := database.DB() limit := 20 if l, err := fmt.Sscanf(c.DefaultQuery("limit", "20"), "%d", &limit); err != nil || l == 0 { limit = 20 } if limit < 1 { limit = 1 } if limit > 100 { limit = 100 } type row struct { ReferrerID string `gorm:"column:referrer_id"` Bindings int64 `gorm:"column:bindings"` Conversions int64 `gorm:"column:conversions"` Commission float64 `gorm:"column:commission"` } var rows []row db.Raw(` SELECT referrer_id, COUNT(*) AS bindings, SUM(CASE WHEN status='converted' THEN 1 ELSE 0 END) AS conversions, COALESCE(SUM(total_commission), 0) AS commission FROM referral_bindings GROUP BY referrer_id ORDER BY bindings DESC LIMIT ?`, limit).Scan(&rows) ids := make([]string, 0, len(rows)) for _, r := range rows { ids = append(ids, r.ReferrerID) } var users []model.User if len(ids) > 0 { db.Select("id", "nickname", "avatar", "phone").Where("id IN ?", ids).Find(&users) } umap := map[string]*model.User{} for i := range users { umap[users[i].ID] = &users[i] } // clicks per referrer type clickRow struct { ReferralCode string `gorm:"column:referral_code"` Clicks int64 `gorm:"column:clicks"` } var clickRows []clickRow db.Raw(`SELECT referral_code, COUNT(*) AS clicks FROM referral_visits WHERE referral_code != '' GROUP BY referral_code`).Scan(&clickRows) clickMap := map[string]int64{} for _, cr := range clickRows { clickMap[cr.ReferralCode] = cr.Clicks } // referral_code per user codeMap := map[string]string{} for _, u := range users { if u.ReferralCode != nil && *u.ReferralCode != "" { codeMap[u.ID] = *u.ReferralCode } } out := make([]gin.H, 0, len(rows)) for i, r := range rows { nick := "" phone := "" avatar := "" if u := umap[r.ReferrerID]; u != nil { if u.Nickname != nil { nick = *u.Nickname } if u.Phone != nil { phone = *u.Phone } if u.Avatar != nil { avatar = resolveAvatarURL(*u.Avatar) } } clicks := int64(0) if code, ok := codeMap[r.ReferrerID]; ok { clicks = clickMap[code] } out = append(out, gin.H{ "rank": i + 1, "userId": r.ReferrerID, "nickname": nick, "avatar": avatar, "phone": phone, "bindings": r.Bindings, "conversions": r.Conversions, "commission": r.Commission, "clicks": clicks, }) } c.JSON(http.StatusOK, gin.H{"success": true, "data": out}) } func formatPercent(v float64) string { return fmt.Sprintf("%.2f", v) + "%" }