Files
soul-yongping/soul-api/internal/handler/db_ckb_leads.go
卡若 80e397f7ac feat: 运营-用户功能四大需求完整实现
1. 客资中心:Dashboard 聚合 CKB 线索+提交记录,联表用户信息
2. @置顶:Person 三端(后端+管理端+小程序)置顶功能,首页优先展示
3. 存客宝场景:一键检查并自动启用所有场景获客计划
4. 去重增强:后端聚合 dupCount,管理端展示重复标记和统计
5. 首页文案:"最新更新"→"推荐","开始阅读"→"点击阅读"

Made-with: Cursor
2026-03-19 16:20:46 +08:00

280 lines
8.1 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"
"strconv"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
)
// DBCKBLeadList GET /api/db/ckb-leads 管理端-CKB线索明细
// mode=submitted: ckb_submit_recordsjoin/match 提交)
// mode=contact: ckb_lead_records链接卡若留资有 phone/wechat
func DBCKBLeadList(c *gin.Context) {
db := database.DB()
mode := c.DefaultQuery("mode", "submitted")
matchType := c.Query("matchType")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
dedup := c.DefaultQuery("dedup", "true")
if mode == "contact" {
q := db.Model(&model.CkbLeadRecord{})
var total int64
var records []model.CkbLeadRecord
if dedup == "true" {
subQ := db.Model(&model.CkbLeadRecord{}).
Select("MAX(id) as id").
Group("COALESCE(NULLIF(user_id,''), COALESCE(NULLIF(phone,''), COALESCE(NULLIF(wechat_id,''), CAST(id AS CHAR))))")
q = db.Model(&model.CkbLeadRecord{}).Where("id IN (?)", subQ)
}
q.Count(&total)
if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&records).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
// 查询每条记录的重复次数
dupCounts := make(map[string]int64)
if dedup == "true" && len(records) > 0 {
for _, r := range records {
key := r.UserID
if key == "" {
key = r.Phone
}
if key == "" {
key = r.WechatID
}
if key != "" {
var cnt int64
cntQ := db.Model(&model.CkbLeadRecord{})
if r.UserID != "" {
cntQ = cntQ.Where("user_id = ?", r.UserID)
} else if r.Phone != "" {
cntQ = cntQ.Where("phone = ?", r.Phone)
} else if r.WechatID != "" {
cntQ = cntQ.Where("wechat_id = ?", r.WechatID)
}
cntQ.Count(&cnt)
dupCounts[key] = cnt
}
}
}
out := make([]gin.H, 0, len(records))
for _, r := range records {
key := r.UserID
if key == "" {
key = r.Phone
}
if key == "" {
key = r.WechatID
}
dupCount := dupCounts[key]
if dupCount <= 1 {
dupCount = 0
}
out = append(out, gin.H{
"id": r.ID,
"userId": r.UserID,
"userNickname": r.Nickname,
"matchType": "lead",
"phone": r.Phone,
"wechatId": r.WechatID,
"name": r.Name,
"createdAt": r.CreatedAt,
"dupCount": dupCount,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "records": out, "total": total, "page": page, "pageSize": pageSize})
return
}
q := db.Model(&model.CkbSubmitRecord{})
if matchType != "" {
if matchType == "join" || matchType == "match" {
q = q.Where("action = ?", matchType)
}
}
if dedup == "true" {
subQ := db.Model(&model.CkbSubmitRecord{}).
Select("MAX(id) as id").
Group("COALESCE(NULLIF(user_id,''), CAST(id AS CHAR))")
if matchType == "join" || matchType == "match" {
subQ = subQ.Where("action = ?", matchType)
}
q = db.Model(&model.CkbSubmitRecord{}).Where("id IN (?)", subQ)
}
var total int64
q.Count(&total)
var records []model.CkbSubmitRecord
if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&records).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
userIDs := make(map[string]bool)
for _, r := range records {
if r.UserID != "" {
userIDs[r.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.Where("id IN ?", ids).Find(&users)
}
userMap := make(map[string]*model.User)
for i := range users {
userMap[users[i].ID] = &users[i]
}
safeNickname := func(u *model.User) string {
if u == nil || u.Nickname == nil {
return ""
}
return *u.Nickname
}
out := make([]gin.H, 0, len(records))
for _, r := range records {
out = append(out, gin.H{
"id": r.ID,
"userId": r.UserID,
"userNickname": safeNickname(userMap[r.UserID]),
"matchType": r.Action,
"nickname": r.Nickname,
"params": r.Params,
"createdAt": r.CreatedAt,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "records": out, "total": total, "page": page, "pageSize": pageSize})
}
// CKBPersonLeadStats GET /api/db/ckb-person-leads 每个人物的获客线索统计及明细
func CKBPersonLeadStats(c *gin.Context) {
db := database.DB()
personToken := c.Query("token")
if personToken != "" {
// 返回某人物的线索明细(通过 token → Person → 用 PersonID 和 Token 匹配 CkbLeadRecord.TargetPersonID
var person model.Person
if err := db.Where("token = ?", personToken).First(&person).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "人物不存在"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
q := db.Model(&model.CkbLeadRecord{}).Where("target_person_id IN ?", []string{person.PersonID, person.Token})
var total int64
q.Count(&total)
var records []model.CkbLeadRecord
q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&records)
out := make([]gin.H, 0, len(records))
for _, r := range records {
out = append(out, gin.H{
"id": r.ID,
"userId": r.UserID,
"nickname": r.Nickname,
"phone": r.Phone,
"wechatId": r.WechatID,
"name": r.Name,
"source": r.Source,
"createdAt": r.CreatedAt,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"personName": person.Name,
"records": out,
"total": total,
"page": page,
"pageSize": pageSize,
})
return
}
// 无 token 参数:返回所有人物的获客数量汇总
type PersonLeadStat struct {
TargetPersonID string `gorm:"column:target_person_id"`
Total int64 `gorm:"column:total"`
}
var stats []PersonLeadStat
db.Raw("SELECT target_person_id, COUNT(*) as total FROM ckb_lead_records WHERE target_person_id != '' GROUP BY target_person_id").Scan(&stats)
// 构建 personId/token → Person.Token 的映射,使前端能用 token 匹配
var persons []model.Person
db.Select("person_id, token").Find(&persons)
pidToToken := make(map[string]string, len(persons))
for _, p := range persons {
pidToToken[p.PersonID] = p.Token
pidToToken[p.Token] = p.Token
}
merged := make(map[string]int64)
for _, s := range stats {
key := pidToToken[s.TargetPersonID]
if key == "" {
key = s.TargetPersonID
}
merged[key] += s.Total
}
byPerson := make([]gin.H, 0, len(merged))
for token, total := range merged {
byPerson = append(byPerson, gin.H{"token": token, "total": total})
}
// 同时统计全局(无特定人物的)线索
var globalTotal int64
db.Model(&model.CkbLeadRecord{}).Where("target_person_id = '' OR target_person_id IS NULL").Count(&globalTotal)
c.JSON(http.StatusOK, gin.H{
"success": true,
"byPerson": byPerson,
"globalLeads": globalTotal,
})
}
// CKBPlanStats GET /api/db/ckb-plan-stats 存客宝获客计划统计(基于 ckb_submit_records + ckb_lead_records
func CKBPlanStats(c *gin.Context) {
db := database.DB()
type TypeStat struct {
Action string `gorm:"column:action" json:"matchType"`
Total int64 `gorm:"column:total" json:"total"`
}
var submitStats []TypeStat
db.Raw("SELECT action, COUNT(*) as total FROM ckb_submit_records GROUP BY action").Scan(&submitStats)
var submitTotal int64
db.Model(&model.CkbSubmitRecord{}).Count(&submitTotal)
var leadTotal int64
db.Model(&model.CkbLeadRecord{}).Count(&leadTotal)
withContact := leadTotal // ckb_lead_records 均有 phone 或 wechat
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"ckbTotal": submitTotal + leadTotal,
"withContact": withContact,
"byType": submitStats,
"ckbApiKey": "***",
"ckbApiUrl": "https://ckbapi.quwanzhi.com/v1/api/scenarios",
"docNotes": "",
"docContent": "",
"routes": gin.H{},
},
})
}