恢复被删除的代码
This commit is contained in:
868
soul-api/internal/handler/db.go
Normal file
868
soul-api/internal/handler/db.go
Normal file
@@ -0,0 +1,868 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetPublicDBConfig GET /api/miniprogram/config 公开接口,供小程序获取完整配置(与 next-project 对齐)
|
||||
// 从 system_config 读取 free_chapters、mp_config、feature_config、chapter_config,合并后返回
|
||||
func GetPublicDBConfig(c *gin.Context) {
|
||||
defaultFree := []string{"preface", "epilogue", "1.1", "appendix-1", "appendix-2", "appendix-3"}
|
||||
defaultPrices := gin.H{"section": float64(1), "fullbook": 9.9}
|
||||
defaultFeatures := gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true}
|
||||
defaultMp := gin.H{"appId": "wxb8bbb2b10dec74aa", "apiDomain": "https://soul.quwanzhi.com", "buyerDiscount": 5, "referralBindDays": 30, "minWithdraw": 10}
|
||||
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"freeChapters": defaultFree,
|
||||
"prices": defaultPrices,
|
||||
"features": defaultFeatures,
|
||||
"mpConfig": defaultMp,
|
||||
"configs": gin.H{}, // 兼容 miniprogram 备用格式 res.configs.feature_config
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
keys := []string{"chapter_config", "free_chapters", "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["freeChapters"].([]interface{}); ok && len(v) > 0 {
|
||||
arr := make([]string, 0, len(v))
|
||||
for _, x := range v {
|
||||
if s, ok := x.(string); ok {
|
||||
arr = append(arr, s)
|
||||
}
|
||||
}
|
||||
if len(arr) > 0 {
|
||||
out["freeChapters"] = arr
|
||||
}
|
||||
}
|
||||
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 "free_chapters":
|
||||
if arr, ok := val.([]interface{}); ok && len(arr) > 0 {
|
||||
ss := make([]string, 0, len(arr))
|
||||
for _, x := range arr {
|
||||
if s, ok := x.(string); ok {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
}
|
||||
if len(ss) > 0 {
|
||||
out["freeChapters"] = ss
|
||||
}
|
||||
out["configs"].(gin.H)["free_chapters"] = arr
|
||||
}
|
||||
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 {
|
||||
out["mpConfig"] = m
|
||||
out["configs"].(gin.H)["mp_config"] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
// 好友优惠(用于 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()
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"freeChapters": []string{"preface", "epilogue", "1.1", "appendix-1", "appendix-2", "appendix-3"},
|
||||
"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{}},
|
||||
}
|
||||
keys := []string{"free_chapters", "feature_config", "site_settings"}
|
||||
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 "free_chapters":
|
||||
if arr, ok := val.([]interface{}); ok && len(arr) > 0 {
|
||||
ss := make([]string, 0, len(arr))
|
||||
for _, x := range arr {
|
||||
if s, ok := x.(string); ok {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
}
|
||||
if len(ss) > 0 {
|
||||
out["freeChapters"] = ss
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// AdminSettingsPost POST /api/admin/settings 系统设置页专用:一次性保存免费章节、功能开关、站点/作者与价格(不包含小程序配置,该配置已移除)
|
||||
func AdminSettingsPost(c *gin.Context) {
|
||||
var body struct {
|
||||
FreeChapters []string `json:"freeChapters"`
|
||||
FeatureConfig map[string]interface{} `json:"featureConfig"`
|
||||
SiteSettings map[string]interface{} `json:"siteSettings"`
|
||||
}
|
||||
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.FreeChapters != nil {
|
||||
if err := saveKey("free_chapters", "免费章节ID列表", body.FreeChapters); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存免费章节失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
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"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
val := gin.H{
|
||||
"distributorShare": body.DistributorShare,
|
||||
"minWithdrawAmount": body.MinWithdrawAmount,
|
||||
"bindingDays": body.BindingDays,
|
||||
"userDiscount": body.UserDiscount,
|
||||
"withdrawFee": body.WithdrawFee,
|
||||
"enableAutoWithdraw": body.EnableAutoWithdraw,
|
||||
}
|
||||
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": "推广设置已保存"})
|
||||
}
|
||||
|
||||
// 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;购买状态、分销收益、绑定人数从订单/绑定表实时计算)
|
||||
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 := 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
|
||||
}
|
||||
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(§ionRows)
|
||||
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()
|
||||
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 更新
|
||||
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"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户ID不能为空"})
|
||||
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 len(updates) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "没有需要更新的字段"})
|
||||
return
|
||||
}
|
||||
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()
|
||||
|
||||
// 分销比例(与小程序 /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}})
|
||||
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 一致,从订单+提现表实时计算
|
||||
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 {
|
||||
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
|
||||
func DBDistribution(c *gin.Context) {
|
||||
userId := c.Query("userId")
|
||||
db := database.DB()
|
||||
var bindings []model.ReferralBinding
|
||||
q := db.Order("binding_date DESC").Limit(500)
|
||||
if userId != "" {
|
||||
q = q.Where("referrer_id = ?", userId)
|
||||
}
|
||||
if err := q.Find(&bindings).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "bindings": []interface{}{}, "total": 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",
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "bindings": out, "total": len(out)})
|
||||
}
|
||||
|
||||
// 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/外部执行"})
|
||||
}
|
||||
Reference in New Issue
Block a user