package handler import ( "encoding/json" "fmt" "math" "net/http" "time" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) const defaultBindingDays = 30 // ReferralBind POST /api/referral/bind 推荐码绑定(新绑定/续期/切换) func ReferralBind(c *gin.Context) { var req struct { UserID string `json:"userId"` ReferralCode string `json:"referralCode" binding:"required"` OpenID string `json:"openId"` Source string `json:"source"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "用户ID和推荐码不能为空"}) return } effectiveUserID := req.UserID if effectiveUserID == "" && req.OpenID != "" { effectiveUserID = "user_" + req.OpenID[len(req.OpenID)-8:] } if effectiveUserID == "" || req.ReferralCode == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "用户ID和推荐码不能为空"}) return } db := database.DB() bindingDays := defaultBindingDays var cfg model.SystemConfig if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil { var config map[string]interface{} if _ = json.Unmarshal(cfg.ConfigValue, &config); config["bindingDays"] != nil { if v, ok := config["bindingDays"].(float64); ok { bindingDays = int(v) } } } var referrer model.User if err := db.Where("referral_code = ?", req.ReferralCode).First(&referrer).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "推荐码无效"}) return } if referrer.ID == effectiveUserID { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "不能使用自己的推荐码"}) return } var user model.User if err := db.Where("id = ?", effectiveUserID).First(&user).Error; err != nil { if req.OpenID != "" { if err := db.Where("open_id = ?", req.OpenID).First(&user).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "用户不存在"}) return } } else { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "用户不存在"}) return } } expiryDate := time.Now().AddDate(0, 0, bindingDays) var existing model.ReferralBinding err := db.Where("referee_id = ? AND status = ?", user.ID, "active").Order("binding_date DESC").First(&existing).Error action := "new" var oldReferrerID interface{} if err == nil { if existing.ReferrerID == referrer.ID { action = "renew" db.Model(&existing).Updates(map[string]interface{}{ "expiry_date": expiryDate, "binding_date": time.Now(), }) } else { action = "switch" oldReferrerID = existing.ReferrerID db.Model(&existing).Update("status", "cancelled") bindID := fmt.Sprintf("bind_%d_%s", time.Now().UnixNano(), randomStr(6)) db.Create(&model.ReferralBinding{ ID: bindID, ReferrerID: referrer.ID, RefereeID: user.ID, ReferralCode: req.ReferralCode, Status: refString("active"), ExpiryDate: expiryDate, BindingDate: time.Now(), }) } } else { bindID := fmt.Sprintf("bind_%d_%s", time.Now().UnixNano(), randomStr(6)) db.Create(&model.ReferralBinding{ ID: bindID, ReferrerID: referrer.ID, RefereeID: user.ID, ReferralCode: req.ReferralCode, Status: refString("active"), ExpiryDate: expiryDate, BindingDate: time.Now(), }) db.Model(&model.User{}).Where("id = ?", referrer.ID).UpdateColumn("referral_count", gorm.Expr("COALESCE(referral_count, 0) + 1")) } msg := "绑定成功" if action == "renew" { msg = "绑定已续期" } else if action == "switch" { msg = "推荐人已切换" } c.JSON(http.StatusOK, gin.H{ "success": true, "message": msg, "data": gin.H{ "action": action, "referrer": gin.H{"id": referrer.ID, "nickname": getStringValue(referrer.Nickname)}, "expiryDate": expiryDate, "bindingDays": bindingDays, "oldReferrerId": oldReferrerID, }, }) } func refString(s string) *string { return &s } func randomStr(n int) string { const letters = "abcdefghijklmnopqrstuvwxyz0123456789" b := make([]byte, n) for i := range b { b[i] = letters[time.Now().UnixNano()%int64(len(letters))] } return string(b) } // ReferralData GET /api/referral/data 获取分销数据统计 func ReferralData(c *gin.Context) { userId := c.Query("userId") if userId == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "用户ID不能为空"}) return } db := database.DB() // 获取分销配置(与 soul-admin 推广设置一致) distributorShare := 0.9 minWithdrawAmount := 10.0 bindingDays := defaultBindingDays userDiscount := 5 withdrawFee := 5.0 var cfg model.SystemConfig if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil { var config map[string]interface{} if err := json.Unmarshal(cfg.ConfigValue, &config); err == nil { if share, ok := config["distributorShare"].(float64); ok { distributorShare = share / 100 } if minAmount, ok := config["minWithdrawAmount"].(float64); ok { minWithdrawAmount = minAmount } if days, ok := config["bindingDays"].(float64); ok && days > 0 { bindingDays = int(days) } if discount, ok := config["userDiscount"].(float64); ok { userDiscount = int(discount) } if fee, ok := config["withdrawFee"].(float64); ok { withdrawFee = fee } } } // 1. 查询用户基本信息 var user model.User if err := db.Where("id = ?", userId).First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "用户不存在"}) return } // 2. 绑定统计 var totalBindings int64 db.Model(&model.ReferralBinding{}).Where("referrer_id = ?", userId).Count(&totalBindings) var activeBindings int64 db.Model(&model.ReferralBinding{}).Where( "referrer_id = ? AND status = 'active' AND expiry_date > ?", userId, time.Now(), ).Count(&activeBindings) var convertedBindings int64 db.Model(&model.ReferralBinding{}).Where( "referrer_id = ? AND status = 'active' AND purchase_count > 0", userId, ).Count(&convertedBindings) var expiredBindings int64 db.Model(&model.ReferralBinding{}).Where( "referrer_id = ? AND (status IN ('expired', 'cancelled') OR (status = 'active' AND expiry_date <= ?))", userId, time.Now(), ).Count(&expiredBindings) // 3. 付款统计 var paidOrders []model.Order db.Where("referrer_id = ? AND status = ?", userId, "paid").Find(&paidOrders) totalAmount := 0.0 totalCommission := 0.0 uniqueUsers := make(map[string]bool) for i := range paidOrders { totalAmount += paidOrders[i].Amount totalCommission += computeOrderCommission(db, &paidOrders[i], nil) uniqueUsers[paidOrders[i].UserID] = true } uniquePaidCount := len(uniqueUsers) // 4. 访问统计 totalVisits := int(totalBindings) var visitCount int64 if err := db.Model(&model.ReferralVisit{}). Select("COUNT(DISTINCT visitor_id) as count"). Where("referrer_id = ?", userId). Count(&visitCount).Error; err == nil { totalVisits = int(visitCount) } // 5. 提现统计(与小程序可提现逻辑一致:可提现 = 累计佣金 - 已提现 - 待审核) // 待审核 = pending + processing + pending_confirm,与 /api/withdraw/pending-confirm 口径一致 var pendingWithdraw struct{ Total float64 } db.Model(&model.Withdrawal{}). Select("COALESCE(SUM(amount), 0) as total"). Where("user_id = ? AND status IN ?", userId, []string{"pending", "processing", "pending_confirm"}). Scan(&pendingWithdraw) var successWithdraw struct{ Total float64 } db.Model(&model.Withdrawal{}). Select("COALESCE(SUM(amount), 0) as total"). Where("user_id = ? AND status = ?", userId, "success"). Scan(&successWithdraw) pendingWithdrawAmount := pendingWithdraw.Total withdrawnFromTable := successWithdraw.Total // 6. 获取活跃绑定用户列表 var activeBindingsList []model.ReferralBinding db.Where("referrer_id = ? AND status = 'active' AND expiry_date > ?", userId, time.Now()). Order("binding_date DESC"). Limit(20). Find(&activeBindingsList) activeUsers := []gin.H{} for _, b := range activeBindingsList { var referee model.User db.Where("id = ?", b.RefereeID).First(&referee) daysRemaining := int(time.Until(b.ExpiryDate).Hours() / 24) if daysRemaining < 0 { daysRemaining = 0 } activeUsers = append(activeUsers, gin.H{ "id": b.RefereeID, "nickname": getStringValue(referee.Nickname), "avatar": getStringValue(referee.Avatar), "daysRemaining": daysRemaining, "hasFullBook": getBoolValue(referee.HasFullBook), "bindingDate": b.BindingDate, "status": "active", }) } // 7. 获取已转化用户列表 var convertedBindingsList []model.ReferralBinding db.Where("referrer_id = ? AND status = 'active' AND purchase_count > 0", userId). Order("last_purchase_date DESC"). Limit(20). Find(&convertedBindingsList) convertedUsers := []gin.H{} for _, b := range convertedBindingsList { var referee model.User db.Where("id = ?", b.RefereeID).First(&referee) commission := 0.0 if b.TotalCommission != nil { commission = *b.TotalCommission } orderAmount := commission / distributorShare convertedUsers = append(convertedUsers, gin.H{ "id": b.RefereeID, "nickname": getStringValue(referee.Nickname), "avatar": getStringValue(referee.Avatar), "commission": commission, "orderAmount": orderAmount, "purchaseCount": getIntValue(b.PurchaseCount), "conversionDate": b.LastPurchaseDate, "status": "converted", }) } // 8. 获取已过期用户列表 var expiredBindingsList []model.ReferralBinding db.Where( "referrer_id = ? AND (status = 'expired' OR (status = 'active' AND expiry_date <= ?))", userId, time.Now(), ).Order("expiry_date DESC").Limit(20).Find(&expiredBindingsList) expiredUsers := []gin.H{} for _, b := range expiredBindingsList { var referee model.User db.Where("id = ?", b.RefereeID).First(&referee) expiredUsers = append(expiredUsers, gin.H{ "id": b.RefereeID, "nickname": getStringValue(referee.Nickname), "avatar": getStringValue(referee.Avatar), "bindingDate": b.BindingDate, "expiryDate": b.ExpiryDate, "status": "expired", }) } // 9. 获取收益明细 var earningsDetailsList []model.Order db.Where("referrer_id = ? AND status = 'paid'", userId). Order("pay_time DESC"). Limit(20). Find(&earningsDetailsList) earningsDetails := []gin.H{} for i := range earningsDetailsList { e := &earningsDetailsList[i] var buyer model.User db.Where("id = ?", e.UserID).First(&buyer) commission := computeOrderCommission(db, e, nil) earningsDetails = append(earningsDetails, gin.H{ "id": e.ID, "orderSn": e.OrderSN, "amount": e.Amount, "commission": commission, "productType": e.ProductType, "productId": getStringValue(e.ProductID), "description": getStringValue(e.Description), "buyerNickname": getStringValue(buyer.Nickname), "buyerAvatar": getStringValue(buyer.Avatar), "payTime": e.PayTime, }) } // 计算收益(totalCommission 已按订单逐条计算) estimatedEarnings := totalCommission availableEarnings := totalCommission - withdrawnFromTable - pendingWithdrawAmount if availableEarnings < 0 { availableEarnings = 0 } // 计算即将过期用户数(7天内) sevenDaysLater := time.Now().Add(7 * 24 * time.Hour) expiringCount := 0 for _, b := range activeBindingsList { if b.ExpiryDate.After(time.Now()) && b.ExpiryDate.Before(sevenDaysLater) { expiringCount++ } } c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ // 核心可见数据 "bindingCount": activeBindings, "visitCount": totalVisits, "paidCount": uniquePaidCount, "expiredCount": expiredBindings, // 收益数据 "totalCommission": round(totalCommission, 2), "availableEarnings": round(availableEarnings, 2), "pendingWithdrawAmount": round(pendingWithdrawAmount, 2), "withdrawnEarnings": withdrawnFromTable, "earnings": getFloatValue(user.Earnings), "pendingEarnings": getFloatValue(user.PendingEarnings), "estimatedEarnings": round(estimatedEarnings, 2), "shareRate": int(distributorShare * 100), "minWithdrawAmount": minWithdrawAmount, "bindingDays": bindingDays, "userDiscount": userDiscount, "withdrawFee": withdrawFee, // 推荐码 "referralCode": getStringValue(user.ReferralCode), "referralCount": getIntValue(user.ReferralCount), // 详细统计 "stats": gin.H{ "totalBindings": totalBindings, "activeBindings": activeBindings, "convertedBindings": convertedBindings, "expiredBindings": expiredBindings, "expiringCount": expiringCount, "totalPaymentAmount": totalAmount, }, // 用户列表 "activeUsers": activeUsers, "convertedUsers": convertedUsers, "expiredUsers": expiredUsers, // 收益明细 "earningsDetails": earningsDetails, }, }) } // round 四舍五入保留小数 func round(val float64, precision int) float64 { ratio := math.Pow(10, float64(precision)) return math.Round(val*ratio) / ratio } // MyEarnings GET /api/miniprogram/earnings 仅返回「我的收益」卡片所需数据(累计、可提现、推荐人数),用于我的页展示与刷新 func MyEarnings(c *gin.Context) { userId := c.Query("userId") if userId == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "用户ID不能为空"}) return } db := database.DB() var user model.User if err := db.Where("id = ?", userId).First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "用户不存在"}) return } var paidOrders []model.Order db.Where("referrer_id = ? AND status = ?", userId, "paid").Find(&paidOrders) totalCommission := 0.0 for i := range paidOrders { totalCommission += computeOrderCommission(db, &paidOrders[i], nil) } var pendingWithdraw struct{ Total float64 } db.Model(&model.Withdrawal{}). Select("COALESCE(SUM(amount), 0) as total"). Where("user_id = ? AND status IN ?", userId, []string{"pending", "processing", "pending_confirm"}). Scan(&pendingWithdraw) var successWithdraw struct{ Total float64 } db.Model(&model.Withdrawal{}). Select("COALESCE(SUM(amount), 0) as total"). Where("user_id = ? AND status = ?", userId, "success"). Scan(&successWithdraw) pendingWithdrawAmount := pendingWithdraw.Total withdrawnFromTable := successWithdraw.Total availableEarnings := totalCommission - withdrawnFromTable - pendingWithdrawAmount if availableEarnings < 0 { availableEarnings = 0 } c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "totalCommission": round(totalCommission, 2), "availableEarnings": round(availableEarnings, 2), "referralCount": getIntValue(user.ReferralCount), }, }) } // ReferralVisit POST /api/referral/visit 记录推荐访问(不需登录) func ReferralVisit(c *gin.Context) { var req struct { ReferralCode string `json:"referralCode" binding:"required"` VisitorOpenID string `json:"visitorOpenId"` VisitorID string `json:"visitorId"` Source string `json:"source"` Page string `json:"page"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "推荐码不能为空"}) return } db := database.DB() var referrer model.User if err := db.Where("referral_code = ?", req.ReferralCode).First(&referrer).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "推荐码无效"}) return } source := req.Source if source == "" { source = "miniprogram" } visitorID := req.VisitorID if visitorID == "" { visitorID = "" } vOpenID := req.VisitorOpenID vPage := req.Page err := db.Create(&model.ReferralVisit{ ReferrerID: referrer.ID, VisitorID: strPtrOrNil(visitorID), VisitorOpenID: strPtrOrNil(vOpenID), Source: strPtrOrNil(source), Page: strPtrOrNil(vPage), }).Error if err != nil { c.JSON(http.StatusOK, gin.H{"success": true, "message": "已处理"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "访问已记录"}) } func strPtrOrNil(s string) *string { if s == "" { return nil } return &s }