2026-03-07 22:58:43 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"crypto/md5"
|
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"sort"
|
|
|
|
|
|
"strconv"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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": "创业合伙,创业伙伴"}
|
|
|
|
|
|
|
|
|
|
|
|
// 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[:])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-08 11:06:26 +08:00
|
|
|
|
// 同时写入 match_records 表,让后台「找伙伴」各 Tab 能看到
|
|
|
|
|
|
if body.UserID != "" {
|
|
|
|
|
|
rec := model.MatchRecord{
|
|
|
|
|
|
ID: fmt.Sprintf("mr_ckb_%d", time.Now().UnixNano()),
|
|
|
|
|
|
UserID: body.UserID,
|
|
|
|
|
|
MatchType: body.Type,
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Phone != "" {
|
|
|
|
|
|
rec.Phone = &body.Phone
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Wechat != "" {
|
|
|
|
|
|
rec.WechatID = &body.Wechat
|
|
|
|
|
|
}
|
|
|
|
|
|
database.DB().Create(&rec)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "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
|
|
|
|
|
|
}
|
|
|
|
|
|
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})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CKBLead POST /api/miniprogram/ckb/lead 小程序-链接卡若:上报线索到存客宝,便于卡若添加好友
|
|
|
|
|
|
// 请求体:phone(可选)、wechatId(可选)、name(可选)、userId(可选,用于补全昵称)
|
|
|
|
|
|
// 至少传 phone 或 wechatId 之一;签名规则同 api_v1.md
|
|
|
|
|
|
func CKBLead(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 == "" && wechatId == "" {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": "请提供手机号或微信号"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
name := strings.TrimSpace(body.Name)
|
|
|
|
|
|
if name == "" && body.UserID != "" {
|
|
|
|
|
|
var u model.User
|
|
|
|
|
|
if database.DB().Select("nickname").Where("id = ?", body.UserID).First(&u).Error == nil && u.Nickname != nil && *u.Nickname != "" {
|
|
|
|
|
|
name = *u.Nickname
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if name == "" {
|
|
|
|
|
|
name = "小程序用户"
|
|
|
|
|
|
}
|
|
|
|
|
|
ts := time.Now().Unix()
|
|
|
|
|
|
params := map[string]interface{}{
|
|
|
|
|
|
"timestamp": ts,
|
|
|
|
|
|
"source": "小程序-链接卡若",
|
2026-03-08 11:35:50 +08:00
|
|
|
|
"tags": "链接卡若,创业实验",
|
|
|
|
|
|
"siteTags": "创业实验APP,链接卡若",
|
2026-03-07 22:58:43 +08:00
|
|
|
|
"remark": "首页点击「链接卡若」留资",
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
}
|
|
|
|
|
|
if phone != "" {
|
|
|
|
|
|
params["phone"] = phone
|
|
|
|
|
|
}
|
|
|
|
|
|
if wechatId != "" {
|
|
|
|
|
|
params["wechatId"] = wechatId
|
|
|
|
|
|
}
|
|
|
|
|
|
params["apiKey"] = ckbAPIKey
|
|
|
|
|
|
params["sign"] = ckbSign(params, ckbAPIKey)
|
|
|
|
|
|
raw, _ := json.Marshal(params)
|
2026-03-08 11:35:50 +08:00
|
|
|
|
fmt.Printf("[CKBLead] 请求: phone=%s wechatId=%s name=%s\n", phone, wechatId, name)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
resp, err := http.Post(ckbAPIURL, "application/json", bytes.NewReader(raw))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
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)
|
2026-03-08 11:36:04 +08:00
|
|
|
|
fmt.Printf("[CKBLead] 响应: code=%d message=%s raw=%s\n", result.Code, result.Message, string(b))
|
2026-03-07 22:58:43 +08:00
|
|
|
|
if result.Code == 200 {
|
|
|
|
|
|
msg := "提交成功,卡若会尽快联系您"
|
|
|
|
|
|
if result.Message == "已存在" {
|
|
|
|
|
|
msg = "您已留资,我们会尽快联系您"
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": result.Data})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
errMsg := result.Message
|
|
|
|
|
|
if errMsg == "" {
|
|
|
|
|
|
errMsg = "提交失败,请稍后重试"
|
|
|
|
|
|
}
|
2026-03-08 11:36:04 +08:00
|
|
|
|
fmt.Printf("[CKBLead] 失败: phone=%s code=%d message=%s\n", phone, result.Code, result.Message)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": errMsg})
|
|
|
|
|
|
}
|