feat(admin): 富文本与内容管理页调整;链接标签模型与 db 同步;更新 dist

Made-with: Cursor
This commit is contained in:
卡若
2026-03-24 12:29:46 +08:00
parent 70b83fdb25
commit f069b71374
8 changed files with 272 additions and 153 deletions

View File

@@ -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
}

View File

@@ -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 位;也可自定义任意 150 字符(如 kr
if body.TagID == "" {
body.TagID = genZId12()
autoCreate = true
}
if !isValidLinkTagID(body.TagID) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "tagId 长度 150 字符,且不能含 #、逗号或换行"})
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 管理端-删除链接标签

View File

@@ -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"`