import Router from "../../../shared/utils/router.js" import UserService from "../../auth/services/userService.js" import formidable from "formidable" import fs from "fs/promises" import path from "path" import { fileURLToPath } from "url" import CommonError from "../../../shared/utils/error/CommonError.js" import { logger } from "../../../app/bootstrap/logger.js" import imageThumbnail from "image-thumbnail" /** * 用户资料控制器 * 负责处理用户资料管理、密码修改、头像上传等功能 */ class ProfileController { constructor() { this.userService = new UserService() } // 获取用户资料 async profileGet(ctx) { if (!ctx.session.user) { return ctx.redirect("/login") } try { const user = await this.userService.getUserById(ctx.session.user.id) return await ctx.render( "page/profile/index", { user, site_title: "用户资料", }, { includeSite: true, includeUser: true } ) } catch (error) { logger.error(`获取用户资料失败: ${error.message}`) ctx.status = 500 ctx.body = { success: false, message: "获取用户资料失败" } } } // 更新用户资料 async profileUpdate(ctx) { if (!ctx.session.user) { ctx.status = 401 ctx.body = { success: false, message: "未登录" } return } try { const { username, email, name, bio, avatar } = ctx.request.body // 验证必填字段 if (!username) { ctx.status = 400 ctx.body = { success: false, message: "用户名不能为空" } return } const updateData = { username, email, name, bio, avatar } // 移除空值 Object.keys(updateData).forEach(key => { if (updateData[key] === undefined || updateData[key] === null || updateData[key] === "") { delete updateData[key] } }) const updatedUser = await this.userService.updateUser(ctx.session.user.id, updateData) // 更新session中的用户信息 ctx.session.user = { ...ctx.session.user, ...updatedUser } ctx.body = { success: true, message: "资料更新成功", user: updatedUser, } } catch (error) { logger.error(`更新用户资料失败: ${error.message}`) ctx.status = 500 ctx.body = { success: false, message: error.message || "更新用户资料失败" } } } // 修改密码 async changePassword(ctx) { if (!ctx.session.user) { ctx.status = 401 ctx.body = { success: false, message: "未登录" } return } try { const { oldPassword, newPassword, confirmPassword } = ctx.request.body if (!oldPassword || !newPassword || !confirmPassword) { ctx.status = 400 ctx.body = { success: false, message: "请填写所有密码字段" } return } if (newPassword !== confirmPassword) { ctx.status = 400 ctx.body = { success: false, message: "新密码与确认密码不匹配" } return } if (newPassword.length < 6) { ctx.status = 400 ctx.body = { success: false, message: "新密码长度不能少于6位" } return } await this.userService.changePassword(ctx.session.user.id, oldPassword, newPassword) ctx.body = { success: true, message: "密码修改成功", } } catch (error) { logger.error(`修改密码失败: ${error.message}`) ctx.status = 500 ctx.body = { success: false, message: error.message || "修改密码失败" } } } // 上传头像(multipart/form-data) async uploadAvatar(ctx) { try { const __dirname = path.dirname(fileURLToPath(import.meta.url)) const publicDir = path.resolve(__dirname, "../../../public") const avatarsDir = path.resolve(publicDir, "uploads/avatars") // 确保目录存在 await fs.mkdir(avatarsDir, { recursive: true }) const form = formidable({ multiples: false, maxFileSize: 5 * 1024 * 1024, // 5MB filter: ({ mimetype }) => { return !!mimetype && /^(image\/jpeg|image\/png|image\/webp|image\/gif)$/.test(mimetype) }, uploadDir: avatarsDir, keepExtensions: true, }) const { files } = await new Promise((resolve, reject) => { form.parse(ctx.req, (err, fields, files) => { if (err) return reject(err) resolve({ fields, files }) }) }) const file = files.avatar || files.file || files.image const picked = Array.isArray(file) ? file[0] : file if (!picked) { ctx.status = 400 ctx.body = { success: false, message: "未选择文件或字段名应为 avatar" } return } // formidable v2 的文件对象 const oldPath = picked.filepath || picked.path const result = { url: "", thumb: "" } const ext = path.extname(picked.originalFilename || picked.newFilename || "") || path.extname(oldPath || "") || ".jpg" const safeExt = [".jpg", ".jpeg", ".png", ".webp", ".gif"].includes(ext.toLowerCase()) ? ext : ".jpg" const filename = `${ctx.session.user.id}-${Date.now()}/raw${safeExt}` const destPath = path.join(avatarsDir, filename) // 如果设置了 uploadDir 且 keepExtensions,文件可能已在该目录,仍统一重命名 if (oldPath && oldPath !== destPath) { await fs.mkdir(path.parse(destPath).dir, { recursive: true }) await fs.rename(oldPath, destPath) try { const thumbnail = await imageThumbnail(destPath) fs.writeFile(destPath.replace(/raw\./, "thumb."), thumbnail) } catch (err) { console.error(err) } } const url = `/uploads/avatars/${filename}` result.url = url result.thumb = url.replace(/raw\./, "thumb.") const updatedUser = await this.userService.updateUser(ctx.session.user.id, { avatar: url }) ctx.session.user = { ...ctx.session.user, ...updatedUser } ctx.body = { success: true, message: "头像上传成功", url, thumb: result.thumb, user: updatedUser, } } catch (error) { logger.error(`上传头像失败: ${error.message}`) ctx.status = 500 ctx.body = { success: false, message: error.message || "上传头像失败" } } } /** * 创建用户资料相关路由 * @returns {Router} 路由实例 */ static createRoutes() { const controller = new ProfileController() const router = new Router({ auth: "try" }) // 用户资料页面 router.get("/profile", controller.profileGet.bind(controller), { auth: true }) // 用户资料更新 router.post("/profile/update", controller.profileUpdate.bind(controller), { auth: true }) // 密码修改 router.post("/profile/change-password", controller.changePassword.bind(controller), { auth: true }) // 头像上传 router.post("/profile/upload-avatar", controller.uploadAvatar.bind(controller), { auth: true }) return router } } export default ProfileController