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

955 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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://soulapi.quwanzhi.com", // 保留以兼容线上旧版小程序(仍从 config 读取)
"buyerDiscount": 5,
"referralBindDays": 30,
"minWithdraw": 10,
"withdrawSubscribeTmplId": "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE",
"mchId": "1318592501",
}
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 {
// 合并默认值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()
defaultMp := gin.H{
"appId": "wxb8bbb2b10dec74aa",
"apiDomain": "https://soulapi.quwanzhi.com", // 保留以兼容线上旧版小程序
"withdrawSubscribeTmplId": "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE",
"mchId": "1318592501",
"minWithdraw": float64(10),
}
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{}},
"mpConfig": defaultMp,
}
keys := []string{"free_chapters", "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 "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
}
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 {
FreeChapters []string `json:"freeChapters"`
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.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
}
}
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,
}
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", "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
}
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 = ?)",
[]string{"fullbook", "vip"}, "paid")
}
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 = ?)",
[]string{"fullbook", "vip"}, "paid")
}
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
}
// 读取推广配置中的分销比例
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 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 订单计算佣金;可提现 = 累计佣金 - 已提现 - 待处理提现
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支持分页 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/外部执行"})
}