Enhance profile editing and sharing functionality
- Added a new feature for sharing profile cards, including special handling for forwarding to friends and displaying a canvas cover with user information. - Updated the mini program's profile-edit page to generate a shareable card with a structured layout, including user avatar, nickname, and additional information. - Improved the documentation to reflect the new sharing capabilities and updated the last modified date for relevant entries.
This commit is contained in:
205
soul-api/internal/handler/autolink.go
Normal file
205
soul-api/internal/handler/autolink.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// sanitizeNameOrLabel 去除括号及内容,如 南风(管理) -> 南风
|
||||
func sanitizeNameOrLabel(s string) string {
|
||||
re := regexp.MustCompile(`\s*[((][^))]*(\)|))?`)
|
||||
return strings.TrimSpace(re.ReplaceAllString(s, ""))
|
||||
}
|
||||
|
||||
// isValidNameOrLabel 仅允许包含至少一个汉字/字母/数字,纯符号(如 "、,、!)则跳过,不创建
|
||||
func isValidNameOrLabel(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
// 至少包含一个 Unicode 字母或数字
|
||||
re := regexp.MustCompile(`[\p{L}\p{N}]`)
|
||||
return re.MatchString(s)
|
||||
}
|
||||
|
||||
// ensurePersonByName 按名称确保 Person 存在,不存在则创建(含存客宝计划),返回 token
|
||||
func ensurePersonByName(db *gorm.DB, name string) (token string, err error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return "", nil
|
||||
}
|
||||
clean := sanitizeNameOrLabel(name)
|
||||
if clean == "" || !isValidNameOrLabel(clean) {
|
||||
return "", nil
|
||||
}
|
||||
var p model.Person
|
||||
if db.Where("name = ?", clean).First(&p).Error == nil {
|
||||
return p.Token, nil
|
||||
}
|
||||
// 按净化后的名字再查一次
|
||||
if db.Where("name = ?", name).First(&p).Error == nil {
|
||||
return p.Token, nil
|
||||
}
|
||||
created, err := createPersonMinimal(db, clean)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return created.Token, nil
|
||||
}
|
||||
|
||||
// ensureLinkTagByLabel 按 label 确保 LinkTag 存在,不存在则创建
|
||||
func ensureLinkTagByLabel(db *gorm.DB, label string) (tagID string, err error) {
|
||||
label = strings.TrimSpace(label)
|
||||
if label == "" {
|
||||
return "", nil
|
||||
}
|
||||
clean := sanitizeNameOrLabel(label)
|
||||
if clean == "" || !isValidNameOrLabel(clean) {
|
||||
return "", nil
|
||||
}
|
||||
var t model.LinkTag
|
||||
if db.Where("label = ?", clean).First(&t).Error == nil {
|
||||
return t.TagID, nil
|
||||
}
|
||||
if db.Where("label = ?", label).First(&t).Error == nil {
|
||||
return t.TagID, nil
|
||||
}
|
||||
t = model.LinkTag{TagID: clean, Label: clean, Type: "url"}
|
||||
if err := db.Create(&t).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
return t.TagID, nil
|
||||
}
|
||||
|
||||
// ParseAutoLinkContent 解析 content 中的 @人物 和 #标签,确保存在并转为带 data-id 的 span,后端统一处理
|
||||
func ParseAutoLinkContent(content string) (string, error) {
|
||||
if content == "" || (!strings.Contains(content, "@") && !strings.Contains(content, "#")) {
|
||||
return content, nil
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
// 1. 提取所有 @name 和 #label(排除 <> 避免匹配到 HTML 标签内)
|
||||
mentionRe := regexp.MustCompile(`@([^\s@#<>]+)`)
|
||||
tagRe := regexp.MustCompile(`#([^\s@#<>]+)`)
|
||||
names := make(map[string]string) // cleanName -> token
|
||||
labels := make(map[string]string) // cleanLabel -> tagId
|
||||
|
||||
for _, m := range mentionRe.FindAllStringSubmatch(content, -1) {
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
raw := m[1]
|
||||
clean := sanitizeNameOrLabel(raw)
|
||||
if clean == "" || !isValidNameOrLabel(clean) || names[clean] != "" {
|
||||
continue
|
||||
}
|
||||
token, err := ensurePersonByName(db, clean)
|
||||
if err == nil && token != "" {
|
||||
names[clean] = token
|
||||
names[raw] = token // 原始名也映射
|
||||
}
|
||||
}
|
||||
for _, m := range tagRe.FindAllStringSubmatch(content, -1) {
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
raw := m[1]
|
||||
clean := sanitizeNameOrLabel(raw)
|
||||
if clean == "" || !isValidNameOrLabel(clean) || labels[clean] != "" {
|
||||
continue
|
||||
}
|
||||
tagID, err := ensureLinkTagByLabel(db, clean)
|
||||
if err == nil && tagID != "" {
|
||||
labels[clean] = tagID
|
||||
labels[raw] = tagID
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 占位符替换:先把已有 mention/linkTag span 保护起来
|
||||
mentionSpanRe := regexp.MustCompile(`<span[^>]*data-type="mention"[^>]*>.*?</span>`)
|
||||
linkTagSpanRe := regexp.MustCompile(`<span[^>]*data-type="linkTag"[^>]*>.*?</span>`)
|
||||
placeholders := []string{}
|
||||
|
||||
content = mentionSpanRe.ReplaceAllStringFunc(content, func(m string) string {
|
||||
// 回填 data-id:若为空则按昵称匹配
|
||||
idRe := regexp.MustCompile(`data-id="([^"]*)"`)
|
||||
labelRe := regexp.MustCompile(`data-label="([^"]*)"`)
|
||||
innerRe := regexp.MustCompile(`>([^<]+)<`)
|
||||
nickname := ""
|
||||
if idRe.MatchString(m) {
|
||||
sub := idRe.FindStringSubmatch(m)
|
||||
if len(sub) >= 2 && strings.TrimSpace(sub[1]) != "" {
|
||||
placeholders = append(placeholders, m)
|
||||
return "\x00PLACEHOLDER\x00"
|
||||
}
|
||||
}
|
||||
if labelRe.MatchString(m) {
|
||||
sub := labelRe.FindStringSubmatch(m)
|
||||
if len(sub) >= 2 {
|
||||
nickname = sanitizeNameOrLabel(sub[1])
|
||||
}
|
||||
}
|
||||
if nickname == "" && innerRe.MatchString(m) {
|
||||
sub := innerRe.FindStringSubmatch(m)
|
||||
if len(sub) >= 2 {
|
||||
nickname = sanitizeNameOrLabel(strings.TrimPrefix(sub[1], "@"))
|
||||
}
|
||||
}
|
||||
if nickname != "" {
|
||||
if token, ok := names[nickname]; ok && token != "" {
|
||||
// 插入或替换 data-id
|
||||
if idRe.MatchString(m) {
|
||||
m = idRe.ReplaceAllString(m, `data-id="`+token+`"`)
|
||||
} else {
|
||||
// 在 data-type 后插入 data-id
|
||||
m = strings.Replace(m, `data-type="mention"`, `data-type="mention" data-id="`+token+`"`, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
placeholders = append(placeholders, m)
|
||||
return "\x00PLACEHOLDER\x00"
|
||||
})
|
||||
|
||||
content = linkTagSpanRe.ReplaceAllStringFunc(content, func(m string) string {
|
||||
placeholders = append(placeholders, m)
|
||||
return "\x00PLACEHOLDER\x00"
|
||||
})
|
||||
|
||||
// 3. 替换纯文本 @name 和 #label
|
||||
content = mentionRe.ReplaceAllStringFunc(content, func(m string) string {
|
||||
raw := strings.TrimPrefix(m, "@")
|
||||
clean := sanitizeNameOrLabel(raw)
|
||||
token := names[clean]
|
||||
if token == "" {
|
||||
token = names[raw]
|
||||
}
|
||||
if token == "" {
|
||||
return m
|
||||
}
|
||||
return `<span data-type="mention" data-id="` + token + `" class="mention-tag">@` + raw + `</span>`
|
||||
})
|
||||
|
||||
content = tagRe.ReplaceAllStringFunc(content, func(m string) string {
|
||||
raw := strings.TrimPrefix(m, "#")
|
||||
clean := sanitizeNameOrLabel(raw)
|
||||
tagID := labels[clean]
|
||||
if tagID == "" {
|
||||
tagID = labels[raw]
|
||||
}
|
||||
if tagID == "" {
|
||||
return m
|
||||
}
|
||||
return `<span data-type="linkTag" data-url="" data-tag-type="url" data-tag-id="` + tagID + `" data-page-path="" data-app-id="" class="link-tag-node">#` + raw + `</span>`
|
||||
})
|
||||
|
||||
// 4. 恢复占位符
|
||||
for _, p := range placeholders {
|
||||
content = strings.Replace(content, "\x00PLACEHOLDER\x00", p, 1)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
@@ -389,6 +389,7 @@ func CKBIndexLead(c *gin.Context) {
|
||||
q.Set("phone", phone)
|
||||
q.Set("sign", params["sign"].(string))
|
||||
reqURL := ckbAPIURL + "?" + q.Encode()
|
||||
fmt.Printf("[CKBIndexLead] 请求存客宝完整链接: %s\n", reqURL)
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
fmt.Printf("[CKBIndexLead] 请求存客宝失败: %v\n", err)
|
||||
@@ -463,18 +464,24 @@ func CKBLead(c *gin.Context) {
|
||||
name = "小程序用户"
|
||||
}
|
||||
|
||||
// 确定使用哪个存客宝密钥:被@人物的 ckb_api_key > 全局(system_config > .env > 代码内置)
|
||||
// 存客宝 scenarios 内部 API 需要计划级 apiKey(persons.ckb_api_key),不是 token
|
||||
// 文章 @ 场景:targetUserId=token → 查 Person 取 CkbApiKey 作为 leadKey
|
||||
// 首页链接卡若:targetUserId 为空 → 用全局 getCkbLeadApiKey()
|
||||
leadKey := getCkbLeadApiKey()
|
||||
targetName := strings.TrimSpace(body.TargetNickname) // 被@人的显示名,用于成功文案
|
||||
targetName := strings.TrimSpace(body.TargetNickname)
|
||||
if body.TargetUserID != "" {
|
||||
var p model.Person
|
||||
if db.Where("token = ?", body.TargetUserID).First(&p).Error == nil {
|
||||
if p.CkbApiKey != "" {
|
||||
leadKey = p.CkbApiKey
|
||||
}
|
||||
if targetName == "" {
|
||||
targetName = p.Name
|
||||
}
|
||||
if db.Where("token = ?", body.TargetUserID).First(&p).Error != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "未找到该人物配置,请稍后重试"})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.CkbApiKey) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "该人物尚未配置存客宝密钥,请联系管理员"})
|
||||
return
|
||||
}
|
||||
leadKey = p.CkbApiKey
|
||||
if targetName == "" {
|
||||
targetName = p.Name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,6 +558,7 @@ func CKBLead(c *gin.Context) {
|
||||
}
|
||||
q.Set("sign", params["sign"].(string))
|
||||
reqURL := ckbAPIURL + "?" + q.Encode()
|
||||
fmt.Printf("[CKBLead] 请求存客宝完整链接: %s\n", reqURL)
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
fmt.Printf("[CKBLead] 请求存客宝失败: %v\n", err)
|
||||
@@ -562,6 +570,7 @@ func CKBLead(c *gin.Context) {
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Msg string `json:"msg"` // 存客保部分接口用 msg 返回错误
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(b, &result)
|
||||
@@ -585,10 +594,23 @@ func CKBLead(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
|
||||
return
|
||||
}
|
||||
errMsg := result.Message
|
||||
ckbMsg := strings.TrimSpace(result.Message)
|
||||
if ckbMsg == "" {
|
||||
ckbMsg = strings.TrimSpace(result.Msg)
|
||||
}
|
||||
errMsg := ckbMsg
|
||||
if errMsg == "" {
|
||||
errMsg = "提交失败,请稍后重试"
|
||||
}
|
||||
fmt.Printf("[CKBLead] 存客宝返回异常 code=%d message=%s raw=%s\n", result.Code, result.Message, string(b))
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": errMsg})
|
||||
fmt.Printf("[CKBLead] 存客宝返回异常 code=%d msg=%s raw=%s\n", result.Code, ckbMsg, string(b))
|
||||
respObj := gin.H{
|
||||
"success": false,
|
||||
"message": errMsg,
|
||||
"ckbCode": result.Code,
|
||||
"ckbMessage": ckbMsg,
|
||||
}
|
||||
if ckbMsg == "" && len(b) > 0 {
|
||||
respObj["ckbRaw"] = string(b) // 存客保未返回 message/msg 时透传原始响应,供调试
|
||||
}
|
||||
c.JSON(http.StatusOK, respObj)
|
||||
}
|
||||
|
||||
@@ -127,6 +127,20 @@ func ckbOpenCreatePlan(token string, payload map[string]interface{}) (planID int
|
||||
return 0, createData, ckbResponse, fmt.Errorf("创建计划返回结果中缺少 planId")
|
||||
}
|
||||
|
||||
// parseApiKeyFromCreateData 从 create 返回的 data 中解析 apiKey(若存客宝直接返回则复用,避免二次请求)
|
||||
func parseApiKeyFromCreateData(data map[string]interface{}) string {
|
||||
for _, key := range []string{"apiKey", "api_key"} {
|
||||
v, ok := data[key]
|
||||
if !ok || v == nil {
|
||||
continue
|
||||
}
|
||||
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parsePlanIDFromData 从 data 中解析 planId,支持 number 或 string;若无则尝试 id
|
||||
func parsePlanIDFromData(data map[string]interface{}) int64 {
|
||||
for _, key := range []string{"planId", "id"} {
|
||||
@@ -412,14 +426,14 @@ func AdminCKBDevices(c *gin.Context) {
|
||||
status = "online"
|
||||
}
|
||||
devices = append(devices, map[string]interface{}{
|
||||
"id": id,
|
||||
"memo": memo,
|
||||
"imei": m["imei"],
|
||||
"wechatId": wechatID,
|
||||
"status": status,
|
||||
"avatar": m["avatar"],
|
||||
"nickname": m["nickname"],
|
||||
"usedInPlan": m["usedInPlans"],
|
||||
"id": id,
|
||||
"memo": memo,
|
||||
"imei": m["imei"],
|
||||
"wechatId": wechatID,
|
||||
"status": status,
|
||||
"avatar": m["avatar"],
|
||||
"nickname": m["nickname"],
|
||||
"usedInPlan": m["usedInPlans"],
|
||||
"totalFriend": m["totalFriend"],
|
||||
})
|
||||
}
|
||||
@@ -444,4 +458,3 @@ func AdminCKBDevices(c *gin.Context) {
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -453,7 +453,8 @@ func DBBookAction(c *gin.Context) {
|
||||
if item.IsFree != nil {
|
||||
isFree = *item.IsFree
|
||||
}
|
||||
wordCount := len(item.Content)
|
||||
processed, _ := ParseAutoLinkContent(item.Content)
|
||||
wordCount := len(processed)
|
||||
status := "published"
|
||||
editionStandard, editionPremium := true, false
|
||||
ch := model.Chapter{
|
||||
@@ -463,7 +464,7 @@ func DBBookAction(c *gin.Context) {
|
||||
ChapterID: strPtr(item.ChapterID, "chapter-1"),
|
||||
ChapterTitle: strPtr(item.ChapterTitle, "未分类"),
|
||||
SectionTitle: item.Title,
|
||||
Content: item.Content,
|
||||
Content: processed,
|
||||
WordCount: &wordCount,
|
||||
IsFree: &isFree,
|
||||
Price: &price,
|
||||
@@ -604,10 +605,16 @@ func DBBookAction(c *gin.Context) {
|
||||
if body.IsFree != nil {
|
||||
isFree = *body.IsFree
|
||||
}
|
||||
wordCount := len(body.Content)
|
||||
// 后端统一解析 @/# 并转为带 data-id 的 span
|
||||
processedContent, err := ParseAutoLinkContent(body.Content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "解析 @/# 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
wordCount := len(processedContent)
|
||||
updates := map[string]interface{}{
|
||||
"section_title": body.Title,
|
||||
"content": body.Content,
|
||||
"content": processedContent,
|
||||
"word_count": wordCount,
|
||||
"price": price,
|
||||
"is_free": isFree,
|
||||
@@ -641,7 +648,7 @@ func DBBookAction(c *gin.Context) {
|
||||
updates["chapter_title"] = body.ChapterTitle
|
||||
}
|
||||
var existing model.Chapter
|
||||
err := db.Where("id = ?", body.ID).First(&existing).Error
|
||||
err = db.Where("id = ?", body.ID).First(&existing).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 新建:Create
|
||||
partID := body.PartID
|
||||
@@ -674,7 +681,7 @@ func DBBookAction(c *gin.Context) {
|
||||
ChapterID: chapterID,
|
||||
ChapterTitle: chapterTitle,
|
||||
SectionTitle: body.Title,
|
||||
Content: body.Content,
|
||||
Content: processedContent,
|
||||
WordCount: &wordCount,
|
||||
IsFree: &isFree,
|
||||
Price: &price,
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
@@ -37,6 +38,10 @@ func DBLinkTagSave(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "label 必填"})
|
||||
return
|
||||
}
|
||||
if !isValidNameOrLabel(strings.TrimSpace(body.Label)) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "label 只能包含汉字/字母/数字,不能为纯符号"})
|
||||
return
|
||||
}
|
||||
if body.TagID == "" {
|
||||
body.TagID = body.Label
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DBPersonList GET /api/db/persons 管理端-@提及人物列表
|
||||
@@ -64,6 +65,10 @@ func DBPersonSave(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "name 必填"})
|
||||
return
|
||||
}
|
||||
if !isValidNameOrLabel(strings.TrimSpace(body.Name)) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "name 只能包含汉字/字母/数字,不能为纯符号"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
var existing model.Person
|
||||
// 按 name 查找:文章编辑自动创建场景,PersonID 为空时先查是否已存在
|
||||
@@ -124,7 +129,6 @@ func DBPersonSave(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -144,7 +148,6 @@ func DBPersonSave(c *gin.Context) {
|
||||
deviceIDs = append(deviceIDs, id)
|
||||
}
|
||||
}
|
||||
// deviceGroups 必填:未传时默认选择名为 soul 的设备
|
||||
if len(deviceIDs) == 0 {
|
||||
defaultID, err := ckbOpenGetDefaultDeviceID(openToken)
|
||||
if err != nil {
|
||||
@@ -155,8 +158,10 @@ func DBPersonSave(c *gin.Context) {
|
||||
}
|
||||
planPayload := map[string]interface{}{
|
||||
"name": name,
|
||||
"sceneId": 11,
|
||||
"scenario": 11,
|
||||
"planType": 1,
|
||||
"sceneId": 9,
|
||||
"scenario": 9,
|
||||
"status": 1,
|
||||
"remarkType": body.RemarkType,
|
||||
"greeting": body.Greeting,
|
||||
"addInterval": addInterval,
|
||||
@@ -178,11 +183,14 @@ func DBPersonSave(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 用 planId 拉计划详情,获取 apiKey
|
||||
apiKey, err := ckbOpenGetPlanDetail(openToken, planID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建成功但获取计划密钥失败: " + err.Error()})
|
||||
return
|
||||
apiKey := parseApiKeyFromCreateData(ckbCreateData)
|
||||
if apiKey == "" {
|
||||
var getErr error
|
||||
apiKey, getErr = ckbOpenGetPlanDetail(openToken, planID)
|
||||
if getErr != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建成功但获取计划密钥失败: " + getErr.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
newPerson := model.Person{
|
||||
@@ -217,6 +225,68 @@ func DBPersonSave(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// createPersonMinimal 仅按 name 创建 Person(含存客宝计划),供 autolink 复用
|
||||
func createPersonMinimal(db *gorm.DB, name string) (*model.Person, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("name 必填")
|
||||
}
|
||||
personID := fmt.Sprintf("%s_%d", strings.ToLower(strings.ReplaceAll(name, " ", "_")), time.Now().UnixMilli())
|
||||
tok, err := genPersonToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成 token 失败")
|
||||
}
|
||||
openToken, err := ckbOpenGetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
planName := fmt.Sprintf("SOUL链接人与事-%s", name)
|
||||
defaultID, err := ckbOpenGetDefaultDeviceID(openToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取默认设备失败: %w", err)
|
||||
}
|
||||
planPayload := map[string]interface{}{
|
||||
"name": planName,
|
||||
"planType": 1,
|
||||
"sceneId": 9,
|
||||
"scenario": 9,
|
||||
"status": 1,
|
||||
"addInterval": 1,
|
||||
"startTime": "09:00",
|
||||
"endTime": "18:00",
|
||||
"enabled": true,
|
||||
"distributionEnabled": false,
|
||||
"deviceGroups": []int64{defaultID},
|
||||
}
|
||||
planID, createData, _, err := ckbOpenCreatePlan(openToken, planPayload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建存客宝计划失败: %w", err)
|
||||
}
|
||||
apiKey := parseApiKeyFromCreateData(createData)
|
||||
if apiKey == "" {
|
||||
var getErr error
|
||||
apiKey, getErr = ckbOpenGetPlanDetail(openToken, planID)
|
||||
if getErr != nil {
|
||||
return nil, fmt.Errorf("获取计划密钥失败: %w", getErr)
|
||||
}
|
||||
}
|
||||
newPerson := model.Person{
|
||||
PersonID: personID,
|
||||
Token: tok,
|
||||
Name: name,
|
||||
CkbApiKey: apiKey,
|
||||
CkbPlanID: planID,
|
||||
AddFriendInterval: 1,
|
||||
StartTime: "09:00",
|
||||
EndTime: "18:00",
|
||||
DeviceGroups: fmt.Sprintf("%d", defaultID),
|
||||
}
|
||||
if err := db.Create(&newPerson).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &newPerson, nil
|
||||
}
|
||||
|
||||
func genPersonToken() (string, error) {
|
||||
b := make([]byte, 24)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user