From ad3725b9f6281bc37218242037303da47083405a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=8B=A5?= Date: Sun, 8 Mar 2026 08:26:11 +0800 Subject: [PATCH] =?UTF-8?q?sync:=20soul-api=20=E6=8E=A5=E5=8F=A3=E9=80=BB?= =?UTF-8?q?=E8=BE=91=20|=20=E5=8E=9F=E5=9B=A0:=20=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E9=80=BB=E8=BE=91=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/handler/admin_shensheshou.go | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 soul-api/internal/handler/admin_shensheshou.go diff --git a/soul-api/internal/handler/admin_shensheshou.go b/soul-api/internal/handler/admin_shensheshou.go new file mode 100644 index 00000000..24091e72 --- /dev/null +++ b/soul-api/internal/handler/admin_shensheshou.go @@ -0,0 +1,185 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "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"]}) +} + +// 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"]}) +}