feat(admin): 富文本与内容管理页调整;链接标签模型与 db 同步;更新 dist
Made-with: Cursor
This commit is contained in:
@@ -317,9 +317,18 @@ func buildMiniprogramConfig() gin.H {
|
||||
_ = db.Order("label ASC").Find(&linkTagRows).Error
|
||||
tags := make([]gin.H, 0, len(linkTagRows))
|
||||
for _, t := range linkTagRows {
|
||||
h := gin.H{"tagId": t.TagID, "label": t.Label, "url": t.URL, "type": t.Type, "pagePath": t.PagePath}
|
||||
cType := t.Type
|
||||
cURL := t.URL
|
||||
// wxlink(小程序短链):下发给 C 端时转为 url 类型,现有 read.js web-view 可直接跳转,无需升级小程序
|
||||
if strings.EqualFold(cType, "wxlink") {
|
||||
cType = "url"
|
||||
if cURL == "" {
|
||||
cURL = t.AppID
|
||||
}
|
||||
}
|
||||
h := gin.H{"tagId": t.TagID, "label": t.Label, "url": cURL, "type": cType, "pagePath": t.PagePath}
|
||||
if t.Type == "miniprogram" {
|
||||
h["mpKey"] = t.AppID // 可为「关联表 key」或「直接 wx AppID」;后者由 mergeDirectMiniProgramLinksFromLinkTags 补全 linkedMiniprograms
|
||||
h["mpKey"] = t.AppID
|
||||
} else {
|
||||
h["appId"] = t.AppID
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"soul-api/internal/cache"
|
||||
"soul-api/internal/database"
|
||||
@@ -14,30 +15,64 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func isDigits12(s string) bool {
|
||||
if len(s) != 12 {
|
||||
// isValidLinkTagID 自定义 tagId:与库字段 tag_id(50) 对齐,禁止破坏 #标签 解析的字符
|
||||
func isValidLinkTagID(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, ch := range s {
|
||||
if ch < '0' || ch > '9' {
|
||||
if utf8.RuneCountInString(s) > 50 {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if r == '#' || r == ',' || r == '\n' || r == '\r' || r == '\t' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isZId12(s string) bool {
|
||||
if len(s) != 12 || s[0] != 'z' {
|
||||
return false
|
||||
// linkTagAdminOut 管理端 API 出参:不含 appSecret 明文,仅 hasAppSecret
|
||||
type linkTagAdminOut struct {
|
||||
ID uint `json:"id"`
|
||||
TagID string `json:"tagId"`
|
||||
Label string `json:"label"`
|
||||
Aliases string `json:"aliases"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
AppID string `json:"appId,omitempty"`
|
||||
PagePath string `json:"pagePath,omitempty"`
|
||||
HasAppSecret bool `json:"hasAppSecret"`
|
||||
CreatedAt string `json:"createdAt,omitempty"`
|
||||
UpdatedAt string `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
func linkTagToAdminOut(t model.LinkTag) linkTagAdminOut {
|
||||
o := linkTagAdminOut{
|
||||
ID: t.ID,
|
||||
TagID: t.TagID,
|
||||
Label: t.Label,
|
||||
Aliases: t.Aliases,
|
||||
URL: t.URL,
|
||||
Type: t.Type,
|
||||
AppID: t.AppID,
|
||||
PagePath: t.PagePath,
|
||||
HasAppSecret: strings.TrimSpace(t.AppSecret) != "",
|
||||
}
|
||||
for i := 1; i < 12; i++ {
|
||||
ch := s[i]
|
||||
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
if !t.CreatedAt.IsZero() {
|
||||
o.CreatedAt = t.CreatedAt.Format(time.RFC3339)
|
||||
}
|
||||
return true
|
||||
if !t.UpdatedAt.IsZero() {
|
||||
o.UpdatedAt = t.UpdatedAt.Format(time.RFC3339)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func linkTagsToAdminOut(rows []model.LinkTag) []linkTagAdminOut {
|
||||
out := make([]linkTagAdminOut, 0, len(rows))
|
||||
for _, t := range rows {
|
||||
out = append(out, linkTagToAdminOut(t))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func genZId12() string {
|
||||
@@ -63,7 +98,7 @@ func DBLinkTagList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTags": rows})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTags": linkTagsToAdminOut(rows)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,7 +134,7 @@ func DBLinkTagList(c *gin.Context) {
|
||||
totalPages := (int(total) + pageSize - 1) / pageSize
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"linkTags": rows,
|
||||
"linkTags": linkTagsToAdminOut(rows),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pageSize": pageSize,
|
||||
@@ -110,13 +145,14 @@ func DBLinkTagList(c *gin.Context) {
|
||||
// DBLinkTagSave POST /api/db/link-tags 管理端-新增或更新链接标签
|
||||
func DBLinkTagSave(c *gin.Context) {
|
||||
var body struct {
|
||||
TagID string `json:"tagId"`
|
||||
Label string `json:"label"`
|
||||
Aliases string `json:"aliases"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
AppID string `json:"appId"`
|
||||
PagePath string `json:"pagePath"`
|
||||
TagID string `json:"tagId"`
|
||||
Label string `json:"label"`
|
||||
Aliases string `json:"aliases"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
AppID string `json:"appId"`
|
||||
AppSecret string `json:"appSecret"`
|
||||
PagePath string `json:"pagePath"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
@@ -128,6 +164,7 @@ func DBLinkTagSave(c *gin.Context) {
|
||||
body.URL = strings.TrimSpace(body.URL)
|
||||
body.Type = strings.TrimSpace(body.Type)
|
||||
body.AppID = strings.TrimSpace(body.AppID)
|
||||
body.AppSecret = strings.TrimSpace(body.AppSecret)
|
||||
body.PagePath = strings.TrimSpace(body.PagePath)
|
||||
|
||||
if body.Label == "" {
|
||||
@@ -141,24 +178,18 @@ func DBLinkTagSave(c *gin.Context) {
|
||||
if body.Type == "" {
|
||||
body.Type = "url"
|
||||
}
|
||||
// tagId 规则:12位数字,或 12位且以 z 开头(z + 11位[a-z0-9])
|
||||
// 管理端新增:可不传 tagId,由后端生成;编辑:通常会携带现有 tagId
|
||||
// 管理端新增:可不传 tagId,由后端生成 z 开头 12 位;也可自定义任意 1~50 字符(如 kr)
|
||||
if body.TagID == "" {
|
||||
body.TagID = genZId12()
|
||||
autoCreate = true
|
||||
}
|
||||
if !isValidLinkTagID(body.TagID) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "tagId 长度 1~50 字符,且不能含 #、逗号或换行"})
|
||||
return
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
var existing model.LinkTag
|
||||
// 若 tagId 不符合新规则:仅允许更新已有记录(兼容历史中文 tagId),禁止新建
|
||||
if !(isDigits12(body.TagID) || isZId12(body.TagID)) {
|
||||
if err := db.Where("tag_id = ?", body.TagID).First(&existing).Error; err == nil {
|
||||
// allow update existing legacy tagId
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "tagId 必须为12位数字,或12位且以 z 开头(z+11位小写字母数字)"})
|
||||
return
|
||||
}
|
||||
}
|
||||
// 小程序类型:只存 appId + pagePath,不存 weixin:// 到 url
|
||||
if body.Type == "miniprogram" {
|
||||
body.URL = ""
|
||||
@@ -166,7 +197,7 @@ func DBLinkTagSave(c *gin.Context) {
|
||||
// 按 label 查找:仅用于「自动创建」场景(tagId 为空时回落 label),若已存在则直接返回
|
||||
if autoCreate {
|
||||
if db.Where("label = ?", body.Label).First(&existing).Error == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": existing})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": linkTagToAdminOut(existing)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -176,21 +207,24 @@ func DBLinkTagSave(c *gin.Context) {
|
||||
existing.URL = body.URL
|
||||
existing.Type = body.Type
|
||||
existing.AppID = body.AppID
|
||||
if body.AppSecret != "" {
|
||||
existing.AppSecret = body.AppSecret
|
||||
}
|
||||
existing.PagePath = body.PagePath
|
||||
db.Save(&existing)
|
||||
cache.InvalidateConfig()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": existing})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": linkTagToAdminOut(existing)})
|
||||
return
|
||||
}
|
||||
// body.URL 已在 miniprogram 类型时置空
|
||||
t := model.LinkTag{TagID: body.TagID, Label: body.Label, Aliases: body.Aliases, URL: body.URL, Type: body.Type, AppID: body.AppID, PagePath: body.PagePath}
|
||||
t := model.LinkTag{TagID: body.TagID, Label: body.Label, Aliases: body.Aliases, URL: body.URL, Type: body.Type, AppID: body.AppID, AppSecret: body.AppSecret, PagePath: body.PagePath}
|
||||
if err := db.Create(&t).Error; err != nil {
|
||||
// 极低概率:生成的 tagId 冲突,重试一次
|
||||
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||
t.TagID = genZId12()
|
||||
if e2 := db.Create(&t).Error; e2 == nil {
|
||||
cache.InvalidateConfig()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": t})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": linkTagToAdminOut(t)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -198,7 +232,7 @@ func DBLinkTagSave(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
cache.InvalidateConfig()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": t})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": linkTagToAdminOut(t)})
|
||||
}
|
||||
|
||||
// DBLinkTagDelete DELETE /api/db/link-tags?tagId=xxx 管理端-删除链接标签
|
||||
|
||||
@@ -11,6 +11,8 @@ type LinkTag struct {
|
||||
URL string `gorm:"column:url;size:500" json:"url"`
|
||||
Type string `gorm:"column:type;size:20" json:"type"`
|
||||
AppID string `gorm:"column:app_id;size:100" json:"appId,omitempty"`
|
||||
// AppSecret 目标小程序 AppSecret,仅存库;列表/保存响应用 hasAppSecret,永不 json 明文下发
|
||||
AppSecret string `gorm:"column:app_secret;size:256;default:''" json:"-"`
|
||||
PagePath string `gorm:"column:page_path;size:500" json:"pagePath,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
|
||||
Reference in New Issue
Block a user