423 lines
13 KiB
Go
423 lines
13 KiB
Go
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
|
||
}
|