Files
assetx/webapp-back/points/routers.go
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

542 lines
14 KiB
Go

package points
import (
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"github.com/gothinkster/golang-gin-realworld-example-app/models"
)
// GetDashboard returns user points dashboard
// GET /api/points/dashboard?wallet_address=0x...
func GetDashboard(c *gin.Context) {
db := common.GetDB()
user, found := getUserByWallet(c)
if !found {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "User not found",
})
return
}
vipLevel := user.VIPLevel
if vipLevel < 1 {
vipLevel = 1
}
var currentTier models.VIPTier
db.Where("tier_level = ?", vipLevel).First(&currentTier)
var nextTier models.VIPTier
var pointsToNextTier int64
var nextTierName string
if db.Where("tier_level = ?", vipLevel+1).First(&nextTier).Error == nil {
pointsToNextTier = nextTier.MinPoints - user.TotalPoints
nextTierName = nextTier.TierName
} else {
pointsToNextTier = 0
nextTierName = "Max Level"
}
var season models.Season
db.Where("status = ?", "active").First(&season)
now := time.Now()
timeRemaining := season.EndTime.Sub(now)
daysRemaining := int(timeRemaining.Hours() / 24)
hoursRemaining := int(timeRemaining.Hours()) % 24
var totalUsers int64
db.Model(&models.User{}).Count(&totalUsers)
topPercentage := "N/A"
globalRank := 0
if user.GlobalRank != nil {
globalRank = *user.GlobalRank
if totalUsers > 0 {
percentage := float64(*user.GlobalRank) / float64(totalUsers) * 100
topPercentage = fmt.Sprintf("%.0f%%", percentage)
}
}
response := models.DashboardResponse{
TotalPoints: user.TotalPoints,
GlobalRank: globalRank,
TopPercentage: topPercentage,
MemberTier: user.MemberTier,
VIPLevel: vipLevel,
PointsToNextTier: pointsToNextTier,
NextTier: nextTierName,
Season: models.SeasonInfo{
SeasonNumber: season.SeasonNumber,
SeasonName: season.SeasonName,
IsLive: season.Status == "active",
EndTime: season.EndTime,
DaysRemaining: daysRemaining,
HoursRemaining: hoursRemaining,
},
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": response,
})
}
// GetLeaderboard returns top performers leaderboard
// GET /api/points/leaderboard?limit=5&wallet_address=0x...
func GetLeaderboard(c *gin.Context) {
db := common.GetDB()
limitStr := c.DefaultQuery("limit", "5")
limit, _ := strconv.Atoi(limitStr)
var topUsers []models.User
db.Where("global_rank IS NOT NULL").
Order("global_rank ASC").
Limit(limit).
Find(&topUsers)
var leaderboard []models.LeaderboardUser
for _, u := range topUsers {
rank := 0
if u.GlobalRank != nil {
rank = *u.GlobalRank
}
leaderboard = append(leaderboard, models.LeaderboardUser{
Rank: rank,
WalletAddress: formatAddress(u.WalletAddress),
Points: u.TotalPoints,
})
}
currentUser, _ := getUserByWallet(c)
myRank := 0
if currentUser.GlobalRank != nil {
myRank = *currentUser.GlobalRank
}
response := models.LeaderboardResponse{
TopUsers: leaderboard,
MyRank: myRank,
MyPoints: currentUser.TotalPoints,
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": response,
})
}
// GetInviteCode returns user's invite code information
// GET /api/points/invite-code?wallet_address=0x...
func GetInviteCode(c *gin.Context) {
db := common.GetDB()
user, found := getUserByWallet(c)
if !found {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "User not found",
})
return
}
var inviteCode models.InviteCode
err := db.Where("wallet_address = ?", user.WalletAddress).First(&inviteCode).Error
if err != nil {
// Create a system-generated invite code
code := strings.ToUpper(common.RandString(8))
defaultExpiry := time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC)
inviteCode = models.InviteCode{
WalletAddress: user.WalletAddress,
Code: code,
MaxUses: -1,
UsedCount: 0,
IsActive: true,
ExpiresAt: models.NullTime{Time: &defaultExpiry},
}
if err := db.Create(&inviteCode).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"error": "Failed to create invite code",
})
return
}
// Also sync to users.invite_code
db.Model(&user).Update("invite_code", code)
}
response := models.InviteCodeResponse{
Code: inviteCode.Code,
UsedCount: inviteCode.UsedCount,
MaxUses: inviteCode.MaxUses,
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": response,
})
}
// BindInvite binds an invite code to current user
// POST /api/points/bind-invite
func BindInvite(c *gin.Context) {
db := common.GetDB()
var req struct {
Code string `json:"code" binding:"required"`
Signature string `json:"signature" binding:"required"`
WalletAddress string `json:"wallet_address"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "Invalid request body",
})
return
}
// Look up user by wallet_address
var user models.User
walletAddr := strings.ToLower(strings.TrimSpace(req.WalletAddress))
if walletAddr == "" {
// Try query param fallback
walletAddr = strings.ToLower(strings.TrimSpace(c.Query("wallet_address")))
}
if walletAddr == "" || db.Where("wallet_address = ?", walletAddr).First(&user).Error != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "User not found. Please connect your wallet first.",
})
return
}
// Check if user already has a referrer
if user.ReferrerWallet != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "Already bound to a referrer",
})
return
}
// Find invite code
var inviteCode models.InviteCode
if err := db.Where("code = ? AND is_active = ?", req.Code, true).First(&inviteCode).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "Invalid invite code",
})
return
}
// Check if max uses reached
if inviteCode.MaxUses != -1 && inviteCode.UsedCount >= inviteCode.MaxUses {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "Invite code max uses reached",
})
return
}
// Prevent self-referral
if inviteCode.WalletAddress == user.WalletAddress {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "Cannot use your own invite code",
})
return
}
// Create invitation record
invitation := models.Invitation{
ReferrerWallet: inviteCode.WalletAddress,
RefereeWallet: user.WalletAddress,
InviteCode: req.Code,
BindSignature: req.Signature,
Status: "active",
ReferrerRewardPoints: 100,
RefereeRewardPoints: 50,
BoundAt: time.Now(),
}
if err := db.Create(&invitation).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"error": "Failed to bind invite code",
})
return
}
// Update user referrer
db.Model(&user).Update("referrer_wallet", inviteCode.WalletAddress)
// Update invite code used count
db.Model(&inviteCode).Update("used_count", inviteCode.UsedCount+1)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Invite code bound successfully",
})
}
// GetTeamTVL returns team TVL statistics
// GET /api/points/team?wallet_address=0x...
func GetTeamTVL(c *gin.Context) {
db := common.GetDB()
user, _ := getUserByWallet(c)
var team models.UserTeam
if err := db.Where("wallet_address = ?", user.WalletAddress).First(&team).Error; err != nil {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": models.TeamTVLResponse{
CurrentTVL: "$0",
TargetTVL: "$10M",
ProgressPercent: 0,
TotalMembers: 0,
Roles: []models.RoleCount{},
},
})
return
}
progressPercent := 0.0
if team.TeamTargetTVLUSD > 0 {
progressPercent = (team.TeamTVLUSD / team.TeamTargetTVLUSD) * 100
}
response := models.TeamTVLResponse{
CurrentTVL: formatUSD(team.TeamTVLUSD),
TargetTVL: formatUSD(team.TeamTargetTVLUSD),
ProgressPercent: math.Round(progressPercent*100) / 100,
TotalMembers: team.TeamMembersCount,
Roles: []models.RoleCount{
{
Icon: "/icons/user/icon-whale.svg",
Label: "Whales",
Current: team.WhalesCount,
Target: team.WhalesTarget,
},
{
Icon: "/icons/user/icon-trader.svg",
Label: "Traders",
Current: team.TradersCount,
Target: team.TradersTarget,
},
{
Icon: "/icons/user/icon-user.svg",
Label: "Users",
Current: team.UsersCount,
Target: team.UsersTarget,
},
},
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": response,
})
}
// GetActivities returns user points activities
// GET /api/points/activities?wallet_address=0x...&type=all&page=1&pageSize=10
func GetActivities(c *gin.Context) {
db := common.GetDB()
activityType := c.DefaultQuery("type", "all")
pageStr := c.DefaultQuery("page", "1")
pageSizeStr := c.DefaultQuery("pageSize", "10")
page, _ := strconv.Atoi(pageStr)
pageSize, _ := strconv.Atoi(pageSizeStr)
offset := (page - 1) * pageSize
user, _ := getUserByWallet(c)
query := db.Model(&models.UserPointsRecord{}).Where("wallet_address = ?", user.WalletAddress)
if activityType == "referrals" {
query = query.Where("source_type = ?", "invitation")
} else if activityType == "deposits" {
query = query.Where("source_type IN ?", []string{"deposit", "holding"})
}
var total int64
query.Count(&total)
var records []models.UserPointsRecord
query.Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&records)
activities := make([]models.ActivityRecord, 0)
for _, record := range records {
activity := models.ActivityRecord{
Type: record.SourceType,
UserAddress: formatAddress(user.WalletAddress),
Points: record.PointsChange,
CreatedAt: record.CreatedAt,
}
if record.SourceType == "invitation" && record.SourceID != nil {
var invitation models.Invitation
if db.Where("id = ?", *record.SourceID).First(&invitation).Error == nil {
activity.FriendAddress = formatAddress(invitation.RefereeWallet)
activity.InviteCode = invitation.InviteCode
}
}
activities = append(activities, activity)
}
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
response := models.ActivitiesResponse{
Activities: activities,
Pagination: models.PaginationInfo{
Page: page,
PageSize: pageSize,
Total: int(total),
TotalPage: totalPages,
},
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": response,
})
}
// Helper function to format wallet address for display
func formatAddress(address string) string {
if len(address) < 10 {
return address
}
return address[:6] + "..." + address[len(address)-4:]
}
// Helper function to format USD values
func formatUSD(value float64) string {
if value >= 1000000 {
return fmt.Sprintf("$%.1fM", value/1000000)
} else if value >= 1000 {
return fmt.Sprintf("$%.1fK", value/1000)
}
return fmt.Sprintf("$%.2f", value)
}
// getUserByWallet looks up a user by wallet_address query param,
// falls back to demo user if not provided
func getUserByWallet(c *gin.Context) (models.User, bool) {
db := common.GetDB()
var user models.User
walletAddress := strings.ToLower(strings.TrimSpace(c.Query("wallet_address")))
if walletAddress != "" {
if err := db.Where("wallet_address = ?", walletAddress).First(&user).Error; err == nil {
return user, true
}
}
// Fall back to first demo user
err := db.Order("created_at ASC").First(&user).Error
return user, err == nil
}
// walletAddressToInt64 is no longer needed (no auth_user_id in new schema)
// kept for reference only
// WalletRegister creates or finds a user by wallet address
// POST /api/points/wallet-register
func WalletRegister(c *gin.Context) {
db := common.GetDB()
var req struct {
WalletAddress string `json:"wallet_address"`
}
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.WalletAddress) == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "wallet_address is required",
})
return
}
walletAddress := strings.ToLower(strings.TrimSpace(req.WalletAddress))
// Check if user already exists
var user models.User
err := db.Where("wallet_address = ?", walletAddress).First(&user).Error
if err != nil {
// Create new user with system-generated invite code
code := strings.ToUpper(common.RandString(8))
user = models.User{
WalletAddress: walletAddress,
InviteCode: code,
MemberTier: "Bronze",
VIPLevel: 1,
TotalPoints: 0,
}
if createErr := db.Create(&user).Error; createErr != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"error": "Failed to create user",
})
return
}
// Create invite_codes record
defaultExpiry := time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC)
inviteCodeRecord := models.InviteCode{
WalletAddress: walletAddress,
Code: code,
MaxUses: -1,
UsedCount: 0,
IsActive: true,
ExpiresAt: models.NullTime{Time: &defaultExpiry},
}
db.Create(&inviteCodeRecord)
}
// Get invite code from invite_codes table
var inviteCodeRecord models.InviteCode
inviteCode := user.InviteCode
usedCount := 0
if db.Where("wallet_address = ?", user.WalletAddress).First(&inviteCodeRecord).Error == nil {
inviteCode = inviteCodeRecord.Code
usedCount = inviteCodeRecord.UsedCount
}
globalRank := 0
if user.GlobalRank != nil {
globalRank = *user.GlobalRank
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"walletAddress": user.WalletAddress,
"inviteCode": inviteCode,
"usedCount": usedCount,
"memberTier": user.MemberTier,
"vipLevel": user.VIPLevel,
"totalPoints": user.TotalPoints,
"globalRank": globalRank,
},
})
}