Refactor user profile handling and navigation logic in the mini program. Introduce functions to ensure user profile completeness after login, update avatar selection process, and enhance navigation between chapters based on backend data. Update API endpoints for user data synchronization and improve user experience with new UI elements for profile editing.
This commit is contained in:
@@ -18,6 +18,9 @@ SKIP_AUTO_MIGRATE=1
|
||||
# 统一 API 域名(测试环境)
|
||||
API_BASE_URL=https://souldev.quwanzhi.com
|
||||
|
||||
#添加卡若
|
||||
CKB_LEAD_API_KEY=2y4v5-rjhfc-sg5wy-zklkv-bg0tl
|
||||
|
||||
# 微信小程序配置
|
||||
WECHAT_APPID=wxb8bbb2b10dec74aa
|
||||
WECHAT_APPSECRET=3c1fb1f63e6e052222bbcead9d07fe0c
|
||||
|
||||
@@ -14,6 +14,9 @@ DB_DSN=cdb_outerroot:Zhiqun1984@tcp(56b4c23f6853c.gz.cdb.myqcloud.com:14413)/sou
|
||||
# 统一 API 域名(支付回调、转账回调、apiDomain 等由此派生;无需尾部斜杠)
|
||||
API_BASE_URL=https://soulapi.quwanzhi.com
|
||||
|
||||
#添加卡若
|
||||
CKB_LEAD_API_KEY=2y4v5-rjhfc-sg5wy-zklkv-bg0tl
|
||||
|
||||
# 微信小程序配置
|
||||
WECHAT_APPID=wxb8bbb2b10dec74aa
|
||||
WECHAT_APPSECRET=3c1fb1f63e6e052222bbcead9d07fe0c
|
||||
|
||||
@@ -382,9 +382,42 @@ func BookHot(c *gin.Context) {
|
||||
|
||||
// BookRecommended GET /api/book/recommended 精选推荐(首页「为你推荐」前 3 章,带 热门/推荐/精选 标签)
|
||||
func BookRecommended(c *gin.Context) {
|
||||
// 优先复用管理端内容排行榜的算法,保证与后台「内容排行榜」顺序一致
|
||||
sections, err := computeArticleRankingSections(database.DB())
|
||||
if err == nil && len(sections) > 0 {
|
||||
// 前 3 名作为精选推荐
|
||||
limit := 3
|
||||
if len(sections) < limit {
|
||||
limit = len(sections)
|
||||
}
|
||||
tags := []string{"热门", "推荐", "精选"}
|
||||
out := make([]gin.H, 0, limit)
|
||||
for i := 0; i < limit; i++ {
|
||||
s := sections[i]
|
||||
tag := "精选"
|
||||
if i < len(tags) {
|
||||
tag = tags[i]
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": s.ID,
|
||||
"mid": s.MID,
|
||||
"sectionTitle": s.Title,
|
||||
"partTitle": s.PartTitle,
|
||||
"chapterTitle": s.ChapterTitle,
|
||||
"tag": tag,
|
||||
"isFree": s.IsFree,
|
||||
"price": s.Price,
|
||||
"isNew": s.IsNew,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
|
||||
return
|
||||
}
|
||||
|
||||
// 兜底:沿用原有热门章节算法,至少保证有推荐
|
||||
list := bookHotChaptersSorted(database.DB(), 3)
|
||||
if len(list) == 0 {
|
||||
// 兜底:按 updated_at 取前 3,同样排除序言/尾声/附录
|
||||
// 第二层兜底:按 updated_at 取前 3,同样排除序言/尾声/附录
|
||||
q := database.DB().Model(&model.Chapter{})
|
||||
for _, p := range excludeParts {
|
||||
q = q.Where("part_title NOT LIKE ?", "%"+p+"%")
|
||||
|
||||
@@ -290,6 +290,134 @@ func CKBSync(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// CKBIndexLead POST /api/miniprogram/ckb/index-lead 小程序首页「点击链接卡若」专用留资接口
|
||||
// - 固定使用全局 CKB_LEAD_API_KEY,不受文章 @ 人物的 ckb_api_key 影响
|
||||
// - 请求体:userId(可选,用于补全昵称)、phone/wechatId(至少一个)、name(可选)
|
||||
func CKBIndexLead(c *gin.Context) {
|
||||
var body struct {
|
||||
UserID string `json:"userId"`
|
||||
Phone string `json:"phone"`
|
||||
WechatID string `json:"wechatId"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&body)
|
||||
phone := strings.TrimSpace(body.Phone)
|
||||
wechatId := strings.TrimSpace(body.WechatID)
|
||||
// 存客宝侧仅接收手机号,不接收微信号;首页入口必须提供手机号
|
||||
if phone == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "请先填写手机号"})
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(body.Name)
|
||||
db := database.DB()
|
||||
if name == "" && body.UserID != "" {
|
||||
var u model.User
|
||||
if db.Select("nickname").Where("id = ?", body.UserID).First(&u).Error == nil && u.Nickname != nil && *u.Nickname != "" {
|
||||
name = *u.Nickname
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
name = "小程序用户"
|
||||
}
|
||||
|
||||
// 首页固定使用全局密钥:CKB_LEAD_API_KEY(.env)或代码内置 ckbAPIKey
|
||||
leadKey := ckbAPIKey
|
||||
if cfg := config.Get(); cfg != nil && cfg.CkbLeadAPIKey != "" {
|
||||
leadKey = cfg.CkbLeadAPIKey
|
||||
}
|
||||
|
||||
// 去重限频:2 分钟内同一用户/手机/微信只能提交一次
|
||||
var cond []string
|
||||
var args []interface{}
|
||||
if body.UserID != "" {
|
||||
cond = append(cond, "user_id = ?")
|
||||
args = append(args, body.UserID)
|
||||
}
|
||||
cond = append(cond, "phone = ?")
|
||||
args = append(args, phone)
|
||||
cutoff := time.Now().Add(-2 * time.Minute)
|
||||
var recentCount int64
|
||||
if db.Model(&model.CkbLeadRecord{}).Where(strings.Join(cond, " OR "), args...).Where("created_at > ?", cutoff).Count(&recentCount) == nil && recentCount > 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "您操作太频繁,请2分钟后再试"})
|
||||
return
|
||||
}
|
||||
repeatedSubmit := false
|
||||
var existCount int64
|
||||
repeatedSubmit = db.Model(&model.CkbLeadRecord{}).Where(strings.Join(cond, " OR "), args...).Count(&existCount) == nil && existCount > 0
|
||||
|
||||
source := "index_link_button"
|
||||
paramsJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"userId": body.UserID, "phone": phone, "wechatId": wechatId, "name": body.Name,
|
||||
"source": source,
|
||||
})
|
||||
_ = db.Create(&model.CkbLeadRecord{
|
||||
UserID: body.UserID,
|
||||
Nickname: name,
|
||||
Phone: phone,
|
||||
WechatID: wechatId,
|
||||
Name: strings.TrimSpace(body.Name),
|
||||
Source: source,
|
||||
Params: string(paramsJSON),
|
||||
}).Error
|
||||
|
||||
ts := time.Now().Unix()
|
||||
params := map[string]interface{}{
|
||||
"name": name,
|
||||
"timestamp": ts,
|
||||
"apiKey": leadKey,
|
||||
}
|
||||
params["phone"] = phone
|
||||
params["sign"] = ckbSign(params, leadKey)
|
||||
q := url.Values{}
|
||||
q.Set("name", name)
|
||||
q.Set("timestamp", strconv.FormatInt(ts, 10))
|
||||
q.Set("apiKey", leadKey)
|
||||
q.Set("phone", phone)
|
||||
q.Set("sign", params["sign"].(string))
|
||||
reqURL := ckbAPIURL + "?" + q.Encode()
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
fmt.Printf("[CKBIndexLead] 请求存客宝失败: %v\n", err)
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "网络异常,请稍后重试"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(b, &result)
|
||||
if result.Code == 200 {
|
||||
msg := "提交成功,卡若会尽快联系您"
|
||||
if repeatedSubmit {
|
||||
msg = "您已留资过,我们已再次通知卡若,请耐心等待添加"
|
||||
}
|
||||
data := gin.H{}
|
||||
if result.Data != nil {
|
||||
if m, ok := result.Data.(map[string]interface{}); ok {
|
||||
data = m
|
||||
}
|
||||
}
|
||||
data["repeatedSubmit"] = repeatedSubmit
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
|
||||
return
|
||||
}
|
||||
// 存客宝返回失败,透传其错误信息与 code,便于前端/运营判断原因
|
||||
errMsg := strings.TrimSpace(result.Message)
|
||||
if errMsg == "" {
|
||||
errMsg = "提交失败,请稍后重试"
|
||||
}
|
||||
fmt.Printf("[CKBIndexLead] 存客宝返回异常 code=%d message=%s raw=%s\n", result.Code, result.Message, string(b))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": errMsg,
|
||||
"ckbCode": result.Code,
|
||||
"ckbMessage": result.Message,
|
||||
})
|
||||
}
|
||||
|
||||
// CKBLead POST /api/miniprogram/ckb/lead 小程序留资加好友:链接卡若(首页)或文章@某人(点击 mention)
|
||||
// 请求体:phone/wechatId(至少一个)、userId(补全昵称)、targetUserId(被@的 personId)、targetNickname、source
|
||||
func CKBLead(c *gin.Context) {
|
||||
|
||||
@@ -2,7 +2,11 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
@@ -13,13 +17,15 @@ import (
|
||||
|
||||
// listSelectCols 列表/导出不加载 content,大幅加速
|
||||
var listSelectCols = []string{
|
||||
"id", "section_title", "price", "is_free", "is_new",
|
||||
"id", "mid", "section_title", "price", "is_free", "is_new",
|
||||
"part_id", "part_title", "chapter_id", "chapter_title", "sort_order",
|
||||
"hot_score", "updated_at",
|
||||
}
|
||||
|
||||
// sectionListItem 与前端 SectionListItem 一致(小写驼峰)
|
||||
type sectionListItem struct {
|
||||
ID string `json:"id"`
|
||||
MID int `json:"mid,omitempty"` // 自增主键,小程序跳转用
|
||||
Title string `json:"title"`
|
||||
Price float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree,omitempty"`
|
||||
@@ -29,6 +35,166 @@ type sectionListItem struct {
|
||||
ChapterID string `json:"chapterId"`
|
||||
ChapterTitle string `json:"chapterTitle"`
|
||||
FilePath *string `json:"filePath,omitempty"`
|
||||
ClickCount int64 `json:"clickCount"` // 阅读次数(reading_progress)
|
||||
PayCount int64 `json:"payCount"` // 付款笔数(orders.product_type=section)
|
||||
HotScore float64 `json:"hotScore"` // 热度积分(加权计算)
|
||||
IsPinned bool `json:"isPinned,omitempty"` // 是否置顶(仅 ranking 返回)
|
||||
}
|
||||
|
||||
// computeSectionListWithHotScore 计算章节列表(含 hotScore),保持 sort_order 顺序,供 章节管理 树使用
|
||||
func computeSectionListWithHotScore(db *gorm.DB) ([]sectionListItem, error) {
|
||||
sections, err := computeSectionsWithHotScore(db, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sections, nil
|
||||
}
|
||||
|
||||
// computeArticleRankingSections 统一计算内容排行榜:置顶优先 + 按 hotScore 降序
|
||||
// 供管理端内容排行榜页与小程序首页精选推荐共用,排序与置顶均在后端计算
|
||||
func computeArticleRankingSections(db *gorm.DB) ([]sectionListItem, error) {
|
||||
sections, err := computeSectionsWithHotScore(db, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 读取置顶配置 pinned_section_ids
|
||||
pinnedIDs := []string{}
|
||||
var cfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "pinned_section_ids").First(&cfg).Error; err == nil && len(cfg.ConfigValue) > 0 {
|
||||
_ = json.Unmarshal(cfg.ConfigValue, &pinnedIDs)
|
||||
}
|
||||
pinnedSet := make(map[string]int) // id -> 置顶顺序
|
||||
for i, id := range pinnedIDs {
|
||||
if id != "" {
|
||||
pinnedSet[id] = i
|
||||
}
|
||||
}
|
||||
// 排序:置顶优先(按置顶顺序),其次按 hotScore 降序
|
||||
sort.Slice(sections, func(i, j int) bool {
|
||||
pi, pj := pinnedSet[sections[i].ID], pinnedSet[sections[j].ID]
|
||||
piOk, pjOk := sections[i].IsPinned, sections[j].IsPinned
|
||||
if piOk && !pjOk {
|
||||
return true
|
||||
}
|
||||
if !piOk && pjOk {
|
||||
return false
|
||||
}
|
||||
if piOk && pjOk {
|
||||
return pi < pj
|
||||
}
|
||||
return sections[i].HotScore > sections[j].HotScore
|
||||
})
|
||||
return sections, nil
|
||||
}
|
||||
|
||||
// computeSectionsWithHotScore 内部:计算 hotScore,可选设置 isPinned
|
||||
func computeSectionsWithHotScore(db *gorm.DB, setPinned bool) ([]sectionListItem, error) {
|
||||
var rows []model.Chapter
|
||||
if err := db.Select(listSelectCols).Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]string, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ids = append(ids, r.ID)
|
||||
}
|
||||
readCountMap := make(map[string]int64)
|
||||
if len(ids) > 0 {
|
||||
var rp []struct {
|
||||
SectionID string `gorm:"column:section_id"`
|
||||
Cnt int64 `gorm:"column:cnt"`
|
||||
}
|
||||
db.Table("reading_progress").Select("section_id, COUNT(*) as cnt").
|
||||
Where("section_id IN ?", ids).Group("section_id").Scan(&rp)
|
||||
for _, r := range rp {
|
||||
readCountMap[r.SectionID] = r.Cnt
|
||||
}
|
||||
}
|
||||
payCountMap := make(map[string]int64)
|
||||
if len(ids) > 0 {
|
||||
var op []struct {
|
||||
ProductID string `gorm:"column:product_id"`
|
||||
Cnt int64 `gorm:"column:cnt"`
|
||||
}
|
||||
db.Model(&model.Order{}).
|
||||
Select("product_id, COUNT(*) as cnt").
|
||||
Where("product_type = ? AND product_id IN ? AND status IN ?", "section", ids, []string{"paid", "completed", "success"}).
|
||||
Group("product_id").Scan(&op)
|
||||
for _, r := range op {
|
||||
payCountMap[r.ProductID] = r.Cnt
|
||||
}
|
||||
}
|
||||
readWeight, payWeight, recencyWeight := 0.5, 0.3, 0.2
|
||||
var cfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "article_ranking_weights").First(&cfg).Error; err == nil && len(cfg.ConfigValue) > 0 {
|
||||
var v struct {
|
||||
ReadWeight float64 `json:"readWeight"`
|
||||
RecencyWeight float64 `json:"recencyWeight"`
|
||||
PayWeight float64 `json:"payWeight"`
|
||||
}
|
||||
if err := json.Unmarshal(cfg.ConfigValue, &v); err == nil {
|
||||
if v.ReadWeight > 0 {
|
||||
readWeight = v.ReadWeight
|
||||
}
|
||||
if v.PayWeight > 0 {
|
||||
payWeight = v.PayWeight
|
||||
}
|
||||
if v.RecencyWeight > 0 {
|
||||
recencyWeight = v.RecencyWeight
|
||||
}
|
||||
}
|
||||
}
|
||||
pinnedIDs := []string{}
|
||||
if setPinned {
|
||||
var cfg2 model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "pinned_section_ids").First(&cfg2).Error; err == nil && len(cfg2.ConfigValue) > 0 {
|
||||
_ = json.Unmarshal(cfg2.ConfigValue, &pinnedIDs)
|
||||
}
|
||||
}
|
||||
pinnedSet := make(map[string]bool)
|
||||
for _, id := range pinnedIDs {
|
||||
if id != "" {
|
||||
pinnedSet[id] = true
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
sections := make([]sectionListItem, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
price := 1.0
|
||||
if r.Price != nil {
|
||||
price = *r.Price
|
||||
}
|
||||
readCnt := readCountMap[r.ID]
|
||||
payCnt := payCountMap[r.ID]
|
||||
recencyScore := 0.0
|
||||
if !r.UpdatedAt.IsZero() {
|
||||
days := now.Sub(r.UpdatedAt).Hours() / 24
|
||||
recencyScore = math.Max(0, (30-days)/30)
|
||||
if recencyScore > 1 {
|
||||
recencyScore = 1
|
||||
}
|
||||
}
|
||||
hot := float64(readCnt)*readWeight + float64(payCnt)*payWeight + recencyScore*recencyWeight
|
||||
item := sectionListItem{
|
||||
ID: r.ID,
|
||||
MID: r.MID,
|
||||
Title: r.SectionTitle,
|
||||
Price: price,
|
||||
IsFree: r.IsFree,
|
||||
IsNew: r.IsNew,
|
||||
PartID: r.PartID,
|
||||
PartTitle: r.PartTitle,
|
||||
ChapterID: r.ChapterID,
|
||||
ChapterTitle: r.ChapterTitle,
|
||||
ClickCount: readCnt,
|
||||
PayCount: payCnt,
|
||||
HotScore: hot,
|
||||
}
|
||||
if setPinned {
|
||||
item.IsPinned = pinnedSet[r.ID]
|
||||
}
|
||||
sections = append(sections, item)
|
||||
}
|
||||
return sections, nil
|
||||
}
|
||||
|
||||
// DBBookAction GET/POST/PUT /api/db/book
|
||||
@@ -40,28 +206,20 @@ func DBBookAction(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
switch action {
|
||||
case "list":
|
||||
var rows []model.Chapter
|
||||
if err := db.Select(listSelectCols).Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
// 章节管理树:按 sort_order 顺序,含 hotScore
|
||||
sections, err := computeSectionListWithHotScore(db)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "sections": []sectionListItem{}})
|
||||
return
|
||||
}
|
||||
sections := make([]sectionListItem, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
price := 1.0
|
||||
if r.Price != nil {
|
||||
price = *r.Price
|
||||
}
|
||||
sections = append(sections, sectionListItem{
|
||||
ID: r.ID,
|
||||
Title: r.SectionTitle,
|
||||
Price: price,
|
||||
IsFree: r.IsFree,
|
||||
IsNew: r.IsNew,
|
||||
PartID: r.PartID,
|
||||
PartTitle: r.PartTitle,
|
||||
ChapterID: r.ChapterID,
|
||||
ChapterTitle: r.ChapterTitle,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections, "total": len(sections)})
|
||||
return
|
||||
case "ranking":
|
||||
// 内容排行榜:置顶优先 + hotScore 降序,排序由后端统一计算,前端只展示
|
||||
sections, err := computeArticleRankingSections(db)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "sections": []sectionListItem{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections, "total": len(sections)})
|
||||
return
|
||||
|
||||
@@ -312,6 +312,10 @@ func formatVipMember(u *model.User, isVip bool) gin.H {
|
||||
if u.VipRole != nil {
|
||||
vipRole = *u.VipRole
|
||||
}
|
||||
vipSort := 0
|
||||
if u.VipSort != nil {
|
||||
vipSort = *u.VipSort
|
||||
}
|
||||
return gin.H{
|
||||
"id": u.ID,
|
||||
"name": name,
|
||||
@@ -343,12 +347,14 @@ func formatVipMember(u *model.User, isVip bool) gin.H {
|
||||
"story_achievement": getStringValue(u.StoryAchievement),
|
||||
"storyTurning": getStringValue(u.StoryTurning),
|
||||
"story_turning": getStringValue(u.StoryTurning),
|
||||
"helpOffer": getStringValue(u.HelpOffer),
|
||||
"helpOffer": getStringValue(u.HelpOffer),
|
||||
"help_offer": getStringValue(u.HelpOffer),
|
||||
"helpNeed": getStringValue(u.HelpNeed),
|
||||
"help_need": getStringValue(u.HelpNeed),
|
||||
"projectIntro": getStringValue(u.ProjectIntro),
|
||||
"project_intro": getStringValue(u.ProjectIntro),
|
||||
"project_intro": getStringValue(u.ProjectIntro),
|
||||
"vipSort": vipSort,
|
||||
"vip_sort": vipSort,
|
||||
"is_vip": isVip,
|
||||
}
|
||||
}
|
||||
|
||||
54
soul-api/internal/handler/vip_members_admin.go
Normal file
54
soul-api/internal/handler/vip_members_admin.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DBVipMembersList GET /api/db/vip-members 管理端 - VIP 成员列表(用于超级个体排序)
|
||||
// 与小程序端 VipMembers 的列表逻辑保持一致:仅列出仍在有效期内的 VIP 用户。
|
||||
func DBVipMembersList(c *gin.Context) {
|
||||
limit := 200
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if n, err := parseInt(l); err == nil && n > 0 && n <= 500 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
|
||||
// 与 VipMembers 一致:优先 users 表(is_vip=1 且 vip_expire_date>NOW),排序使用 vip_sort
|
||||
var users []model.User
|
||||
err := db.Table("users").
|
||||
Select("id", "nickname", "avatar", "vip_name", "vip_role", "vip_project", "vip_avatar", "vip_bio", "vip_activated_at", "vip_sort", "vip_expire_date", "is_vip", "phone", "wechat_id").
|
||||
Where("is_vip = 1 AND vip_expire_date > ?", time.Now()).
|
||||
Order("COALESCE(vip_sort, 999999) ASC, COALESCE(vip_activated_at, vip_expire_date) DESC").
|
||||
Limit(limit).
|
||||
Find(&users).Error
|
||||
|
||||
if err != nil || len(users) == 0 {
|
||||
// 兜底:从 orders 查,逻辑与 VipMembers 保持一致
|
||||
var userIDs []string
|
||||
db.Model(&model.Order{}).Select("DISTINCT user_id").
|
||||
Where("(status = ? OR status = ?) AND (product_type = ? OR product_type = ?)", "paid", "completed", "fullbook", "vip").
|
||||
Pluck("user_id", &userIDs)
|
||||
if len(userIDs) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}, "total": 0})
|
||||
return
|
||||
}
|
||||
db.Where("id IN ?", userIDs).Find(&users)
|
||||
}
|
||||
|
||||
list := make([]gin.H, 0, len(users))
|
||||
for i := range users {
|
||||
list = append(list, formatVipMember(&users[i], true))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
db.POST("/vip-roles", handler.DBVipRolesAction)
|
||||
db.PUT("/vip-roles", handler.DBVipRolesAction)
|
||||
db.DELETE("/vip-roles", handler.DBVipRolesAction)
|
||||
db.GET("/vip-members", handler.DBVipMembersList)
|
||||
db.GET("/match-records", handler.DBMatchRecordsList)
|
||||
db.GET("/match-pool-counts", handler.DBMatchPoolCounts)
|
||||
db.GET("/mentors", handler.DBMentorsList)
|
||||
@@ -267,6 +268,7 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
miniprogram.POST("/ckb/join", handler.CKBJoin)
|
||||
miniprogram.POST("/ckb/match", handler.CKBMatch)
|
||||
miniprogram.POST("/ckb/lead", handler.CKBLead)
|
||||
miniprogram.POST("/ckb/index-lead", handler.CKBIndexLead)
|
||||
miniprogram.POST("/upload", handler.UploadPost)
|
||||
miniprogram.DELETE("/upload", handler.UploadDelete)
|
||||
miniprogram.GET("/user/addresses", handler.UserAddressesGet)
|
||||
|
||||
Reference in New Issue
Block a user