203
This commit is contained in:
@@ -2,8 +2,7 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"time"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@@ -30,47 +29,80 @@ func NewGetAgentDownloadsLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
||||
}
|
||||
|
||||
func (l *GetAgentDownloadsLogic) GetAgentDownloads(req *types.GetAgentDownloadsRequest) (resp *types.GetAgentDownloadsResponse, err error) {
|
||||
// 1. Get current user
|
||||
_, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
// 1. 从 context 获取用户信息
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Errorw("[GetAgentDownloads] user not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
// 2. Check configuration
|
||||
// 2. 检查 OpenInstall 是否启用
|
||||
cfg := l.svcCtx.Config.OpenInstall
|
||||
if !cfg.Enable {
|
||||
l.Infow("[GetAgentDownloads] OpenInstall is disabled, returning zero stats")
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: []types.AgentDownloadStats{},
|
||||
Total: 0,
|
||||
Platforms: &types.PlatformDownloads{
|
||||
IOS: 0,
|
||||
Android: 0,
|
||||
Windows: 0,
|
||||
Mac: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 3. Call OpenInstall API
|
||||
// Default to last 30 days
|
||||
endDate := time.Now()
|
||||
startDate := endDate.AddDate(0, 0, -30)
|
||||
// 3. 检查 ApiKey 是否配置
|
||||
if cfg.ApiKey == "" {
|
||||
l.Errorw("[GetAgentDownloads] OpenInstall ApiKey not configured")
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
Total: 0,
|
||||
Platforms: &types.PlatformDownloads{
|
||||
IOS: 0,
|
||||
Android: 0,
|
||||
Windows: 0,
|
||||
Mac: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
client := openinstall.NewClient(cfg.AppKey)
|
||||
stats, err := client.GetPlatformStats(l.ctx, startDate, endDate)
|
||||
// 4. 调用 OpenInstall API 获取各端下载量
|
||||
client := openinstall.NewClient(cfg.ApiKey)
|
||||
platformDownloads, err := client.GetPlatformDownloads(l.ctx)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to fetch OpenInstall stats", logger.Field("error", err))
|
||||
// Return empty list on error
|
||||
l.Errorw("Failed to fetch OpenInstall platform downloads", logger.Field("error", err), logger.Field("user_id", u.Id))
|
||||
// 返回空数据而不是错误,避免影响前端显示
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: []types.AgentDownloadStats{},
|
||||
Total: 0,
|
||||
Platforms: &types.PlatformDownloads{
|
||||
IOS: 0,
|
||||
Android: 0,
|
||||
Windows: 0,
|
||||
Mac: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 4. Map response
|
||||
var list []types.AgentDownloadStats
|
||||
for _, s := range stats {
|
||||
list = append(list, types.AgentDownloadStats{
|
||||
Platform: s.Platform,
|
||||
Clicks: s.Clicks,
|
||||
Visits: s.Visits,
|
||||
})
|
||||
// 5. 构造响应
|
||||
var comparisonRate *string
|
||||
if platformDownloads.Comparison != nil {
|
||||
percent := platformDownloads.Comparison.ChangePercent
|
||||
var formatted string
|
||||
if percent >= 0 {
|
||||
formatted = fmt.Sprintf("+%.1f%%", percent)
|
||||
} else {
|
||||
formatted = fmt.Sprintf("%.1f%%", percent)
|
||||
}
|
||||
comparisonRate = &formatted
|
||||
}
|
||||
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: list,
|
||||
Total: platformDownloads.Total,
|
||||
Platforms: &types.PlatformDownloads{
|
||||
IOS: platformDownloads.IOS,
|
||||
Android: platformDownloads.Android,
|
||||
Windows: platformDownloads.Windows,
|
||||
Mac: platformDownloads.Mac,
|
||||
},
|
||||
ComparisonRate: comparisonRate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -133,10 +133,27 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ
|
||||
// 8. Calculate paid user growth rate (month-over-month)
|
||||
paidGrowthRate := l.calculatePaidGrowthRate(u.Id)
|
||||
|
||||
// Use current month clicks if available
|
||||
var currentMonthClicks int64
|
||||
if stats != nil && len(stats.LastYear.Views) > 0 {
|
||||
currentMonthClicks = int64(stats.LastYear.Views[len(stats.LastYear.Views)-1])
|
||||
} else {
|
||||
// Fallback to total if stats not available (or 0)
|
||||
// Requirement suggests monthly, so maybe 0 is better if stats fail?
|
||||
// Existing logic: total. Let's stick to total if stats fail unless strict requirement.
|
||||
// User said: "kutt interface data requires monthly clicks".
|
||||
// If stats fail, it means we don't have monthly data.
|
||||
// Let's fallback to 0 to be safe/strict about "Monthly", OR keep using VisitCount if that's the only thing we have.
|
||||
// Given the user wants "current month", finding 0 is more accurate than Total All Time if we can't find month.
|
||||
// However, typically fallback to Total is less "broken" looking.
|
||||
// But let's follow the instruction: "Use current month".
|
||||
// If stats err, currentMonthClicks remains 0.
|
||||
}
|
||||
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: int64(link.VisitCount),
|
||||
Clicks: int64(link.VisitCount),
|
||||
Views: int64(link.VisitCount),
|
||||
Total: currentMonthClicks,
|
||||
Clicks: currentMonthClicks,
|
||||
Views: currentMonthClicks,
|
||||
PaidCount: paidCount,
|
||||
GrowthRate: growthRate,
|
||||
PaidGrowthRate: paidGrowthRate,
|
||||
|
||||
@@ -2,6 +2,9 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"strconv"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@@ -40,7 +43,7 @@ func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("`order` o").
|
||||
Joins("JOIN user u ON o.user_id = u.id").
|
||||
Where("u.referer_id = ? AND o.status IN (?, ?)", userId, 2, 5).
|
||||
Where("u.referer_id = ? AND o.status = ?", userId, 5).
|
||||
Count(&totalSales).Error
|
||||
if err != nil {
|
||||
l.Errorw("[GetInviteSales] count sales failed",
|
||||
@@ -64,18 +67,21 @@ func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (
|
||||
|
||||
// 4. Get sales data
|
||||
type OrderWithUser struct {
|
||||
Amount int64 `gorm:"column:amount"`
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
UserId int64 `gorm:"column:user_id"`
|
||||
Amount int64 `gorm:"column:amount"`
|
||||
UpdatedAt int64 `gorm:"column:updated_at"`
|
||||
UserId int64 `gorm:"column:user_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
Quantity int64 `gorm:"column:quantity"`
|
||||
}
|
||||
|
||||
var orderData []OrderWithUser
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("`order` o").
|
||||
Select("o.amount, CAST(UNIX_TIMESTAMP(o.created_at) * 1000 AS SIGNED) as created_at, u.id as user_id").
|
||||
Select("o.amount, CAST(UNIX_TIMESTAMP(o.updated_at) * 1000 AS SIGNED) as updated_at, u.id as user_id, s.name as product_name, o.quantity").
|
||||
Joins("JOIN user u ON o.user_id = u.id").
|
||||
Where("u.referer_id = ? AND o.status IN (?, ?)", userId, 2, 5). // status 2: Paid, 5: Finished
|
||||
Order("o.created_at DESC").
|
||||
Joins("LEFT JOIN subscribe s ON o.subscribe_id = s.id").
|
||||
Where("u.referer_id = ? AND o.status = ?", userId, 5). // status 5: Finished
|
||||
Order("o.updated_at DESC").
|
||||
Limit(req.Size).
|
||||
Offset(offset).
|
||||
Scan(&orderData).Error
|
||||
@@ -87,34 +93,29 @@ func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (
|
||||
"query sales failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// 5. Get user emails
|
||||
// 5. Get sales list
|
||||
const HashSalt = "ppanel_invite_sales_v1" // Fixed Key
|
||||
var list []types.InvitedUserSale
|
||||
for _, order := range orderData {
|
||||
var email string
|
||||
// Try email auth first
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_auth_methods").
|
||||
Select("auth_identifier").
|
||||
Where("user_id = ? AND auth_type = ?", order.UserId, "email").
|
||||
Limit(1).
|
||||
Scan(&email).Error
|
||||
// Calculate unique numeric hash (FNV-64a)
|
||||
h := fnv.New64a()
|
||||
h.Write([]byte(HashSalt))
|
||||
h.Write([]byte(strconv.FormatInt(order.UserId, 10)))
|
||||
// Truncate to 10 digits using modulo 10^10
|
||||
hashVal := h.Sum64() % 10000000000
|
||||
userHashStr := fmt.Sprintf("%010d", hashVal)
|
||||
|
||||
// Fallback to any auth method
|
||||
if err != nil || email == "" {
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_auth_methods").
|
||||
Select("auth_identifier").
|
||||
Where("user_id = ?", order.UserId).
|
||||
Order("created_at ASC").
|
||||
Limit(1).
|
||||
Scan(&email).Error
|
||||
// Format product name as "{{ quantity }}天VPN服务"
|
||||
productName := fmt.Sprintf("%d天VPN服务", order.Quantity)
|
||||
if order.Quantity <= 0 {
|
||||
productName = "1天VPN服务"
|
||||
}
|
||||
|
||||
list = append(list, types.InvitedUserSale{
|
||||
Amount: float64(order.Amount) / 100.0, // Convert cents to dollars
|
||||
CreatedAt: order.CreatedAt,
|
||||
UserEmail: email,
|
||||
UserId: order.UserId,
|
||||
Amount: float64(order.Amount) / 100.0, // Convert cents to dollars
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
UserHash: userHashStr,
|
||||
ProductName: productName,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user