diff --git a/bun.lockb b/bun.lockb index 03b0eca..8f97feb 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 6782898..199afea 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "knex": "^3.1.0", "koa": "^3.0.0", "koa-bodyparser": "^4.4.1", - "koa-conditional-get": "^3.0.0", "koa-helmet": "^8.0.1", "koa-ratelimit": "^6.0.0", "koa-session": "^7.0.2", diff --git a/public/css/layouts/empty.css b/public/css/layouts/empty.css index 3ccdb48..d2e68b1 100644 --- a/public/css/layouts/empty.css +++ b/public/css/layouts/empty.css @@ -1,3 +1,7 @@ +* { + box-sizing: border-box; +} + html, body { margin: 0; @@ -33,8 +37,8 @@ body { max-width: 1226px; margin-right: auto; margin-left: auto; - padding-left: 20px; - padding-right: 20px; + /* padding-left: 20px; + padding-right: 20px; */ } @media (max-width: 640px) { @@ -179,12 +183,6 @@ body { background-color: #ffffff; } -.footer-content { - max-width: 1226px; - margin: 0 auto; - padding: 0 20px; -} - .footer-main { display: grid; grid-template-columns: 1fr; diff --git a/public/images/dashboard-bg.svg b/public/images/dashboard-bg.svg deleted file mode 100644 index f21bff1..0000000 --- a/public/images/dashboard-bg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/public/images/hero-bg.svg b/public/images/hero-bg.svg deleted file mode 100644 index 5fe0eaf..0000000 --- a/public/images/hero-bg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/public/images/stats-bg.svg b/public/images/stats-bg.svg deleted file mode 100644 index ec590c6..0000000 --- a/public/images/stats-bg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/public/static/bg.jpg b/public/static/bg.jpg deleted file mode 100644 index 47aeb9c..0000000 Binary files a/public/static/bg.jpg and /dev/null differ diff --git a/public/static/bg2.webp b/public/static/bg2.webp deleted file mode 100644 index 6e1a18c..0000000 Binary files a/public/static/bg2.webp and /dev/null differ diff --git a/src/middlewares/install.js b/src/middlewares/install.js index 910b859..a7cafa9 100644 --- a/src/middlewares/install.js +++ b/src/middlewares/install.js @@ -9,7 +9,6 @@ import bodyParser from "koa-bodyparser" import Views from "./Views" import Session from "./Session" import etag from "@koa/etag" -import conditional from "koa-conditional-get" import Controller from "./Controller/index.js" import app from "@/global" import fs from "fs" @@ -41,16 +40,26 @@ export default async app => { } return next() }) - // 跨域设置 + // koa-conditional-get app.use(async (ctx, next) => { - ctx.set("Access-Control-Allow-Origin", "*") - ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS") - ctx.set("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With") - ctx.set("Access-Control-Allow-Credentials", true) - if (ctx.method == "OPTIONS") { - ctx.status = 200 + await next() + // https://koajs.cn/ + if (ctx.fresh) { + ctx.status = 304 + ctx.body = null + } + }) + app.use(etag()) + // 注册完成之后静态资源设置 + app.use(async (ctx, next) => { + if (!ctx.path.startsWith("/public")) return await next() + if (ctx.method.toLowerCase() === "get") { + try { + await Send(ctx, ctx.path.replace("/public", ""), { root: publicPath, maxAge: 0, immutable: false }) + } catch (err) { + if (err.status !== 404) throw err + } } - return await next() }) // 安全设置 @@ -58,7 +67,8 @@ export default async app => { helmet({ contentSecurityPolicy: { directives: { - "script-src": ["'self'", "'unsafe-inline'"], + "script-src": ["'self'", "'unsafe-inline'", "https://unpkg.com"], + "img-src": ["'self'", "data:", "https://bpic.588ku.com", "https://user-images.githubusercontent.com"], }, }, }) @@ -138,9 +148,17 @@ export default async app => { ], }) ) - // 验证用户 - // 注入全局变量:ctx.state.user - // app.use(VerifyUserMiddleware()) + // 跨域设置 + app.use(async (ctx, next) => { + ctx.set("Access-Control-Allow-Origin", "*") + ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS") + ctx.set("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With") + ctx.set("Access-Control-Allow-Credentials", true) + if (ctx.method == "OPTIONS") { + ctx.status = 200 + } + return await next() + }) // 请求体解析 app.use(bodyParser()) app.use( @@ -178,19 +196,4 @@ export default async app => { }, }) ) - // 注册完成之后静态资源设置 - app.use(async (ctx, next) => { - if (ctx.body) return await next() - if (ctx.status === 200) return await next() - if (ctx.method.toLowerCase() === "get") { - try { - await Send(ctx, ctx.path, { root: publicPath, maxAge: 0, immutable: false }) - } catch (err) { - if (err.status !== 404) throw err - } - } - await next() - }) - app.use(conditional()) - app.use(etag()) } diff --git a/src/modules/Upload/controller/index.js b/src/modules/Upload/controller/index.js new file mode 100644 index 0000000..e49bc17 --- /dev/null +++ b/src/modules/Upload/controller/index.js @@ -0,0 +1,205 @@ +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" +import BaseController from "@/base/BaseController.js" + +/** + * 文件上传控制器 + * 负责处理通用文件上传功能 + */ +class UploadController extends BaseController { + + /** + * 创建文件上传相关路由 + * @returns {Router} 路由实例 + */ + static createRoutes() { + const controller = new this() + const router = new Router({ auth: "try" }) + + // 通用文件上传 + router.post("/upload", controller.handleRequest(controller.upload), { auth: "try" }) + + return router + } + + constructor() { + super() + // 初始化上传配置 + 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}` + return `${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 `/public/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.response(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 || "上传失败" } + } + } + +} + +export default UploadController \ No newline at end of file diff --git a/src/views/htmx/navbar/index.pug b/src/views/htmx/navbar/index.pug index 2262055..0093901 100644 --- a/src/views/htmx/navbar/index.pug +++ b/src/views/htmx/navbar/index.pug @@ -25,7 +25,7 @@ nav.navbar(class="relative") .right.menu.desktop-only a.menu-item(href="/profile") span 欢迎您, - span.font-semibold #{user.name || user.username} + span.font-semibold #{user.name || user.nickname} a.menu-item(href="/notice") .fe--notice-active a.menu-item(hx-post="/logout") 退出 \ No newline at end of file diff --git a/src/views/layouts/root.pug b/src/views/layouts/root.pug index 479f568..965e3d6 100644 --- a/src/views/layouts/root.pug +++ b/src/views/layouts/root.pug @@ -3,12 +3,11 @@ include utils.pug doctype html html(lang="zh-CN") head - block $$head - title #{site_title || $site && $site.site_title || ''} - meta(name="description" content=site_description || $site && $site.site_description || '') - meta(name="keywords" content=keywords || $site && $site.keywords || '') - if $site && $site.site_favicon - link(rel="shortcut icon", href=$site.site_favicon) + title #{site_title || siteConfig && siteConfig.site_title || ''} + meta(name="description" content=site_description || siteConfig && siteConfig.site_description || '') + meta(name="keywords" content=keywords || siteConfig && siteConfig.keywords || '') + if siteConfig && siteConfig.site_favicon + link(rel="shortcut icon", href=siteConfig.site_favicon) meta(charset="utf-8") meta(name="viewport" content="width=device-width, initial-scale=1") +css('lib/reset.css') @@ -18,6 +17,7 @@ html(lang="zh-CN") +js('lib/htmx.min.js') +js('lib/tailwindcss.3.4.17.js') +js('lib/simplebar.min.js') + block $$head body noscript style. diff --git a/src/views/layouts/utils.pug b/src/views/layouts/utils.pug index 7cc90a7..28cebef 100644 --- a/src/views/layouts/utils.pug +++ b/src/views/layouts/utils.pug @@ -10,13 +10,13 @@ mixin css(url, extranl = false) if extranl || url.startsWith('http') || url.startsWith('//') link(rel="stylesheet" type="text/css" href=url) else - link(rel="stylesheet", href=($config && $config.base || "") + (url.startsWith('/') ? url.slice(1) : url)) + link(rel="stylesheet", href=($config && $config.base || "") + "public/"+ (url.startsWith('/') ? url.slice(1) : url)) mixin js(url, extranl = false) if extranl || url.startsWith('http') || url.startsWith('//') script(type="text/javascript" src=url) else - script(src=($config && $config.base || "") + (url.startsWith('/') ? url.slice(1) : url)) + script(src=($config && $config.base || "") + "public/" + (url.startsWith('/') ? url.slice(1) : url)) mixin link(href, name) //- attributes == {class: "btn"} diff --git a/src/views/page/extra/help.pug b/src/views/page/extra/help.pug index b19c616..e41d82d 100644 --- a/src/views/page/extra/help.pug +++ b/src/views/page/extra/help.pug @@ -3,95 +3,96 @@ extends /layouts/empty.pug block pageHead block pageContent - .help.container(class=" bg-white rounded-[12px] shadow p-6 border border-gray-100") - h1(class="text-3xl font-bold mb-6 text-center text-gray-800") 帮助中心 - p(class="text-gray-600 mb-8 text-center text-lg") 欢迎使用帮助中心,这里为您提供完整的使用指南和问题解答 + .container + .help(class=" bg-white rounded-[12px] shadow p-6 border border-gray-100") + h1(class="text-3xl font-bold mb-6 text-center text-gray-800") 帮助中心 + p(class="text-gray-600 mb-8 text-center text-lg") 欢迎使用帮助中心,这里为您提供完整的使用指南和问题解答 - // 快速入门 - .help-section(class="mb-8") - h2(class="text-2xl font-semibold mb-4 text-blue-600 flex items-center") - span(class="mr-2") 🚀 - | 快速入门 - .grid.grid-cols-1(class="md:grid-cols-2 gap-4") - .help-card(class="p-4 bg-blue-50 rounded-lg border border-blue-200") - h3(class="font-semibold text-blue-800 mb-2") 注册登录 - p(class="text-sm text-gray-700") 点击右上角"注册"按钮,填写基本信息即可创建账户 - .help-card(class="p-4 bg-green-50 rounded-lg border border-green-200") - h3(class="font-semibold text-green-800 mb-2") 浏览文章 - p(class="text-sm text-gray-700") 在首页或文章页面浏览各类精彩内容 - .help-card(class="p-4 bg-purple-50 rounded-lg border border-purple-200") - h3(class="font-semibold text-purple-800 mb-2") 收藏管理 - p(class="text-sm text-gray-700") 点击文章下方的收藏按钮,在个人中心管理收藏 - .help-card(class="p-4 bg-orange-50 rounded-lg border border-orange-200") - h3(class="font-semibold text-orange-800 mb-2") 个人设置 - p(class="text-sm text-gray-700") 在个人中心修改头像、密码等账户信息 + // 快速入门 + .help-section(class="mb-8") + h2(class="text-2xl font-semibold mb-4 text-blue-600 flex items-center") + span(class="mr-2") 🚀 + | 快速入门 + .grid.grid-cols-1(class="md:grid-cols-2 gap-4") + .help-card(class="p-4 bg-blue-50 rounded-lg border border-blue-200") + h3(class="font-semibold text-blue-800 mb-2") 注册登录 + p(class="text-sm text-gray-700") 点击右上角"注册"按钮,填写基本信息即可创建账户 + .help-card(class="p-4 bg-green-50 rounded-lg border border-green-200") + h3(class="font-semibold text-green-800 mb-2") 浏览文章 + p(class="text-sm text-gray-700") 在首页或文章页面浏览各类精彩内容 + .help-card(class="p-4 bg-purple-50 rounded-lg border border-purple-200") + h3(class="font-semibold text-purple-800 mb-2") 收藏管理 + p(class="text-sm text-gray-700") 点击文章下方的收藏按钮,在个人中心管理收藏 + .help-card(class="p-4 bg-orange-50 rounded-lg border border-orange-200") + h3(class="font-semibold text-orange-800 mb-2") 个人设置 + p(class="text-sm text-gray-700") 在个人中心修改头像、密码等账户信息 - // 功能指南 - .help-section(class="mb-8") - h2(class="text-2xl font-semibold mb-4 text-green-600 flex items-center") - span(class="mr-2") 📚 - | 功能指南 - .help-features(class="space-y-4") - .feature-item(class="p-4 bg-gray-50 rounded-lg") - h3(class="font-semibold text-gray-800 mb-2") 文章阅读 - p(class="text-gray-700 text-sm") 支持多种格式的文章阅读,提供舒适的阅读体验。可以调整字体大小、切换主题等。 - .feature-item(class="p-4 bg-gray-50 rounded-lg") - h3(class="font-semibold text-gray-800 mb-2") 智能搜索 - p(class="text-gray-700 text-sm") 使用关键词搜索文章内容,支持模糊匹配和标签筛选。 - .feature-item(class="p-4 bg-gray-50 rounded-lg") - h3(class="font-semibold text-gray-800 mb-2") 收藏夹 - p(class="text-gray-700 text-sm") 创建个人收藏夹,分类管理感兴趣的内容,支持标签和备注功能。 + // 功能指南 + .help-section(class="mb-8") + h2(class="text-2xl font-semibold mb-4 text-green-600 flex items-center") + span(class="mr-2") 📚 + | 功能指南 + .help-features(class="space-y-4") + .feature-item(class="p-4 bg-gray-50 rounded-lg") + h3(class="font-semibold text-gray-800 mb-2") 文章阅读 + p(class="text-gray-700 text-sm") 支持多种格式的文章阅读,提供舒适的阅读体验。可以调整字体大小、切换主题等。 + .feature-item(class="p-4 bg-gray-50 rounded-lg") + h3(class="font-semibold text-gray-800 mb-2") 智能搜索 + p(class="text-gray-700 text-sm") 使用关键词搜索文章内容,支持模糊匹配和标签筛选。 + .feature-item(class="p-4 bg-gray-50 rounded-lg") + h3(class="font-semibold text-gray-800 mb-2") 收藏夹 + p(class="text-gray-700 text-sm") 创建个人收藏夹,分类管理感兴趣的内容,支持标签和备注功能。 - // 常见问题 - .help-section(class="mb-8") - h2(class="text-2xl font-semibold mb-4 text-purple-600 flex items-center") - span(class="mr-2") ❓ - | 常见问题 - .faq-list(class="space-y-3") - details(class="group") - summary(class="cursor-pointer p-3 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors font-medium text-purple-800") - | 如何修改密码? - .faq-content(class="p-3 bg-white rounded-lg mt-2 text-gray-700 text-sm") - | 登录后进入个人中心 → 账户安全 → 修改密码,输入原密码和新密码即可。 - - details(class="group") - summary(class="cursor-pointer p-3 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors font-medium text-purple-800") - | 忘记密码怎么办? - .faq-content(class="p-3 bg-white rounded-lg mt-2 text-gray-700 text-sm") - | 请联系客服协助处理,提供注册时的邮箱或手机号进行身份验证。 - - details(class="group") - summary(class="cursor-pointer p-3 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors font-medium text-purple-800") - | 如何批量管理收藏? - .faq-content(class="p-3 bg-white rounded-lg mt-2 text-gray-700 text-sm") - | 在个人中心的收藏页面,可以选择多个项目进行批量删除或移动操作。 + // 常见问题 + .help-section(class="mb-8") + h2(class="text-2xl font-semibold mb-4 text-purple-600 flex items-center") + span(class="mr-2") ❓ + | 常见问题 + .faq-list(class="space-y-3") + details(class="group") + summary(class="cursor-pointer p-3 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors font-medium text-purple-800") + | 如何修改密码? + .faq-content(class="p-3 bg-white rounded-lg mt-2 text-gray-700 text-sm") + | 登录后进入个人中心 → 账户安全 → 修改密码,输入原密码和新密码即可。 + + details(class="group") + summary(class="cursor-pointer p-3 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors font-medium text-purple-800") + | 忘记密码怎么办? + .faq-content(class="p-3 bg-white rounded-lg mt-2 text-gray-700 text-sm") + | 请联系客服协助处理,提供注册时的邮箱或手机号进行身份验证。 + + details(class="group") + summary(class="cursor-pointer p-3 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors font-medium text-purple-800") + | 如何批量管理收藏? + .faq-content(class="p-3 bg-white rounded-lg mt-2 text-gray-700 text-sm") + | 在个人中心的收藏页面,可以选择多个项目进行批量删除或移动操作。 - // 联系支持 - .help-section(class="mb-6") - h2(class="text-2xl font-semibold mb-4 text-red-600 flex items-center") - span(class="mr-2") 📞 - | 联系支持 - .support-info(class="grid grid-cols-1 md:grid-cols-3 gap-4") - .support-item(class="text-center p-4 bg-red-50 rounded-lg") - h3(class="font-semibold text-red-800 mb-2") 在线客服 - p(class="text-sm text-gray-700") 工作日 9:00-18:00 - .support-item(class="text-center p-4 bg-red-50 rounded-lg") - h3(class="font-semibold text-red-800 mb-2") 邮箱支持 - p(class="text-sm text-gray-700") support@example.com - .support-item(class="text-center p-4 bg-red-50 rounded-lg") - h3(class="font-semibold text-red-800 mb-2") 反馈建议 - p(class="text-sm text-gray-700") - a(href="/feedback" class="text-blue-600 hover:underline") 意见反馈页面 + // 联系支持 + .help-section(class="mb-6") + h2(class="text-2xl font-semibold mb-4 text-red-600 flex items-center") + span(class="mr-2") 📞 + | 联系支持 + .support-info(class="grid grid-cols-1 md:grid-cols-3 gap-4") + .support-item(class="text-center p-4 bg-red-50 rounded-lg") + h3(class="font-semibold text-red-800 mb-2") 在线客服 + p(class="text-sm text-gray-700") 工作日 9:00-18:00 + .support-item(class="text-center p-4 bg-red-50 rounded-lg") + h3(class="font-semibold text-red-800 mb-2") 邮箱支持 + p(class="text-sm text-gray-700") support@example.com + .support-item(class="text-center p-4 bg-red-50 rounded-lg") + h3(class="font-semibold text-red-800 mb-2") 反馈建议 + p(class="text-sm text-gray-700") + a(href="/feedback" class="text-blue-600 hover:underline") 意见反馈页面 - // 相关链接 - .help-links(class="text-center pt-6 border-t border-gray-200") - p(class="text-gray-600 mb-3") 更多帮助资源: - .links(class="flex flex-wrap justify-center gap-4") - a(href="/faq" class="text-blue-600 hover:text-blue-800 hover:underline") 常见问题 - a(href="/terms" class="text-blue-600 hover:text-blue-800 hover:underline") 服务条款 - a(href="/privacy" class="text-blue-600 hover:text-blue-800 hover:underline") 隐私政策 - a(href="/contact" class="text-blue-600 hover:text-blue-800 hover:underline") 联系我们 + // 相关链接 + .help-links(class="text-center pt-6 border-t border-gray-200") + p(class="text-gray-600 mb-3") 更多帮助资源: + .links(class="flex flex-wrap justify-center gap-4") + a(href="/faq" class="text-blue-600 hover:text-blue-800 hover:underline") 常见问题 + a(href="/terms" class="text-blue-600 hover:text-blue-800 hover:underline") 服务条款 + a(href="/privacy" class="text-blue-600 hover:text-blue-800 hover:underline") 隐私政策 + a(href="/contact" class="text-blue-600 hover:text-blue-800 hover:underline") 联系我们 - .help-footer(class="text-center mt-8 pt-6 border-t border-gray-200") - p(class="text-gray-500 text-sm") 最后更新:#{new Date().getFullYear()} 年 #{new Date().getMonth()+1} 月 #{new Date().getDate()} 日 - p(class="text-gray-400 text-xs mt-2") 如有其他问题,欢迎随时联系我们 + .help-footer(class="text-center mt-8 pt-6 border-t border-gray-200") + p(class="text-gray-500 text-sm") 最后更新:#{new Date().getFullYear()} 年 #{new Date().getMonth()+1} 月 #{new Date().getDate()} 日 + p(class="text-gray-400 text-xs mt-2") 如有其他问题,欢迎随时联系我们 diff --git a/src/views/page/index/index.pug b/src/views/page/index/index.pug index 4a30cdf..bd411dc 100644 --- a/src/views/page/index/index.pug +++ b/src/views/page/index/index.pug @@ -1,6 +1,7 @@ extends /layouts/empty.pug block pageHead + +js("https://unpkg.com/tiny-swiper@latest/lib/index.min.js") mixin PeopleCared(name, role, desc, avatar) .bg-white.shadow-md.rounded-md.p-6.flex.items-center.gap-4 @@ -14,10 +15,37 @@ mixin PeopleCared(name, role, desc, avatar) block pageContent - .container - .grid.grid-cols-1.gap-4(class="md:grid-cols-4") - +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') - +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') - +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') - +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') - +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') \ No newline at end of file + .-my-5 + form(action="/upload" method="post" enctype="multipart/form-data" class="mb-4 flex items-center") + input(type="file" name="file" required) + button(type="submit" class="ml-2 px-4 py-2 bg-blue-500 text-white rounded") 上传文件 + //- box-shadow 是在所有内容底部 + .swiper-container(style="height:400px;box-shadow: inset 0 -100px 120px #fff;overflow:hidden;mask-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 40%);") + .swiper-wrapper.h-full + .swiper-slide(style="flex-shrink: 0;") + img(src="https://user-images.githubusercontent.com/10026019/102327264-712d5880-3fc0-11eb-8f07-7d58264938c1.png") + .swiper-slide(style="flex-shrink: 0;") + img(src="https://user-images.githubusercontent.com/10026019/102327264-712d5880-3fc0-11eb-8f07-7d58264938c1.png") + .swiper-slide(style="flex-shrink: 0;") + img(src="https://user-images.githubusercontent.com/10026019/102327264-712d5880-3fc0-11eb-8f07-7d58264938c1.png") + .container + .grid.grid-cols-1.gap-4(class="md:grid-cols-4") + +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') + +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') + +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') + +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') + +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') + +block pageScripts + script. + //- Swiper.use([ SwiperPluginLazyload, SwiperPluginPagination ]) + const swiper = new Swiper(".swiper-container", { + //- loop: true, + //- pagination: { + //- el: ".swiper-pagination", + //- clickable: true, + //- }, + //- lazy: { + //- loadPrevNext: true, + //- }, + }); \ No newline at end of file diff --git a/tests/db/BaseModel.test.js b/tests/db/BaseModel.test.js deleted file mode 100644 index 766bdf3..0000000 --- a/tests/db/BaseModel.test.js +++ /dev/null @@ -1,165 +0,0 @@ -import { expect } from 'chai' -import BaseModel from '../../src/db/models/BaseModel.js' -import db from '../../src/db/index.js' - -// 创建测试模型类 -class TestModel extends BaseModel { - static get tableName() { - return 'test_table' - } -} - -describe('BaseModel', () => { - before(async () => { - // 创建测试表 - await db.schema.createTableIfNotExists('test_table', (table) => { - table.increments('id').primary() - table.string('name') - table.string('email') - table.integer('age') - table.timestamp('created_at').defaultTo(db.fn.now()) - table.timestamp('updated_at').defaultTo(db.fn.now()) - }) - }) - - after(async () => { - // 清理测试表 - await db.schema.dropTableIfExists('test_table') - }) - - beforeEach(async () => { - // 清空测试数据 - await db('test_table').del() - }) - - describe('CRUD Operations', () => { - it('应该正确创建记录', async () => { - const data = { name: 'Test User', email: 'test@example.com', age: 25 } - const result = await TestModel.create(data) - - expect(result).to.have.property('id') - expect(result.name).to.equal('Test User') - expect(result.email).to.equal('test@example.com') - expect(result.age).to.equal(25) - expect(result).to.have.property('created_at') - expect(result).to.have.property('updated_at') - }) - - it('应该正确查找记录', async () => { - // 先创建一条记录 - const data = { name: 'Test User', email: 'test@example.com', age: 25 } - const created = await TestModel.create(data) - - // 按ID查找 - const found = await TestModel.findById(created.id) - expect(found).to.deep.equal(created) - - // 查找不存在的记录 - const notFound = await TestModel.findById(999999) - expect(notFound).to.be.null - }) - - it('应该正确更新记录', async () => { - // 先创建一条记录 - const data = { name: 'Test User', email: 'test@example.com', age: 25 } - const created = await TestModel.create(data) - - // 更新记录 - const updateData = { name: 'Updated User', age: 30 } - const updated = await TestModel.update(created.id, updateData) - - expect(updated.name).to.equal('Updated User') - expect(updated.age).to.equal(30) - expect(updated.email).to.equal('test@example.com') // 未更新的字段保持不变 - }) - - it('应该正确删除记录', async () => { - // 先创建一条记录 - const data = { name: 'Test User', email: 'test@example.com', age: 25 } - const created = await TestModel.create(data) - - // 删除记录 - await TestModel.delete(created.id) - - // 验证记录已被删除 - const found = await TestModel.findById(created.id) - expect(found).to.be.null - }) - }) - - describe('Query Methods', () => { - beforeEach(async () => { - // 插入测试数据 - await TestModel.createMany([ - { name: 'User 1', email: 'user1@example.com', age: 20 }, - { name: 'User 2', email: 'user2@example.com', age: 25 }, - { name: 'User 3', email: 'user3@example.com', age: 30 } - ]) - }) - - it('应该正确查找所有记录', async () => { - const results = await TestModel.findAll() - expect(results).to.have.length(3) - }) - - it('应该正确分页查找记录', async () => { - const results = await TestModel.findAll({ page: 1, limit: 2 }) - expect(results).to.have.length(2) - }) - - it('应该正确按条件查找记录', async () => { - const results = await TestModel.findWhere({ age: 25 }) - expect(results).to.have.length(1) - expect(results[0].name).to.equal('User 2') - }) - - it('应该正确统计记录数量', async () => { - const count = await TestModel.count() - expect(count).to.equal(3) - - const filteredCount = await TestModel.count({ age: 25 }) - expect(filteredCount).to.equal(1) - }) - - it('应该正确检查记录是否存在', async () => { - const exists = await TestModel.exists({ age: 25 }) - expect(exists).to.be.true - - const notExists = await TestModel.exists({ age: 99 }) - expect(notExists).to.be.false - }) - - it('应该正确分页查询', async () => { - const result = await TestModel.paginate({ page: 1, limit: 2, orderBy: 'age' }) - expect(result.data).to.have.length(2) - expect(result.pagination).to.have.property('total', 3) - expect(result.pagination).to.have.property('totalPages', 2) - }) - }) - - describe('Batch Operations', () => { - it('应该正确批量创建记录', async () => { - const data = [ - { name: 'Batch User 1', email: 'batch1@example.com', age: 20 }, - { name: 'Batch User 2', email: 'batch2@example.com', age: 25 } - ] - - const results = await TestModel.createMany(data) - expect(results).to.have.length(2) - expect(results[0].name).to.equal('Batch User 1') - expect(results[1].name).to.equal('Batch User 2') - }) - }) - - describe('Error Handling', () => { - it('应该正确处理数据库错误', async () => { - try { - // 尝试创建违反约束的记录(如果有的话) - await TestModel.create({ name: null }) // 假设name是必需的 - } catch (error) { - expect(error).to.be.instanceOf(Error) - expect(error.message).to.include('数据库操作失败') - } - }) - }) -}) \ No newline at end of file diff --git a/tests/db/UserModel.test.js b/tests/db/UserModel.test.js deleted file mode 100644 index 588ca83..0000000 --- a/tests/db/UserModel.test.js +++ /dev/null @@ -1,258 +0,0 @@ -import { expect } from 'chai' -import { UserModel } from '../../src/db/models/UserModel.js' -import db from '../../src/db/index.js' - -describe('UserModel', () => { - before(async () => { - // 确保users表存在 - const exists = await db.schema.hasTable('users') - if (!exists) { - await db.schema.createTable('users', (table) => { - table.increments('id').primary() - table.string('username').unique() - table.string('email').unique() - table.string('password') - table.string('role').defaultTo('user') - table.string('status').defaultTo('active') - table.string('phone') - table.integer('age') - table.string('name') - table.text('bio') - table.string('avatar') - table.timestamp('created_at').defaultTo(db.fn.now()) - table.timestamp('updated_at').defaultTo(db.fn.now()) - }) - } - }) - - after(async () => { - // 清理测试数据 - await db('users').del() - }) - - beforeEach(async () => { - // 清空用户数据 - await db('users').del() - }) - - describe('User Creation', () => { - it('应该正确创建用户', async () => { - const userData = { - username: 'testuser', - email: 'test@example.com', - password: 'password123', - name: 'Test User' - } - - const user = await UserModel.create(userData) - - expect(user).to.have.property('id') - expect(user.username).to.equal('testuser') - expect(user.email).to.equal('test@example.com') - expect(user.name).to.equal('Test User') - expect(user.role).to.equal('user') - expect(user.status).to.equal('active') - }) - - it('应该防止重复用户名', async () => { - const userData1 = { - username: 'duplicateuser', - email: 'test1@example.com', - password: 'password123' - } - - const userData2 = { - username: 'duplicateuser', - email: 'test2@example.com', - password: 'password123' - } - - await UserModel.create(userData1) - - try { - await UserModel.create(userData2) - expect.fail('应该抛出错误') - } catch (error) { - expect(error.message).to.include('用户名已存在') - } - }) - - it('应该防止重复邮箱', async () => { - const userData1 = { - username: 'user1', - email: 'duplicate@example.com', - password: 'password123' - } - - const userData2 = { - username: 'user2', - email: 'duplicate@example.com', - password: 'password123' - } - - await UserModel.create(userData1) - - try { - await UserModel.create(userData2) - expect.fail('应该抛出错误') - } catch (error) { - expect(error.message).to.include('邮箱已存在') - } - }) - }) - - describe('User Queries', () => { - let testUser - - beforeEach(async () => { - testUser = await UserModel.create({ - username: 'testuser', - email: 'test@example.com', - password: 'password123', - name: 'Test User' - }) - }) - - it('应该按ID查找用户', async () => { - const user = await UserModel.findById(testUser.id) - expect(user).to.deep.equal(testUser) - }) - - it('应该按用户名查找用户', async () => { - const user = await UserModel.findByUsername('testuser') - expect(user).to.deep.equal(testUser) - }) - - it('应该按邮箱查找用户', async () => { - const user = await UserModel.findByEmail('test@example.com') - expect(user).to.deep.equal(testUser) - }) - - it('应该查找所有用户', async () => { - await UserModel.create({ - username: 'anotheruser', - email: 'another@example.com', - password: 'password123' - }) - - const users = await UserModel.findAll() - expect(users).to.have.length(2) - }) - }) - - describe('User Updates', () => { - let testUser - - beforeEach(async () => { - testUser = await UserModel.create({ - username: 'testuser', - email: 'test@example.com', - password: 'password123', - name: 'Test User' - }) - }) - - it('应该正确更新用户', async () => { - const updated = await UserModel.update(testUser.id, { - name: 'Updated Name', - phone: '123456789' - }) - - expect(updated.name).to.equal('Updated Name') - expect(updated.phone).to.equal('123456789') - expect(updated.email).to.equal('test@example.com') // 未更新的字段保持不变 - }) - - it('应该防止更新为重复的用户名', async () => { - await UserModel.create({ - username: 'anotheruser', - email: 'another@example.com', - password: 'password123' - }) - - try { - await UserModel.update(testUser.id, { username: 'anotheruser' }) - expect.fail('应该抛出错误') - } catch (error) { - expect(error.message).to.include('用户名已存在') - } - }) - - it('应该防止更新为重复的邮箱', async () => { - await UserModel.create({ - username: 'anotheruser', - email: 'another@example.com', - password: 'password123' - }) - - try { - await UserModel.update(testUser.id, { email: 'another@example.com' }) - expect.fail('应该抛出错误') - } catch (error) { - expect(error.message).to.include('邮箱已存在') - } - }) - }) - - describe('User Status Management', () => { - let testUser - - beforeEach(async () => { - testUser = await UserModel.create({ - username: 'testuser', - email: 'test@example.com', - password: 'password123' - }) - }) - - it('应该激活用户', async () => { - await UserModel.deactivate(testUser.id) - let user = await UserModel.findById(testUser.id) - expect(user.status).to.equal('inactive') - - await UserModel.activate(testUser.id) - user = await UserModel.findById(testUser.id) - expect(user.status).to.equal('active') - }) - - it('应该停用用户', async () => { - await UserModel.deactivate(testUser.id) - const user = await UserModel.findById(testUser.id) - expect(user.status).to.equal('inactive') - }) - }) - - describe('User Statistics', () => { - beforeEach(async () => { - await db('users').del() - - await UserModel.create({ - username: 'activeuser1', - email: 'active1@example.com', - password: 'password123', - status: 'active' - }) - - await UserModel.create({ - username: 'activeuser2', - email: 'active2@example.com', - password: 'password123', - status: 'active' - }) - - await UserModel.create({ - username: 'inactiveuser', - email: 'inactive@example.com', - password: 'password123', - status: 'inactive' - }) - }) - - it('应该正确获取用户统计', async () => { - const stats = await UserModel.getUserStats() - expect(stats.total).to.equal(3) - expect(stats.active).to.equal(2) - expect(stats.inactive).to.equal(1) - }) - }) -}) \ No newline at end of file diff --git a/tests/db/cache.test.js b/tests/db/cache.test.js deleted file mode 100644 index d24328f..0000000 --- a/tests/db/cache.test.js +++ /dev/null @@ -1,212 +0,0 @@ -import { expect } from 'chai' -import db, { DbQueryCache } from '../../src/db/index.js' -import { UserModel } from '../../src/db/models/UserModel.js' - -describe('Query Cache', () => { - before(async () => { - // 确保users表存在 - const exists = await db.schema.hasTable('users') - if (!exists) { - await db.schema.createTable('users', (table) => { - table.increments('id').primary() - table.string('username').unique() - table.string('email').unique() - table.string('password') - table.timestamp('created_at').defaultTo(db.fn.now()) - table.timestamp('updated_at').defaultTo(db.fn.now()) - }) - } - - // 清空缓存 - DbQueryCache.clear() - }) - - afterEach(async () => { - // 清理测试数据 - await db('users').del() - // 清空缓存 - DbQueryCache.clear() - }) - - describe('Cache Basic Operations', () => { - it('应该正确设置和获取缓存', async () => { - const key = 'test_key' - const value = { data: 'test_value', timestamp: Date.now() } - - DbQueryCache.set(key, value, 1000) // 1秒过期 - const cached = DbQueryCache.get(key) - - expect(cached).to.deep.equal(value) - }) - - it('应该正确检查缓存存在性', async () => { - const key = 'existence_test' - expect(DbQueryCache.has(key)).to.be.false - - DbQueryCache.set(key, 'test_value', 1000) - expect(DbQueryCache.has(key)).to.be.true - }) - - it('应该正确删除缓存', async () => { - const key = 'delete_test' - DbQueryCache.set(key, 'test_value', 1000) - expect(DbQueryCache.has(key)).to.be.true - - DbQueryCache.delete(key) - expect(DbQueryCache.has(key)).to.be.false - }) - - it('应该正确清空所有缓存', async () => { - DbQueryCache.set('key1', 'value1', 1000) - DbQueryCache.set('key2', 'value2', 1000) - - const statsBefore = DbQueryCache.stats() - expect(statsBefore.valid).to.be.greaterThan(0) - - DbQueryCache.clear() - - const statsAfter = DbQueryCache.stats() - expect(statsAfter.valid).to.equal(0) - }) - }) - - describe('Query Builder Cache', () => { - beforeEach(async () => { - // 创建测试用户 - await UserModel.create({ - username: 'cache_test', - email: 'cache_test@example.com', - password: 'password123' - }) - }) - - it('应该正确缓存查询结果', async () => { - // 第一次查询(应该执行数据库查询) - const result1 = await db('users') - .where('username', 'cache_test') - .cache(5000) // 5秒缓存 - - expect(result1).to.have.length(1) - expect(result1[0].username).to.equal('cache_test') - - // 修改数据库中的数据 - await db('users') - .where('username', 'cache_test') - .update({ name: 'Cached User' }) - - // 第二次查询(应该从缓存获取,不会看到更新) - const result2 = await db('users') - .where('username', 'cache_test') - .cache(5000) - - expect(result2).to.have.length(1) - expect(result2[0]).to.not.have.property('name') // 缓存的结果不会有新添加的字段 - }) - - it('应该支持自定义缓存键', async () => { - const result = await db('users') - .where('username', 'cache_test') - .cacheAs('custom_cache_key') - .cache(5000) - - // 检查自定义键是否在缓存中 - expect(DbQueryCache.has('custom_cache_key')).to.be.true - }) - - it('应该正确使缓存失效', async () => { - // 设置缓存 - await db('users') - .where('username', 'cache_test') - .cacheAs('invalidate_test') - .cache(5000) - - expect(DbQueryCache.has('invalidate_test')).to.be.true - - // 使缓存失效 - await db('users') - .where('username', 'cache_test') - .cacheInvalidate() - - // 检查缓存是否已清除 - expect(DbQueryCache.has('invalidate_test')).to.be.false - }) - - it('应该按前缀清理缓存', async () => { - // 设置多个缓存项 - await db('users').where('id', 1).cacheAs('user:1:data').cache(5000) - await db('users').where('id', 2).cacheAs('user:2:data').cache(5000) - await db('posts').where('id', 1).cacheAs('post:1:data').cache(5000) - - // 检查缓存项存在 - expect(DbQueryCache.has('user:1:data')).to.be.true - expect(DbQueryCache.has('user:2:data')).to.be.true - expect(DbQueryCache.has('post:1:data')).to.be.true - - // 按前缀清理 - await db('users').cacheInvalidateByPrefix('user:') - - // 检查清理结果 - expect(DbQueryCache.has('user:1:data')).to.be.false - expect(DbQueryCache.has('user:2:data')).to.be.false - expect(DbQueryCache.has('post:1:data')).to.be.true // 不受影响 - }) - }) - - describe('Cache Expiration', () => { - it('应该正确处理缓存过期', async () => { - const key = 'expire_test' - DbQueryCache.set(key, 'test_value', 10) // 10ms过期 - - // 立即检查应该存在 - expect(DbQueryCache.has(key)).to.be.true - expect(DbQueryCache.get(key)).to.equal('test_value') - - // 等待过期 - await new Promise(resolve => setTimeout(resolve, 20)) - - // 检查应该已过期 - expect(DbQueryCache.has(key)).to.be.false - expect(DbQueryCache.get(key)).to.be.undefined - }) - - it('应该正确清理过期缓存', async () => { - // 设置一些会过期的缓存项 - DbQueryCache.set('expired_1', 'value1', 10) // 10ms过期 - DbQueryCache.set('expired_2', 'value2', 10) // 10ms过期 - DbQueryCache.set('valid', 'value3', 5000) // 5秒过期 - - // 检查初始状态 - const statsBefore = DbQueryCache.stats() - expect(statsBefore.size).to.equal(3) - - // 等待过期 - await new Promise(resolve => setTimeout(resolve, 20)) - - // 清理过期缓存 - const cleaned = DbQueryCache.cleanup() - expect(cleaned).to.be.greaterThanOrEqual(2) - - // 检查最终状态 - const statsAfter = DbQueryCache.stats() - expect(statsAfter.size).to.equal(1) // 只剩下valid项 - expect(DbQueryCache.has('valid')).to.be.true - }) - }) - - describe('Cache Statistics', () => { - it('应该正确报告缓存统计', async () => { - // 清空并设置一些测试数据 - DbQueryCache.clear() - DbQueryCache.set('stat_test_1', 'value1', 5000) - DbQueryCache.set('stat_test_2', 'value2', 10) // 将过期 - await new Promise(resolve => setTimeout(resolve, 20)) // 等待过期 - - const stats = DbQueryCache.stats() - expect(stats).to.have.property('size') - expect(stats).to.have.property('valid') - expect(stats).to.have.property('expired') - expect(stats).to.have.property('totalSize') - expect(stats).to.have.property('averageSize') - }) - }) -}) \ No newline at end of file diff --git a/tests/db/performance.test.js b/tests/db/performance.test.js deleted file mode 100644 index b3d70dc..0000000 --- a/tests/db/performance.test.js +++ /dev/null @@ -1,142 +0,0 @@ -import { expect } from 'chai' -import db, { DbQueryCache, checkDatabaseHealth, getDatabaseStats } from '../../src/db/index.js' -import { UserModel } from '../../src/db/models/UserModel.js' -import { logQuery, getQueryStats, getSlowQueries, resetStats } from '../../src/db/monitor.js' - -describe('Database Performance', () => { - before(() => { - // 重置统计 - resetStats() - }) - - describe('Connection Pool', () => { - it('应该保持健康的数据库连接', async () => { - const health = await checkDatabaseHealth() - expect(health.status).to.equal('healthy') - expect(health).to.have.property('responseTime') - expect(health.responseTime).to.be.a('number') - }) - - it('应该正确报告连接池状态', async () => { - const stats = getDatabaseStats() - expect(stats).to.have.property('connectionPool') - expect(stats.connectionPool).to.have.property('min') - expect(stats.connectionPool).to.have.property('max') - expect(stats.connectionPool).to.have.property('used') - }) - }) - - describe('Query Performance', () => { - beforeEach(async () => { - // 清空用户表 - await db('users').del() - }) - - it('应该正确记录查询统计', async () => { - const initialStats = getQueryStats() - - // 执行一些查询 - await UserModel.create({ - username: 'perf_test', - email: 'perf_test@example.com', - password: 'password123' - }) - - await UserModel.findByUsername('perf_test') - await UserModel.findAll() - - const finalStats = getQueryStats() - expect(finalStats.totalQueries).to.be.greaterThan(initialStats.totalQueries) - }) - - it('应该正确处理缓存查询', async () => { - // 清空缓存 - DbQueryCache.clear() - - const cacheStatsBefore = DbQueryCache.stats() - - // 执行带缓存的查询 - const query = db('users').select('*').cache(1000) // 1秒缓存 - await query - - const cacheStatsAfter = DbQueryCache.stats() - expect(cacheStatsAfter.valid).to.be.greaterThan(cacheStatsBefore.valid) - }) - - it('应该正确识别慢查询', async function() { - this.timeout(5000) // 增加超时时间 - - // 清空慢查询记录 - resetStats() - - // 执行一个可能较慢的查询(通过复杂连接) - try { - const result = await db.raw(` - SELECT u1.*, u2.username as related_user - FROM users u1 - LEFT JOIN users u2 ON u1.id != u2.id - WHERE u1.id IN ( - SELECT id FROM users - WHERE username LIKE '%test%' - ORDER BY id - ) - ORDER BY u1.id, u2.id - LIMIT 100 - `) - } catch (error) { - // 忽略查询错误 - } - - // 检查是否有慢查询记录 - const slowQueries = getSlowQueries() - // 注意:由于测试环境可能很快,不一定能触发慢查询 - }) - }) - - describe('Cache Performance', () => { - it('应该正确管理缓存统计', async () => { - const cacheStats = DbQueryCache.stats() - expect(cacheStats).to.have.property('size') - expect(cacheStats).to.have.property('valid') - expect(cacheStats).to.have.property('expired') - }) - - it('应该正确清理过期缓存', async () => { - // 添加一些带短生命周期的缓存项 - DbQueryCache.set('test_key_1', 'test_value_1', 10) // 10ms过期 - DbQueryCache.set('test_key_2', 'test_value_2', 5000) // 5秒过期 - - // 等待第一个缓存项过期 - await new Promise(resolve => setTimeout(resolve, 20)) - - const cleaned = DbQueryCache.cleanup() - expect(cleaned).to.be.greaterThanOrEqual(0) - }) - - it('应该按前缀清理缓存', async () => { - DbQueryCache.set('user:123', 'user_data') - DbQueryCache.set('user:456', 'user_data') - DbQueryCache.set('post:123', 'post_data') - - const before = DbQueryCache.stats() - DbQueryCache.clearByPrefix('user:') - const after = DbQueryCache.stats() - - expect(after.valid).to.be.lessThan(before.valid) - }) - }) - - describe('Memory Usage', () => { - it('应该报告缓存内存使用情况', async () => { - // 添加一些测试数据到缓存 - DbQueryCache.set('memory_test_1', { data: 'test data 1', timestamp: Date.now() }) - DbQueryCache.set('memory_test_2', { data: 'test data 2 with more content', timestamp: Date.now() }) - - const memoryUsage = DbQueryCache.getMemoryUsage() - expect(memoryUsage).to.have.property('entryCount') - expect(memoryUsage).to.have.property('totalMemoryBytes') - expect(memoryUsage).to.have.property('averageEntrySize') - expect(memoryUsage).to.have.property('estimatedMemoryMB') - }) - }) -}) \ No newline at end of file diff --git a/tests/db/transaction.test.js b/tests/db/transaction.test.js deleted file mode 100644 index 96752ef..0000000 --- a/tests/db/transaction.test.js +++ /dev/null @@ -1,159 +0,0 @@ -import { expect } from 'chai' -import db from '../../src/db/index.js' -import { withTransaction, bulkCreate, bulkUpdate, bulkDelete } from '../../src/db/transaction.js' -import { UserModel } from '../../src/db/models/UserModel.js' - -describe('Transaction Handling', () => { - before(async () => { - // 确保users表存在 - const exists = await db.schema.hasTable('users') - if (!exists) { - await db.schema.createTable('users', (table) => { - table.increments('id').primary() - table.string('username').unique() - table.string('email').unique() - table.string('password') - table.timestamp('created_at').defaultTo(db.fn.now()) - table.timestamp('updated_at').defaultTo(db.fn.now()) - }) - } - }) - - afterEach(async () => { - // 清理测试数据 - await db('users').del() - }) - - describe('Basic Transactions', () => { - it('应该在事务中成功执行操作', async () => { - const result = await withTransaction(async (trx) => { - const user = await UserModel.createInTransaction(trx, { - username: 'trx_user', - email: 'trx@example.com', - password: 'password123' - }) - - const updated = await UserModel.updateInTransaction(trx, user.id, { - name: 'Transaction User' - }) - - return updated - }) - - expect(result).to.have.property('id') - expect(result.username).to.equal('trx_user') - expect(result.name).to.equal('Transaction User') - - // 验证数据已提交到数据库 - const user = await UserModel.findById(result.id) - expect(user).to.deep.equal(result) - }) - - it('应该在事务失败时回滚操作', async () => { - try { - await withTransaction(async (trx) => { - await UserModel.createInTransaction(trx, { - username: 'rollback_user', - email: 'rollback@example.com', - password: 'password123' - }) - - // 故意抛出错误触发回滚 - throw new Error('测试回滚') - }) - expect.fail('应该抛出错误') - } catch (error) { - expect(error.message).to.equal('测试回滚') - } - - // 验证数据未保存到数据库 - const user = await UserModel.findByUsername('rollback_user') - expect(user).to.be.null - }) - }) - - describe('Bulk Operations', () => { - it('应该正确批量创建记录', async () => { - const userData = [ - { username: 'bulk1', email: 'bulk1@example.com', password: 'password123' }, - { username: 'bulk2', email: 'bulk2@example.com', password: 'password123' }, - { username: 'bulk3', email: 'bulk3@example.com', password: 'password123' } - ] - - const results = await bulkCreate('users', userData) - - expect(results).to.have.length(3) - expect(results[0].username).to.equal('bulk1') - expect(results[1].username).to.equal('bulk2') - expect(results[2].username).to.equal('bulk3') - - // 验证数据已保存 - const count = await UserModel.count() - expect(count).to.equal(3) - }) - - it('应该正确批量更新记录', async () => { - // 先创建测试数据 - const userData = [ - { username: 'update1', email: 'update1@example.com', password: 'password123' }, - { username: 'update2', email: 'update2@example.com', password: 'password123' } - ] - - const created = await bulkCreate('users', userData) - - // 批量更新 - const updates = [ - { where: { id: created[0].id }, data: { name: 'Updated User 1' } }, - { where: { id: created[1].id }, data: { name: 'Updated User 2' } } - ] - - const results = await bulkUpdate('users', updates) - - expect(results).to.have.length(2) - expect(results[0].name).to.equal('Updated User 1') - expect(results[1].name).to.equal('Updated User 2') - }) - - it('应该正确批量删除记录', async () => { - // 先创建测试数据 - const userData = [ - { username: 'delete1', email: 'delete1@example.com', password: 'password123' }, - { username: 'delete2', email: 'delete2@example.com', password: 'password123' }, - { username: 'keep', email: 'keep@example.com', password: 'password123' } - ] - - const created = await bulkCreate('users', userData) - - // 批量删除前两个用户 - const conditions = [ - { id: created[0].id }, - { id: created[1].id } - ] - - const deletedCount = await bulkDelete('users', conditions) - expect(deletedCount).to.equal(2) - - // 验证只有第三个用户保留 - const remaining = await UserModel.findAll() - expect(remaining).to.have.length(1) - expect(remaining[0].username).to.equal('keep') - }) - }) - - describe('Atomic Operations', () => { - it('应该执行原子操作', async () => { - // 这个测试比较复杂,因为需要模拟并发场景 - // 简单测试原子操作是否能正常执行 - const result = await withTransaction(async (trx) => { - return await UserModel.createInTransaction(trx, { - username: 'atomic_user', - email: 'atomic@example.com', - password: 'password123' - }) - }) - - expect(result).to.have.property('id') - expect(result.username).to.equal('atomic_user') - }) - }) -}) \ No newline at end of file