package handler import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" "soul-api/internal/database" "github.com/gin-gonic/gin" ) var sssHTTPClient = &http.Client{Timeout: 15 * time.Second} func sssBaseURL() string { u := os.Getenv("SHENSHESHOU_BASE_URL") if u == "" { u = "https://your-domain.com/api/shensheshou" } return strings.TrimRight(u, "/") } func sssAPIKey() string { return os.Getenv("SHENSHESHOU_API_KEY") } func sssDoRequest(method, url string, body interface{}) (map[string]interface{}, error) { var reqBody io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return nil, err } reqBody = bytes.NewReader(b) } req, err := http.NewRequest(method, url, reqBody) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+sssAPIKey()) req.Header.Set("Content-Type", "application/json") resp, err := sssHTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %v", err) } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var result map[string]interface{} if err := json.Unmarshal(raw, &result); err != nil { return nil, fmt.Errorf("响应解析失败: %v", err) } return result, nil } // AdminShensheShouQuery GET /api/admin/shensheshou/query?phone=xxx&openId=xxx func AdminShensheShouQuery(c *gin.Context) { if sssAPIKey() == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "神射手 API Key 未配置,请在 .env 中设置 SHENSHESHOU_API_KEY"}) return } phone := strings.TrimSpace(c.Query("phone")) openID := strings.TrimSpace(c.Query("openId")) if phone == "" && openID == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "需要 phone 或 openId 参数"}) return } queryURL := sssBaseURL() + "?endpoint=user" if phone != "" { queryURL += "&phone=" + phone } result, err := sssDoRequest(http.MethodGet, queryURL, nil) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } if success, ok := result["success"].(bool); ok && !success { errMsg := "查询失败" if e, ok := result["error"].(string); ok { errMsg = e } c.JSON(http.StatusOK, gin.H{"success": false, "error": errMsg}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": result["data"]}) } // AdminShensheShouIngest POST /api/admin/shensheshou/ingest — 推送用户数据到神射手 func AdminShensheShouIngest(c *gin.Context) { if sssAPIKey() == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "神射手 API Key 未配置"}) return } var body struct { Users []map[string]interface{} `json:"users" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"}) return } payload := map[string]interface{}{ "endpoint": "ingest", "source": "cunkebao", "users": body.Users, } result, err := sssDoRequest(http.MethodPost, sssBaseURL(), payload) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } if success, ok := result["success"].(bool); ok && !success { errMsg := "推送失败" if e, ok := result["error"].(string); ok { errMsg = e } c.JSON(http.StatusOK, gin.H{"success": false, "error": errMsg}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": result["data"]}) } // AdminShensheShouEnrich POST /api/admin/shensheshou/enrich — 查询并回填用户基础信息 func AdminShensheShouEnrich(c *gin.Context) { if sssAPIKey() == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "神射手 API Key 未配置"}) return } var body struct { UserID string `json:"userId" binding:"required"` Phone string `json:"phone"` OpenID string `json:"openId"` WechatID string `json:"wechatId"` Address string `json:"address"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"}) return } if body.Phone == "" && body.OpenID == "" && body.WechatID == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "至少提供手机号、OpenID 或微信号之一"}) return } // 查询神射手 queryURL := sssBaseURL() + "?endpoint=user" if body.Phone != "" { queryURL += "&phone=" + body.Phone } sssResult, err := sssDoRequest(http.MethodGet, queryURL, nil) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } var sssData map[string]interface{} if d, ok := sssResult["data"].(map[string]interface{}); ok { sssData = d } // 将神射手数据回填到用户 tags if sssData != nil { db := database.DB() updates := make(map[string]interface{}) // 自动合并标签 if tags, ok := sssData["tags"].([]interface{}); ok && len(tags) > 0 { tagStrs := make([]string, 0, len(tags)) for _, t := range tags { if s, ok := t.(string); ok { tagStrs = append(tagStrs, s) } } if len(tagStrs) > 0 { tagsJSON, _ := json.Marshal(tagStrs) updates["ckb_tags"] = string(tagsJSON) } } if len(updates) > 0 { db.Table("users").Where("id = ?", body.UserID).Updates(updates) } } c.JSON(http.StatusOK, gin.H{"success": true, "data": sssData}) } // AdminShensheShouAutoTag POST /api/admin/shensheshou/auto-tag — 基于行为自动打标(内部调用) func AdminShensheShouAutoTag(userID string, phone string) { if sssAPIKey() == "" || phone == "" { return } go func() { queryURL := sssBaseURL() + "?endpoint=user&phone=" + phone result, err := sssDoRequest(http.MethodGet, queryURL, nil) if err != nil || result == nil { return } data, ok := result["data"].(map[string]interface{}) if !ok { return } tags, ok := data["tags"].([]interface{}) if !ok || len(tags) == 0 { return } tagStrs := make([]string, 0, len(tags)) for _, t := range tags { if s, ok := t.(string); ok { tagStrs = append(tagStrs, s) } } tagsJSON, _ := json.Marshal(tagStrs) database.DB().Table("users").Where("id = ?", userID). Updates(map[string]interface{}{"ckb_tags": string(tagsJSON)}) }() } // AdminShensheShouBatchQuery POST /api/admin/shensheshou/batch — 批量查询 func AdminShensheShouBatchQuery(c *gin.Context) { if sssAPIKey() == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "神射手 API Key 未配置"}) return } var body struct { Phones []string `json:"phones"` Fields []string `json:"fields"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"}) return } if len(body.Phones) == 0 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "phones 不能为空"}) return } if len(body.Phones) > 100 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "单次最多 100 个"}) return } fields := body.Fields if len(fields) == 0 { fields = []string{"rfm_score", "user_level", "tags"} } payload := map[string]interface{}{ "endpoint": "users/batch", "phones": body.Phones, "fields": fields, } result, err := sssDoRequest(http.MethodPost, sssBaseURL(), payload) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": result["data"]}) }