hulai-website/server/api/admin/upload.post.js
wx 61c94b741e fix(blog): tags 双重编码导致详情页 500 + 上传文件名去中文
问题:
- 管理后台编辑博客保存后,访问 /blog/{slug} 返回 500
- 用户上传封面图后前端不识别/裂图

根因:
1. admin [id].get.js 原样返回 DB 里的 tags 字符串 "[]",编辑页 loadItem
   透传到 form.tags,保存时 JSON.stringify 再编码一次 → DB 写入 '"[]"'。
   公开接口 JSON.parse 后得到字符串 "[]" 而非数组,前端 tags.join(',')
   对字符串调用 → TypeError → SSR 500。
2. 上传接口保留中文文件名(\u4e00-\u9fa5 正则放行),Nginx/Nuxt 静态服务
   对含中文 URL 处理不一致,静态请求 404。

修复:
- admin GET:tags 统一解析为数组返回
- admin POST/PUT:只接受数组,非数组写入空数组
- 公开 /api/content/blog|news:safeParseTags 兼容历史脏数据,
  即使 DB 里是 '"[]"' / 非法 JSON 也保证输出数组
- upload.post.js:safeName 只保留 ASCII 字母数字,去除中文
- scripts/fix-articles-tags.mjs:一次性清理历史双重编码数据
2026-04-20 13:21:04 +08:00

61 行
2.0 KiB
JavaScript

import { writeFileSync, existsSync, mkdirSync } from 'fs'
import { join, extname } from 'path'
import { requireAdmin } from '../../utils/auth.js'
// 允许的图片类型
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml']
const MAX_SIZE = 10 * 1024 * 1024 // 10MB
export default defineEventHandler(async (event) => {
requireAdmin(event)
const formData = await readMultipartFormData(event)
if (!formData || !formData.length) {
throw createError({ statusCode: 422, message: '请选择要上传的文件' })
}
const results = []
for (const file of formData) {
if (!file.filename || !file.data) continue
// 校验文件类型
const mimeType = file.type || ''
if (!ALLOWED_TYPES.includes(mimeType)) {
throw createError({ statusCode: 422, message: `不支持的文件类型: ${mimeType},仅支持 JPG/PNG/WebP/GIF/SVG` })
}
// 校验文件大小
if (file.data.length > MAX_SIZE) {
throw createError({ statusCode: 422, message: `文件过大,最大 10MB` })
}
// 生成文件名: 时间戳-原始文件名
// 只保留 ASCII 字母数字下划线短横,中文/特殊字符一律去掉,避免 URL 编码与静态服务文件名不一致导致 404
const ext = extname(file.filename).toLowerCase()
const stripped = file.filename
.replace(ext, '')
.replace(/[^a-zA-Z0-9_-]/g, '')
.slice(0, 30)
const safeName = stripped || 'img'
const fileName = `${Date.now()}-${safeName}${ext}`
// 按年月分目录
const now = new Date()
const subDir = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`
const uploadDir = join(process.cwd(), 'public', 'uploads', subDir)
if (!existsSync(uploadDir)) {
mkdirSync(uploadDir, { recursive: true })
}
const filePath = join(uploadDir, fileName)
writeFileSync(filePath, file.data)
const url = `/uploads/${subDir}/${fileName}`
results.push({ url, name: file.filename, size: file.data.length })
}
return { success: true, files: results }
})