This commit is contained in:
2026-01-31 08:30:58 -08:00
parent 16c261bd36
commit af5231747a
16 changed files with 895 additions and 24 deletions
@@ -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
}