Files
soul-yongping/soul-api/internal/handler/db_person.go
Alex-larget d4ba905ee5 通过自动提及和标签创建功能,增强文章编辑功能
- 在文章编辑过程中,实现了自动创建不存在的@提及和#标签的功能,确保它们被添加到相应的数据库中。
- 更新了内容处理逻辑,以利用新创建的提及和标签,从而改善用户体验和内容管理。
- 增强了人物和链接标签创建的后端处理能力,使文章编辑过程中能够实现无缝集成。
2026-03-16 11:09:26 +08:00

261 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
db := database.DB()
var existing model.Person
// 按 name 查找文章编辑自动创建场景PersonID 为空时先查是否已存在
if body.PersonID == "" {
if db.Where("name = ?", strings.TrimSpace(body.Name)).First(&existing).Error == nil {
c.JSON(http.StatusOK, gin.H{"success": true, "person": existing})
return
}
}
if body.PersonID == "" {
body.PersonID = fmt.Sprintf("%s_%d", strings.ToLower(strings.ReplaceAll(strings.TrimSpace(body.Name), " ", "_")), time.Now().UnixMilli())
}
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 createPlanname, 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 管理端-删除人物
// 若有 ckb_plan_id先调存客宝删除计划再删本地
func DBPersonDelete(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": "人物不存在"})
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
}
}
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})
}