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