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