今日新增修改:解决好友自动推送问题

This commit is contained in:
乘风
2026-05-08 15:08:01 +08:00
parent bfd7610527
commit 7399e392aa
6 changed files with 561 additions and 165 deletions

View File

@@ -287,6 +287,7 @@ func pushLeadToCKB(name, phone, wechatId, leadKey string) (ckbLeadPushResult, er
}
q.Set("sign", params["sign"].(string))
reqURL := ckbAPIURL + "?" + q.Encode()
fmt.Printf("[存客宝 scenarios] 请求 URLGET: %s\n", reqURL)
resp, err := http.Get(reqURL)
if err != nil {
return ckbLeadPushResult{}, err
@@ -327,6 +328,55 @@ func resolvePersonForLead(db *gorm.DB, targetUserID string) (model.Person, bool)
return model.Person{}, false
}
// resolveLeadPlanAPIKeyForRetry 与 GET /api/db/ckb-leads?mode=contact 中 planApiKey 展示顺序一致:
// 1) ckb_lead_records.plan_api_key
// 2) action/source 默认join/match → 内置 ckbAPIKeylead+index_link_button → getCkbLeadApiKey
// 3) 仍为空:按 target_person_id 或 params.targetUserId 查人物 persons.ckb_api_key
// 4) index_link 且无 target与列表一致用「全局 leadKey 对应 Person」兜底
// 5) 最后才 getCkbLeadApiKey()
//
// 旧逻辑在 plan_api_key 为空时直接 getCkbLeadApiKey(),且非 index_link 时用人物 key 无条件覆盖 —
// 会导致:列表因人物兜底显示 2y4v5…重推却带全局 IPQ9s…或在库内已有 plan_api_key 时仍被人物旧 key 覆盖。
func resolveLeadPlanAPIKeyForRetry(db *gorm.DB, r model.CkbLeadRecord, p map[string]interface{}) string {
planKey := strings.TrimSpace(r.PlanAPIKey)
action := strings.TrimSpace(r.Action)
source := strings.TrimSpace(r.Source)
if planKey == "" {
if action == "join" || action == "match" {
planKey = ckbAPIKey
} else if action == "lead" && source == "index_link_button" {
planKey = getCkbLeadApiKey()
}
}
if planKey == "" {
targetTok := strings.TrimSpace(r.TargetPersonID)
if targetTok == "" && p != nil {
if v, ok := p["targetUserId"].(string); ok {
targetTok = strings.TrimSpace(v)
}
}
if targetTok != "" {
if person, found := resolvePersonForLead(db, targetTok); found && strings.TrimSpace(person.CkbApiKey) != "" {
planKey = strings.TrimSpace(person.CkbApiKey)
}
}
}
if planKey == "" && source == "index_link_button" && strings.TrimSpace(r.TargetPersonID) == "" {
gk := getCkbLeadApiKey()
if gk != "" {
var fp model.Person
if db.Where("ckb_api_key = ? AND ckb_api_key != ''", gk).First(&fp).Error == nil && strings.TrimSpace(fp.CkbApiKey) != "" {
planKey = strings.TrimSpace(fp.CkbApiKey)
}
}
}
if planKey == "" {
planKey = getCkbLeadApiKey()
}
return planKey
}
// existsUnifiedLeadRecent join/match 幂等去重:同用户+动作+来源+联系方式在窗口期内仅保留一条,避免重复点击刷数据
func existsUnifiedLeadRecent(db *gorm.DB, action, userID, source, phone, wechatID string, within time.Duration) bool {
if db == nil {
@@ -391,10 +441,14 @@ func retryOneLeadRecord(ctx context.Context, db *gorm.DB, r model.CkbLeadRecord)
wechatId = strings.TrimSpace(v)
}
}
leadKey := strings.TrimSpace(r.PlanAPIKey)
if leadKey == "" {
leadKey = getCkbLeadApiKey()
if wechatId == "" && p != nil {
if v, ok := p["wechat"].(string); ok {
wechatId = strings.TrimSpace(v)
}
}
leadKey := resolveLeadPlanAPIKeyForRetry(db, r, p)
targetName := ""
targetMemberID := ""
targetMemberName := ""
@@ -402,16 +456,23 @@ func retryOneLeadRecord(ctx context.Context, db *gorm.DB, r model.CkbLeadRecord)
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) != "" {
if person, found := resolvePersonForLead(db, v); found && strings.TrimSpace(person.CkbApiKey) != "" {
leadKey = strings.TrimSpace(person.CkbApiKey)
targetTok := strings.TrimSpace(r.TargetPersonID)
if targetTok == "" && p != nil {
if v, ok := p["targetUserId"].(string); ok {
targetTok = strings.TrimSpace(v)
}
}
if targetTok != "" {
if person, found := resolvePersonForLead(db, targetTok); found {
if strings.TrimSpace(person.Name) != "" {
targetName = strings.TrimSpace(person.Name)
if person.UserID != nil {
targetMemberID = strings.TrimSpace(*person.UserID)
}
}
if person.UserID != nil {
targetMemberID = strings.TrimSpace(*person.UserID)
}
}
}
if p != nil {
if v, ok := p["targetNickname"].(string); ok && strings.TrimSpace(v) != "" {
targetName = strings.TrimSpace(v)
}

View File

@@ -1,8 +1,10 @@
package handler
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
@@ -13,6 +15,54 @@ import (
"github.com/gin-gonic/gin"
)
// jsonValAsTrimmedString 从 JSON map 取值(兼容 number / string用于 params 里手机号等字段
func jsonValAsTrimmedString(v interface{}) string {
if v == nil {
return ""
}
switch t := v.(type) {
case string:
return strings.TrimSpace(t)
case float64:
if t == 0 {
return ""
}
if t == float64(int64(t)) {
return strconv.FormatInt(int64(t), 10)
}
return strings.TrimSpace(strconv.FormatFloat(t, 'f', -1, 64))
case json.Number:
return strings.TrimSpace(string(t))
default:
return strings.TrimSpace(fmt.Sprint(v))
}
}
// extractPhoneWechatFromParamsJSON 从线索 params JSON 抽取联系方式(加入/匹配上报结构可能只用顶层 phone/wechat
func extractPhoneWechatFromParamsJSON(paramsJSON string) (phone, wechat string) {
s := strings.TrimSpace(paramsJSON)
if s == "" {
return "", ""
}
var p map[string]interface{}
if json.Unmarshal([]byte(s), &p) != nil {
return "", ""
}
for _, k := range []string{"phone", "mobile", "tel"} {
if v := jsonValAsTrimmedString(p[k]); v != "" {
phone = v
break
}
}
for _, k := range []string{"wechat", "wechatId", "wechat_id", "wx", "wxId"} {
if v := jsonValAsTrimmedString(p[k]); v != "" {
wechat = v
break
}
}
return phone, wechat
}
// DBCKBLeadList GET /api/db/ckb-leads 管理端-CKB线索明细
// mode=submitted: ckb_lead_recordsaction=join/match兼容旧面板命名
// mode=contact: ckb_lead_records链接卡若留资有 phone/wechat
@@ -232,22 +282,22 @@ func DBCKBLeadList(c *gin.Context) {
for _, r := range records {
phone := strings.TrimSpace(r.Phone)
wechatID := strings.TrimSpace(r.WechatID)
// 工作台 join/match 行曾只读 row.phone老数据或仅写在 params 里的,回退解析便于运营核对
if (phone == "" || wechatID == "") && strings.TrimSpace(r.Params) != "" {
var p map[string]interface{}
if json.Unmarshal([]byte(r.Params), &p) == nil {
if phone == "" {
if v, ok := p["phone"].(string); ok {
phone = strings.TrimSpace(v)
}
}
if wechatID == "" {
if v, ok := p["wechat"].(string); ok {
wechatID = strings.TrimSpace(v)
} else if v, ok := p["wechatId"].(string); ok {
wechatID = strings.TrimSpace(v)
}
}
if phone == "" || wechatID == "" {
pp, ww := extractPhoneWechatFromParamsJSON(r.Params)
if phone == "" {
phone = pp
}
if wechatID == "" {
wechatID = ww
}
}
// 仍为空:按 user_id 回填会员资料autoCKBReport 历史误写、或库内列为空但用户已补全资料)
if u := userMap[r.UserID]; u != nil {
if phone == "" && u.Phone != nil {
phone = strings.TrimSpace(*u.Phone)
}
if wechatID == "" && u.WechatID != nil {
wechatID = strings.TrimSpace(*u.WechatID)
}
}
out = append(out, gin.H{
@@ -265,13 +315,48 @@ func DBCKBLeadList(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "records": out, "total": total, "page": page, "pageSize": pageSize})
}
func logCkbLeadRetryResponse(resp gin.H) {
b, err := json.MarshalIndent(resp, "", " ")
if err != nil {
fmt.Printf("[DBCKBLeadRetry] response(marshal err=%v): %+v\n", err, resp)
return
}
fmt.Printf("[DBCKBLeadRetry] response JSON:\n%s\n", string(b))
}
func logCkbLeadRetryRequest(raw []byte, readErr error) {
if readErr != nil {
fmt.Printf("[DBCKBLeadRetry] request body read error: %v\n", readErr)
return
}
s := strings.TrimSpace(string(raw))
if s == "" {
fmt.Printf("[DBCKBLeadRetry] request body: (empty)\n")
return
}
if json.Valid([]byte(s)) {
var buf bytes.Buffer
if err := json.Indent(&buf, []byte(s), "", " "); err == nil {
fmt.Printf("[DBCKBLeadRetry] request JSON:\n%s\n", buf.String())
return
}
}
fmt.Printf("[DBCKBLeadRetry] request body: %s\n", s)
}
// DBCKBLeadRetry POST /api/db/ckb-leads/retry 管理端-手动重推单条失败线索
func DBCKBLeadRetry(c *gin.Context) {
raw, readErr := io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(raw))
logCkbLeadRetryRequest(raw, readErr)
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"})
resp := gin.H{"success": false, "error": "缺少有效 id"}
logCkbLeadRetryResponse(resp)
c.JSON(http.StatusOK, resp)
return
}
ok, err := RetryCkbLeadByID(c.Request.Context(), body.ID)
@@ -280,16 +365,20 @@ func DBCKBLeadRetry(c *gin.Context) {
if msg == "" {
msg = "重推失败"
}
c.JSON(http.StatusOK, gin.H{"success": false, "error": msg})
resp := gin.H{"success": false, "error": msg}
logCkbLeadRetryResponse(resp)
c.JSON(http.StatusOK, resp)
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})
resp := gin.H{"success": true, "pushed": ok}
logCkbLeadRetryResponse(resp)
c.JSON(http.StatusOK, resp)
return
}
c.JSON(http.StatusOK, gin.H{
resp := gin.H{
"success": true,
"pushed": ok,
"record": gin.H{
@@ -303,7 +392,9 @@ func DBCKBLeadRetry(c *gin.Context) {
"lastPushAt": r.LastPushAt,
"nextRetryAt": r.NextRetryAt,
},
})
}
logCkbLeadRetryResponse(resp)
c.JSON(http.StatusOK, resp)
}
// DBCKBLeadDelete POST /api/db/ckb-leads/delete 管理端-删除一条留资记录(运营清理误报/测试数据)

View File

@@ -403,8 +403,17 @@ func MatchUsers(c *gin.Context) {
fmt.Printf("[MatchUsers] 写入 match_records 失败: %v\n", err)
}
// 后端兜底:匹配成功自动上报 CKB小程序也会上报CKBMatch 内部有 5 分钟去重)
// 必须用「发起人」联系方式写入 ckb_lead_records误用被匹配人 r 会导致 user_id 与手机号不一致或整列为空
go func() {
autoCKBReport(db, body.UserID, body.MatchType, phone, wechat)
initPhone := strings.TrimSpace(body.Phone)
initWechat := strings.TrimSpace(body.WechatID)
if initPhone == "" && user.Phone != nil {
initPhone = strings.TrimSpace(*user.Phone)
}
if initWechat == "" && user.WechatID != nil {
initWechat = strings.TrimSpace(*user.WechatID)
}
autoCKBReport(db, body.UserID, body.MatchType, initPhone, initWechat)
}()
commonInterests := buildCommonInterests(user, r)
dynamicTags := buildDynamicTags(r, tag)