From d2e8df87f3e025a368ea1b861b0e65b9b1a85500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E4=BA=9A=E6=98=95?= <1549469775@qq.com> Date: Tue, 17 Jun 2025 17:41:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=B9=B6=E6=B7=BB=E5=8A=A0=E9=9D=99=E6=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=9C=8D=E5=8A=A1=E5=92=8C=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除旧的userController并重构用户表结构 - 添加静态文件服务中间件和路径解析工具 - 实现自定义路由系统支持路由组和参数 - 更新package.json依赖和模块别名配置 - 优化响应时间中间件添加彩色日志输出 - 添加jsconfig.json改善开发体验 --- .trae/.ignore | 0 bun.lockb | Bin 73078 -> 73460 bytes jsconfig.json | 23 +++ package.json | 4 +- public/aa.txt | 1 + src/controllers/userController.mjs | 23 --- .../20250616065041_create_users_table.mjs | 2 +- src/db/seeds/20250616071157_users_seed.mjs | 6 +- src/main.js | 85 +++++++--- src/plugins/ResponseTime/index.js | 35 +++- src/plugins/Send/index.js | 185 +++++++++++++++++++++ src/plugins/Send/resolve-path.js | 74 +++++++++ src/plugins/install.js | 16 ++ src/utils/router.js | 110 ++++++++++++ 14 files changed, 501 insertions(+), 63 deletions(-) create mode 100644 .trae/.ignore create mode 100644 jsconfig.json create mode 100644 public/aa.txt delete mode 100644 src/controllers/userController.mjs create mode 100644 src/plugins/Send/index.js create mode 100644 src/plugins/Send/resolve-path.js create mode 100644 src/utils/router.js diff --git a/.trae/.ignore b/.trae/.ignore new file mode 100644 index 0000000..e69de29 diff --git a/bun.lockb b/bun.lockb index 42889961bfb925d380d051c67f1eddb4950c1b6e..771d03bed36d95fd712a53975bc909cf3b5f8f5f 100644 GIT binary patch delta 10163 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 E0%XtMxc~qF delta 9864 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 { return knex.schema.createTable("users", function (table) { table.increments("id").primary() // 自增主键 - table.string("name", 100).notNullable() // 字符串字段(最大长度100) + table.string("username", 100).notNullable() // 字符串字段(最大长度100) table.string("email", 100).unique().notNullable() // 唯一邮箱 table.integer("age").unsigned() // 无符号整数 table.timestamp("created_at").defaultTo(knex.fn.now()) // 创建时间 diff --git a/src/db/seeds/20250616071157_users_seed.mjs b/src/db/seeds/20250616071157_users_seed.mjs index 521b6b2..9cb6728 100644 --- a/src/db/seeds/20250616071157_users_seed.mjs +++ b/src/db/seeds/20250616071157_users_seed.mjs @@ -1,7 +1,7 @@ export const seed = async knex => { // 检查表是否存在 const tables = await knex.raw(` - SELECT name FROM sqlite_master WHERE type='table' AND name='users' + SELECT name FROM sqlite_master WHERE type='table' AND username='users' `) if (tables.length === 0) { @@ -13,7 +13,7 @@ export const seed = async knex => { // Inserts seed entries await knex("users").insert([ - { name: "Alice", email: "alice@example.com" }, - { name: "Bob", email: "bob@example.com" }, + { username: "Alice", email: "alice@example.com" }, + { username: "Bob", email: "bob@example.com" }, ]) } diff --git a/src/main.js b/src/main.js index f38d0ae..bdc064a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,38 +1,71 @@ -import "./logger" -import "module-alias/register" -import Koa from "koa" -import os from "os" -import LoadPlugins from "./plugins/install" -import UserModel from "./db/models/UserModel" +import './logger.js'; +import Koa from 'koa'; +import os from 'os'; import log4js from "log4js" +import LoadPlugins from "./plugins/install.js" +import Router from './utils/router.js'; const logger = log4js.getLogger() -const app = new Koa() +const app = new Koa(); LoadPlugins(app) -app.use(async ctx => { - ctx.body = await UserModel.findAll() -}) -app.on("error", err => { - logger.error("server error", err) -}) +const router = new Router({ prefix: '/api' }); + +// 基础路由 +router.get('/hello', (ctx) => { + ctx.body = 'Hello World'; +}); + +// 参数路由 +router.get('/user/:id', (ctx) => { + ctx.body = `User ID: ${ctx.params.id}`; +}); + +// 路由组 +router.group('/v1', (v1) => { + v1.use((ctx, next) => { + ctx.set('X-API-Version', 'v1'); + return next(); + }); + v1.get('/status', (ctx) => ctx.body = 'OK'); +}); -const server = app.listen(3000, () => { - const port = server.address().port - const getLocalIP = () => { - const interfaces = os.networkInterfaces() - for (const name of Object.keys(interfaces)) { - for (const iface of interfaces[name]) { - if (iface.family === "IPv4" && !iface.internal) { - return iface.address - } - } +app.use(router.middleware()); + +// 错误处理中间件 +app.use(async (ctx, next) => { + try { + await next(); + if (ctx.status === 404) { + ctx.status = 404; + ctx.body = { success: false, error: 'Resource not found' }; + } + } catch (err) { + ctx.status = err.statusCode || 500; + ctx.body = { success: false, error: err.message || 'Internal server error' }; + } +}); + +const PORT = process.env.PORT || 3000; + +const server = app.listen(PORT, () => { + const port = server.address().port + const getLocalIP = () => { + const interfaces = os.networkInterfaces() + for (const name of Object.keys(interfaces)) { + for (const iface of interfaces[name]) { + if (iface.family === "IPv4" && !iface.internal) { + return iface.address } - return "localhost" + } } - const localIP = getLocalIP() - logger.trace(`服务器运行在: http://${localIP}:${port}`) + return "localhost" + } + const localIP = getLocalIP() + logger.trace(`服务器运行在: http://${localIP}:${port}`) }) + +export default app; diff --git a/src/plugins/ResponseTime/index.js b/src/plugins/ResponseTime/index.js index de0ad8d..a8e7443 100644 --- a/src/plugins/ResponseTime/index.js +++ b/src/plugins/ResponseTime/index.js @@ -1,13 +1,30 @@ -import log4js from "log4js" +import log4js from "log4js"; -const logger = log4js.getLogger() +// ANSI颜色代码常量定义 +const COLORS = { + REQ: '\x1b[36m', // 青色 - 请求标记 + RES: '\x1b[32m', // 绿色 - 响应标记 + TIME: '\x1b[33m', // 黄色 - 响应时间 + METHOD: '\x1b[1m', // 加粗 - HTTP方法 + RESET: '\x1b[0m' // 重置颜色 +}; +const logger = log4js.getLogger(); + +/** + * 响应时间记录中间件 + * 为请求和响应添加彩色日志标记,并记录处理时间 + * @param {Object} ctx - Koa上下文对象 + * @param {Function} next - Koa中间件链函数 + */ export default async (ctx, next) => { - logger.debug("::in:: %s %s", ctx.method, ctx.path) - const start = Date.now() - await next() - const ms = Date.now() - start - ctx.set("X-Response-Time", `${ms}ms`) - const rt = ctx.response.get("X-Response-Time") - logger.debug(`::out:: takes ${rt} for ${ctx.method} ${ctx.url}`) + // 彩色请求日志:青色标记 + 加粗方法名 + logger.debug(`${COLORS.REQ}::req::${COLORS.RESET} ${COLORS.METHOD}%s${COLORS.RESET} %s`, ctx.method, ctx.path); + const start = Date.now(); + await next(); + const ms = Date.now() - start; + ctx.set("X-Response-Time", `${ms}ms`); + // 彩色响应日志:绿色标记 + 黄色响应时间 + 加粗方法名 + logger.debug(`${COLORS.RES}::res::${COLORS.RESET} takes ${COLORS.TIME}%s${COLORS.RESET} for ${COLORS.METHOD}%s${COLORS.RESET} %s`, + ctx.response.get("X-Response-Time"), ctx.method, ctx.url); } diff --git a/src/plugins/Send/index.js b/src/plugins/Send/index.js new file mode 100644 index 0000000..1502d3f --- /dev/null +++ b/src/plugins/Send/index.js @@ -0,0 +1,185 @@ +/** + * koa-send@5.0.1 转换为ES Module版本 + * 静态资源服务中间件 + */ +import fs from 'fs'; +import { promisify } from 'util'; +import logger from 'log4js'; +import resolvePath from './resolve-path.js'; +import createError from 'http-errors'; +import assert from 'assert'; +import { normalize, basename, extname, resolve, parse, sep } from 'path'; +import { fileURLToPath } from 'url'; +import path from "path" + +// 转换为ES Module格式 +const log = logger.getLogger('koa-send'); +const stat = promisify(fs.stat); +const access = promisify(fs.access); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * 检查文件是否存在 + * @param {string} path - 文件路径 + * @returns {Promise} 文件是否存在 + */ +async function exists(path) { + try { + await access(path); + return true; + } catch (e) { + return false; + } +} + +/** + * 发送文件给客户端 + * @param {Context} ctx - Koa上下文对象 + * @param {String} path - 文件路径 + * @param {Object} [opts] - 配置选项 + * @returns {Promise} - 异步Promise + */ +async function send(ctx, path, opts = {}) { + assert(ctx, 'koa context required'); + assert(path, 'pathname required'); + + // 移除硬编码的public目录,要求必须通过opts.root配置 + const root = opts.root; + if (!root) { + throw new Error('Static root directory must be configured via opts.root'); + } + const trailingSlash = path[path.length - 1] === '/'; + path = path.substr(parse(path).root.length); + const index = opts.index || 'index.html'; + const maxage = opts.maxage || opts.maxAge || 0; + const immutable = opts.immutable || false; + const hidden = opts.hidden || false; + const format = opts.format !== false; + const extensions = Array.isArray(opts.extensions) ? opts.extensions : false; + const brotli = opts.brotli !== false; + const gzip = opts.gzip !== false; + const setHeaders = opts.setHeaders; + + if (setHeaders && typeof setHeaders !== 'function') { + throw new TypeError('option setHeaders must be function'); + } + + // 解码路径 + path = decode(path); + if (path === -1) return ctx.throw(400, 'failed to decode'); + + // 索引文件支持 + if (index && trailingSlash) path += index; + + path = resolvePath(root, path); + + // 隐藏文件支持 + if (!hidden && isHidden(root, path)) return; + + let encodingExt = ''; + // 尝试提供压缩文件 + if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (await exists(path + '.br'))) { + path = path + '.br'; + ctx.set('Content-Encoding', 'br'); + ctx.res.removeHeader('Content-Length'); + encodingExt = '.br'; + } else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (await exists(path + '.gz'))) { + path = path + '.gz'; + ctx.set('Content-Encoding', 'gzip'); + ctx.res.removeHeader('Content-Length'); + encodingExt = '.gz'; + } + + // 尝试添加文件扩展名 + if (extensions && !/\./.exec(basename(path))) { + const list = [].concat(extensions); + for (let i = 0; i < list.length; i++) { + let ext = list[i]; + if (typeof ext !== 'string') { + throw new TypeError('option extensions must be array of strings or false'); + } + if (!/^\./.exec(ext)) ext = `.${ext}`; + if (await exists(`${path}${ext}`)) { + path = `${path}${ext}`; + break; + } + } + } + + // 获取文件状态 + let stats; + try { + stats = await stat(path); + + // 处理目录 + if (stats.isDirectory()) { + if (format && index) { + path += `/${index}`; + stats = await stat(path); + } else { + return; + } + } + } catch (err) { + const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; + if (notfound.includes(err.code)) { + throw createError(404, err); + } + err.status = 500; + throw err; + } + + if (setHeaders) setHeaders(ctx.res, path, stats); + + // 设置响应头 + ctx.set('Content-Length', stats.size); + if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString()); + if (!ctx.response.get('Cache-Control')) { + const directives = [`max-age=${(maxage / 1000) | 0}`]; + if (immutable) directives.push('immutable'); + ctx.set('Cache-Control', directives.join(',')); + } + if (!ctx.type) ctx.type = type(path, encodingExt); + ctx.body = fs.createReadStream(path); + + return path; +} + +/** + * 检查是否为隐藏文件 + * @param {string} root - 根目录 + * @param {string} path - 文件路径 + * @returns {boolean} 是否为隐藏文件 + */ +function isHidden(root, path) { + path = path.substr(root.length).split(sep); + for (let i = 0; i < path.length; i++) { + if (path[i][0] === '.') return true; + } + return false; +} + +/** + * 获取文件类型 + * @param {string} file - 文件路径 + * @param {string} ext - 编码扩展名 + * @returns {string} 文件MIME类型 + */ +function type(file, ext) { + return ext !== '' ? extname(basename(file, ext)) : extname(file); +} + +/** + * 解码URL路径 + * @param {string} path - 需要解码的路径 + * @returns {string|number} 解码后的路径或错误代码 + */ +function decode(path) { + try { + return decodeURIComponent(path); + } catch (err) { + return -1; + } +} + +export default send; diff --git a/src/plugins/Send/resolve-path.js b/src/plugins/Send/resolve-path.js new file mode 100644 index 0000000..9c6dce6 --- /dev/null +++ b/src/plugins/Send/resolve-path.js @@ -0,0 +1,74 @@ +/*! + * resolve-path + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015-2018 Douglas Christopher Wilson + * MIT Licensed + */ + +/** + * ES Module 转换版本 + * 路径解析工具,防止路径遍历攻击 + */ +import createError from 'http-errors'; +import { join, normalize, resolve, sep } from 'path'; +import pathIsAbsolute from 'path-is-absolute'; + +/** + * 模块变量 + * @private + */ +const UP_PATH_REGEXP = /(?:^|[\/])\.\.(?:[\/]|$)/; + +/** + * 解析相对路径到根路径 + * @param {string} rootPath - 根目录路径 + * @param {string} relativePath - 相对路径 + * @returns {string} 解析后的绝对路径 + * @public + */ +function resolvePath(rootPath, relativePath) { + let path = relativePath; + let root = rootPath; + + // root是可选的,类似于root.resolve + if (arguments.length === 1) { + path = rootPath; + root = process.cwd(); + } + + if (root == null) { + throw new TypeError('argument rootPath is required'); + } + + if (typeof root !== 'string') { + throw new TypeError('argument rootPath must be a string'); + } + + if (path == null) { + throw new TypeError('argument relativePath is required'); + } + + if (typeof path !== 'string') { + throw new TypeError('argument relativePath must be a string'); + } + + // 包含NULL字节是恶意的 + if (path.indexOf('\0') !== -1) { + throw createError(400, 'Malicious Path'); + } + + // 路径绝不能是绝对路径 + if (pathIsAbsolute.posix(path) || pathIsAbsolute.win32(path)) { + throw createError(400, 'Malicious Path'); + } + + // 路径超出根目录 + if (UP_PATH_REGEXP.test(normalize('.' + sep + path))) { + throw createError(403); + } + + // 拼接相对路径 + return normalize(join(resolve(root), path)); +} + +export default resolvePath; diff --git a/src/plugins/install.js b/src/plugins/install.js index 6eb43a1..7d3daf0 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -1,5 +1,21 @@ import ResponseTime from "./ResponseTime"; +import Send from "./Send/index.js"; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; +import path from "path" + + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const publicPath = resolve(__dirname, '../../public'); export default (app)=>{ app.use(ResponseTime) + app.use(async (ctx, next) => { + try { + await Send(ctx, ctx.path, { root: publicPath }); + } catch (err) { + if (err.status !== 404) throw err; + } + await next(); + }) } \ No newline at end of file diff --git a/src/utils/router.js b/src/utils/router.js new file mode 100644 index 0000000..d302aa8 --- /dev/null +++ b/src/utils/router.js @@ -0,0 +1,110 @@ +import { pathToRegexp } from 'path-to-regexp'; + +class Router { + /** + * 初始化路由实例 + * @param {Object} options - 路由配置 + * @param {string} options.prefix - 全局路由前缀 + */ + constructor(options = {}) { + this.prefix = options.prefix || ''; + this.routes = { get: [], post: [], put: [], delete: [] }; + this.middlewares = []; + } + + /** + * 注册中间件 + * @param {Function} middleware - 中间件函数 + */ + use(middleware) { + this.middlewares.push(middleware); + } + + /** + * 注册GET路由 + * @param {string} path - 路由路径 + * @param {Function} handler - 处理函数 + */ + get(path, handler) { + this._registerRoute('get', path, handler); + } + + /** + * 注册POST路由 + * @param {string} path - 路由路径 + * @param {Function} handler - 处理函数 + */ + post(path, handler) { + this._registerRoute('post', path, handler); + } + + /** + * 创建路由组 + * @param {string} prefix - 组内路由前缀 + * @param {Function} callback - 组路由注册回调 + */ + group(prefix, callback) { + const groupRouter = new Router({ prefix: this.prefix + prefix }); + callback(groupRouter); + // 合并组路由到当前路由 + Object.keys(groupRouter.routes).forEach(method => { + this.routes[method].push(...groupRouter.routes[method]); + }); + this.middlewares.push(...groupRouter.middlewares); + } + + /** + * 生成Koa中间件 + * @returns {Function} Koa中间件函数 + */ + middleware() { + return async (ctx, next) => { + // 执行全局中间件 + for (const middleware of this.middlewares) { + await middleware(ctx, next); + } + + const { method, path } = ctx; + const route = this._matchRoute(method.toLowerCase(), path); + + if (route) { + ctx.params = route.params; + await route.handler(ctx, next); + } else { + await next(); + } + }; + } + + /** + * 内部路由注册方法 + * @private + */ + _registerRoute(method, path, handler) { + const fullPath = this.prefix + path; + const keys = []; + const regexp = pathToRegexp(fullPath, keys).regexp; + this.routes[method].push({ path: fullPath, regexp, keys, handler }); + } + + /** + * 匹配路由 + * @private + */ + _matchRoute(method, currentPath) { + const routes = this.routes[method] || []; + for (const route of routes) { + const match = route.regexp.exec(currentPath); + if (match) { + const params = {}; + for (let i = 1; i < match.length; i++) { + params[route.keys[i - 1].name] = match[i] || ''; + } + return { ...route, params }; + } + } + return null; + } +} + +export default Router; \ No newline at end of file