更新
This commit is contained in:
@@ -1,525 +0,0 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user