更新管理端用户详情弹窗,新增 VIP 手动设置功能,支持到期日、展示名、项目、联系方式和简介的编辑。优化 VIP 相关接口,确保用户状态和资料更新功能正常,增强用户体验。调整文档,明确 VIP 设置的必填项和格式要求。

This commit is contained in:
Alex-larget
2026-02-26 18:03:01 +08:00
parent ab27acdb21
commit a5e2cfaa61
40 changed files with 1520 additions and 993 deletions

View File

@@ -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
}

View File

@@ -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").

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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

View 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
}

View File

@@ -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,

View 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": "不支持的请求方法"})
}

View File

@@ -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)

View File

@@ -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"`

View 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" }

View File

@@ -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)
}