更新管理端用户详情弹窗,新增 VIP 手动设置功能,支持到期日、展示名、项目、联系方式和简介的编辑。优化 VIP 相关接口,确保用户状态和资料更新功能正常,增强用户体验。调整文档,明确 VIP 设置的必填项和格式要求。
This commit is contained in:
@@ -30,6 +30,9 @@ func Init(dsn string) error {
|
||||
if err := db.AutoMigrate(&model.UserAddress{}); err != nil {
|
||||
log.Printf("database: user_addresses migrate warning: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.VipRole{}); err != nil {
|
||||
log.Printf("database: vip_roles migrate warning: %v", err)
|
||||
}
|
||||
log.Println("database: connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -278,12 +278,14 @@ func AdminSettingsPost(c *gin.Context) {
|
||||
func AdminReferralSettingsGet(c *gin.Context) {
|
||||
db := database.DB()
|
||||
defaultConfig := gin.H{
|
||||
"distributorShare": float64(90),
|
||||
"distributorShare": float64(90),
|
||||
"minWithdrawAmount": float64(10),
|
||||
"bindingDays": float64(30),
|
||||
"userDiscount": float64(5),
|
||||
"withdrawFee": float64(5),
|
||||
"enableAutoWithdraw": false,
|
||||
"vipOrderShareVip": float64(20),
|
||||
"vipOrderShareNonVip": float64(10),
|
||||
}
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&row).Error; err != nil {
|
||||
@@ -301,24 +303,36 @@ func AdminReferralSettingsGet(c *gin.Context) {
|
||||
// AdminReferralSettingsPost POST /api/admin/referral-settings 推广设置页专用:仅保存 referral_config(请求体为完整配置对象)
|
||||
func AdminReferralSettingsPost(c *gin.Context) {
|
||||
var body struct {
|
||||
DistributorShare float64 `json:"distributorShare"`
|
||||
MinWithdrawAmount float64 `json:"minWithdrawAmount"`
|
||||
BindingDays float64 `json:"bindingDays"`
|
||||
UserDiscount float64 `json:"userDiscount"`
|
||||
WithdrawFee float64 `json:"withdrawFee"`
|
||||
EnableAutoWithdraw bool `json:"enableAutoWithdraw"`
|
||||
DistributorShare float64 `json:"distributorShare"`
|
||||
MinWithdrawAmount float64 `json:"minWithdrawAmount"`
|
||||
BindingDays float64 `json:"bindingDays"`
|
||||
UserDiscount float64 `json:"userDiscount"`
|
||||
WithdrawFee float64 `json:"withdrawFee"`
|
||||
EnableAutoWithdraw bool `json:"enableAutoWithdraw"`
|
||||
VipOrderShareVip float64 `json:"vipOrderShareVip"`
|
||||
VipOrderShareNonVip float64 `json:"vipOrderShareNonVip"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
vipOrderShareVip := body.VipOrderShareVip
|
||||
if vipOrderShareVip == 0 {
|
||||
vipOrderShareVip = 20
|
||||
}
|
||||
vipOrderShareNonVip := body.VipOrderShareNonVip
|
||||
if vipOrderShareNonVip == 0 {
|
||||
vipOrderShareNonVip = 10
|
||||
}
|
||||
val := gin.H{
|
||||
"distributorShare": body.DistributorShare,
|
||||
"minWithdrawAmount": body.MinWithdrawAmount,
|
||||
"bindingDays": body.BindingDays,
|
||||
"userDiscount": body.UserDiscount,
|
||||
"withdrawFee": body.WithdrawFee,
|
||||
"distributorShare": body.DistributorShare,
|
||||
"minWithdrawAmount": body.MinWithdrawAmount,
|
||||
"bindingDays": body.BindingDays,
|
||||
"userDiscount": body.UserDiscount,
|
||||
"withdrawFee": body.WithdrawFee,
|
||||
"enableAutoWithdraw": body.EnableAutoWithdraw,
|
||||
"vipOrderShareVip": vipOrderShareVip,
|
||||
"vipOrderShareNonVip": vipOrderShareNonVip,
|
||||
}
|
||||
valBytes, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
@@ -456,18 +470,6 @@ func DBUsersList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 读取推广配置中的分销比例
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userIDs := make([]string, 0, len(users))
|
||||
for _, u := range users {
|
||||
userIDs = append(userIDs, u.ID)
|
||||
@@ -494,17 +496,15 @@ func DBUsersList(c *gin.Context) {
|
||||
sectionCountMap[r.UserID] = int(r.Count)
|
||||
}
|
||||
|
||||
// 2. 分销收益:从 referrer 订单计算佣金;可提现 = 累计佣金 - 已提现 - 待处理提现
|
||||
// 2. 分销收益:从 referrer 订单逐条 computeOrderCommission 求和(会员订单 20%/10%,内容订单 90%)
|
||||
referrerEarningsMap := make(map[string]float64)
|
||||
var referrerRows []struct {
|
||||
ReferrerID string
|
||||
Total float64
|
||||
}
|
||||
db.Model(&model.Order{}).Select("referrer_id, COALESCE(SUM(amount), 0) as total").
|
||||
Where("referrer_id IS NOT NULL AND referrer_id != '' AND status = ?", "paid").
|
||||
Group("referrer_id").Find(&referrerRows)
|
||||
for _, r := range referrerRows {
|
||||
referrerEarningsMap[r.ReferrerID] = r.Total * distributorShare
|
||||
var referrerOrders []model.Order
|
||||
db.Where("referrer_id IS NOT NULL AND referrer_id != '' AND status = ?", "paid").Find(&referrerOrders)
|
||||
for i := range referrerOrders {
|
||||
rid := referrerOrders[i].ReferrerID
|
||||
if rid != nil && *rid != "" {
|
||||
referrerEarningsMap[*rid] += computeOrderCommission(db, &referrerOrders[i], nil)
|
||||
}
|
||||
}
|
||||
withdrawnMap := make(map[string]float64)
|
||||
var withdrawnRows []struct {
|
||||
@@ -625,6 +625,8 @@ func DBUsersAction(c *gin.Context) {
|
||||
PendingEarnings *float64 `json:"pendingEarnings"`
|
||||
IsVip *bool `json:"isVip"`
|
||||
VipExpireDate *string `json:"vipExpireDate"` // "2026-12-31" 或 "2026-12-31 23:59:59"
|
||||
VipSort *int `json:"vipSort"` // 手动排序,越小越前
|
||||
VipRole *string `json:"vipRole"` // 角色:从 vip_roles 选或手动填写
|
||||
VipName *string `json:"vipName"`
|
||||
VipAvatar *string `json:"vipAvatar"`
|
||||
VipProject *string `json:"vipProject"`
|
||||
@@ -675,6 +677,12 @@ func DBUsersAction(c *gin.Context) {
|
||||
}
|
||||
if body.IsVip != nil {
|
||||
updates["is_vip"] = *body.IsVip
|
||||
if *body.IsVip {
|
||||
now := time.Now()
|
||||
updates["vip_activated_at"] = now // 手动设置时与付款一致:按时间排序,最新在前
|
||||
} else {
|
||||
updates["vip_activated_at"] = nil
|
||||
}
|
||||
}
|
||||
if body.VipExpireDate != nil {
|
||||
if *body.VipExpireDate == "" {
|
||||
@@ -687,6 +695,17 @@ func DBUsersAction(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if body.VipSort != nil {
|
||||
updates["vip_sort"] = *body.VipSort
|
||||
}
|
||||
if body.VipRole != nil {
|
||||
s := strings.TrimSpace(*body.VipRole)
|
||||
if s == "" {
|
||||
updates["vip_role"] = nil
|
||||
} else {
|
||||
updates["vip_role"] = s
|
||||
}
|
||||
}
|
||||
if body.VipName != nil {
|
||||
updates["vip_name"] = *body.VipName
|
||||
}
|
||||
@@ -752,18 +771,6 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
// 分销比例(与小程序 /api/miniprogram/earnings、支付回调一致)
|
||||
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 bindings []model.ReferralBinding
|
||||
if err := db.Where("referrer_id = ?", userId).Order("binding_date DESC").Find(&bindings).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "referrals": []interface{}{}, "stats": gin.H{"total": 0, "purchased": 0, "free": 0, "earnings": 0, "pendingEarnings": 0, "withdrawnEarnings": 0}})
|
||||
@@ -817,12 +824,13 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// 累计收益、待提现:与小程序 MyEarnings 一致,从订单+提现表实时计算
|
||||
var orderSum struct{ Total float64 }
|
||||
db.Model(&model.Order{}).Select("COALESCE(SUM(amount), 0) as total").
|
||||
Where("referrer_id = ? AND status = ?", userId, "paid").
|
||||
Scan(&orderSum)
|
||||
earningsE := orderSum.Total * distributorShare
|
||||
// 累计收益、待提现:与小程序 MyEarnings 一致,从订单逐条 computeOrderCommission 求和
|
||||
var refOrders []model.Order
|
||||
db.Where("referrer_id = ? AND status = ?", userId, "paid").Find(&refOrders)
|
||||
earningsE := 0.0
|
||||
for i := range refOrders {
|
||||
earningsE += computeOrderCommission(db, &refOrders[i], nil)
|
||||
}
|
||||
|
||||
var withdrawnSum struct{ Total float64 }
|
||||
db.Model(&model.Withdrawal{}).Select("COALESCE(SUM(amount), 0) as total").
|
||||
|
||||
@@ -457,13 +457,18 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true)
|
||||
fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID)
|
||||
} else if attach.ProductType == "vip" {
|
||||
// VIP 支付成功:更新 users.is_vip、vip_expire_date(与 next-project 一致)
|
||||
// VIP 支付成功:更新 users.is_vip、vip_expire_date、vip_activated_at(排序:后付款在前)
|
||||
expireDate := time.Now().AddDate(0, 0, 365)
|
||||
vipActivatedAt := time.Now()
|
||||
if order.PayTime != nil {
|
||||
vipActivatedAt = *order.PayTime
|
||||
}
|
||||
db.Model(&model.User{}).Where("id = ?", buyerUserID).Updates(map[string]interface{}{
|
||||
"is_vip": true,
|
||||
"vip_expire_date": expireDate,
|
||||
"is_vip": true,
|
||||
"vip_expire_date": expireDate,
|
||||
"vip_activated_at": vipActivatedAt,
|
||||
})
|
||||
fmt.Printf("[VIP] 设置方式=支付设置, userId=%s, orderSn=%s, 过期日=%s\n", buyerUserID, orderSn, expireDate.Format("2006-01-02"))
|
||||
fmt.Printf("[VIP] 设置方式=支付设置, userId=%s, orderSn=%s, 过期日=%s, activatedAt=%s\n", buyerUserID, orderSn, expireDate.Format("2006-01-02"), vipActivatedAt.Format("2006-01-02 15:04:05"))
|
||||
} else if attach.ProductType == "match" {
|
||||
fmt.Printf("[PayNotify] 用户购买匹配次数: %s,订单 %s\n", buyerUserID, orderSn)
|
||||
} else if attach.ProductType == "section" && attach.ProductID != "" {
|
||||
@@ -505,33 +510,8 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
|
||||
// 处理分销佣金
|
||||
// amount 为实付金额(若有好友优惠则已打折);order 用于判断是否有推荐人从而反推原价
|
||||
// 处理分销佣金(会员订单 20%/10%,内容订单 90%)
|
||||
func processReferralCommission(db *gorm.DB, buyerUserID string, amount float64, orderSn string, order *model.Order) {
|
||||
// 获取分成配置,默认 90%;好友优惠用于反推原价(佣金按原价计算)
|
||||
distributorShare := 0.9
|
||||
userDiscount := 0.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 disc, ok := config["userDiscount"].(float64); ok {
|
||||
userDiscount = disc / 100
|
||||
}
|
||||
}
|
||||
}
|
||||
// 佣金按原价计算:若有推荐人则实付已打折,反推原价 = amount / (1 - userDiscount)
|
||||
commissionBase := amount
|
||||
if order != nil && userDiscount > 0 && (order.ReferrerID != nil && *order.ReferrerID != "" || order.ReferralCode != nil && *order.ReferralCode != "") {
|
||||
if (1 - userDiscount) > 0 {
|
||||
commissionBase = amount / (1 - userDiscount)
|
||||
}
|
||||
}
|
||||
|
||||
// 查找有效推广绑定
|
||||
type Binding struct {
|
||||
ID int `gorm:"column:id"`
|
||||
ReferrerID string `gorm:"column:referrer_id"`
|
||||
@@ -539,7 +519,6 @@ func processReferralCommission(db *gorm.DB, buyerUserID string, amount float64,
|
||||
PurchaseCount int `gorm:"column:purchase_count"`
|
||||
TotalCommission float64 `gorm:"column:total_commission"`
|
||||
}
|
||||
|
||||
var binding Binding
|
||||
err := db.Raw(`
|
||||
SELECT id, referrer_id, expiry_date, purchase_count, total_commission
|
||||
@@ -548,31 +527,35 @@ func processReferralCommission(db *gorm.DB, buyerUserID string, amount float64,
|
||||
ORDER BY binding_date DESC
|
||||
LIMIT 1
|
||||
`, buyerUserID).Scan(&binding).Error
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("[PayNotify] 用户无有效推广绑定,跳过分佣: %s\n", buyerUserID)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if time.Now().After(binding.ExpiryDate) {
|
||||
fmt.Printf("[PayNotify] 绑定已过期,跳过分佣: %s\n", buyerUserID)
|
||||
return
|
||||
}
|
||||
|
||||
// 计算佣金(按原价)
|
||||
commission := commissionBase * distributorShare
|
||||
// 确保 order 有 referrer_id(补记订单可能缺失)
|
||||
if order != nil && (order.ReferrerID == nil || *order.ReferrerID == "") {
|
||||
order.ReferrerID = &binding.ReferrerID
|
||||
db.Model(order).Update("referrer_id", binding.ReferrerID)
|
||||
}
|
||||
// 构建用于计算的 order(若为 nil 则用 binding 信息)
|
||||
calcOrder := order
|
||||
if calcOrder == nil {
|
||||
calcOrder = &model.Order{Amount: amount, ProductType: "unknown", ReferrerID: &binding.ReferrerID}
|
||||
}
|
||||
commission := computeOrderCommission(db, calcOrder, nil)
|
||||
if commission <= 0 {
|
||||
fmt.Printf("[PayNotify] 佣金为 0,跳过分佣: orderSn=%s\n", orderSn)
|
||||
return
|
||||
}
|
||||
newPurchaseCount := binding.PurchaseCount + 1
|
||||
newTotalCommission := binding.TotalCommission + commission
|
||||
|
||||
fmt.Printf("[PayNotify] 处理分佣: referrerId=%s, amount=%.2f, commission=%.2f, shareRate=%.0f%%\n",
|
||||
binding.ReferrerID, amount, commission, distributorShare*100)
|
||||
|
||||
// 更新推广者的待结算收益
|
||||
fmt.Printf("[PayNotify] 处理分佣: referrerId=%s, amount=%.2f, commission=%.2f\n",
|
||||
binding.ReferrerID, amount, commission)
|
||||
db.Model(&model.User{}).Where("id = ?", binding.ReferrerID).
|
||||
Update("pending_earnings", db.Raw("pending_earnings + ?", commission))
|
||||
|
||||
// 更新绑定记录(COALESCE 避免 total_commission 为 NULL 时 NULL+?=NULL)
|
||||
db.Exec(`
|
||||
UPDATE referral_bindings
|
||||
SET last_purchase_date = NOW(),
|
||||
@@ -580,7 +563,6 @@ func processReferralCommission(db *gorm.DB, buyerUserID string, amount float64,
|
||||
total_commission = COALESCE(total_commission, 0) + ?
|
||||
WHERE id = ?
|
||||
`, commission, binding.ID)
|
||||
|
||||
fmt.Printf("[PayNotify] 分佣完成: 推广者 %s 获得 %.2f 元(第 %d 次购买,累计 %.2f 元)\n",
|
||||
binding.ReferrerID, commission, newPurchaseCount, newTotalCommission)
|
||||
}
|
||||
|
||||
@@ -85,18 +85,6 @@ func OrdersList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 分销比例(与支付回调一致)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收集订单中的 user_id、referrer_id,查用户信息
|
||||
userIDs := make(map[string]bool)
|
||||
for _, o := range orders {
|
||||
@@ -150,10 +138,14 @@ func OrdersList(c *gin.Context) {
|
||||
m["referrerCode"] = getStr(u.ReferralCode)
|
||||
}
|
||||
}
|
||||
// 分销佣金:仅对已支付且存在推荐人的订单,按配置比例计算(与支付回调口径一致)
|
||||
// 分销佣金:仅对已支付且存在推荐人的订单,按 computeOrderCommission(会员 20%/10%,内容 90%)
|
||||
status := getStr(o.Status)
|
||||
if status == "paid" && o.ReferrerID != nil && *o.ReferrerID != "" {
|
||||
m["referrerEarnings"] = o.Amount * distributorShare
|
||||
var refUser *model.User
|
||||
if u := userMap[*o.ReferrerID]; u != nil {
|
||||
refUser = u
|
||||
}
|
||||
m["referrerEarnings"] = computeOrderCommission(db, &o, refUser)
|
||||
} else {
|
||||
m["referrerEarnings"] = nil
|
||||
}
|
||||
|
||||
@@ -212,20 +212,16 @@ func ReferralData(c *gin.Context) {
|
||||
).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)
|
||||
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 _, order := range paidOrders {
|
||||
totalAmount += order.Amount
|
||||
uniqueUsers[order.UserID] = true
|
||||
for i := range paidOrders {
|
||||
totalAmount += paidOrders[i].Amount
|
||||
totalCommission += computeOrderCommission(db, &paidOrders[i], nil)
|
||||
uniqueUsers[paidOrders[i].UserID] = true
|
||||
}
|
||||
uniquePaidCount := len(uniqueUsers)
|
||||
|
||||
@@ -344,11 +340,12 @@ func ReferralData(c *gin.Context) {
|
||||
Find(&earningsDetailsList)
|
||||
|
||||
earningsDetails := []gin.H{}
|
||||
for _, e := range earningsDetailsList {
|
||||
for i := range earningsDetailsList {
|
||||
e := &earningsDetailsList[i]
|
||||
var buyer model.User
|
||||
db.Where("id = ?", e.UserID).First(&buyer)
|
||||
|
||||
commission := e.Amount * distributorShare
|
||||
commission := computeOrderCommission(db, e, nil)
|
||||
earningsDetails = append(earningsDetails, gin.H{
|
||||
"id": e.ID,
|
||||
"orderSn": e.OrderSN,
|
||||
@@ -363,9 +360,8 @@ func ReferralData(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// 计算收益
|
||||
totalCommission := totalAmount * distributorShare
|
||||
estimatedEarnings := totalAmount * distributorShare
|
||||
// 计算收益(totalCommission 已按订单逐条计算)
|
||||
estimatedEarnings := totalCommission
|
||||
availableEarnings := totalCommission - withdrawnFromTable - pendingWithdrawAmount
|
||||
if availableEarnings < 0 {
|
||||
availableEarnings = 0
|
||||
@@ -442,31 +438,16 @@ func MyEarnings(c *gin.Context) {
|
||||
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 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{}).
|
||||
@@ -478,7 +459,6 @@ func MyEarnings(c *gin.Context) {
|
||||
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
|
||||
|
||||
69
soul-api/internal/handler/referral_commission.go
Normal file
69
soul-api/internal/handler/referral_commission.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// computeOrderCommission 按订单计算应付给推广者的佣金
|
||||
// 会员订单:推广者会员 20%、非会员 10%;内容订单:90%(好友优惠 5% 仅针对内容)
|
||||
// order: 已支付订单,需有 product_type、amount、referrer_id
|
||||
// referrerUser: 推广者用户信息,用于判断 is_vip(可为 nil,会查库)
|
||||
func computeOrderCommission(db *gorm.DB, order *model.Order, referrerUser *model.User) float64 {
|
||||
if order == nil || order.ReferrerID == nil || *order.ReferrerID == "" {
|
||||
return 0
|
||||
}
|
||||
// 读取推广配置
|
||||
distributorShare := 0.9
|
||||
userDiscount := 0.0
|
||||
vipOrderShareVip := 20.0
|
||||
vipOrderShareNonVip := 10.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 disc, ok := config["userDiscount"].(float64); ok {
|
||||
userDiscount = disc / 100
|
||||
}
|
||||
if v, ok := config["vipOrderShareVip"].(float64); ok {
|
||||
vipOrderShareVip = v / 100
|
||||
}
|
||||
if v, ok := config["vipOrderShareNonVip"].(float64); ok {
|
||||
vipOrderShareNonVip = v / 100
|
||||
}
|
||||
}
|
||||
}
|
||||
// 会员订单:无好友优惠,按推广者是否会员分 20%/10%
|
||||
if order.ProductType == "vip" {
|
||||
base := order.Amount
|
||||
var referrer model.User
|
||||
if referrerUser != nil {
|
||||
referrer = *referrerUser
|
||||
} else if err := db.Where("id = ?", *order.ReferrerID).First(&referrer).Error; err != nil {
|
||||
return 0
|
||||
}
|
||||
isVip := referrer.IsVip != nil && *referrer.IsVip
|
||||
if referrer.VipExpireDate != nil && referrer.VipExpireDate.Before(time.Now()) {
|
||||
isVip = false
|
||||
}
|
||||
if isVip {
|
||||
return base * vipOrderShareVip
|
||||
}
|
||||
return base * vipOrderShareNonVip
|
||||
}
|
||||
// 内容订单:若有推荐人且 userDiscount>0,反推原价;否则按实付
|
||||
commissionBase := order.Amount
|
||||
if userDiscount > 0 && (order.ReferrerID != nil && *order.ReferrerID != "" || (order.ReferralCode != nil && *order.ReferralCode != "")) {
|
||||
if (1 - userDiscount) > 0 {
|
||||
commissionBase = order.Amount / (1 - userDiscount)
|
||||
}
|
||||
}
|
||||
return commissionBase * distributorShare
|
||||
}
|
||||
@@ -255,12 +255,12 @@ func VipMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 列表:优先 users 表(is_vip=1 且 vip_expire_date>NOW)
|
||||
// 列表:优先 users 表(is_vip=1 且 vip_expire_date>NOW),排序:vip_sort 优先(小在前),否则 vip_activated_at DESC
|
||||
var users []model.User
|
||||
err := db.Table("users").
|
||||
Select("id", "nickname", "avatar", "vip_name", "vip_project", "vip_avatar", "vip_bio").
|
||||
Select("id", "nickname", "avatar", "vip_name", "vip_role", "vip_project", "vip_avatar", "vip_bio", "vip_activated_at", "vip_sort").
|
||||
Where("is_vip = 1 AND vip_expire_date > ?", time.Now()).
|
||||
Order("vip_expire_date DESC").
|
||||
Order("COALESCE(vip_sort, 999999) ASC, COALESCE(vip_activated_at, vip_expire_date) DESC").
|
||||
Limit(limit).
|
||||
Find(&users).Error
|
||||
|
||||
@@ -320,12 +320,17 @@ func formatVipMember(u *model.User, isVip bool) gin.H {
|
||||
if contact == "" && u.WechatID != nil {
|
||||
contact = *u.WechatID
|
||||
}
|
||||
vipRole := ""
|
||||
if u.VipRole != nil {
|
||||
vipRole = *u.VipRole
|
||||
}
|
||||
return gin.H{
|
||||
"id": u.ID,
|
||||
"name": name,
|
||||
"nickname": name,
|
||||
"avatar": avatar,
|
||||
"vip_name": name,
|
||||
"vip_role": vipRole,
|
||||
"vip_avatar": avatar,
|
||||
"vip_project": project,
|
||||
"vip_contact": contact,
|
||||
|
||||
90
soul-api/internal/handler/vip_roles.go
Normal file
90
soul-api/internal/handler/vip_roles.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DBVipRolesList GET /api/db/vip-roles 角色列表(管理端 Set VIP 下拉用)
|
||||
func DBVipRolesList(c *gin.Context) {
|
||||
db := database.DB()
|
||||
var roles []model.VipRole
|
||||
if err := db.Order("sort ASC, id ASC").Find(&roles).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": roles})
|
||||
}
|
||||
|
||||
// DBVipRolesAction POST /api/db/vip-roles 新增角色;PUT 更新;DELETE 删除
|
||||
func DBVipRolesAction(c *gin.Context) {
|
||||
db := database.DB()
|
||||
method := c.Request.Method
|
||||
|
||||
if method == "POST" {
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "name 不能为空"})
|
||||
return
|
||||
}
|
||||
role := model.VipRole{Name: body.Name, Sort: body.Sort}
|
||||
if err := db.Create(&role).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": role})
|
||||
return
|
||||
}
|
||||
|
||||
if method == "PUT" {
|
||||
var body struct {
|
||||
ID int `json:"id" binding:"required"`
|
||||
Name *string `json:"name"`
|
||||
Sort *int `json:"sort"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "id 不能为空"})
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
if body.Name != nil {
|
||||
updates["name"] = *body.Name
|
||||
}
|
||||
if body.Sort != nil {
|
||||
updates["sort"] = *body.Sort
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "没有需要更新的字段"})
|
||||
return
|
||||
}
|
||||
if err := db.Model(&model.VipRole{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "更新成功"})
|
||||
return
|
||||
}
|
||||
|
||||
if method == "DELETE" {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "id 不能为空"})
|
||||
return
|
||||
}
|
||||
if err := db.Where("id = ?", id).Delete(&model.VipRole{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "删除成功"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
||||
}
|
||||
@@ -16,26 +16,23 @@ import (
|
||||
)
|
||||
|
||||
// computeAvailableWithdraw 与小程序 / referral 页可提现逻辑一致:可提现 = 累计佣金 - 已提现 - 待审核
|
||||
// 用于 referral/data 展示与 withdraw 接口二次查库校验(不信任前端传参)
|
||||
// 佣金按订单逐条 computeOrderCommission 求和(会员订单 20%/10%,内容订单 90%)
|
||||
func computeAvailableWithdraw(db *gorm.DB, userID string) (available, totalCommission, withdrawn, pending float64, minAmount float64) {
|
||||
distributorShare := 0.9
|
||||
minAmount = 10
|
||||
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 != nil {
|
||||
if share, ok := config["distributorShare"].(float64); ok {
|
||||
distributorShare = share / 100
|
||||
}
|
||||
if m, ok := config["minWithdrawAmount"].(float64); ok {
|
||||
minAmount = m
|
||||
}
|
||||
}
|
||||
}
|
||||
var sumOrder struct{ Total float64 }
|
||||
db.Model(&model.Order{}).Where("referrer_id = ? AND status = ?", userID, "paid").
|
||||
Select("COALESCE(SUM(amount), 0) as total").Scan(&sumOrder)
|
||||
totalCommission = sumOrder.Total * distributorShare
|
||||
var orders []model.Order
|
||||
db.Where("referrer_id = ? AND status = ?", userID, "paid").Find(&orders)
|
||||
for i := range orders {
|
||||
totalCommission += computeOrderCommission(db, &orders[i], nil)
|
||||
}
|
||||
var w struct{ Total float64 }
|
||||
db.Model(&model.Withdrawal{}).Where("user_id = ? AND status = ?", userID, "success").
|
||||
Select("COALESCE(SUM(amount), 0)").Scan(&w)
|
||||
|
||||
@@ -24,9 +24,12 @@ type User struct {
|
||||
Source *string `gorm:"column:source;size:50" json:"source,omitempty"`
|
||||
|
||||
// VIP 相关(与 next-project 线上 users 表一致,支持手动设置;管理端需读写)
|
||||
IsVip *bool `gorm:"column:is_vip" json:"isVip,omitempty"`
|
||||
VipExpireDate *time.Time `gorm:"column:vip_expire_date" json:"vipExpireDate,omitempty"`
|
||||
VipName *string `gorm:"column:vip_name;size:100" json:"vipName,omitempty"`
|
||||
IsVip *bool `gorm:"column:is_vip" json:"isVip,omitempty"`
|
||||
VipExpireDate *time.Time `gorm:"column:vip_expire_date" json:"vipExpireDate,omitempty"`
|
||||
VipActivatedAt *time.Time `gorm:"column:vip_activated_at" json:"vipActivatedAt,omitempty"` // 成为 VIP 时间,排序用:付款=pay_time,手动=now
|
||||
VipSort *int `gorm:"column:vip_sort" json:"vipSort,omitempty"` // 手动排序,越小越前,NULL 按 vip_activated_at
|
||||
VipRole *string `gorm:"column:vip_role;size:50" json:"vipRole,omitempty"` // 角色:从 vip_roles 选或手动填写
|
||||
VipName *string `gorm:"column:vip_name;size:100" json:"vipName,omitempty"`
|
||||
VipAvatar *string `gorm:"column:vip_avatar;size:500" json:"vipAvatar,omitempty"`
|
||||
VipProject *string `gorm:"column:vip_project;size:200" json:"vipProject,omitempty"`
|
||||
VipContact *string `gorm:"column:vip_contact;size:100" json:"vipContact,omitempty"`
|
||||
|
||||
14
soul-api/internal/model/vip_role.go
Normal file
14
soul-api/internal/model/vip_role.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// VipRole 超级个体固定角色,用于 Set VIP 时下拉选择
|
||||
type VipRole struct {
|
||||
ID int `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;size:50;not null" json:"name"`
|
||||
Sort int `gorm:"column:sort;default:0" json:"sort"` // 下拉展示顺序,越小越前
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (VipRole) TableName() string { return "vip_roles" }
|
||||
@@ -130,6 +130,10 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
db.PUT("/users", handler.DBUsersAction)
|
||||
db.DELETE("/users", handler.DBUsersDelete)
|
||||
db.GET("/users/referrals", handler.DBUsersReferrals)
|
||||
db.GET("/vip-roles", handler.DBVipRolesList)
|
||||
db.POST("/vip-roles", handler.DBVipRolesAction)
|
||||
db.PUT("/vip-roles", handler.DBVipRolesAction)
|
||||
db.DELETE("/vip-roles", handler.DBVipRolesAction)
|
||||
db.GET("/match-records", handler.DBMatchRecordsList)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user