Files
soul-yongping/soul-api/internal/handler/db_person.go
2026-03-16 09:21:39 +08:00

313 lines
10 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"`
Aliases string `json:"aliases"`
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.Aliases = body.Aliases
existing.Label = body.Label
if strings.TrimSpace(body.CkbApiKey) != "" {
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 = ""
}
if err := db.Save(&existing).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "person": existing})
return
}
// 新增:先创建本地人物记录,存客宝同步失败不阻断 @人物 与内容管理主链路
tok, err := genPersonToken()
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "生成 token 失败"})
return
}
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)
}
}
newPerson := model.Person{
PersonID: body.PersonID,
Token: tok,
Name: body.Name,
Aliases: body.Aliases,
Label: body.Label,
CkbApiKey: strings.TrimSpace(body.CkbApiKey),
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}
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
}
openToken, tokenErr := ckbOpenGetToken()
if tokenErr != nil {
resp["ckbSyncError"] = tokenErr.Error()
resp["message"] = "人物已保存,存客宝同步失败,可稍后补同步"
c.JSON(http.StatusOK, resp)
return
}
planID, ckbCreateData, ckbResponse, planErr := ckbOpenCreatePlan(openToken, planPayload)
if planErr != nil {
resp["ckbSyncError"] = "创建存客宝计划失败: " + planErr.Error()
if ckbResponse != nil {
resp["ckbResponse"] = ckbResponse
}
resp["message"] = "人物已保存,存客宝同步失败,可稍后补同步"
c.JSON(http.StatusOK, resp)
return
}
apiKey, detailErr := ckbOpenGetPlanDetail(openToken, planID)
if detailErr != nil {
db.Model(&model.Person{}).Where("person_id = ?", newPerson.PersonID).Update("ckb_plan_id", planID)
newPerson.CkbPlanID = planID
resp["person"] = newPerson
resp["ckbSyncError"] = "创建成功但获取计划密钥失败: " + detailErr.Error()
resp["message"] = "人物已保存,存客宝部分同步成功"
if len(ckbCreateData) > 0 {
resp["ckbCreateResult"] = ckbCreateData
}
c.JSON(http.StatusOK, resp)
return
}
newPerson.CkbPlanID = planID
newPerson.CkbApiKey = apiKey
if err := db.Model(&model.Person{}).Where("person_id = ?", newPerson.PersonID).Updates(map[string]interface{}{
"ckb_api_key": apiKey,
"ckb_plan_id": planID,
}).Error; err == nil {
resp["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
}
// DBPersonFixCKB POST /api/db/persons/fix-ckb 为缺少 ckb_api_key 的人物批量创建存客宝获客计划
func DBPersonFixCKB(c *gin.Context) {
db := database.DB()
var persons []model.Person
if err := db.Where("ckb_api_key = '' OR ckb_api_key IS NULL").Find(&persons).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
if len(persons) == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "message": "所有人物已有 ckb_api_key", "fixed": 0})
return
}
openToken, err := ckbOpenGetToken()
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
results := make([]gin.H, 0, len(persons))
fixed := 0
for _, p := range persons {
planPayload := map[string]interface{}{
"name": fmt.Sprintf("SOUL链接人与事-%s", p.Name),
"sceneId": 11,
"scenario": 11,
"remarkType": p.RemarkType,
"greeting": p.Greeting,
"addInterval": 1,
"startTime": "09:00",
"endTime": "18:00",
"enabled": true,
"tips": p.Tips,
"distributionEnabled": false,
}
planID, _, _, planErr := ckbOpenCreatePlan(openToken, planPayload)
if planErr != nil {
results = append(results, gin.H{"personId": p.PersonID, "name": p.Name, "error": planErr.Error()})
continue
}
apiKey, keyErr := ckbOpenGetPlanDetail(openToken, planID)
if keyErr != nil {
results = append(results, gin.H{"personId": p.PersonID, "name": p.Name, "error": keyErr.Error()})
continue
}
db.Model(&model.Person{}).Where("person_id = ?", p.PersonID).Updates(map[string]interface{}{
"ckb_api_key": apiKey,
"ckb_plan_id": planID,
})
results = append(results, gin.H{"personId": p.PersonID, "name": p.Name, "apiKey": apiKey, "planId": planID})
fixed++
}
c.JSON(http.StatusOK, gin.H{"success": true, "fixed": fixed, "total": len(persons), "results": results})
}
// 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})
}