2026-03-10 11:04:34 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-14 14:37:17 +08:00
|
|
|
|
"crypto/rand"
|
|
|
|
|
|
"encoding/base64"
|
2026-03-10 11:04:34 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"net/http"
|
2026-03-14 14:37:17 +08:00
|
|
|
|
"strings"
|
2026-03-10 11:04:34 +08:00
|
|
|
|
"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})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 14:37:17 +08:00
|
|
|
|
// 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})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 11:04:34 +08:00
|
|
|
|
// DBPersonSave POST /api/db/persons 管理端-新增或更新人物
|
2026-03-14 14:37:17 +08:00
|
|
|
|
// 新增时自动生成 32 位唯一 token,文章 @ 时存 token,小程序点击时用 token 兑换真实密钥
|
2026-03-10 11:04:34 +08:00
|
|
|
|
func DBPersonSave(c *gin.Context) {
|
|
|
|
|
|
var body struct {
|
2026-03-14 14:37:17 +08:00
|
|
|
|
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列表,由管理端选择后传入
|
2026-03-10 11:04:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
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
|
2026-03-10 18:06:10 +08:00
|
|
|
|
existing.CkbApiKey = body.CkbApiKey
|
2026-03-14 14:37:17 +08:00
|
|
|
|
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 = ""
|
|
|
|
|
|
}
|
2026-03-10 11:04:34 +08:00
|
|
|
|
db.Save(&existing)
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "person": existing})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-14 14:37:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 新增:创建本地 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 {
|
2026-03-10 11:04:34 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-14 14:37:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 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{}{
|
2026-03-14 16:23:01 +08:00
|
|
|
|
"name": name,
|
|
|
|
|
|
"sceneId": 11,
|
|
|
|
|
|
"scenario": 11,
|
|
|
|
|
|
"remarkType": body.RemarkType,
|
|
|
|
|
|
"greeting": body.Greeting,
|
|
|
|
|
|
"addInterval": addInterval,
|
|
|
|
|
|
"startTime": startTime,
|
|
|
|
|
|
"endTime": endTime,
|
|
|
|
|
|
"enabled": true,
|
|
|
|
|
|
"tips": body.Tips,
|
2026-03-14 14:37:17 +08:00
|
|
|
|
"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{
|
2026-03-14 16:23:01 +08:00
|
|
|
|
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,
|
2026-03-14 14:37:17 +08:00
|
|
|
|
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
|
2026-03-10 11:04:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DBPersonDelete DELETE /api/db/persons?personId=xxx 管理端-删除人物
|
2026-03-14 16:23:01 +08:00
|
|
|
|
// 若有 ckb_plan_id,先调存客宝删除计划,再删本地
|
2026-03-10 11:04:34 +08:00
|
|
|
|
func DBPersonDelete(c *gin.Context) {
|
|
|
|
|
|
pid := c.Query("personId")
|
|
|
|
|
|
if pid == "" {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 personId"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-14 16:23:01 +08:00
|
|
|
|
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": "人物不存在"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 若有存客宝计划,先调 CKB 删除
|
|
|
|
|
|
if row.CkbPlanID > 0 {
|
|
|
|
|
|
token, err := ckbOpenGetToken()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "获取存客宝鉴权失败: " + err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := ckbOpenDeletePlan(token, row.CkbPlanID); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "删除存客宝计划失败: " + err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 11:04:34 +08:00
|
|
|
|
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})
|
|
|
|
|
|
}
|