Files
soul-yongping/soul-api/internal/handler/ckb.go
卡若 5724fba877 feat: 小程序超级个体/个人资料/CKB获客;VIP列表展示过滤;管理端与API联调
- 超级个体:去掉首位特例;列表仅展示有头像且非微信默认昵称(vip.go)
- 个人资料:居中头像、低调联系方式、点头像优先走存客宝 lead(ckbLeadToken)
- 阅读页分享朋友圈复制与 toast 去重
- soul-api: miniprogram users 带 ckbLeadToken;其它 handler 与路由调整
- 脚本:content_upload、miniprogram 上传辅助等

Made-with: Cursor
2026-03-22 08:34:28 +08:00

726 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"soul-api/internal/config"
"soul-api/internal/database"
"soul-api/internal/model"
)
// 存客宝 API Key 约定(详见 开发文档/8、部署/存客宝API-Key约定.md
// - 链接卡若(添加卡若好友):使用 CKB_LEAD_API_KEY.env 配置),未配则用下方 ckbAPIKey
// - 其他场景join/match 等):使用 ckbAPIKey
const ckbAPIKey = "fyngh-ecy9h-qkdae-epwd5-rz6kd"
const ckbAPIURL = "https://ckbapi.quwanzhi.com/v1/api/scenarios"
var ckbSourceMap = map[string]string{"team": "团队招募", "investor": "资源对接", "mentor": "导师顾问", "partner": "创业合伙"}
var ckbTagsMap = map[string]string{"team": "切片团队,团队招募", "investor": "资源对接,资源群", "mentor": "导师顾问,咨询服务", "partner": "创业合伙,创业伙伴"}
// ckbSubmitSave 加好友/留资类接口统一落库:记录 action、userId、昵称、用户提交的传参写入 ckb_submit_records
func ckbSubmitSave(action, userID, nickname string, params interface{}) {
paramsJSON, _ := json.Marshal(params)
_ = database.DB().Create(&model.CkbSubmitRecord{
Action: action,
UserID: userID,
Nickname: nickname,
Params: string(paramsJSON),
}).Error
}
// ckbSign 与 next-project app/api/ckb/join 一致:排除 sign/apiKey/portrait空值跳过按键升序拼接值MD5(拼接串) 再 MD5(结果+apiKey)
func ckbSign(params map[string]interface{}, apiKey string) string {
keys := make([]string, 0, len(params))
for k := range params {
if k == "sign" || k == "apiKey" || k == "portrait" {
continue
}
v := params[k]
if v == nil || v == "" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var concat string
for _, k := range keys {
v := params[k]
switch val := v.(type) {
case string:
concat += val
case float64:
concat += strconv.FormatFloat(val, 'f', -1, 64)
case int:
concat += strconv.Itoa(val)
case int64:
concat += strconv.FormatInt(val, 10)
default:
concat += ""
}
}
h := md5.Sum([]byte(concat))
first := hex.EncodeToString(h[:])
h2 := md5.Sum([]byte(first + apiKey))
return hex.EncodeToString(h2[:])
}
// getCkbLeadApiKey 链接卡若密钥优先级system_config.site_settings.ckbLeadApiKey > .env CKB_LEAD_API_KEY > 代码内置 ckbAPIKey
func getCkbLeadApiKey() string {
var row model.SystemConfig
if err := database.DB().Where("config_key = ?", "site_settings").First(&row).Error; err == nil && len(row.ConfigValue) > 0 {
var m map[string]interface{}
if err := json.Unmarshal(row.ConfigValue, &m); err == nil {
if v, ok := m["ckbLeadApiKey"].(string); ok && strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
}
}
if cfg := config.Get(); cfg != nil && cfg.CkbLeadAPIKey != "" {
return cfg.CkbLeadAPIKey
}
return ckbAPIKey
}
// CKBJoin POST /api/ckb/join
func CKBJoin(c *gin.Context) {
var body struct {
Type string `json:"type" binding:"required"`
Phone string `json:"phone"`
Wechat string `json:"wechat"`
Name string `json:"name"`
UserID string `json:"userId"`
Remark string `json:"remark"`
CanHelp string `json:"canHelp"` // 资源对接:我能帮到你什么
NeedHelp string `json:"needHelp"` // 资源对接:我需要什么帮助
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请提供手机号或微信号"})
return
}
if body.Phone == "" && body.Wechat == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请提供手机号或微信号"})
return
}
if body.Type != "team" && body.Type != "investor" && body.Type != "mentor" && body.Type != "partner" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的加入类型"})
return
}
nickname := strings.TrimSpace(body.Name)
if nickname == "" && body.UserID != "" {
var u model.User
if database.DB().Select("nickname").Where("id = ?", body.UserID).First(&u).Error == nil && u.Nickname != nil && *u.Nickname != "" {
nickname = *u.Nickname
}
}
if nickname == "" {
nickname = "-"
}
ckbSubmitSave("join", body.UserID, nickname, map[string]interface{}{
"type": body.Type, "phone": body.Phone, "wechat": body.Wechat, "name": body.Name,
"userId": body.UserID, "remark": body.Remark, "canHelp": body.CanHelp, "needHelp": body.NeedHelp,
})
ts := time.Now().Unix()
params := map[string]interface{}{
"timestamp": ts,
"source": "创业实验-" + ckbSourceMap[body.Type],
"tags": ckbTagsMap[body.Type],
"siteTags": "创业实验APP",
"remark": body.Remark,
}
if body.Remark == "" {
remark := "用户通过创业实验APP申请" + ckbSourceMap[body.Type]
if body.Type == "investor" && (body.CanHelp != "" || body.NeedHelp != "") {
remark = fmt.Sprintf("能帮:%s 需要:%s", body.CanHelp, body.NeedHelp)
}
params["remark"] = remark
}
if body.Phone != "" {
params["phone"] = body.Phone
}
if body.Wechat != "" {
params["wechatId"] = body.Wechat
}
if body.Name != "" {
params["name"] = body.Name
}
params["apiKey"] = ckbAPIKey
params["sign"] = ckbSign(params, ckbAPIKey)
sourceData := map[string]interface{}{
"joinType": body.Type, "joinLabel": ckbSourceMap[body.Type], "userId": body.UserID,
"device": "webapp", "timestamp": time.Now().Format(time.RFC3339),
}
if body.Type == "investor" {
if body.CanHelp != "" {
sourceData["canHelp"] = body.CanHelp
}
if body.NeedHelp != "" {
sourceData["needHelp"] = body.NeedHelp
}
}
params["portrait"] = map[string]interface{}{
"type": 4, "source": 0,
"sourceData": sourceData,
"remark": ckbSourceMap[body.Type] + "申请",
"uniqueId": "soul_" + body.Phone + body.Wechat + strconv.FormatInt(ts, 10),
}
raw, _ := json.Marshal(params)
resp, err := http.Post(ckbAPIURL, "application/json", bytes.NewReader(raw))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "服务器错误,请稍后重试"})
return
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
var result struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
_ = json.Unmarshal(b, &result)
if result.Code == 200 {
// 资源对接:同步更新用户资料中的 help_offer、help_need、phone、wechat_id
if body.Type == "investor" && body.UserID != "" {
updates := map[string]interface{}{}
if body.CanHelp != "" {
updates["help_offer"] = body.CanHelp
}
if body.NeedHelp != "" {
updates["help_need"] = body.NeedHelp
}
if body.Phone != "" {
updates["phone"] = body.Phone
}
if body.Wechat != "" {
updates["wechat_id"] = body.Wechat
}
if len(updates) > 0 {
database.DB().Model(&model.User{}).Where("id = ?", body.UserID).Updates(updates)
}
}
msg := "成功加入" + ckbSourceMap[body.Type]
if result.Message == "已存在" {
msg = "您已加入,我们会尽快联系您"
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": result.Data})
return
}
errMsg := result.Message
if errMsg == "" {
errMsg = "加入失败,请稍后重试"
}
// 打印 CKB 原始响应便于排查
fmt.Printf("[CKBJoin] 失败 type=%s wechat=%s code=%d message=%s raw=%s\n",
body.Type, body.Wechat, result.Code, result.Message, string(b))
c.JSON(http.StatusOK, gin.H{"success": false, "message": errMsg})
}
// CKBMatch POST /api/ckb/match
func CKBMatch(c *gin.Context) {
var body struct {
MatchType string `json:"matchType"`
Phone string `json:"phone"`
Wechat string `json:"wechat"`
UserID string `json:"userId"`
Nickname string `json:"nickname"`
MatchedUser interface{} `json:"matchedUser"`
}
_ = c.ShouldBindJSON(&body)
if body.Phone == "" && body.Wechat == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请提供手机号或微信号"})
return
}
nickname := strings.TrimSpace(body.Nickname)
if nickname == "" {
nickname = "-"
}
ckbSubmitSave("match", body.UserID, nickname, map[string]interface{}{
"matchType": body.MatchType, "phone": body.Phone, "wechat": body.Wechat,
"userId": body.UserID, "nickname": body.Nickname, "matchedUser": body.MatchedUser,
})
ts := time.Now().Unix()
label := ckbSourceMap[body.MatchType]
if label == "" {
label = "创业合伙"
}
params := map[string]interface{}{
"timestamp": ts,
"source": "创业实验-找伙伴匹配",
"tags": "找伙伴," + label,
"siteTags": "创业实验APP,匹配用户",
"remark": "用户发起" + label + "匹配",
}
if body.Phone != "" {
params["phone"] = body.Phone
}
if body.Wechat != "" {
params["wechatId"] = body.Wechat
}
if body.Nickname != "" {
params["name"] = body.Nickname
}
params["apiKey"] = ckbAPIKey
params["sign"] = ckbSign(params, ckbAPIKey)
params["portrait"] = map[string]interface{}{
"type": 4, "source": 0,
"sourceData": map[string]interface{}{
"action": "match", "matchType": body.MatchType, "matchLabel": label,
"userId": body.UserID, "device": "webapp", "timestamp": time.Now().Format(time.RFC3339),
},
"remark": "找伙伴匹配-" + label,
"uniqueId": "soul_match_" + body.Phone + body.Wechat + strconv.FormatInt(ts, 10),
}
raw, _ := json.Marshal(params)
resp, err := http.Post(ckbAPIURL, "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Printf("[CKBMatch] 请求存客宝失败: %v\n", err)
c.JSON(http.StatusOK, gin.H{"success": false, "message": "网络异常,请稍后重试"})
return
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
var result struct {
Code int `json:"code"`
Message string `json:"message"`
}
_ = json.Unmarshal(b, &result)
if result.Code == 200 {
c.JSON(http.StatusOK, gin.H{"success": true, "message": "匹配记录已上报", "data": nil})
return
}
fmt.Printf("[CKBMatch] 存客宝返回异常 code=%d message=%s\n", result.Code, result.Message)
c.JSON(http.StatusOK, gin.H{"success": true, "message": "匹配成功"})
}
// CKBSync GET/POST /api/ckb/sync
func CKBSync(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
}
// CKBIndexLead POST /api/miniprogram/ckb/index-lead 小程序首页「点击链接卡若」专用留资接口
// - 固定使用全局 CKB_LEAD_API_KEY不受文章 @ 人物的 ckb_api_key 影响
// - 请求体userId可选用于补全昵称、phone/wechatId至少一个、name可选
func CKBIndexLead(c *gin.Context) {
var body struct {
UserID string `json:"userId"`
Phone string `json:"phone"`
WechatID string `json:"wechatId"`
Name string `json:"name"`
}
_ = c.ShouldBindJSON(&body)
phone := strings.TrimSpace(body.Phone)
wechatId := strings.TrimSpace(body.WechatID)
// 存客宝侧仅接收手机号,不接收微信号;首页入口必须提供手机号
if phone == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "请先填写手机号"})
return
}
name := strings.TrimSpace(body.Name)
db := database.DB()
if name == "" && body.UserID != "" {
var u model.User
if db.Select("nickname").Where("id = ?", body.UserID).First(&u).Error == nil && u.Nickname != nil && *u.Nickname != "" {
name = *u.Nickname
}
}
if name == "" {
name = "小程序用户"
}
// 首页固定使用全局密钥system_config > .env > 代码内置
leadKey := getCkbLeadApiKey()
if leadKey == "" {
fmt.Printf("[CKBIndexLead] 警告CKB_LEAD_API_KEY 未配置,使用默认密钥\n")
leadKey = ckbAPIKey
}
// 去重:同一用户只记录一次(首页链接卡若)
repeatedSubmit := false
if body.UserID != "" {
var existCount int64
db.Model(&model.CkbLeadRecord{}).Where("user_id = ? AND source = ?", body.UserID, "index_link_button").Count(&existCount)
repeatedSubmit = existCount > 0
}
source := "index_link_button"
paramsJSON, _ := json.Marshal(map[string]interface{}{
"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
ts := time.Now().Unix()
params := map[string]interface{}{
"name": name,
"timestamp": ts,
"apiKey": leadKey,
}
params["phone"] = phone
params["sign"] = ckbSign(params, leadKey)
q := url.Values{}
q.Set("name", name)
q.Set("timestamp", strconv.FormatInt(ts, 10))
q.Set("apiKey", leadKey)
q.Set("phone", phone)
q.Set("sign", params["sign"].(string))
reqURL := ckbAPIURL + "?" + q.Encode()
fmt.Printf("[CKBIndexLead] 请求存客宝完整链接: %s\n", reqURL)
resp, err := http.Get(reqURL)
if err != nil {
fmt.Printf("[CKBIndexLead] 请求存客宝失败: %v\n", err)
c.JSON(http.StatusOK, gin.H{"success": false, "message": "网络异常,请稍后重试"})
return
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
var result struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
_ = json.Unmarshal(b, &result)
if result.Code == 200 {
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) != "" {
msg = strings.TrimSpace(defaultPerson.Tips)
} else if repeatedSubmit {
msg = "您已留资过,我们已再次通知卡若,请耐心等待添加"
} else {
msg = "提交成功,卡若会尽快联系您"
}
data := gin.H{}
if result.Data != nil {
if m, ok := result.Data.(map[string]interface{}); ok {
data = m
}
}
data["repeatedSubmit"] = repeatedSubmit
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
return
}
// 存客宝返回失败,透传其错误信息与 code便于前端/运营判断原因
errMsg := strings.TrimSpace(result.Message)
if errMsg == "" {
errMsg = "提交失败,请稍后重试"
}
// 特殊处理无效的apiKey错误提示检查配置
if strings.Contains(strings.ToLower(errMsg), "无效") || strings.Contains(strings.ToLower(errMsg), "invalid") || strings.Contains(strings.ToLower(errMsg), "apikey") {
if leadKey == "" || leadKey == ckbAPIKey {
errMsg = "系统配置异常请联系管理员检查CKB_LEAD_API_KEY配置"
} else {
errMsg = "提交失败,请稍后重试(配置错误)"
}
}
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,
})
}
// CKBLead POST /api/miniprogram/ckb/lead 小程序留资加好友:链接卡若(首页)、文章@某人、超级个体详情点头像
// 请求体phone/wechatId至少一个、userId补全昵称、targetUserIdPerson.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
}
_ = c.ShouldBindJSON(&body)
phone := strings.TrimSpace(body.Phone)
wechatId := strings.TrimSpace(body.WechatID)
if phone == "" && wechatId == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "请提供手机号或微信号"})
return
}
name := strings.TrimSpace(body.Name)
db := database.DB()
if name == "" && body.UserID != "" {
var u model.User
if db.Select("nickname").Where("id = ?", body.UserID).First(&u).Error == nil && u.Nickname != nil && *u.Nickname != "" {
name = *u.Nickname
}
}
if name == "" {
name = "小程序用户"
}
// 存客宝 scenarios 内部 API 需要计划级 apiKeypersons.ckb_api_key不是 token
// 文章 @ 场景targetUserId=token → 查 Person 取 CkbApiKey 作为 leadKey
// 首页链接卡若targetUserId 为空 → 用全局 getCkbLeadApiKey()
leadKey := getCkbLeadApiKey()
targetName := strings.TrimSpace(body.TargetNickname)
personTips := "" // Person 配置的获客成功提示,优先于默认文案
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
}
}
// 去重:同一用户对同一目标人物只记录一次(不再限制时间间隔,允许对不同人物立即提交)
repeatedSubmit := false
if body.UserID != "" && body.TargetUserID != "" {
var existCount int64
db.Model(&model.CkbLeadRecord{}).Where("user_id = ? AND target_person_id = ?", body.UserID, body.TargetUserID).Count(&existCount)
repeatedSubmit = existCount > 0
}
source := strings.TrimSpace(body.Source)
if source == "" {
source = "index_lead"
}
paramsJSON, _ := json.Marshal(map[string]interface{}{
"userId": body.UserID, "phone": phone, "wechatId": wechatId, "name": body.Name,
"targetUserId": body.TargetUserID, "source": source,
})
_ = db.Create(&model.CkbLeadRecord{
UserID: body.UserID,
Nickname: name,
Phone: phone,
WechatID: wechatId,
Name: strings.TrimSpace(body.Name),
TargetPersonID: body.TargetUserID,
Source: source,
Params: string(paramsJSON),
}).Error
ts := time.Now().Unix()
params := map[string]interface{}{
"name": name,
"timestamp": ts,
"apiKey": leadKey,
}
if phone != "" {
params["phone"] = phone
}
if wechatId != "" {
params["wechatId"] = 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 phone != "" {
q.Set("phone", phone)
}
if wechatId != "" {
q.Set("wechatId", wechatId)
}
q.Set("sign", params["sign"].(string))
reqURL := ckbAPIURL + "?" + q.Encode()
fmt.Printf("[CKBLead] 请求存客宝完整链接: %s\n", reqURL)
resp, err := http.Get(reqURL)
if err != nil {
fmt.Printf("[CKBLead] 请求存客宝失败: %v\n", err)
c.JSON(http.StatusOK, gin.H{"success": false, "message": "网络异常,请稍后重试"})
return
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
var result struct {
Code int `json:"code"`
Message string `json:"message"`
Msg string `json:"msg"` // 存客保部分接口用 msg 返回错误
Data interface{} `json:"data"`
}
_ = json.Unmarshal(b, &result)
if result.Code == 200 {
who := targetName
if who == "" {
who = "对方"
}
var msg string
if personTips != "" {
msg = personTips
} else if repeatedSubmit {
msg = fmt.Sprintf("您已留资过,我们已再次通知 %s请耐心等待添加", who)
} else {
msg = fmt.Sprintf("提交成功,%s 会尽快联系您", who)
}
data := gin.H{}
if result.Data != nil {
if m, ok := result.Data.(map[string]interface{}); ok {
data = m
}
}
data["repeatedSubmit"] = repeatedSubmit
go sendLeadWebhook(db, name, phone, wechatId, who, source, repeatedSubmit)
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
return
}
ckbMsg := strings.TrimSpace(result.Message)
if ckbMsg == "" {
ckbMsg = strings.TrimSpace(result.Msg)
}
errMsg := ckbMsg
if errMsg == "" {
errMsg = "提交失败,请稍后重试"
}
// apiKey 无效时,若使用的是 person-specific key自动回退到全局 key 重试一次
isKeyError := strings.Contains(strings.ToLower(errMsg), "无效") || strings.Contains(strings.ToLower(errMsg), "invalid") || strings.Contains(strings.ToLower(errMsg), "apikey")
globalKey := getCkbLeadApiKey()
if isKeyError && leadKey != globalKey && globalKey != "" {
fmt.Printf("[CKBLead] person key 无效,回退全局 key 重试 personKey=%s globalKey=%s\n", leadKey, globalKey)
retryParams := map[string]interface{}{
"name": name,
"timestamp": time.Now().Unix(),
"apiKey": globalKey,
}
if phone != "" {
retryParams["phone"] = phone
}
if wechatId != "" {
retryParams["wechatId"] = wechatId
}
retryParams["sign"] = ckbSign(retryParams, globalKey)
rq := url.Values{}
rq.Set("name", name)
rq.Set("timestamp", strconv.FormatInt(retryParams["timestamp"].(int64), 10))
rq.Set("apiKey", globalKey)
if phone != "" {
rq.Set("phone", phone)
}
if wechatId != "" {
rq.Set("wechatId", wechatId)
}
rq.Set("sign", retryParams["sign"].(string))
retryURL := ckbAPIURL + "?" + rq.Encode()
if retryResp, retryErr := http.Get(retryURL); retryErr == nil {
defer retryResp.Body.Close()
rb, _ := io.ReadAll(retryResp.Body)
var rr struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
_ = json.Unmarshal(rb, &rr)
if rr.Code == 200 {
who := targetName
if who == "" {
who = "对方"
}
var msg string
if personTips != "" {
msg = personTips
} else if repeatedSubmit {
msg = fmt.Sprintf("您已留资过,我们已再次通知 %s请耐心等待添加", who)
} else {
msg = fmt.Sprintf("提交成功,%s 会尽快联系您", who)
}
go sendLeadWebhook(db, name, phone, wechatId, who, source, repeatedSubmit)
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg})
return
}
fmt.Printf("[CKBLead] 全局 key 重试仍失败 code=%d msg=%s\n", rr.Code, rr.Message)
}
}
if isKeyError {
if leadKey == "" {
errMsg = "系统配置异常,请联系管理员检查存客宝密钥配置"
} else {
errMsg = "提交失败,请稍后重试(配置错误)"
}
}
fmt.Printf("[CKBLead] 存客宝返回异常 code=%d msg=%s raw=%s leadKey=%s\n", result.Code, ckbMsg, string(b), leadKey)
respObj := gin.H{
"success": false,
"message": errMsg,
"ckbCode": result.Code,
"ckbMessage": ckbMsg,
}
if ckbMsg == "" && len(b) > 0 {
respObj["ckbRaw"] = string(b)
}
c.JSON(http.StatusOK, respObj)
}
func sendLeadWebhook(db *gorm.DB, name, phone, wechat, target, source string, repeated bool) {
var cfg model.SystemConfig
if db.Where("config_key = ?", "ckb_lead_webhook_url").First(&cfg).Error != nil {
return
}
var webhookURL string
if len(cfg.ConfigValue) > 0 {
_ = json.Unmarshal(cfg.ConfigValue, &webhookURL)
}
webhookURL = strings.TrimSpace(webhookURL)
if webhookURL == "" || !strings.HasPrefix(webhookURL, "http") {
return
}
tag := "新获客"
if repeated {
tag = "重复获客"
}
text := fmt.Sprintf("[%s] %s → %s\n姓名: %s", tag, source, target, name)
if phone != "" {
text += fmt.Sprintf("\n手机: %s", phone)
}
if wechat != "" {
text += fmt.Sprintf("\n微信: %s", wechat)
}
text += fmt.Sprintf("\n时间: %s", time.Now().Format("2006-01-02 15:04"))
var payload []byte
if strings.Contains(webhookURL, "qyapi.weixin.qq.com") {
payload, _ = json.Marshal(map[string]interface{}{
"msgtype": "text",
"text": map[string]string{"content": text},
})
} else {
payload, _ = json.Marshal(map[string]interface{}{
"msg_type": "text",
"content": map[string]string{"text": text},
})
}
resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(payload))
if err != nil {
fmt.Printf("[CKBWebhook] 发送失败: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("[CKBWebhook] 已推送获客通知 → %s (status=%d)\n", target, resp.StatusCode)
}