feat: 小程序阅读记录与资料链路、管理端用户规则、API/VIP/推荐与运营脚本
- miniprogram: reading-records、imageUrl/mpNavigate、多页资料与 VIP 展示调整 - soul-admin: Users/Settings/UserDetailModal、dist 构建产物更新 - soul-api: user/vip/referral/ckb/db、MBTI 头像管理、user_rule_completion、迁移 SQL - .cursor: karuo-party 与飞书文档;.gitignore 忽略 .tmp_skill_bundle Made-with: Cursor
This commit is contained in:
@@ -7,11 +7,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -417,6 +419,21 @@ func CKBIndexLead(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
data["repeatedSubmit"] = repeatedSubmit
|
||||
|
||||
personName := "卡若"
|
||||
if defaultPerson.Name != "" {
|
||||
personName = defaultPerson.Name
|
||||
}
|
||||
go sendLeadWebhook(db, leadWebhookPayload{
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: personName,
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
|
||||
return
|
||||
}
|
||||
@@ -446,13 +463,15 @@ func CKBIndexLead(c *gin.Context) {
|
||||
// 请求体:phone/wechatId(至少一个)、userId(补全昵称)、targetUserId(Person.token)、targetNickname、source(如 article_mention、member_detail_avatar)
|
||||
func CKBLead(c *gin.Context) {
|
||||
var body struct {
|
||||
UserID string `json:"userId"`
|
||||
Phone string `json:"phone"`
|
||||
WechatID string `json:"wechatId"`
|
||||
Name string `json:"name"`
|
||||
TargetUserID string `json:"targetUserId"` // 被@的 personId(文章 mention 场景)
|
||||
TargetNickname string `json:"targetNickname"` // 被@的人显示名(用于文案)
|
||||
Source string `json:"source"` // index_lead / article_mention
|
||||
UserID string `json:"userId"`
|
||||
Phone string `json:"phone"`
|
||||
WechatID string `json:"wechatId"`
|
||||
Name string `json:"name"`
|
||||
TargetUserID string `json:"targetUserId"` // 被@的 personId(文章 mention / 超级个体人物 token)
|
||||
TargetNickname string `json:"targetNickname"` // 被@的人显示名(用于文案)
|
||||
TargetMemberID string `json:"targetMemberId"` // 超级个体用户 id(无 person token 时全局留资,写入 params 便于运营)
|
||||
TargetMemberName string `json:"targetMemberName"` // 超级个体展示名(仅入 params,不误导读为「对方会联系您」)
|
||||
Source string `json:"source"` // index_lead / article_mention / member_detail_global
|
||||
}
|
||||
_ = c.ShouldBindJSON(&body)
|
||||
phone := strings.TrimSpace(body.Phone)
|
||||
@@ -510,7 +529,8 @@ func CKBLead(c *gin.Context) {
|
||||
}
|
||||
paramsJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"userId": body.UserID, "phone": phone, "wechatId": wechatId, "name": body.Name,
|
||||
"targetUserId": body.TargetUserID, "source": source,
|
||||
"targetUserId": body.TargetUserID, "targetMemberId": strings.TrimSpace(body.TargetMemberID),
|
||||
"targetMemberName": strings.TrimSpace(body.TargetMemberName), "source": source,
|
||||
})
|
||||
_ = db.Create(&model.CkbLeadRecord{
|
||||
UserID: body.UserID,
|
||||
@@ -585,7 +605,16 @@ func CKBLead(c *gin.Context) {
|
||||
}
|
||||
data["repeatedSubmit"] = repeatedSubmit
|
||||
|
||||
go sendLeadWebhook(db, name, phone, wechatId, who, source, repeatedSubmit)
|
||||
go sendLeadWebhook(db, leadWebhookPayload{
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: who,
|
||||
MemberName: strings.TrimSpace(body.TargetMemberName),
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
|
||||
return
|
||||
@@ -649,7 +678,16 @@ func CKBLead(c *gin.Context) {
|
||||
} else {
|
||||
msg = fmt.Sprintf("提交成功,%s 会尽快联系您", who)
|
||||
}
|
||||
go sendLeadWebhook(db, name, phone, wechatId, who, source, repeatedSubmit)
|
||||
go sendLeadWebhook(db, leadWebhookPayload{
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: who,
|
||||
MemberName: strings.TrimSpace(body.TargetMemberName),
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg})
|
||||
return
|
||||
}
|
||||
@@ -676,7 +714,64 @@ func CKBLead(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, respObj)
|
||||
}
|
||||
|
||||
func sendLeadWebhook(db *gorm.DB, name, phone, wechat, target, source string, repeated bool) {
|
||||
type leadWebhookPayload struct {
|
||||
LeadName string // 留资客户姓名
|
||||
Phone string
|
||||
Wechat string
|
||||
PersonName string // 对接人(Person 表 name / targetNickname)
|
||||
MemberName string // 超级个体名称(targetMemberName)
|
||||
Source string // 技术来源标识
|
||||
Repeated bool
|
||||
LeadUserID string // 留资用户ID,用于查询行为轨迹
|
||||
}
|
||||
|
||||
func leadSourceLabel(source string) string {
|
||||
switch source {
|
||||
case "member_detail_global":
|
||||
return "超级个体详情页·全局链接"
|
||||
case "member_detail_avatar":
|
||||
return "超级个体详情页·点击头像"
|
||||
case "article_mention":
|
||||
return "文章正文·@提及人物"
|
||||
case "index_link_button":
|
||||
return "首页·链接卡若按钮"
|
||||
case "index_lead":
|
||||
return "首页·留资弹窗"
|
||||
default:
|
||||
if source == "" {
|
||||
return "未知来源"
|
||||
}
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
var _webhookDedupCache = struct {
|
||||
sync.Mutex
|
||||
m map[string]string
|
||||
}{m: make(map[string]string)}
|
||||
|
||||
func webhookShouldSkip(userId string) bool {
|
||||
if userId == "" {
|
||||
return false
|
||||
}
|
||||
today := time.Now().Format("2006-01-02")
|
||||
_webhookDedupCache.Lock()
|
||||
defer _webhookDedupCache.Unlock()
|
||||
if _webhookDedupCache.m[userId] == today {
|
||||
return true
|
||||
}
|
||||
_webhookDedupCache.m[userId] = today
|
||||
if len(_webhookDedupCache.m) > 10000 {
|
||||
_webhookDedupCache.m = map[string]string{userId: today}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sendLeadWebhook(db *gorm.DB, p leadWebhookPayload) {
|
||||
if p.LeadUserID != "" && webhookShouldSkip(p.LeadUserID) {
|
||||
log.Printf("webhook: skip duplicate for user %s today", p.LeadUserID)
|
||||
return
|
||||
}
|
||||
var cfg model.SystemConfig
|
||||
if db.Where("config_key = ?", "ckb_lead_webhook_url").First(&cfg).Error != nil {
|
||||
return
|
||||
@@ -690,19 +785,41 @@ func sendLeadWebhook(db *gorm.DB, name, phone, wechat, target, source string, re
|
||||
return
|
||||
}
|
||||
|
||||
tag := "新获客"
|
||||
if repeated {
|
||||
tag = "重复获客"
|
||||
tag := "📋 新获客"
|
||||
if p.Repeated {
|
||||
tag = "🔄 重复获客"
|
||||
}
|
||||
text := fmt.Sprintf("[%s] %s → %s\n姓名: %s", tag, source, target, name)
|
||||
if phone != "" {
|
||||
text += fmt.Sprintf("\n手机: %s", phone)
|
||||
sourceLabel := leadSourceLabel(p.Source)
|
||||
|
||||
contactPerson := p.PersonName
|
||||
if contactPerson == "" {
|
||||
contactPerson = p.MemberName
|
||||
}
|
||||
if wechat != "" {
|
||||
text += fmt.Sprintf("\n微信: %s", wechat)
|
||||
if contactPerson == "" || contactPerson == "对方" {
|
||||
contactPerson = "(公共获客池)"
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("%s\n来源: %s\n对接人: %s", tag, sourceLabel, contactPerson)
|
||||
text += "\n━━━━━━━━━━"
|
||||
text += fmt.Sprintf("\n姓名: %s", p.LeadName)
|
||||
if p.Phone != "" {
|
||||
text += fmt.Sprintf("\n手机: %s", p.Phone)
|
||||
}
|
||||
if p.Wechat != "" {
|
||||
text += fmt.Sprintf("\n微信: %s", p.Wechat)
|
||||
}
|
||||
text += fmt.Sprintf("\n时间: %s", time.Now().Format("2006-01-02 15:04"))
|
||||
|
||||
if p.LeadUserID != "" {
|
||||
recentTracks := GetUserRecentTracks(db, p.LeadUserID, 5)
|
||||
if len(recentTracks) > 0 {
|
||||
text += "\n━━━━━━━━━━\n最近行为:"
|
||||
for i, line := range recentTracks {
|
||||
text += fmt.Sprintf("\n %d. %s", i+1, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
if strings.Contains(webhookURL, "qyapi.weixin.qq.com") {
|
||||
payload, _ = json.Marshal(map[string]interface{}{
|
||||
@@ -721,5 +838,5 @@ func sendLeadWebhook(db *gorm.DB, name, phone, wechat, target, source string, re
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
fmt.Printf("[CKBWebhook] 已推送获客通知 → %s (status=%d)\n", target, resp.StatusCode)
|
||||
fmt.Printf("[CKBWebhook] 已推送获客通知 → %s (status=%d)\n", contactPerson, resp.StatusCode)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user