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) } } 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": "小程序-链接卡若", "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) 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) 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 = "提交失败,请稍后重试" } c.JSON(http.StatusOK, gin.H{"success": false, "message": errMsg}) }