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" 时仅返回 VIP(hasFullBook) 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(§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(支持分页 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/外部执行"}) }