package handler import ( "crypto/rand" "encoding/base64" "fmt" "net/http" "strings" "time" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" ) // DBPersonList GET /api/db/persons 管理端-@提及人物列表 func DBPersonList(c *gin.Context) { var rows []model.Person if err := database.DB().Order("name ASC").Find(&rows).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "persons": rows}) } // DBPersonDetail GET /api/db/person 管理端-单个人物详情(编辑回显用) func DBPersonDetail(c *gin.Context) { pid := c.Query("personId") if pid == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 personId"}) return } var row model.Person if err := database.DB().Where("person_id = ?", pid).First(&row).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "person": row}) } // DBPersonSave POST /api/db/persons 管理端-新增或更新人物 // 新增时自动生成 32 位唯一 token,文章 @ 时存 token,小程序点击时用 token 兑换真实密钥 func DBPersonSave(c *gin.Context) { var body struct { PersonID string `json:"personId"` Name string `json:"name"` Label string `json:"label"` CkbApiKey string `json:"ckbApiKey"` // 存客宝真实密钥,留空则 fallback 全局 Key Greeting string `json:"greeting"` Tips string `json:"tips"` RemarkType string `json:"remarkType"` RemarkFormat string `json:"remarkFormat"` AddFriendInterval *int `json:"addFriendInterval"` StartTime string `json:"startTime"` EndTime string `json:"endTime"` DeviceGroups []int64 `json:"deviceGroups"` // 设备ID列表,由管理端选择后传入 } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"}) return } if body.Name == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "name 必填"}) return } if body.PersonID == "" { body.PersonID = fmt.Sprintf("%s_%d", body.Name, time.Now().UnixMilli()) } db := database.DB() var existing model.Person if db.Where("person_id = ?", body.PersonID).First(&existing).Error == nil { existing.Name = body.Name existing.Label = body.Label existing.CkbApiKey = body.CkbApiKey existing.Greeting = body.Greeting existing.Tips = body.Tips existing.RemarkType = body.RemarkType existing.RemarkFormat = body.RemarkFormat if body.AddFriendInterval != nil && *body.AddFriendInterval > 0 { existing.AddFriendInterval = *body.AddFriendInterval } if strings.TrimSpace(body.StartTime) != "" { existing.StartTime = strings.TrimSpace(body.StartTime) } if strings.TrimSpace(body.EndTime) != "" { existing.EndTime = strings.TrimSpace(body.EndTime) } if len(body.DeviceGroups) > 0 { ids := make([]string, 0, len(body.DeviceGroups)) for _, id := range body.DeviceGroups { if id > 0 { ids = append(ids, fmt.Sprintf("%d", id)) } } existing.DeviceGroups = strings.Join(ids, ",") } else { existing.DeviceGroups = "" } db.Save(&existing) c.JSON(http.StatusOK, gin.H{"success": true, "person": existing}) return } // 新增:创建本地 Person 记录前,先在存客宝创建获客计划并获取 planId + apiKey tok, err := genPersonToken() if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "生成 token 失败"}) return } // 1. 获取开放 API token openToken, err := ckbOpenGetToken() if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } // 2. 构造创建计划请求体 // 参考 Cunkebao createPlan:name, sceneId, scenario, remarkType, greeting, addInterval, startTime, endTime, enabled, tips, deviceGroups name := fmt.Sprintf("SOUL链接人与事-%s", body.Name) addInterval := 1 if body.AddFriendInterval != nil && *body.AddFriendInterval > 0 { addInterval = *body.AddFriendInterval } startTime := "09:00" if strings.TrimSpace(body.StartTime) != "" { startTime = strings.TrimSpace(body.StartTime) } endTime := "18:00" if strings.TrimSpace(body.EndTime) != "" { endTime = strings.TrimSpace(body.EndTime) } deviceIDs := make([]int64, 0, len(body.DeviceGroups)) for _, id := range body.DeviceGroups { if id > 0 { deviceIDs = append(deviceIDs, id) } } planPayload := map[string]interface{}{ "name": name, "sceneId": 11, "scenario": 11, "remarkType": body.RemarkType, "greeting": body.Greeting, "addInterval": addInterval, "startTime": startTime, "endTime": endTime, "enabled": true, "tips": body.Tips, "distributionEnabled": false, } if len(deviceIDs) > 0 { planPayload["deviceGroups"] = deviceIDs } planID, ckbCreateData, ckbResponse, err := ckbOpenCreatePlan(openToken, planPayload) if err != nil { out := gin.H{"success": false, "error": "创建存客宝计划失败: " + err.Error()} if ckbResponse != nil { out["ckbResponse"] = ckbResponse } c.JSON(http.StatusOK, out) return } // 3. 用 planId 拉计划详情,获取 apiKey apiKey, err := ckbOpenGetPlanDetail(openToken, planID) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建成功但获取计划密钥失败: " + err.Error()}) return } newPerson := model.Person{ PersonID: body.PersonID, Token: tok, Name: body.Name, Label: body.Label, CkbApiKey: apiKey, CkbPlanID: planID, Greeting: body.Greeting, Tips: body.Tips, RemarkType: body.RemarkType, RemarkFormat: body.RemarkFormat, AddFriendInterval: addInterval, StartTime: startTime, EndTime: endTime, } if len(deviceIDs) > 0 { idsStr := make([]string, 0, len(deviceIDs)) for _, id := range deviceIDs { idsStr = append(idsStr, fmt.Sprintf("%d", id)) } newPerson.DeviceGroups = strings.Join(idsStr, ",") } if err := db.Create(&newPerson).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } resp := gin.H{"success": true, "person": newPerson} if len(ckbCreateData) > 0 { resp["ckbCreateResult"] = ckbCreateData } c.JSON(http.StatusOK, resp) } func genPersonToken() (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return "", err } s := base64.URLEncoding.EncodeToString(b) s = strings.ReplaceAll(s, "+", "") s = strings.ReplaceAll(s, "/", "") s = strings.ReplaceAll(s, "=", "") if len(s) >= 32 { return s[:32], nil } return s + "0123456789abcdefghijklmnopqrstuv"[:(32-len(s))], nil } // DBPersonDelete DELETE /api/db/persons?personId=xxx 管理端-删除人物 func DBPersonDelete(c *gin.Context) { pid := c.Query("personId") if pid == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 personId"}) return } if err := database.DB().Where("person_id = ?", pid).Delete(&model.Person{}).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true}) }