From 5b238919f5ae7e686a2eb94e28ba5acd68068eb3 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Sun, 1 Feb 2026 19:07:50 -0800 Subject: [PATCH] x --- apis/public/user.api | 10 +- .../public/user/getAgentRealtimeLogic.go | 111 ++++++++++++++++-- .../logic/public/user/getInviteSalesLogic.go | 2 +- .../public/user/getUserInviteStatsLogic.go | 20 ++-- internal/types/types.go | 18 +-- pkg/kutt/kutt.go | 86 ++++++++++++++ test_data_mock_invites.sql | 24 ++-- 7 files changed, 231 insertions(+), 40 deletions(-) diff --git a/apis/public/user.api b/apis/public/user.api index 276af2e..1205f04 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -122,10 +122,12 @@ type ( GetAgentRealtimeRequest {} // GetAgentRealtimeResponse - 代理链接实时数据响应 GetAgentRealtimeResponse { - Total int64 `json:"total"` // 访问总人数 - Clicks int64 `json:"clicks"` // 点击量 - Views int64 `json:"views"` // 浏览量 - PaidCount int64 `json:"paid_count"` // 付费数量 + Total int64 `json:"total"` // 访问总人数 + Clicks int64 `json:"clicks"` // 点击量 + Views int64 `json:"views"` // 浏览量 + PaidCount int64 `json:"paid_count"` // 付费数量 + GrowthRate string `json:"growth_rate"` // 访问量环比增长率(例如:"+10.5%"、"-5.2%"、"0%") + PaidGrowthRate string `json:"paid_growth_rate"` // 付费用户环比增长率(例如:"+20.0%"、"-10.0%"、"0%") } // GetUserInviteStatsRequest - 获取用户邀请统计 GetUserInviteStatsRequest {} diff --git a/internal/logic/public/user/getAgentRealtimeLogic.go b/internal/logic/public/user/getAgentRealtimeLogic.go index e916d97..ce7473f 100644 --- a/internal/logic/public/user/getAgentRealtimeLogic.go +++ b/internal/logic/public/user/getAgentRealtimeLogic.go @@ -2,9 +2,9 @@ package user import ( "context" - "encoding/json" "fmt" + "time" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" @@ -85,17 +85,32 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ link, err := client.CreateShortLink(l.ctx, kuttReq) if err != nil { - l.Errorw("Failed to fetch Kutt stats", + l.Errorw("Failed to create/fetch Kutt link", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("target", target)) // Return 0 on error, don't block the UI return &types.GetAgentRealtimeResponse{ - Total: 0, + Total: 0, + GrowthRate: "0%", }, nil } - // 6. Get paid user count + // 6. Get detailed stats for growth rate calculation + stats, err := client.GetLinkStats(l.ctx, link.ID) + var growthRate string + if err != nil { + l.Errorw("Failed to fetch Kutt detailed stats, using basic count only", + logger.Field("error", err.Error()), + logger.Field("link_id", link.ID)) + growthRate = "N/A" + } else { + // Calculate month-over-month growth rate + // lastYear.views is an array of 12 months, last element is current month, second-to-last is previous month + growthRate = calculateGrowthRate(stats.LastYear.Views) + } + + // 7. Get paid user count var paidCount int64 db := l.svcCtx.DB // Count users invited by me who have paid orders @@ -115,11 +130,16 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ paidCount = 0 } + // 8. Calculate paid user growth rate (month-over-month) + paidGrowthRate := l.calculatePaidGrowthRate(u.Id) + return &types.GetAgentRealtimeResponse{ - Total: int64(link.VisitCount), - Clicks: int64(link.VisitCount), - Views: int64(link.VisitCount), - PaidCount: paidCount, + Total: int64(link.VisitCount), + Clicks: int64(link.VisitCount), + Views: int64(link.VisitCount), + PaidCount: paidCount, + GrowthRate: growthRate, + PaidGrowthRate: paidGrowthRate, }, nil } @@ -148,3 +168,78 @@ func (l *GetAgentRealtimeLogic) getDomain() string { } return l.svcCtx.Config.Kutt.Domain } + +// calculatePaidGrowthRate 计算付费用户的环比增长率 +func (l *GetAgentRealtimeLogic) calculatePaidGrowthRate(userId int64) string { + db := l.svcCtx.DB + + // 获取本月第一天和上月第一天 + now := time.Now() + currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + lastMonthStart := currentMonthStart.AddDate(0, -1, 0) + + // 查询本月付费用户数(本月有新订单的) + var currentMonthCount int64 + err := db.Table("`order` o"). + Joins("JOIN user u ON o.user_id = u.id"). + Where("u.referer_id = ? AND o.status IN (?, ?) AND o.created_at >= ?", + userId, 2, 5, currentMonthStart). + Distinct("o.user_id"). + Count(¤tMonthCount).Error + + if err != nil { + l.Errorw("Failed to count current month paid users", + logger.Field("error", err.Error()), + logger.Field("user_id", userId)) + return "N/A" + } + + // 查询上月付费用户数 + var lastMonthCount int64 + err = db.Table("`order` o"). + Joins("JOIN user u ON o.user_id = u.id"). + Where("u.referer_id = ? AND o.status IN (?, ?) AND o.created_at >= ? AND o.created_at < ?", + userId, 2, 5, lastMonthStart, currentMonthStart). + Distinct("o.user_id"). + Count(&lastMonthCount).Error + + if err != nil { + l.Errorw("Failed to count last month paid users", + logger.Field("error", err.Error()), + logger.Field("user_id", userId)) + return "N/A" + } + + // 计算增长率 + return calculateGrowthRate([]int{int(lastMonthCount), int(currentMonthCount)}) +} + +// calculateGrowthRate 计算环比增长率 +// views: 月份数据数组,最后一个是本月,倒数第二个是上月 +func calculateGrowthRate(views []int) string { + if len(views) < 2 { + return "N/A" + } + + currentMonth := views[len(views)-1] + lastMonth := views[len(views)-2] + + // 如果上月是0,无法计算百分比 + if lastMonth == 0 { + if currentMonth == 0 { + return "0%" + } + return "+100%" + } + + // 计算增长率 + growth := float64(currentMonth-lastMonth) / float64(lastMonth) * 100 + + // 格式化输出 + if growth > 0 { + return fmt.Sprintf("+%.1f%%", growth) + } else if growth < 0 { + return fmt.Sprintf("%.1f%%", growth) + } + return "0%" +} diff --git a/internal/logic/public/user/getInviteSalesLogic.go b/internal/logic/public/user/getInviteSalesLogic.go index ee80bc5..ca0f4ba 100644 --- a/internal/logic/public/user/getInviteSalesLogic.go +++ b/internal/logic/public/user/getInviteSalesLogic.go @@ -111,7 +111,7 @@ func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) ( } list = append(list, types.InvitedUserSale{ - Amount: order.Amount, + Amount: float64(order.Amount) / 100.0, // Convert cents to dollars CreatedAt: order.CreatedAt, UserEmail: email, UserId: order.UserId, diff --git a/internal/logic/public/user/getUserInviteStatsLogic.go b/internal/logic/public/user/getUserInviteStatsLogic.go index 0c20ca2..dc2106d 100644 --- a/internal/logic/public/user/getUserInviteStatsLogic.go +++ b/internal/logic/public/user/getUserInviteStatsLogic.go @@ -2,6 +2,7 @@ package user import ( "context" + "database/sql" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" @@ -36,21 +37,26 @@ func (l *GetUserInviteStatsLogic) GetUserInviteStats(req *types.GetUserInviteSta } userId := u.Id - // 2. 获取有效邀请数 (FriendlyCount): 被邀请用户中至少有1个已支付订单 (status=2或5) - var friendlyCount int64 + // 2. 获取历史邀请佣金 (FriendlyCount): 所有被邀请用户产生订单的佣金总和 + // 注意:这里复用了 friendly_count 字段名,实际含义是佣金总额 + var totalCommission sql.NullInt64 err = l.svcCtx.DB.WithContext(l.ctx). - Table("user"). - Where("referer_id = ? AND EXISTS (SELECT 1 FROM `order` o WHERE o.user_id = user.id AND o.status IN (?, ?))", userId, 2, 5). - Count(&friendlyCount).Error + Table("`order` o"). + Select("COALESCE(SUM(o.commission), 0) as total"). + Joins("JOIN user u ON o.user_id = u.id"). + Where("u.referer_id = ? AND o.status IN (?, ?)", userId, 2, 5). // 只统计已支付和已完成的订单 + Scan(&totalCommission).Error if err != nil { - l.Errorw("[GetUserInviteStats] count friendly users failed", + l.Errorw("[GetUserInviteStats] sum commission failed", logger.Field("error", err.Error()), logger.Field("user_id", userId)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), - "count friendly users failed: %v", err.Error()) + "sum commission failed: %v", err.Error()) } + friendlyCount := totalCommission.Int64 + // 3. 获取历史邀请总数 (HistoryCount) var historyCount int64 err = l.svcCtx.DB.WithContext(l.ctx). diff --git a/internal/types/types.go b/internal/types/types.go index a022390..e52a05b 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -909,10 +909,12 @@ type GetGlobalConfigResponse struct { type GetAgentRealtimeRequest struct{} type GetAgentRealtimeResponse struct { - Total int64 `json:"total"` // 访问总人数 - Clicks int64 `json:"clicks"` // 点击量 - Views int64 `json:"views"` // 浏览量 - PaidCount int64 `json:"paid_count"` // 付费数量 + Total int64 `json:"total"` // 访问总人数 + Clicks int64 `json:"clicks"` // 点击量 + Views int64 `json:"views"` // 浏览量 + PaidCount int64 `json:"paid_count"` // 付费数量 + GrowthRate string `json:"growth_rate"` // 访问量环比增长率 + PaidGrowthRate string `json:"paid_growth_rate"` // 付费用户环比增长率 } type GetAgentDownloadsRequest struct{} @@ -945,10 +947,10 @@ type GetInviteSalesResponse struct { } type InvitedUserSale struct { - Amount int64 `json:"amount"` - CreatedAt int64 `json:"created_at"` - UserEmail string `json:"user_email"` - UserId int64 `json:"user_id"` + Amount float64 `json:"amount"` + CreatedAt int64 `json:"created_at"` + UserEmail string `json:"user_email"` + UserId int64 `json:"user_id"` } type GetLoginLogRequest struct { diff --git a/pkg/kutt/kutt.go b/pkg/kutt/kutt.go index 390c457..136adb6 100644 --- a/pkg/kutt/kutt.go +++ b/pkg/kutt/kutt.go @@ -161,3 +161,89 @@ func (c *Client) CreateInviteShortLink(ctx context.Context, baseURL, inviteCode, return link.Link, nil } + +// PeriodStats 时间段统计数据 +type PeriodStats struct { + Total int `json:"total"` // 总访问量 + Views []int `json:"views"` // 时间序列数据(按天/小时) + Stats StatsDetail `json:"stats"` // 详细统计 +} + +// StatsDetail 详细统计信息 +type StatsDetail struct { + Browser []StatItem `json:"browser"` // 浏览器分布 + OS []StatItem `json:"os"` // 操作系统分布 + Country []StatItem `json:"country"` // 国家分布 + Referrer []StatItem `json:"referrer"` // 来源分布 +} + +// StatItem 统计项 +type StatItem struct { + Name string `json:"name"` + Value int `json:"value"` +} + +// LinkStatsResponse 链接详细统计响应 +type LinkStatsResponse struct { + ID string `json:"id"` + Address string `json:"address"` + Link string `json:"link"` + Target string `json:"target"` + VisitCount int `json:"visit_count"` + LastDay PeriodStats `json:"lastDay"` + LastWeek PeriodStats `json:"lastWeek"` + LastMonth PeriodStats `json:"lastMonth"` + LastYear PeriodStats `json:"lastYear"` +} + +// GetLinkStats 获取链接的详细统计数据 +// +// 参数: +// - ctx: 上下文 +// - linkID: 链接的 UUID +// +// 返回: +// - *LinkStatsResponse: 详细统计数据 +// - error: 错误信息 +func (c *Client) GetLinkStats(ctx context.Context, linkID string) (*LinkStatsResponse, error) { + // 创建 HTTP 请求 + url := fmt.Sprintf("%s/links/%s/stats", c.apiURL, linkID) + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("create request failed: %w", err) + } + + // 设置请求头 + httpReq.Header.Set("X-API-KEY", c.apiKey) + httpReq.Header.Set("Content-Type", "application/json") + + // 发送请求 + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("send request failed: %w", err) + } + defer resp.Body.Close() + + // 读取响应体 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response failed: %w", err) + } + + // 检查响应状态 + if resp.StatusCode != http.StatusOK { + var errResp ErrorResponse + if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error != "" { + return nil, fmt.Errorf("kutt api error: %s - %s", errResp.Error, errResp.Message) + } + return nil, fmt.Errorf("kutt api error: status %d, body: %s", resp.StatusCode, string(respBody)) + } + + // 解析响应 + var stats LinkStatsResponse + if err := json.Unmarshal(respBody, &stats); err != nil { + return nil, fmt.Errorf("unmarshal response failed: %w", err) + } + + return &stats, nil +} diff --git a/test_data_mock_invites.sql b/test_data_mock_invites.sql index e96d1a7..d5ccc81 100644 --- a/test_data_mock_invites.sql +++ b/test_data_mock_invites.sql @@ -84,39 +84,39 @@ ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_005@test.com'; -- status: 2 = Paid(已支付), 5 = Finished(已完成) -- =================================================================== --- 订单 1:用户 10001,已支付,金额 $9.99 +-- 订单 1:用户 10001,已支付,金额 $9.99,佣金 $0.99 INSERT INTO `order` ( - `user_id`, `order_no`, `type`, `status`, `amount`, `quantity`, + `user_id`, `order_no`, `type`, `status`, `amount`, `commission`, `quantity`, `payment_id`, `created_at`, `updated_at` ) VALUES ( - 10001, 'MOCK_ORDER_001', 1, 2, 999, 1, + 10001, 'MOCK_ORDER_001', 1, 2, 999, 99, 1, 1, DATE_SUB(NOW(), INTERVAL 10 DAY), DATE_SUB(NOW(), INTERVAL 10 DAY) ); --- 订单 2:用户 10001,已支付,金额 $19.99 +-- 订单 2:用户 10001,已支付,金额 $19.99,佣金 $1.99 INSERT INTO `order` ( - `user_id`, `order_no`, `type`, `status`, `amount`, `quantity`, + `user_id`, `order_no`, `type`, `status`, `amount`, `commission`, `quantity`, `payment_id`, `created_at`, `updated_at` ) VALUES ( - 10001, 'MOCK_ORDER_002', 1, 2, 1999, 1, + 10001, 'MOCK_ORDER_002', 1, 2, 1999, 199, 1, 1, DATE_SUB(NOW(), INTERVAL 5 DAY), DATE_SUB(NOW(), INTERVAL 5 DAY) ); --- 订单 3:用户 10002,已支付,金额 $29.99 +-- 订单 3:用户 10002,已支付,金额 $29.99,佣金 $2.99 INSERT INTO `order` ( - `user_id`, `order_no`, `type`, `status`, `amount`, `quantity`, + `user_id`, `order_no`, `type`, `status`, `amount`, `commission`, `quantity`, `payment_id`, `created_at`, `updated_at` ) VALUES ( - 10002, 'MOCK_ORDER_003', 1, 2, 2999, 1, + 10002, 'MOCK_ORDER_003', 1, 2, 2999, 299, 1, 1, DATE_SUB(NOW(), INTERVAL 3 DAY), DATE_SUB(NOW(), INTERVAL 3 DAY) ); --- 订单 4:用户 10003,已完成,金额 $49.99 +-- 订单 4:用户 10003,已完成,金额 $49.99,佣金 $4.99 INSERT INTO `order` ( - `user_id`, `order_no`, `type`, `status`, `amount`, `quantity`, + `user_id`, `order_no`, `type`, `status`, `amount`, `commission`, `quantity`, `payment_id`, `created_at`, `updated_at` ) VALUES ( - 10003, 'MOCK_ORDER_004', 1, 5, 4999, 1, + 10003, 'MOCK_ORDER_004', 1, 5, 4999, 499, 1, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY) );