2026-03-07 22:58:43 +08:00
package handler
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"soul-api/internal/config"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
)
// GetPublicDBConfig GET /api/miniprogram/config 公开接口,供小程序获取完整配置(与 next-project 对齐)
// 从 system_config 读取 chapter_config、feature_config、mp_config, 合并后返回( 免费以章节 is_free/price 为准)
func GetPublicDBConfig ( c * gin . Context ) {
defaultPrices := gin . H { "section" : float64 ( 1 ) , "fullbook" : 9.9 }
defaultFeatures := gin . H { "matchEnabled" : true , "referralEnabled" : true , "searchEnabled" : true , "aboutEnabled" : true }
apiDomain := "https://soulapi.quwanzhi.com"
if cfg := config . Get ( ) ; cfg != nil && cfg . BaseURL != "" {
apiDomain = cfg . BaseURL
}
defaultMp := gin . H {
"appId" : "wxb8bbb2b10dec74aa" ,
"apiDomain" : apiDomain ,
"buyerDiscount" : 5 ,
"referralBindDays" : 30 ,
"minWithdraw" : 10 ,
"withdrawSubscribeTmplId" : "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE" ,
"mchId" : "1318592501" ,
}
out := gin . H {
"success" : true ,
"prices" : defaultPrices ,
"features" : defaultFeatures ,
"mpConfig" : defaultMp ,
"configs" : gin . H { } ,
}
db := database . DB ( )
keys := [ ] string { "chapter_config" , "feature_config" , "mp_config" }
for _ , k := range keys {
var row model . SystemConfig
if err := db . Where ( "config_key = ?" , k ) . First ( & row ) . Error ; err != nil {
continue
}
var val interface { }
if err := json . Unmarshal ( row . ConfigValue , & val ) ; err != nil {
continue
}
switch k {
case "chapter_config" :
if m , ok := val . ( map [ string ] interface { } ) ; ok {
if v , ok := m [ "prices" ] . ( map [ string ] interface { } ) ; ok {
out [ "prices" ] = v
}
if v , ok := m [ "features" ] . ( map [ string ] interface { } ) ; ok {
out [ "features" ] = v
}
out [ "configs" ] . ( gin . H ) [ "chapter_config" ] = m
}
case "feature_config" :
if m , ok := val . ( map [ string ] interface { } ) ; ok {
// 合并到 features, 不整体覆盖以保留 chapter_config 里的
cur := out [ "features" ] . ( gin . H )
for kk , vv := range m {
cur [ kk ] = vv
}
out [ "configs" ] . ( gin . H ) [ "feature_config" ] = m
}
case "mp_config" :
if m , ok := val . ( map [ string ] interface { } ) ; ok {
// 合并默认值, DB 有则覆盖
merged := make ( gin . H )
for k , v := range defaultMp {
merged [ k ] = v
}
for k , v := range m {
merged [ k ] = v
}
out [ "mpConfig" ] = merged
out [ "configs" ] . ( gin . H ) [ "mp_config" ] = merged
}
}
}
// 好友优惠(用于 read 页展示优惠价)
var refRow model . SystemConfig
if err := db . Where ( "config_key = ?" , "referral_config" ) . First ( & refRow ) . Error ; err == nil {
var refVal map [ string ] interface { }
if err := json . Unmarshal ( refRow . ConfigValue , & refVal ) ; err == nil {
if v , ok := refVal [ "userDiscount" ] . ( float64 ) ; ok {
out [ "userDiscount" ] = v
}
}
}
if _ , has := out [ "userDiscount" ] ; ! has {
out [ "userDiscount" ] = float64 ( 5 )
}
c . JSON ( http . StatusOK , out )
}
// DBConfigGet GET /api/db/config( 管理端鉴权后同路径由 db 组处理时用)
func DBConfigGet ( c * gin . Context ) {
key := c . Query ( "key" )
db := database . DB ( )
var list [ ] model . SystemConfig
q := db . Table ( "system_config" )
if key != "" {
q = q . Where ( "config_key = ?" , key )
}
if err := q . Find ( & list ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
if key != "" && len ( list ) == 1 {
var val interface { }
_ = json . Unmarshal ( list [ 0 ] . ConfigValue , & val )
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : val } )
return
}
data := make ( [ ] gin . H , 0 , len ( list ) )
for _ , row := range list {
var val interface { }
_ = json . Unmarshal ( row . ConfigValue , & val )
data = append ( data , gin . H { "configKey" : row . ConfigKey , "configValue" : val } )
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : data } )
}
// AdminSettingsGet GET /api/admin/settings 系统设置页专用:仅返回功能开关、站点/作者与价格、小程序配置
func AdminSettingsGet ( c * gin . Context ) {
db := database . DB ( )
apiDomain := "https://soulapi.quwanzhi.com"
if cfg := config . Get ( ) ; cfg != nil && cfg . BaseURL != "" {
apiDomain = cfg . BaseURL
}
defaultMp := gin . H {
"appId" : "wxb8bbb2b10dec74aa" ,
"apiDomain" : apiDomain ,
"withdrawSubscribeTmplId" : "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE" ,
"mchId" : "1318592501" ,
"minWithdraw" : float64 ( 10 ) ,
}
out := gin . H {
"success" : true ,
"featureConfig" : gin . H { "matchEnabled" : true , "referralEnabled" : true , "searchEnabled" : true , "aboutEnabled" : true } ,
"siteSettings" : gin . H { "sectionPrice" : float64 ( 1 ) , "baseBookPrice" : 9.9 , "distributorShare" : float64 ( 90 ) , "authorInfo" : gin . H { } } ,
"mpConfig" : defaultMp ,
}
keys := [ ] string { "feature_config" , "site_settings" , "mp_config" }
for _ , k := range keys {
var row model . SystemConfig
if err := db . Where ( "config_key = ?" , k ) . First ( & row ) . Error ; err != nil {
continue
}
var val interface { }
if err := json . Unmarshal ( row . ConfigValue , & val ) ; err != nil {
continue
}
switch k {
case "feature_config" :
if m , ok := val . ( map [ string ] interface { } ) ; ok && len ( m ) > 0 {
out [ "featureConfig" ] = m
}
case "site_settings" :
if m , ok := val . ( map [ string ] interface { } ) ; ok && len ( m ) > 0 {
out [ "siteSettings" ] = m
}
case "mp_config" :
if m , ok := val . ( map [ string ] interface { } ) ; ok {
merged := make ( gin . H )
for k , v := range defaultMp {
merged [ k ] = v
}
for k , v := range m {
merged [ k ] = v
}
out [ "mpConfig" ] = merged
}
}
}
c . JSON ( http . StatusOK , out )
}
// AdminSettingsPost POST /api/admin/settings 系统设置页专用:一次性保存功能开关、站点/作者与价格、小程序配置
func AdminSettingsPost ( c * gin . Context ) {
var body struct {
FeatureConfig map [ string ] interface { } ` json:"featureConfig" `
SiteSettings map [ string ] interface { } ` json:"siteSettings" `
MpConfig map [ string ] interface { } ` json:"mpConfig" `
}
if err := c . ShouldBindJSON ( & body ) ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "请求体无效" } )
return
}
db := database . DB ( )
saveKey := func ( key , desc string , value interface { } ) error {
valBytes , err := json . Marshal ( value )
if err != nil {
return err
}
var row model . SystemConfig
err = db . Where ( "config_key = ?" , key ) . First ( & row ) . Error
if err != nil {
row = model . SystemConfig { ConfigKey : key , ConfigValue : valBytes , Description : & desc }
return db . Create ( & row ) . Error
}
row . ConfigValue = valBytes
if desc != "" {
row . Description = & desc
}
return db . Save ( & row ) . Error
}
if body . FeatureConfig != nil {
if err := saveKey ( "feature_config" , "功能开关配置" , body . FeatureConfig ) ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "保存功能开关失败: " + err . Error ( ) } )
return
}
}
if body . SiteSettings != nil {
if err := saveKey ( "site_settings" , "站点与作者配置" , body . SiteSettings ) ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "保存站点设置失败: " + err . Error ( ) } )
return
}
}
if body . MpConfig != nil {
if err := saveKey ( "mp_config" , "小程序专用配置" , body . MpConfig ) ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "保存小程序配置失败: " + err . Error ( ) } )
return
}
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "设置已保存" } )
}
// AdminReferralSettingsGet GET /api/admin/referral-settings 推广设置页专用:仅返回 referral_config
func AdminReferralSettingsGet ( c * gin . Context ) {
db := database . DB ( )
defaultConfig := gin . H {
"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 {
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : defaultConfig } )
return
}
var val map [ string ] interface { }
if err := json . Unmarshal ( row . ConfigValue , & val ) ; err != nil || len ( val ) == 0 {
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : defaultConfig } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : val } )
}
// 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" `
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 ,
"enableAutoWithdraw" : body . EnableAutoWithdraw ,
"vipOrderShareVip" : vipOrderShareVip ,
"vipOrderShareNonVip" : vipOrderShareNonVip ,
}
valBytes , err := json . Marshal ( val )
if err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
db := database . DB ( )
desc := "分销 / 推广规则配置"
var row model . SystemConfig
if err := db . Where ( "config_key = ?" , "referral_config" ) . First ( & row ) . Error ; err != nil {
row = model . SystemConfig { ConfigKey : "referral_config" , ConfigValue : valBytes , Description : & desc }
err = db . Create ( & row ) . Error
} else {
row . ConfigValue = valBytes
row . Description = & desc
err = db . Save ( & row ) . Error
}
if err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "推广设置已保存" } )
}
func authorConfigToResponse ( row * model . AuthorConfig ) gin . H {
defaultStats := [ ] gin . H { { "label" : "商业案例" , "value" : "62" } , { "label" : "连续直播" , "value" : "365天" } , { "label" : "派对分享" , "value" : "1000+" } }
defaultHighlights := [ ] string { "5年私域运营经验" , "帮助100+品牌从0到1增长" , "连续创业者,擅长商业模式设计" }
var stats [ ] gin . H
if row . Stats != "" {
_ = json . Unmarshal ( [ ] byte ( row . Stats ) , & stats )
}
if len ( stats ) == 0 {
stats = defaultStats
}
var highlights [ ] string
if row . Highlights != "" {
_ = json . Unmarshal ( [ ] byte ( row . Highlights ) , & highlights )
}
if len ( highlights ) == 0 {
highlights = defaultHighlights
}
return gin . H {
"name" : row . Name ,
"avatar" : row . Avatar ,
"avatarImg" : row . AvatarImg ,
"title" : row . Title ,
"bio" : row . Bio ,
"stats" : stats ,
"highlights" : highlights ,
}
}
// AdminAuthorSettingsGet GET /api/admin/author-settings 作者详情配置(管理端专用)
func AdminAuthorSettingsGet ( c * gin . Context ) {
defaultAuthor := gin . H {
"name" : "卡若" ,
"avatar" : "K" ,
"avatarImg" : "" ,
"title" : "Soul派对房主理人 · 私域运营专家" ,
"bio" : "每天早上6点到9点, 在Soul派对房分享真实的创业故事。专注私域运营与项目变现, 用云阿米巴模式帮助创业者构建可持续的商业体系。" ,
"stats" : [ ] gin . H { { "label" : "商业案例" , "value" : "62" } , { "label" : "连续直播" , "value" : "365天" } , { "label" : "派对分享" , "value" : "1000+" } } ,
"highlights" : [ ] string { "5年私域运营经验" , "帮助100+品牌从0到1增长" , "连续创业者,擅长商业模式设计" } ,
}
db := database . DB ( )
var row model . AuthorConfig
if err := db . First ( & row ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : defaultAuthor } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : authorConfigToResponse ( & row ) } )
}
// AdminAuthorSettingsPost POST /api/admin/author-settings 保存作者详情配置
func AdminAuthorSettingsPost ( c * gin . Context ) {
var body map [ string ] interface { }
if err := c . ShouldBindJSON ( & body ) ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "请求体无效" } )
return
}
str := func ( k string ) string {
if v , ok := body [ k ] ; ok && v != nil {
if s , ok := v . ( string ) ; ok {
return s
}
return fmt . Sprintf ( "%v" , v )
}
return ""
}
name := str ( "name" )
if name == "" {
name = "卡若"
}
avatar := str ( "avatar" )
if avatar == "" {
avatar = "K"
}
statsVal := body [ "stats" ]
if statsVal == nil {
statsVal = [ ] gin . H { { "label" : "商业案例" , "value" : "62" } , { "label" : "连续直播" , "value" : "365天" } , { "label" : "派对分享" , "value" : "1000+" } }
}
highlightsVal := body [ "highlights" ]
if highlightsVal == nil {
highlightsVal = [ ] string { }
}
statsBytes , _ := json . Marshal ( statsVal )
highlightsBytes , _ := json . Marshal ( highlightsVal )
db := database . DB ( )
var row model . AuthorConfig
err := db . First ( & row ) . Error
if err != nil {
row = model . AuthorConfig {
Name : name ,
Avatar : avatar ,
AvatarImg : str ( "avatarImg" ) ,
Title : str ( "title" ) ,
Bio : str ( "bio" ) ,
Stats : string ( statsBytes ) ,
Highlights : string ( highlightsBytes ) ,
}
err = db . Create ( & row ) . Error
} else {
row . Name = name
row . Avatar = avatar
row . AvatarImg = str ( "avatarImg" )
row . Title = str ( "title" )
row . Bio = str ( "bio" )
row . Stats = string ( statsBytes )
row . Highlights = string ( highlightsBytes )
err = db . Save ( & row ) . Error
}
if err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "作者设置已保存" } )
}
// MiniprogramAboutAuthor GET /api/miniprogram/about/author 小程序-关于作者页拉取作者配置(公开,无需鉴权)
func MiniprogramAboutAuthor ( c * gin . Context ) {
defaultAuthor := gin . H {
"name" : "卡若" ,
"avatar" : "K" ,
"avatarImg" : "" ,
"title" : "Soul派对房主理人 · 私域运营专家" ,
"bio" : "每天早上6点到9点, 在Soul派对房分享真实的创业故事。" ,
"stats" : [ ] gin . H { { "label" : "商业案例" , "value" : "62" } , { "label" : "连续直播" , "value" : "365天" } , { "label" : "派对分享" , "value" : "1000+" } } ,
"highlights" : [ ] string { "5年私域运营经验" , "帮助100+品牌从0到1增长" , "连续创业者,擅长商业模式设计" } ,
}
db := database . DB ( )
var row model . AuthorConfig
if err := db . First ( & row ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : defaultAuthor } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : authorConfigToResponse ( & row ) } )
}
// DBConfigPost POST /api/db/config
func DBConfigPost ( c * gin . Context ) {
var body struct {
Key string ` json:"key" `
Value interface { } ` json:"value" `
Description string ` json:"description" `
}
if err := c . ShouldBindJSON ( & body ) ; err != nil || body . Key == "" {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "配置键不能为空" } )
return
}
valBytes , err := json . Marshal ( body . Value )
if err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
db := database . DB ( )
desc := body . Description
var row model . SystemConfig
err = db . Where ( "config_key = ?" , body . Key ) . First ( & row ) . Error
if err != nil {
row = model . SystemConfig { ConfigKey : body . Key , ConfigValue : valBytes , Description : & desc }
err = db . Create ( & row ) . Error
} else {
row . ConfigValue = valBytes
if body . Description != "" {
row . Description = & desc
}
err = db . Save ( & row ) . Error
}
if err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "配置保存成功" } )
}
// DBUsersList GET /api/db/users( 支持分页 page、pageSize, 可选搜索 search; 有 id 时返回单个 user; 购买状态、分销收益、绑定人数从订单/绑定表实时计算)
func DBUsersList ( c * gin . Context ) {
db := database . DB ( )
id := strings . TrimSpace ( c . Query ( "id" ) )
page , _ := strconv . Atoi ( c . DefaultQuery ( "page" , "1" ) )
pageSize , _ := strconv . Atoi ( c . DefaultQuery ( "pageSize" , "10" ) )
search := strings . TrimSpace ( c . DefaultQuery ( "search" , "" ) )
vipFilter := c . Query ( "vip" ) // "true" 时仅返回 VIP( hasFullBook)
2026-03-08 11:05:40 +08:00
poolFilter := c . Query ( "pool" ) // "complete" 时仅返回已完善资料的用户
2026-03-07 22:58:43 +08:00
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
// 有 id 时返回单个用户(供 UserDetailModal 等使用)
if id != "" {
var user model . User
if err := db . Where ( "id = ?" , id ) . First ( & user ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : true , "user" : nil } )
return
}
// 填充 hasFullBook( 含 is_vip 或 orders)
var cnt int64
db . Model ( & model . Order { } ) . Where ( "user_id = ? AND (status = ? OR status = ?) AND (product_type = ? OR product_type = ?)" ,
id , "paid" , "completed" , "fullbook" , "vip" ) . Count ( & cnt )
user . HasFullBook = ptrBool ( cnt > 0 )
if user . IsVip != nil && * user . IsVip {
user . HasFullBook = ptrBool ( true )
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "user" : user } )
return
}
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 )
}
2026-03-08 11:05:47 +08:00
if poolFilter == "complete" {
2026-03-08 11:34:18 +08:00
q = q . Where ( "(phone IS NOT NULL AND phone != '') AND (nickname IS NOT NULL AND nickname != '' AND nickname != '微信用户') AND (avatar IS NOT NULL AND avatar != '')" )
2026-03-08 11:05:47 +08:00
} else if vipFilter == "true" || vipFilter == "1" {
2026-03-07 22:58:43 +08:00
q = q . Where ( "id IN (SELECT user_id FROM orders WHERE product_type IN ? AND (status = ? OR status = ?)) OR (is_vip = 1 AND vip_expire_date > ?)" ,
[ ] string { "fullbook" , "vip" } , "paid" , "completed" , time . Now ( ) )
}
var total int64
q . Count ( & total )
var users [ ] model . User
query := db . Model ( & model . User { } )
if search != "" {
pattern := "%" + search + "%"
query = query . Where ( "COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?" , pattern , pattern , pattern )
}
if vipFilter == "true" || vipFilter == "1" {
query = query . Where ( "id IN (SELECT user_id FROM orders WHERE product_type IN ? AND (status = ? OR status = ?)) OR (is_vip = 1 AND vip_expire_date > ?)" ,
[ ] string { "fullbook" , "vip" } , "paid" , "completed" , time . Now ( ) )
}
if err := query . 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
}
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
}
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 IN ? AND status = ?" , [ ] string { "fullbook" , "vip" } , "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 订单逐条 computeOrderCommission 求和(会员订单 20%/10%,内容订单 90%)
referrerEarningsMap := make ( map [ string ] float64 )
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 {
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
// 购买状态(含手动设置的 VIP: is_vip=1 且 vip_expire_date>NOW)
hasFull := hasFullBookMap [ uid ]
if users [ i ] . IsVip != nil && * users [ i ] . IsVip && users [ i ] . VipExpireDate != nil && users [ i ] . VipExpireDate . After ( time . Now ( ) ) {
hasFull = true
}
users [ i ] . HasFullBook = ptrBool ( hasFull )
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 ( )
if c . Request . Method == http . MethodPost {
var body struct {
OpenID * string ` json:"openId" `
Phone * string ` json:"phone" `
Nickname * string ` json:"nickname" `
WechatID * string ` json:"wechatId" `
Avatar * string ` json:"avatar" `
IsAdmin * bool ` json:"isAdmin" `
}
if err := c . ShouldBindJSON ( & body ) ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "请求体无效" } )
return
}
userID := "user_" + randomSuffix ( )
code := "SOUL" + randomSuffix ( ) [ : 4 ]
nick := "用户"
if body . Nickname != nil && * body . Nickname != "" {
nick = * body . Nickname
} else {
nick = nick + userID [ len ( userID ) - 4 : ]
}
u := model . User {
ID : userID , Nickname : & nick , ReferralCode : & code ,
OpenID : body . OpenID , Phone : body . Phone , WechatID : body . WechatID , Avatar : body . Avatar ,
}
if body . IsAdmin != nil {
u . IsAdmin = body . IsAdmin
}
if err := db . Create ( & u ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "user" : u , "isNew" : true , "message" : "用户创建成功" } )
return
}
// PUT 更新(含 VIP 手动设置: is_vip、vip_expire_date、vip_name、vip_avatar、vip_project、vip_contact、vip_bio)
var body struct {
ID string ` json:"id" `
Nickname * string ` json:"nickname" `
Phone * string ` json:"phone" `
WechatID * string ` json:"wechatId" `
Avatar * string ` json:"avatar" `
HasFullBook * bool ` json:"hasFullBook" `
IsAdmin * bool ` json:"isAdmin" `
Earnings * float64 ` json:"earnings" `
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" `
VipContact * string ` json:"vipContact" `
VipBio * string ` json:"vipBio" `
}
if err := c . ShouldBindJSON ( & body ) ; err != nil || body . ID == "" {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "用户ID不能为空" } )
return
}
// 手动设置 VIP 时,必须提供有效到期日
if body . IsVip != nil && * body . IsVip {
if body . VipExpireDate == nil || strings . TrimSpace ( * body . VipExpireDate ) == "" {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "开启 VIP 时请填写有效到期日" } )
return
}
if _ , err := time . ParseInLocation ( "2006-01-02" , strings . TrimSpace ( * body . VipExpireDate ) , time . Local ) ; err != nil {
if _ , err2 := time . ParseInLocation ( "2006-01-02 15:04:05" , strings . TrimSpace ( * body . VipExpireDate ) , time . Local ) ; err2 != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "到期日格式无效,请使用 YYYY-MM-DD" } )
return
}
}
}
updates := map [ string ] interface { } { }
if body . Nickname != nil {
updates [ "nickname" ] = * body . Nickname
}
if body . Phone != nil {
updates [ "phone" ] = * body . Phone
}
if body . WechatID != nil {
updates [ "wechat_id" ] = * body . WechatID
}
if body . Avatar != nil {
updates [ "avatar" ] = * body . Avatar
}
if body . HasFullBook != nil {
updates [ "has_full_book" ] = * body . HasFullBook
}
if body . IsAdmin != nil {
updates [ "is_admin" ] = * body . IsAdmin
}
if body . Earnings != nil {
updates [ "earnings" ] = * body . Earnings
}
if body . PendingEarnings != nil {
updates [ "pending_earnings" ] = * body . PendingEarnings
}
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 == "" {
updates [ "vip_expire_date" ] = nil
} else {
if t , err := time . ParseInLocation ( "2006-01-02" , * body . VipExpireDate , time . Local ) ; err == nil {
updates [ "vip_expire_date" ] = t
} else if t , err := time . ParseInLocation ( "2006-01-02 15:04:05" , * body . VipExpireDate , time . Local ) ; err == nil {
updates [ "vip_expire_date" ] = t
}
}
}
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
}
if body . VipAvatar != nil {
updates [ "vip_avatar" ] = * body . VipAvatar
}
if body . VipProject != nil {
updates [ "vip_project" ] = * body . VipProject
}
if body . VipContact != nil {
updates [ "vip_contact" ] = * body . VipContact
}
if body . VipBio != nil {
updates [ "vip_bio" ] = * body . VipBio
}
if len ( updates ) == 0 {
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "没有需要更新的字段" } )
return
}
// VIP 相关更新时记录日志(手动设置)
if body . IsVip != nil || body . VipExpireDate != nil || body . VipName != nil || body . VipAvatar != nil || body . VipProject != nil || body . VipContact != nil || body . VipBio != nil {
isVipStr := "-"
if body . IsVip != nil {
isVipStr = fmt . Sprintf ( "%v" , * body . IsVip )
}
vipExpire := "-"
if body . VipExpireDate != nil {
vipExpire = * body . VipExpireDate
}
fmt . Printf ( "[VIP] 设置方式=手动设置, userId=%s, isVip=%s, vipExpireDate=%s\n" , body . ID , isVipStr , vipExpire )
}
if err := db . Model ( & model . User { } ) . 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" : "用户更新成功" } )
}
func randomSuffix ( ) string {
return fmt . Sprintf ( "%d%x" , time . Now ( ) . UnixNano ( ) % 100000 , time . Now ( ) . UnixNano ( ) & 0xfff )
}
// DBUsersDelete DELETE /api/db/users
func DBUsersDelete ( c * gin . Context ) {
id := c . Query ( "id" )
if id == "" {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "用户ID不能为空" } )
return
}
if err := database . DB ( ) . Where ( "id = ?" , id ) . Delete ( & model . User { } ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "用户删除成功" } )
}
// DBUsersReferrals GET /api/db/users/referrals( 绑定关系详情弹窗; 收益与「已付费」与小程序口径一致: 订单+提现表实时计算)
func DBUsersReferrals ( c * gin . Context ) {
userId := c . Query ( "userId" )
if userId == "" {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "缺少 userId" } )
return
}
db := database . DB ( )
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 } } )
return
}
refereeIds := make ( [ ] string , 0 , len ( bindings ) )
for _ , b := range bindings {
refereeIds = append ( refereeIds , b . RefereeID )
}
var users [ ] model . User
if len ( refereeIds ) > 0 {
db . Where ( "id IN ?" , refereeIds ) . Find ( & users )
}
userMap := make ( map [ string ] * model . User )
for i := range users {
userMap [ users [ i ] . ID ] = & users [ i ]
}
referrals := make ( [ ] gin . H , 0 , len ( bindings ) )
for _ , b := range bindings {
u := userMap [ b . RefereeID ]
nick := "微信用户"
var avatar * string
var phone * string
hasFullBook := false
if u != nil {
if u . Nickname != nil {
nick = * u . Nickname
}
avatar , phone = u . Avatar , u . Phone
if u . HasFullBook != nil {
hasFullBook = * u . HasFullBook
}
}
status := "active"
if b . Status != nil {
status = * b . Status
}
daysRemaining := 0
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" ,
"purchasedSections" : getBindingPurchaseCount ( b ) ,
"createdAt" : b . BindingDate , "bindingStatus" : status , "daysRemaining" : daysRemaining , "commission" : b . TotalCommission ,
"status" : displayStatus ,
} )
}
// 累计收益、待提现:与小程序 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" ) .
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 {
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" : 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" : "初始化接口已就绪(表结构由迁移维护)" } } )
}
// DBDistribution GET /api/db/distribution( 支持分页 page、pageSize, 筛选 status、search)
func DBDistribution ( c * gin . Context ) {
userId := c . Query ( "userId" )
page , _ := strconv . Atoi ( c . DefaultQuery ( "page" , "1" ) )
pageSize , _ := strconv . Atoi ( c . DefaultQuery ( "pageSize" , "10" ) )
statusFilter := c . Query ( "status" )
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
db := database . DB ( )
q := db . Model ( & model . ReferralBinding { } )
if userId != "" {
q = q . Where ( "referrer_id = ?" , userId )
}
if statusFilter != "" && statusFilter != "all" {
q = q . Where ( "status = ?" , statusFilter )
}
var total int64
q . Count ( & total )
var bindings [ ] model . ReferralBinding
query := db . Model ( & model . ReferralBinding { } ) . Order ( "binding_date DESC" )
if userId != "" {
query = query . Where ( "referrer_id = ?" , userId )
}
if statusFilter != "" && statusFilter != "all" {
query = query . Where ( "status = ?" , statusFilter )
}
if err := query . Offset ( ( page - 1 ) * pageSize ) . Limit ( pageSize ) . Find ( & bindings ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : true , "bindings" : [ ] interface { } { } , "total" : 0 , "page" : page , "pageSize" : pageSize , "totalPages" : 0 } )
return
}
referrerIds := make ( map [ string ] bool )
refereeIds := make ( map [ string ] bool )
for _ , b := range bindings {
referrerIds [ b . ReferrerID ] = true
refereeIds [ b . RefereeID ] = true
}
allIds := make ( [ ] string , 0 , len ( referrerIds ) + len ( refereeIds ) )
for id := range referrerIds {
allIds = append ( allIds , id )
}
for id := range refereeIds {
if ! referrerIds [ id ] {
allIds = append ( allIds , id )
}
}
var users [ ] model . User
if len ( allIds ) > 0 {
db . Where ( "id IN ?" , allIds ) . Find ( & users )
}
userMap := make ( map [ string ] * model . User )
for i := range users {
userMap [ users [ i ] . ID ] = & users [ i ]
}
getStr := func ( s * string ) string {
if s == nil || * s == "" {
return ""
}
return * s
}
out := make ( [ ] gin . H , 0 , len ( bindings ) )
for _ , b := range bindings {
refNick := "微信用户"
var refereePhone , refereeAvatar * string
if u := userMap [ b . RefereeID ] ; u != nil {
if u . Nickname != nil && * u . Nickname != "" {
refNick = * u . Nickname
} else {
refNick = "微信用户"
}
refereePhone = u . Phone
refereeAvatar = u . Avatar
}
var referrerName , referrerAvatar * string
if u := userMap [ b . ReferrerID ] ; u != nil {
referrerName = u . Nickname
referrerAvatar = u . Avatar
}
days := 0
if b . ExpiryDate . After ( time . Now ( ) ) {
days = int ( b . ExpiryDate . Sub ( time . Now ( ) ) . Hours ( ) / 24 )
}
// 佣金展示用累计佣金 total_commission( 支付回调累加) , 无则用 commission_amount
commissionVal := b . TotalCommission
if commissionVal == nil {
commissionVal = b . CommissionAmount
}
statusVal := ""
if b . Status != nil {
statusVal = * b . Status
}
out = append ( out , gin . H {
"id" : b . ID , "referrerId" : b . ReferrerID , "referrerName" : getStr ( referrerName ) , "referrerCode" : b . ReferralCode , "referrerAvatar" : getStr ( referrerAvatar ) ,
"refereeId" : b . RefereeID , "refereeNickname" : refNick , "refereePhone" : getStr ( refereePhone ) , "refereeAvatar" : getStr ( refereeAvatar ) ,
"boundAt" : b . BindingDate , "expiresAt" : b . ExpiryDate , "status" : statusVal ,
"daysRemaining" : days , "commission" : commissionVal , "totalCommission" : commissionVal , "source" : "miniprogram" ,
} )
}
totalPages := int ( total ) / pageSize
if int ( total ) % pageSize > 0 {
totalPages ++
}
c . JSON ( http . StatusOK , gin . H {
"success" : true , "bindings" : out ,
"total" : total , "page" : page , "pageSize" : pageSize , "totalPages" : totalPages ,
} )
}
// DBChapters GET/POST /api/db/chapters
func DBChapters ( c * gin . Context ) {
var list [ ] model . Chapter
if err := database . DB ( ) . Order ( "sort_order ASC, id ASC" ) . Find ( & list ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) , "data" : [ ] interface { } { } } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : list } )
}
// DBConfigDelete DELETE /api/db/config
func DBConfigDelete ( c * gin . Context ) {
key := c . Query ( "key" )
if key == "" {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "配置键不能为空" } )
return
}
if err := database . DB ( ) . Where ( "config_key = ?" , key ) . Delete ( & model . SystemConfig { } ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "success" : true } )
}
// DBInitGet GET /api/db/init
func DBInitGet ( c * gin . Context ) {
c . JSON ( http . StatusOK , gin . H { "success" : true , "data" : gin . H { "message" : "ok" } } )
}
// DBMigrateGet GET /api/db/migrate
func DBMigrateGet ( c * gin . Context ) {
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "迁移状态查询(由 Prisma/外部维护)" } )
}
// DBMigratePost POST /api/db/migrate
func DBMigratePost ( c * gin . Context ) {
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "迁移由 Prisma/外部执行" } )
}