diff --git a/scripts/fix-articles-tags.mjs b/scripts/fix-articles-tags.mjs new file mode 100644 index 0000000..72fea21 --- /dev/null +++ b/scripts/fix-articles-tags.mjs @@ -0,0 +1,48 @@ +/** + * 修复 articles 表 tags 字段被双重/三重 JSON.stringify 污染的历史数据 + * + * 背景: + * admin 后台编辑文章时,loadItem 把 DB 里 tags 字段(字符串 "[]") + * 直接透传到 form.tags,保存时 JSON.stringify(form.tags) 导致双重编码, + * 形如 '"[]"' / '"\\"[]\\""'。公开接口 JSON.parse 后得到字符串而非数组, + * 前端 `tags.join(',')` TypeError → 页面 500。 + * + * 本脚本把所有非数组 JSON 的 tags 统一重置为 '[]'。 + * + * 运行: node scripts/fix-articles-tags.mjs + * 生产部署后执行一次即可(幂等)。 + */ + +import Database from 'better-sqlite3' +import { dirname, join } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const dbPath = process.env.DB_PATH || join(__dirname, '..', 'hulai-website.db') + +const db = new Database(dbPath) +const rows = db.prepare('SELECT id, slug, tags FROM articles').all() + +let fixed = 0 +const update = db.prepare('UPDATE articles SET tags = ? WHERE id = ?') + +for (const r of rows) { + let ok = false + if (r.tags) { + try { + const v = JSON.parse(r.tags) + ok = Array.isArray(v) + } catch {} + } else { + ok = true // null / 空字符串:稍后统一写成 [] + } + + if (!ok) { + update.run('[]', r.id) + fixed++ + console.log(`fixed id=${r.id} slug=${r.slug} old=${JSON.stringify(r.tags)}`) + } +} + +console.log(`\nDone. scanned=${rows.length} fixed=${fixed}`) +db.close() diff --git a/server/api/admin/articles.post.js b/server/api/admin/articles.post.js index 2d18f28..bd9d78c 100644 --- a/server/api/admin/articles.post.js +++ b/server/api/admin/articles.post.js @@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => { type: body.type || 'news', coverImage: body.coverImage || null, author: body.author || '呼籁旅行', - tags: body.tags ? JSON.stringify(body.tags) : '[]', + tags: JSON.stringify(Array.isArray(body.tags) ? body.tags : []), seoTitle: body.seoTitle || null, seoDesc: body.seoDesc || null, seoKeywords: body.seoKeywords || null, diff --git a/server/api/admin/articles/[id].get.js b/server/api/admin/articles/[id].get.js index d3d19f7..ec13d8a 100644 --- a/server/api/admin/articles/[id].get.js +++ b/server/api/admin/articles/[id].get.js @@ -14,5 +14,13 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 404, message: '文章不存在' }) } - return item + let tags = [] + if (item.tags) { + try { + const parsed = JSON.parse(item.tags) + tags = Array.isArray(parsed) ? parsed : [] + } catch { tags = [] } + } + + return { ...item, tags } }) diff --git a/server/api/admin/articles/[id].put.js b/server/api/admin/articles/[id].put.js index b80a1ab..0966fae 100644 --- a/server/api/admin/articles/[id].put.js +++ b/server/api/admin/articles/[id].put.js @@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => { if (body.category !== undefined) updateData.category = body.category if (body.coverImage !== undefined) updateData.coverImage = body.coverImage if (body.author !== undefined) updateData.author = body.author - if (body.tags !== undefined) updateData.tags = JSON.stringify(body.tags) + if (body.tags !== undefined) updateData.tags = JSON.stringify(Array.isArray(body.tags) ? body.tags : []) if (body.seoTitle !== undefined) updateData.seoTitle = body.seoTitle if (body.seoDesc !== undefined) updateData.seoDesc = body.seoDesc if (body.seoKeywords !== undefined) updateData.seoKeywords = body.seoKeywords diff --git a/server/api/admin/upload.post.js b/server/api/admin/upload.post.js index 6cc4f79..5261b3f 100644 --- a/server/api/admin/upload.post.js +++ b/server/api/admin/upload.post.js @@ -31,11 +31,13 @@ export default defineEventHandler(async (event) => { } // 生成文件名: 时间戳-原始文件名 + // 只保留 ASCII 字母数字下划线短横,中文/特殊字符一律去掉,避免 URL 编码与静态服务文件名不一致导致 404 const ext = extname(file.filename).toLowerCase() - const safeName = file.filename + const stripped = file.filename .replace(ext, '') - .replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '_') - .slice(0, 50) + .replace(/[^a-zA-Z0-9_-]/g, '') + .slice(0, 30) + const safeName = stripped || 'img' const fileName = `${Date.now()}-${safeName}${ext}` // 按年月分目录 diff --git a/server/api/content/[key].get.js b/server/api/content/[key].get.js index f64a78a..3e978e5 100644 --- a/server/api/content/[key].get.js +++ b/server/api/content/[key].get.js @@ -1,6 +1,18 @@ import { eq, desc, asc } from 'drizzle-orm' import { useDB, schema } from '../../utils/db.js' +// 防御性解析 tags:兼容历史脏数据(字符串字面量 "[]"、null、非法 JSON),始终返回数组 +function safeParseTags(raw) { + if (!raw) return [] + try { + let v = JSON.parse(raw) + if (typeof v === 'string') { + try { v = JSON.parse(v) } catch { return [] } + } + return Array.isArray(v) ? v : [] + } catch { return [] } +} + /** * 实体表数据构建器 * 当 key 对应独立实体表时,从实体表动态读取数据, @@ -121,7 +133,7 @@ function buildFromEntityTable(db, key, siteData) { category: a.category, coverImage: a.coverImage, author: a.author, - tags: a.tags ? JSON.parse(a.tags) : [], + tags: safeParseTags(a.tags), date: a.createdAt, })) return { articles: items } @@ -141,7 +153,7 @@ function buildFromEntityTable(db, key, siteData) { category: a.category, coverImage: a.coverImage, author: a.author, - tags: a.tags ? JSON.parse(a.tags) : [], + tags: safeParseTags(a.tags), seoTitle: a.seoTitle, seoDescription: a.seoDesc, seoKeywords: a.seoKeywords,