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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Soul创业派对</title> <title>管理后台 - Soul创业派对</title>
<script type="module" crossorigin src="/assets/index-Dv-LWSbq.js"></script> <script type="module" crossorigin src="/assets/index-Dz0mx7au.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DXojA1Za.css"> <link rel="stylesheet" crossorigin href="/assets/index-qjssBjc3.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -37,9 +37,11 @@ export interface LinkTagItem {
label: string label: string
aliases?: string aliases?: string
url: string url: string
type: 'url' | 'miniprogram' | 'ckb' type: 'url' | 'miniprogram' | 'ckb' | 'wxlink'
appId?: string appId?: string
pagePath?: string pagePath?: string
/** 管理端列表用:库内是否已存目标小程序 AppSecret接口不下发明文 */
hasAppSecret?: boolean
} }
/** 插入附件 HTML 时转义,防 XSS */ /** 插入附件 HTML 时转义,防 XSS */

View File

@@ -260,8 +260,9 @@ export function ContentPage() {
label: '', label: '',
aliases: '', aliases: '',
url: '', url: '',
type: 'url' as 'url' | 'miniprogram' | 'ckb', type: 'url' as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
appId: '', appId: '',
appSecret: '',
pagePath: '', pagePath: '',
}) })
const [linkTagSaving, setLinkTagSaving] = useState(false) const [linkTagSaving, setLinkTagSaving] = useState(false)
@@ -537,7 +538,15 @@ export function ContentPage() {
try { try {
const data = await get<{ const data = await get<{
success?: boolean success?: boolean
linkTags?: { tagId: string; label: string; url: string; type: string; appId?: string; pagePath?: string }[] linkTags?: {
tagId: string
label: string
url: string
type: string
appId?: string
pagePath?: string
hasAppSecret?: boolean
}[]
}>('/api/db/link-tags') }>('/api/db/link-tags')
if (data?.success && data.linkTags) { if (data?.success && data.linkTags) {
setLinkTags( setLinkTags(
@@ -545,9 +554,10 @@ export function ContentPage() {
id: t.tagId, id: t.tagId,
label: t.label, label: t.label,
url: t.url, url: t.url,
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb', type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
appId: t.appId || '', appId: t.appId || '',
pagePath: t.pagePath || '', pagePath: t.pagePath || '',
hasAppSecret: !!t.hasAppSecret,
})), })),
) )
} }
@@ -608,7 +618,16 @@ export function ContentPage() {
if (s) qs.set('search', s) if (s) qs.set('search', s)
const data = await get<{ const data = await get<{
success?: boolean success?: boolean
linkTags?: { tagId: string; label: string; aliases?: string; url: string; type: string; appId?: string; pagePath?: string }[] linkTags?: {
tagId: string
label: string
aliases?: string
url: string
type: string
appId?: string
pagePath?: string
hasAppSecret?: boolean
}[]
total?: number total?: number
page?: number page?: number
pageSize?: number pageSize?: number
@@ -622,9 +641,10 @@ export function ContentPage() {
label: t.label, label: t.label,
aliases: t.aliases || '', aliases: t.aliases || '',
url: t.url, url: t.url,
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb', type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
appId: t.appId || '', appId: t.appId || '',
pagePath: t.pagePath || '', pagePath: t.pagePath || '',
hasAppSecret: !!t.hasAppSecret,
})), })),
) )
setLinkTagTotal(typeof data.total === 'number' ? data.total : 0) setLinkTagTotal(typeof data.total === 'number' ? data.total : 0)
@@ -2810,7 +2830,7 @@ export function ContentPage() {
className="bg-amber-500 hover:bg-amber-600 text-white h-8" className="bg-amber-500 hover:bg-amber-600 text-white h-8"
onClick={() => { onClick={() => {
setLinkTagEditing(null) setLinkTagEditing(null)
setLinkTagForm({ tagId: '', label: '', aliases: '', url: '', type: 'url', appId: '', pagePath: '' }) setLinkTagForm({ tagId: '', label: '', aliases: '', url: '', type: 'url', appId: '', appSecret: '', pagePath: '' })
setMpSearchQuery('') setMpSearchQuery('')
setMpDropdownOpen(false) setMpDropdownOpen(false)
setLinkTagModalOpen(true) setLinkTagModalOpen(true)
@@ -2862,6 +2882,7 @@ export function ContentPage() {
url: t.url, url: t.url,
type: t.type, type: t.type,
appId: t.appId ?? '', appId: t.appId ?? '',
appSecret: '',
pagePath: t.pagePath ?? '', pagePath: t.pagePath ?? '',
}) })
setMpSearchQuery(t.appId ?? '') setMpSearchQuery(t.appId ?? '')
@@ -2882,12 +2903,12 @@ export function ContentPage() {
className={`text-[10px] ${ className={`text-[10px] ${
t.type === 'ckb' t.type === 'ckb'
? 'bg-green-500/20 text-green-300 border-green-500/30' ? 'bg-green-500/20 text-green-300 border-green-500/30'
: t.type === 'miniprogram' : t.type === 'miniprogram' || t.type === 'wxlink'
? 'bg-[#38bdac]/20 text-[#38bdac] border-[#38bdac]/30' ? 'bg-[#38bdac]/20 text-[#38bdac] border-[#38bdac]/30'
: 'bg-gray-700 text-gray-300' : 'bg-gray-700 text-gray-300'
}`} }`}
> >
{t.type === 'url' ? '网页' : t.type === 'ckb' ? '存客宝' : '小程序'} {t.type === 'url' ? '网页' : t.type === 'ckb' ? '存客宝' : t.type === 'wxlink' ? '小程序链接' : '小程序'}
</Badge> </Badge>
</td> </td>
<td className="px-3 py-2 text-gray-300"> <td className="px-3 py-2 text-gray-300">
@@ -2903,6 +2924,18 @@ export function ContentPage() {
) )
})()} })()}
{t.pagePath && <div className="text-xs text-gray-500 font-mono">{t.pagePath}</div>} {t.pagePath && <div className="text-xs text-gray-500 font-mono">{t.pagePath}</div>}
<div
className={`text-xs ${t.hasAppSecret ? 'text-emerald-400/90' : 'text-amber-500/80'}`}
>
AppSecret{t.hasAppSecret ? '已保存(仅服务端)' : '未配置'}
</div>
</div>
) : t.type === 'wxlink' ? (
<div className="space-y-0.5">
<div className="text-xs text-[#38bdac] truncate max-w-[420px] font-mono" title={t.url}>
{t.url || '—'}
</div>
<div className="text-[11px] text-gray-500"> web-view </div>
</div> </div>
) : t.url ? ( ) : t.url ? (
<a <a
@@ -2932,6 +2965,7 @@ export function ContentPage() {
url: t.url, url: t.url,
type: t.type, type: t.type,
appId: t.appId ?? '', appId: t.appId ?? '',
appSecret: '',
pagePath: t.pagePath ?? '', pagePath: t.pagePath ?? '',
}) })
setMpSearchQuery(t.appId ?? '') setMpSearchQuery(t.appId ?? '')
@@ -2996,7 +3030,7 @@ export function ContentPage() {
<DialogHeader className="gap-1"> <DialogHeader className="gap-1">
<DialogTitle className="text-base">{linkTagEditing ? '编辑链接标签' : '添加链接标签'}</DialogTitle> <DialogTitle className="text-base">{linkTagEditing ? '编辑链接标签' : '添加链接标签'}</DialogTitle>
<DialogDescription className="text-gray-400 text-xs"> <DialogDescription className="text-gray-400 text-xs">
# # mpKey AppIDAppSecret 使
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -3006,7 +3040,7 @@ export function ContentPage() {
<Label className="text-gray-300 text-sm">ID</Label> <Label className="text-gray-300 text-sm">ID</Label>
<Input <Input
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono" className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
placeholder="留空自动生成;或填 12位数字 / z开头12位" placeholder="留空自动生成;或自定义短 ID如 kr最长 50 字符"
value={linkTagForm.tagId} value={linkTagForm.tagId}
disabled={!!linkTagEditing} disabled={!!linkTagEditing}
onChange={(e) => setLinkTagForm((p) => ({ ...p, tagId: e.target.value }))} onChange={(e) => setLinkTagForm((p) => ({ ...p, tagId: e.target.value }))}
@@ -3038,7 +3072,7 @@ export function ContentPage() {
<Select <Select
value={linkTagForm.type} value={linkTagForm.type}
onValueChange={(v) => onValueChange={(v) =>
setLinkTagForm((p) => ({ ...p, type: v as 'url' | 'miniprogram' | 'ckb' })) setLinkTagForm((p) => ({ ...p, type: v as 'url' | 'miniprogram' | 'ckb' | 'wxlink' }))
} }
> >
<SelectTrigger className="bg-[#0a1628] border-gray-700 text-white h-8"> <SelectTrigger className="bg-[#0a1628] border-gray-700 text-white h-8">
@@ -3046,7 +3080,8 @@ export function ContentPage() {
</SelectTrigger> </SelectTrigger>
<SelectContent className="bg-[#0f2137] border-gray-700 text-white"> <SelectContent className="bg-[#0f2137] border-gray-700 text-white">
<SelectItem value="url"></SelectItem> <SelectItem value="url"></SelectItem>
<SelectItem value="miniprogram"></SelectItem> <SelectItem value="miniprogram">API跳转</SelectItem>
<SelectItem value="wxlink"></SelectItem>
<SelectItem value="ckb"></SelectItem> <SelectItem value="ckb"></SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -3057,9 +3092,18 @@ export function ContentPage() {
? 'URL地址' ? 'URL地址'
: linkTagForm.type === 'ckb' : linkTagForm.type === 'ckb'
? '存客宝计划URL' ? '存客宝计划URL'
: '小程序(选密钥)'} : linkTagForm.type === 'wxlink'
? '小程序链接'
: '小程序 mpKey / 微信 AppID'}
</Label> </Label>
{linkTagForm.type === 'miniprogram' && linkedMps.length > 0 ? ( {linkTagForm.type === 'wxlink' ? (
<Input
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm"
placeholder="粘贴小程序右上角 ... → 复制链接 得到的 URL"
value={linkTagForm.url}
onChange={(e) => setLinkTagForm((p) => ({ ...p, url: e.target.value }))}
/>
) : linkTagForm.type === 'miniprogram' && linkedMps.length > 0 ? (
<div ref={mpDropdownRef} className="relative"> <div ref={mpDropdownRef} className="relative">
<Input <Input
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm" className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm"
@@ -3110,7 +3154,7 @@ export function ContentPage() {
? 'https://...' ? 'https://...'
: linkTagForm.type === 'ckb' : linkTagForm.type === 'ckb'
? 'https://ckbapi.quwanzhi.com/...' ? 'https://ckbapi.quwanzhi.com/...'
: '关联小程序的32位密钥' : '关联配置的 key或直接填 wx 开头的 AppID'
} }
value={linkTagForm.type === 'url' || linkTagForm.type === 'ckb' ? linkTagForm.url : linkTagForm.appId} value={linkTagForm.type === 'url' || linkTagForm.type === 'ckb' ? linkTagForm.url : linkTagForm.appId}
onChange={(e) => { onChange={(e) => {
@@ -3123,16 +3167,38 @@ export function ContentPage() {
</div> </div>
</div> </div>
{linkTagForm.type === 'wxlink' && (
<p className="text-[11px] text-amber-400/80 leading-snug px-0.5">
... web-view
</p>
)}
{linkTagForm.type === 'miniprogram' && ( {linkTagForm.type === 'miniprogram' && (
<div className="space-y-1"> <>
<Label className="text-gray-300 text-sm"></Label> <div className="space-y-1">
<Input <Label className="text-gray-300 text-sm"></Label>
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono" <Input
placeholder="pages/index/index" className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
value={linkTagForm.pagePath} placeholder="pages/index/index"
onChange={(e) => setLinkTagForm((p) => ({ ...p, pagePath: e.target.value }))} value={linkTagForm.pagePath}
/> onChange={(e) => setLinkTagForm((p) => ({ ...p, pagePath: e.target.value }))}
</div> />
</div>
<div className="space-y-1">
<Label className="text-gray-300 text-sm">AppSecret · </Label>
<Input
type="password"
autoComplete="new-password"
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
placeholder={linkTagEditing?.hasAppSecret ? '已保存密钥,留空不改;填写则覆盖' : '粘贴目标小程序 AppSecret'}
value={linkTagForm.appSecret}
onChange={(e) => setLinkTagForm((p) => ({ ...p, appSecret: e.target.value }))}
/>
<p className="text-[11px] text-gray-500 leading-snug">
AppID
</p>
</div>
</>
)} )}
</div> </div>
@@ -3149,13 +3215,18 @@ export function ContentPage() {
url: linkTagForm.url.trim(), url: linkTagForm.url.trim(),
type: linkTagForm.type, type: linkTagForm.type,
appId: linkTagForm.appId.trim(), appId: linkTagForm.appId.trim(),
appSecret: linkTagForm.appSecret.trim(),
pagePath: linkTagForm.pagePath.trim(), pagePath: linkTagForm.pagePath.trim(),
} }
// 新增:允许留空后端自动生成;编辑tagId 已锁定 // 留空后端自动生成;自定义时与库一致150 字符,勿含 #、逗号、换行
if (payload.tagId) { if (payload.tagId) {
const ok = /^\d{12}$/.test(payload.tagId) || /^z[a-z0-9]{11}$/.test(payload.tagId) const id = payload.tagId
if (!ok) { if ([...id].length > 50) {
toast.error('标签ID需为12位数字或 z 开头的12位z+11位小写字母数字') toast.error('标签ID 最长 50 个字符')
return
}
if (/[#,\n\r\t]/.test(id)) {
toast.error('标签ID 不能含 #、逗号或换行')
return return
} }
} }
@@ -3164,6 +3235,7 @@ export function ContentPage() {
return return
} }
if (payload.type === 'miniprogram') payload.url = '' if (payload.type === 'miniprogram') payload.url = ''
if (payload.type === 'wxlink') { payload.appId = ''; payload.pagePath = '' }
setLinkTagSaving(true) setLinkTagSaving(true)
try { try {
const res = await post<{ success?: boolean; error?: string }>('/api/db/link-tags', payload) const res = await post<{ success?: boolean; error?: string }>('/api/db/link-tags', payload)

View File

@@ -317,9 +317,18 @@ func buildMiniprogramConfig() gin.H {
_ = db.Order("label ASC").Find(&linkTagRows).Error _ = db.Order("label ASC").Find(&linkTagRows).Error
tags := make([]gin.H, 0, len(linkTagRows)) tags := make([]gin.H, 0, len(linkTagRows))
for _, t := range 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" { if t.Type == "miniprogram" {
h["mpKey"] = t.AppID // 可为「关联表 key」或「直接 wx AppID」后者由 mergeDirectMiniProgramLinksFromLinkTags 补全 linkedMiniprograms h["mpKey"] = t.AppID
} else { } else {
h["appId"] = t.AppID h["appId"] = t.AppID
} }

View File

@@ -6,6 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8"
"soul-api/internal/cache" "soul-api/internal/cache"
"soul-api/internal/database" "soul-api/internal/database"
@@ -14,30 +15,64 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func isDigits12(s string) bool { // isValidLinkTagID 自定义 tagId与库字段 tag_id(50) 对齐,禁止破坏 #标签 解析的字符
if len(s) != 12 { func isValidLinkTagID(s string) bool {
if s == "" {
return false return false
} }
for _, ch := range s { if utf8.RuneCountInString(s) > 50 {
if ch < '0' || ch > '9' { return false
}
for _, r := range s {
if r == '#' || r == ',' || r == '\n' || r == '\r' || r == '\t' {
return false return false
} }
} }
return true return true
} }
func isZId12(s string) bool { // linkTagAdminOut 管理端 API 出参:不含 appSecret 明文,仅 hasAppSecret
if len(s) != 12 || s[0] != 'z' { type linkTagAdminOut struct {
return false 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++ { if !t.CreatedAt.IsZero() {
ch := s[i] o.CreatedAt = t.CreatedAt.Format(time.RFC3339)
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') {
continue
}
return false
} }
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 { func genZId12() string {
@@ -63,7 +98,7 @@ func DBLinkTagList(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return return
} }
c.JSON(http.StatusOK, gin.H{"success": true, "linkTags": rows}) c.JSON(http.StatusOK, gin.H{"success": true, "linkTags": linkTagsToAdminOut(rows)})
return return
} }
@@ -99,7 +134,7 @@ func DBLinkTagList(c *gin.Context) {
totalPages := (int(total) + pageSize - 1) / pageSize totalPages := (int(total) + pageSize - 1) / pageSize
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"linkTags": rows, "linkTags": linkTagsToAdminOut(rows),
"total": total, "total": total,
"page": page, "page": page,
"pageSize": pageSize, "pageSize": pageSize,
@@ -110,13 +145,14 @@ func DBLinkTagList(c *gin.Context) {
// DBLinkTagSave POST /api/db/link-tags 管理端-新增或更新链接标签 // DBLinkTagSave POST /api/db/link-tags 管理端-新增或更新链接标签
func DBLinkTagSave(c *gin.Context) { func DBLinkTagSave(c *gin.Context) {
var body struct { var body struct {
TagID string `json:"tagId"` TagID string `json:"tagId"`
Label string `json:"label"` Label string `json:"label"`
Aliases string `json:"aliases"` Aliases string `json:"aliases"`
URL string `json:"url"` URL string `json:"url"`
Type string `json:"type"` Type string `json:"type"`
AppID string `json:"appId"` AppID string `json:"appId"`
PagePath string `json:"pagePath"` AppSecret string `json:"appSecret"`
PagePath string `json:"pagePath"`
} }
if err := c.ShouldBindJSON(&body); err != nil { if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"}) 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.URL = strings.TrimSpace(body.URL)
body.Type = strings.TrimSpace(body.Type) body.Type = strings.TrimSpace(body.Type)
body.AppID = strings.TrimSpace(body.AppID) body.AppID = strings.TrimSpace(body.AppID)
body.AppSecret = strings.TrimSpace(body.AppSecret)
body.PagePath = strings.TrimSpace(body.PagePath) body.PagePath = strings.TrimSpace(body.PagePath)
if body.Label == "" { if body.Label == "" {
@@ -141,24 +178,18 @@ func DBLinkTagSave(c *gin.Context) {
if body.Type == "" { if body.Type == "" {
body.Type = "url" body.Type = "url"
} }
// tagId 规则12位数字或 12位且以 z 开头z + 11位[a-z0-9] // 管理端新增:可不传 tagId由后端生成 z 开头 12 位;也可自定义任意 150 字符(如 kr
// 管理端新增:可不传 tagId由后端生成编辑通常会携带现有 tagId
if body.TagID == "" { if body.TagID == "" {
body.TagID = genZId12() body.TagID = genZId12()
autoCreate = true autoCreate = true
} }
if !isValidLinkTagID(body.TagID) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "tagId 长度 150 字符,且不能含 #、逗号或换行"})
return
}
db := database.DB() db := database.DB()
var existing model.LinkTag 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 // 小程序类型:只存 appId + pagePath不存 weixin:// 到 url
if body.Type == "miniprogram" { if body.Type == "miniprogram" {
body.URL = "" body.URL = ""
@@ -166,7 +197,7 @@ func DBLinkTagSave(c *gin.Context) {
// 按 label 查找仅用于「自动创建」场景tagId 为空时回落 label若已存在则直接返回 // 按 label 查找仅用于「自动创建」场景tagId 为空时回落 label若已存在则直接返回
if autoCreate { if autoCreate {
if db.Where("label = ?", body.Label).First(&existing).Error == nil { 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 return
} }
} }
@@ -176,21 +207,24 @@ func DBLinkTagSave(c *gin.Context) {
existing.URL = body.URL existing.URL = body.URL
existing.Type = body.Type existing.Type = body.Type
existing.AppID = body.AppID existing.AppID = body.AppID
if body.AppSecret != "" {
existing.AppSecret = body.AppSecret
}
existing.PagePath = body.PagePath existing.PagePath = body.PagePath
db.Save(&existing) db.Save(&existing)
cache.InvalidateConfig() cache.InvalidateConfig()
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": existing}) c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": linkTagToAdminOut(existing)})
return return
} }
// body.URL 已在 miniprogram 类型时置空 // 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 { if err := db.Create(&t).Error; err != nil {
// 极低概率:生成的 tagId 冲突,重试一次 // 极低概率:生成的 tagId 冲突,重试一次
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") { if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
t.TagID = genZId12() t.TagID = genZId12()
if e2 := db.Create(&t).Error; e2 == nil { if e2 := db.Create(&t).Error; e2 == nil {
cache.InvalidateConfig() cache.InvalidateConfig()
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": t}) c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": linkTagToAdminOut(t)})
return return
} }
} }
@@ -198,7 +232,7 @@ func DBLinkTagSave(c *gin.Context) {
return return
} }
cache.InvalidateConfig() 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 管理端-删除链接标签 // 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"` URL string `gorm:"column:url;size:500" json:"url"`
Type string `gorm:"column:type;size:20" json:"type"` Type string `gorm:"column:type;size:20" json:"type"`
AppID string `gorm:"column:app_id;size:100" json:"appId,omitempty"` 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"` PagePath string `gorm:"column:page_path;size:500" json:"pagePath,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"` CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`