tokenFactory/model/distributor_analytics.go

423 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package model
import (
"fmt"
"time"
"github.com/QuantumNous/new-api/common"
)
func sqlUnixToYMDColumn(col string) string {
switch {
case common.UsingPostgreSQL:
return fmt.Sprintf("TO_CHAR(TO_TIMESTAMP(%s), 'YYYY-MM-DD')", col)
case common.UsingMySQL:
return fmt.Sprintf("DATE_FORMAT(FROM_UNIXTIME(%s), '%%Y-%%m-%%d')", col)
default:
return fmt.Sprintf("strftime('%%Y-%%m-%%d', %s, 'unixepoch')", col)
}
}
// DistributorAnalyticsDay 单日聚合(分销商看板与管理端序列共用形状)。
type DistributorAnalyticsDay struct {
Date string `json:"date"`
ShortLinkClicks int `json:"short_link_clicks"`
RegisterPageViews int `json:"register_page_views"`
NewRegistrations int `json:"new_registrations"`
RewardQuota int64 `json:"reward_quota"`
InviteeQuotaAdded int64 `json:"invitee_quota_added"`
}
type aggCountRow struct {
Day string `gorm:"column:day"`
Count int64 `gorm:"column:cnt"`
}
type aggSumRow struct {
Day string `gorm:"column:day"`
Sum int64 `gorm:"column:sum"`
SumIn int64 `gorm:"column:sum_in"`
}
// InviteeTopAnalyticsRow 被邀请人排行(当前分销商维度)。
type InviteeTopAnalyticsRow struct {
InviteeUserId int `json:"invitee_user_id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
TotalRewardQuota int64 `json:"total_reward_quota"`
PeriodRewardQuota int64 `json:"period_reward_quota"`
TotalInviteeQuotaIn int64 `json:"total_invitee_quota_added"`
}
// DistributorAdminTopRow 管理端分销商排行一行。
type DistributorAdminTopRow struct {
UserId int `json:"user_id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
AffCode string `json:"aff_code"`
TotalRewardQuota int64 `json:"total_reward_quota"`
PeriodRewardQuota int64 `json:"period_reward_quota"`
InviteeCount int64 `json:"invitee_count"`
}
func distributorUserJoinSQL(alias string) string {
// 与 UserIsDistributor 一致非管理员且is_distributor=1 或历史 role=5
return fmt.Sprintf(`INNER JOIN users u ON u.id = %s.inviter_id AND u.role < %d AND (u.is_distributor = %d OR u.role = %d)`,
alias, common.RoleAdminUser, common.DistributorFlagYes, common.RoleDistributorUser)
}
// BuildDistributorSelfAnalytics 合并漏斗表、注册关系、分成日志,生成连续日期序列。
func BuildDistributorSelfAnalytics(inviterId int, days int) ([]DistributorAnalyticsDay, error) {
if inviterId <= 0 || days <= 0 {
return []DistributorAnalyticsDay{}, nil
}
if days > 90 {
days = 90
}
end := time.Now().UTC().Truncate(24 * time.Hour)
start := end.AddDate(0, 0, -(days - 1))
dateFrom := start.Format("2006-01-02")
dateTo := end.Format("2006-01-02")
startUnix := start.Unix()
endUnix := end.AddDate(0, 0, 1).Unix() // [start, end] 闭区间按 created_at < 次日
funnelRows, err := ListAffFunnelDailyForInviter(inviterId, dateFrom, dateTo)
if err != nil {
return nil, err
}
funnelMap := make(map[string]AffFunnelDaily, len(funnelRows))
for _, r := range funnelRows {
funnelMap[r.StatDate] = r
}
regMap, err := countAffRegistrationsByDay(inviterId, startUnix, endUnix)
if err != nil {
return nil, err
}
rewMap, inMap, err := sumAffCommissionByDay(inviterId, startUnix, endUnix)
if err != nil {
return nil, err
}
var dates []string
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
dates = append(dates, d.Format("2006-01-02"))
}
out := make([]DistributorAnalyticsDay, 0, len(dates))
for _, ds := range dates {
f := funnelMap[ds]
out = append(out, DistributorAnalyticsDay{
Date: ds,
ShortLinkClicks: f.ShortLinkClicks,
RegisterPageViews: f.RegisterPageViews,
NewRegistrations: int(regMap[ds]),
RewardQuota: rewMap[ds],
InviteeQuotaAdded: inMap[ds],
})
}
return out, nil
}
func countAffRegistrationsByDay(inviterId int, startUnix, endUnix int64) (map[string]int64, error) {
dayExpr := sqlUnixToYMDColumn("created_at")
var rows []aggCountRow
err := DB.Model(&AffInviteRelation{}).
Select(dayExpr+" AS day, COUNT(*) AS cnt").
Where("inviter_id = ? AND created_at >= ? AND created_at < ?", inviterId, startUnix, endUnix).
Group(dayExpr).
Scan(&rows).Error
if err != nil {
return nil, err
}
m := make(map[string]int64, len(rows))
for _, r := range rows {
m[r.Day] = r.Count
}
return m, nil
}
func sumAffCommissionByDay(inviterId int, startUnix, endUnix int64) (reward map[string]int64, inviteeIn map[string]int64, err error) {
dayExpr := sqlUnixToYMDColumn("created_at")
var rows []aggSumRow
q := DB.Model(&AffInviteCommissionLog{}).
Select(dayExpr+" AS day, COALESCE(SUM(reward_quota),0) AS sum, COALESCE(SUM(invitee_quota_added),0) AS sum_in").
Where("inviter_id = ? AND created_at >= ? AND created_at < ?", inviterId, startUnix, endUnix).
Group(dayExpr)
err = q.Scan(&rows).Error
if err != nil {
return nil, nil, err
}
reward = make(map[string]int64, len(rows))
inviteeIn = make(map[string]int64, len(rows))
for _, r := range rows {
reward[r.Day] = r.Sum
inviteeIn[r.Day] = r.SumIn
}
return reward, inviteeIn, nil
}
// ListInviteeTopForDistributorAnalytics 当前分销商下被邀请人 TOP按累计收益附近 7 日收益)。
func ListInviteeTopForDistributorAnalytics(inviterId int, topN int) ([]InviteeTopAnalyticsRow, error) {
if inviterId <= 0 {
return []InviteeTopAnalyticsRow{}, nil
}
if topN <= 0 {
topN = 10
}
if topN > 50 {
topN = 50
}
now := time.Now().UTC()
periodStart := now.AddDate(0, 0, -7).Unix()
type sumRow struct {
InviteeUserId int `gorm:"column:invitee_user_id"`
TotalReward int64 `gorm:"column:total_reward"`
PeriodReward int64 `gorm:"column:period_reward"`
TotalInviteeQuota int64 `gorm:"column:total_invitee_quota"`
}
var sums []sumRow
sel := fmt.Sprintf(`invitee_user_id,
COALESCE(SUM(reward_quota),0) AS total_reward,
COALESCE(SUM(CASE WHEN created_at >= %d THEN reward_quota ELSE 0 END),0) AS period_reward,
COALESCE(SUM(invitee_quota_added),0) AS total_invitee_quota`, periodStart)
err := DB.Model(&AffInviteCommissionLog{}).
Select(sel).
Where("inviter_id = ?", inviterId).
Group("invitee_user_id").
Order("total_reward DESC").
Limit(topN).
Scan(&sums).Error
if err != nil {
return nil, err
}
if len(sums) == 0 {
return []InviteeTopAnalyticsRow{}, nil
}
ids := make([]int, 0, len(sums))
for _, s := range sums {
ids = append(ids, s.InviteeUserId)
}
var users []User
_ = DB.Select("id", "username", "display_name").Where("id IN ?", ids).Find(&users).Error
uMap := make(map[int]User, len(users))
for _, u := range users {
uMap[u.Id] = u
}
out := make([]InviteeTopAnalyticsRow, 0, len(sums))
for _, s := range sums {
u := uMap[s.InviteeUserId]
out = append(out, InviteeTopAnalyticsRow{
InviteeUserId: s.InviteeUserId,
Username: u.Username,
DisplayName: u.DisplayName,
TotalRewardQuota: s.TotalReward,
PeriodRewardQuota: s.PeriodReward,
TotalInviteeQuotaIn: s.TotalInviteeQuota,
})
}
return out, nil
}
// BuildPlatformAffiliateAnalytics 管理端:全平台按日序列 + 分销商排行。
func BuildPlatformAffiliateAnalytics(days int) (series []DistributorAnalyticsDay, topTotal, topPeriod, topInvite []DistributorAdminTopRow, err error) {
if days <= 0 {
days = 30
}
if days > 90 {
days = 90
}
end := time.Now().UTC().Truncate(24 * time.Hour)
start := end.AddDate(0, 0, -(days - 1))
dateFrom := start.Format("2006-01-02")
dateTo := end.Format("2006-01-02")
startUnix := start.Unix()
endUnix := end.AddDate(0, 0, 1).Unix()
funnelPlat, err := SumAffFunnelDailyPlatform(dateFrom, dateTo)
if err != nil {
return nil, nil, nil, nil, err
}
regPlat, err := countAllAffRegistrationsByDay(startUnix, endUnix)
if err != nil {
return nil, nil, nil, nil, err
}
rewPlat, inPlat, err := sumAllAffCommissionByDay(startUnix, endUnix)
if err != nil {
return nil, nil, nil, nil, err
}
var dates []string
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
dates = append(dates, d.Format("2006-01-02"))
}
series = make([]DistributorAnalyticsDay, 0, len(dates))
for _, ds := range dates {
f := funnelPlat[ds]
series = append(series, DistributorAnalyticsDay{
Date: ds,
ShortLinkClicks: f.Clicks,
RegisterPageViews: f.RegViews,
NewRegistrations: int(regPlat[ds]),
RewardQuota: rewPlat[ds],
InviteeQuotaAdded: inPlat[ds],
})
}
topTotal, err = listAdminTopDistributorsByReward(0, 20)
if err != nil {
return nil, nil, nil, nil, err
}
periodStart := time.Now().UTC().AddDate(0, 0, -30).Unix()
topPeriod, err = listAdminTopDistributorsByReward(periodStart, 20)
if err != nil {
return nil, nil, nil, nil, err
}
topInvite, err = listAdminTopDistributorsByInviteeCount(20)
if err != nil {
return nil, nil, nil, nil, err
}
return series, topTotal, topPeriod, topInvite, nil
}
func countAllAffRegistrationsByDay(startUnix, endUnix int64) (map[string]int64, error) {
dayExpr := sqlUnixToYMDColumn("created_at")
var rows []aggCountRow
err := DB.Model(&AffInviteRelation{}).
Select(dayExpr+" AS day, COUNT(*) AS cnt").
Where("created_at >= ? AND created_at < ?", startUnix, endUnix).
Group(dayExpr).
Scan(&rows).Error
if err != nil {
return nil, err
}
m := make(map[string]int64, len(rows))
for _, r := range rows {
m[r.Day] = r.Count
}
return m, nil
}
func sumAllAffCommissionByDay(startUnix, endUnix int64) (reward map[string]int64, inviteeIn map[string]int64, err error) {
dayExpr := sqlUnixToYMDColumn("created_at")
var rows []aggSumRow
err = DB.Model(&AffInviteCommissionLog{}).
Select(dayExpr+" AS day, COALESCE(SUM(reward_quota),0) AS sum, COALESCE(SUM(invitee_quota_added),0) AS sum_in").
Where("created_at >= ? AND created_at < ?", startUnix, endUnix).
Group(dayExpr).
Scan(&rows).Error
if err != nil {
return nil, nil, err
}
reward = make(map[string]int64, len(rows))
inviteeIn = make(map[string]int64, len(rows))
for _, r := range rows {
reward[r.Day] = r.Sum
inviteeIn[r.Day] = r.SumIn
}
return reward, inviteeIn, nil
}
func listAdminTopDistributorsByReward(periodStartUnix int64, limit int) ([]DistributorAdminTopRow, error) {
type row struct {
InviterId int `gorm:"column:inviter_id"`
Sum int64 `gorm:"column:sum_reward"`
}
var sums []row
q := DB.Model(&AffInviteCommissionLog{}).Table("aff_invite_commission_logs AS l").
Select("l.inviter_id, COALESCE(SUM(l.reward_quota),0) AS sum_reward").
Joins(distributorUserJoinSQL("l")).
Group("l.inviter_id")
if periodStartUnix > 0 {
q = q.Where("l.created_at >= ?", periodStartUnix)
}
err := q.Order("sum_reward DESC").Limit(limit).Scan(&sums).Error
if err != nil {
return nil, err
}
out := make([]DistributorAdminTopRow, 0, len(sums))
for _, s := range sums {
out = append(out, DistributorAdminTopRow{
UserId: s.InviterId,
TotalRewardQuota: s.Sum,
})
}
if len(out) == 0 {
return out, nil
}
ids := make([]int, len(out))
for i := range out {
ids[i] = out[i].UserId
}
var users []User
_ = DB.Select("id", "username", "display_name", "aff_code").Where("id IN ?", ids).Find(&users).Error
uMap := make(map[int]User, len(users))
for _, u := range users {
uMap[u.Id] = u
}
type ic struct {
InviterId int `gorm:"column:inviter_id"`
Cnt int64 `gorm:"column:cnt"`
}
var icRows []ic
_ = DB.Model(&User{}).Select("inviter_id, COUNT(*) AS cnt").Where("inviter_id IN ?", ids).Group("inviter_id").Scan(&icRows).Error
icMap := make(map[int]int64, len(icRows))
for _, r := range icRows {
icMap[r.InviterId] = r.Cnt
}
for i := range out {
u := uMap[out[i].UserId]
out[i].Username = u.Username
out[i].DisplayName = u.DisplayName
out[i].AffCode = u.AffCode
out[i].InviteeCount = icMap[out[i].UserId]
}
return out, nil
}
func listAdminTopDistributorsByInviteeCount(limit int) ([]DistributorAdminTopRow, error) {
type row struct {
InviterId int `gorm:"column:inviter_id"`
Cnt int64 `gorm:"column:cnt"`
}
var sums []row
err := DB.Model(&User{}).Table("users AS u").
Select("u.id AS inviter_id, COUNT(c.id) AS cnt").
Joins("LEFT JOIN users c ON c.inviter_id = u.id").
Where("u.role < ? AND (u.is_distributor = ? OR u.role = ?)", common.RoleAdminUser, common.DistributorFlagYes, common.RoleDistributorUser).
Group("u.id").
Order("cnt DESC").
Limit(limit).
Scan(&sums).Error
if err != nil {
return nil, err
}
out := make([]DistributorAdminTopRow, 0, len(sums))
for _, s := range sums {
out = append(out, DistributorAdminTopRow{
UserId: s.InviterId,
InviteeCount: s.Cnt,
})
}
if len(out) == 0 {
return out, nil
}
ids := make([]int, len(out))
for i := range out {
ids[i] = out[i].UserId
}
var users []User
_ = DB.Select("id", "username", "display_name", "aff_code").Where("id IN ?", ids).Find(&users).Error
uMap := make(map[int]User, len(users))
for _, u := range users {
uMap[u.Id] = u
}
for i := range out {
u := uMap[out[i].UserId]
out[i].Username = u.Username
out[i].DisplayName = u.DisplayName
out[i].AffCode = u.AffCode
}
return out, nil
}