新增置顶人功能,允许在小程序首页展示特定用户信息(昵称、头像)。更新相关 API 接口以支持获取和设置置顶人,优化用户体验。调整小程序页面以动态显示置顶人信息,确保一致性和可用性。

This commit is contained in:
Alex-larget
2026-03-20 15:58:18 +08:00
parent f7e7552eb1
commit 1b87fa92f7
12 changed files with 389 additions and 30 deletions

View File

@@ -77,6 +77,24 @@ func ckbSign(params map[string]interface{}, apiKey string) string {
return hex.EncodeToString(h2[:])
}
const pinnedPersonTokenKey = "pinned_person_token"
// getPinnedPersonToken 读取置顶人物 tokensystem_config
func getPinnedPersonToken() string {
var row model.SystemConfig
if err := database.DB().Where("config_key = ?", pinnedPersonTokenKey).First(&row).Error; err != nil || len(row.ConfigValue) == 0 {
return ""
}
var m map[string]interface{}
if err := json.Unmarshal(row.ConfigValue, &m); err != nil {
return ""
}
if v, ok := m["token"].(string); ok && strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
return ""
}
// getCkbLeadApiKey 链接卡若密钥优先级system_config.site_settings.ckbLeadApiKey > .env CKB_LEAD_API_KEY > 代码内置 ckbAPIKey
func getCkbLeadApiKey() string {
var row model.SystemConfig
@@ -307,8 +325,49 @@ func CKBSync(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
}
// CKBPinnedPerson GET /api/miniprogram/ckb/pinned-person 小程序首页获取置顶人信息(昵称、头像)
// 有 user_id 时从 users 表取;无则用 Person.name、Person.avatar
func CKBPinnedPerson(c *gin.Context) {
token := getPinnedPersonToken()
if token == "" {
c.JSON(http.StatusOK, gin.H{"success": true, "data": nil})
return
}
db := database.DB()
var p model.Person
if err := db.Where("token = ?", token).First(&p).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": nil})
return
}
nickname := strings.TrimSpace(p.Name)
avatar := strings.TrimSpace(p.Avatar)
if p.UserID != nil && strings.TrimSpace(*p.UserID) != "" {
var u model.User
if db.Select("nickname", "avatar").Where("id = ?", *p.UserID).First(&u).Error == nil {
if u.Nickname != nil && strings.TrimSpace(*u.Nickname) != "" {
nickname = strings.TrimSpace(*u.Nickname)
}
if u.Avatar != nil && strings.TrimSpace(*u.Avatar) != "" {
avatar = strings.TrimSpace(*u.Avatar)
}
}
}
if nickname == "" {
nickname = "链接人"
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"nickname": nickname,
"avatar": avatar,
"token": token,
},
})
}
// CKBIndexLead POST /api/miniprogram/ckb/index-lead 小程序首页「点击链接卡若」专用留资接口
// - 固定使用全局 CKB_LEAD_API_KEY不受文章 @ 人物的 ckb_api_key 影响
// - 有置顶人时:使用置顶人的 ckb_api_key 推送到其存客宝计划(与文章 @ 获客逻辑一致)
// - 无置顶人时fallback 全局 CKB_LEAD_API_KEY
// - 请求体userId可选用于补全昵称、phone/wechatId至少一个、name可选
func CKBIndexLead(c *gin.Context) {
var body struct {
@@ -337,8 +396,17 @@ func CKBIndexLead(c *gin.Context) {
name = "小程序用户"
}
// 首页固定使用全局密钥system_config > .env > 代码内置
// 优先使用置顶人的 ckb_api_key无则用全局密钥
leadKey := getCkbLeadApiKey()
targetPersonID := ""
pinnedToken := getPinnedPersonToken()
if pinnedToken != "" {
var p model.Person
if db.Where("token = ?", pinnedToken).First(&p).Error == nil && strings.TrimSpace(p.CkbApiKey) != "" {
leadKey = p.CkbApiKey
targetPersonID = p.PersonID
}
}
// 去重限频2 分钟内同一用户/手机/微信只能提交一次
var cond []string
@@ -362,16 +430,17 @@ func CKBIndexLead(c *gin.Context) {
source := "index_link_button"
paramsJSON, _ := json.Marshal(map[string]interface{}{
"userId": body.UserID, "phone": phone, "wechatId": wechatId, "name": body.Name,
"source": source,
"source": source, "pinnedToken": pinnedToken,
})
_ = db.Create(&model.CkbLeadRecord{
UserID: body.UserID,
Nickname: name,
Phone: phone,
WechatID: wechatId,
Name: strings.TrimSpace(body.Name),
Source: source,
Params: string(paramsJSON),
UserID: body.UserID,
Nickname: name,
Phone: phone,
WechatID: wechatId,
Name: strings.TrimSpace(body.Name),
TargetPersonID: targetPersonID,
Source: source,
Params: string(paramsJSON),
}).Error
ts := time.Now().Unix()
@@ -405,9 +474,17 @@ func CKBIndexLead(c *gin.Context) {
}
_ = json.Unmarshal(b, &result)
if result.Code == 200 {
msg := "提交成功,卡若会尽快联系您"
msg := "提交成功,我们会尽快联系您"
if pinnedToken != "" {
var p model.Person
if db.Where("token = ?", pinnedToken).First(&p).Error == nil && p.Name != "" {
msg = "提交成功," + p.Name + "会尽快联系您"
}
} else {
msg = "提交成功,卡若会尽快联系您"
}
if repeatedSubmit {
msg = "您已留资过,我们已再次通知卡若,请耐心等待添加"
msg = "您已留资过,我们已再次通知,请耐心等待添加"
}
data := gin.H{}
if result.Data != nil {

View File

@@ -3,6 +3,7 @@ package handler
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -404,8 +405,65 @@ func genPersonToken() (string, error) {
return s + "0123456789abcdefghijklmnopqrstuv"[:(32-len(s))], nil
}
// DBPersonPin PUT /api/db/persons/pin 管理端-设置置顶人(唯一,新置顶会覆盖旧的)
func DBPersonPin(c *gin.Context) {
var body struct {
Token string `json:"token" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 token"})
return
}
token := strings.TrimSpace(body.Token)
if token == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "token 不能为空"})
return
}
db := database.DB()
var p model.Person
if err := db.Where("token = ?", token).First(&p).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "该人物不存在"})
return
}
valBytes, _ := json.Marshal(map[string]string{"token": token})
desc := "小程序首页置顶人物 token"
var row model.SystemConfig
var err error
if db.Where("config_key = ?", pinnedPersonTokenKey).First(&row).Error != nil {
row = model.SystemConfig{ConfigKey: pinnedPersonTokenKey, ConfigValue: model.ConfigValue(valBytes), Description: &desc}
err = db.Create(&row).Error
} else {
row.ConfigValue = model.ConfigValue(valBytes)
err = db.Save(&row).Error
}
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "已置顶 " + p.Name})
}
// DBPersonPinnedToken GET /api/db/persons/pinned-token 管理端-获取当前置顶人 token
func DBPersonPinnedToken(c *gin.Context) {
var row model.SystemConfig
if err := database.DB().Where("config_key = ?", pinnedPersonTokenKey).First(&row).Error; err != nil || len(row.ConfigValue) == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "token": ""})
return
}
var m map[string]interface{}
if err := json.Unmarshal(row.ConfigValue, &m); err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "token": ""})
return
}
token := ""
if v, ok := m["token"].(string); ok {
token = strings.TrimSpace(v)
}
c.JSON(http.StatusOK, gin.H{"success": true, "token": token})
}
// DBPersonDelete DELETE /api/db/persons?personId=xxx 管理端-删除人物
// 若有 ckb_plan_id先调存客宝删除计划再删本地
// 若有 ckb_plan_id先调存客宝删除计划再删本地;若为当前置顶则清空置顶
func DBPersonDelete(c *gin.Context) {
pid := c.Query("personId")
if pid == "" {
@@ -417,6 +475,16 @@ func DBPersonDelete(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "人物不存在"})
return
}
// 若为当前置顶,先清空置顶配置
var cfg model.SystemConfig
if database.DB().Where("config_key = ?", pinnedPersonTokenKey).First(&cfg).Error == nil && len(cfg.ConfigValue) > 0 {
var m map[string]interface{}
if json.Unmarshal(cfg.ConfigValue, &m) == nil {
if v, ok := m["token"].(string); ok && strings.TrimSpace(v) == row.Token {
_ = database.DB().Where("config_key = ?", pinnedPersonTokenKey).Delete(&model.SystemConfig{}).Error
}
}
}
// 若有存客宝计划,先调 CKB 删除
if row.CkbPlanID > 0 {
token, err := ckbOpenGetToken()