Files
soul-yongping/soul-api/internal/handler/db.go

1145 lines
38 KiB
Go
Raw Normal View History

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" 时仅返回 VIPhasFullBook
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)
}
if vipFilter == "true" || vipFilter == "1" {
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
// 购买状态(含手动设置的 VIPis_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/外部执行"})
}