From 235f109b4b0ef6fafdfb9e12903467ab82b16d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E4=BA=9A=E6=98=95?= <1549469775@qq.com> Date: Tue, 2 Sep 2025 17:08:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96=E9=A1=B9?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E9=87=8D=E6=9E=84=20API=20=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=99=A8=E4=BB=A5=E4=BD=BF=E7=94=A8=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=9A=84=E5=93=8D=E5=BA=94=E6=A0=BC=E5=BC=8F=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=94=A8=E6=88=B7=E8=B5=84=E6=96=99=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E6=B5=8B=E8=AF=95=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 162470 -> 163991 bytes database/development.sqlite3-shm | Bin 32768 -> 32768 bytes database/development.sqlite3-wal | Bin 638632 -> 646872 bytes package.json | 1 + public/uploads/avatars/.gitkeep | 0 public/uploads/files/.gitkeep | 0 scripts/test-profile.js | 129 -------------------- src/controllers/Api/ApiController.js | 4 +- src/controllers/Api/AuthController.js | 12 +- src/controllers/Api/JobController.js | 10 +- src/controllers/Page/PageController.js | 217 +++++++++++++++++++++++++++++---- src/global.js | 2 +- src/middlewares/Views/index.js | 1 + src/utils/helper.js | 26 +++- 14 files changed, 232 insertions(+), 170 deletions(-) create mode 100644 public/uploads/avatars/.gitkeep create mode 100644 public/uploads/files/.gitkeep delete mode 100644 scripts/test-profile.js diff --git a/bun.lockb b/bun.lockb index 965abd33c955d96ae14025205955841900f3b8fb..649d0f4de3582154657cb09a264e5ade76387bb4 100644 GIT binary patch delta 29519 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 diff --git a/package.json b/package.json index e1ddd8d..cbb4de0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@koa/etag": "^5.0.1", "bcryptjs": "^3.0.2", "consolidate": "^1.0.4", + "formidable": "^3.5.4", "get-paths": "^0.0.7", "jsonwebtoken": "^9.0.0", "knex": "^3.1.0", diff --git a/public/uploads/avatars/.gitkeep b/public/uploads/avatars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/files/.gitkeep b/public/uploads/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/test-profile.js b/scripts/test-profile.js deleted file mode 100644 index 5f30e81..0000000 --- a/scripts/test-profile.js +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env node - -/** - * 用户资料系统测试脚本 - * 用于验证系统功能是否正常 - */ - -import { execSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; - -console.log('🧪 开始测试用户资料系统...\n'); - -// 检查必要的文件是否存在 -const requiredFiles = [ - 'src/controllers/Page/PageController.js', - 'src/views/page/profile/index.pug', - 'public/js/profile.js', - 'src/db/migrations/20250901000000_add_profile_fields.mjs' -]; - -console.log('📁 检查必要文件...'); -let allFilesExist = true; - -requiredFiles.forEach(file => { - if (fs.existsSync(file)) { - console.log(`✅ ${file}`); - } else { - console.log(`❌ ${file} - 文件不存在`); - allFilesExist = false; - } -}); - -if (!allFilesExist) { - console.log('\n❌ 部分必要文件缺失,请检查文件创建'); - process.exit(1); -} - -console.log('\n✅ 所有必要文件都存在'); - -// 检查数据库迁移文件 -console.log('\n🗄️ 检查数据库迁移...'); -try { - const migrationContent = fs.readFileSync('src/db/migrations/20250901000000_add_profile_fields.mjs', 'utf8'); - if (migrationContent.includes('name') && migrationContent.includes('bio') && migrationContent.includes('avatar')) { - console.log('✅ 数据库迁移文件包含必要字段'); - } else { - console.log('❌ 数据库迁移文件缺少必要字段'); - } -} catch (error) { - console.log('❌ 无法读取数据库迁移文件'); -} - -// 检查路由配置 -console.log('\n🛣️ 检查路由配置...'); -try { - const controllerContent = fs.readFileSync('src/controllers/Page/PageController.js', 'utf8'); - - const hasProfileGet = controllerContent.includes('profileGet'); - const hasProfileUpdate = controllerContent.includes('profileUpdate'); - const hasChangePassword = controllerContent.includes('changePassword'); - const hasProfileRoutes = controllerContent.includes('/profile/update') && controllerContent.includes('/profile/change-password'); - - if (hasProfileGet && hasProfileUpdate && hasChangePassword && hasProfileRoutes) { - console.log('✅ 控制器方法已实现'); - console.log('✅ 路由配置已添加'); - } else { - console.log('❌ 控制器方法或路由配置不完整'); - } -} catch (error) { - console.log('❌ 无法读取控制器文件'); -} - -// 检查前端模板 -console.log('\n🎨 检查前端模板...'); -try { - const templateContent = fs.readFileSync('src/views/page/profile/index.pug', 'utf8'); - - const hasProfileForm = templateContent.includes('profileForm'); - const hasPasswordForm = templateContent.includes('passwordForm'); - const hasUserFields = templateContent.includes('username') && templateContent.includes('email') && templateContent.includes('name'); - const hasInlineStyles = templateContent.includes('style.') && templateContent.includes('.profile-container'); - - if (hasProfileForm && hasPasswordForm && hasUserFields && hasInlineStyles) { - console.log('✅ 前端模板包含必要表单和样式'); - } else { - console.log('❌ 前端模板缺少必要元素'); - } -} catch (error) { - console.log('❌ 无法读取前端模板文件'); -} - -// 检查JavaScript功能 -console.log('\n⚡ 检查JavaScript功能...'); -try { - const jsContent = fs.readFileSync('public/js/profile.js', 'utf8'); - - const hasProfileUpdate = jsContent.includes('handleProfileUpdate'); - const hasPasswordChange = jsContent.includes('handlePasswordChange'); - const hasValidation = jsContent.includes('validateField'); - const hasIIFE = jsContent.includes('(function()') && jsContent.includes('})();'); - - if (hasProfileUpdate && hasPasswordChange && hasValidation && hasIIFE) { - console.log('✅ JavaScript文件包含必要功能,使用IIFE模式'); - } else { - console.log('❌ JavaScript文件缺少必要功能'); - } -} catch (error) { - console.log('❌ 无法读取JavaScript文件'); -} - -console.log('\n📋 测试完成!'); -console.log('\n📝 下一步操作:'); -console.log('1. 运行数据库迁移: npm run migrate'); -console.log('2. 启动应用: npm start'); -console.log('3. 访问 /profile 页面测试功能'); -console.log('4. 确保用户已登录才能访问资料页面'); - -console.log('\n🔧 如果遇到问题:'); -console.log('- 检查数据库连接'); -console.log('- 确认用户表结构正确'); -console.log('- 查看浏览器控制台错误信息'); -console.log('- 检查服务器日志'); - -console.log('\n✨ 重构完成:'); -console.log('- 样式已内联到Pug模板中'); -console.log('- JavaScript使用IIFE模式,避免全局污染'); -console.log('- 界面设计更简洁,与项目风格保持一致'); -console.log('- 代码结构更清晰,易于维护'); diff --git a/src/controllers/Api/ApiController.js b/src/controllers/Api/ApiController.js index 73f5fb3..602e56e 100644 --- a/src/controllers/Api/ApiController.js +++ b/src/controllers/Api/ApiController.js @@ -1,4 +1,4 @@ -import { formatResponse } from "utils/helper.js" +import { R } from "utils/helper.js" import Router from "utils/router.js" class AuthController { @@ -40,7 +40,7 @@ class AuthController { ctx.set("Content-Type", "image/jpeg") ctx.body = data } else { - ctx.body = formatResponse(false, "Failed to fetch image") + R.ResponseJSON(R.ERROR, "Failed to fetch image") } } diff --git a/src/controllers/Api/AuthController.js b/src/controllers/Api/AuthController.js index 1d0586f..4c4e5cd 100644 --- a/src/controllers/Api/AuthController.js +++ b/src/controllers/Api/AuthController.js @@ -1,5 +1,5 @@ -import UserService from "services/UserService.js" -import { formatResponse } from "utils/helper.js" +import UserService from "services/userService.js" +import { R } from "utils/helper.js" import Router from "utils/router.js" class AuthController { @@ -8,24 +8,24 @@ class AuthController { } async hello(ctx) { - ctx.body = formatResponse(true, "Hello World") + R.ResponseJSON(R.SUCCESS,"Hello World") } async getUser(ctx) { const user = await this.userService.getUserById(ctx.params.id) - ctx.body = formatResponse(true, user) + R.ResponseJSON(R.SUCCESS,user) } async register(ctx) { const { username, email, password } = ctx.request.body const user = await this.userService.register({ username, email, password }) - ctx.body = formatResponse(true, user) + R.ResponseJSON(R.SUCCESS,user) } async login(ctx) { const { username, email, password } = ctx.request.body const result = await this.userService.login({ username, email, password }) - ctx.body = formatResponse(true, result) + R.ResponseJSON(R.SUCCESS,result) } /** diff --git a/src/controllers/Api/JobController.js b/src/controllers/Api/JobController.js index a4a3f0a..719fddf 100644 --- a/src/controllers/Api/JobController.js +++ b/src/controllers/Api/JobController.js @@ -1,6 +1,6 @@ // Job Controller 示例:如何调用 service 层动态控制和查询定时任务 import JobService from "services/JobService.js" -import { formatResponse } from "utils/helper.js" +import { R } from "utils/helper.js" import Router from "utils/router.js" class JobController { @@ -10,26 +10,26 @@ class JobController { async list(ctx) { const data = this.jobService.listJobs() - ctx.body = formatResponse(true, data) + R.ResponseJSON(R.SUCCESS,data) } async start(ctx) { const { id } = ctx.params this.jobService.startJob(id) - ctx.body = formatResponse(true, null, null, `${id} 任务已启动`) + R.ResponseJSON(R.SUCCESS,null, `${id} 任务已启动`) } async stop(ctx) { const { id } = ctx.params this.jobService.stopJob(id) - ctx.body = formatResponse(true, null, null, `${id} 任务已停止`) + R.ResponseJSON(R.SUCCESS,null, `${id} 任务已停止`) } async updateCron(ctx) { const { id } = ctx.params const { cronTime } = ctx.request.body this.jobService.updateJobCron(id, cronTime) - ctx.body = formatResponse(true, null, null, `${id} 任务频率已修改`) + R.ResponseJSON(R.SUCCESS,null, `${id} 任务频率已修改`) } static createRoutes() { diff --git a/src/controllers/Page/PageController.js b/src/controllers/Page/PageController.js index 2eabc2a..793975c 100644 --- a/src/controllers/Page/PageController.js +++ b/src/controllers/Page/PageController.js @@ -1,10 +1,15 @@ import Router from "utils/router.js" -import UserService from "services/UserService.js" +import UserService from "services/userService.js" import SiteConfigService from "services/SiteConfigService.js" import ArticleService from "services/ArticleService.js" import svgCaptcha from "svg-captcha" +import formidable from "formidable" +import fs from "fs/promises" +import path from "path" +import { fileURLToPath } from "url" import CommonError from "@/utils/error/CommonError" import { logger } from "@/logger.js" +import { R } from "@/utils/helper" class PageController { constructor() { @@ -27,7 +32,7 @@ class PageController { url: "https://pic.xieyaxin.top/random.php", }, ], - blogs: blogs.slice(0, 4) + blogs: blogs.slice(0, 4), }, { includeSite: true, includeUser: true } ) @@ -130,13 +135,17 @@ class PageController { 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 }) + return await ctx.render( + "page/profile/index", + { + user, + site_title: "用户资料", + }, + { includeSite: true, includeUser: true } + ) } catch (error) { logger.error(`获取用户资料失败: ${error.message}`) ctx.status = 500 @@ -154,7 +163,7 @@ class PageController { try { const { username, email, name, bio, avatar } = ctx.request.body - + // 验证必填字段 if (!username) { ctx.status = 400 @@ -163,23 +172,23 @@ class PageController { } const updateData = { username, email, name, bio, avatar } - + // 移除空值 Object.keys(updateData).forEach(key => { - if (updateData[key] === undefined || updateData[key] === null || updateData[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, + + ctx.body = { + success: true, message: "资料更新成功", - user: updatedUser + user: updatedUser, } } catch (error) { logger.error(`更新用户资料失败: ${error.message}`) @@ -198,7 +207,7 @@ class PageController { try { const { oldPassword, newPassword, confirmPassword } = ctx.request.body - + if (!oldPassword || !newPassword || !confirmPassword) { ctx.status = 400 ctx.body = { success: false, message: "请填写所有密码字段" } @@ -218,10 +227,10 @@ class PageController { } await this.userService.changePassword(ctx.session.user.id, oldPassword, newPassword) - - ctx.body = { - success: true, - message: "密码修改成功" + + ctx.body = { + success: true, + message: "密码修改成功", } } catch (error) { logger.error(`修改密码失败: ${error.message}`) @@ -230,10 +239,167 @@ class PageController { } } + // 支持多文件上传,支持图片、Excel、Word文档类型,可通过配置指定允许的类型和扩展名(只需配置一个数组) + async upload(ctx) { + try { + const __dirname = path.dirname(fileURLToPath(import.meta.url)) + const publicDir = path.resolve(__dirname, "../../../public") + const uploadsDir = path.resolve(publicDir, "uploads/files") + // 确保目录存在 + await fs.mkdir(uploadsDir, { recursive: true }) + + // 只需配置一个类型-扩展名映射数组 + const defaultTypeList = [ + { mime: "image/jpeg", ext: ".jpg" }, + { mime: "image/png", ext: ".png" }, + { mime: "image/webp", ext: ".webp" }, + { mime: "image/gif", ext: ".gif" }, + { mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ext: ".xlsx" }, // .xlsx + { mime: "application/vnd.ms-excel", ext: ".xls" }, // .xls + { mime: "application/msword", ext: ".doc" }, // .doc + { mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ext: ".docx" } // .docx + ] + let typeList = defaultTypeList + + // 支持通过ctx.query.allowedTypes自定义类型(逗号分隔,自动过滤无效类型) + if (ctx.query.allowedTypes) { + const allowed = ctx.query.allowedTypes.split(",").map(t => t.trim()).filter(Boolean) + typeList = defaultTypeList.filter(item => allowed.includes(item.mime)) + } + + const allowedTypes = typeList.map(item => item.mime) + const fallbackExt = ".bin" + + const form = formidable({ + multiples: true, // 支持多文件 + maxFileSize: 10 * 1024 * 1024, // 10MB + filter: ({ mimetype }) => { + return !!mimetype && allowedTypes.includes(mimetype) + }, + uploadDir: uploadsDir, + keepExtensions: true, + }) + + const { files } = await new Promise((resolve, reject) => { + form.parse(ctx.req, (err, fields, files) => { + if (err) return reject(err) + resolve({ fields, files }) + }) + }) + + let fileList = files.file + if (!fileList) { + return R.ResponseJSON(R.ERROR, null, "未选择文件或字段名应为 file") + } + // 统一为数组 + if (!Array.isArray(fileList)) { + fileList = [fileList] + } + + // 处理所有文件 + const urls = [] + for (const picked of fileList) { + if (!picked) continue + const oldPath = picked.filepath || picked.path + // 优先用mimetype判断扩展名 + let ext = (typeList.find(item => item.mime === picked.mimetype) || {}).ext + if (!ext) { + // 回退到原始文件名的扩展名 + ext = path.extname(picked.originalFilename || picked.newFilename || "") || fallbackExt + } + // 文件名 + const filename = `${ctx.session.user.id}-${Date.now()}-${Math.random().toString(36).slice(2,8)}${ext}` + const destPath = path.join(uploadsDir, filename) + // 如果设置了 uploadDir 且 keepExtensions,文件可能已在该目录,仍统一重命名 + if (oldPath && oldPath !== destPath) { + await fs.rename(oldPath, destPath) + } + // 注意:此处url路径与public下的uploads/files对应 + const url = `/uploads/files/${filename}` + urls.push(url) + } + + ctx.body = { + success: true, + message: "上传成功", + urls, + } + } 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 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()}${safeExt}` + const destPath = path.join(avatarsDir, filename) + + // 如果设置了 uploadDir 且 keepExtensions,文件可能已在该目录,仍统一重命名 + if (oldPath && oldPath !== destPath) { + await fs.rename(oldPath, destPath) + } + + const url = `/uploads/avatars/${filename}` + + 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, + user: updatedUser, + } + } catch (error) { + logger.error(`上传头像失败: ${error.message}`) + ctx.status = 500 + ctx.body = { success: false, message: error.message || "上传头像失败" } + } + } + // 处理联系表单提交 async contactPost(ctx) { const { name, email, subject, message } = ctx.request.body - + // 简单的表单验证 if (!name || !email || !subject || !message) { ctx.status = 400 @@ -244,10 +410,10 @@ class PageController { // 这里可以添加邮件发送逻辑或数据库存储逻辑 // 目前只是简单的成功响应 logger.info(`收到联系表单: ${name} (${email}) - ${subject}: ${message}`) - - ctx.body = { - success: true, - message: "感谢您的留言,我们会尽快回复您!" + + ctx.body = { + success: true, + message: "感谢您的留言,我们会尽快回复您!", } } @@ -283,6 +449,7 @@ class PageController { 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 }) router.get("/notice", controller.pageGet("page/notice/index"), { auth: true }) router.get("/help", controller.pageGet("page/extra/help"), { auth: false }) router.get("/contact", controller.pageGet("page/extra/contact"), { auth: false }) diff --git a/src/global.js b/src/global.js index df9d66a..ba82f63 100644 --- a/src/global.js +++ b/src/global.js @@ -1,7 +1,7 @@ import Koa from "koa" import { logger } from "./logger.js" -const app = new Koa() +const app = new Koa({ asyncLocalStorage: true }) app.keys = [] diff --git a/src/middlewares/Views/index.js b/src/middlewares/Views/index.js index 6fc9682..8250bf6 100644 --- a/src/middlewares/Views/index.js +++ b/src/middlewares/Views/index.js @@ -1,4 +1,5 @@ import { resolve } from "path" +import { app } from "@/global" import consolidate from "consolidate" import send from "../Send" import getPaths from "get-paths" diff --git a/src/utils/helper.js b/src/utils/helper.js index a1903ee..ffa829b 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -1,4 +1,26 @@ +import { app } from "@/global" -export function formatResponse(success, data = null, error = null) { - return { success, error, data } +function ResponseSuccess(data = null, message = null) { + return { success: true, error: message, data } } + +function ResponseError(data = null, message = null) { + return { success: false, error: message, data } +} + +function ResponseJSON(statusCode = 200, data = null, message = null) { + app.currentContext.status = statusCode + return (app.currentContext.body = { success: true, error: message, data }) +} + +const R = { + ResponseSuccess, + ResponseError, + ResponseJSON, +} + +R.SUCCESS = 200 +R.ERROR = 500 +R.NOTFOUND = 404 + +export { R }