@@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"soul-api/internal/database"
@@ -336,16 +338,162 @@ func DBConfigPost(c *gin.Context) {
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "配置保存成功" } )
}
// DBUsersList GET /api/db/users
// DBUsersList GET /api/db/users(支持分页 page、pageSize, 可选搜索 search; 购买状态、分销收益、绑定人数从订单/绑定表实时计算)
func DBUsersList ( c * gin . Context ) {
db := database . DB ( )
page , _ := strconv . Atoi ( c . DefaultQuery ( "page" , "1" ) )
pageSize , _ := strconv . Atoi ( c . DefaultQuery ( "pageSize" , "15" ) )
search := strings . TrimSpace ( c . DefaultQuery ( "search" , "" ) )
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 15
}
q := db . Model ( & model . User { } )
if search != "" {
pattern := "%" + search + "%"
q = q . Where ( "COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?" , pattern , pattern , pattern )
}
var total int64
q . Count ( & total )
var users [ ] model . User
if err := database . DB ( ) . Find ( & users ) . Error ; err != nil {
if err := q . Order ( "created_at DESC" ) .
Offset ( ( page - 1 ) * pageSize ) .
Limit ( pageSize ) .
Find ( & users ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) , "users" : [ ] interface { } { } } )
return
}
c . JSON ( http . StatusOK , g in. H { "success" : true , "users" : users } )
totalPages := int ( total ) / pageSize
if int ( total ) % pageSize > 0 {
totalPages ++
}
if len ( users ) == 0 {
c . JSON ( http . StatusOK , gin . H {
"success" : true , "users" : users ,
"total" : total , "page" : page , "pageSize" : pageSize , "totalPages" : totalPages ,
} )
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 )
}
// 1. 购买状态:全书已购、已付费章节数(从 orders 计算)
hasFullBookMap := make ( map [ string ] bool )
sectionCountMap := make ( map [ string ] int )
var fullbookRows [ ] struct {
UserID string
}
db . Model ( & model . Order { } ) . Select ( "user_id" ) . Where ( "product_type = ? AND status = ?" , "fullbook" , "paid" ) . Find ( & fullbookRows )
for _ , r := range fullbookRows {
hasFullBookMap [ r . UserID ] = true
}
var sectionRows [ ] struct {
UserID string
Count int64
}
db . Model ( & model . Order { } ) . Select ( "user_id, COUNT(*) as count" ) .
Where ( "product_type = ? AND status = ?" , "section" , "paid" ) .
Group ( "user_id" ) . Find ( & sectionRows )
for _ , r := range sectionRows {
sectionCountMap [ r . UserID ] = int ( r . Count )
}
// 2. 分销收益:从 referrer 订单计算佣金;可提现 = 累计佣金 - 已提现 - 待处理提现
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
}
withdrawnMap := make ( map [ string ] float64 )
var withdrawnRows [ ] struct {
UserID string
Total float64
}
db . Model ( & model . Withdrawal { } ) . Select ( "user_id, COALESCE(SUM(amount), 0) as total" ) .
Where ( "status = ?" , "success" ) .
Group ( "user_id" ) . Find ( & withdrawnRows )
for _ , r := range withdrawnRows {
withdrawnMap [ r . UserID ] = r . Total
}
pendingWithdrawMap := make ( map [ string ] float64 )
var pendingRows [ ] struct {
UserID string
Total float64
}
db . Model ( & model . Withdrawal { } ) . Select ( "user_id, COALESCE(SUM(amount), 0) as total" ) .
Where ( "status IN ?" , [ ] string { "pending" , "processing" , "pending_confirm" } ) .
Group ( "user_id" ) . Find ( & pendingRows )
for _ , r := range pendingRows {
pendingWithdrawMap [ r . UserID ] = r . Total
}
// 3. 绑定人数:从 referral_bindings 计算
referralCountMap := make ( map [ string ] int )
var refCountRows [ ] struct {
ReferrerID string
Count int64
}
db . Model ( & model . ReferralBinding { } ) . Select ( "referrer_id, COUNT(*) as count" ) .
Group ( "referrer_id" ) . Find ( & refCountRows )
for _ , r := range refCountRows {
referralCountMap [ r . ReferrerID ] = int ( r . Count )
}
// 填充每个用户的实时计算字段
for i := range users {
uid := users [ i ] . ID
// 购买状态
users [ i ] . HasFullBook = ptrBool ( hasFullBookMap [ uid ] )
users [ i ] . PurchasedSectionCount = sectionCountMap [ uid ]
// 分销收益
totalE := referrerEarningsMap [ uid ]
withdrawn := withdrawnMap [ uid ]
pendingWd := pendingWithdrawMap [ uid ]
available := totalE - withdrawn - pendingWd
if available < 0 {
available = 0
}
users [ i ] . Earnings = ptrFloat64 ( totalE )
users [ i ] . PendingEarnings = ptrFloat64 ( available )
users [ i ] . ReferralCount = ptrInt ( referralCountMap [ uid ] )
}
c . JSON ( http . StatusOK , gin . H {
"success" : true , "users" : users ,
"total" : total , "page" : page , "pageSize" : pageSize , "totalPages" : totalPages ,
} )
}
func ptrBool ( b bool ) * bool { return & b }
func ptrFloat64 ( f float64 ) * float64 { v := f ; return & v }
func ptrInt ( n int ) * int { return & n }
// DBUsersAction POST /api/db/users( 创建) 、PUT /api/db/users( 更新)
func DBUsersAction ( c * gin . Context ) {
db := database . DB ( )
@@ -454,7 +602,7 @@ func DBUsersDelete(c *gin.Context) {
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "用户删除成功" } )
}
// DBUsersReferrals GET /api/db/users/referrals
// DBUsersReferrals GET /api/db/users/referrals(绑定关系详情弹窗;收益与「已付费」与小程序口径一致:订单+提现表实时计算)
func DBUsersReferrals ( c * gin . Context ) {
userId := c . Query ( "userId" )
if userId == "" {
@@ -462,6 +610,19 @@ func DBUsersReferrals(c *gin.Context) {
return
}
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 } } )
@@ -503,42 +664,82 @@ func DBUsersReferrals(c *gin.Context) {
if b . ExpiryDate . After ( time . Now ( ) ) {
daysRemaining = int ( b . ExpiryDate . Sub ( time . Now ( ) ) . Hours ( ) / 24 )
}
// 已付费:与小程序一致,以绑定记录的 purchase_count > 0 为准(支付回调会更新该字段)
hasPaid := b . PurchaseCount != nil && * b . PurchaseCount > 0
displayStatus := bindingStatusDisplay ( hasPaid , hasFullBook ) // vip | paid | free, 供前端徽章展示
referrals = append ( referrals , gin . H {
"id" : b . RefereeID , "nickname" : nick , "avatar" : avatar , "phone" : phone ,
"hasFullBook" : hasFullBook || status == "converted" ,
"createdAt" : b . BindingDate , "bindingStatus" : status , "daysRemaining" : daysRema ining, "commission" : b . CommissionAmount ,
"status" : status ,
"purchasedSections" : getB ind ingPurchaseCount ( b ) ,
"createdAt" : b . BindingDate , "bindingStatus" : status , "daysRemaining" : daysRemaining , "commission" : b . TotalCommission ,
"status" : displayStatus ,
} )
}
var referrer model . User
earningsE , pendingE , withdrawnE := 0.0 , 0.0 , 0.0
if err := db . Where ( "id = ?" , userId ) . Select ( "earnings" , "pending_earnings" , "withdrawn_earnings" ) . First ( & referrer ) . Error ; err == nil {
if referrer . Earnings != nil {
earningsE = * referrer . Earnings
}
if referrer . PendingEarnings != nil {
pendingE = * referrer . PendingEarnings
}
if referrer . WithdrawnEarnings != nil {
withdrawnE = * referrer . WithdrawnEarnings
}
// 累计收益、待提现:与小程序 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
var withdrawnSum struct { Total float64 }
db . Model ( & model . Withdrawal { } ) . Select ( "COALESCE(SUM(amount), 0) as total" ) .
Where ( "user_id = ? AND status = ?" , userId , "success" ) .
Scan ( & withdrawnSum )
withdrawnE := withdrawnSum . Total
var pendingWdSum 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 ( & pendingWdSum )
availableE := earningsE - withdrawnE - pendingWdSum . Total
if availableE < 0 {
availableE = 0
}
// 已付费人数:与小程序一致,绑定中 purchase_count > 0 的条数
purchased := 0
for _ , b := range bindings {
u : = userMap [ b . RefereeID ]
if ( u != nil && u . HasFullBook != nil && * u . HasFullBook ) || ( b . Status != nil && * b . Status == "converted" ) {
if b . PurchaseCount ! = nil && * b . PurchaseCount > 0 {
purchased ++
}
}
c . JSON ( http . StatusOK , gin . H {
"success" : true , "referrals" : referrals ,
"stats" : gin . H {
"total" : len ( bindings ) , "purchased" : purchased , "free" : len ( bindings ) - purchased ,
"earnings" : earningsE , "pendingEarnings" : pendingE , "withdrawnEarnings" : withdrawnE ,
"earnings" : roundFloat ( earningsE , 2 ) , "pendingEarnings" : roundFloat ( availableE , 2 ) , "withdrawnEarnings" : roundFloat ( withdrawnE , 2 ) ,
} ,
} )
}
func getBindingPurchaseCount ( b model . ReferralBinding ) int {
if b . PurchaseCount == nil {
return 0
}
return * b . PurchaseCount
}
func bindingStatusDisplay ( hasPaid bool , hasFullBook bool ) string {
if hasFullBook {
return "vip"
}
if hasPaid {
return "paid"
}
return "free"
}
func roundFloat ( v float64 , prec int ) float64 {
ratio := 1.0
for i := 0 ; i < prec ; i ++ {
ratio *= 10
}
return float64 ( int ( v * ratio + 0.5 ) ) / ratio
}
// DBInit POST /api/db/init
func DBInit ( c * gin . Context ) {
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : gin . H { "message" : "初始化接口已就绪(表结构由迁移维护)" } } )