x
This commit is contained in:
parent
af5231747a
commit
5b238919f5
@ -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 {}
|
||||
|
||||
@ -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%"
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user