import { Router, Response } from 'express' import multer from 'multer' import path from 'path' import fs from 'fs' import pool from '../db/connection' import { AuthRequest } from '../middleware/auth' const router = Router() // 头像上传配置 const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads' const AVATAR_DIR = path.resolve(UPLOAD_DIR, 'avatars') if (!fs.existsSync(AVATAR_DIR)) { fs.mkdirSync(AVATAR_DIR, { recursive: true }) } const storage = multer.diskStorage({ destination: (_req, _file, cb) => { // 确保目录存在(可能被手动删除) if (!fs.existsSync(AVATAR_DIR)) { fs.mkdirSync(AVATAR_DIR, { recursive: true }) } cb(null, AVATAR_DIR) }, filename: (req: AuthRequest, file, cb) => { const extMap: Record = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp' } const ext = extMap[file.mimetype] || '.jpg' cb(null, `${req.userId}_${Date.now()}${ext}`) } }) const upload = multer({ storage, limits: { fileSize: 2 * 1024 * 1024 }, // 最大 2MB fileFilter: (_req, file, cb) => { const allowed = ['image/jpeg', 'image/png', 'image/webp'] if (allowed.includes(file.mimetype)) { cb(null, true) } else { cb(new Error('仅支持 JPEG、PNG、WebP 格式')) } } }) /** 获取当前用户信息 */ router.get('/me', async (req: AuthRequest, res: Response) => { try { const [rows] = await pool.query( 'SELECT id, nickname, avatar_url, slogan, role, created_at FROM users WHERE id = ? AND deleted_at IS NULL', [req.userId] ) const user = (rows as any[])[0] if (!user) { return res.status(404).json({ code: 40400, message: '用户不存在' }) } res.json({ code: 0, data: user }) } catch (err) { console.error('[User] GET /me error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 更新用户昵称/签名 */ router.put('/me', async (req: AuthRequest, res: Response) => { try { const { nickname, slogan } = req.body if (nickname !== undefined) { if (typeof nickname !== 'string' || nickname.trim().length === 0) { return res.status(400).json({ code: 40001, message: '昵称不能为空' }) } if (nickname.length > 50) { return res.status(400).json({ code: 40001, message: '昵称不能超过50个字符' }) } } if (slogan !== undefined) { if (typeof slogan !== 'string') { return res.status(400).json({ code: 40001, message: '签名格式错误' }) } if (slogan.length > 100) { return res.status(400).json({ code: 40001, message: '签名不能超过100个字符' }) } } const updates: string[] = [] const params: any[] = [] if (nickname !== undefined) { updates.push('nickname = ?') params.push(nickname.trim()) } if (slogan !== undefined) { updates.push('slogan = ?') params.push(slogan.trim()) } if (updates.length === 0) { return res.status(400).json({ code: 40001, message: '没有需要更新的字段' }) } params.push(req.userId) await pool.query(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, params) res.json({ code: 0 }) } catch (err) { console.error('[User] PUT /me error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 上传头像 */ router.post('/avatar', (req: AuthRequest, res: Response) => { upload.single('file')(req, res, async (err) => { if (err) { return res.status(400).json({ code: 40001, message: err.message }) } if (!req.file) { return res.status(400).json({ code: 40001, message: '请选择图片文件' }) } try { // 先查询旧头像 const [rows] = await pool.query('SELECT avatar_url FROM users WHERE id = ? AND deleted_at IS NULL', [req.userId]) const oldUrl = (rows as any[])[0]?.avatar_url // 先更新 DB,再删旧文件(保证 DB 和文件至少有一个正确) await pool.query('UPDATE users SET avatar_url = ? WHERE id = ?', [req.file.filename, req.userId]) if (oldUrl) { const oldPath = path.join(AVATAR_DIR, oldUrl) if (fs.existsSync(oldPath)) { try { fs.unlinkSync(oldPath) } catch (e) { console.warn('[User] 删除旧头像失败:', e) } } } res.json({ code: 0, data: { avatar_url: req.file.filename } }) } catch (err) { console.error('[User] avatar upload error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) }) export default router