201
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/openinstall"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetAgentDownloadsLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetAgentDownloadsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentDownloadsLogic {
|
||||
return &GetAgentDownloadsLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetAgentDownloadsLogic) GetAgentDownloads(req *types.GetAgentDownloadsRequest) (resp *types.GetAgentDownloadsResponse, err error) {
|
||||
// 1. Get current user
|
||||
_, 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
|
||||
cfg := l.svcCtx.Config.OpenInstall
|
||||
if !cfg.Enable {
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: []types.AgentDownloadStats{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 3. Call OpenInstall API
|
||||
// Default to last 30 days
|
||||
endDate := time.Now()
|
||||
startDate := endDate.AddDate(0, 0, -30)
|
||||
|
||||
client := openinstall.NewClient(cfg.AppKey)
|
||||
stats, err := client.GetPlatformStats(l.ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to fetch OpenInstall stats", logger.Field("error", err))
|
||||
// Return empty list on error
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: []types.AgentDownloadStats{},
|
||||
}, 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,
|
||||
})
|
||||
}
|
||||
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/kutt"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetAgentRealtimeLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetAgentRealtimeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentRealtimeLogic {
|
||||
return &GetAgentRealtimeLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequest) (resp *types.GetAgentRealtimeResponse, err error) {
|
||||
// 1. Get current user
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Errorw("[GetAgentRealtime] user not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
// 2. Check if Kutt is enabled
|
||||
cfg := l.svcCtx.Config.Kutt
|
||||
if !cfg.Enable || cfg.ApiKey == "" || cfg.ApiURL == "" {
|
||||
l.Infow("Kutt service not enabled or configured",
|
||||
logger.Field("enable", cfg.Enable),
|
||||
logger.Field("has_key", cfg.ApiKey != ""),
|
||||
logger.Field("has_url", cfg.ApiURL != ""))
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 3. Get share URL and domain
|
||||
shareUrl := l.getShareUrl()
|
||||
domain := l.getDomain()
|
||||
|
||||
if shareUrl == "" {
|
||||
l.Errorw("ShareUrl not configured for Kutt integration")
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 4. Construct target URL
|
||||
// Format: https://gethifast.net/?ic=INVITECODE
|
||||
inviteCode := u.ReferCode
|
||||
if inviteCode == "" {
|
||||
l.Errorw("User has no refer code", logger.Field("user_id", u.Id))
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s?ic=%s", shareUrl, inviteCode)
|
||||
|
||||
// 5. Call Kutt API to get link stats
|
||||
// We use Reuse=true to get the existing link details including stats
|
||||
client := kutt.NewClient(cfg.ApiURL, cfg.ApiKey)
|
||||
kuttReq := &kutt.CreateLinkRequest{
|
||||
Target: target,
|
||||
Description: fmt.Sprintf("Invite link for code: %s", inviteCode),
|
||||
Reuse: true,
|
||||
Domain: domain,
|
||||
}
|
||||
|
||||
link, err := client.CreateShortLink(l.ctx, kuttReq)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to fetch Kutt stats",
|
||||
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,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 6. Get paid user count
|
||||
var paidCount int64
|
||||
db := l.svcCtx.DB
|
||||
// Count users invited by me who have paid orders
|
||||
// Sub-query to get user IDs invited by current user
|
||||
// Count distinct users who have paid orders (Status 2=Paid, 5=Finished)
|
||||
err = db.Table("order").
|
||||
Joins("LEFT JOIN user ON user.id = order.user_id").
|
||||
Where("user.referer_id = ? AND order.status IN ?", u.Id, []int{2, 5}).
|
||||
Distinct("order.user_id").
|
||||
Count(&paidCount).Error
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("Failed to count paid users",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", u.Id))
|
||||
// Don't fail the whole request, just return 0 for paid count
|
||||
paidCount = 0
|
||||
}
|
||||
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: int64(link.VisitCount),
|
||||
Clicks: int64(link.VisitCount),
|
||||
Views: int64(link.VisitCount),
|
||||
PaidCount: paidCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *GetAgentRealtimeLogic) getShareUrl() string {
|
||||
siteConfig := l.svcCtx.Config.Site
|
||||
if siteConfig.CustomData != "" {
|
||||
var data customData
|
||||
if err := json.Unmarshal([]byte(siteConfig.CustomData), &data); err == nil {
|
||||
if data.ShareUrl != "" {
|
||||
return data.ShareUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
return l.svcCtx.Config.Kutt.TargetURL
|
||||
}
|
||||
|
||||
func (l *GetAgentRealtimeLogic) getDomain() string {
|
||||
siteConfig := l.svcCtx.Config.Site
|
||||
if siteConfig.CustomData != "" {
|
||||
var data customData
|
||||
if err := json.Unmarshal([]byte(siteConfig.CustomData), &data); err == nil {
|
||||
if data.Domain != "" {
|
||||
return data.Domain
|
||||
}
|
||||
}
|
||||
}
|
||||
return l.svcCtx.Config.Kutt.Domain
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetInviteSalesLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetInviteSalesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteSalesLogic {
|
||||
return &GetInviteSalesLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (resp *types.GetInviteSalesResponse, err error) {
|
||||
// 1. Get current user
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Errorw("[GetInviteSales] user not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
userId := u.Id
|
||||
|
||||
// 2. Count total sales
|
||||
var totalSales int64
|
||||
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).
|
||||
Count(&totalSales).Error
|
||||
if err != nil {
|
||||
l.Errorw("[GetInviteSales] count sales failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
|
||||
"count sales failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// 3. Pagination
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.Size < 1 {
|
||||
req.Size = 10
|
||||
}
|
||||
if req.Size > 100 {
|
||||
req.Size = 100
|
||||
}
|
||||
offset := (req.Page - 1) * req.Size
|
||||
|
||||
// 4. Get sales data
|
||||
type OrderWithUser struct {
|
||||
Amount int64 `gorm:"column:amount"`
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
UserId int64 `gorm:"column:user_id"`
|
||||
}
|
||||
|
||||
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").
|
||||
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").
|
||||
Limit(req.Size).
|
||||
Offset(offset).
|
||||
Scan(&orderData).Error
|
||||
if err != nil {
|
||||
l.Errorw("[GetInviteSales] query sales failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
|
||||
"query sales failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// 5. Get user emails
|
||||
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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
list = append(list, types.InvitedUserSale{
|
||||
Amount: order.Amount,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UserEmail: email,
|
||||
UserId: order.UserId,
|
||||
})
|
||||
}
|
||||
|
||||
return &types.GetInviteSalesResponse{
|
||||
Total: totalSales,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetUserInviteStatsLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get user invite statistics
|
||||
func NewGetUserInviteStatsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInviteStatsLogic {
|
||||
return &GetUserInviteStatsLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetUserInviteStatsLogic) GetUserInviteStats(req *types.GetUserInviteStatsRequest) (resp *types.GetUserInviteStatsResponse, err error) {
|
||||
// 1. 从 context 中获取当前登录用户
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Errorw("[GetUserInviteStats] user not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
userId := u.Id
|
||||
|
||||
// 2. 获取有效邀请数 (FriendlyCount): 被邀请用户中至少有1个已支付订单 (status=2或5)
|
||||
var friendlyCount int64
|
||||
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
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("[GetUserInviteStats] count friendly users 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())
|
||||
}
|
||||
|
||||
// 3. 获取历史邀请总数 (HistoryCount)
|
||||
var historyCount int64
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user").
|
||||
Where("referer_id = ?", userId).
|
||||
Count(&historyCount).Error
|
||||
if err != nil {
|
||||
l.Errorw("[GetUserInviteStats] count history users failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
|
||||
"count history users failed: %v", err.Error())
|
||||
}
|
||||
|
||||
return &types.GetUserInviteStatsResponse{
|
||||
FriendlyCount: friendlyCount,
|
||||
HistoryCount: historyCount,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user