2026-03-10 11:04:34 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-25 15:47:31 +08:00
|
|
|
|
"fmt"
|
2026-03-10 11:04:34 +08:00
|
|
|
|
"net/http"
|
|
|
|
|
|
"strconv"
|
2026-03-24 18:45:32 +08:00
|
|
|
|
"strings"
|
2026-03-10 11:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// DBCKBLeadList GET /api/db/ckb-leads 管理端-CKB线索明细
|
2026-03-26 20:26:35 +08:00
|
|
|
|
// mode=submitted: ckb_lead_records(action=join/match,兼容旧面板命名)
|
2026-03-10 11:04:34 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if mode == "contact" {
|
2026-03-22 08:34:28 +08:00
|
|
|
|
search := c.Query("search")
|
|
|
|
|
|
source := c.Query("source")
|
2026-03-26 20:26:35 +08:00
|
|
|
|
action := strings.TrimSpace(c.Query("action"))
|
2026-03-24 18:45:32 +08:00
|
|
|
|
pushStatus := strings.TrimSpace(c.Query("pushStatus"))
|
2026-03-10 11:04:34 +08:00
|
|
|
|
q := db.Model(&model.CkbLeadRecord{})
|
2026-03-22 08:34:28 +08:00
|
|
|
|
if search != "" {
|
|
|
|
|
|
q = q.Where("nickname LIKE ? OR phone LIKE ? OR wechat_id LIKE ? OR name LIKE ?",
|
|
|
|
|
|
"%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%")
|
|
|
|
|
|
}
|
|
|
|
|
|
if source != "" {
|
|
|
|
|
|
q = q.Where("source = ?", source)
|
|
|
|
|
|
}
|
2026-03-26 20:26:35 +08:00
|
|
|
|
if action != "" {
|
|
|
|
|
|
q = q.Where("action = ?", action)
|
|
|
|
|
|
}
|
2026-03-24 18:45:32 +08:00
|
|
|
|
if pushStatus != "" {
|
|
|
|
|
|
q = q.Where("push_status = ?", pushStatus)
|
|
|
|
|
|
}
|
2026-03-10 11:04:34 +08:00
|
|
|
|
var total int64
|
|
|
|
|
|
q.Count(&total)
|
|
|
|
|
|
var records []model.CkbLeadRecord
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-03-19 18:26:45 +08:00
|
|
|
|
// 批量查 persons 获取 personName、ckbPlanId
|
|
|
|
|
|
personIDs := make([]string, 0)
|
|
|
|
|
|
for _, r := range records {
|
|
|
|
|
|
if r.TargetPersonID != "" {
|
|
|
|
|
|
personIDs = append(personIDs, r.TargetPersonID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
personMap := make(map[string]*model.Person)
|
|
|
|
|
|
if len(personIDs) > 0 {
|
|
|
|
|
|
var persons []model.Person
|
2026-03-22 08:34:28 +08:00
|
|
|
|
db.Where("person_id IN ? OR token IN ?", personIDs, personIDs).Find(&persons)
|
2026-03-19 18:26:45 +08:00
|
|
|
|
for i := range persons {
|
|
|
|
|
|
personMap[persons[i].PersonID] = &persons[i]
|
2026-03-22 08:34:28 +08:00
|
|
|
|
personMap[persons[i].Token] = &persons[i]
|
2026-03-19 18:26:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-25 15:47:31 +08:00
|
|
|
|
// 批量查 users:头像、昵称(与会员资料一致)
|
|
|
|
|
|
uidSet := make(map[string]struct{})
|
|
|
|
|
|
for _, r := range records {
|
|
|
|
|
|
if strings.TrimSpace(r.UserID) != "" {
|
|
|
|
|
|
uidSet[r.UserID] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
uids := make([]string, 0, len(uidSet))
|
|
|
|
|
|
for id := range uidSet {
|
|
|
|
|
|
uids = append(uids, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
userMap := make(map[string]*model.User)
|
|
|
|
|
|
if len(uids) > 0 {
|
|
|
|
|
|
var urows []model.User
|
|
|
|
|
|
db.Select("id", "nickname", "avatar").Where("id IN ?", uids).Find(&urows)
|
|
|
|
|
|
for i := range urows {
|
|
|
|
|
|
userMap[urows[i].ID] = &urows[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-26 20:26:35 +08:00
|
|
|
|
// 首页 index_link_button 历史数据可能未写 target_person_id:用全局 leadKey 对应 Person 回退展示
|
|
|
|
|
|
var indexLinkFallback *model.Person
|
|
|
|
|
|
leadKey := getCkbLeadApiKey()
|
|
|
|
|
|
if leadKey != "" {
|
|
|
|
|
|
var fp model.Person
|
|
|
|
|
|
if db.Where("ckb_api_key = ? AND ckb_api_key != ''", leadKey).First(&fp).Error == nil {
|
|
|
|
|
|
indexLinkFallback = &fp
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 11:04:34 +08:00
|
|
|
|
out := make([]gin.H, 0, len(records))
|
|
|
|
|
|
for _, r := range records {
|
2026-03-26 20:26:35 +08:00
|
|
|
|
// 兜底:历史数据 plan_api_key 可能为空(已迁移但仍有漏网),按 action 回填展示,不改库
|
|
|
|
|
|
planKey := strings.TrimSpace(r.PlanAPIKey)
|
|
|
|
|
|
if planKey == "" {
|
|
|
|
|
|
if strings.TrimSpace(r.Action) == "join" || strings.TrimSpace(r.Action) == "match" {
|
|
|
|
|
|
planKey = ckbAPIKey
|
|
|
|
|
|
} else if strings.TrimSpace(r.Action) == "lead" && r.Source == "index_link_button" {
|
|
|
|
|
|
planKey = getCkbLeadApiKey()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-19 18:26:45 +08:00
|
|
|
|
personName := ""
|
|
|
|
|
|
ckbPlanId := int64(0)
|
|
|
|
|
|
if p := personMap[r.TargetPersonID]; p != nil {
|
|
|
|
|
|
personName = p.Name
|
|
|
|
|
|
ckbPlanId = p.CkbPlanID
|
2026-03-26 21:29:18 +08:00
|
|
|
|
// 兜底:迁移后计划 key 多写在 persons.ckb_api_key,但 lead_records.plan_api_key 可能仍为空
|
|
|
|
|
|
// 此处仅用于管理端回显(前端会做掩码展示),不改库
|
|
|
|
|
|
if planKey == "" && strings.TrimSpace(p.CkbApiKey) != "" {
|
|
|
|
|
|
planKey = strings.TrimSpace(p.CkbApiKey)
|
|
|
|
|
|
}
|
2026-03-26 20:26:35 +08:00
|
|
|
|
} else if strings.TrimSpace(r.TargetPersonID) == "" && r.Source == "index_link_button" && indexLinkFallback != nil {
|
|
|
|
|
|
personName = indexLinkFallback.Name
|
|
|
|
|
|
ckbPlanId = indexLinkFallback.CkbPlanID
|
2026-03-26 21:29:18 +08:00
|
|
|
|
if planKey == "" && strings.TrimSpace(indexLinkFallback.CkbApiKey) != "" {
|
|
|
|
|
|
planKey = strings.TrimSpace(indexLinkFallback.CkbApiKey)
|
|
|
|
|
|
}
|
2026-03-19 18:26:45 +08:00
|
|
|
|
}
|
2026-03-25 15:47:31 +08:00
|
|
|
|
displayNick := r.Nickname
|
|
|
|
|
|
userAvatar := ""
|
|
|
|
|
|
if u := userMap[r.UserID]; u != nil {
|
|
|
|
|
|
userAvatar = resolveAvatarURL(getStringValue(u.Avatar))
|
|
|
|
|
|
if u.Nickname != nil && strings.TrimSpace(*u.Nickname) != "" {
|
|
|
|
|
|
displayNick = strings.TrimSpace(*u.Nickname)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 11:04:34 +08:00
|
|
|
|
out = append(out, gin.H{
|
2026-03-22 08:34:28 +08:00
|
|
|
|
"id": r.ID,
|
2026-03-26 20:26:35 +08:00
|
|
|
|
"action": r.Action,
|
2026-03-22 08:34:28 +08:00
|
|
|
|
"userId": r.UserID,
|
2026-03-25 15:47:31 +08:00
|
|
|
|
"userNickname": displayNick,
|
|
|
|
|
|
"userAvatar": userAvatar,
|
2026-03-22 08:34:28 +08:00
|
|
|
|
"matchType": "lead",
|
|
|
|
|
|
"phone": r.Phone,
|
|
|
|
|
|
"wechatId": r.WechatID,
|
|
|
|
|
|
"name": r.Name,
|
|
|
|
|
|
"source": r.Source,
|
2026-03-26 20:26:35 +08:00
|
|
|
|
"planApiKey": planKey,
|
2026-03-22 08:34:28 +08:00
|
|
|
|
"targetPersonId": r.TargetPersonID,
|
|
|
|
|
|
"personName": personName,
|
|
|
|
|
|
"ckbPlanId": ckbPlanId,
|
2026-03-24 18:45:32 +08:00
|
|
|
|
"pushStatus": r.PushStatus,
|
|
|
|
|
|
"retryCount": r.RetryCount,
|
2026-03-26 20:26:35 +08:00
|
|
|
|
"ckbCode": r.CkbCode,
|
|
|
|
|
|
"ckbMessage": r.CkbMessage,
|
|
|
|
|
|
"ckbData": r.CkbData,
|
2026-03-24 18:45:32 +08:00
|
|
|
|
"ckbError": r.CkbError,
|
|
|
|
|
|
"lastPushAt": r.LastPushAt,
|
|
|
|
|
|
"nextRetryAt": r.NextRetryAt,
|
2026-03-22 08:34:28 +08:00
|
|
|
|
"createdAt": r.CreatedAt,
|
2026-03-10 11:04:34 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-03-22 08:34:28 +08:00
|
|
|
|
// 统计摘要:来源分布、去重获客人数
|
|
|
|
|
|
type sourceStat struct {
|
2026-03-25 15:47:31 +08:00
|
|
|
|
Source string `gorm:"column:source" json:"source"`
|
|
|
|
|
|
Cnt int64 `gorm:"column:cnt" json:"cnt"`
|
2026-03-22 08:34:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
var sourceStats []sourceStat
|
|
|
|
|
|
db.Raw("SELECT COALESCE(source,'未知') as source, COUNT(*) as cnt FROM ckb_lead_records GROUP BY source ORDER BY cnt DESC").Scan(&sourceStats)
|
|
|
|
|
|
var uniqueUsers int64
|
|
|
|
|
|
db.Raw("SELECT COUNT(DISTINCT user_id) FROM ckb_lead_records WHERE user_id IS NOT NULL AND user_id != ''").Scan(&uniqueUsers)
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true, "records": out, "total": total, "page": page, "pageSize": pageSize,
|
|
|
|
|
|
"stats": gin.H{
|
2026-03-24 18:45:32 +08:00
|
|
|
|
"uniqueUsers": uniqueUsers,
|
|
|
|
|
|
"sourceStats": sourceStats,
|
2026-03-22 08:34:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
})
|
2026-03-10 11:04:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 20:26:35 +08:00
|
|
|
|
// mode=submitted: 兼容旧面板,统一从 ckb_lead_records 中读取 join/match
|
|
|
|
|
|
q := db.Model(&model.CkbLeadRecord{}).Where("action IN ?", []string{"join", "match"})
|
2026-03-10 11:04:34 +08:00
|
|
|
|
if matchType != "" {
|
|
|
|
|
|
if matchType == "join" || matchType == "match" {
|
|
|
|
|
|
q = q.Where("action = ?", matchType)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var total int64
|
|
|
|
|
|
q.Count(&total)
|
2026-03-26 20:26:35 +08:00
|
|
|
|
var records []model.CkbLeadRecord
|
2026-03-10 11:04:34 +08:00
|
|
|
|
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})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 18:45:32 +08:00
|
|
|
|
// DBCKBLeadRetry POST /api/db/ckb-leads/retry 管理端-手动重推单条失败线索
|
|
|
|
|
|
func DBCKBLeadRetry(c *gin.Context) {
|
|
|
|
|
|
var body struct {
|
|
|
|
|
|
ID int64 `json:"id" binding:"required"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil || body.ID <= 0 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少有效 id"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
ok, err := RetryCkbLeadByID(c.Request.Context(), body.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
msg := strings.TrimSpace(err.Error())
|
|
|
|
|
|
if msg == "" {
|
|
|
|
|
|
msg = "重推失败"
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": msg})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-25 15:47:31 +08:00
|
|
|
|
db := database.DB()
|
|
|
|
|
|
var r model.CkbLeadRecord
|
|
|
|
|
|
if err := db.Where("id = ?", body.ID).First(&r).Error; err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "pushed": ok})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"pushed": ok,
|
|
|
|
|
|
"record": gin.H{
|
|
|
|
|
|
"id": r.ID,
|
|
|
|
|
|
"pushStatus": r.PushStatus,
|
|
|
|
|
|
"retryCount": r.RetryCount,
|
2026-03-26 20:26:35 +08:00
|
|
|
|
"ckbCode": r.CkbCode,
|
|
|
|
|
|
"ckbMessage": r.CkbMessage,
|
|
|
|
|
|
"ckbData": r.CkbData,
|
2026-03-25 15:47:31 +08:00
|
|
|
|
"ckbError": r.CkbError,
|
|
|
|
|
|
"lastPushAt": r.LastPushAt,
|
|
|
|
|
|
"nextRetryAt": r.NextRetryAt,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DBCKBLeadDelete POST /api/db/ckb-leads/delete 管理端-删除一条留资记录(运营清理误报/测试数据)
|
|
|
|
|
|
func DBCKBLeadDelete(c *gin.Context) {
|
|
|
|
|
|
var body struct {
|
|
|
|
|
|
ID int64 `json:"id" binding:"required"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil || body.ID <= 0 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少有效 id"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
res := db.Delete(&model.CkbLeadRecord{}, body.ID)
|
|
|
|
|
|
if res.Error != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": res.Error.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if res.RowsAffected == 0 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "记录不存在或已删除"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ckbLeadDeleteBatchMax 单次批量删除上限,防止误操作与请求过大
|
|
|
|
|
|
const ckbLeadDeleteBatchMax = 500
|
|
|
|
|
|
|
|
|
|
|
|
// DBCKBLeadDeleteBatch POST /api/db/ckb-leads/delete-batch 管理端-批量删除留资记录
|
|
|
|
|
|
func DBCKBLeadDeleteBatch(c *gin.Context) {
|
|
|
|
|
|
var body struct {
|
|
|
|
|
|
IDs []int64 `json:"ids" binding:"required"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请传入 ids 数组"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
seen := make(map[int64]struct{})
|
|
|
|
|
|
clean := make([]int64, 0, len(body.IDs))
|
|
|
|
|
|
for _, id := range body.IDs {
|
|
|
|
|
|
if id <= 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := seen[id]; ok {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
seen[id] = struct{}{}
|
|
|
|
|
|
clean = append(clean, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(clean) == 0 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "没有有效的 id"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(clean) > ckbLeadDeleteBatchMax {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"error": fmt.Sprintf("单次最多删除 %d 条,请减少勾选或分批提交", ckbLeadDeleteBatchMax),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
res := db.Where("id IN ?", clean).Delete(&model.CkbLeadRecord{})
|
|
|
|
|
|
if res.Error != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": res.Error.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "deleted": res.RowsAffected})
|
2026-03-24 18:45:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 20:26:35 +08:00
|
|
|
|
// CKBPlanStats GET /api/db/ckb-plan-stats 存客宝获客计划统计(统一基于 ckb_lead_records)
|
2026-03-10 11:04:34 +08:00
|
|
|
|
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
|
2026-03-26 20:26:35 +08:00
|
|
|
|
db.Raw("SELECT action, COUNT(*) as total FROM ckb_lead_records WHERE action IN ('join','match') GROUP BY action").Scan(&submitStats)
|
2026-03-10 11:04:34 +08:00
|
|
|
|
var submitTotal int64
|
2026-03-26 20:26:35 +08:00
|
|
|
|
db.Model(&model.CkbLeadRecord{}).Where("action IN ?", []string{"join", "match"}).Count(&submitTotal)
|
2026-03-10 11:04:34 +08:00
|
|
|
|
var leadTotal int64
|
2026-03-26 20:26:35 +08:00
|
|
|
|
db.Model(&model.CkbLeadRecord{}).Where("action = ?", "lead").Count(&leadTotal)
|
|
|
|
|
|
withContact := leadTotal // lead 记录均有 phone 或 wechat
|
2026-03-10 11:04:34 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": gin.H{
|
2026-03-22 08:34:28 +08:00
|
|
|
|
"ckbTotal": submitTotal + leadTotal,
|
|
|
|
|
|
"withContact": withContact,
|
|
|
|
|
|
"byType": submitStats,
|
|
|
|
|
|
"ckbApiKey": "***",
|
|
|
|
|
|
"ckbApiUrl": "https://ckbapi.quwanzhi.com/v1/api/scenarios",
|
|
|
|
|
|
"docNotes": "",
|
|
|
|
|
|
"docContent": "",
|
|
|
|
|
|
"routes": gin.H{},
|
2026-03-10 11:04:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|