546 lines
17 KiB
Go
546 lines
17 KiB
Go
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 []struct {
|
||
Amount float64
|
||
UserID string
|
||
}
|
||
db.Model(&model.Order{}).
|
||
Select("amount, user_id").
|
||
Where("referrer_id = ? AND status = 'paid'", userId).
|
||
Find(&paidOrders)
|
||
|
||
totalAmount := 0.0
|
||
uniqueUsers := make(map[string]bool)
|
||
for _, order := range paidOrders {
|
||
totalAmount += order.Amount
|
||
uniqueUsers[order.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 _, e := range earningsDetailsList {
|
||
var buyer model.User
|
||
db.Where("id = ?", e.UserID).First(&buyer)
|
||
|
||
commission := e.Amount * distributorShare
|
||
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 := totalAmount * distributorShare
|
||
estimatedEarnings := totalAmount * distributorShare
|
||
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()
|
||
distributorShare := 0.9
|
||
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["distributorShare"] != nil {
|
||
if share, ok := config["distributorShare"].(float64); ok {
|
||
distributorShare = share / 100
|
||
}
|
||
}
|
||
}
|
||
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 []struct {
|
||
Amount float64
|
||
}
|
||
db.Model(&model.Order{}).
|
||
Select("amount").
|
||
Where("referrer_id = ? AND status = 'paid'", userId).
|
||
Find(&paidOrders)
|
||
totalAmount := 0.0
|
||
for _, o := range paidOrders {
|
||
totalAmount += o.Amount
|
||
}
|
||
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)
|
||
totalCommission := totalAmount * distributorShare
|
||
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
|
||
}
|