Browse Source
- 将原PageController拆分为AuthPageController、BasePageController、ProfileController、UploadController,职责更加单一明确 - AuthPageController负责认证相关功能:登录、注册、验证码、登出等 - BasePageController负责基础页面功能:首页、静态页面和联系表单 - ProfileController负责用户资料管理、密码修改和头像上传 - UploadController负责通用文件上传功能 - 移除原PageController相关代码,优化代码结构和维护性 - 保留各控制器对应的路由创建方法,保持接口兼容性re
10 changed files with 733 additions and 546 deletions
Binary file not shown.
Binary file not shown.
@ -0,0 +1,136 @@ |
|||||
|
import Router from "utils/router.js" |
||||
|
import UserService from "services/userService.js" |
||||
|
import svgCaptcha from "svg-captcha" |
||||
|
import CommonError from "@/utils/error/CommonError" |
||||
|
import { logger } from "@/logger.js" |
||||
|
|
||||
|
/** |
||||
|
* 认证相关页面控制器 |
||||
|
* 负责处理登录、注册、验证码、登出等认证相关功能 |
||||
|
*/ |
||||
|
class AuthPageController { |
||||
|
constructor() { |
||||
|
this.userService = new UserService() |
||||
|
} |
||||
|
|
||||
|
// 未授权报错页
|
||||
|
async indexNoAuth(ctx) { |
||||
|
return await ctx.render("page/auth/no-auth", {}) |
||||
|
} |
||||
|
|
||||
|
// 登录页
|
||||
|
async loginGet(ctx) { |
||||
|
if (ctx.session.user) { |
||||
|
ctx.status = 200 |
||||
|
ctx.redirect("/?msg=用户已登录") |
||||
|
return |
||||
|
} |
||||
|
return await ctx.render("page/login/index", { site_title: "登录" }) |
||||
|
} |
||||
|
|
||||
|
// 处理登录请求
|
||||
|
async loginPost(ctx) { |
||||
|
const { username, email, password } = ctx.request.body |
||||
|
const result = await this.userService.login({ username, email, password }) |
||||
|
ctx.session.user = result.user |
||||
|
ctx.body = { success: true, message: "登录成功" } |
||||
|
} |
||||
|
|
||||
|
// 获取验证码
|
||||
|
async captchaGet(ctx) { |
||||
|
var captcha = svgCaptcha.create({ |
||||
|
size: 4, // 个数
|
||||
|
width: 100, // 宽
|
||||
|
height: 30, // 高
|
||||
|
fontSize: 38, // 字体大小
|
||||
|
color: true, // 字体颜色是否多变
|
||||
|
noise: 4, // 干扰线几条
|
||||
|
}) |
||||
|
// 记录验证码信息(文本+过期时间)
|
||||
|
// 这里设置5分钟后过期
|
||||
|
const expireTime = Date.now() + 5 * 60 * 1000 |
||||
|
ctx.session.captcha = { |
||||
|
text: captcha.text.toLowerCase(), // 转小写,忽略大小写验证
|
||||
|
expireTime: expireTime, |
||||
|
} |
||||
|
ctx.type = "image/svg+xml" |
||||
|
ctx.body = captcha.data |
||||
|
} |
||||
|
|
||||
|
// 注册页
|
||||
|
async registerGet(ctx) { |
||||
|
if (ctx.session.user) { |
||||
|
return ctx.redirect("/?msg=用户已登录") |
||||
|
} |
||||
|
return await ctx.render("page/register/index", { site_title: "注册" }) |
||||
|
} |
||||
|
|
||||
|
// 处理注册请求
|
||||
|
async registerPost(ctx) { |
||||
|
const { username, password, code } = ctx.request.body |
||||
|
|
||||
|
// 检查Session中是否存在验证码
|
||||
|
if (!ctx.session.captcha) { |
||||
|
throw new CommonError("验证码不存在,请重新获取") |
||||
|
} |
||||
|
|
||||
|
const { text, expireTime } = ctx.session.captcha |
||||
|
|
||||
|
// 检查是否过期
|
||||
|
if (Date.now() > expireTime) { |
||||
|
// 过期后清除Session中的验证码
|
||||
|
delete ctx.session.captcha |
||||
|
throw new CommonError("验证码已过期,请重新获取") |
||||
|
} |
||||
|
|
||||
|
if (!code) { |
||||
|
throw new CommonError("请输入验证码") |
||||
|
} |
||||
|
|
||||
|
if (code.toLowerCase() !== text) { |
||||
|
throw new CommonError("验证码错误") |
||||
|
} |
||||
|
|
||||
|
delete ctx.session.captcha |
||||
|
|
||||
|
await this.userService.register({ username, name: username, password, role: "user" }) |
||||
|
return ctx.redirect("/login") |
||||
|
} |
||||
|
|
||||
|
// 退出登录
|
||||
|
async logout(ctx) { |
||||
|
ctx.status = 200 |
||||
|
delete ctx.session.user |
||||
|
ctx.set("hx-redirect", "/") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建认证相关路由 |
||||
|
* @returns {Router} 路由实例 |
||||
|
*/ |
||||
|
static createRoutes() { |
||||
|
const controller = new AuthPageController() |
||||
|
const router = new Router({ auth: "try" }) |
||||
|
|
||||
|
// 未授权报错页
|
||||
|
router.get("/no-auth", controller.indexNoAuth.bind(controller), { auth: false }) |
||||
|
|
||||
|
// 登录相关
|
||||
|
router.get("/login", controller.loginGet.bind(controller), { auth: "try" }) |
||||
|
router.post("/login", controller.loginPost.bind(controller), { auth: false }) |
||||
|
|
||||
|
// 注册相关
|
||||
|
router.get("/register", controller.registerGet.bind(controller), { auth: "try" }) |
||||
|
router.post("/register", controller.registerPost.bind(controller), { auth: false }) |
||||
|
|
||||
|
// 验证码
|
||||
|
router.get("/captcha", controller.captchaGet.bind(controller), { auth: false }) |
||||
|
|
||||
|
// 登出
|
||||
|
router.post("/logout", controller.logout.bind(controller), { auth: true }) |
||||
|
|
||||
|
return router |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default AuthPageController |
||||
@ -0,0 +1,102 @@ |
|||||
|
import Router from "utils/router.js" |
||||
|
import ArticleService from "services/ArticleService.js" |
||||
|
import { logger } from "@/logger.js" |
||||
|
|
||||
|
/** |
||||
|
* 基础页面控制器 |
||||
|
* 负责处理首页、静态页面、联系表单等基础功能 |
||||
|
*/ |
||||
|
class BasePageController { |
||||
|
constructor() { |
||||
|
this.articleService = new ArticleService() |
||||
|
} |
||||
|
|
||||
|
// 首页
|
||||
|
async indexGet(ctx) { |
||||
|
const blogs = await this.articleService.getPublishedArticles() |
||||
|
return await ctx.render( |
||||
|
"page/index/index", |
||||
|
{ |
||||
|
apiList: [ |
||||
|
{ |
||||
|
name: "随机图片", |
||||
|
desc: "随机图片,点击查看。<br> 右键可复制链接", |
||||
|
url: "https://pic.xieyaxin.top/random.php", |
||||
|
}, |
||||
|
], |
||||
|
blogs: blogs.slice(0, 4), |
||||
|
}, |
||||
|
{ includeSite: true, includeUser: true } |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// 处理联系表单提交
|
||||
|
async contactPost(ctx) { |
||||
|
const { name, email, subject, message } = ctx.request.body |
||||
|
|
||||
|
// 简单的表单验证
|
||||
|
if (!name || !email || !subject || !message) { |
||||
|
ctx.status = 400 |
||||
|
ctx.body = { success: false, message: "请填写所有必填字段" } |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 这里可以添加邮件发送逻辑或数据库存储逻辑
|
||||
|
// 目前只是简单的成功响应
|
||||
|
logger.info(`收到联系表单: ${name} (${email}) - ${subject}: ${message}`) |
||||
|
|
||||
|
ctx.body = { |
||||
|
success: true, |
||||
|
message: "感谢您的留言,我们会尽快回复您!", |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 通用页面渲染方法 |
||||
|
* @param {string} name - 模板名称 |
||||
|
* @param {Object} data - 页面数据 |
||||
|
* @returns {Function} 页面渲染函数 |
||||
|
*/ |
||||
|
pageGet(name, data) { |
||||
|
return async ctx => { |
||||
|
return await ctx.render( |
||||
|
name, |
||||
|
{ |
||||
|
...(data || {}), |
||||
|
}, |
||||
|
{ includeSite: true, includeUser: true } |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建基础页面相关路由 |
||||
|
* @returns {Router} 路由实例 |
||||
|
*/ |
||||
|
static createRoutes() { |
||||
|
const controller = new BasePageController() |
||||
|
const router = new Router({ auth: "try" }) |
||||
|
|
||||
|
// 首页
|
||||
|
router.get("/", controller.indexGet.bind(controller), { auth: false }) |
||||
|
|
||||
|
// 静态页面
|
||||
|
router.get("/about", controller.pageGet("page/about/index"), { auth: false }) |
||||
|
router.get("/terms", controller.pageGet("page/extra/terms"), { auth: false }) |
||||
|
router.get("/privacy", controller.pageGet("page/extra/privacy"), { auth: false }) |
||||
|
router.get("/faq", controller.pageGet("page/extra/faq"), { auth: false }) |
||||
|
router.get("/feedback", controller.pageGet("page/extra/feedback"), { auth: false }) |
||||
|
router.get("/help", controller.pageGet("page/extra/help"), { auth: false }) |
||||
|
router.get("/contact", controller.pageGet("page/extra/contact"), { auth: false }) |
||||
|
|
||||
|
// 需要登录的页面
|
||||
|
router.get("/notice", controller.pageGet("page/notice/index"), { auth: true }) |
||||
|
|
||||
|
// 联系表单处理
|
||||
|
router.post("/contact", controller.contactPost.bind(controller), { auth: false }) |
||||
|
|
||||
|
return router |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default BasePageController |
||||
@ -1,481 +0,0 @@ |
|||||
import Router from "utils/router.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" |
|
||||
import imageThumbnail from "image-thumbnail" |
|
||||
|
|
||||
class PageController { |
|
||||
constructor() { |
|
||||
this.userService = new UserService() |
|
||||
this.siteConfigService = new SiteConfigService() |
|
||||
this.siteConfigService = new SiteConfigService() |
|
||||
this.articleService = new ArticleService() |
|
||||
} |
|
||||
|
|
||||
// 首页
|
|
||||
async indexGet(ctx) { |
|
||||
const blogs = await this.articleService.getPublishedArticles() |
|
||||
return await ctx.render( |
|
||||
"page/index/index", |
|
||||
{ |
|
||||
apiList: [ |
|
||||
{ |
|
||||
name: "随机图片", |
|
||||
desc: "随机图片,点击查看。<br> 右键可复制链接", |
|
||||
url: "https://pic.xieyaxin.top/random.php", |
|
||||
}, |
|
||||
], |
|
||||
blogs: blogs.slice(0, 4), |
|
||||
}, |
|
||||
{ includeSite: true, includeUser: true } |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// 未授权报错页
|
|
||||
async indexNoAuth(ctx) { |
|
||||
return await ctx.render("page/auth/no-auth", {}) |
|
||||
} |
|
||||
|
|
||||
// 登录页
|
|
||||
async loginGet(ctx) { |
|
||||
if (ctx.session.user) { |
|
||||
ctx.status = 200 |
|
||||
ctx.redirect("/?msg=用户已登录") |
|
||||
return |
|
||||
} |
|
||||
return await ctx.render("page/login/index", { site_title: "登录" }) |
|
||||
} |
|
||||
|
|
||||
// 处理登录请求
|
|
||||
async loginPost(ctx) { |
|
||||
const { username, email, password } = ctx.request.body |
|
||||
const result = await this.userService.login({ username, email, password }) |
|
||||
ctx.session.user = result.user |
|
||||
ctx.body = { success: true, message: "登录成功" } |
|
||||
} |
|
||||
|
|
||||
// 获取验证码
|
|
||||
async captchaGet(ctx) { |
|
||||
var captcha = svgCaptcha.create({ |
|
||||
size: 4, // 个数
|
|
||||
width: 100, // 宽
|
|
||||
height: 30, // 高
|
|
||||
fontSize: 38, // 字体大小
|
|
||||
color: true, // 字体颜色是否多变
|
|
||||
noise: 4, // 干扰线几条
|
|
||||
}) |
|
||||
// 记录验证码信息(文本+过期时间)
|
|
||||
// 这里设置5分钟后过期
|
|
||||
const expireTime = Date.now() + 5 * 60 * 1000 |
|
||||
ctx.session.captcha = { |
|
||||
text: captcha.text.toLowerCase(), // 转小写,忽略大小写验证
|
|
||||
expireTime: expireTime, |
|
||||
} |
|
||||
ctx.type = "image/svg+xml" |
|
||||
ctx.body = captcha.data |
|
||||
} |
|
||||
|
|
||||
// 注册页
|
|
||||
async registerGet(ctx) { |
|
||||
if (ctx.session.user) { |
|
||||
return ctx.redirect("/?msg=用户已登录") |
|
||||
} |
|
||||
// TODO 多个
|
|
||||
return await ctx.render("page/register/index", { site_title: "注册" }) |
|
||||
} |
|
||||
|
|
||||
// 处理注册请求
|
|
||||
async registerPost(ctx) { |
|
||||
const { username, password, code } = ctx.request.body |
|
||||
|
|
||||
// 检查Session中是否存在验证码
|
|
||||
if (!ctx.session.captcha) { |
|
||||
throw new CommonError("验证码不存在,请重新获取") |
|
||||
} |
|
||||
|
|
||||
const { text, expireTime } = ctx.session.captcha |
|
||||
|
|
||||
// 检查是否过期
|
|
||||
if (Date.now() > expireTime) { |
|
||||
// 过期后清除Session中的验证码
|
|
||||
delete ctx.session.captcha |
|
||||
throw new CommonError("验证码已过期,请重新获取") |
|
||||
} |
|
||||
|
|
||||
if (!code) { |
|
||||
throw new CommonError("请输入验证码") |
|
||||
} |
|
||||
|
|
||||
if (code.toLowerCase() !== text) { |
|
||||
throw new CommonError("验证码错误") |
|
||||
} |
|
||||
|
|
||||
delete ctx.session.captcha |
|
||||
|
|
||||
await this.userService.register({ username, name: username, password, role: "user" }) |
|
||||
return ctx.redirect("/login") |
|
||||
} |
|
||||
|
|
||||
// 退出登录
|
|
||||
async logout(ctx) { |
|
||||
ctx.status = 200 |
|
||||
delete ctx.session.user |
|
||||
ctx.set("hx-redirect", "/") |
|
||||
} |
|
||||
|
|
||||
// 获取用户资料
|
|
||||
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 || "修改密码失败" } |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 支持多文件上传,支持图片、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 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 || "上传头像失败" } |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 处理联系表单提交
|
|
||||
async contactPost(ctx) { |
|
||||
const { name, email, subject, message } = ctx.request.body |
|
||||
|
|
||||
// 简单的表单验证
|
|
||||
if (!name || !email || !subject || !message) { |
|
||||
ctx.status = 400 |
|
||||
ctx.body = { success: false, message: "请填写所有必填字段" } |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 这里可以添加邮件发送逻辑或数据库存储逻辑
|
|
||||
// 目前只是简单的成功响应
|
|
||||
logger.info(`收到联系表单: ${name} (${email}) - ${subject}: ${message}`) |
|
||||
|
|
||||
ctx.body = { |
|
||||
success: true, |
|
||||
message: "感谢您的留言,我们会尽快回复您!", |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 渲染页面
|
|
||||
pageGet(name, data) { |
|
||||
return async ctx => { |
|
||||
return await ctx.render( |
|
||||
name, |
|
||||
{ |
|
||||
...(data || {}), |
|
||||
}, |
|
||||
{ includeSite: true, includeUser: true } |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static createRoutes() { |
|
||||
const controller = new PageController() |
|
||||
const router = new Router({ auth: "try" }) |
|
||||
// 首页
|
|
||||
router.get("/", controller.indexGet.bind(controller), { auth: false }) |
|
||||
// 未授权报错页
|
|
||||
router.get("/no-auth", controller.indexNoAuth.bind(controller), { auth: false }) |
|
||||
|
|
||||
// router.get("/article/:id", controller.pageGet("page/articles/index"), { auth: false })
|
|
||||
// router.get("/articles", controller.pageGet("page/articles/index"), { auth: false })
|
|
||||
|
|
||||
router.get("/about", controller.pageGet("page/about/index"), { auth: false }) |
|
||||
router.get("/terms", controller.pageGet("page/extra/terms"), { auth: false }) |
|
||||
router.get("/privacy", controller.pageGet("page/extra/privacy"), { auth: false }) |
|
||||
router.get("/faq", controller.pageGet("page/extra/faq"), { auth: false }) |
|
||||
router.get("/feedback", controller.pageGet("page/extra/feedback"), { auth: false }) |
|
||||
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 }) |
|
||||
router.post("/contact", controller.contactPost.bind(controller), { auth: false }) |
|
||||
router.get("/login", controller.loginGet.bind(controller), { auth: "try" }) |
|
||||
router.post("/login", controller.loginPost.bind(controller), { auth: false }) |
|
||||
router.get("/captcha", controller.captchaGet.bind(controller), { auth: false }) |
|
||||
router.get("/register", controller.registerGet.bind(controller), { auth: "try" }) |
|
||||
router.post("/register", controller.registerPost.bind(controller), { auth: false }) |
|
||||
router.post("/logout", controller.logout.bind(controller), { auth: true }) |
|
||||
return router |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default PageController |
|
||||
@ -0,0 +1,228 @@ |
|||||
|
import Router from "utils/router.js" |
||||
|
import UserService from "services/userService.js" |
||||
|
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 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 |
||||
@ -0,0 +1,200 @@ |
|||||
|
import Router from "utils/router.js" |
||||
|
import formidable from "formidable" |
||||
|
import fs from "fs/promises" |
||||
|
import path from "path" |
||||
|
import { fileURLToPath } from "url" |
||||
|
import { logger } from "@/logger.js" |
||||
|
import { R } from "@/utils/helper" |
||||
|
|
||||
|
/** |
||||
|
* 文件上传控制器 |
||||
|
* 负责处理通用文件上传功能 |
||||
|
*/ |
||||
|
class UploadController { |
||||
|
constructor() { |
||||
|
// 初始化上传配置
|
||||
|
this.initConfig() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化上传配置 |
||||
|
*/ |
||||
|
initConfig() { |
||||
|
// 默认支持的文件类型配置
|
||||
|
this.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
|
||||
|
] |
||||
|
|
||||
|
this.fallbackExt = ".bin" |
||||
|
this.maxFileSize = 10 * 1024 * 1024 // 10MB
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取允许的文件类型 |
||||
|
* @param {Object} ctx - Koa上下文 |
||||
|
* @returns {Array} 允许的文件类型列表 |
||||
|
*/ |
||||
|
getAllowedTypes(ctx) { |
||||
|
let typeList = this.defaultTypeList |
||||
|
|
||||
|
// 支持通过ctx.query.allowedTypes自定义类型(逗号分隔,自动过滤无效类型)
|
||||
|
if (ctx.query.allowedTypes) { |
||||
|
const allowed = ctx.query.allowedTypes |
||||
|
.split(",") |
||||
|
.map(t => t.trim()) |
||||
|
.filter(Boolean) |
||||
|
typeList = this.defaultTypeList.filter(item => allowed.includes(item.mime)) |
||||
|
} |
||||
|
|
||||
|
return typeList |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取上传目录路径 |
||||
|
* @returns {string} 上传目录路径 |
||||
|
*/ |
||||
|
getUploadDir() { |
||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
||||
|
const publicDir = path.resolve(__dirname, "../../../public") |
||||
|
return path.resolve(publicDir, "uploads/files") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 确保上传目录存在 |
||||
|
* @param {string} dir - 目录路径 |
||||
|
*/ |
||||
|
async ensureUploadDir(dir) { |
||||
|
await fs.mkdir(dir, { recursive: true }) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成安全的文件名 |
||||
|
* @param {Object} ctx - Koa上下文 |
||||
|
* @param {string} ext - 文件扩展名 |
||||
|
* @returns {string} 生成的文件名 |
||||
|
*/ |
||||
|
generateFileName(ctx, ext) { |
||||
|
return `${ctx.session.user.id}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}` |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取文件扩展名 |
||||
|
* @param {Object} file - 文件对象 |
||||
|
* @param {Array} typeList - 类型列表 |
||||
|
* @returns {string} 文件扩展名 |
||||
|
*/ |
||||
|
getFileExtension(file, typeList) { |
||||
|
// 优先用mimetype判断扩展名
|
||||
|
let ext = (typeList.find(item => item.mime === file.mimetype) || {}).ext |
||||
|
if (!ext) { |
||||
|
// 回退到原始文件名的扩展名
|
||||
|
ext = path.extname(file.originalFilename || file.newFilename || "") || this.fallbackExt |
||||
|
} |
||||
|
return ext |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理单个文件上传 |
||||
|
* @param {Object} file - 文件对象 |
||||
|
* @param {Object} ctx - Koa上下文 |
||||
|
* @param {string} uploadsDir - 上传目录 |
||||
|
* @param {Array} typeList - 类型列表 |
||||
|
* @returns {string} 文件URL |
||||
|
*/ |
||||
|
async processFile(file, ctx, uploadsDir, typeList) { |
||||
|
if (!file) return null |
||||
|
|
||||
|
const oldPath = file.filepath || file.path |
||||
|
const ext = this.getFileExtension(file, typeList) |
||||
|
const filename = this.generateFileName(ctx, ext) |
||||
|
const destPath = path.join(uploadsDir, filename) |
||||
|
|
||||
|
// 移动文件到目标位置
|
||||
|
if (oldPath && oldPath !== destPath) { |
||||
|
await fs.rename(oldPath, destPath) |
||||
|
} |
||||
|
|
||||
|
// 返回相对于public的URL路径
|
||||
|
return `/uploads/files/${filename}` |
||||
|
} |
||||
|
|
||||
|
// 支持多文件上传,支持图片、Excel、Word文档类型,可通过配置指定允许的类型和扩展名(只需配置一个数组)
|
||||
|
async upload(ctx) { |
||||
|
try { |
||||
|
const uploadsDir = this.getUploadDir() |
||||
|
await this.ensureUploadDir(uploadsDir) |
||||
|
|
||||
|
const typeList = this.getAllowedTypes(ctx) |
||||
|
const allowedTypes = typeList.map(item => item.mime) |
||||
|
|
||||
|
const form = formidable({ |
||||
|
multiples: true, // 支持多文件
|
||||
|
maxFileSize: this.maxFileSize, |
||||
|
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 file of fileList) { |
||||
|
const url = await this.processFile(file, ctx, uploadsDir, typeList) |
||||
|
if (url) { |
||||
|
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 || "上传失败" } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建文件上传相关路由 |
||||
|
* @returns {Router} 路由实例 |
||||
|
*/ |
||||
|
static createRoutes() { |
||||
|
const controller = new UploadController() |
||||
|
const router = new Router({ auth: "try" }) |
||||
|
|
||||
|
// 通用文件上传
|
||||
|
router.post("/upload", controller.upload.bind(controller), { auth: true }) |
||||
|
|
||||
|
return router |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default UploadController |
||||
Loading…
Reference in new issue