feat(admin): 富文本与内容管理页调整;链接标签模型与 db 同步;更新 dist
Made-with: Cursor
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
soul-admin/dist/index.html
vendored
4
soul-admin/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Soul创业派对</title>
|
||||
<script type="module" crossorigin src="/assets/index-Dv-LWSbq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DXojA1Za.css">
|
||||
<script type="module" crossorigin src="/assets/index-Dz0mx7au.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-qjssBjc3.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -37,9 +37,11 @@ export interface LinkTagItem {
|
||||
label: string
|
||||
aliases?: string
|
||||
url: string
|
||||
type: 'url' | 'miniprogram' | 'ckb'
|
||||
type: 'url' | 'miniprogram' | 'ckb' | 'wxlink'
|
||||
appId?: string
|
||||
pagePath?: string
|
||||
/** 管理端列表用:库内是否已存目标小程序 AppSecret(接口不下发明文) */
|
||||
hasAppSecret?: boolean
|
||||
}
|
||||
|
||||
/** 插入附件 HTML 时转义,防 XSS */
|
||||
|
||||
@@ -260,8 +260,9 @@ export function ContentPage() {
|
||||
label: '',
|
||||
aliases: '',
|
||||
url: '',
|
||||
type: 'url' as 'url' | 'miniprogram' | 'ckb',
|
||||
type: 'url' as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
|
||||
appId: '',
|
||||
appSecret: '',
|
||||
pagePath: '',
|
||||
})
|
||||
const [linkTagSaving, setLinkTagSaving] = useState(false)
|
||||
@@ -537,7 +538,15 @@ export function ContentPage() {
|
||||
try {
|
||||
const data = await get<{
|
||||
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')
|
||||
if (data?.success && data.linkTags) {
|
||||
setLinkTags(
|
||||
@@ -545,9 +554,10 @@ export function ContentPage() {
|
||||
id: t.tagId,
|
||||
label: t.label,
|
||||
url: t.url,
|
||||
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb',
|
||||
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
|
||||
appId: t.appId || '',
|
||||
pagePath: t.pagePath || '',
|
||||
hasAppSecret: !!t.hasAppSecret,
|
||||
})),
|
||||
)
|
||||
}
|
||||
@@ -608,7 +618,16 @@ export function ContentPage() {
|
||||
if (s) qs.set('search', s)
|
||||
const data = await get<{
|
||||
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
|
||||
page?: number
|
||||
pageSize?: number
|
||||
@@ -622,9 +641,10 @@ export function ContentPage() {
|
||||
label: t.label,
|
||||
aliases: t.aliases || '',
|
||||
url: t.url,
|
||||
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb',
|
||||
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
|
||||
appId: t.appId || '',
|
||||
pagePath: t.pagePath || '',
|
||||
hasAppSecret: !!t.hasAppSecret,
|
||||
})),
|
||||
)
|
||||
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"
|
||||
onClick={() => {
|
||||
setLinkTagEditing(null)
|
||||
setLinkTagForm({ tagId: '', label: '', aliases: '', url: '', type: 'url', appId: '', pagePath: '' })
|
||||
setLinkTagForm({ tagId: '', label: '', aliases: '', url: '', type: 'url', appId: '', appSecret: '', pagePath: '' })
|
||||
setMpSearchQuery('')
|
||||
setMpDropdownOpen(false)
|
||||
setLinkTagModalOpen(true)
|
||||
@@ -2862,6 +2882,7 @@ export function ContentPage() {
|
||||
url: t.url,
|
||||
type: t.type,
|
||||
appId: t.appId ?? '',
|
||||
appSecret: '',
|
||||
pagePath: t.pagePath ?? '',
|
||||
})
|
||||
setMpSearchQuery(t.appId ?? '')
|
||||
@@ -2882,12 +2903,12 @@ export function ContentPage() {
|
||||
className={`text-[10px] ${
|
||||
t.type === 'ckb'
|
||||
? '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-gray-700 text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{t.type === 'url' ? '网页' : t.type === 'ckb' ? '存客宝' : '小程序'}
|
||||
{t.type === 'url' ? '网页' : t.type === 'ckb' ? '存客宝' : t.type === 'wxlink' ? '小程序链接' : '小程序'}
|
||||
</Badge>
|
||||
</td>
|
||||
<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>}
|
||||
<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>
|
||||
) : t.url ? (
|
||||
<a
|
||||
@@ -2932,6 +2965,7 @@ export function ContentPage() {
|
||||
url: t.url,
|
||||
type: t.type,
|
||||
appId: t.appId ?? '',
|
||||
appSecret: '',
|
||||
pagePath: t.pagePath ?? '',
|
||||
})
|
||||
setMpSearchQuery(t.appId ?? '')
|
||||
@@ -2996,7 +3030,7 @@ export function ContentPage() {
|
||||
<DialogHeader className="gap-1">
|
||||
<DialogTitle className="text-base">{linkTagEditing ? '编辑链接标签' : '添加链接标签'}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400 text-xs">
|
||||
配置后可在富文本编辑器中通过 #标签 插入,并在小程序端点击跳转
|
||||
配置后可在富文本编辑器中通过 #标签 插入,并在小程序端点击跳转。小程序类型需填 mpKey 或微信 AppID;AppSecret 仅存服务端(不下发小程序),供后续开放接口与台账使用。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -3006,7 +3040,7 @@ export function ContentPage() {
|
||||
<Label className="text-gray-300 text-sm">标签ID</Label>
|
||||
<Input
|
||||
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}
|
||||
disabled={!!linkTagEditing}
|
||||
onChange={(e) => setLinkTagForm((p) => ({ ...p, tagId: e.target.value }))}
|
||||
@@ -3038,7 +3072,7 @@ export function ContentPage() {
|
||||
<Select
|
||||
value={linkTagForm.type}
|
||||
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">
|
||||
@@ -3046,7 +3080,8 @@ export function ContentPage() {
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0f2137] border-gray-700 text-white">
|
||||
<SelectItem value="url">网页链接</SelectItem>
|
||||
<SelectItem value="miniprogram">小程序</SelectItem>
|
||||
<SelectItem value="miniprogram">小程序(API跳转)</SelectItem>
|
||||
<SelectItem value="wxlink">小程序链接(右上角复制)</SelectItem>
|
||||
<SelectItem value="ckb">存客宝</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -3057,9 +3092,18 @@ export function ContentPage() {
|
||||
? 'URL地址'
|
||||
: linkTagForm.type === 'ckb'
|
||||
? '存客宝计划URL'
|
||||
: '小程序(选密钥)'}
|
||||
: linkTagForm.type === 'wxlink'
|
||||
? '小程序链接'
|
||||
: '小程序 mpKey / 微信 AppID'}
|
||||
</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">
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm"
|
||||
@@ -3110,7 +3154,7 @@ export function ContentPage() {
|
||||
? 'https://...'
|
||||
: linkTagForm.type === 'ckb'
|
||||
? 'https://ckbapi.quwanzhi.com/...'
|
||||
: '关联小程序的32位密钥'
|
||||
: '关联配置的 key,或直接填 wx 开头的 AppID'
|
||||
}
|
||||
value={linkTagForm.type === 'url' || linkTagForm.type === 'ckb' ? linkTagForm.url : linkTagForm.appId}
|
||||
onChange={(e) => {
|
||||
@@ -3123,16 +3167,38 @@ export function ContentPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{linkTagForm.type === 'wxlink' && (
|
||||
<p className="text-[11px] text-amber-400/80 leading-snug px-0.5">
|
||||
操作:打开目标小程序 → 右上角「...」→「复制链接」→ 粘贴到上面。小程序内点击此标签会在 web-view 中打开,微信自动唤起目标小程序,无需修改小程序版本。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{linkTagForm.type === 'miniprogram' && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-300 text-sm">页面路径(可选)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
|
||||
placeholder="pages/index/index"
|
||||
value={linkTagForm.pagePath}
|
||||
onChange={(e) => setLinkTagForm((p) => ({ ...p, pagePath: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-300 text-sm">页面路径(可选)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
|
||||
placeholder="pages/index/index"
|
||||
value={linkTagForm.pagePath}
|
||||
onChange={(e) => setLinkTagForm((p) => ({ ...p, pagePath: e.target.value }))}
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -3149,13 +3215,18 @@ export function ContentPage() {
|
||||
url: linkTagForm.url.trim(),
|
||||
type: linkTagForm.type,
|
||||
appId: linkTagForm.appId.trim(),
|
||||
appSecret: linkTagForm.appSecret.trim(),
|
||||
pagePath: linkTagForm.pagePath.trim(),
|
||||
}
|
||||
// 新增:允许留空,后端自动生成;编辑:tagId 已锁定
|
||||
// 留空则后端自动生成;自定义时与库一致:1~50 字符,勿含 #、逗号、换行
|
||||
if (payload.tagId) {
|
||||
const ok = /^\d{12}$/.test(payload.tagId) || /^z[a-z0-9]{11}$/.test(payload.tagId)
|
||||
if (!ok) {
|
||||
toast.error('标签ID需为12位数字,或 z 开头的12位(z+11位小写字母数字)')
|
||||
const id = payload.tagId
|
||||
if ([...id].length > 50) {
|
||||
toast.error('标签ID 最长 50 个字符')
|
||||
return
|
||||
}
|
||||
if (/[#,\n\r\t]/.test(id)) {
|
||||
toast.error('标签ID 不能含 #、逗号或换行')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -3164,6 +3235,7 @@ export function ContentPage() {
|
||||
return
|
||||
}
|
||||
if (payload.type === 'miniprogram') payload.url = ''
|
||||
if (payload.type === 'wxlink') { payload.appId = ''; payload.pagePath = '' }
|
||||
setLinkTagSaving(true)
|
||||
try {
|
||||
const res = await post<{ success?: boolean; error?: string }>('/api/db/link-tags', payload)
|
||||
|
||||
@@ -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