Files
soul-yongping/soul-api/internal/handler/vip_members_admin.go
卡若 5724fba877 feat: 小程序超级个体/个人资料/CKB获客;VIP列表展示过滤;管理端与API联调
- 超级个体:去掉首位特例;列表仅展示有头像且非微信默认昵称(vip.go)
- 个人资料:居中头像、低调联系方式、点头像优先走存客宝 lead(ckbLeadToken)
- 阅读页分享朋友圈复制与 toast 去重
- soul-api: miniprogram users 带 ckbLeadToken;其它 handler 与路由调整
- 脚本:content_upload、miniprogram 上传辅助等

Made-with: Cursor
2026-03-22 08:34:28 +08:00

118 lines
3.6 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 (
"net/http"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// batchSuperIndividualClicks 与 AdminSuperIndividualStats 一致user_tracks 中 action=card_click 且 target 前缀「超级个体_」
func batchSuperIndividualClicks(db *gorm.DB, userIDs []string) map[string]int64 {
out := make(map[string]int64)
if len(userIDs) == 0 {
return out
}
type row struct {
UserID string `gorm:"column:user_id"`
Clicks int64 `gorm:"column:clicks"`
}
var rows []row
_ = db.Raw(`
SELECT SUBSTRING(target, 6) AS user_id, COUNT(*) AS clicks
FROM user_tracks
WHERE action = 'card_click' AND target LIKE '超级个体\_%' AND SUBSTRING(target, 6) IN ?
GROUP BY user_id
`, userIDs).Scan(&rows)
for _, r := range rows {
if r.UserID != "" {
out[r.UserID] = r.Clicks
}
}
return out
}
// batchSuperIndividualLeads 与 AdminSuperIndividualStats 一致persons.user_id 绑定 + ckb_lead_records 去重获客人数
func batchSuperIndividualLeads(db *gorm.DB, userIDs []string) map[string]int64 {
out := make(map[string]int64)
if len(userIDs) == 0 {
return out
}
type row struct {
UserID string `gorm:"column:user_id"`
Leads int64 `gorm:"column:leads"`
}
var rows []row
_ = db.Raw(`
SELECT p.user_id AS user_id, COUNT(DISTINCT l.user_id) AS leads
FROM persons p
INNER JOIN ckb_lead_records l ON l.target_person_id = p.person_id
WHERE p.user_id IN ?
GROUP BY p.user_id
`, userIDs).Scan(&rows)
for _, r := range rows {
if r.UserID != "" {
out[r.UserID] = r.Leads
}
}
return out
}
// DBVipMembersList GET /api/db/vip-members 管理端 - VIP 成员列表(用于超级个体排序)
// 与小程序端 VipMembers 的列表逻辑保持一致:仅列出仍在有效期内的 VIP 用户。
// 额外聚合clickCount首页超级个体卡片点击、leadCount绑定人物后的去重获客供管理端表格展示。
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)
}
ids := make([]string, 0, len(users))
for i := range users {
ids = append(ids, users[i].ID)
}
clickByUser := batchSuperIndividualClicks(db, ids)
leadByUser := batchSuperIndividualLeads(db, ids)
list := make([]gin.H, 0, len(users))
for i := range users {
item := formatVipMember(db, &users[i], true)
uid := users[i].ID
item["clickCount"] = clickByUser[uid]
item["leadCount"] = leadByUser[uid]
list = append(list, item)
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
}