package handler import ( "fmt" "net/http" "strconv" "strings" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" ) // DBCKBLeadList GET /api/db/ckb-leads 管理端-CKB线索明细 // mode=submitted: ckb_submit_records(join/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 } if mode == "contact" { search := c.Query("search") source := c.Query("source") pushStatus := strings.TrimSpace(c.Query("pushStatus")) q := db.Model(&model.CkbLeadRecord{}) 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) } if pushStatus != "" { q = q.Where("push_status = ?", pushStatus) } 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 } // 批量查 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 db.Where("person_id IN ? OR token IN ?", personIDs, personIDs).Find(&persons) for i := range persons { personMap[persons[i].PersonID] = &persons[i] personMap[persons[i].Token] = &persons[i] } } // 批量查 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] } } out := make([]gin.H, 0, len(records)) for _, r := range records { personName := "" ckbPlanId := int64(0) if p := personMap[r.TargetPersonID]; p != nil { personName = p.Name ckbPlanId = p.CkbPlanID } 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) } } out = append(out, gin.H{ "id": r.ID, "userId": r.UserID, "userNickname": displayNick, "userAvatar": userAvatar, "matchType": "lead", "phone": r.Phone, "wechatId": r.WechatID, "name": r.Name, "source": r.Source, "targetPersonId": r.TargetPersonID, "personName": personName, "ckbPlanId": ckbPlanId, "pushStatus": r.PushStatus, "retryCount": r.RetryCount, "ckbError": r.CkbError, "lastPushAt": r.LastPushAt, "nextRetryAt": r.NextRetryAt, "createdAt": r.CreatedAt, }) } // 统计摘要:来源分布、去重获客人数 type sourceStat struct { Source string `gorm:"column:source" json:"source"` Cnt int64 `gorm:"column:cnt" json:"cnt"` } 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{ "uniqueUsers": uniqueUsers, "sourceStats": sourceStats, }, }) return } // mode=submitted: ckb_submit_records q := db.Model(&model.CkbSubmitRecord{}) if matchType != "" { // matchType 对应 action: join 时 type 在 params 中,match 时 matchType 在 params 中 // 简化:仅按 action 过滤,join 时 params 含 type if matchType == "join" || matchType == "match" { q = q.Where("action = ?", matchType) } } 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}) } // 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 } 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, "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}) } // 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{}, }, }) }