20 changed files with 373 additions and 1084 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 139 B |
|
Before Width: | Height: | Size: 135 B |
|
Before Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 105 KiB |
@ -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 |
|||
@ -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('数据库操作失败') |
|||
} |
|||
}) |
|||
}) |
|||
}) |
|||
@ -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) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -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') |
|||
}) |
|||
}) |
|||
}) |
|||
@ -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') |
|||
}) |
|||
}) |
|||
}) |
|||
@ -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…
Reference in new issue