Browse Source

调整

pure
dash 2 months ago
parent
commit
ea6525591d
  1. BIN
      bun.lockb
  2. 1
      package.json
  3. 14
      public/css/layouts/empty.css
  4. 3
      public/images/dashboard-bg.svg
  5. 3
      public/images/hero-bg.svg
  6. 3
      public/images/stats-bg.svg
  7. BIN
      public/static/bg.jpg
  8. BIN
      public/static/bg2.webp
  9. 59
      src/middlewares/install.js
  10. 205
      src/modules/Upload/controller/index.js
  11. 2
      src/views/htmx/navbar/index.pug
  12. 12
      src/views/layouts/root.pug
  13. 4
      src/views/layouts/utils.pug
  14. 3
      src/views/page/extra/help.pug
  15. 38
      src/views/page/index/index.pug
  16. 165
      tests/db/BaseModel.test.js
  17. 258
      tests/db/UserModel.test.js
  18. 212
      tests/db/cache.test.js
  19. 142
      tests/db/performance.test.js
  20. 159
      tests/db/transaction.test.js

BIN
bun.lockb

Binary file not shown.

1
package.json

@ -39,7 +39,6 @@
"knex": "^3.1.0", "knex": "^3.1.0",
"koa": "^3.0.0", "koa": "^3.0.0",
"koa-bodyparser": "^4.4.1", "koa-bodyparser": "^4.4.1",
"koa-conditional-get": "^3.0.0",
"koa-helmet": "^8.0.1", "koa-helmet": "^8.0.1",
"koa-ratelimit": "^6.0.0", "koa-ratelimit": "^6.0.0",
"koa-session": "^7.0.2", "koa-session": "^7.0.2",

14
public/css/layouts/empty.css

@ -1,3 +1,7 @@
* {
box-sizing: border-box;
}
html, html,
body { body {
margin: 0; margin: 0;
@ -33,8 +37,8 @@ body {
max-width: 1226px; max-width: 1226px;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
padding-left: 20px; /* padding-left: 20px;
padding-right: 20px; padding-right: 20px; */
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@ -179,12 +183,6 @@ body {
background-color: #ffffff; background-color: #ffffff;
} }
.footer-content {
max-width: 1226px;
margin: 0 auto;
padding: 0 20px;
}
.footer-main { .footer-main {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;

3
public/images/dashboard-bg.svg

@ -1,3 +0,0 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<polygon points="50,1 99,99 1,99" fill="#8b5cf6" opacity="0.1"/>
</svg>

Before

Width:  |  Height:  |  Size: 139 B

3
public/images/hero-bg.svg

@ -1,3 +0,0 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="1" fill="#3b82f6" opacity="0.1"/>
</svg>

Before

Width:  |  Height:  |  Size: 135 B

3
public/images/stats-bg.svg

@ -1,3 +0,0 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<rect width="1" height="1" fill="#10b981" opacity="0.1"/>
</svg>

Before

Width:  |  Height:  |  Size: 132 B

BIN
public/static/bg.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/static/bg2.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

59
src/middlewares/install.js

@ -9,7 +9,6 @@ import bodyParser from "koa-bodyparser"
import Views from "./Views" import Views from "./Views"
import Session from "./Session" import Session from "./Session"
import etag from "@koa/etag" import etag from "@koa/etag"
import conditional from "koa-conditional-get"
import Controller from "./Controller/index.js" import Controller from "./Controller/index.js"
import app from "@/global" import app from "@/global"
import fs from "fs" import fs from "fs"
@ -41,16 +40,26 @@ export default async app => {
} }
return next() return next()
}) })
// 跨域设置 // koa-conditional-get
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", "*") await next()
ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS") // https://koajs.cn/
ctx.set("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With") if (ctx.fresh) {
ctx.set("Access-Control-Allow-Credentials", true) ctx.status = 304
if (ctx.method == "OPTIONS") { ctx.body = null
ctx.status = 200 }
})
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({ helmet({
contentSecurityPolicy: { contentSecurityPolicy: {
directives: { 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(async (ctx, next) => {
// app.use(VerifyUserMiddleware()) 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(bodyParser())
app.use( 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())
} }

205
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

2
src/views/htmx/navbar/index.pug

@ -25,7 +25,7 @@ nav.navbar(class="relative")
.right.menu.desktop-only .right.menu.desktop-only
a.menu-item(href="/profile") a.menu-item(href="/profile")
span 欢迎您, span 欢迎您,
span.font-semibold #{user.name || user.username} span.font-semibold #{user.name || user.nickname}
a.menu-item(href="/notice") a.menu-item(href="/notice")
.fe--notice-active .fe--notice-active
a.menu-item(hx-post="/logout") 退出 a.menu-item(hx-post="/logout") 退出

12
src/views/layouts/root.pug

@ -3,12 +3,11 @@ include utils.pug
doctype html doctype html
html(lang="zh-CN") html(lang="zh-CN")
head head
block $$head title #{site_title || siteConfig && siteConfig.site_title || ''}
title #{site_title || $site && $site.site_title || ''} meta(name="description" content=site_description || siteConfig && siteConfig.site_description || '')
meta(name="description" content=site_description || $site && $site.site_description || '') meta(name="keywords" content=keywords || siteConfig && siteConfig.keywords || '')
meta(name="keywords" content=keywords || $site && $site.keywords || '') if siteConfig && siteConfig.site_favicon
if $site && $site.site_favicon link(rel="shortcut icon", href=siteConfig.site_favicon)
link(rel="shortcut icon", href=$site.site_favicon)
meta(charset="utf-8") meta(charset="utf-8")
meta(name="viewport" content="width=device-width, initial-scale=1") meta(name="viewport" content="width=device-width, initial-scale=1")
+css('lib/reset.css') +css('lib/reset.css')
@ -18,6 +17,7 @@ html(lang="zh-CN")
+js('lib/htmx.min.js') +js('lib/htmx.min.js')
+js('lib/tailwindcss.3.4.17.js') +js('lib/tailwindcss.3.4.17.js')
+js('lib/simplebar.min.js') +js('lib/simplebar.min.js')
block $$head
body body
noscript noscript
style. style.

4
src/views/layouts/utils.pug

@ -10,13 +10,13 @@ mixin css(url, extranl = false)
if extranl || url.startsWith('http') || url.startsWith('//') if extranl || url.startsWith('http') || url.startsWith('//')
link(rel="stylesheet" type="text/css" href=url) link(rel="stylesheet" type="text/css" href=url)
else 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) mixin js(url, extranl = false)
if extranl || url.startsWith('http') || url.startsWith('//') if extranl || url.startsWith('http') || url.startsWith('//')
script(type="text/javascript" src=url) script(type="text/javascript" src=url)
else 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) mixin link(href, name)
//- attributes == {class: "btn"} //- attributes == {class: "btn"}

3
src/views/page/extra/help.pug

@ -3,7 +3,8 @@ extends /layouts/empty.pug
block pageHead block pageHead
block pageContent block pageContent
.help.container(class=" bg-white rounded-[12px] shadow p-6 border border-gray-100") .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") 帮助中心 h1(class="text-3xl font-bold mb-6 text-center text-gray-800") 帮助中心
p(class="text-gray-600 mb-8 text-center text-lg") 欢迎使用帮助中心,这里为您提供完整的使用指南和问题解答 p(class="text-gray-600 mb-8 text-center text-lg") 欢迎使用帮助中心,这里为您提供完整的使用指南和问题解答

38
src/views/page/index/index.pug

@ -1,6 +1,7 @@
extends /layouts/empty.pug extends /layouts/empty.pug
block pageHead block pageHead
+js("https://unpkg.com/tiny-swiper@latest/lib/index.min.js")
mixin PeopleCared(name, role, desc, avatar) mixin PeopleCared(name, role, desc, avatar)
.bg-white.shadow-md.rounded-md.p-6.flex.items-center.gap-4 .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 block pageContent
.-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 .container
.grid.grid-cols-1.gap-4(class="md:grid-cols-4") .grid.grid-cols-1.gap-4(class="md:grid-cols-4")
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650')
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650')
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650')
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') +PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650')
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', '/public/static/bg.jpg') +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,
//- },
});

165
tests/db/BaseModel.test.js

@ -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('数据库操作失败')
}
})
})
})

258
tests/db/UserModel.test.js

@ -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)
})
})
})

212
tests/db/cache.test.js

@ -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')
})
})
})

142
tests/db/performance.test.js

@ -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')
})
})
})

159
tests/db/transaction.test.js

@ -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')
})
})
})
Loading…
Cancel
Save