231 lines
9.1 KiB
Go
231 lines
9.1 KiB
Go
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) + "%"
|
||
}
|