This commit is contained in:
shanshanzhong 2026-02-01 19:07:50 -08:00
parent af5231747a
commit 5b238919f5
7 changed files with 231 additions and 40 deletions

View File

@ -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 {}

View File

@ -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(&currentMonthCount).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%"
}

View File

@ -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,

View File

@ -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).

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
);