同步
This commit is contained in:
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -113,6 +114,216 @@ func getCkbLeadApiKey() string {
|
||||
return ckbAPIKey
|
||||
}
|
||||
|
||||
func markLeadPushSuccess(db *gorm.DB, recordID int64) {
|
||||
if db == nil || recordID <= 0 {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_ = db.Model(&model.CkbLeadRecord{}).Where("id = ?", recordID).Updates(map[string]interface{}{
|
||||
"push_status": "success",
|
||||
"ckb_error": "",
|
||||
"last_push_at": now,
|
||||
"next_retry_at": nil,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func markLeadPushFailed(db *gorm.DB, recordID int64, errMsg string, incRetry bool) {
|
||||
if db == nil || recordID <= 0 {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"push_status": "failed",
|
||||
"ckb_error": strings.TrimSpace(errMsg),
|
||||
"last_push_at": now,
|
||||
"next_retry_at": now.Add(5 * time.Minute),
|
||||
}
|
||||
if incRetry {
|
||||
updates["retry_count"] = gorm.Expr("retry_count + 1")
|
||||
}
|
||||
_ = db.Model(&model.CkbLeadRecord{}).Where("id = ?", recordID).Updates(updates).Error
|
||||
}
|
||||
|
||||
type ckbLeadPushResult struct {
|
||||
Code int
|
||||
Message string
|
||||
Raw string
|
||||
}
|
||||
|
||||
func pushLeadToCKB(name, phone, wechatId, leadKey string) (ckbLeadPushResult, error) {
|
||||
ts := time.Now().Unix()
|
||||
params := map[string]interface{}{
|
||||
"name": name,
|
||||
"timestamp": ts,
|
||||
"apiKey": leadKey,
|
||||
}
|
||||
if strings.TrimSpace(phone) != "" {
|
||||
params["phone"] = strings.TrimSpace(phone)
|
||||
}
|
||||
if strings.TrimSpace(wechatId) != "" {
|
||||
params["wechatId"] = strings.TrimSpace(wechatId)
|
||||
}
|
||||
params["sign"] = ckbSign(params, leadKey)
|
||||
q := url.Values{}
|
||||
q.Set("name", name)
|
||||
q.Set("timestamp", strconv.FormatInt(ts, 10))
|
||||
q.Set("apiKey", leadKey)
|
||||
if v, ok := params["phone"].(string); ok && v != "" {
|
||||
q.Set("phone", v)
|
||||
}
|
||||
if v, ok := params["wechatId"].(string); ok && v != "" {
|
||||
q.Set("wechatId", v)
|
||||
}
|
||||
q.Set("sign", params["sign"].(string))
|
||||
reqURL := ckbAPIURL + "?" + q.Encode()
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
return ckbLeadPushResult{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
_ = json.Unmarshal(b, &result)
|
||||
msg := strings.TrimSpace(result.Message)
|
||||
if msg == "" {
|
||||
msg = strings.TrimSpace(result.Msg)
|
||||
}
|
||||
return ckbLeadPushResult{
|
||||
Code: result.Code,
|
||||
Message: msg,
|
||||
Raw: string(b),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func retryOneLeadRecord(ctx context.Context, db *gorm.DB, r model.CkbLeadRecord) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
default:
|
||||
}
|
||||
_ = db.Model(&model.CkbLeadRecord{}).Where("id = ?", r.ID).Update("push_status", "pending").Error
|
||||
|
||||
var p map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(r.Params), &p)
|
||||
source := strings.TrimSpace(r.Source)
|
||||
name := strings.TrimSpace(r.Nickname)
|
||||
if name == "" {
|
||||
if v, ok := p["name"].(string); ok {
|
||||
name = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
name = "小程序用户"
|
||||
}
|
||||
phone := strings.TrimSpace(r.Phone)
|
||||
wechatId := strings.TrimSpace(r.WechatID)
|
||||
if phone == "" {
|
||||
if v, ok := p["phone"].(string); ok {
|
||||
phone = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
if wechatId == "" {
|
||||
if v, ok := p["wechatId"].(string); ok {
|
||||
wechatId = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
leadKey := getCkbLeadApiKey()
|
||||
targetName := ""
|
||||
targetMemberID := ""
|
||||
targetMemberName := ""
|
||||
leadUserID := strings.TrimSpace(r.UserID)
|
||||
if v, ok := p["userId"].(string); ok && leadUserID == "" {
|
||||
leadUserID = strings.TrimSpace(v)
|
||||
}
|
||||
if source != "index_link_button" {
|
||||
if v, ok := p["targetUserId"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
var person model.Person
|
||||
if db.Where("token = ?", strings.TrimSpace(v)).First(&person).Error == nil && strings.TrimSpace(person.CkbApiKey) != "" {
|
||||
leadKey = strings.TrimSpace(person.CkbApiKey)
|
||||
targetName = strings.TrimSpace(person.Name)
|
||||
if person.UserID != nil {
|
||||
targetMemberID = strings.TrimSpace(*person.UserID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := p["targetNickname"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
targetName = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := p["targetMemberId"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
targetMemberID = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := p["targetMemberName"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
targetMemberName = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
res, perr := pushLeadToCKB(name, phone, wechatId, leadKey)
|
||||
if perr != nil {
|
||||
markLeadPushFailed(db, r.ID, perr.Error(), true)
|
||||
return false
|
||||
}
|
||||
if res.Code == 200 {
|
||||
markLeadPushSuccess(db, r.ID)
|
||||
go sendLeadWebhook(db, leadWebhookPayload{
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: targetName,
|
||||
MemberName: targetMemberName,
|
||||
TargetMemberID: targetMemberID,
|
||||
Source: source,
|
||||
Repeated: true,
|
||||
LeadUserID: leadUserID,
|
||||
})
|
||||
return true
|
||||
}
|
||||
msg := res.Message
|
||||
if msg == "" {
|
||||
msg = "重推失败"
|
||||
}
|
||||
markLeadPushFailed(db, r.ID, msg, true)
|
||||
return false
|
||||
}
|
||||
|
||||
func RetryCkbLeadByID(ctx context.Context, recordID int64) (bool, error) {
|
||||
if recordID <= 0 {
|
||||
return false, fmt.Errorf("recordID 无效")
|
||||
}
|
||||
db := database.DB()
|
||||
var r model.CkbLeadRecord
|
||||
if err := db.Where("id = ?", recordID).First(&r).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return retryOneLeadRecord(ctx, db, r), nil
|
||||
}
|
||||
|
||||
// RetryFailedCkbLeads 重推存客宝失败留资记录(供定时任务调用)
|
||||
func RetryFailedCkbLeads(ctx context.Context, limit int) (retried, success int, err error) {
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
db := database.DB()
|
||||
now := time.Now()
|
||||
var rows []model.CkbLeadRecord
|
||||
if err := db.Where("push_status = ? AND (next_retry_at IS NULL OR next_retry_at <= ?)", "failed", now).
|
||||
Order("id ASC").Limit(limit).Find(&rows).Error; err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
retried = len(rows)
|
||||
for i := range rows {
|
||||
if retryOneLeadRecord(ctx, db, rows[i]) {
|
||||
success++
|
||||
}
|
||||
}
|
||||
return retried, success, nil
|
||||
}
|
||||
|
||||
// CKBJoin POST /api/ckb/join
|
||||
func CKBJoin(c *gin.Context) {
|
||||
var body struct {
|
||||
@@ -388,15 +599,17 @@ func CKBIndexLead(c *gin.Context) {
|
||||
"userId": body.UserID, "phone": phone, "wechatId": wechatId, "name": body.Name,
|
||||
"source": source,
|
||||
})
|
||||
_ = db.Create(&model.CkbLeadRecord{
|
||||
UserID: body.UserID,
|
||||
Nickname: name,
|
||||
Phone: phone,
|
||||
WechatID: wechatId,
|
||||
Name: strings.TrimSpace(body.Name),
|
||||
Source: source,
|
||||
Params: string(paramsJSON),
|
||||
}).Error
|
||||
rec := model.CkbLeadRecord{
|
||||
UserID: body.UserID,
|
||||
Nickname: name,
|
||||
Phone: phone,
|
||||
WechatID: wechatId,
|
||||
Name: strings.TrimSpace(body.Name),
|
||||
Source: source,
|
||||
Params: string(paramsJSON),
|
||||
PushStatus: "pending",
|
||||
}
|
||||
_ = db.Create(&rec).Error
|
||||
|
||||
ts := time.Now().Unix()
|
||||
params := map[string]interface{}{
|
||||
@@ -417,7 +630,8 @@ func CKBIndexLead(c *gin.Context) {
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
fmt.Printf("[CKBIndexLead] 请求存客宝失败: %v\n", err)
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "网络异常,请稍后重试"})
|
||||
markLeadPushFailed(db, rec.ID, err.Error(), true)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "添加成功,我们正在为您安排对接"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -429,6 +643,7 @@ func CKBIndexLead(c *gin.Context) {
|
||||
}
|
||||
_ = json.Unmarshal(b, &result)
|
||||
if result.Code == 200 {
|
||||
markLeadPushSuccess(db, rec.ID)
|
||||
var msg string
|
||||
var defaultPerson model.Person
|
||||
if db.Where("ckb_api_key = ? AND ckb_api_key != ''", leadKey).First(&defaultPerson).Error == nil && strings.TrimSpace(defaultPerson.Tips) != "" {
|
||||
@@ -451,14 +666,14 @@ func CKBIndexLead(c *gin.Context) {
|
||||
personName = defaultPerson.Name
|
||||
}
|
||||
go sendLeadWebhook(db, leadWebhookPayload{
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: personName,
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: personName,
|
||||
TargetMemberID: "",
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
|
||||
@@ -478,12 +693,8 @@ func CKBIndexLead(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
fmt.Printf("[CKBIndexLead] 存客宝返回异常 code=%d message=%s raw=%s leadKey=%s\n", result.Code, result.Message, string(b), leadKey)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": errMsg,
|
||||
"ckbCode": result.Code,
|
||||
"ckbMessage": result.Message,
|
||||
})
|
||||
markLeadPushFailed(db, rec.ID, errMsg, true)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "添加成功,我们正在为您安排对接"})
|
||||
}
|
||||
|
||||
// CKBLead POST /api/miniprogram/ckb/lead 小程序留资加好友:链接卡若(首页)、文章@某人、超级个体详情点头像
|
||||
@@ -496,9 +707,9 @@ func CKBLead(c *gin.Context) {
|
||||
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
|
||||
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)
|
||||
@@ -529,21 +740,19 @@ func CKBLead(c *gin.Context) {
|
||||
if body.TargetUserID != "" {
|
||||
var p model.Person
|
||||
if db.Where("token = ?", body.TargetUserID).First(&p).Error != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "未找到该人物配置,请稍后重试"})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.CkbApiKey) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "该人物尚未配置存客宝密钥,请联系管理员"})
|
||||
return
|
||||
}
|
||||
leadKey = p.CkbApiKey
|
||||
personTips = strings.TrimSpace(p.Tips)
|
||||
if targetName == "" {
|
||||
targetName = p.Name
|
||||
}
|
||||
if targetMemberID == "" {
|
||||
if p.UserID != nil {
|
||||
targetMemberID = strings.TrimSpace(*p.UserID)
|
||||
fmt.Printf("[CKBLead] 未找到人物 token=%s,回退全局获客池\n", body.TargetUserID)
|
||||
} else {
|
||||
if strings.TrimSpace(p.CkbApiKey) != "" {
|
||||
leadKey = p.CkbApiKey
|
||||
}
|
||||
personTips = strings.TrimSpace(p.Tips)
|
||||
if targetName == "" {
|
||||
targetName = p.Name
|
||||
}
|
||||
if targetMemberID == "" {
|
||||
if p.UserID != nil {
|
||||
targetMemberID = strings.TrimSpace(*p.UserID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,7 +774,7 @@ func CKBLead(c *gin.Context) {
|
||||
"targetUserId": body.TargetUserID, "targetMemberId": strings.TrimSpace(body.TargetMemberID),
|
||||
"targetMemberName": strings.TrimSpace(body.TargetMemberName), "source": source,
|
||||
})
|
||||
_ = db.Create(&model.CkbLeadRecord{
|
||||
rec := model.CkbLeadRecord{
|
||||
UserID: body.UserID,
|
||||
Nickname: name,
|
||||
Phone: phone,
|
||||
@@ -574,7 +783,9 @@ func CKBLead(c *gin.Context) {
|
||||
TargetPersonID: body.TargetUserID,
|
||||
Source: source,
|
||||
Params: string(paramsJSON),
|
||||
}).Error
|
||||
PushStatus: "pending",
|
||||
}
|
||||
_ = db.Create(&rec).Error
|
||||
|
||||
ts := time.Now().Unix()
|
||||
params := map[string]interface{}{
|
||||
@@ -605,7 +816,8 @@ func CKBLead(c *gin.Context) {
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
fmt.Printf("[CKBLead] 请求存客宝失败: %v\n", err)
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "网络异常,请稍后重试"})
|
||||
markLeadPushFailed(db, rec.ID, err.Error(), true)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "添加成功,我们正在为您安排对接"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -618,6 +830,7 @@ func CKBLead(c *gin.Context) {
|
||||
}
|
||||
_ = json.Unmarshal(b, &result)
|
||||
if result.Code == 200 {
|
||||
markLeadPushSuccess(db, rec.ID)
|
||||
who := targetName
|
||||
if who == "" {
|
||||
who = "对方"
|
||||
@@ -639,15 +852,15 @@ func CKBLead(c *gin.Context) {
|
||||
data["repeatedSubmit"] = repeatedSubmit
|
||||
|
||||
go sendLeadWebhook(db, leadWebhookPayload{
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: who,
|
||||
MemberName: strings.TrimSpace(body.TargetMemberName),
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: who,
|
||||
MemberName: strings.TrimSpace(body.TargetMemberName),
|
||||
TargetMemberID: targetMemberID,
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
|
||||
@@ -713,16 +926,17 @@ func CKBLead(c *gin.Context) {
|
||||
msg = fmt.Sprintf("提交成功,%s 会尽快联系您", who)
|
||||
}
|
||||
go sendLeadWebhook(db, leadWebhookPayload{
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: who,
|
||||
MemberName: strings.TrimSpace(body.TargetMemberName),
|
||||
LeadName: name,
|
||||
Phone: phone,
|
||||
Wechat: wechatId,
|
||||
PersonName: who,
|
||||
MemberName: strings.TrimSpace(body.TargetMemberName),
|
||||
TargetMemberID: targetMemberID,
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
Source: source,
|
||||
Repeated: repeatedSubmit,
|
||||
LeadUserID: body.UserID,
|
||||
})
|
||||
markLeadPushSuccess(db, rec.ID)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg})
|
||||
return
|
||||
}
|
||||
@@ -737,9 +951,10 @@ func CKBLead(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
fmt.Printf("[CKBLead] 存客宝返回异常 code=%d msg=%s raw=%s leadKey=%s\n", result.Code, ckbMsg, string(b), leadKey)
|
||||
markLeadPushFailed(db, rec.ID, errMsg, true)
|
||||
respObj := gin.H{
|
||||
"success": false,
|
||||
"message": errMsg,
|
||||
"success": true,
|
||||
"message": "添加成功,我们正在为您安排对接",
|
||||
"ckbCode": result.Code,
|
||||
"ckbMessage": ckbMsg,
|
||||
}
|
||||
@@ -750,15 +965,15 @@ func CKBLead(c *gin.Context) {
|
||||
}
|
||||
|
||||
type leadWebhookPayload struct {
|
||||
LeadName string // 留资客户姓名
|
||||
Phone string
|
||||
Wechat string
|
||||
PersonName string // 对接人(Person 表 name / targetNickname)
|
||||
MemberName string // 超级个体名称(targetMemberName)
|
||||
LeadName string // 留资客户姓名
|
||||
Phone string
|
||||
Wechat string
|
||||
PersonName string // 对接人(Person 表 name / targetNickname)
|
||||
MemberName string // 超级个体名称(targetMemberName)
|
||||
TargetMemberID string // 超级个体 userId,用于按人路由 webhook
|
||||
Source string // 技术来源标识
|
||||
Repeated bool
|
||||
LeadUserID string // 留资用户ID,用于查询行为轨迹
|
||||
Source string // 技术来源标识
|
||||
Repeated bool
|
||||
LeadUserID string // 留资用户ID,用于查询行为轨迹
|
||||
}
|
||||
|
||||
func leadSourceLabel(source string) string {
|
||||
@@ -783,7 +998,7 @@ func leadSourceLabel(source string) string {
|
||||
|
||||
var _webhookDedupCache = struct {
|
||||
sync.Mutex
|
||||
m map[string]string
|
||||
m map[string]string
|
||||
}{m: make(map[string]string)}
|
||||
|
||||
func webhookShouldSkip(userId string, targetMemberID string) bool {
|
||||
|
||||
Reference in New Issue
Block a user