2026-03-10 18:06:10 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"net/http"
|
2026-03-17 18:22:06 +08:00
|
|
|
|
"strconv"
|
2026-03-10 18:06:10 +08:00
|
|
|
|
"sync"
|
2026-03-17 18:22:06 +08:00
|
|
|
|
"time"
|
2026-03-10 18:06:10 +08:00
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
2026-03-14 16:23:01 +08:00
|
|
|
|
"soul-api/internal/wechat"
|
2026-03-10 18:06:10 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var paidStatuses = []string{"paid", "completed", "success"}
|
|
|
|
|
|
|
|
|
|
|
|
// AdminDashboardStats GET /api/admin/dashboard/stats
|
|
|
|
|
|
// 轻量聚合:总用户、付费订单数、付费用户数、总营收、转化率(无订单/用户明细)
|
|
|
|
|
|
func AdminDashboardStats(c *gin.Context) {
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
var (
|
|
|
|
|
|
totalUsers int64
|
|
|
|
|
|
paidOrderCount int64
|
|
|
|
|
|
totalRevenue float64
|
|
|
|
|
|
paidUserCount int64
|
|
|
|
|
|
)
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
wg.Add(4)
|
|
|
|
|
|
go func() { defer wg.Done(); db.Model(&model.User{}).Count(&totalUsers) }()
|
|
|
|
|
|
go func() { defer wg.Done(); db.Model(&model.Order{}).Where("status IN ?", paidStatuses).Count(&paidOrderCount) }()
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
db.Model(&model.Order{}).Where("status IN ?", paidStatuses).
|
|
|
|
|
|
Select("COALESCE(SUM(amount), 0)").Scan(&totalRevenue)
|
|
|
|
|
|
}()
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
db.Table("orders").Where("status IN ?", paidStatuses).
|
|
|
|
|
|
Select("COUNT(DISTINCT user_id)").Scan(&paidUserCount)
|
|
|
|
|
|
}()
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
conversionRate := 0.0
|
|
|
|
|
|
if totalUsers > 0 && paidUserCount > 0 {
|
|
|
|
|
|
conversionRate = float64(paidUserCount) / float64(totalUsers) * 100
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"totalUsers": totalUsers,
|
|
|
|
|
|
"paidOrderCount": paidOrderCount,
|
|
|
|
|
|
"paidUserCount": paidUserCount,
|
|
|
|
|
|
"totalRevenue": totalRevenue,
|
|
|
|
|
|
"conversionRate": conversionRate,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:22:06 +08:00
|
|
|
|
// AdminDashboardRecentOrders GET /api/admin/dashboard/recent-orders?limit=10
|
2026-03-10 18:06:10 +08:00
|
|
|
|
func AdminDashboardRecentOrders(c *gin.Context) {
|
|
|
|
|
|
db := database.DB()
|
2026-03-17 18:22:06 +08:00
|
|
|
|
limit := 5
|
|
|
|
|
|
if l := c.Query("limit"); l != "" {
|
|
|
|
|
|
if n, err := strconv.Atoi(l); err == nil && n >= 1 && n <= 20 {
|
|
|
|
|
|
limit = n
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 18:06:10 +08:00
|
|
|
|
var recentOrders []model.Order
|
2026-03-17 18:22:06 +08:00
|
|
|
|
db.Where("status IN ?", paidStatuses).Order("created_at DESC").Limit(limit).Find(&recentOrders)
|
2026-03-10 18:06:10 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "recentOrders": buildRecentOrdersOut(db, recentOrders)})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AdminDashboardNewUsers GET /api/admin/dashboard/new-users
|
|
|
|
|
|
func AdminDashboardNewUsers(c *gin.Context) {
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
var newUsers []model.User
|
|
|
|
|
|
db.Model(&model.User{}).Order("created_at DESC").Limit(10).Find(&newUsers)
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "newUsers": buildNewUsersOut(newUsers)})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AdminDashboardOverview GET /api/admin/dashboard/overview
|
|
|
|
|
|
// 数据概览:总用户、付费订单数、付费用户数、总营收、转化率、最近订单、新用户
|
|
|
|
|
|
// 优化:6 组查询并行执行,减少总耗时
|
|
|
|
|
|
func AdminDashboardOverview(c *gin.Context) {
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
totalUsers int64
|
|
|
|
|
|
paidOrderCount int64
|
|
|
|
|
|
totalRevenue float64
|
|
|
|
|
|
paidUserCount int64
|
|
|
|
|
|
recentOrders []model.Order
|
|
|
|
|
|
newUsers []model.User
|
|
|
|
|
|
)
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
wg.Add(6)
|
|
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
db.Model(&model.User{}).Count(&totalUsers)
|
|
|
|
|
|
}()
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
db.Model(&model.Order{}).Where("status IN ?", paidStatuses).Count(&paidOrderCount)
|
|
|
|
|
|
}()
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
db.Model(&model.Order{}).Where("status IN ?", paidStatuses).
|
|
|
|
|
|
Select("COALESCE(SUM(amount), 0)").Scan(&totalRevenue)
|
|
|
|
|
|
}()
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
db.Table("orders").Where("status IN ?", paidStatuses).
|
|
|
|
|
|
Select("COUNT(DISTINCT user_id)").Scan(&paidUserCount)
|
|
|
|
|
|
}()
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
db.Where("status IN ?", paidStatuses).
|
|
|
|
|
|
Order("created_at DESC").Limit(5).Find(&recentOrders)
|
|
|
|
|
|
}()
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
db.Model(&model.User{}).Order("created_at DESC").Limit(10).Find(&newUsers)
|
|
|
|
|
|
}()
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
|
|
|
|
conversionRate := 0.0
|
|
|
|
|
|
if totalUsers > 0 && paidUserCount > 0 {
|
|
|
|
|
|
conversionRate = float64(paidUserCount) / float64(totalUsers) * 100
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
recentOut := buildRecentOrdersOut(db, recentOrders)
|
|
|
|
|
|
newOut := buildNewUsersOut(newUsers)
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"totalUsers": totalUsers,
|
|
|
|
|
|
"paidOrderCount": paidOrderCount,
|
|
|
|
|
|
"paidUserCount": paidUserCount,
|
|
|
|
|
|
"totalRevenue": totalRevenue,
|
|
|
|
|
|
"conversionRate": conversionRate,
|
|
|
|
|
|
"recentOrders": recentOut,
|
|
|
|
|
|
"newUsers": newOut,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func dashStr(s *string) string {
|
|
|
|
|
|
if s == nil || *s == "" {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
return *s
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildRecentOrdersOut(db *gorm.DB, recentOrders []model.Order) []gin.H {
|
|
|
|
|
|
if len(recentOrders) == 0 {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
userIDs := make(map[string]bool)
|
|
|
|
|
|
for _, o := range recentOrders {
|
|
|
|
|
|
if o.UserID != "" {
|
|
|
|
|
|
userIDs[o.UserID] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ids := make([]string, 0, len(userIDs))
|
|
|
|
|
|
for id := range userIDs {
|
|
|
|
|
|
ids = append(ids, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
var users []model.User
|
|
|
|
|
|
db.Where("id IN ?", ids).Find(&users)
|
|
|
|
|
|
userMap := make(map[string]*model.User)
|
|
|
|
|
|
for i := range users {
|
|
|
|
|
|
userMap[users[i].ID] = &users[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
out := make([]gin.H, 0, len(recentOrders))
|
|
|
|
|
|
for _, o := range recentOrders {
|
|
|
|
|
|
b, _ := json.Marshal(o)
|
|
|
|
|
|
var m map[string]interface{}
|
|
|
|
|
|
_ = json.Unmarshal(b, &m)
|
|
|
|
|
|
if u := userMap[o.UserID]; u != nil {
|
|
|
|
|
|
m["userNickname"] = dashStr(u.Nickname)
|
|
|
|
|
|
m["userAvatar"] = dashStr(u.Avatar)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m["userNickname"] = ""
|
|
|
|
|
|
m["userAvatar"] = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, m)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:22:06 +08:00
|
|
|
|
// AdminBalanceSummary GET /api/admin/balance/summary
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 汇总代付金额(product_type 为 gift_pay 或 gift_pay_batch 的已支付订单),用于 Dashboard 显示「含代付 ¥xx」
|
2026-03-17 18:22:06 +08:00
|
|
|
|
func AdminBalanceSummary(c *gin.Context) {
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
var totalGifted float64
|
2026-03-18 12:40:51 +08:00
|
|
|
|
db.Model(&model.Order{}).Where("product_type IN ? AND status IN ?", []string{"gift_pay", "gift_pay_batch"}, paidStatuses).
|
2026-03-17 18:22:06 +08:00
|
|
|
|
Select("COALESCE(SUM(amount), 0)").Scan(&totalGifted)
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalGifted": totalGifted}})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 16:23:01 +08:00
|
|
|
|
// AdminDashboardMerchantBalance GET /api/admin/dashboard/merchant-balance
|
|
|
|
|
|
// 查询微信商户号实时余额(可用余额、待结算余额),用于看板展示
|
|
|
|
|
|
// 注意:普通商户可能需向微信申请开通权限,未开通时返回 error
|
|
|
|
|
|
func AdminDashboardMerchantBalance(c *gin.Context) {
|
|
|
|
|
|
bal, err := wechat.QueryMerchantBalance("BASIC")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
|
"message": "查询商户余额失败,可能未开通权限(请联系微信支付运营申请)",
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"availableAmount": bal.AvailableAmount, // 单位:分
|
|
|
|
|
|
"pendingAmount": bal.PendingAmount, // 单位:分
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 16:20:46 +08:00
|
|
|
|
// AdminDashboardLeads GET /api/admin/dashboard/leads?limit=20
|
|
|
|
|
|
// 管理端-首页客资中心:聚合 ckb_lead_records(链接卡若留资)+ ckb_submit_records(join/match),
|
|
|
|
|
|
// 联表 users 补齐头像/昵称,按时间倒序,每条包含联系方式(phone/wechatId)与来源。
|
|
|
|
|
|
func AdminDashboardLeads(c *gin.Context) {
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
limit := 20
|
|
|
|
|
|
if l := c.Query("limit"); l != "" {
|
|
|
|
|
|
if n, err := strconv.Atoi(l); err == nil && n >= 1 && n <= 100 {
|
|
|
|
|
|
limit = n
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
search := c.Query("search")
|
|
|
|
|
|
|
|
|
|
|
|
// 1. ckb_lead_records(链接卡若 / 文章@)
|
|
|
|
|
|
var leads []model.CkbLeadRecord
|
|
|
|
|
|
qLead := db.Model(&model.CkbLeadRecord{}).Order("created_at DESC")
|
|
|
|
|
|
if search != "" {
|
|
|
|
|
|
qLead = qLead.Where("nickname LIKE ? OR phone LIKE ? OR name LIKE ? OR wechat_id LIKE ?",
|
|
|
|
|
|
"%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%")
|
|
|
|
|
|
}
|
|
|
|
|
|
qLead.Limit(limit).Find(&leads)
|
|
|
|
|
|
|
|
|
|
|
|
// 2. ckb_submit_records(join/match)
|
|
|
|
|
|
var submits []model.CkbSubmitRecord
|
|
|
|
|
|
qSub := db.Model(&model.CkbSubmitRecord{}).Order("created_at DESC")
|
|
|
|
|
|
if search != "" {
|
|
|
|
|
|
qSub = qSub.Where("nickname LIKE ? OR params LIKE ?", "%"+search+"%", "%"+search+"%")
|
|
|
|
|
|
}
|
|
|
|
|
|
qSub.Limit(limit).Find(&submits)
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有 userID 关联用户信息
|
|
|
|
|
|
userIDs := make(map[string]bool)
|
|
|
|
|
|
for _, l := range leads {
|
|
|
|
|
|
if l.UserID != "" {
|
|
|
|
|
|
userIDs[l.UserID] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, s := range submits {
|
|
|
|
|
|
if s.UserID != "" {
|
|
|
|
|
|
userIDs[s.UserID] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ids := make([]string, 0, len(userIDs))
|
|
|
|
|
|
for id := range userIDs {
|
|
|
|
|
|
ids = append(ids, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
var users []model.User
|
|
|
|
|
|
if len(ids) > 0 {
|
|
|
|
|
|
db.Select("id", "nickname", "avatar", "phone", "wechat_id", "is_vip", "tags", "ckb_tags").Where("id IN ?", ids).Find(&users)
|
|
|
|
|
|
}
|
|
|
|
|
|
userMap := make(map[string]*model.User)
|
|
|
|
|
|
for i := range users {
|
|
|
|
|
|
userMap[users[i].ID] = &users[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 统计
|
|
|
|
|
|
var totalLeads, totalSubmits int64
|
|
|
|
|
|
db.Model(&model.CkbLeadRecord{}).Count(&totalLeads)
|
|
|
|
|
|
db.Model(&model.CkbSubmitRecord{}).Count(&totalSubmits)
|
|
|
|
|
|
var withPhone int64
|
|
|
|
|
|
db.Model(&model.CkbLeadRecord{}).Where("phone != '' AND phone IS NOT NULL").Count(&withPhone)
|
|
|
|
|
|
|
|
|
|
|
|
// 去重统计:按 userId/phone/wechatId 聚合重复次数
|
|
|
|
|
|
dupCounts := make(map[string]int64)
|
|
|
|
|
|
for _, l := range leads {
|
|
|
|
|
|
key := l.UserID
|
|
|
|
|
|
if key == "" {
|
|
|
|
|
|
key = l.Phone
|
|
|
|
|
|
}
|
|
|
|
|
|
if key == "" {
|
|
|
|
|
|
key = l.WechatID
|
|
|
|
|
|
}
|
|
|
|
|
|
if key != "" {
|
|
|
|
|
|
if _, ok := dupCounts[key]; !ok {
|
|
|
|
|
|
var cnt int64
|
|
|
|
|
|
q := db.Model(&model.CkbLeadRecord{})
|
|
|
|
|
|
if l.UserID != "" {
|
|
|
|
|
|
q = q.Where("user_id = ?", l.UserID)
|
|
|
|
|
|
} else if l.Phone != "" {
|
|
|
|
|
|
q = q.Where("phone = ?", l.Phone)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
q = q.Where("wechat_id = ?", l.WechatID)
|
|
|
|
|
|
}
|
|
|
|
|
|
q.Count(&cnt)
|
|
|
|
|
|
dupCounts[key] = cnt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构造输出
|
|
|
|
|
|
type leadOut struct {
|
|
|
|
|
|
SortTime time.Time
|
|
|
|
|
|
Data gin.H
|
|
|
|
|
|
}
|
|
|
|
|
|
all := make([]leadOut, 0, len(leads)+len(submits))
|
|
|
|
|
|
for _, l := range leads {
|
|
|
|
|
|
u := userMap[l.UserID]
|
|
|
|
|
|
avatar := ""
|
|
|
|
|
|
userNickname := l.Nickname
|
|
|
|
|
|
if u != nil {
|
|
|
|
|
|
avatar = dashStr(u.Avatar)
|
|
|
|
|
|
if dashStr(u.Nickname) != "" {
|
|
|
|
|
|
userNickname = dashStr(u.Nickname)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
sourceLabel := "链接卡若"
|
|
|
|
|
|
if l.Source == "article_mention" {
|
|
|
|
|
|
sourceLabel = "文章@"
|
|
|
|
|
|
} else if l.Source == "index_link_button" {
|
|
|
|
|
|
sourceLabel = "首页链接"
|
|
|
|
|
|
}
|
|
|
|
|
|
key := l.UserID
|
|
|
|
|
|
if key == "" {
|
|
|
|
|
|
key = l.Phone
|
|
|
|
|
|
}
|
|
|
|
|
|
if key == "" {
|
|
|
|
|
|
key = l.WechatID
|
|
|
|
|
|
}
|
|
|
|
|
|
dupCount := dupCounts[key]
|
|
|
|
|
|
if dupCount <= 1 {
|
|
|
|
|
|
dupCount = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
all = append(all, leadOut{
|
|
|
|
|
|
SortTime: l.CreatedAt,
|
|
|
|
|
|
Data: gin.H{
|
|
|
|
|
|
"id": l.ID,
|
|
|
|
|
|
"type": "lead",
|
|
|
|
|
|
"userId": l.UserID,
|
|
|
|
|
|
"userNickname": userNickname,
|
|
|
|
|
|
"userAvatar": avatar,
|
|
|
|
|
|
"phone": l.Phone,
|
|
|
|
|
|
"wechatId": l.WechatID,
|
|
|
|
|
|
"name": l.Name,
|
|
|
|
|
|
"source": l.Source,
|
|
|
|
|
|
"sourceLabel": sourceLabel,
|
|
|
|
|
|
"createdAt": l.CreatedAt,
|
|
|
|
|
|
"dupCount": dupCount,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, s := range submits {
|
|
|
|
|
|
u := userMap[s.UserID]
|
|
|
|
|
|
avatar := ""
|
|
|
|
|
|
userNickname := s.Nickname
|
|
|
|
|
|
if u != nil {
|
|
|
|
|
|
avatar = dashStr(u.Avatar)
|
|
|
|
|
|
if dashStr(u.Nickname) != "" {
|
|
|
|
|
|
userNickname = dashStr(u.Nickname)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
all = append(all, leadOut{
|
|
|
|
|
|
SortTime: s.CreatedAt,
|
|
|
|
|
|
Data: gin.H{
|
|
|
|
|
|
"id": s.ID,
|
|
|
|
|
|
"type": "submit",
|
|
|
|
|
|
"userId": s.UserID,
|
|
|
|
|
|
"userNickname": userNickname,
|
|
|
|
|
|
"userAvatar": avatar,
|
|
|
|
|
|
"matchType": s.Action,
|
|
|
|
|
|
"source": s.Action,
|
|
|
|
|
|
"sourceLabel": ckbSourceMap[s.Action],
|
|
|
|
|
|
"createdAt": s.CreatedAt,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
// 按时间倒序合并
|
|
|
|
|
|
for i := 0; i < len(all); i++ {
|
|
|
|
|
|
for j := i + 1; j < len(all); j++ {
|
|
|
|
|
|
if all[j].SortTime.After(all[i].SortTime) {
|
|
|
|
|
|
all[i], all[j] = all[j], all[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(all) > limit {
|
|
|
|
|
|
all = all[:limit]
|
|
|
|
|
|
}
|
|
|
|
|
|
out := make([]gin.H, 0, len(all))
|
|
|
|
|
|
for _, a := range all {
|
|
|
|
|
|
out = append(out, a.Data)
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"leads": out,
|
|
|
|
|
|
"totalLeads": totalLeads,
|
|
|
|
|
|
"totalSubmits": totalSubmits,
|
|
|
|
|
|
"withPhone": withPhone,
|
|
|
|
|
|
"total": totalLeads + totalSubmits,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 18:06:10 +08:00
|
|
|
|
func buildNewUsersOut(newUsers []model.User) []gin.H {
|
|
|
|
|
|
out := make([]gin.H, 0, len(newUsers))
|
|
|
|
|
|
for _, u := range newUsers {
|
|
|
|
|
|
out = append(out, gin.H{
|
|
|
|
|
|
"id": u.ID,
|
|
|
|
|
|
"nickname": dashStr(u.Nickname),
|
|
|
|
|
|
"phone": dashStr(u.Phone),
|
|
|
|
|
|
"referralCode": dashStr(u.ReferralCode),
|
|
|
|
|
|
"createdAt": u.CreatedAt,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|