From f3a9a9b53eb11b8315593c835b873b686c32e824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E4=BA=9A=E6=98=95?= <1549469775@qq.com> Date: Wed, 24 Sep 2025 17:27:24 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=A1=B9=E7=9B=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E5=88=A0=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E6=8E=A7=E5=88=B6=E5=99=A8=E5=92=8C=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=EF=BC=8C=E4=BC=98=E5=8C=96=E8=B7=AF=E7=94=B1=E5=92=8C?= =?UTF-8?q?=E4=B8=AD=E9=97=B4=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 AuthController 和 RouteCacheController 控制器,简化 API 结构 - 移除多余的服务文件,提升代码可维护性 - 更新路由配置,确保新控制器的注册和中间件的使用 - 在登录页面中引入新的用户名和密码验证组件,增强用户体验 - 更新样式文件,改善登录页面的视觉效果 --- .vscode/settings.json | 3 +- jsconfig.json | 3 - package.json | 2 - src/base/BaseController.js | 4 +- src/config/index.js | 53 --- src/controllers/Api/ApiController.js | 60 --- src/controllers/Api/AuthController.js | 25 -- src/controllers/Api/JobController.js | 47 --- src/controllers/Api/RouteCacheController.js | 175 --------- src/controllers/Page/AuthController.js | 44 --- src/controllers/Page/CommonController.js | 62 --- src/db/index.js | 196 +++++----- src/db/models/BaseModel.js | 238 +++++------- src/db/models/UserModel.js | 39 +- src/main.js | 3 + src/middlewares/Auth/index.js | 69 ++-- src/middlewares/Controller/index.js | 98 +++++ src/middlewares/RoutePerformance/index.js | 304 --------------- src/middlewares/errorHandler/index.js | 1 - src/middlewares/install.js | 62 ++- src/modules/Api/controller/index.js | 60 +++ src/modules/Auth/controller/index.js | 75 ++++ src/modules/Auth/services/index.js | 88 +++++ src/modules/Contact/controller/index.js | 0 src/modules/Contact/services/index.js | 511 +++++++++++++++++++++++++ src/modules/Index/controller/index.js | 61 +++ src/modules/Index/services/index.js | 0 src/modules/Job/controller/index.js | 47 +++ src/modules/Job/services/index.js | 18 + src/modules/SiteConfig/services/index.js | 513 +++++++++++++++++++++++++ src/services/ArticleService.js | 563 ---------------------------- src/services/AuthService.js | 84 ----- src/services/BookmarkService.js | 492 ------------------------ src/services/ContactService.js | 511 ------------------------- src/services/JobService.js | 18 - src/services/SiteConfigService.js | 513 ------------------------- src/services/UserService.js | 415 -------------------- src/services/index.js | 84 ----- src/utils/ForRegister.js | 222 ----------- src/utils/cache/RouteCache.js | 388 ------------------- src/utils/error/ApiError.js | 19 + src/utils/error/CommonError.js | 11 +- src/utils/router.js | 77 +--- src/utils/router/RouteAuth.js | 26 -- src/utils/test/ConfigTest.js | 198 ---------- src/utils/test/RouteCacheTest.js | 222 ----------- src/utils/user.js | 20 - src/views/page/login/_ui/password.pug | 12 + src/views/page/login/_ui/username.pug | 16 +- src/views/page/login/index.pug | 44 ++- vite.config.ts | 1 - 51 files changed, 1894 insertions(+), 4903 deletions(-) delete mode 100644 src/controllers/Api/ApiController.js delete mode 100644 src/controllers/Api/AuthController.js delete mode 100644 src/controllers/Api/JobController.js delete mode 100644 src/controllers/Api/RouteCacheController.js delete mode 100644 src/controllers/Page/AuthController.js delete mode 100644 src/controllers/Page/CommonController.js create mode 100644 src/middlewares/Controller/index.js delete mode 100644 src/middlewares/RoutePerformance/index.js create mode 100644 src/modules/Api/controller/index.js create mode 100644 src/modules/Auth/controller/index.js create mode 100644 src/modules/Auth/services/index.js create mode 100644 src/modules/Contact/controller/index.js create mode 100644 src/modules/Contact/services/index.js create mode 100644 src/modules/Index/controller/index.js create mode 100644 src/modules/Index/services/index.js create mode 100644 src/modules/Job/controller/index.js create mode 100644 src/modules/Job/services/index.js create mode 100644 src/modules/SiteConfig/services/index.js delete mode 100644 src/services/ArticleService.js delete mode 100644 src/services/AuthService.js delete mode 100644 src/services/BookmarkService.js delete mode 100644 src/services/ContactService.js delete mode 100644 src/services/JobService.js delete mode 100644 src/services/SiteConfigService.js delete mode 100644 src/services/UserService.js delete mode 100644 src/services/index.js delete mode 100644 src/utils/ForRegister.js delete mode 100644 src/utils/cache/RouteCache.js create mode 100644 src/utils/error/ApiError.js delete mode 100644 src/utils/router/RouteAuth.js delete mode 100644 src/utils/test/ConfigTest.js delete mode 100644 src/utils/test/RouteCacheTest.js delete mode 100644 src/utils/user.js create mode 100644 src/views/page/login/_ui/password.pug diff --git a/.vscode/settings.json b/.vscode/settings.json index 6bb4812..264029e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,5 +31,6 @@ "debug.showBreakpointsInOverviewRuler": true, "debug.showInlineBreakpointCandidates": true, "bun.enable": true, - "bun.path": "bun" + "bun.path": "bun", + "CodeFree.index": true } \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json index 8b7bdc7..a781a3a 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -13,9 +13,6 @@ ], "utils/*": [ "src/utils/*" - ], - "services/*": [ - "src/services/*" ] }, "module": "ESNext", diff --git a/package.json b/package.json index d242066..6782898 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,7 @@ "seed": "npx knex seed:run ", "dev:init": "bun run scripts/init.js", "init": "cross-env NODE_ENV=production bun run scripts/init.js", - "test:env": "bun run scripts/test-env-validation.js", "test": "bun test", - "test:db": "bun test tests/db", "test:db:run": "bun run scripts/run-db-tests.js", "test:db:benchmark": "bun run scripts/db-benchmark.js" }, diff --git a/src/base/BaseController.js b/src/base/BaseController.js index a464c97..a41ffb7 100644 --- a/src/base/BaseController.js +++ b/src/base/BaseController.js @@ -220,8 +220,8 @@ class BaseController { */ async render(ctx, template, data = {}, options = {}) { const defaultOptions = { - includeSite: true, - includeUser: true, + // includeSite: true, + // includeUser: true, ...options } diff --git a/src/config/index.js b/src/config/index.js index 0067798..2b0beb8 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,56 +1,3 @@ export default { base: "/", - - // 路由缓存配置 - routeCache: { - // 是否启用路由缓存(生产环境建议启用) - enabled: process.env.NODE_ENV === 'production', - - // 各类缓存的最大条目数 - maxMatchCacheSize: 1000, // 路由匹配缓存 - maxControllerCacheSize: 100, // 控制器实例缓存 - maxMiddlewareCacheSize: 200, // 中间件组合缓存 - maxRegistrationCacheSize: 50, // 路由注册缓存 - - // 缓存清理配置 - cleanupInterval: 5 * 60 * 1000, // 清理间隔(5分钟) - - // 性能监控配置 - performance: { - enabled: process.env.NODE_ENV === 'production', - windowSize: 100, // 监控窗口大小 - slowRouteThreshold: 500, // 慢路由阈值(毫秒) - cleanupInterval: 5 * 60 * 1000 // 清理间隔 - } - }, - - // 路由性能监控配置 - routePerformance: { - // 是否启用性能监控 - enabled: process.env.NODE_ENV === 'production' || process.env.PERFORMANCE_MONITOR === 'true', - - // 监控窗口大小(保留最近N次请求的数据) - windowSize: parseInt(process.env.PERFORMANCE_WINDOW_SIZE) || 100, - - // 慢路由阈值(毫秒) - slowRouteThreshold: parseInt(process.env.SLOW_ROUTE_THRESHOLD) || 500, - - // 自动清理间隔(毫秒) - cleanupInterval: parseInt(process.env.PERFORMANCE_CLEANUP_INTERVAL) || 5 * 60 * 1000, - - // 性能数据保留时间(毫秒) - dataRetentionTime: parseInt(process.env.PERFORMANCE_DATA_RETENTION) || 10 * 60 * 1000, - - // 最小分析数据量(少于此数量不进行性能分析) - minAnalysisDataCount: parseInt(process.env.MIN_ANALYSIS_DATA_COUNT) || 10, - - // 缓存命中率警告阈值(百分比) - cacheHitRateWarningThreshold: parseFloat(process.env.CACHE_HIT_RATE_WARNING) || 0.5, - - // 是否启用自动优化建议 - enableOptimizationSuggestions: process.env.ENABLE_OPTIMIZATION_SUGGESTIONS !== 'false', - - // 性能报告的最大路由数量 - maxRouteReportCount: parseInt(process.env.MAX_ROUTE_REPORT_COUNT) || 50 - } } diff --git a/src/controllers/Api/ApiController.js b/src/controllers/Api/ApiController.js deleted file mode 100644 index 0551785..0000000 --- a/src/controllers/Api/ApiController.js +++ /dev/null @@ -1,60 +0,0 @@ -import { R } from "utils/helper.js" -import Router from "utils/router.js" - -class AuthController { - constructor() {} - - /** - * 通用请求函数:依次请求网址数组,返回第一个成功的响应及其类型 - * @param {string[]} urls - * @returns {Promise<{type: string, data: any}>} - */ - async fetchFirstSuccess(urls) { - for (const url of urls) { - try { - const res = await fetch(url, { method: "get", mode: "cors", redirect: "follow" }) - if (!res.ok) continue - const contentType = res.headers.get("content-type") || "" - let data, type - if (contentType.includes("application/json")) { - data = await res.json() - type = "json" - } else if (contentType.includes("text/")) { - data = await res.text() - type = "text" - } else { - data = await res.blob() - type = "blob" - } - return { type, data } - } catch (e) { - // ignore and try next url - } - } - throw new Error("All requests failed") - } - - async random(ctx) { - const { type, data } = await this.fetchFirstSuccess(["https://api.miaomc.cn/image/get"]) - if (type === "blob") { - ctx.set("Content-Type", "image/jpeg") - // 下载 - // ctx.set("Content-Disposition", "attachment; filename=random.jpg") - ctx.body = data - } else { - R.response(R.ERROR, "Failed to fetch image") - } - } - - /** - * 路由注册 - */ - static createRoutes() { - const controller = new AuthController() - const router = new Router({ prefix: "/api/pics" }) - router.get("/random", controller.random.bind(controller), { auth: "try" }) - return router - } -} - -export default AuthController diff --git a/src/controllers/Api/AuthController.js b/src/controllers/Api/AuthController.js deleted file mode 100644 index a4c61fd..0000000 --- a/src/controllers/Api/AuthController.js +++ /dev/null @@ -1,25 +0,0 @@ -import { R } from "utils/helper.js" -import Router from "utils/router.js" -import BaseController from "@/base/BaseController.js" - -class AuthController extends BaseController { - constructor() { - super() - } - - async loginPost(ctx) { - return this.success(ctx, null, "登录成功") - } - - /** - * 路由注册 - */ - static createRoutes() { - const controller = new AuthController() - const router = new Router({ prefix: "/api/auth", auth: "try" }) - router.post("/login", controller.handleRequest(controller.loginPost)) - return router - } -} - -export default AuthController diff --git a/src/controllers/Api/JobController.js b/src/controllers/Api/JobController.js deleted file mode 100644 index a691f3c..0000000 --- a/src/controllers/Api/JobController.js +++ /dev/null @@ -1,47 +0,0 @@ -// Job Controller 示例:如何调用 service 层动态控制和查询定时任务 -import JobService from "services/JobService.js" -import { R } from "utils/helper.js" -import Router from "utils/router.js" - -class JobController { - constructor() { - this.jobService = new JobService() - } - - async list(ctx) { - const data = this.jobService.listJobs() - R.response(R.SUCCESS,data) - } - - async start(ctx) { - const { id } = ctx.params - this.jobService.startJob(id) - R.response(R.SUCCESS,null, `${id} 任务已启动`) - } - - async stop(ctx) { - const { id } = ctx.params - this.jobService.stopJob(id) - R.response(R.SUCCESS,null, `${id} 任务已停止`) - } - - async updateCron(ctx) { - const { id } = ctx.params - const { cronTime } = ctx.request.body - this.jobService.updateJobCron(id, cronTime) - R.response(R.SUCCESS,null, `${id} 任务频率已修改`) - } - - static createRoutes() { - const controller = new JobController() - const router = new Router({ prefix: "/api/jobs", auth: true }) - router.get("/", controller.list.bind(controller)) - router.get("/", controller.list.bind(controller)) - router.post("/start/:id", controller.start.bind(controller)) - router.post("/stop/:id", controller.stop.bind(controller)) - router.post("/update/:id", controller.updateCron.bind(controller)) - return router - } -} - -export default JobController diff --git a/src/controllers/Api/RouteCacheController.js b/src/controllers/Api/RouteCacheController.js deleted file mode 100644 index 76b8fb4..0000000 --- a/src/controllers/Api/RouteCacheController.js +++ /dev/null @@ -1,175 +0,0 @@ -import BaseController from "@/base/BaseController.js" -import Router from "utils/router.js" -import routeCache from "utils/cache/RouteCache.js" - -/** - * 路由缓存管理控制器 - * 提供缓存监控、清理等管理功能 - */ -class RouteCacheController extends BaseController { - constructor() { - super() - } - - /** - * 获取缓存统计信息 - */ - async getStats(ctx) { - const stats = routeCache.getStats() - return this.success(ctx, stats, "获取缓存统计信息成功") - } - - /** - * 清除所有缓存 - */ - async clearAll(ctx) { - routeCache.clearAll() - return this.success(ctx, null, "所有缓存已清除") - } - - /** - * 清除路由匹配缓存 - */ - async clearRouteMatches(ctx) { - routeCache.clearRouteMatches() - return this.success(ctx, null, "路由匹配缓存已清除") - } - - /** - * 清除控制器实例缓存 - */ - async clearControllers(ctx) { - routeCache.clearControllers() - return this.success(ctx, null, "控制器实例缓存已清除") - } - - /** - * 清除中间件组合缓存 - */ - async clearMiddlewares(ctx) { - routeCache.clearMiddlewares() - return this.success(ctx, null, "中间件组合缓存已清除") - } - - /** - * 清除路由注册缓存 - */ - async clearRegistrations(ctx) { - routeCache.clearRegistrations() - return this.success(ctx, null, "路由注册缓存已清除") - } - - /** - * 根据文件路径清除相关缓存 - */ - async clearByFile(ctx) { - const data = this.validateParams(ctx, { - filePath: { required: true, label: '文件路径' } - }) - - routeCache.clearByFile(data.filePath) - return this.success(ctx, null, `文件 ${data.filePath} 相关缓存已清除`) - } - - /** - * 更新缓存配置 - */ - async updateConfig(ctx) { - const data = this.validateParams(ctx, { - enabled: { type: 'boolean', label: '启用状态' }, - maxMatchCacheSize: { type: 'number', label: '路由匹配缓存最大大小' }, - maxControllerCacheSize: { type: 'number', label: '控制器缓存最大大小' }, - maxMiddlewareCacheSize: { type: 'number', label: '中间件缓存最大大小' }, - maxRegistrationCacheSize: { type: 'number', label: '注册缓存最大大小' } - }) - - // 过滤掉undefined值 - const config = Object.fromEntries( - Object.entries(data).filter(([_, value]) => value !== undefined) - ) - - routeCache.updateConfig(config) - return this.success(ctx, routeCache.getStats(), "缓存配置已更新") - } - - /** - * 启用缓存 - */ - async enable(ctx) { - routeCache.enable() - return this.success(ctx, null, "路由缓存已启用") - } - - /** - * 禁用缓存 - */ - async disable(ctx) { - routeCache.disable() - return this.success(ctx, null, "路由缓存已禁用") - } - - /** - * 获取缓存健康状态 - */ - async getHealth(ctx) { - const stats = routeCache.getStats() - - // 简单的健康检查逻辑 - const health = { - status: 'healthy', - issues: [], - recommendations: [] - } - - // 检查命中率 - const overallHitRate = parseFloat(stats.hitRate) - if (overallHitRate < 50) { - health.status = 'warning' - health.issues.push('总体缓存命中率较低') - health.recommendations.push('考虑调整缓存策略或检查路由模式') - } - - // 检查缓存大小 - Object.entries(stats.caches).forEach(([cacheType, cacheStats]) => { - if (cacheStats.size > 500) { - health.issues.push(`${cacheType} 缓存大小过大 (${cacheStats.size})`) - health.recommendations.push(`考虑清理 ${cacheType} 缓存或调整最大大小`) - } - }) - - if (health.issues.length > 0 && health.status === 'healthy') { - health.status = 'warning' - } - - return this.success(ctx, { ...stats, health }, "获取缓存健康状态成功") - } - - /** - * 创建路由 - */ - static createRoutes() { - const controller = new RouteCacheController() - const router = new Router({ prefix: '/api/system/route-cache' }) - - // 缓存统计 - router.get('/stats', controller.handleRequest(controller.getStats), { auth: true }) - router.get('/health', controller.handleRequest(controller.getHealth), { auth: true }) - - // 缓存清理 - router.delete('/clear/all', controller.handleRequest(controller.clearAll), { auth: true }) - router.delete('/clear/routes', controller.handleRequest(controller.clearRouteMatches), { auth: true }) - router.delete('/clear/controllers', controller.handleRequest(controller.clearControllers), { auth: true }) - router.delete('/clear/middlewares', controller.handleRequest(controller.clearMiddlewares), { auth: true }) - router.delete('/clear/registrations', controller.handleRequest(controller.clearRegistrations), { auth: true }) - router.delete('/clear/file', controller.handleRequest(controller.clearByFile), { auth: true }) - - // 缓存配置 - router.put('/config', controller.handleRequest(controller.updateConfig), { auth: true }) - router.post('/enable', controller.handleRequest(controller.enable), { auth: true }) - router.post('/disable', controller.handleRequest(controller.disable), { auth: true }) - - return router - } -} - -export default RouteCacheController \ No newline at end of file diff --git a/src/controllers/Page/AuthController.js b/src/controllers/Page/AuthController.js deleted file mode 100644 index 2ddfc7a..0000000 --- a/src/controllers/Page/AuthController.js +++ /dev/null @@ -1,44 +0,0 @@ -import Router from "utils/router.js" -import { logger } from "@/logger.js" -import BaseController from "@/base/BaseController.js" - -export default class AuthController extends BaseController { - constructor() { - super() - } - - // 首页 - async loginGet(ctx) { - return this.render(ctx, "page/login/index", {}) - } - - async loginPost(ctx) { - console.log(ctx.request.body); - return this.success(ctx, null, "登录成功") - } - - async validateUsername(ctx) { - return this.render(ctx, "page/login/_ui/username", { - value: ctx.request.body.username, - error: undefined - }) - } - - /** - * 创建基础页面相关路由 - * @returns {Router} 路由实例 - */ - static createRoutes() { - const controller = new AuthController() - const router = new Router({ auth: false }) - - // 首页 - router.get("", controller.handleRequest(controller.loginGet)) - router.get("/login", controller.handleRequest(controller.loginGet)) - router.post("/login", controller.handleRequest(controller.loginPost)) - - router.post("/login/validate/username", controller.handleRequest(controller.validateUsername)) - - return router - } -} diff --git a/src/controllers/Page/CommonController.js b/src/controllers/Page/CommonController.js deleted file mode 100644 index a9e77a2..0000000 --- a/src/controllers/Page/CommonController.js +++ /dev/null @@ -1,62 +0,0 @@ -import Router from "utils/router.js" -import { logger } from "@/logger.js" -import BaseController from "@/base/BaseController.js" -import SiteConfigModel from '@/db/models/SiteConfigModel.js' - -export default class CommonController extends BaseController { - constructor() { - super() - } - - pageGet(...args) { - return (ctx) => { - return ctx.render(...args) - } - } - - // 首页 - async indexGet(ctx) { - // 可以在这里添加一些需要用户信息的逻辑 - // 例如获取用户相关的统计数据等 - const user = ctx.state.user || null; - - // 示例数据,实际项目中可以从数据库获取 - const stats = { - articles: 1234, - users: 567, - categories: 89, - responseTime: "24h" - }; - - return await ctx.render( - "page/index/index", - { - stats, - // 其他需要传递给模板的数据 - } - ) - } - - /** - * 创建基础页面相关路由 - * @returns {Router} 路由实例 - */ - static createRoutes() { - const controller = new CommonController() - const router = new Router({ auth: "try" }) - - // 首页 - router.get("", controller.handleRequest(controller.indexGet)) - router.get("/", controller.handleRequest(controller.indexGet)) - // router.get("/about", controller.handleRequest(controller.pageGet("page/about/index"))) - router.get("/contact", controller.handleRequest(controller.pageGet("page/extra/contact"))) - router.get("/faq", controller.handleRequest(controller.pageGet("page/extra/faq"))) - router.get("/feedback", controller.handleRequest(controller.pageGet("page/extra/feedback"))) - router.get("/help", controller.handleRequest(controller.pageGet("page/extra/help"))) - router.get("/privacy", controller.handleRequest(controller.pageGet("page/extra/privacy"))) - router.get("/terms", controller.handleRequest(controller.pageGet("page/extra/terms"))) - router.get("/no-auth", controller.handleRequest(controller.pageGet("page/auth/no-auth"))) - - return router - } -} \ No newline at end of file diff --git a/src/db/index.js b/src/db/index.js index 9a23f47..bbf5611 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -5,31 +5,31 @@ import { logQuery, logQueryError } from "./monitor.js" // 简单内存缓存(支持 TTL 与按前缀清理) const queryCache = new Map() -const crypto = await import('crypto') +const crypto = await import("crypto") const getNow = () => Date.now() -const computeExpiresAt = (ttlMs) => { +const computeExpiresAt = ttlMs => { if (!ttlMs || ttlMs <= 0) return null return getNow() + ttlMs } -const isExpired = (entry) => { +const isExpired = entry => { if (!entry) return true if (entry.expiresAt == null) return false return entry.expiresAt <= getNow() } -const getCacheKeyForBuilder = (builder) => { +const getCacheKeyForBuilder = builder => { if (builder._customCacheKey) return String(builder._customCacheKey) - + // 改进的缓存键生成策略 const sql = builder.toString() - const tableName = builder._single?.table || 'unknown' - + const tableName = builder._single?.table || "unknown" + // 使用 MD5 生成简短的哈希值,避免键冲突 - const hash = crypto.createHash('md5').update(sql).digest('hex') - + const hash = crypto.createHash("md5").update(sql).digest("hex") + // 缓存键格式:表名:哈希值:时间戳 const timestamp = Math.floor(Date.now() / 60000) // 每分钟更新一次时间戳 return `${tableName}:${hash}:${timestamp}` @@ -74,7 +74,7 @@ export const DbQueryCache = { let totalSize = 0 let hitCount = 0 let missCount = 0 - + for (const [k, entry] of queryCache.entries()) { if (isExpired(entry)) { expired++ @@ -83,14 +83,14 @@ export const DbQueryCache = { totalSize += JSON.stringify(entry.value).length } } - - return { - size: queryCache.size, - valid, + + return { + size: queryCache.size, + valid, expired, totalSize, averageSize: valid > 0 ? Math.round(totalSize / valid) : 0, - hitRate: (hitCount + missCount) > 0 ? (hitCount / (hitCount + missCount)) : 0 + hitRate: hitCount + missCount > 0 ? hitCount / (hitCount + missCount) : 0, } }, // 缓存一致性管理 @@ -115,14 +115,14 @@ export const DbQueryCache = { entryCount: stats.size, totalMemoryBytes: stats.totalSize, averageEntrySize: stats.averageSize, - estimatedMemoryMB: Math.round(stats.totalSize / 1024 / 1024 * 100) / 100 + estimatedMemoryMB: Math.round((stats.totalSize / 1024 / 1024) * 100) / 100, } - } + }, } // QueryBuilder 扩展 // 1) cache(ttlMs?): 读取缓存,不存在则执行并写入 -if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === 'function') { +if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === "function") { buildKnex.QueryBuilder.extend("cache", async function (ttlMs) { const key = getCacheKeyForBuilder(this) const entry = queryCache.get(key) @@ -172,7 +172,7 @@ if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === 'function }) // 7) 数据变更时自动清理相关缓存 - buildKnex.QueryBuilder.extend("invalidateCache", function() { + buildKnex.QueryBuilder.extend("invalidateCache", function () { const tableName = this._single?.table if (tableName) { DbQueryCache.invalidateByTable(tableName) @@ -184,36 +184,38 @@ if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === 'function // 8) 为 CUD 操作添加自动缓存失效 // 使用更安全的方式扩展 QueryBuilder 方法 -const addCacheInvalidation = (methodName) => { +const addCacheInvalidation = methodName => { if (buildKnex.QueryBuilder && buildKnex.QueryBuilder.prototype && buildKnex.QueryBuilder.prototype[methodName]) { - const originalMethod = buildKnex.QueryBuilder.prototype[methodName]; - buildKnex.QueryBuilder.prototype[methodName] = function(...args) { - const result = originalMethod.apply(this, args); - const tableName = this._single?.table; - - if (tableName && result && typeof result.then === 'function') { + const originalMethod = buildKnex.QueryBuilder.prototype[methodName] + buildKnex.QueryBuilder.prototype[methodName] = function (...args) { + const result = originalMethod.apply(this, args) + const tableName = this._single?.table + + if (tableName && result && typeof result.then === "function") { // 在操作完成后清理缓存 - const originalThen = result.then; - result.then = function(...thenArgs) { - const promise = originalThen.apply(this, thenArgs); - promise.then(() => { - DbQueryCache.invalidateByTable(tableName); - }).catch(() => { - DbQueryCache.invalidateByTable(tableName); - }); - return promise; - }; + const originalThen = result.then + result.then = function (...thenArgs) { + const promise = originalThen.apply(this, thenArgs) + promise + .then(() => { + DbQueryCache.invalidateByTable(tableName) + }) + .catch(() => { + DbQueryCache.invalidateByTable(tableName) + }) + return promise + } } - - return result; - }; + + return result + } } -}; +} // 安全地扩展 CUD 方法 -addCacheInvalidation('insert'); -addCacheInvalidation('update'); -addCacheInvalidation('del'); +addCacheInvalidation("insert") +addCacheInvalidation("update") +addCacheInvalidation("del") const environment = process.env.NODE_ENV || "development" const db = buildKnex(knexConfig[environment]) @@ -226,7 +228,7 @@ const connectionStats = { slowQueries: 0, errors: 0, lastHealthCheck: null, - uptime: Date.now() + uptime: Date.now(), } /** @@ -237,9 +239,9 @@ export const checkDatabaseHealth = async () => { const start = Date.now() await db.raw("SELECT 1 as health_check") const duration = Date.now() - start - + connectionStats.lastHealthCheck = new Date() - + return { status: "healthy", timestamp: connectionStats.lastHealthCheck, @@ -250,19 +252,19 @@ export const checkDatabaseHealth = async () => { used: db.client.pool.numUsed(), free: db.client.pool.numFree(), pending: db.client.pool.numPendingAcquires(), - pendingCreates: db.client.pool.numPendingCreates() + pendingCreates: db.client.pool.numPendingCreates(), }, - stats: connectionStats + stats: connectionStats, } } catch (error) { connectionStats.errors++ logger.error("数据库健康检查失败:", error) - + return { status: "unhealthy", error: error.message, timestamp: new Date(), - stats: connectionStats + stats: connectionStats, } } } @@ -280,8 +282,8 @@ export const getDatabaseStats = () => { used: db.client.pool.numUsed(), free: db.client.pool.numFree(), pending: db.client.pool.numPendingAcquires(), - pendingCreates: db.client.pool.numPendingCreates() - } + pendingCreates: db.client.pool.numPendingCreates(), + }, } } @@ -309,59 +311,79 @@ export const isDatabaseConnected = async () => { } } -// 数据库事件监听 -db.on('query', (queryData) => { +function listenQuery(queryData) { connectionStats.totalQueries++ - + // 记录查询统计 const duration = queryData.duration || 0 logQuery(queryData.sql, duration, queryData.bindings) - + // 记录慢查询(大于500ms) if (duration > 500) { connectionStats.slowQueries++ logger.warn("检测到慢查询:", { sql: queryData.sql, duration: duration, - bindings: queryData.bindings + bindings: queryData.bindings, }) } -}) - -db.on('query-error', (error, queryData) => { +} +function listenQueryError(error, queryData) { connectionStats.errors++ logQueryError(error, queryData?.sql, queryData?.bindings) -}) - -db.on('error', (error) => { +} +function listenError(error) { connectionStats.errors++ logger.error("数据库错误:", error) -}) +} +let checkInterval = null +let cleanupInterval = null +let memoryUsageInterval = null +export function dispose() { + db.off("query", listenQuery) + db.off("query-error", listenQueryError) + db.off("error", listenError) + clearInterval(checkInterval) + clearInterval(cleanupInterval) + clearInterval(memoryUsageInterval) +} +export function bootstrapDatabase() { + // 启动前清理之前的事件监听 + dispose() -// 定时健康检查(每5分钟) -setInterval(async () => { - const health = await checkDatabaseHealth() - if (health.status === "unhealthy") { - logger.error("数据库健康检查失败:", health) - } -}, 5 * 60 * 1000) + // 数据库事件监听 + db.on("query", listenQuery) -// 定时清理过期缓存(每2分钟) -setInterval(() => { - const cleaned = DbQueryCache.cleanup() - if (cleaned > 0) { - logger.debug(`清理了 ${cleaned} 个过期缓存项`) - } -}, 2 * 60 * 1000) - -// 内存使用监控(每10分钟) -setInterval(() => { - const memoryUsage = DbQueryCache.getMemoryUsage() - if (memoryUsage.estimatedMemoryMB > 50) { // 如果缓存超过50MB - logger.warn("缓存内存使用过高:", memoryUsage) - // 可以在这里实现更激进的清理策略 - } -}, 10 * 60 * 1000) + db.on("query-error", listenQueryError) + + db.on("error", listenError) + + // 定时健康检查(每5分钟) + checkInterval = setInterval(async () => { + const health = await checkDatabaseHealth() + if (health.status === "unhealthy") { + logger.error("数据库健康检查失败:", health) + } + }, 5 * 60 * 1000) + + // 定时清理过期缓存(每2分钟) + cleanupInterval = setInterval(() => { + const cleaned = DbQueryCache.cleanup() + if (cleaned > 0) { + logger.debug(`清理了 ${cleaned} 个过期缓存项`) + } + }, 2 * 60 * 1000) + + // 内存使用监控(每10分钟) + memoryUsageInterval = setInterval(() => { + const memoryUsage = DbQueryCache.getMemoryUsage() + if (memoryUsage.estimatedMemoryMB > 50) { + // 如果缓存超过50MB + logger.warn("缓存内存使用过高:", memoryUsage) + // 可以在这里实现更激进的清理策略 + } + }, 10 * 60 * 1000) +} export default db diff --git a/src/db/models/BaseModel.js b/src/db/models/BaseModel.js index 9f80c3c..c15e39f 100644 --- a/src/db/models/BaseModel.js +++ b/src/db/models/BaseModel.js @@ -1,5 +1,6 @@ import db from "../index.js" import { logger } from "../../logger.js" +import { BaseSingleton } from "@/utils/BaseSingleton" /** * 数据库错误类 @@ -18,7 +19,7 @@ export class DatabaseError extends Error { */ export const handleDatabaseError = (error, operation = "数据库操作") => { logger.error(`${operation}失败:`, error) - + if (error.code === "SQLITE_CONSTRAINT") { return new DatabaseError("数据约束违反", "CONSTRAINT_VIOLATION", error) } @@ -31,7 +32,7 @@ export const handleDatabaseError = (error, operation = "数据库操作") => { if (error.code === "SQLITE_NOTFOUND") { return new DatabaseError("记录不存在", "NOT_FOUND", error) } - + return new DatabaseError(`${operation}失败: ${error.message}`, "DATABASE_ERROR", error) } @@ -39,46 +40,58 @@ export const handleDatabaseError = (error, operation = "数据库操作") => { * 统一的数据库基础模型类 * 提供标准化的CRUD操作和错误处理 */ -export default class BaseModel { +export default class BaseModel extends BaseSingleton { + + /** + * @returns {BaseModel} + */ + static getInstance() { + return super.getInstance() + } + + constructor() { + super() + } + /** * 获取表名,必须由子类实现 */ - static get tableName() { + get tableName() { throw new Error("tableName must be defined in subclass") } /** * 获取默认排序字段 */ - static get defaultOrderBy() { + get defaultOrderBy() { return "id" } /** * 获取默认排序方向 */ - static get defaultOrder() { + get defaultOrder() { return "desc" } /** * 获取可搜索字段列表 */ - static get searchableFields() { + get searchableFields() { return [] } /** * 获取可过滤字段列表 */ - static get filterableFields() { + get filterableFields() { return [] } /** * 根据ID查找单条记录 */ - static async findById(id) { + async findById(id) { try { const result = await db(this.tableName).where("id", id).first() return result || null @@ -90,29 +103,22 @@ export default class BaseModel { /** * 查找所有记录,支持分页和排序 */ - static async findAll(options = {}) { + async findAll(options = {}) { try { - const { - page = 1, - limit = 10, - orderBy = this.defaultOrderBy, - order = this.defaultOrder, - where = {}, - select = "*" - } = options + const { page = 1, limit = 10, orderBy = this.defaultOrderBy, order = this.defaultOrder, where = {}, select = "*" } = options const offset = (page - 1) * limit - + let query = db(this.tableName).select(select) - + // 添加where条件 if (Object.keys(where).length > 0) { query = query.where(where) } - + // 添加排序和分页 query = query.orderBy(orderBy, order).limit(limit).offset(offset) - + return await query } catch (error) { throw handleDatabaseError(error, `查找${this.tableName}记录列表`) @@ -122,9 +128,9 @@ export default class BaseModel { /** * 查找第一条记录 */ - static async findFirst(conditions = {}) { + async findFirst(conditions = {}) { try { - return await db(this.tableName).where(conditions).first() || null + return (await db(this.tableName).where(conditions).first()) || null } catch (error) { throw handleDatabaseError(error, `查找${this.tableName}第一条记录`) } @@ -133,25 +139,20 @@ export default class BaseModel { /** * 根据条件查找记录 */ - static async findWhere(conditions, options = {}) { + async findWhere(conditions, options = {}) { try { - const { - orderBy = this.defaultOrderBy, - order = this.defaultOrder, - limit, - select = "*" - } = options + const { orderBy = this.defaultOrderBy, order = this.defaultOrder, limit, select = "*" } = options let query = db(this.tableName).select(select).where(conditions) - + if (orderBy) { query = query.orderBy(orderBy, order) } - + if (limit) { query = query.limit(limit) } - + return await query } catch (error) { throw handleDatabaseError(error, `按条件查找${this.tableName}记录`) @@ -161,7 +162,7 @@ export default class BaseModel { /** * 创建新记录 */ - static async create(data) { + async create(data) { try { const insertData = { ...data, @@ -169,10 +170,8 @@ export default class BaseModel { updated_at: db.fn.now(), } - const result = await db(this.tableName) - .insert(insertData) - .returning("*") - + const result = await db(this.tableName).insert(insertData).returning("*") + // SQLite returning() 总是返回数组,这里统一返回第一个元素 return Array.isArray(result) ? result[0] : result } catch (error) { @@ -183,18 +182,15 @@ export default class BaseModel { /** * 更新记录 */ - static async update(id, data) { + async update(id, data) { try { const updateData = { ...data, updated_at: db.fn.now(), } - const result = await db(this.tableName) - .where("id", id) - .update(updateData) - .returning("*") - + const result = await db(this.tableName).where("id", id).update(updateData).returning("*") + // SQLite returning() 总是返回数组,这里统一返回第一个元素 return Array.isArray(result) ? result[0] : result } catch (error) { @@ -205,16 +201,14 @@ export default class BaseModel { /** * 根据条件更新记录 */ - static async updateWhere(conditions, data) { + async updateWhere(conditions, data) { try { const updateData = { ...data, updated_at: db.fn.now(), } - return await db(this.tableName) - .where(conditions) - .update(updateData) + return await db(this.tableName).where(conditions).update(updateData) } catch (error) { throw handleDatabaseError(error, `按条件更新${this.tableName}记录`) } @@ -223,7 +217,7 @@ export default class BaseModel { /** * 删除记录 */ - static async delete(id) { + async delete(id) { try { return await db(this.tableName).where("id", id).del() } catch (error) { @@ -234,7 +228,7 @@ export default class BaseModel { /** * 根据条件删除记录 */ - static async deleteWhere(conditions) { + async deleteWhere(conditions) { try { return await db(this.tableName).where(conditions).del() } catch (error) { @@ -245,12 +239,9 @@ export default class BaseModel { /** * 统计记录数量 */ - static async count(conditions = {}) { + async count(conditions = {}) { try { - const result = await db(this.tableName) - .where(conditions) - .count("id as count") - .first() + const result = await db(this.tableName).where(conditions).count("id as count").first() return parseInt(result.count) || 0 } catch (error) { throw handleDatabaseError(error, `统计${this.tableName}记录数量`) @@ -260,7 +251,7 @@ export default class BaseModel { /** * 检查记录是否存在 */ - static async exists(conditions) { + async exists(conditions) { try { const count = await this.count(conditions) return count > 0 @@ -272,7 +263,7 @@ export default class BaseModel { /** * 分页查询 */ - static async paginate(options = {}) { + async paginate(options = {}) { try { const { page = 1, @@ -282,19 +273,19 @@ export default class BaseModel { where = {}, select = "*", search = "", - searchFields = this.searchableFields + searchFields = this.searchableFields, } = options let query = db(this.tableName).select(select) - + // 添加where条件 if (Object.keys(where).length > 0) { query = query.where(where) } - + // 添加搜索条件 if (search && searchFields.length > 0) { - query = query.where(function() { + query = query.where(function () { searchFields.forEach((field, index) => { if (index === 0) { this.where(field, "like", `%${search}%`) @@ -312,10 +303,7 @@ export default class BaseModel { // 分页查询 const offset = (page - 1) * limit - const data = await query - .orderBy(orderBy, order) - .limit(limit) - .offset(offset) + const data = await query.orderBy(orderBy, order).limit(limit).offset(offset) return { data, @@ -325,8 +313,8 @@ export default class BaseModel { total, totalPages: Math.ceil(total / limit), hasNext: page * limit < total, - hasPrev: page > 1 - } + hasPrev: page > 1, + }, } } catch (error) { throw handleDatabaseError(error, `分页查询${this.tableName}记录`) @@ -336,24 +324,22 @@ export default class BaseModel { /** * 批量创建记录 */ - static async createMany(dataArray, batchSize = 100) { + async createMany(dataArray, batchSize = 100) { try { const results = [] - + for (let i = 0; i < dataArray.length; i += batchSize) { const batch = dataArray.slice(i, i + batchSize).map(data => ({ ...data, created_at: db.fn.now(), updated_at: db.fn.now(), })) - - const batchResults = await db(this.tableName) - .insert(batch) - .returning("*") - + + const batchResults = await db(this.tableName).insert(batch).returning("*") + results.push(...(Array.isArray(batchResults) ? batchResults : [batchResults])) } - + return results } catch (error) { throw handleDatabaseError(error, `批量创建${this.tableName}记录`) @@ -363,16 +349,14 @@ export default class BaseModel { /** * 批量更新记录 */ - static async updateMany(conditions, data) { + async updateMany(conditions, data) { try { const updateData = { ...data, updated_at: db.fn.now(), } - return await db(this.tableName) - .where(conditions) - .update(updateData) + return await db(this.tableName).where(conditions).update(updateData) } catch (error) { throw handleDatabaseError(error, `批量更新${this.tableName}记录`) } @@ -381,7 +365,7 @@ export default class BaseModel { /** * 获取表结构信息 */ - static async getTableInfo() { + async getTableInfo() { try { return await db.raw(`PRAGMA table_info(${this.tableName})`) } catch (error) { @@ -392,7 +376,7 @@ export default class BaseModel { /** * 清空表数据 */ - static async truncate() { + async truncate() { try { return await db(this.tableName).del() } catch (error) { @@ -403,11 +387,9 @@ export default class BaseModel { /** * 获取随机记录 */ - static async findRandom(limit = 1) { + async findRandom(limit = 1) { try { - return await db(this.tableName) - .orderByRaw("RANDOM()") - .limit(limit) + return await db(this.tableName).orderByRaw("RANDOM()").limit(limit) } catch (error) { throw handleDatabaseError(error, `获取${this.tableName}随机记录`) } @@ -416,83 +398,78 @@ export default class BaseModel { /** * 关联查询基础方法 - 左连接 */ - static leftJoin(joinTable, leftKey, rightKey) { + leftJoin(joinTable, leftKey, rightKey) { return db(this.tableName).leftJoin(joinTable, leftKey, rightKey) } /** * 关联查询基础方法 - 内连接 */ - static innerJoin(joinTable, leftKey, rightKey) { + innerJoin(joinTable, leftKey, rightKey) { return db(this.tableName).innerJoin(joinTable, leftKey, rightKey) } /** * 关联查询基础方法 - 右连接 */ - static rightJoin(joinTable, leftKey, rightKey) { + rightJoin(joinTable, leftKey, rightKey) { return db(this.tableName).rightJoin(joinTable, leftKey, rightKey) } /** * 构建复杂关联查询 */ - static buildRelationQuery(relations = []) { + buildRelationQuery(relations = []) { let query = db(this.tableName) - + relations.forEach(relation => { const { type, table, on, select } = relation - + switch (type) { - case 'left': + case "left": query = query.leftJoin(table, on[0], on[1]) break - case 'inner': + case "inner": query = query.innerJoin(table, on[0], on[1]) break - case 'right': + case "right": query = query.rightJoin(table, on[0], on[1]) break } - + if (select) { query = query.select(select) } }) - + return query } /** * 通用关联查询方法 */ - static async findWithRelations(conditions = {}, relations = [], options = {}) { + async findWithRelations(conditions = {}, relations = [], options = {}) { try { - const { - orderBy = this.defaultOrderBy, - order = this.defaultOrder, - limit, - select = [`${this.tableName}.*`] - } = options + const { orderBy = this.defaultOrderBy, order = this.defaultOrder, limit, select = [`${this.tableName}.*`] } = options let query = this.buildRelationQuery(relations) - + if (select && select.length > 0) { query = query.select(...select) } - + if (Object.keys(conditions).length > 0) { query = query.where(conditions) } - + if (orderBy) { query = query.orderBy(orderBy, order) } - + if (limit) { query = query.limit(limit) } - + return await query } catch (error) { throw handleDatabaseError(error, `关联查询${this.tableName}记录`) @@ -504,7 +481,7 @@ export default class BaseModel { /** * 在事务中创建记录 */ - static async createInTransaction(trx, data) { + async createInTransaction(trx, data) { try { const insertData = { ...data, @@ -512,10 +489,8 @@ export default class BaseModel { updated_at: trx.fn.now(), } - const result = await trx(this.tableName) - .insert(insertData) - .returning("*") - + const result = await trx(this.tableName).insert(insertData).returning("*") + return Array.isArray(result) ? result[0] : result } catch (error) { throw handleDatabaseError(error, `在事务中创建${this.tableName}记录`) @@ -525,18 +500,15 @@ export default class BaseModel { /** * 在事务中更新记录 */ - static async updateInTransaction(trx, id, data) { + async updateInTransaction(trx, id, data) { try { const updateData = { ...data, updated_at: trx.fn.now(), } - const result = await trx(this.tableName) - .where("id", id) - .update(updateData) - .returning("*") - + const result = await trx(this.tableName).where("id", id).update(updateData).returning("*") + return Array.isArray(result) ? result[0] : result } catch (error) { throw handleDatabaseError(error, `在事务中更新${this.tableName}记录(ID: ${id})`) @@ -546,7 +518,7 @@ export default class BaseModel { /** * 在事务中删除记录 */ - static async deleteInTransaction(trx, id) { + async deleteInTransaction(trx, id) { try { return await trx(this.tableName).where("id", id).del() } catch (error) { @@ -557,24 +529,22 @@ export default class BaseModel { /** * 在事务中批量创建记录 */ - static async createManyInTransaction(trx, dataArray, batchSize = 100) { + async createManyInTransaction(trx, dataArray, batchSize = 100) { try { const results = [] - + for (let i = 0; i < dataArray.length; i += batchSize) { const batch = dataArray.slice(i, i + batchSize).map(data => ({ ...data, created_at: trx.fn.now(), updated_at: trx.fn.now(), })) - - const batchResults = await trx(this.tableName) - .insert(batch) - .returning("*") - + + const batchResults = await trx(this.tableName).insert(batch).returning("*") + results.push(...(Array.isArray(batchResults) ? batchResults : [batchResults])) } - + return results } catch (error) { throw handleDatabaseError(error, `在事务中批量创建${this.tableName}记录`) @@ -584,16 +554,14 @@ export default class BaseModel { /** * 在事务中批量更新记录 */ - static async updateManyInTransaction(trx, conditions, data) { + async updateManyInTransaction(trx, conditions, data) { try { const updateData = { ...data, updated_at: trx.fn.now(), } - return await trx(this.tableName) - .where(conditions) - .update(updateData) + return await trx(this.tableName).where(conditions).update(updateData) } catch (error) { throw handleDatabaseError(error, `在事务中批量更新${this.tableName}记录`) } @@ -602,11 +570,11 @@ export default class BaseModel { /** * 在事务中执行原生 SQL */ - static async rawInTransaction(trx, query, bindings = []) { + async rawInTransaction(trx, query, bindings = []) { try { return await trx.raw(query, bindings) } catch (error) { throw handleDatabaseError(error, `在事务中执行原生 SQL`) } } -} \ No newline at end of file +} diff --git a/src/db/models/UserModel.js b/src/db/models/UserModel.js index 5cfc09d..5e467e0 100644 --- a/src/db/models/UserModel.js +++ b/src/db/models/UserModel.js @@ -1,29 +1,40 @@ import BaseModel from "./BaseModel.js" class UserModel extends BaseModel { - static get tableName() { + /** + * @returns {UserModel} + */ + static getInstance() { + return super.getInstance() + } + + constructor() { + super() + } + + get tableName() { return "users" } - static get searchableFields() { + get searchableFields() { return ["username", "email", "name"] } - static get filterableFields() { + get filterableFields() { return ["role", "status"] } // 特定业务方法 - static async findByUsername(username) { + async findByUsername(username) { return this.findFirst({ username }) } - - static async findByEmail(email) { + + async findByEmail(email) { return this.findFirst({ email }) } // 重写create方法添加验证 - static async create(data) { + async create(data) { // 验证唯一性 if (data.username) { const existingUser = await this.findByUsername(data.username) @@ -43,7 +54,7 @@ class UserModel extends BaseModel { } // 重写update方法添加验证 - static async update(id, data) { + async update(id, data) { // 验证唯一性(排除当前用户) if (data.username) { const existingUser = await this.findFirst({ username: data.username }) @@ -63,29 +74,29 @@ class UserModel extends BaseModel { } // 用户状态管理 - static async activate(id) { + async activate(id) { return this.update(id, { status: "active" }) } - static async deactivate(id) { + async deactivate(id) { return this.update(id, { status: "inactive" }) } // 按角色查找用户 - static async findByRole(role) { + async findByRole(role) { return this.findWhere({ role }) } // 获取用户统计 - static async getUserStats() { + async getUserStats() { const total = await this.count() const active = await this.count({ status: "active" }) const inactive = await this.count({ status: "inactive" }) - + return { total, active, - inactive + inactive, } } } diff --git a/src/main.js b/src/main.js index 8cc58c3..4706552 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,5 @@ import { app } from "./global" +import { bootstrapDatabase } from "@/db/index.js" // 日志、全局插件、定时任务等基础设施 import { logger } from "./logger.js" import "./jobs/index.js" @@ -15,6 +16,8 @@ const PORT = process.env.PORT || 3001; // 注册插件 await LoadMiddlewares(app) + + bootstrapDatabase() const server = app.listen(PORT, () => { const port = server.address().port diff --git a/src/middlewares/Auth/index.js b/src/middlewares/Auth/index.js index 96dbdd3..8d5b650 100644 --- a/src/middlewares/Auth/index.js +++ b/src/middlewares/Auth/index.js @@ -7,10 +7,10 @@ export const JWT_SECRET = process.env.JWT_SECRET function matchList(list, path) { for (const item of list) { if (typeof item === "string" && minimatch(path, item)) { - return { matched: true, auth: false } + return { matched: true } } if (typeof item === "object" && minimatch(path, item.pattern)) { - return { matched: true, auth: item.auth } + return { matched: true } } } return { matched: false } @@ -30,45 +30,36 @@ export function AuthMiddleware(options = { } // 白名单处理 const white = matchList(options.whiteList, ctx.path) - if (white.matched) { - if (white.auth === false) { - ctx.authType = false - } else if (white.auth === "try") { - ctx.authType = "try" - } else { - ctx.authType = true - } - } else { - // 默认需要登录 - ctx.authType = true + if (!white.matched) { + throw new CommonError("禁止访问", CommonError.ERR_CODE.FORBIDDEN) } return next() } } -export function VerifyUserMiddleware() { - return (ctx, next) => { - if (ctx.session.user) { - ctx.state.user = ctx.session.user - } else { - const authorizationString = ctx.headers["authorization"] - if (authorizationString) { - const token = authorizationString.replace(/^Bearer\s/, "") - ctx.state.user = jwt.verify(token, process.env.JWT_SECRET) - } - } - if (ctx.authType === false) { - if (ctx.state.user) { - throw new CommonError("该接口不能登录查看") - } - return next() - } - if (ctx.authType === "try") { - return next() - } - if (!ctx.state.user && ctx.authType === true) { - throw new CommonError("请登录") - } - return next() - } -} +// export function VerifyUserMiddleware() { +// return (ctx, next) => { +// if (ctx.session.user) { +// ctx.state.user = ctx.session.user +// } else { +// const authorizationString = ctx.headers["authorization"] +// if (authorizationString) { +// const token = authorizationString.replace(/^Bearer\s/, "") +// ctx.state.user = jwt.verify(token, process.env.JWT_SECRET) +// } +// } +// if (ctx.authType === false) { +// if (ctx.state.user) { +// throw new CommonError("该接口不能登录查看") +// } +// return next() +// } +// if (ctx.authType === "try") { +// return next() +// } +// if (!ctx.state.user && ctx.authType === true) { +// throw new CommonError("请登录") +// } +// return next() +// } +// } diff --git a/src/middlewares/Controller/index.js b/src/middlewares/Controller/index.js new file mode 100644 index 0000000..dfbd86a --- /dev/null +++ b/src/middlewares/Controller/index.js @@ -0,0 +1,98 @@ +import fs from "fs" +import path from "path" +import { logger } from "@/logger.js" +import compose from "koa-compose" + +async function scanControllers(rootDir) { + const routers = [] + const stack = [rootDir] + while (stack.length) { + const dir = stack.pop() + let files + try { + files = fs.readdirSync(dir) + } catch (error) { + logger.error(`[控制器注册] ❌ 读取目录失败 ${dir}: ${error.message}`) + continue + } + + for (const file of files) { + if (file.startsWith("_")) continue + const fullPath = path.join(dir, file) + let stat + try { + stat = fs.statSync(fullPath) + } catch (error) { + logger.error(`[控制器注册] ❌ 读取文件信息失败 ${fullPath}: ${error.message}`) + continue + } + + if (stat.isDirectory()) { + stack.push(fullPath) + continue + } + + if (!fullPath.replace(/\\/g, "/").includes("/controller/")) continue + + let fileName = fullPath.replace(rootDir+path.sep, "") + + try { + const controllerModule = await import(fullPath) + const controller = controllerModule.default || controllerModule + if (!controller) { + logger.warn(`[控制器注册] ${fileName} - 缺少默认导出,跳过注册`) + continue + } + + let routesFactory = controller.createRoutes || controller.default?.createRoutes || controller.default || controller + if(typeof routesFactory === "function") { + routesFactory = routesFactory.bind(controller) + } + if (typeof routesFactory !== "function") { + logger.warn(`[控制器注册] ⚠️ ${fileName} - 未找到 createRoutes 方法或导出对象`) + continue + } + + let routerResult + try { + routerResult = routesFactory() + } catch (error) { + logger.error(`[控制器注册] ❌ ${fileName} - createRoutes() 执行失败: ${error.message}`) + continue + } + + const list = Array.isArray(routerResult) ? routerResult : [routerResult] + let added = 0 + for (const r of list) { + if (r && typeof r.middleware === "function") { + routers.push(r) + added++ + } else { + logger.warn(`[控制器注册] ⚠️ ${fileName} - createRoutes() 返回的部分路由器对象无效`) + } + } + if (added > 0) logger.debug(`[控制器注册] ✅ ${fileName} - 创建成功 (${added})`) + } catch (importError) { + logger.error(`[控制器注册] ❌ ${fileName} - 模块导入失败: ${importError.message}`) + logger.error(importError) + } + } + } + return routers +} + +export default async function (options = {}) { + const { root, handleBeforeEachRequest } = options + if (!root) { + throw new Error("controller root is required") + } + const routers = await scanControllers(root) + const allRouters = [] + for (let i = 0; i < routers.length; i++) { + const router = routers[i] + allRouters.push(router.middleware((options = {}) => handleBeforeEachRequest(options))) + } + return async function (ctx, next) { + return await compose(allRouters)(ctx, next) + } +} diff --git a/src/middlewares/RoutePerformance/index.js b/src/middlewares/RoutePerformance/index.js deleted file mode 100644 index a77aca7..0000000 --- a/src/middlewares/RoutePerformance/index.js +++ /dev/null @@ -1,304 +0,0 @@ -import routeCache from "../../utils/cache/RouteCache.js" -import { logger } from "@/logger.js" -import config from "@/config/index.js" - -/** - * 路由性能监控中间件 - * 监控路由响应时间并提供缓存优化建议 - */ -class RoutePerformanceMonitor { - constructor() { - // 性能统计 - this.performanceStats = new Map() - - // 配置(从通用配置中获取) - this.config = { - // 监控窗口大小(保留最近N次请求的数据) - windowSize: config.routePerformance?.windowSize || 100, - // 慢路由阈值(毫秒) - slowRouteThreshold: config.routePerformance?.slowRouteThreshold || 500, - // 自动清理间隔(毫秒) - cleanupInterval: config.routePerformance?.cleanupInterval || 5 * 60 * 1000, - // 数据保留时间(毫秒) - dataRetentionTime: config.routePerformance?.dataRetentionTime || 10 * 60 * 1000, - // 最小分析数据量 - minAnalysisDataCount: config.routePerformance?.minAnalysisDataCount || 10, - // 缓存命中率警告阈值 - cacheHitRateWarningThreshold: config.routePerformance?.cacheHitRateWarningThreshold || 0.5, - // 是否启用优化建议 - enableOptimizationSuggestions: config.routePerformance?.enableOptimizationSuggestions ?? true, - // 性能报告最大路由数 - maxRouteReportCount: config.routePerformance?.maxRouteReportCount || 50, - // 是否启用性能监控 - enabled: config.routePerformance?.enabled ?? (process.env.NODE_ENV === 'production') - } - - // 启动定期清理 - if (this.config.enabled) { - this.startPeriodicCleanup() - } - } - - /** - * 启动定期清理任务 - */ - startPeriodicCleanup() { - setInterval(() => { - this.cleanupStats() - }, this.config.cleanupInterval) - } - - /** - * 清理过期的统计数据 - */ - cleanupStats() { - const cutoff = Date.now() - this.config.dataRetentionTime - - for (const [key, stats] of this.performanceStats.entries()) { - // 清理超过数据保留时间的数据 - stats.requests = stats.requests.filter(req => req.timestamp > cutoff) - - // 如果没有请求数据了,删除这个路由的统计 - if (stats.requests.length === 0) { - this.performanceStats.delete(key) - } - } - - logger.debug(`[性能监控] 清理完成,当前监控路由数: ${this.performanceStats.size}`) - } - - /** - * 生成路由统计键 - * @param {string} method - HTTP方法 - * @param {string} path - 路由路径 - * @returns {string} 统计键 - */ - getStatsKey(method, path) { - return `${method}:${path}` - } - - /** - * 记录路由性能数据 - * @param {string} method - HTTP方法 - * @param {string} path - 路由路径 - * @param {number} duration - 响应时间(毫秒) - * @param {boolean} cacheHit - 是否命中缓存 - */ - recordPerformance(method, path, duration, cacheHit = false) { - if (!this.config.enabled) return - - const key = this.getStatsKey(method, path) - - if (!this.performanceStats.has(key)) { - this.performanceStats.set(key, { - method, - path, - requests: [] - }) - } - - const stats = this.performanceStats.get(key) - stats.requests.push({ - timestamp: Date.now(), - duration, - cacheHit - }) - - // 保持窗口大小 - if (stats.requests.length > this.config.windowSize) { - stats.requests = stats.requests.slice(-this.config.windowSize) - } - - // 检查是否需要缓存优化 - this.checkForOptimization(key, stats) - } - - /** - * 检查是否需要缓存优化 - * @param {string} key - 统计键 - * @param {Object} stats - 统计数据 - */ - checkForOptimization(key, stats) { - if (stats.requests.length < this.config.minAnalysisDataCount) return // 数据太少,不进行分析 - - const recentRequests = stats.requests.slice(-20) // 最近20次请求 - const avgDuration = recentRequests.reduce((sum, req) => sum + req.duration, 0) / recentRequests.length - const cacheHitRate = recentRequests.filter(req => req.cacheHit).length / recentRequests.length - - // 慢路由且缓存命中率低 - if (avgDuration > this.config.slowRouteThreshold && cacheHitRate < this.config.cacheHitRateWarningThreshold) { - logger.warn(`[性能监控] 发现慢路由: ${key}, 平均响应时间: ${avgDuration.toFixed(2)}ms, 缓存命中率: ${(cacheHitRate * 100).toFixed(1)}%`) - - if (this.config.enableOptimizationSuggestions) { - this.generateOptimizationSuggestions(key, avgDuration, cacheHitRate) - } - } - } - - /** - * 生成优化庺议 - * @param {string} routeKey - 路由键 - * @param {number} avgDuration - 平均响应时间 - * @param {number} cacheHitRate - 缓存命中率 - */ - generateOptimizationSuggestions(routeKey, avgDuration, cacheHitRate) { - const suggestions = [] - - if (cacheHitRate < 0.3) { - suggestions.push('考虑增加路由缓存策略') - } - - if (avgDuration > this.config.slowRouteThreshold * 2) { - suggestions.push('考虑优化数据库查询或业务逻辑') - } - - if (cacheHitRate < 0.5 && avgDuration > this.config.slowRouteThreshold) { - suggestions.push('建议启用或优化响应缓存') - } - - if (suggestions.length > 0) { - logger.info(`[性能监控] ${routeKey} 优化建议: ${suggestions.join('; ')}`) - } - } - - /** - * 获取性能统计报告 - * @returns {Object} 性能报告 - */ - getPerformanceReport() { - const report = { - enabled: this.config.enabled, - totalRoutes: this.performanceStats.size, - config: { - windowSize: this.config.windowSize, - slowRouteThreshold: this.config.slowRouteThreshold, - cacheHitRateWarningThreshold: this.config.cacheHitRateWarningThreshold - }, - routes: [] - } - - for (const [key, stats] of this.performanceStats.entries()) { - if (stats.requests.length === 0) continue - - const recentRequests = stats.requests.slice(-50) // 最近50次请求 - const durations = recentRequests.map(req => req.duration) - const cacheHits = recentRequests.filter(req => req.cacheHit).length - - const routeReport = { - route: key, - method: stats.method, - path: stats.path, - requestCount: recentRequests.length, - avgDuration: durations.reduce((sum, d) => sum + d, 0) / durations.length, - minDuration: Math.min(...durations), - maxDuration: Math.max(...durations), - cacheHitRate: (cacheHits / recentRequests.length * 100).toFixed(1) + '%', - isSlowRoute: durations.reduce((sum, d) => sum + d, 0) / durations.length > this.config.slowRouteThreshold, - needsOptimization: (cacheHits / recentRequests.length) < this.config.cacheHitRateWarningThreshold - } - - report.routes.push(routeReport) - } - - // 按平均响应时间排序并限制数量 - report.routes.sort((a, b) => b.avgDuration - a.avgDuration) - if (report.routes.length > this.config.maxRouteReportCount) { - report.routes = report.routes.slice(0, this.config.maxRouteReportCount) - } - - return report - } - - /** - * 获取慢路由列表 - * @returns {Array} 慢路由列表 - */ - getSlowRoutes() { - return this.getPerformanceReport().routes.filter(route => route.isSlowRoute) - } - - /** - * 获取需要优化的路由列表 - * @returns {Array} 需要优化的路由列表 - */ - getRoutesNeedingOptimization() { - return this.getPerformanceReport().routes.filter(route => route.needsOptimization || route.isSlowRoute) - } - - /** - * 创建中间件函数 - * @returns {Function} Koa中间件 - */ - middleware() { - return async (ctx, next) => { - if (!this.config.enabled) { - await next() - return - } - - const start = Date.now() - let cacheHit = false - - // 检查是否命中路由缓存 - const routeMatch = routeCache.getRouteMatch(ctx.method, ctx.path) - if (routeMatch) { - cacheHit = true - } - - try { - await next() - } finally { - const duration = Date.now() - start - this.recordPerformance(ctx.method, ctx.path, duration, cacheHit) - } - } - } - - /** - * 更新配置 - * @param {Object} newConfig - 新配置 - */ - updateConfig(newConfig) { - const oldEnabled = this.config.enabled - - // 合并配置 - this.config = { ...this.config, ...newConfig } - - // 如果启用状态发生变化,重新初始化 - if (oldEnabled !== this.config.enabled) { - if (this.config.enabled) { - this.startPeriodicCleanup() - logger.info('[性能监控] 性能监控已启用') - } else { - // 清理现有数据 - this.performanceStats.clear() - logger.info('[性能监控] 性能监控已禁用并清除数据') - } - } - - logger.info('[性能监控] 配置已更新', this.config) - } - - /** - * 启用性能监控 - */ - enable() { - this.config.enabled = true - this.startPeriodicCleanup() - logger.info('[性能监控] 性能监控已启用') - } - - /** - * 禁用性能监控 - */ - disable() { - this.config.enabled = false - this.performanceStats.clear() - logger.info('[性能监控] 性能监控已禁用') - } -} - -// 导出单例实例 -const performanceMonitor = new RoutePerformanceMonitor() -export default performanceMonitor -export { RoutePerformanceMonitor } \ No newline at end of file diff --git a/src/middlewares/errorHandler/index.js b/src/middlewares/errorHandler/index.js index 6023895..ced2c5e 100644 --- a/src/middlewares/errorHandler/index.js +++ b/src/middlewares/errorHandler/index.js @@ -90,7 +90,6 @@ export default function () { // 确保状态码在合理范围内 status = status >= 100 && status < 600 ? status : 500 - await formatError( ctx, status, diff --git a/src/middlewares/install.js b/src/middlewares/install.js index b302e8e..910b859 100644 --- a/src/middlewares/install.js +++ b/src/middlewares/install.js @@ -4,21 +4,22 @@ import { resolve } from "path" import { fileURLToPath } from "url" import path from "path" import ErrorHandler from "./ErrorHandler" -import { VerifyUserMiddleware, AuthMiddleware } from "./Auth" +import { AuthMiddleware } from "./Auth" import bodyParser from "koa-bodyparser" import Views from "./Views" import Session from "./Session" import etag from "@koa/etag" import conditional from "koa-conditional-get" -import { autoRegisterControllers } from "@/utils/ForRegister.js" -import performanceMonitor from "./RoutePerformance/index.js" +import Controller from "./Controller/index.js" import app from "@/global" import fs from "fs" import helmet from "koa-helmet" import ratelimit from "koa-ratelimit" import { render } from "./PugHelper/sass.js" +import AuthError from "@/utils/error/AuthError.js" +import CommonError from "@/utils/error/CommonError.js" -import { SiteConfigService } from "services/SiteConfigService.js" +import { SiteConfigService } from "@/modules/SiteConfig/services/index.js" import config from "config/index.js" const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -40,6 +41,7 @@ export default async app => { } return next() }) + // 跨域设置 app.use(async (ctx, next) => { ctx.set("Access-Control-Allow-Origin", "*") ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS") @@ -51,6 +53,7 @@ export default async app => { return await next() }) + // 安全设置 app.use( helmet({ contentSecurityPolicy: { @@ -61,7 +64,7 @@ export default async app => { }) ) - // apply rate limit + // 应用限流 const db = new Map() app.use( ratelimit({ @@ -85,17 +88,14 @@ export default async app => { }, }) ) - + // 提供全局数据 app.use(async (ctx, next) => { - // 提供全局数据 ctx.state.siteConfig = await SiteConfigService.getAll() ctx.state.$config = config return await next() }) // 错误处理,主要处理运行中抛出的错误 app.use(ErrorHandler()) - // 路由性能监控(在路由处理之前) - app.use(performanceMonitor.middleware()) // session设置 app.use(Session(app)) // 视图设置 @@ -105,6 +105,7 @@ export default async app => { options: { basedir: resolve(__dirname, "../views"), filters: { + // 处理scss scss: function (text, options) { //- process.env.SASS_PATH = "D:/@code/demo/koa3-demo/src/views/page/index" const root = path.resolve(__dirname, "../views") @@ -126,8 +127,8 @@ export default async app => { AuthMiddleware({ whiteList: [ // 所有请求放行 - { pattern: "/", auth: "try" }, - { pattern: "/**/*", auth: "try" }, + { pattern: "/" }, + { pattern: "/**/*" }, ], blackList: [ // 禁用api请求 @@ -139,11 +140,44 @@ export default async app => { ) // 验证用户 // 注入全局变量:ctx.state.user - app.use(VerifyUserMiddleware()) + // app.use(VerifyUserMiddleware()) // 请求体解析 app.use(bodyParser()) - // 自动注册控制器 - await autoRegisterControllers(app, path.resolve(__dirname, "../controllers")) + app.use( + await Controller({ + root: path.resolve(__dirname, "../modules"), + handleBeforeEachRequest: options => { + const { auth = true } = options || {} + return async (ctx, next) => { + if (ctx.session && ctx.session.user) { + ctx.state.user = ctx.session.user + } else { + const authorizationString = ctx.headers && ctx.headers["authorization"] + if (authorizationString) { + const token = authorizationString.replace(/^Bearer\s/, "") + try { + ctx.state.user = jwt.verify(token, process.env.JWT_SECRET) + } catch (_) { + // 无效token忽略 + } + } + } + + if (auth === false && ctx.state.user) { + throw new CommonError("不能登录查看") + } + if (auth === "try") { + return next() + } + if (auth === true && !ctx.state.user) { + throw new AuthError("需要登录才能访问") + } + + return await next() + } + }, + }) + ) // 注册完成之后静态资源设置 app.use(async (ctx, next) => { if (ctx.body) return await next() diff --git a/src/modules/Api/controller/index.js b/src/modules/Api/controller/index.js new file mode 100644 index 0000000..0551785 --- /dev/null +++ b/src/modules/Api/controller/index.js @@ -0,0 +1,60 @@ +import { R } from "utils/helper.js" +import Router from "utils/router.js" + +class AuthController { + constructor() {} + + /** + * 通用请求函数:依次请求网址数组,返回第一个成功的响应及其类型 + * @param {string[]} urls + * @returns {Promise<{type: string, data: any}>} + */ + async fetchFirstSuccess(urls) { + for (const url of urls) { + try { + const res = await fetch(url, { method: "get", mode: "cors", redirect: "follow" }) + if (!res.ok) continue + const contentType = res.headers.get("content-type") || "" + let data, type + if (contentType.includes("application/json")) { + data = await res.json() + type = "json" + } else if (contentType.includes("text/")) { + data = await res.text() + type = "text" + } else { + data = await res.blob() + type = "blob" + } + return { type, data } + } catch (e) { + // ignore and try next url + } + } + throw new Error("All requests failed") + } + + async random(ctx) { + const { type, data } = await this.fetchFirstSuccess(["https://api.miaomc.cn/image/get"]) + if (type === "blob") { + ctx.set("Content-Type", "image/jpeg") + // 下载 + // ctx.set("Content-Disposition", "attachment; filename=random.jpg") + ctx.body = data + } else { + R.response(R.ERROR, "Failed to fetch image") + } + } + + /** + * 路由注册 + */ + static createRoutes() { + const controller = new AuthController() + const router = new Router({ prefix: "/api/pics" }) + router.get("/random", controller.random.bind(controller), { auth: "try" }) + return router + } +} + +export default AuthController diff --git a/src/modules/Auth/controller/index.js b/src/modules/Auth/controller/index.js new file mode 100644 index 0000000..172cda9 --- /dev/null +++ b/src/modules/Auth/controller/index.js @@ -0,0 +1,75 @@ +import Router from "utils/router.js" +import { logger } from "@/logger.js" +import BaseController from "@/base/BaseController.js" +import AuthService from "../services" + +export default class AuthController extends BaseController { + /** + * 创建基础页面相关路由 + * @returns {Router} 路由实例 + */ + static createRoutes() { + const controller = new this() + const router = new Router({ auth: false }) + + router.get("/login", controller.handleRequest(controller.loginGet)) + router.post("/login", controller.handleRequest(controller.loginPost)) + + router.post("/login/validate/username", controller.handleRequest(controller.validateUsername)) + router.post("/login/validate/password", controller.handleRequest(controller.validatePassword)) + + router.post("/logout", controller.handleRequest(controller.logout), { auth: true }) + + return router + } + + constructor() { + super() + } + + // 首页 + async loginGet(ctx) { + return this.render(ctx, "page/login/index", {}) + } + + async loginPost(ctx) { + const res = await AuthService.login(ctx.request.body) + ctx.session.user = res.user + ctx.set("HX-Redirect", "/") + } + + async validateUsername(ctx) { + const { username } = ctx.request.body + const uiPath = "page/login/_ui/username" + if (username === "") { + return this.render(ctx, uiPath, { + value: username, + error: "用户名不能为空", + }) + } + return this.render(ctx, uiPath, { + value: username, + error: undefined, + }) + } + + async validatePassword(ctx) { + const { password } = ctx.request.body + const uiPath = "page/login/_ui/password" + if (password === "") { + return this.render(ctx, uiPath, { + value: password, + error: "密码不能为空", + }) + } + return this.render(ctx, uiPath, { + value: password, + error: undefined, + }) + } + + async logout(ctx) { + ctx.session.user = null + ctx.set("HX-Redirect", "/") + } +} diff --git a/src/modules/Auth/services/index.js b/src/modules/Auth/services/index.js new file mode 100644 index 0000000..063d413 --- /dev/null +++ b/src/modules/Auth/services/index.js @@ -0,0 +1,88 @@ +import UserModel from "@/db/models/UserModel.js" +import CommonError from "@/utils/error/CommonError.js" +import { comparePassword } from "@/utils/bcrypt.js" +import { JWT_SECRET } from "@/middlewares/Auth/index.js" +import jwt from "jsonwebtoken" + +/** + * 认证服务类 + * 提供认证相关的业务逻辑 + */ +export class AuthService { + // 注册新用户 + static async register(data) { + try { + if (!data.username || !data.password) { + throw new CommonError("用户名和密码不能为空") + } + + // 检查用户名是否已存在 + const existUser = await UserModel.getInstance().findByUsername(data.username) + if (existUser) { + throw new CommonError(`用户名${data.username}已存在`) + } + + // 检查邮箱是否已存在 + if (data.email) { + const existEmail = await UserModel.getInstance().findByEmail(data.email) + if (existEmail) { + throw new CommonError(`邮箱${data.email}已被使用`) + } + } + + // 密码加密 + const hashed = await hashPassword(data.password) + + const user = await UserModel.getInstance().create({ ...data, password: hashed }) + + // 返回脱敏信息 + const { password, ...userInfo } = Array.isArray(user) ? user[0] : user + return userInfo + } catch (error) { + if (error instanceof CommonError) throw error + throw new CommonError(`注册失败: ${error.message}`) + } + } + + // 登录 + static async login({ username, email, password }) { + try { + if (!password) { + throw new CommonError("密码不能为空") + } + + if (!username && !email) { + throw new CommonError("用户名或邮箱不能为空") + } + + let user + if (username) { + user = await UserModel.getInstance().findByUsername(username) + } else if (email) { + user = await UserModel.getInstance().findByEmail(email) + } + + if (!user) { + throw new CommonError("用户不存在") + } + + // 校验密码 + const ok = await comparePassword(password, user.password) + if (!ok) { + throw new CommonError("密码错误") + } + + // 生成token + const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: "2h" }) + + // 返回token和用户信息 + const { password: pwd, ...userInfo } = user + return { token, user: userInfo } + } catch (error) { + if (error instanceof CommonError) throw error + throw new CommonError(`登录失败: ${error.message}`) + } + } +} + +export default AuthService diff --git a/src/modules/Contact/controller/index.js b/src/modules/Contact/controller/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/Contact/services/index.js b/src/modules/Contact/services/index.js new file mode 100644 index 0000000..0d5b481 --- /dev/null +++ b/src/modules/Contact/services/index.js @@ -0,0 +1,511 @@ +import ContactModel from "@/db/models/ContactModel.js" +import { logger } from "@/logger.js" + +/** + * 联系信息服务类 + * 提供联系信息相关的业务逻辑 + */ +class ContactService { + /** + * 创建新联系信息 + * @param {Object} contactData - 联系信息数据 + * @returns {Promise} 创建的联系信息 + */ + static async createContact(contactData) { + try { + // 数据验证 + this.validateContactData(contactData) + + // 创建联系信息 + const contact = await ContactModel.create(contactData) + + logger.info(`联系信息创建成功: ${contact.name} (ID: ${contact.id})`) + return this.formatContactResponse(contact) + } catch (error) { + logger.error(`创建联系信息失败:`, error) + throw error + } + } + + /** + * 根据ID获取联系信息 + * @param {number} id - 联系信息ID + * @returns {Promise} 联系信息 + */ + static async getContactById(id) { + try { + const contact = await ContactModel.findById(id) + return contact ? this.formatContactResponse(contact) : null + } catch (error) { + logger.error(`获取联系信息失败 (ID: ${id}):`, error) + throw error + } + } + + /** + * 更新联系信息 + * @param {number} id - 联系信息ID + * @param {Object} updateData - 更新数据 + * @returns {Promise} 更新后的联系信息 + */ + static async updateContact(id, updateData) { + try { + // 验证联系信息是否存在 + const existingContact = await ContactModel.findById(id) + if (!existingContact) { + throw new Error("联系信息不存在") + } + + // 数据验证 + this.validateContactUpdateData(updateData) + + // 更新联系信息 + const contact = await ContactModel.update(id, updateData) + + logger.info(`联系信息更新成功: ${contact.name} (ID: ${id})`) + return this.formatContactResponse(contact) + } catch (error) { + logger.error(`更新联系信息失败 (ID: ${id}):`, error) + throw error + } + } + + /** + * 删除联系信息 + * @param {number} id - 联系信息ID + * @returns {Promise} 删除结果 + */ + static async deleteContact(id) { + try { + const contact = await ContactModel.findById(id) + if (!contact) { + throw new Error("联系信息不存在") + } + + const result = await ContactModel.delete(id) + + logger.info(`联系信息删除成功: ${contact.name} (ID: ${id})`) + return result > 0 + } catch (error) { + logger.error(`删除联系信息失败 (ID: ${id}):`, error) + throw error + } + } + + /** + * 获取联系信息列表 + * @param {Object} options - 查询选项 + * @returns {Promise} 联系信息列表和分页信息 + */ + static async getContactList(options = {}) { + try { + const { + page = 1, + limit = 20, + search = "", + status = null, + orderBy = "created_at", + order = "desc" + } = options + + const where = {} + if (status) where.status = status + + const result = await ContactModel.paginate({ + page, + limit, + where, + search, + searchFields: ContactModel.searchableFields, + orderBy, + order + }) + + return { + contacts: result.data.map(contact => this.formatContactResponse(contact)), + pagination: result.pagination + } + } catch (error) { + logger.error(`获取联系信息列表失败:`, error) + throw error + } + } + + /** + * 根据邮箱获取联系信息 + * @param {string} email - 邮箱 + * @returns {Promise} 联系信息列表 + */ + static async getContactsByEmail(email) { + try { + const contacts = await ContactModel.findByEmail(email) + return contacts.map(contact => this.formatContactResponse(contact)) + } catch (error) { + logger.error(`根据邮箱获取联系信息失败 (${email}):`, error) + throw error + } + } + + /** + * 根据状态获取联系信息 + * @param {string} status - 状态 + * @returns {Promise} 联系信息列表 + */ + static async getContactsByStatus(status) { + try { + const contacts = await ContactModel.findByStatus(status) + return contacts.map(contact => this.formatContactResponse(contact)) + } catch (error) { + logger.error(`根据状态获取联系信息失败 (${status}):`, error) + throw error + } + } + + /** + * 根据日期范围获取联系信息 + * @param {string} startDate - 开始日期 + * @param {string} endDate - 结束日期 + * @returns {Promise} 联系信息列表 + */ + static async getContactsByDateRange(startDate, endDate) { + try { + const contacts = await ContactModel.findByDateRange(startDate, endDate) + return contacts.map(contact => this.formatContactResponse(contact)) + } catch (error) { + logger.error(`根据日期范围获取联系信息失败:`, error) + throw error + } + } + + /** + * 标记为已读 + * @param {number} id - 联系信息ID + * @returns {Promise} 更新后的联系信息 + */ + static async markAsRead(id) { + try { + const contact = await ContactModel.markAsRead(id) + logger.info(`联系信息标记为已读成功 (ID: ${id})`) + return this.formatContactResponse(contact) + } catch (error) { + logger.error(`标记联系信息为已读失败 (ID: ${id}):`, error) + throw error + } + } + + /** + * 标记为已回复 + * @param {number} id - 联系信息ID + * @returns {Promise} 更新后的联系信息 + */ + static async markAsReplied(id) { + try { + const contact = await ContactModel.markAsReplied(id) + logger.info(`联系信息标记为已回复成功 (ID: ${id})`) + return this.formatContactResponse(contact) + } catch (error) { + logger.error(`标记联系信息为已回复失败 (ID: ${id}):`, error) + throw error + } + } + + /** + * 批量更新状态 + * @param {Array} ids - 联系信息ID数组 + * @param {string} status - 新状态 + * @returns {Promise} 更新数量 + */ + static async batchUpdateStatus(ids, status) { + try { + if (!Array.isArray(ids) || ids.length === 0) { + throw new Error("联系信息ID数组不能为空") + } + + if (!["unread", "read", "replied"].includes(status)) { + throw new Error("无效的联系信息状态") + } + + const result = await ContactModel.updateStatusBatchByIds(ids, status) + + logger.info(`批量更新联系信息状态成功: ${ids.length} 条记录状态更新为 ${status}`) + return result + } catch (error) { + logger.error(`批量更新联系信息状态失败:`, error) + throw error + } + } + + /** + * 批量删除联系信息 + * @param {Array} ids - 联系信息ID数组 + * @returns {Promise} 删除数量 + */ + static async batchDeleteContacts(ids) { + try { + if (!Array.isArray(ids) || ids.length === 0) { + throw new Error("联系信息ID数组不能为空") + } + + const result = await ContactModel.deleteWhere({ id: ids }) + + logger.info(`批量删除联系信息成功: ${result} 条记录`) + return result + } catch (error) { + logger.error(`批量删除联系信息失败:`, error) + throw error + } + } + + /** + * 获取联系信息统计 + * @returns {Promise} 统计信息 + */ + static async getContactStats() { + try { + const stats = await ContactModel.getStats() + const todayCount = await ContactModel.getTodayCount() + + return { + ...stats, + today: todayCount + } + } catch (error) { + logger.error(`获取联系信息统计失败:`, error) + throw error + } + } + + /** + * 获取今日新联系数量 + * @returns {Promise} 今日新联系数量 + */ + static async getTodayContactCount() { + try { + return await ContactModel.getTodayCount() + } catch (error) { + logger.error(`获取今日新联系数量失败:`, error) + throw error + } + } + + /** + * 搜索联系信息 + * @param {string} keyword - 搜索关键词 + * @param {Object} options - 搜索选项 + * @returns {Promise} 搜索结果 + */ + static async searchContacts(keyword, options = {}) { + try { + const { + status = null, + limit = 20, + orderBy = "created_at", + order = "desc" + } = options + + const where = {} + if (status) where.status = status + + const contacts = await ContactModel.findWhere(where, { + search: keyword, + searchFields: ContactModel.searchableFields, + limit, + orderBy, + order + }) + + return contacts.map(contact => this.formatContactResponse(contact)) + } catch (error) { + logger.error(`搜索联系信息失败:`, error) + throw error + } + } + + /** + * 获取未读联系信息数量 + * @returns {Promise} 未读数量 + */ + static async getUnreadCount() { + try { + return await ContactModel.count({ status: "unread" }) + } catch (error) { + logger.error(`获取未读联系信息数量失败:`, error) + throw error + } + } + + /** + * 获取最近联系信息 + * @param {number} limit - 数量限制 + * @returns {Promise} 最近联系信息列表 + */ + static async getRecentContacts(limit = 10) { + try { + const contacts = await ContactModel.findWhere( + {}, + { orderBy: "created_at", order: "desc", limit } + ) + return contacts.map(contact => this.formatContactResponse(contact)) + } catch (error) { + logger.error(`获取最近联系信息失败:`, error) + throw error + } + } + + /** + * 验证联系信息数据 + * @param {Object} contactData - 联系信息数据 + */ + static validateContactData(contactData) { + if (!contactData.name) { + throw new Error("姓名不能为空") + } + if (!contactData.email) { + throw new Error("邮箱不能为空") + } + if (!contactData.subject) { + throw new Error("主题不能为空") + } + if (!contactData.message) { + throw new Error("消息内容不能为空") + } + + // 邮箱格式验证 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(contactData.email)) { + throw new Error("邮箱格式不正确") + } + + // 姓名长度验证 + if (contactData.name.length > 100) { + throw new Error("姓名不能超过100个字符") + } + + // 主题长度验证 + if (contactData.subject.length > 200) { + throw new Error("主题不能超过200个字符") + } + + // 消息长度验证 + if (contactData.message.length > 2000) { + throw new Error("消息内容不能超过2000个字符") + } + } + + /** + * 验证联系信息更新数据 + * @param {Object} updateData - 更新数据 + */ + static validateContactUpdateData(updateData) { + if (updateData.email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(updateData.email)) { + throw new Error("邮箱格式不正确") + } + } + + if (updateData.name && updateData.name.length > 100) { + throw new Error("姓名不能超过100个字符") + } + + if (updateData.subject && updateData.subject.length > 200) { + throw new Error("主题不能超过200个字符") + } + + if (updateData.message && updateData.message.length > 2000) { + throw new Error("消息内容不能超过2000个字符") + } + + if (updateData.status && !["unread", "read", "replied"].includes(updateData.status)) { + throw new Error("无效的状态值") + } + } + + /** + * 格式化联系信息响应数据 + * @param {Object} contact - 联系信息数据 + * @returns {Object} 格式化后的联系信息数据 + */ + static formatContactResponse(contact) { + return { + ...contact, + // 确保数字字段为数字类型 + id: parseInt(contact.id), + // 格式化日期字段 + created_at: contact.created_at, + updated_at: contact.updated_at + } + } + + /** + * 导出联系信息 + * @param {Object} options - 导出选项 + * @returns {Promise} 导出的联系信息 + */ + static async exportContacts(options = {}) { + try { + const { + status = null, + startDate = null, + endDate = null, + limit = 1000 + } = options + + let where = {} + if (status) where.status = status + + let contacts + if (startDate && endDate) { + contacts = await ContactModel.findByDateRange(startDate, endDate) + } else { + contacts = await ContactModel.findWhere(where, { + orderBy: "created_at", + order: "desc", + limit + }) + } + + return contacts.map(contact => this.formatContactResponse(contact)) + } catch (error) { + logger.error(`导出联系信息失败:`, error) + throw error + } + } + + /** + * 获取联系信息趋势数据 + * @param {number} days - 天数 + * @returns {Promise} 趋势数据 + */ + static async getContactTrends(days = 30) { + try { + const endDate = new Date() + const startDate = new Date() + startDate.setDate(startDate.getDate() - days) + + const contacts = await ContactModel.findByDateRange( + startDate.toISOString().split('T')[0], + endDate.toISOString().split('T')[0] + ) + + // 按日期分组统计 + const trends = {} + contacts.forEach(contact => { + const date = contact.created_at.split('T')[0] + if (!trends[date]) { + trends[date] = { date, count: 0 } + } + trends[date].count++ + }) + + return Object.values(trends).sort((a, b) => a.date.localeCompare(b.date)) + } catch (error) { + logger.error(`获取联系信息趋势数据失败:`, error) + throw error + } + } +} + +export default ContactService +export { ContactService } + diff --git a/src/modules/Index/controller/index.js b/src/modules/Index/controller/index.js new file mode 100644 index 0000000..d77cea1 --- /dev/null +++ b/src/modules/Index/controller/index.js @@ -0,0 +1,61 @@ +import Router from "utils/router.js" +import { logger } from "@/logger.js" +import BaseController from "@/base/BaseController.js" + +export default class CommonController extends BaseController { + constructor() { + super() + } + + pageGet(...args) { + return (ctx) => { + return ctx.render(...args) + } + } + + // 首页 + async indexGet(ctx) { + // 可以在这里添加一些需要用户信息的逻辑 + // 例如获取用户相关的统计数据等 + const user = ctx.state.user || null; + + // 示例数据,实际项目中可以从数据库获取 + const stats = { + articles: 1234, + users: 567, + categories: 89, + responseTime: "24h" + }; + + return await ctx.render( + "page/index/index", + { + stats, + // 其他需要传递给模板的数据 + } + ) + } + + /** + * 创建基础页面相关路由 + * @returns {Router} 路由实例 + */ + static createRoutes() { + const controller = new CommonController() + const router = new Router({ auth: "try" }) + + // 首页 + router.get("", controller.handleRequest(controller.indexGet), { auth: "try" }) + router.get("/", controller.handleRequest(controller.indexGet), { auth: "try" }) + // router.get("/about", controller.handleRequest(controller.pageGet("page/about/index"))) + router.get("/contact", controller.handleRequest(controller.pageGet("page/extra/contact"))) + router.get("/faq", controller.handleRequest(controller.pageGet("page/extra/faq"))) + router.get("/feedback", controller.handleRequest(controller.pageGet("page/extra/feedback"))) + router.get("/help", controller.handleRequest(controller.pageGet("page/extra/help"))) + router.get("/privacy", controller.handleRequest(controller.pageGet("page/extra/privacy"))) + router.get("/terms", controller.handleRequest(controller.pageGet("page/extra/terms"))) + router.get("/no-auth", controller.handleRequest(controller.pageGet("page/auth/no-auth"))) + + return router + } +} \ No newline at end of file diff --git a/src/modules/Index/services/index.js b/src/modules/Index/services/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/Job/controller/index.js b/src/modules/Job/controller/index.js new file mode 100644 index 0000000..44c8655 --- /dev/null +++ b/src/modules/Job/controller/index.js @@ -0,0 +1,47 @@ +// Job Controller 示例:如何调用 service 层动态控制和查询定时任务 +import JobService from "../services" +import { R } from "@/utils/helper.js" +import Router from "@/utils/router.js" + +class JobController { + constructor() { + this.jobService = new JobService() + } + + async list(ctx) { + const data = this.jobService.listJobs() + R.response(R.SUCCESS,data) + } + + async start(ctx) { + const { id } = ctx.params + this.jobService.startJob(id) + R.response(R.SUCCESS,null, `${id} 任务已启动`) + } + + async stop(ctx) { + const { id } = ctx.params + this.jobService.stopJob(id) + R.response(R.SUCCESS,null, `${id} 任务已停止`) + } + + async updateCron(ctx) { + const { id } = ctx.params + const { cronTime } = ctx.request.body + this.jobService.updateJobCron(id, cronTime) + R.response(R.SUCCESS,null, `${id} 任务频率已修改`) + } + + static createRoutes() { + const controller = new this() + const router = new Router({ prefix: "/api/jobs", auth: "try" }) + router.get("", controller.list.bind(controller)) + router.get("/", controller.list.bind(controller)) + router.post("/start/:id", controller.start.bind(controller)) + router.post("/stop/:id", controller.stop.bind(controller)) + router.post("/update/:id", controller.updateCron.bind(controller)) + return router + } +} + +export default JobController diff --git a/src/modules/Job/services/index.js b/src/modules/Job/services/index.js new file mode 100644 index 0000000..921d89d --- /dev/null +++ b/src/modules/Job/services/index.js @@ -0,0 +1,18 @@ +import jobs from "@/jobs" + +class JobService { + startJob(id) { + return jobs.start(id) + } + stopJob(id) { + return jobs.stop(id) + } + updateJobCron(id, cronTime) { + return jobs.updateCronTime(id, cronTime) + } + listJobs() { + return jobs.list() + } +} + +export default JobService diff --git a/src/modules/SiteConfig/services/index.js b/src/modules/SiteConfig/services/index.js new file mode 100644 index 0000000..0d414a2 --- /dev/null +++ b/src/modules/SiteConfig/services/index.js @@ -0,0 +1,513 @@ +import SiteConfigModel from "@/db/models/SiteConfigModel.js" +import { logger } from "@/logger.js" + +/** + * 站点配置服务类 + * 提供站点配置相关的业务逻辑 + */ +class SiteConfigService { + /** + * 获取配置值 + * @param {string} key - 配置键 + * @param {*} defaultValue - 默认值 + * @returns {Promise<*>} 配置值 + */ + static async get(key, defaultValue = null) { + try { + const value = await SiteConfigModel.get(key) + return value !== null ? value : defaultValue + } catch (error) { + logger.error(`获取配置失败 (${key}):`, error) + throw error + } + } + + /** + * 设置配置值 + * @param {string} key - 配置键 + * @param {*} value - 配置值 + * @returns {Promise} 配置对象 + */ + static async set(key, value) { + try { + // 验证配置键 + this.validateConfigKey(key) + + // 序列化值 + const serializedValue = this.serializeValue(value) + + const config = await SiteConfigModel.set(key, serializedValue) + + logger.info(`配置设置成功: ${key}`) + return this.formatConfigResponse(config) + } catch (error) { + logger.error(`设置配置失败 (${key}):`, error) + throw error + } + } + + /** + * 批量获取配置 + * @param {Array} keys - 配置键数组 + * @returns {Promise} 配置对象 + */ + static async getMany(keys) { + try { + if (!Array.isArray(keys) || keys.length === 0) { + return {} + } + + const configs = await SiteConfigModel.getMany(keys) + + // 反序列化值 + const result = {} + for (const [key, value] of Object.entries(configs)) { + result[key] = this.deserializeValue(value) + } + + return result + } catch (error) { + logger.error(`批量获取配置失败:`, error) + throw error + } + } + + /** + * 获取所有配置 + * @returns {Promise} 所有配置 + */ + static async getAll() { + try { + const configs = await SiteConfigModel.getAll() + + // 反序列化值 + const result = {} + for (const [key, value] of Object.entries(configs)) { + result[key] = this.deserializeValue(value) + } + + return result + } catch (error) { + logger.error(`获取所有配置失败:`, error) + throw error + } + } + + /** + * 批量设置配置 + * @param {Object} configs - 配置对象 + * @returns {Promise} 设置结果 + */ + static async setMany(configs) { + try { + if (!configs || typeof configs !== 'object') { + throw new Error("配置对象不能为空") + } + + const results = [] + for (const [key, value] of Object.entries(configs)) { + try { + this.validateConfigKey(key) + const serializedValue = this.serializeValue(value) + const config = await SiteConfigModel.set(key, serializedValue) + results.push(this.formatConfigResponse(config)) + } catch (error) { + logger.error(`设置配置失败 (${key}):`, error) + results.push({ key, error: error.message }) + } + } + + logger.info(`批量设置配置完成: ${Object.keys(configs).length} 个配置`) + return results + } catch (error) { + logger.error(`批量设置配置失败:`, error) + throw error + } + } + + /** + * 删除配置 + * @param {string} key - 配置键 + * @returns {Promise} 删除结果 + */ + static async delete(key) { + try { + const result = await SiteConfigModel.deleteByKey(key) + + logger.info(`配置删除成功: ${key}`) + return result > 0 + } catch (error) { + logger.error(`删除配置失败 (${key}):`, error) + throw error + } + } + + /** + * 检查配置是否存在 + * @param {string} key - 配置键 + * @returns {Promise} 是否存在 + */ + static async has(key) { + try { + return await SiteConfigModel.hasKey(key) + } catch (error) { + logger.error(`检查配置是否存在失败 (${key}):`, error) + throw error + } + } + + /** + * 获取配置统计 + * @returns {Promise} 统计信息 + */ + static async getStats() { + try { + return await SiteConfigModel.getConfigStats() + } catch (error) { + logger.error(`获取配置统计失败:`, error) + throw error + } + } + + /** + * 获取站点基本信息配置 + * @returns {Promise} 站点基本信息 + */ + static async getSiteInfo() { + try { + const keys = [ + 'site_name', + 'site_description', + 'site_keywords', + 'site_author', + 'site_url', + 'site_logo', + 'site_favicon', + 'site_theme', + 'site_language', + 'site_timezone' + ] + + const configs = await this.getMany(keys) + + return { + name: configs.site_name || '我的网站', + description: configs.site_description || '', + keywords: configs.site_keywords || '', + author: configs.site_author || '', + url: configs.site_url || '', + logo: configs.site_logo || '', + favicon: configs.site_favicon || '', + theme: configs.site_theme || 'default', + language: configs.site_language || 'zh-CN', + timezone: configs.site_timezone || 'Asia/Shanghai' + } + } catch (error) { + logger.error(`获取站点基本信息失败:`, error) + throw error + } + } + + /** + * 设置站点基本信息配置 + * @param {Object} siteInfo - 站点信息 + * @returns {Promise} 设置结果 + */ + static async setSiteInfo(siteInfo) { + try { + const configs = {} + + if (siteInfo.name) configs.site_name = siteInfo.name + if (siteInfo.description) configs.site_description = siteInfo.description + if (siteInfo.keywords) configs.site_keywords = siteInfo.keywords + if (siteInfo.author) configs.site_author = siteInfo.author + if (siteInfo.url) configs.site_url = siteInfo.url + if (siteInfo.logo) configs.site_logo = siteInfo.logo + if (siteInfo.favicon) configs.site_favicon = siteInfo.favicon + if (siteInfo.theme) configs.site_theme = siteInfo.theme + if (siteInfo.language) configs.site_language = siteInfo.language + if (siteInfo.timezone) configs.site_timezone = siteInfo.timezone + + return await this.setMany(configs) + } catch (error) { + logger.error(`设置站点基本信息失败:`, error) + throw error + } + } + + /** + * 获取邮件配置 + * @returns {Promise} 邮件配置 + */ + static async getEmailConfig() { + try { + const keys = [ + 'email_host', + 'email_port', + 'email_secure', + 'email_user', + 'email_password', + 'email_from', + 'email_name' + ] + + const configs = await this.getMany(keys) + + return { + host: configs.email_host || '', + port: parseInt(configs.email_port) || 587, + secure: configs.email_secure === 'true', + user: configs.email_user || '', + password: configs.email_password || '', + from: configs.email_from || '', + name: configs.email_name || '' + } + } catch (error) { + logger.error(`获取邮件配置失败:`, error) + throw error + } + } + + /** + * 设置邮件配置 + * @param {Object} emailConfig - 邮件配置 + * @returns {Promise} 设置结果 + */ + static async setEmailConfig(emailConfig) { + try { + const configs = {} + + if (emailConfig.host) configs.email_host = emailConfig.host + if (emailConfig.port) configs.email_port = emailConfig.port.toString() + if (emailConfig.secure !== undefined) configs.email_secure = emailConfig.secure.toString() + if (emailConfig.user) configs.email_user = emailConfig.user + if (emailConfig.password) configs.email_password = emailConfig.password + if (emailConfig.from) configs.email_from = emailConfig.from + if (emailConfig.name) configs.email_name = emailConfig.name + + return await this.setMany(configs) + } catch (error) { + logger.error(`设置邮件配置失败:`, error) + throw error + } + } + + /** + * 获取系统配置 + * @returns {Promise} 系统配置 + */ + static async getSystemConfig() { + try { + const keys = [ + 'maintenance_mode', + 'registration_enabled', + 'email_verification_required', + 'max_upload_size', + 'allowed_file_types', + 'session_timeout', + 'password_min_length', + 'login_attempts_limit' + ] + + const configs = await this.getMany(keys) + + return { + maintenanceMode: configs.maintenance_mode === 'true', + registrationEnabled: configs.registration_enabled !== 'false', + emailVerificationRequired: configs.email_verification_required === 'true', + maxUploadSize: parseInt(configs.max_upload_size) || 10485760, // 10MB + allowedFileTypes: configs.allowed_file_types ? configs.allowed_file_types.split(',') : ['jpg', 'jpeg', 'png', 'gif', 'pdf'], + sessionTimeout: parseInt(configs.session_timeout) || 3600, // 1小时 + passwordMinLength: parseInt(configs.password_min_length) || 6, + loginAttemptsLimit: parseInt(configs.login_attempts_limit) || 5 + } + } catch (error) { + logger.error(`获取系统配置失败:`, error) + throw error + } + } + + /** + * 设置系统配置 + * @param {Object} systemConfig - 系统配置 + * @returns {Promise} 设置结果 + */ + static async setSystemConfig(systemConfig) { + try { + const configs = {} + + if (systemConfig.maintenanceMode !== undefined) configs.maintenance_mode = systemConfig.maintenanceMode.toString() + if (systemConfig.registrationEnabled !== undefined) configs.registration_enabled = systemConfig.registrationEnabled.toString() + if (systemConfig.emailVerificationRequired !== undefined) configs.email_verification_required = systemConfig.emailVerificationRequired.toString() + if (systemConfig.maxUploadSize) configs.max_upload_size = systemConfig.maxUploadSize.toString() + if (systemConfig.allowedFileTypes) configs.allowed_file_types = Array.isArray(systemConfig.allowedFileTypes) ? systemConfig.allowedFileTypes.join(',') : systemConfig.allowedFileTypes + if (systemConfig.sessionTimeout) configs.session_timeout = systemConfig.sessionTimeout.toString() + if (systemConfig.passwordMinLength) configs.password_min_length = systemConfig.passwordMinLength.toString() + if (systemConfig.loginAttemptsLimit) configs.login_attempts_limit = systemConfig.loginAttemptsLimit.toString() + + return await this.setMany(configs) + } catch (error) { + logger.error(`设置系统配置失败:`, error) + throw error + } + } + + /** + * 重置配置为默认值 + * @param {Array} keys - 要重置的配置键数组(可选,默认重置所有) + * @returns {Promise} 重置结果 + */ + static async resetToDefaults(keys = null) { + try { + const defaultConfigs = { + site_name: '我的网站', + site_description: '欢迎来到我的网站', + site_keywords: '网站,博客,个人网站', + site_author: '网站管理员', + site_url: 'http://localhost:3000', + site_theme: 'default', + site_language: 'zh-CN', + site_timezone: 'Asia/Shanghai', + maintenance_mode: 'false', + registration_enabled: 'true', + email_verification_required: 'false', + max_upload_size: '10485760', + allowed_file_types: 'jpg,jpeg,png,gif,pdf', + session_timeout: '3600', + password_min_length: '6', + login_attempts_limit: '5' + } + + const configsToReset = keys ? + Object.fromEntries(keys.filter(key => defaultConfigs[key]).map(key => [key, defaultConfigs[key]])) : + defaultConfigs + + return await this.setMany(configsToReset) + } catch (error) { + logger.error(`重置配置为默认值失败:`, error) + throw error + } + } + + /** + * 验证配置键 + * @param {string} key - 配置键 + */ + static validateConfigKey(key) { + if (!key || typeof key !== 'string') { + throw new Error("配置键不能为空") + } + + if (key.length > 100) { + throw new Error("配置键长度不能超过100个字符") + } + + if (!/^[a-zA-Z0-9_]+$/.test(key)) { + throw new Error("配置键只能包含字母、数字和下划线") + } + } + + /** + * 序列化值 + * @param {*} value - 要序列化的值 + * @returns {string} 序列化后的字符串 + */ + static serializeValue(value) { + if (value === null || value === undefined) { + return '' + } + + if (typeof value === 'string') { + return value + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return value.toString() + } + + return JSON.stringify(value) + } + + /** + * 反序列化值 + * @param {string} value - 要反序列化的字符串 + * @returns {*} 反序列化后的值 + */ + static deserializeValue(value) { + if (value === null || value === undefined || value === '') { + return null + } + + // 尝试解析为JSON + try { + return JSON.parse(value) + } catch (e) { + // 如果不是有效的JSON,返回原字符串 + return value + } + } + + /** + * 格式化配置响应数据 + * @param {Object} config - 配置数据 + * @returns {Object} 格式化后的配置数据 + */ + static formatConfigResponse(config) { + return { + ...config, + // 确保数字字段为数字类型 + id: parseInt(config.id), + // 反序列化值 + value: this.deserializeValue(config.value), + // 格式化日期字段 + created_at: config.created_at, + updated_at: config.updated_at + } + } + + /** + * 导出配置 + * @returns {Promise} 导出的配置 + */ + static async exportConfig() { + try { + const configs = await this.getAll() + return { + exported_at: new Date().toISOString(), + configs + } + } catch (error) { + logger.error(`导出配置失败:`, error) + throw error + } + } + + /** + * 导入配置 + * @param {Object} configData - 配置数据 + * @returns {Promise} 导入结果 + */ + static async importConfig(configData) { + try { + if (!configData || !configData.configs) { + throw new Error("无效的配置数据") + } + + const results = await this.setMany(configData.configs) + + logger.info(`配置导入完成: ${Object.keys(configData.configs).length} 个配置`) + return { + success: results.filter(r => !r.error).length, + failed: results.filter(r => r.error).length, + results + } + } catch (error) { + logger.error(`导入配置失败:`, error) + throw error + } + } +} + +export default SiteConfigService +export { SiteConfigService } + diff --git a/src/services/ArticleService.js b/src/services/ArticleService.js deleted file mode 100644 index 1e7d8b0..0000000 --- a/src/services/ArticleService.js +++ /dev/null @@ -1,563 +0,0 @@ -import ArticleModel from "../db/models/ArticleModel.js" -import { logger } from "../logger.js" - -/** - * 文章服务类 - * 提供文章相关的业务逻辑 - */ -class ArticleService { - /** - * 创建新文章 - * @param {Object} articleData - 文章数据 - * @returns {Promise} 创建的文章信息 - */ - static async createArticle(articleData) { - try { - // 数据验证 - this.validateArticleData(articleData) - - // 创建文章 - const article = await ArticleModel.create(articleData) - - logger.info(`文章创建成功: ${article.title} (ID: ${article.id})`) - return this.formatArticleResponse(article) - } catch (error) { - logger.error(`创建文章失败:`, error) - throw error - } - } - - /** - * 根据ID获取文章 - * @param {number} id - 文章ID - * @param {boolean} incrementView - 是否增加浏览量 - * @returns {Promise} 文章信息 - */ - static async getArticleById(id, incrementView = false) { - try { - const article = await ArticleModel.findById(id) - if (!article) { - return null - } - - // 如果需要增加浏览量 - if (incrementView) { - await ArticleModel.incrementViewCount(id) - article.view_count = (article.view_count || 0) + 1 - } - - return this.formatArticleResponse(article) - } catch (error) { - logger.error(`获取文章失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 根据slug获取文章 - * @param {string} slug - 文章slug - * @param {boolean} incrementView - 是否增加浏览量 - * @returns {Promise} 文章信息 - */ - static async getArticleBySlug(slug, incrementView = false) { - try { - const article = await ArticleModel.findBySlug(slug) - if (!article) { - return null - } - - // 如果需要增加浏览量 - if (incrementView) { - await ArticleModel.incrementViewCount(article.id) - article.view_count = (article.view_count || 0) + 1 - } - - return this.formatArticleResponse(article) - } catch (error) { - logger.error(`根据slug获取文章失败 (${slug}):`, error) - throw error - } - } - - /** - * 更新文章 - * @param {number} id - 文章ID - * @param {Object} updateData - 更新数据 - * @returns {Promise} 更新后的文章信息 - */ - static async updateArticle(id, updateData) { - try { - // 验证文章是否存在 - const existingArticle = await ArticleModel.findById(id) - if (!existingArticle) { - throw new Error("文章不存在") - } - - // 数据验证 - this.validateArticleUpdateData(updateData) - - // 更新文章 - const article = await ArticleModel.update(id, updateData) - - logger.info(`文章更新成功: ${article.title} (ID: ${id})`) - return this.formatArticleResponse(article) - } catch (error) { - logger.error(`更新文章失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 删除文章 - * @param {number} id - 文章ID - * @returns {Promise} 删除结果 - */ - static async deleteArticle(id) { - try { - const article = await ArticleModel.findById(id) - if (!article) { - throw new Error("文章不存在") - } - - const result = await ArticleModel.delete(id) - - logger.info(`文章删除成功: ${article.title} (ID: ${id})`) - return result > 0 - } catch (error) { - logger.error(`删除文章失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 发布文章 - * @param {number} id - 文章ID - * @returns {Promise} 发布后的文章信息 - */ - static async publishArticle(id) { - try { - const article = await ArticleModel.publish(id) - logger.info(`文章发布成功: ${article.title} (ID: ${id})`) - return this.formatArticleResponse(article) - } catch (error) { - logger.error(`发布文章失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 取消发布文章 - * @param {number} id - 文章ID - * @returns {Promise} 取消发布后的文章信息 - */ - static async unpublishArticle(id) { - try { - const article = await ArticleModel.unpublish(id) - logger.info(`文章取消发布成功: ${article.title} (ID: ${id})`) - return this.formatArticleResponse(article) - } catch (error) { - logger.error(`取消发布文章失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 获取文章列表 - * @param {Object} options - 查询选项 - * @returns {Promise} 文章列表和分页信息 - */ - static async getArticleList(options = {}) { - try { - const { - page = 1, - limit = 10, - search = "", - category = null, - status = null, - author = null, - orderBy = "created_at", - order = "desc" - } = options - - const where = {} - if (category) where.category = category - if (status) where.status = status - if (author) where.author = author - - const result = await ArticleModel.paginate({ - page, - limit, - where, - search, - searchFields: ArticleModel.searchableFields, - orderBy, - order - }) - - return { - articles: result.data.map(article => this.formatArticleResponse(article)), - pagination: result.pagination - } - } catch (error) { - logger.error(`获取文章列表失败:`, error) - throw error - } - } - - /** - * 获取已发布文章列表 - * @param {Object} options - 查询选项 - * @returns {Promise} 文章列表和分页信息 - */ - static async getPublishedArticles(options = {}) { - try { - const { - page = 1, - limit = 10, - search = "", - category = null, - author = null, - orderBy = "published_at", - order = "desc" - } = options - - const where = { status: "published" } - if (category) where.category = category - if (author) where.author = author - - const result = await ArticleModel.paginate({ - page, - limit, - where, - search, - searchFields: ArticleModel.searchableFields, - orderBy, - order - }) - - return { - articles: result.data.map(article => this.formatArticleResponse(article)), - pagination: result.pagination - } - } catch (error) { - logger.error(`获取已发布文章列表失败:`, error) - throw error - } - } - - /** - * 获取作者文章列表 - * @param {string} author - 作者用户名 - * @param {Object} options - 查询选项 - * @returns {Promise} 文章列表和分页信息 - */ - static async getAuthorArticles(author, options = {}) { - try { - const { - page = 1, - limit = 10, - status = null, - search = "", - orderBy = "updated_at", - order = "desc" - } = options - - const where = { author } - if (status) where.status = status - - const result = await ArticleModel.paginate({ - page, - limit, - where, - search, - searchFields: ArticleModel.searchableFields, - orderBy, - order - }) - - return { - articles: result.data.map(article => this.formatArticleResponse(article)), - pagination: result.pagination - } - } catch (error) { - logger.error(`获取作者文章列表失败 (${author}):`, error) - throw error - } - } - - /** - * 根据分类获取文章 - * @param {string} category - 分类 - * @param {Object} options - 查询选项 - * @returns {Promise} 文章列表 - */ - static async getArticlesByCategory(category, options = {}) { - try { - const { limit = 20 } = options - const articles = await ArticleModel.findByCategoryWithAuthor(category, limit) - return articles.map(article => this.formatArticleResponse(article)) - } catch (error) { - logger.error(`根据分类获取文章失败 (${category}):`, error) - throw error - } - } - - /** - * 根据标签获取文章 - * @param {string} tags - 标签(逗号分隔) - * @param {Object} options - 查询选项 - * @returns {Promise} 文章列表 - */ - static async getArticlesByTags(tags, options = {}) { - try { - const articles = await ArticleModel.findByTags(tags) - return articles.map(article => this.formatArticleResponse(article)) - } catch (error) { - logger.error(`根据标签获取文章失败 (${tags}):`, error) - throw error - } - } - - /** - * 搜索文章 - * @param {string} keyword - 搜索关键词 - * @param {Object} options - 搜索选项 - * @returns {Promise} 搜索结果 - */ - static async searchArticles(keyword, options = {}) { - try { - const { limit = 20 } = options - const articles = await ArticleModel.searchWithAuthor(keyword, limit) - return articles.map(article => this.formatArticleResponse(article)) - } catch (error) { - logger.error(`搜索文章失败:`, error) - throw error - } - } - - /** - * 获取最新文章 - * @param {number} limit - 数量限制 - * @returns {Promise} 最新文章列表 - */ - static async getRecentArticles(limit = 10) { - try { - const articles = await ArticleModel.getRecentArticlesWithAuthor(limit) - return articles.map(article => this.formatArticleResponse(article)) - } catch (error) { - logger.error(`获取最新文章失败:`, error) - throw error - } - } - - /** - * 获取热门文章 - * @param {number} limit - 数量限制 - * @returns {Promise} 热门文章列表 - */ - static async getPopularArticles(limit = 10) { - try { - const articles = await ArticleModel.getPopularArticlesWithAuthor(limit) - return articles.map(article => this.formatArticleResponse(article)) - } catch (error) { - logger.error(`获取热门文章失败:`, error) - throw error - } - } - - /** - * 获取精选文章 - * @param {number} limit - 数量限制 - * @returns {Promise} 精选文章列表 - */ - static async getFeaturedArticles(limit = 5) { - try { - const articles = await ArticleModel.getFeaturedArticlesWithAuthor(limit) - return articles.map(article => this.formatArticleResponse(article)) - } catch (error) { - logger.error(`获取精选文章失败:`, error) - throw error - } - } - - /** - * 获取相关文章 - * @param {number} articleId - 文章ID - * @param {number} limit - 数量限制 - * @returns {Promise} 相关文章列表 - */ - static async getRelatedArticles(articleId, limit = 5) { - try { - const articles = await ArticleModel.getRelatedArticles(articleId, limit) - return articles.map(article => this.formatArticleResponse(article)) - } catch (error) { - logger.error(`获取相关文章失败 (ID: ${articleId}):`, error) - throw error - } - } - - /** - * 获取文章统计信息 - * @returns {Promise} 统计信息 - */ - static async getArticleStats() { - try { - const [ - total, - published, - drafts, - byCategory, - byStatus - ] = await Promise.all([ - ArticleModel.getArticleCount(), - ArticleModel.getPublishedArticleCount(), - ArticleModel.count({ status: "draft" }), - ArticleModel.getArticleCountByCategory(), - ArticleModel.getArticleCountByStatus() - ]) - - return { - total, - published, - drafts, - byCategory, - byStatus - } - } catch (error) { - logger.error(`获取文章统计失败:`, error) - throw error - } - } - - /** - * 验证文章数据 - * @param {Object} articleData - 文章数据 - */ - static validateArticleData(articleData) { - if (!articleData.title) { - throw new Error("文章标题不能为空") - } - if (!articleData.content) { - throw new Error("文章内容不能为空") - } - if (!articleData.author) { - throw new Error("文章作者不能为空") - } - - // 标题长度验证 - if (articleData.title.length > 200) { - throw new Error("文章标题不能超过200个字符") - } - - // 内容长度验证 - if (articleData.content.length < 10) { - throw new Error("文章内容不能少于10个字符") - } - } - - /** - * 验证文章更新数据 - * @param {Object} updateData - 更新数据 - */ - static validateArticleUpdateData(updateData) { - if (updateData.title && updateData.title.length > 200) { - throw new Error("文章标题不能超过200个字符") - } - - if (updateData.content && updateData.content.length < 10) { - throw new Error("文章内容不能少于10个字符") - } - } - - /** - * 格式化文章响应数据 - * @param {Object} article - 文章数据 - * @returns {Object} 格式化后的文章数据 - */ - static formatArticleResponse(article) { - return { - ...article, - // 确保数字字段为数字类型 - id: parseInt(article.id), - view_count: parseInt(article.view_count) || 0, - reading_time: parseInt(article.reading_time) || 0, - // 格式化日期字段 - created_at: article.created_at, - updated_at: article.updated_at, - published_at: article.published_at - } - } - - /** - * 批量更新文章状态 - * @param {Array} ids - 文章ID数组 - * @param {string} status - 新状态 - * @returns {Promise} 更新数量 - */ - static async batchUpdateStatus(ids, status) { - try { - if (!Array.isArray(ids) || ids.length === 0) { - throw new Error("文章ID数组不能为空") - } - - if (!["draft", "published", "archived"].includes(status)) { - throw new Error("无效的文章状态") - } - - const result = await ArticleModel.updateMany( - { id: ids }, - { status } - ) - - logger.info(`批量更新文章状态成功: ${ids.length} 篇文章状态更新为 ${status}`) - return result - } catch (error) { - logger.error(`批量更新文章状态失败:`, error) - throw error - } - } - - /** - * 获取文章分类统计 - * @returns {Promise} 分类统计 - */ - static async getCategoryStats() { - try { - return await ArticleModel.getArticleCountByCategory() - } catch (error) { - logger.error(`获取文章分类统计失败:`, error) - throw error - } - } - - /** - * 获取文章标签列表 - * @returns {Promise} 标签列表 - */ - static async getTagList() { - try { - const articles = await ArticleModel.findWhere( - { status: "published" }, - { select: ["tags"] } - ) - - const tagSet = new Set() - articles.forEach(article => { - if (article.tags) { - const tags = article.tags.split(",").map(tag => tag.trim()) - tags.forEach(tag => { - if (tag) tagSet.add(tag) - }) - } - }) - - return Array.from(tagSet).sort() - } catch (error) { - logger.error(`获取文章标签列表失败:`, error) - throw error - } - } -} - -export default ArticleService -export { ArticleService } diff --git a/src/services/AuthService.js b/src/services/AuthService.js deleted file mode 100644 index f528411..0000000 --- a/src/services/AuthService.js +++ /dev/null @@ -1,84 +0,0 @@ -import UserModel from "../db/models/UserModel.js" -import CommonError from "utils/error/CommonError.js" -import { comparePassword } from "utils/bcrypt.js" - -/** - * 认证服务类 - * 提供认证相关的业务逻辑 - */ -class AuthService { - // 注册新用户 - static async register(data) { - try { - if (!data.username || !data.password) { - throw new CommonError("用户名和密码不能为空") - } - - // 检查用户名是否已存在 - const existUser = await UserModel.findByUsername(data.username) - if (existUser) { - throw new CommonError(`用户名${data.username}已存在`) - } - - // 检查邮箱是否已存在 - if (data.email) { - const existEmail = await UserModel.findByEmail(data.email) - if (existEmail) { - throw new CommonError(`邮箱${data.email}已被使用`) - } - } - - // 密码加密 - const hashed = await hashPassword(data.password) - - const user = await UserModel.create({ ...data, password: hashed }) - - // 返回脱敏信息 - const { password, ...userInfo } = Array.isArray(user) ? user[0] : user - return userInfo - } catch (error) { - if (error instanceof CommonError) throw error - throw new CommonError(`注册失败: ${error.message}`) - } - } - - // 登录 - static async login({ username, email, password }) { - try { - if (!password) { - throw new CommonError("密码不能为空") - } - - if (!username && !email) { - throw new CommonError("用户名或邮箱不能为空") - } - - let user - if (username) { - user = await UserModel.findByUsername(username) - } else if (email) { - user = await UserModel.findByEmail(email) - } - - if (!user) { - throw new CommonError("用户不存在") - } - - // 校验密码 - const ok = await comparePassword(password, user.password) - if (!ok) { - throw new CommonError("密码错误") - } - - // 生成token - const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: "2h" }) - - // 返回token和用户信息 - const { password: pwd, ...userInfo } = user - return { token, user: userInfo } - } catch (error) { - if (error instanceof CommonError) throw error - throw new CommonError(`登录失败: ${error.message}`) - } - } -} diff --git a/src/services/BookmarkService.js b/src/services/BookmarkService.js deleted file mode 100644 index 64a5920..0000000 --- a/src/services/BookmarkService.js +++ /dev/null @@ -1,492 +0,0 @@ -import BookmarkModel from "../db/models/BookmarkModel.js" -import { logger } from "../logger.js" - -/** - * 书签服务类 - * 提供书签相关的业务逻辑 - */ -class BookmarkService { - /** - * 创建新书签 - * @param {Object} bookmarkData - 书签数据 - * @returns {Promise} 创建的书签信息 - */ - static async createBookmark(bookmarkData) { - try { - // 数据验证 - this.validateBookmarkData(bookmarkData) - - // 创建书签 - const bookmark = await BookmarkModel.create(bookmarkData) - - logger.info(`书签创建成功: ${bookmark.title} (ID: ${bookmark.id})`) - return this.formatBookmarkResponse(bookmark) - } catch (error) { - logger.error(`创建书签失败:`, error) - throw error - } - } - - /** - * 根据ID获取书签 - * @param {number} id - 书签ID - * @returns {Promise} 书签信息 - */ - static async getBookmarkById(id) { - try { - const bookmark = await BookmarkModel.findById(id) - return bookmark ? this.formatBookmarkResponse(bookmark) : null - } catch (error) { - logger.error(`获取书签失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 更新书签 - * @param {number} id - 书签ID - * @param {Object} updateData - 更新数据 - * @returns {Promise} 更新后的书签信息 - */ - static async updateBookmark(id, updateData) { - try { - // 验证书签是否存在 - const existingBookmark = await BookmarkModel.findById(id) - if (!existingBookmark) { - throw new Error("书签不存在") - } - - // 数据验证 - this.validateBookmarkUpdateData(updateData) - - // 更新书签 - const bookmark = await BookmarkModel.update(id, updateData) - - logger.info(`书签更新成功: ${bookmark.title} (ID: ${id})`) - return this.formatBookmarkResponse(bookmark) - } catch (error) { - logger.error(`更新书签失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 删除书签 - * @param {number} id - 书签ID - * @returns {Promise} 删除结果 - */ - static async deleteBookmark(id) { - try { - const bookmark = await BookmarkModel.findById(id) - if (!bookmark) { - throw new Error("书签不存在") - } - - const result = await BookmarkModel.delete(id) - - logger.info(`书签删除成功: ${bookmark.title} (ID: ${id})`) - return result > 0 - } catch (error) { - logger.error(`删除书签失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 获取用户书签列表 - * @param {number} userId - 用户ID - * @param {Object} options - 查询选项 - * @returns {Promise} 书签列表和分页信息 - */ - static async getUserBookmarks(userId, options = {}) { - try { - const { - page = 1, - limit = 20, - search = "", - orderBy = "created_at", - order = "desc" - } = options - - const result = await BookmarkModel.findByUserWithPagination(userId, { - page, - limit, - search, - searchFields: BookmarkModel.searchableFields, - orderBy, - order - }) - - return { - bookmarks: result.data.map(bookmark => this.formatBookmarkResponse(bookmark)), - pagination: result.pagination - } - } catch (error) { - logger.error(`获取用户书签列表失败 (用户ID: ${userId}):`, error) - throw error - } - } - - /** - * 获取所有书签列表(管理员) - * @param {Object} options - 查询选项 - * @returns {Promise} 书签列表和分页信息 - */ - static async getAllBookmarks(options = {}) { - try { - const { - page = 1, - limit = 20, - search = "", - userId = null, - orderBy = "created_at", - order = "desc" - } = options - - const where = {} - if (userId) where.user_id = userId - - const result = await BookmarkModel.paginate({ - page, - limit, - where, - search, - searchFields: BookmarkModel.searchableFields, - orderBy, - order - }) - - return { - bookmarks: result.data.map(bookmark => this.formatBookmarkResponse(bookmark)), - pagination: result.pagination - } - } catch (error) { - logger.error(`获取所有书签列表失败:`, error) - throw error - } - } - - /** - * 获取书签及其用户信息 - * @param {Object} options - 查询选项 - * @returns {Promise} 书签列表 - */ - static async getBookmarksWithUsers(options = {}) { - try { - const { limit = 50, orderBy = "created_at", order = "desc" } = options - const bookmarks = await BookmarkModel.findAllWithUsers({ limit, orderBy, order }) - return bookmarks.map(bookmark => this.formatBookmarkResponse(bookmark)) - } catch (error) { - logger.error(`获取书签及用户信息失败:`, error) - throw error - } - } - - /** - * 获取热门书签 - * @param {number} limit - 数量限制 - * @returns {Promise} 热门书签列表 - */ - static async getPopularBookmarks(limit = 10) { - try { - const bookmarks = await BookmarkModel.getPopularBookmarks(limit) - return bookmarks.map(bookmark => ({ - url: bookmark.url, - title: bookmark.title, - bookmark_count: parseInt(bookmark.bookmark_count), - latest_bookmark: bookmark.latest_bookmark - })) - } catch (error) { - logger.error(`获取热门书签失败:`, error) - throw error - } - } - - /** - * 搜索书签 - * @param {string} keyword - 搜索关键词 - * @param {Object} options - 搜索选项 - * @returns {Promise} 搜索结果 - */ - static async searchBookmarks(keyword, options = {}) { - try { - const { - userId = null, - limit = 20, - orderBy = "created_at", - order = "desc" - } = options - - const where = {} - if (userId) where.user_id = userId - - const bookmarks = await BookmarkModel.findWhere(where, { - search: keyword, - searchFields: BookmarkModel.searchableFields, - limit, - orderBy, - order - }) - - return bookmarks.map(bookmark => this.formatBookmarkResponse(bookmark)) - } catch (error) { - logger.error(`搜索书签失败:`, error) - throw error - } - } - - /** - * 检查书签是否存在 - * @param {number} userId - 用户ID - * @param {string} url - URL - * @returns {Promise} 是否存在 - */ - static async checkBookmarkExists(userId, url) { - try { - const bookmark = await BookmarkModel.findByUserAndUrl(userId, url) - return !!bookmark - } catch (error) { - logger.error(`检查书签是否存在失败:`, error) - throw error - } - } - - /** - * 获取用户书签统计 - * @param {number} userId - 用户ID - * @returns {Promise} 统计信息 - */ - static async getUserBookmarkStats(userId) { - try { - return await BookmarkModel.getUserBookmarkStats(userId) - } catch (error) { - logger.error(`获取用户书签统计失败 (用户ID: ${userId}):`, error) - throw error - } - } - - /** - * 批量删除书签 - * @param {Array} ids - 书签ID数组 - * @param {number} userId - 用户ID(可选,用于权限验证) - * @returns {Promise} 删除数量 - */ - static async batchDeleteBookmarks(ids, userId = null) { - try { - if (!Array.isArray(ids) || ids.length === 0) { - throw new Error("书签ID数组不能为空") - } - - // 如果提供了用户ID,验证书签是否属于该用户 - if (userId) { - const bookmarks = await BookmarkModel.findWhere({ id: ids, user_id: userId }) - if (bookmarks.length !== ids.length) { - throw new Error("部分书签不存在或无权限删除") - } - } - - const result = await BookmarkModel.deleteWhere({ id: ids }) - - logger.info(`批量删除书签成功: ${result} 个书签`) - return result - } catch (error) { - logger.error(`批量删除书签失败:`, error) - throw error - } - } - - /** - * 批量创建书签 - * @param {Array} bookmarksData - 书签数据数组 - * @returns {Promise} 创建结果 - */ - static async batchCreateBookmarks(bookmarksData) { - try { - const results = [] - const errors = [] - - for (let i = 0; i < bookmarksData.length; i++) { - try { - const bookmarkData = bookmarksData[i] - this.validateBookmarkData(bookmarkData) - - const bookmark = await BookmarkModel.create(bookmarkData) - results.push(this.formatBookmarkResponse(bookmark)) - } catch (error) { - errors.push({ - index: i, - data: bookmarksData[i], - error: error.message - }) - } - } - - return { - success: results, - errors, - summary: { - total: bookmarksData.length, - success: results.length, - failed: errors.length - } - } - } catch (error) { - logger.error(`批量创建书签失败:`, error) - throw error - } - } - - /** - * 获取书签分类统计 - * @param {number} userId - 用户ID(可选) - * @returns {Promise} 分类统计 - */ - static async getCategoryStats(userId = null) { - try { - const where = userId ? { user_id: userId } : {} - const bookmarks = await BookmarkModel.findWhere(where, { select: ["category"] }) - - const categoryStats = {} - bookmarks.forEach(bookmark => { - const category = bookmark.category || "未分类" - categoryStats[category] = (categoryStats[category] || 0) + 1 - }) - - return Object.entries(categoryStats) - .map(([category, count]) => ({ category, count })) - .sort((a, b) => b.count - a.count) - } catch (error) { - logger.error(`获取书签分类统计失败:`, error) - throw error - } - } - - /** - * 验证书签数据 - * @param {Object} bookmarkData - 书签数据 - */ - static validateBookmarkData(bookmarkData) { - if (!bookmarkData.user_id) { - throw new Error("用户ID不能为空") - } - if (!bookmarkData.url) { - throw new Error("URL不能为空") - } - if (!bookmarkData.title) { - throw new Error("书签标题不能为空") - } - - // URL格式验证 - const urlRegex = /^https?:\/\/.+\..+/ - if (!urlRegex.test(bookmarkData.url)) { - throw new Error("URL格式不正确") - } - - // 标题长度验证 - if (bookmarkData.title.length > 200) { - throw new Error("书签标题不能超过200个字符") - } - - // 描述长度验证 - if (bookmarkData.description && bookmarkData.description.length > 500) { - throw new Error("书签描述不能超过500个字符") - } - } - - /** - * 验证书签更新数据 - * @param {Object} updateData - 更新数据 - */ - static validateBookmarkUpdateData(updateData) { - if (updateData.url) { - const urlRegex = /^https?:\/\/.+\..+/ - if (!urlRegex.test(updateData.url)) { - throw new Error("URL格式不正确") - } - } - - if (updateData.title && updateData.title.length > 200) { - throw new Error("书签标题不能超过200个字符") - } - - if (updateData.description && updateData.description.length > 500) { - throw new Error("书签描述不能超过500个字符") - } - } - - /** - * 格式化书签响应数据 - * @param {Object} bookmark - 书签数据 - * @returns {Object} 格式化后的书签数据 - */ - static formatBookmarkResponse(bookmark) { - return { - ...bookmark, - // 确保数字字段为数字类型 - id: parseInt(bookmark.id), - user_id: parseInt(bookmark.user_id), - // 格式化日期字段 - created_at: bookmark.created_at, - updated_at: bookmark.updated_at - } - } - - /** - * 导入书签 - * @param {number} userId - 用户ID - * @param {Array} bookmarksData - 书签数据数组 - * @returns {Promise} 导入结果 - */ - static async importBookmarks(userId, bookmarksData) { - try { - const results = [] - const errors = [] - const skipped = [] - - for (let i = 0; i < bookmarksData.length; i++) { - try { - const bookmarkData = { ...bookmarksData[i], user_id: userId } - this.validateBookmarkData(bookmarkData) - - // 检查是否已存在 - const exists = await this.checkBookmarkExists(userId, bookmarkData.url) - if (exists) { - skipped.push({ - index: i, - data: bookmarkData, - reason: "书签已存在" - }) - continue - } - - const bookmark = await BookmarkModel.create(bookmarkData) - results.push(this.formatBookmarkResponse(bookmark)) - } catch (error) { - errors.push({ - index: i, - data: bookmarksData[i], - error: error.message - }) - } - } - - return { - success: results, - errors, - skipped, - summary: { - total: bookmarksData.length, - success: results.length, - failed: errors.length, - skipped: skipped.length - } - } - } catch (error) { - logger.error(`导入书签失败:`, error) - throw error - } - } -} - -export default BookmarkService -export { BookmarkService } diff --git a/src/services/ContactService.js b/src/services/ContactService.js deleted file mode 100644 index 293903d..0000000 --- a/src/services/ContactService.js +++ /dev/null @@ -1,511 +0,0 @@ -import ContactModel from "../db/models/ContactModel.js" -import { logger } from "../logger.js" - -/** - * 联系信息服务类 - * 提供联系信息相关的业务逻辑 - */ -class ContactService { - /** - * 创建新联系信息 - * @param {Object} contactData - 联系信息数据 - * @returns {Promise} 创建的联系信息 - */ - static async createContact(contactData) { - try { - // 数据验证 - this.validateContactData(contactData) - - // 创建联系信息 - const contact = await ContactModel.create(contactData) - - logger.info(`联系信息创建成功: ${contact.name} (ID: ${contact.id})`) - return this.formatContactResponse(contact) - } catch (error) { - logger.error(`创建联系信息失败:`, error) - throw error - } - } - - /** - * 根据ID获取联系信息 - * @param {number} id - 联系信息ID - * @returns {Promise} 联系信息 - */ - static async getContactById(id) { - try { - const contact = await ContactModel.findById(id) - return contact ? this.formatContactResponse(contact) : null - } catch (error) { - logger.error(`获取联系信息失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 更新联系信息 - * @param {number} id - 联系信息ID - * @param {Object} updateData - 更新数据 - * @returns {Promise} 更新后的联系信息 - */ - static async updateContact(id, updateData) { - try { - // 验证联系信息是否存在 - const existingContact = await ContactModel.findById(id) - if (!existingContact) { - throw new Error("联系信息不存在") - } - - // 数据验证 - this.validateContactUpdateData(updateData) - - // 更新联系信息 - const contact = await ContactModel.update(id, updateData) - - logger.info(`联系信息更新成功: ${contact.name} (ID: ${id})`) - return this.formatContactResponse(contact) - } catch (error) { - logger.error(`更新联系信息失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 删除联系信息 - * @param {number} id - 联系信息ID - * @returns {Promise} 删除结果 - */ - static async deleteContact(id) { - try { - const contact = await ContactModel.findById(id) - if (!contact) { - throw new Error("联系信息不存在") - } - - const result = await ContactModel.delete(id) - - logger.info(`联系信息删除成功: ${contact.name} (ID: ${id})`) - return result > 0 - } catch (error) { - logger.error(`删除联系信息失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 获取联系信息列表 - * @param {Object} options - 查询选项 - * @returns {Promise} 联系信息列表和分页信息 - */ - static async getContactList(options = {}) { - try { - const { - page = 1, - limit = 20, - search = "", - status = null, - orderBy = "created_at", - order = "desc" - } = options - - const where = {} - if (status) where.status = status - - const result = await ContactModel.paginate({ - page, - limit, - where, - search, - searchFields: ContactModel.searchableFields, - orderBy, - order - }) - - return { - contacts: result.data.map(contact => this.formatContactResponse(contact)), - pagination: result.pagination - } - } catch (error) { - logger.error(`获取联系信息列表失败:`, error) - throw error - } - } - - /** - * 根据邮箱获取联系信息 - * @param {string} email - 邮箱 - * @returns {Promise} 联系信息列表 - */ - static async getContactsByEmail(email) { - try { - const contacts = await ContactModel.findByEmail(email) - return contacts.map(contact => this.formatContactResponse(contact)) - } catch (error) { - logger.error(`根据邮箱获取联系信息失败 (${email}):`, error) - throw error - } - } - - /** - * 根据状态获取联系信息 - * @param {string} status - 状态 - * @returns {Promise} 联系信息列表 - */ - static async getContactsByStatus(status) { - try { - const contacts = await ContactModel.findByStatus(status) - return contacts.map(contact => this.formatContactResponse(contact)) - } catch (error) { - logger.error(`根据状态获取联系信息失败 (${status}):`, error) - throw error - } - } - - /** - * 根据日期范围获取联系信息 - * @param {string} startDate - 开始日期 - * @param {string} endDate - 结束日期 - * @returns {Promise} 联系信息列表 - */ - static async getContactsByDateRange(startDate, endDate) { - try { - const contacts = await ContactModel.findByDateRange(startDate, endDate) - return contacts.map(contact => this.formatContactResponse(contact)) - } catch (error) { - logger.error(`根据日期范围获取联系信息失败:`, error) - throw error - } - } - - /** - * 标记为已读 - * @param {number} id - 联系信息ID - * @returns {Promise} 更新后的联系信息 - */ - static async markAsRead(id) { - try { - const contact = await ContactModel.markAsRead(id) - logger.info(`联系信息标记为已读成功 (ID: ${id})`) - return this.formatContactResponse(contact) - } catch (error) { - logger.error(`标记联系信息为已读失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 标记为已回复 - * @param {number} id - 联系信息ID - * @returns {Promise} 更新后的联系信息 - */ - static async markAsReplied(id) { - try { - const contact = await ContactModel.markAsReplied(id) - logger.info(`联系信息标记为已回复成功 (ID: ${id})`) - return this.formatContactResponse(contact) - } catch (error) { - logger.error(`标记联系信息为已回复失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 批量更新状态 - * @param {Array} ids - 联系信息ID数组 - * @param {string} status - 新状态 - * @returns {Promise} 更新数量 - */ - static async batchUpdateStatus(ids, status) { - try { - if (!Array.isArray(ids) || ids.length === 0) { - throw new Error("联系信息ID数组不能为空") - } - - if (!["unread", "read", "replied"].includes(status)) { - throw new Error("无效的联系信息状态") - } - - const result = await ContactModel.updateStatusBatchByIds(ids, status) - - logger.info(`批量更新联系信息状态成功: ${ids.length} 条记录状态更新为 ${status}`) - return result - } catch (error) { - logger.error(`批量更新联系信息状态失败:`, error) - throw error - } - } - - /** - * 批量删除联系信息 - * @param {Array} ids - 联系信息ID数组 - * @returns {Promise} 删除数量 - */ - static async batchDeleteContacts(ids) { - try { - if (!Array.isArray(ids) || ids.length === 0) { - throw new Error("联系信息ID数组不能为空") - } - - const result = await ContactModel.deleteWhere({ id: ids }) - - logger.info(`批量删除联系信息成功: ${result} 条记录`) - return result - } catch (error) { - logger.error(`批量删除联系信息失败:`, error) - throw error - } - } - - /** - * 获取联系信息统计 - * @returns {Promise} 统计信息 - */ - static async getContactStats() { - try { - const stats = await ContactModel.getStats() - const todayCount = await ContactModel.getTodayCount() - - return { - ...stats, - today: todayCount - } - } catch (error) { - logger.error(`获取联系信息统计失败:`, error) - throw error - } - } - - /** - * 获取今日新联系数量 - * @returns {Promise} 今日新联系数量 - */ - static async getTodayContactCount() { - try { - return await ContactModel.getTodayCount() - } catch (error) { - logger.error(`获取今日新联系数量失败:`, error) - throw error - } - } - - /** - * 搜索联系信息 - * @param {string} keyword - 搜索关键词 - * @param {Object} options - 搜索选项 - * @returns {Promise} 搜索结果 - */ - static async searchContacts(keyword, options = {}) { - try { - const { - status = null, - limit = 20, - orderBy = "created_at", - order = "desc" - } = options - - const where = {} - if (status) where.status = status - - const contacts = await ContactModel.findWhere(where, { - search: keyword, - searchFields: ContactModel.searchableFields, - limit, - orderBy, - order - }) - - return contacts.map(contact => this.formatContactResponse(contact)) - } catch (error) { - logger.error(`搜索联系信息失败:`, error) - throw error - } - } - - /** - * 获取未读联系信息数量 - * @returns {Promise} 未读数量 - */ - static async getUnreadCount() { - try { - return await ContactModel.count({ status: "unread" }) - } catch (error) { - logger.error(`获取未读联系信息数量失败:`, error) - throw error - } - } - - /** - * 获取最近联系信息 - * @param {number} limit - 数量限制 - * @returns {Promise} 最近联系信息列表 - */ - static async getRecentContacts(limit = 10) { - try { - const contacts = await ContactModel.findWhere( - {}, - { orderBy: "created_at", order: "desc", limit } - ) - return contacts.map(contact => this.formatContactResponse(contact)) - } catch (error) { - logger.error(`获取最近联系信息失败:`, error) - throw error - } - } - - /** - * 验证联系信息数据 - * @param {Object} contactData - 联系信息数据 - */ - static validateContactData(contactData) { - if (!contactData.name) { - throw new Error("姓名不能为空") - } - if (!contactData.email) { - throw new Error("邮箱不能为空") - } - if (!contactData.subject) { - throw new Error("主题不能为空") - } - if (!contactData.message) { - throw new Error("消息内容不能为空") - } - - // 邮箱格式验证 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(contactData.email)) { - throw new Error("邮箱格式不正确") - } - - // 姓名长度验证 - if (contactData.name.length > 100) { - throw new Error("姓名不能超过100个字符") - } - - // 主题长度验证 - if (contactData.subject.length > 200) { - throw new Error("主题不能超过200个字符") - } - - // 消息长度验证 - if (contactData.message.length > 2000) { - throw new Error("消息内容不能超过2000个字符") - } - } - - /** - * 验证联系信息更新数据 - * @param {Object} updateData - 更新数据 - */ - static validateContactUpdateData(updateData) { - if (updateData.email) { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(updateData.email)) { - throw new Error("邮箱格式不正确") - } - } - - if (updateData.name && updateData.name.length > 100) { - throw new Error("姓名不能超过100个字符") - } - - if (updateData.subject && updateData.subject.length > 200) { - throw new Error("主题不能超过200个字符") - } - - if (updateData.message && updateData.message.length > 2000) { - throw new Error("消息内容不能超过2000个字符") - } - - if (updateData.status && !["unread", "read", "replied"].includes(updateData.status)) { - throw new Error("无效的状态值") - } - } - - /** - * 格式化联系信息响应数据 - * @param {Object} contact - 联系信息数据 - * @returns {Object} 格式化后的联系信息数据 - */ - static formatContactResponse(contact) { - return { - ...contact, - // 确保数字字段为数字类型 - id: parseInt(contact.id), - // 格式化日期字段 - created_at: contact.created_at, - updated_at: contact.updated_at - } - } - - /** - * 导出联系信息 - * @param {Object} options - 导出选项 - * @returns {Promise} 导出的联系信息 - */ - static async exportContacts(options = {}) { - try { - const { - status = null, - startDate = null, - endDate = null, - limit = 1000 - } = options - - let where = {} - if (status) where.status = status - - let contacts - if (startDate && endDate) { - contacts = await ContactModel.findByDateRange(startDate, endDate) - } else { - contacts = await ContactModel.findWhere(where, { - orderBy: "created_at", - order: "desc", - limit - }) - } - - return contacts.map(contact => this.formatContactResponse(contact)) - } catch (error) { - logger.error(`导出联系信息失败:`, error) - throw error - } - } - - /** - * 获取联系信息趋势数据 - * @param {number} days - 天数 - * @returns {Promise} 趋势数据 - */ - static async getContactTrends(days = 30) { - try { - const endDate = new Date() - const startDate = new Date() - startDate.setDate(startDate.getDate() - days) - - const contacts = await ContactModel.findByDateRange( - startDate.toISOString().split('T')[0], - endDate.toISOString().split('T')[0] - ) - - // 按日期分组统计 - const trends = {} - contacts.forEach(contact => { - const date = contact.created_at.split('T')[0] - if (!trends[date]) { - trends[date] = { date, count: 0 } - } - trends[date].count++ - }) - - return Object.values(trends).sort((a, b) => a.date.localeCompare(b.date)) - } catch (error) { - logger.error(`获取联系信息趋势数据失败:`, error) - throw error - } - } -} - -export default ContactService -export { ContactService } - diff --git a/src/services/JobService.js b/src/services/JobService.js deleted file mode 100644 index 35a04a3..0000000 --- a/src/services/JobService.js +++ /dev/null @@ -1,18 +0,0 @@ -import jobs from "../jobs" - -class JobService { - startJob(id) { - return jobs.start(id) - } - stopJob(id) { - return jobs.stop(id) - } - updateJobCron(id, cronTime) { - return jobs.updateCronTime(id, cronTime) - } - listJobs() { - return jobs.list() - } -} - -export default JobService diff --git a/src/services/SiteConfigService.js b/src/services/SiteConfigService.js deleted file mode 100644 index cbc7148..0000000 --- a/src/services/SiteConfigService.js +++ /dev/null @@ -1,513 +0,0 @@ -import SiteConfigModel from "../db/models/SiteConfigModel.js" -import { logger } from "../logger.js" - -/** - * 站点配置服务类 - * 提供站点配置相关的业务逻辑 - */ -class SiteConfigService { - /** - * 获取配置值 - * @param {string} key - 配置键 - * @param {*} defaultValue - 默认值 - * @returns {Promise<*>} 配置值 - */ - static async get(key, defaultValue = null) { - try { - const value = await SiteConfigModel.get(key) - return value !== null ? value : defaultValue - } catch (error) { - logger.error(`获取配置失败 (${key}):`, error) - throw error - } - } - - /** - * 设置配置值 - * @param {string} key - 配置键 - * @param {*} value - 配置值 - * @returns {Promise} 配置对象 - */ - static async set(key, value) { - try { - // 验证配置键 - this.validateConfigKey(key) - - // 序列化值 - const serializedValue = this.serializeValue(value) - - const config = await SiteConfigModel.set(key, serializedValue) - - logger.info(`配置设置成功: ${key}`) - return this.formatConfigResponse(config) - } catch (error) { - logger.error(`设置配置失败 (${key}):`, error) - throw error - } - } - - /** - * 批量获取配置 - * @param {Array} keys - 配置键数组 - * @returns {Promise} 配置对象 - */ - static async getMany(keys) { - try { - if (!Array.isArray(keys) || keys.length === 0) { - return {} - } - - const configs = await SiteConfigModel.getMany(keys) - - // 反序列化值 - const result = {} - for (const [key, value] of Object.entries(configs)) { - result[key] = this.deserializeValue(value) - } - - return result - } catch (error) { - logger.error(`批量获取配置失败:`, error) - throw error - } - } - - /** - * 获取所有配置 - * @returns {Promise} 所有配置 - */ - static async getAll() { - try { - const configs = await SiteConfigModel.getAll() - - // 反序列化值 - const result = {} - for (const [key, value] of Object.entries(configs)) { - result[key] = this.deserializeValue(value) - } - - return result - } catch (error) { - logger.error(`获取所有配置失败:`, error) - throw error - } - } - - /** - * 批量设置配置 - * @param {Object} configs - 配置对象 - * @returns {Promise} 设置结果 - */ - static async setMany(configs) { - try { - if (!configs || typeof configs !== 'object') { - throw new Error("配置对象不能为空") - } - - const results = [] - for (const [key, value] of Object.entries(configs)) { - try { - this.validateConfigKey(key) - const serializedValue = this.serializeValue(value) - const config = await SiteConfigModel.set(key, serializedValue) - results.push(this.formatConfigResponse(config)) - } catch (error) { - logger.error(`设置配置失败 (${key}):`, error) - results.push({ key, error: error.message }) - } - } - - logger.info(`批量设置配置完成: ${Object.keys(configs).length} 个配置`) - return results - } catch (error) { - logger.error(`批量设置配置失败:`, error) - throw error - } - } - - /** - * 删除配置 - * @param {string} key - 配置键 - * @returns {Promise} 删除结果 - */ - static async delete(key) { - try { - const result = await SiteConfigModel.deleteByKey(key) - - logger.info(`配置删除成功: ${key}`) - return result > 0 - } catch (error) { - logger.error(`删除配置失败 (${key}):`, error) - throw error - } - } - - /** - * 检查配置是否存在 - * @param {string} key - 配置键 - * @returns {Promise} 是否存在 - */ - static async has(key) { - try { - return await SiteConfigModel.hasKey(key) - } catch (error) { - logger.error(`检查配置是否存在失败 (${key}):`, error) - throw error - } - } - - /** - * 获取配置统计 - * @returns {Promise} 统计信息 - */ - static async getStats() { - try { - return await SiteConfigModel.getConfigStats() - } catch (error) { - logger.error(`获取配置统计失败:`, error) - throw error - } - } - - /** - * 获取站点基本信息配置 - * @returns {Promise} 站点基本信息 - */ - static async getSiteInfo() { - try { - const keys = [ - 'site_name', - 'site_description', - 'site_keywords', - 'site_author', - 'site_url', - 'site_logo', - 'site_favicon', - 'site_theme', - 'site_language', - 'site_timezone' - ] - - const configs = await this.getMany(keys) - - return { - name: configs.site_name || '我的网站', - description: configs.site_description || '', - keywords: configs.site_keywords || '', - author: configs.site_author || '', - url: configs.site_url || '', - logo: configs.site_logo || '', - favicon: configs.site_favicon || '', - theme: configs.site_theme || 'default', - language: configs.site_language || 'zh-CN', - timezone: configs.site_timezone || 'Asia/Shanghai' - } - } catch (error) { - logger.error(`获取站点基本信息失败:`, error) - throw error - } - } - - /** - * 设置站点基本信息配置 - * @param {Object} siteInfo - 站点信息 - * @returns {Promise} 设置结果 - */ - static async setSiteInfo(siteInfo) { - try { - const configs = {} - - if (siteInfo.name) configs.site_name = siteInfo.name - if (siteInfo.description) configs.site_description = siteInfo.description - if (siteInfo.keywords) configs.site_keywords = siteInfo.keywords - if (siteInfo.author) configs.site_author = siteInfo.author - if (siteInfo.url) configs.site_url = siteInfo.url - if (siteInfo.logo) configs.site_logo = siteInfo.logo - if (siteInfo.favicon) configs.site_favicon = siteInfo.favicon - if (siteInfo.theme) configs.site_theme = siteInfo.theme - if (siteInfo.language) configs.site_language = siteInfo.language - if (siteInfo.timezone) configs.site_timezone = siteInfo.timezone - - return await this.setMany(configs) - } catch (error) { - logger.error(`设置站点基本信息失败:`, error) - throw error - } - } - - /** - * 获取邮件配置 - * @returns {Promise} 邮件配置 - */ - static async getEmailConfig() { - try { - const keys = [ - 'email_host', - 'email_port', - 'email_secure', - 'email_user', - 'email_password', - 'email_from', - 'email_name' - ] - - const configs = await this.getMany(keys) - - return { - host: configs.email_host || '', - port: parseInt(configs.email_port) || 587, - secure: configs.email_secure === 'true', - user: configs.email_user || '', - password: configs.email_password || '', - from: configs.email_from || '', - name: configs.email_name || '' - } - } catch (error) { - logger.error(`获取邮件配置失败:`, error) - throw error - } - } - - /** - * 设置邮件配置 - * @param {Object} emailConfig - 邮件配置 - * @returns {Promise} 设置结果 - */ - static async setEmailConfig(emailConfig) { - try { - const configs = {} - - if (emailConfig.host) configs.email_host = emailConfig.host - if (emailConfig.port) configs.email_port = emailConfig.port.toString() - if (emailConfig.secure !== undefined) configs.email_secure = emailConfig.secure.toString() - if (emailConfig.user) configs.email_user = emailConfig.user - if (emailConfig.password) configs.email_password = emailConfig.password - if (emailConfig.from) configs.email_from = emailConfig.from - if (emailConfig.name) configs.email_name = emailConfig.name - - return await this.setMany(configs) - } catch (error) { - logger.error(`设置邮件配置失败:`, error) - throw error - } - } - - /** - * 获取系统配置 - * @returns {Promise} 系统配置 - */ - static async getSystemConfig() { - try { - const keys = [ - 'maintenance_mode', - 'registration_enabled', - 'email_verification_required', - 'max_upload_size', - 'allowed_file_types', - 'session_timeout', - 'password_min_length', - 'login_attempts_limit' - ] - - const configs = await this.getMany(keys) - - return { - maintenanceMode: configs.maintenance_mode === 'true', - registrationEnabled: configs.registration_enabled !== 'false', - emailVerificationRequired: configs.email_verification_required === 'true', - maxUploadSize: parseInt(configs.max_upload_size) || 10485760, // 10MB - allowedFileTypes: configs.allowed_file_types ? configs.allowed_file_types.split(',') : ['jpg', 'jpeg', 'png', 'gif', 'pdf'], - sessionTimeout: parseInt(configs.session_timeout) || 3600, // 1小时 - passwordMinLength: parseInt(configs.password_min_length) || 6, - loginAttemptsLimit: parseInt(configs.login_attempts_limit) || 5 - } - } catch (error) { - logger.error(`获取系统配置失败:`, error) - throw error - } - } - - /** - * 设置系统配置 - * @param {Object} systemConfig - 系统配置 - * @returns {Promise} 设置结果 - */ - static async setSystemConfig(systemConfig) { - try { - const configs = {} - - if (systemConfig.maintenanceMode !== undefined) configs.maintenance_mode = systemConfig.maintenanceMode.toString() - if (systemConfig.registrationEnabled !== undefined) configs.registration_enabled = systemConfig.registrationEnabled.toString() - if (systemConfig.emailVerificationRequired !== undefined) configs.email_verification_required = systemConfig.emailVerificationRequired.toString() - if (systemConfig.maxUploadSize) configs.max_upload_size = systemConfig.maxUploadSize.toString() - if (systemConfig.allowedFileTypes) configs.allowed_file_types = Array.isArray(systemConfig.allowedFileTypes) ? systemConfig.allowedFileTypes.join(',') : systemConfig.allowedFileTypes - if (systemConfig.sessionTimeout) configs.session_timeout = systemConfig.sessionTimeout.toString() - if (systemConfig.passwordMinLength) configs.password_min_length = systemConfig.passwordMinLength.toString() - if (systemConfig.loginAttemptsLimit) configs.login_attempts_limit = systemConfig.loginAttemptsLimit.toString() - - return await this.setMany(configs) - } catch (error) { - logger.error(`设置系统配置失败:`, error) - throw error - } - } - - /** - * 重置配置为默认值 - * @param {Array} keys - 要重置的配置键数组(可选,默认重置所有) - * @returns {Promise} 重置结果 - */ - static async resetToDefaults(keys = null) { - try { - const defaultConfigs = { - site_name: '我的网站', - site_description: '欢迎来到我的网站', - site_keywords: '网站,博客,个人网站', - site_author: '网站管理员', - site_url: 'http://localhost:3000', - site_theme: 'default', - site_language: 'zh-CN', - site_timezone: 'Asia/Shanghai', - maintenance_mode: 'false', - registration_enabled: 'true', - email_verification_required: 'false', - max_upload_size: '10485760', - allowed_file_types: 'jpg,jpeg,png,gif,pdf', - session_timeout: '3600', - password_min_length: '6', - login_attempts_limit: '5' - } - - const configsToReset = keys ? - Object.fromEntries(keys.filter(key => defaultConfigs[key]).map(key => [key, defaultConfigs[key]])) : - defaultConfigs - - return await this.setMany(configsToReset) - } catch (error) { - logger.error(`重置配置为默认值失败:`, error) - throw error - } - } - - /** - * 验证配置键 - * @param {string} key - 配置键 - */ - static validateConfigKey(key) { - if (!key || typeof key !== 'string') { - throw new Error("配置键不能为空") - } - - if (key.length > 100) { - throw new Error("配置键长度不能超过100个字符") - } - - if (!/^[a-zA-Z0-9_]+$/.test(key)) { - throw new Error("配置键只能包含字母、数字和下划线") - } - } - - /** - * 序列化值 - * @param {*} value - 要序列化的值 - * @returns {string} 序列化后的字符串 - */ - static serializeValue(value) { - if (value === null || value === undefined) { - return '' - } - - if (typeof value === 'string') { - return value - } - - if (typeof value === 'number' || typeof value === 'boolean') { - return value.toString() - } - - return JSON.stringify(value) - } - - /** - * 反序列化值 - * @param {string} value - 要反序列化的字符串 - * @returns {*} 反序列化后的值 - */ - static deserializeValue(value) { - if (value === null || value === undefined || value === '') { - return null - } - - // 尝试解析为JSON - try { - return JSON.parse(value) - } catch (e) { - // 如果不是有效的JSON,返回原字符串 - return value - } - } - - /** - * 格式化配置响应数据 - * @param {Object} config - 配置数据 - * @returns {Object} 格式化后的配置数据 - */ - static formatConfigResponse(config) { - return { - ...config, - // 确保数字字段为数字类型 - id: parseInt(config.id), - // 反序列化值 - value: this.deserializeValue(config.value), - // 格式化日期字段 - created_at: config.created_at, - updated_at: config.updated_at - } - } - - /** - * 导出配置 - * @returns {Promise} 导出的配置 - */ - static async exportConfig() { - try { - const configs = await this.getAll() - return { - exported_at: new Date().toISOString(), - configs - } - } catch (error) { - logger.error(`导出配置失败:`, error) - throw error - } - } - - /** - * 导入配置 - * @param {Object} configData - 配置数据 - * @returns {Promise} 导入结果 - */ - static async importConfig(configData) { - try { - if (!configData || !configData.configs) { - throw new Error("无效的配置数据") - } - - const results = await this.setMany(configData.configs) - - logger.info(`配置导入完成: ${Object.keys(configData.configs).length} 个配置`) - return { - success: results.filter(r => !r.error).length, - failed: results.filter(r => r.error).length, - results - } - } catch (error) { - logger.error(`导入配置失败:`, error) - throw error - } - } -} - -export default SiteConfigService -export { SiteConfigService } - diff --git a/src/services/UserService.js b/src/services/UserService.js deleted file mode 100644 index 2a0144e..0000000 --- a/src/services/UserService.js +++ /dev/null @@ -1,415 +0,0 @@ -import UserModel from "../db/models/UserModel.js" -import { logger } from "../logger.js" - -/** - * 用户服务类 - * 提供用户相关的业务逻辑 - */ -class UserService { - /** - * 创建新用户 - * @param {Object} userData - 用户数据 - * @returns {Promise} 创建的用户信息 - */ - static async createUser(userData) { - try { - // 数据验证 - this.validateUserData(userData) - - // 检查用户名和邮箱唯一性 - await this.checkUniqueConstraints(userData) - - // 创建用户 - const user = await UserModel.create(userData) - - logger.info(`用户创建成功: ${user.username} (ID: ${user.id})`) - return this.formatUserResponse(user) - } catch (error) { - logger.error(`创建用户失败:`, error) - throw error - } - } - - /** - * 根据ID获取用户 - * @param {number} id - 用户ID - * @returns {Promise} 用户信息 - */ - static async getUserById(id) { - try { - const user = await UserModel.findById(id) - return user ? this.formatUserResponse(user) : null - } catch (error) { - logger.error(`获取用户失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 根据用户名获取用户 - * @param {string} username - 用户名 - * @returns {Promise} 用户信息 - */ - static async getUserByUsername(username) { - try { - const user = await UserModel.findByUsername(username) - return user ? this.formatUserResponse(user) : null - } catch (error) { - logger.error(`根据用户名获取用户失败 (${username}):`, error) - throw error - } - } - - /** - * 根据邮箱获取用户 - * @param {string} email - 邮箱 - * @returns {Promise} 用户信息 - */ - static async getUserByEmail(email) { - try { - const user = await UserModel.findByEmail(email) - return user ? this.formatUserResponse(user) : null - } catch (error) { - logger.error(`根据邮箱获取用户失败 (${email}):`, error) - throw error - } - } - - /** - * 更新用户信息 - * @param {number} id - 用户ID - * @param {Object} updateData - 更新数据 - * @returns {Promise} 更新后的用户信息 - */ - static async updateUser(id, updateData) { - try { - // 验证用户是否存在 - const existingUser = await UserModel.findById(id) - if (!existingUser) { - throw new Error("用户不存在") - } - - // 数据验证 - this.validateUserUpdateData(updateData) - - // 检查唯一性约束 - await this.checkUniqueConstraintsForUpdate(id, updateData) - - // 更新用户 - const user = await UserModel.update(id, updateData) - - logger.info(`用户更新成功: ${user.username} (ID: ${id})`) - return this.formatUserResponse(user) - } catch (error) { - logger.error(`更新用户失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 删除用户 - * @param {number} id - 用户ID - * @returns {Promise} 删除结果 - */ - static async deleteUser(id) { - try { - const user = await UserModel.findById(id) - if (!user) { - throw new Error("用户不存在") - } - - const result = await UserModel.delete(id) - - logger.info(`用户删除成功: ${user.username} (ID: ${id})`) - return result > 0 - } catch (error) { - logger.error(`删除用户失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 获取用户列表 - * @param {Object} options - 查询选项 - * @returns {Promise} 用户列表和分页信息 - */ - static async getUserList(options = {}) { - try { - const { - page = 1, - limit = 10, - search = "", - role = null, - status = null, - orderBy = "created_at", - order = "desc" - } = options - - const where = {} - if (role) where.role = role - if (status) where.status = status - - const result = await UserModel.paginate({ - page, - limit, - where, - search, - searchFields: UserModel.searchableFields, - orderBy, - order - }) - - return { - users: result.data.map(user => this.formatUserResponse(user)), - pagination: result.pagination - } - } catch (error) { - logger.error(`获取用户列表失败:`, error) - throw error - } - } - - /** - * 激活用户 - * @param {number} id - 用户ID - * @returns {Promise} 更新后的用户信息 - */ - static async activateUser(id) { - try { - const user = await UserModel.activate(id) - logger.info(`用户激活成功: ${user.username} (ID: ${id})`) - return this.formatUserResponse(user) - } catch (error) { - logger.error(`激活用户失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 停用用户 - * @param {number} id - 用户ID - * @returns {Promise} 更新后的用户信息 - */ - static async deactivateUser(id) { - try { - const user = await UserModel.deactivate(id) - logger.info(`用户停用成功: ${user.username} (ID: ${id})`) - return this.formatUserResponse(user) - } catch (error) { - logger.error(`停用用户失败 (ID: ${id}):`, error) - throw error - } - } - - /** - * 根据角色获取用户 - * @param {string} role - 角色 - * @returns {Promise} 用户列表 - */ - static async getUsersByRole(role) { - try { - const users = await UserModel.findByRole(role) - return users.map(user => this.formatUserResponse(user)) - } catch (error) { - logger.error(`根据角色获取用户失败 (${role}):`, error) - throw error - } - } - - /** - * 获取用户统计信息 - * @returns {Promise} 统计信息 - */ - static async getUserStats() { - try { - return await UserModel.getUserStats() - } catch (error) { - logger.error(`获取用户统计失败:`, error) - throw error - } - } - - /** - * 验证用户数据 - * @param {Object} userData - 用户数据 - */ - static validateUserData(userData) { - if (!userData.username) { - throw new Error("用户名不能为空") - } - if (!userData.email) { - throw new Error("邮箱不能为空") - } - if (!userData.password) { - throw new Error("密码不能为空") - } - - // 邮箱格式验证 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(userData.email)) { - throw new Error("邮箱格式不正确") - } - - // 用户名长度验证 - if (userData.username.length < 3 || userData.username.length > 20) { - throw new Error("用户名长度必须在3-20个字符之间") - } - - // 密码强度验证 - if (userData.password.length < 6) { - throw new Error("密码长度不能少于6个字符") - } - } - - /** - * 验证用户更新数据 - * @param {Object} updateData - 更新数据 - */ - static validateUserUpdateData(updateData) { - if (updateData.email) { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(updateData.email)) { - throw new Error("邮箱格式不正确") - } - } - - if (updateData.username) { - if (updateData.username.length < 3 || updateData.username.length > 20) { - throw new Error("用户名长度必须在3-20个字符之间") - } - } - - if (updateData.password && updateData.password.length < 6) { - throw new Error("密码长度不能少于6个字符") - } - } - - /** - * 检查唯一性约束 - * @param {Object} userData - 用户数据 - */ - static async checkUniqueConstraints(userData) { - if (userData.username) { - const existingUser = await UserModel.findByUsername(userData.username) - if (existingUser) { - throw new Error("用户名已存在") - } - } - - if (userData.email) { - const existingEmail = await UserModel.findByEmail(userData.email) - if (existingEmail) { - throw new Error("邮箱已存在") - } - } - } - - /** - * 检查更新时的唯一性约束 - * @param {number} id - 用户ID - * @param {Object} updateData - 更新数据 - */ - static async checkUniqueConstraintsForUpdate(id, updateData) { - if (updateData.username) { - const existingUser = await UserModel.findByUsername(updateData.username) - if (existingUser && existingUser.id !== parseInt(id)) { - throw new Error("用户名已存在") - } - } - - if (updateData.email) { - const existingEmail = await UserModel.findByEmail(updateData.email) - if (existingEmail && existingEmail.id !== parseInt(id)) { - throw new Error("邮箱已存在") - } - } - } - - /** - * 格式化用户响应数据 - * @param {Object} user - 用户数据 - * @returns {Object} 格式化后的用户数据 - */ - static formatUserResponse(user) { - const { password, ...userWithoutPassword } = user - return userWithoutPassword - } - - /** - * 批量创建用户 - * @param {Array} usersData - 用户数据数组 - * @returns {Promise} 创建结果 - */ - static async createUsersBatch(usersData) { - try { - const results = [] - const errors = [] - - for (let i = 0; i < usersData.length; i++) { - try { - const userData = usersData[i] - this.validateUserData(userData) - await this.checkUniqueConstraints(userData) - - const user = await UserModel.create(userData) - results.push(this.formatUserResponse(user)) - } catch (error) { - errors.push({ - index: i, - data: usersData[i], - error: error.message - }) - } - } - - return { - success: results, - errors, - summary: { - total: usersData.length, - success: results.length, - failed: errors.length - } - } - } catch (error) { - logger.error(`批量创建用户失败:`, error) - throw error - } - } - - /** - * 搜索用户 - * @param {string} keyword - 搜索关键词 - * @param {Object} options - 搜索选项 - * @returns {Promise} 搜索结果 - */ - static async searchUsers(keyword, options = {}) { - try { - const { - limit = 20, - role = null, - status = null - } = options - - const where = {} - if (role) where.role = role - if (status) where.status = status - - const users = await UserModel.findWhere(where, { - search: keyword, - searchFields: UserModel.searchableFields, - limit, - orderBy: "created_at", - order: "desc" - }) - - return users.map(user => this.formatUserResponse(user)) - } catch (error) { - logger.error(`搜索用户失败:`, error) - throw error - } - } -} - -export default UserService -export { UserService } diff --git a/src/services/index.js b/src/services/index.js deleted file mode 100644 index 357fa27..0000000 --- a/src/services/index.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * 服务层统一导出 - * 提供所有业务服务的统一访问入口 - */ - -import UserService from "./UserService.js" -import ArticleService from "./ArticleService.js" -import BookmarkService from "./BookmarkService.js" -import ContactService from "./ContactService.js" -import SiteConfigService from "./SiteConfigService.js" -import JobService from "./JobService.js" - -/** - * 服务层统一管理类 - * 提供所有业务服务的统一访问和管理 - */ -class ServiceManager { - constructor() { - this.services = { - user: UserService, - article: ArticleService, - bookmark: BookmarkService, - contact: ContactService, - siteConfig: SiteConfigService, - job: JobService - } - } - - /** - * 获取指定服务 - * @param {string} serviceName - 服务名称 - * @returns {Object} 服务实例 - */ - getService(serviceName) { - const service = this.services[serviceName] - if (!service) { - throw new Error(`服务 ${serviceName} 不存在`) - } - return service - } - - /** - * 获取所有服务列表 - * @returns {Array} 服务名称列表 - */ - getServiceList() { - return Object.keys(this.services) - } - - /** - * 检查服务是否存在 - * @param {string} serviceName - 服务名称 - * @returns {boolean} 是否存在 - */ - hasService(serviceName) { - return serviceName in this.services - } -} - -// 创建全局服务管理器实例 -const serviceManager = new ServiceManager() - -// 导出所有服务 -export { - UserService, - ArticleService, - BookmarkService, - ContactService, - SiteConfigService, - JobService, - ServiceManager -} - -// 导出服务管理器实例 -export default serviceManager - -// 便捷访问方法 -export const getUserService = () => serviceManager.getService('user') -export const getArticleService = () => serviceManager.getService('article') -export const getBookmarkService = () => serviceManager.getService('bookmark') -export const getContactService = () => serviceManager.getService('contact') -export const getSiteConfigService = () => serviceManager.getService('siteConfig') -export const getJobService = () => serviceManager.getService('job') - diff --git a/src/utils/ForRegister.js b/src/utils/ForRegister.js deleted file mode 100644 index 1107e0d..0000000 --- a/src/utils/ForRegister.js +++ /dev/null @@ -1,222 +0,0 @@ -// 自动扫描 controllers 目录并注册路由 -// 兼容传统 routes 方式和自动注册 controller 方式 -import fs from "fs" -import path from "path" -import { logger } from "@/logger.js" -import routeCache from "./cache/RouteCache.js" - -// 保证不会被摇树(tree-shaking),即使在生产环境也会被打包 -if (import.meta.env.PROD) { - // 通过引用返回值,防止被摇树优化 - let controllers = import.meta.glob("../controllers/**/*Controller.js", { eager: true }) - controllers = null -} - -/** - * 自动扫描 controllers 目录,注册所有导出的路由 - * 自动检测 routes 目录下已手动注册的 controller,避免重复注册 - * @param {Koa} app - Koa 实例 - * @param {string} controllersDir - controllers 目录路径 - * @param {string} prefix - 路由前缀 - * @param {Set} [manualControllers] - 可选,手动传入已注册 controller 文件名集合,优先于自动扫描 - */ -export async function autoRegisterControllers(app, controllersDir) { - let allRouter = [] - - async function scan(dir, routePrefix = "") { - try { - for (const file of fs.readdirSync(dir)) { - const fullPath = path.join(dir, file) - const stat = fs.statSync(fullPath) - - if (stat.isDirectory()) { - if (!file.startsWith("_")) { - await scan(fullPath, routePrefix + "/" + file) - } - } else if (file.endsWith("Controller.js") && !file.startsWith("_")) { - try { - const stat = fs.statSync(fullPath) - const mtime = stat.mtime.getTime() - - // 尝试从缓存获取路由注册结果 - let cachedRoutes = routeCache.getRegistration(fullPath, mtime) - - if (cachedRoutes) { - // 缓存命中,直接使用缓存结果 - allRouter.push(...cachedRoutes) - logger.debug(`[控制器注册] ✨ ${file} - 从缓存加载`) - continue - } - - // 使用动态导入ES模块 - const controllerModule = await import(fullPath) - const controller = controllerModule.default || controllerModule - - if (!controller) { - logger.warn(`[控制器注册] ${file} - 缺少默认导出,跳过注册`) - continue - } - - // 尝试从缓存获取控制器实例 - const className = controller.name || file.replace('.js', '') - let cachedController = routeCache.getController(className) - - const routes = controller.createRoutes || controller.default?.createRoutes || controller.default || controller - - if (typeof routes === "function") { - try { - const routerResult = routes() - const routersToProcess = Array.isArray(routerResult) ? routerResult : [routerResult] - const validRouters = [] - - for (const router of routersToProcess) { - if (router && typeof router.middleware === "function") { - validRouters.push(router) - } else { - logger.warn(`[控制器注册] ⚠️ ${file} - createRoutes() 返回的部分路由器对象无效`) - } - } - - if (validRouters.length > 0) { - allRouter.push(...validRouters) - - // 将路由注册结果存入缓存(如果缓存启用) - routeCache.setRegistration(fullPath, mtime, validRouters) - - // 将控制器类存入缓存以便复用(如果缓存启用) - if (!cachedController) { - routeCache.setController(className, controller) - } - - // 根据缓存状态显示不同的日志信息 - const cacheEnabled = routeCache.config.enabled - if (cacheEnabled) { - logger.debug(`[控制器注册] ✅ ${file} - 已缓存`) - } else { - logger.debug(`[控制器注册] ✅ ${file} - 创建成功`) - } - } else { - logger.warn(`[控制器注册] ⚠️ ${file} - createRoutes() 返回的不是有效的路由器对象`) - } - } catch (error) { - logger.error(`[控制器注册] ❌ ${file} - createRoutes() 执行失败: ${error.message}`) - } - } else { - logger.warn(`[控制器注册] ⚠️ ${file} - 未找到 createRoutes 方法或导出对象`) - } - } catch (importError) { - logger.error(`[控制器注册] ❌ ${file} - 模块导入失败: ${importError.message}`) - logger.error(importError) - } - } - } - } catch (error) { - logger.error(`[控制器注册] ❌ 扫描目录失败 ${dir}: ${error.message}`) - } - } - - try { - await scan(controllersDir) - - if (allRouter.length === 0) { - logger.warn("[路由注册] ⚠️ 未发现任何可注册的控制器") - return - } - - logger.info(`[路由注册] 📋 发现 ${allRouter.length} 个控制器,开始注册`) - - // 按顺序注册路由,确保中间件执行顺序 - const routeTable = [] - let totalRoutes = 0 - - for (let i = 0; i < allRouter.length; i++) { - const router = allRouter[i] - try { - app.use(router.middleware()) - - // 收集路由信息 - const methodEntries = Object.entries(router.routes || {}) - const routes = [] - for (const [method, list] of methodEntries) { - if (!Array.isArray(list) || list.length === 0) continue - for (const r of list) { - if (!r || !r.path) continue - routes.push({ - method: method.toUpperCase(), - path: r.path, - fullPath: `${method.toUpperCase()} ${r.path}` - }) - } - } - - if (routes.length > 0) { - const prefix = router.options && router.options.prefix ? router.options.prefix : "/" - routeTable.push({ - prefix: prefix, - count: routes.length, - routes: routes - }) - totalRoutes += routes.length - } - } catch (error) { - logger.error(`[路由注册] ❌ 路由注册失败: ${error.message}`) - } - } - - // 输出表格形式的路由摘要 - if (routeTable.length > 0) { - logger.info(`[路由注册] ✅ 注册完成!`) - logger.info(`+===============================================================+`) - logger.info(`| 路由注册摘要表 |`) - logger.info(`+===============================================================+`) - logger.info(`| 前缀路径 | 数量 | 路由详情 |`) - logger.info(`+===============================================================+`) - - routeTable.forEach(({ prefix, count, routes }) => { - const prefixStr = prefix.padEnd(28) - const countStr = count.toString().padStart(3) - const routesStr = routes.map(r => r.fullPath).join(', ') - - // 如果路由详情太长,分行显示 - if (routesStr.length > 40) { - const lines = [] - let currentLine = '' - const routeParts = routesStr.split(', ') - - for (const part of routeParts) { - if (currentLine.length + part.length + 2 > 40) { - lines.push(currentLine) - currentLine = part - } else { - currentLine += (currentLine ? ', ' : '') + part - } - } - if (currentLine) lines.push(currentLine) - - // 输出第一行 - logger.info(`| ${prefixStr} | ${countStr} | ${lines[0].padEnd(40)} |`) - - // 输出剩余行 - for (let i = 1; i < lines.length; i++) { - logger.info(`| ${' '.repeat(28)} | ${' '.repeat(3)} | ${lines[i].padEnd(40)} |`) - } - } else { - logger.info(`| ${prefixStr} | ${countStr} | ${routesStr.padEnd(40)} |`) - } - }) - - logger.info(`+===============================================================+`) - logger.info(`| 总计: ${totalRoutes} 条路由,${routeTable.length} 个控制器组 |`) - logger.info(`+===============================================================+`) - } - - // 输出缓存统计信息 - const cacheStats = routeCache.getStats() - if (cacheStats.enabled) { - logger.debug(`[路由缓存] 状态: 启用, 命中率: ${cacheStats.hitRate}`) - } - - } catch (error) { - logger.error(`[路由注册] ❌ 自动注册过程中发生严重错误: ${error.message}`) - } -} diff --git a/src/utils/cache/RouteCache.js b/src/utils/cache/RouteCache.js deleted file mode 100644 index 75edf81..0000000 --- a/src/utils/cache/RouteCache.js +++ /dev/null @@ -1,388 +0,0 @@ -import { BaseSingleton } from '../BaseSingleton.js' -import { logger } from '@/logger.js' -import config from '@/config/index.js' - -/** - * 路由缓存系统 - * 提供路由匹配、控制器实例、中间件组合等多层缓存 - * 使用单例模式确保全局唯一 - */ -class RouteCache extends BaseSingleton { - constructor() { - super() - - // 路由匹配缓存:method:path -> route - this.matchCache = new Map() - - // 控制器实例缓存:className -> instance - this.controllerCache = new Map() - - // 中间件组合缓存:cacheKey -> composedMiddleware - this.middlewareCache = new Map() - - // 路由注册缓存:filePath:mtime -> routes - this.registrationCache = new Map() - - // 缓存统计 - this.stats = { - matchHits: 0, - matchMisses: 0, - controllerHits: 0, - controllerMisses: 0, - middlewareHits: 0, - middlewareMisses: 0, - registrationHits: 0, - registrationMisses: 0 - } - - // 缓存配置 - this.config = { - // 路由匹配缓存最大条目数 - maxMatchCacheSize: config.routeCache?.maxMatchCacheSize || 1000, - // 控制器实例缓存最大条目数 - maxControllerCacheSize: config.routeCache?.maxControllerCacheSize || 100, - // 中间件组合缓存最大条目数 - maxMiddlewareCacheSize: config.routeCache?.maxMiddlewareCacheSize || 200, - // 路由注册缓存最大条目数 - maxRegistrationCacheSize: config.routeCache?.maxRegistrationCacheSize || 50, - // 是否启用缓存(开发环境可能需要禁用) - enabled: config.routeCache?.enabled ?? (process.env.NODE_ENV === 'production') - } - - logger.debug(`[路由缓存] 初始化完成,状态: ${this.config.enabled ? '启用' : '禁用'}`) - } - - /** - * 生成路由匹配缓存键 - * @param {string} method - HTTP方法 - * @param {string} path - 请求路径 - * @returns {string} 缓存键 - */ - _getMatchCacheKey(method, path) { - return `${method.toLowerCase()}:${path}` - } - - /** - * 生成中间件组合缓存键 - * @param {Array} middlewares - 中间件数组 - * @param {Object} authConfig - 认证配置 - * @returns {string} 缓存键 - */ - _getMiddlewareCacheKey(middlewares, authConfig) { - const middlewareIds = middlewares.map(m => m.name || m.toString().slice(0, 50)) - const authKey = JSON.stringify(authConfig) - return `${middlewareIds.join(':')}:${authKey}` - } - - /** - * 清理过期或超量缓存 - * @param {Map} cache - 缓存Map - * @param {number} maxSize - 最大大小 - */ - _evictCache(cache, maxSize) { - if (cache.size <= maxSize) return - - // 删除最旧的条目(简单LRU) - const toDelete = cache.size - maxSize + 1 - let deleted = 0 - for (const key of cache.keys()) { - cache.delete(key) - if (++deleted >= toDelete) break - } - } - - /** - * 获取路由匹配缓存 - * @param {string} method - HTTP方法 - * @param {string} path - 请求路径 - * @returns {Object|null} 缓存的路由匹配结果 - */ - getRouteMatch(method, path) { - if (!this.config.enabled) return null - - const key = this._getMatchCacheKey(method, path) - const cached = this.matchCache.get(key) - - if (cached) { - this.stats.matchHits++ - return cached - } - - this.stats.matchMisses++ - return null - } - - /** - * 设置路由匹配缓存 - * @param {string} method - HTTP方法 - * @param {string} path - 请求路径 - * @param {Object} route - 路由匹配结果 - */ - setRouteMatch(method, path, route) { - if (!this.config.enabled) return - - const key = this._getMatchCacheKey(method, path) - this.matchCache.set(key, route) - - // 缓存清理 - this._evictCache(this.matchCache, this.config.maxMatchCacheSize) - } - - /** - * 获取控制器实例缓存 - * @param {string} className - 控制器类名 - * @returns {Object|null} 缓存的控制器实例 - */ - getController(className) { - if (!this.config.enabled) return null - - const cached = this.controllerCache.get(className) - - if (cached) { - this.stats.controllerHits++ - return cached - } - - this.stats.controllerMisses++ - return null - } - - /** - * 设置控制器实例缓存 - * @param {string} className - 控制器类名 - * @param {Object} instance - 控制器实例 - */ - setController(className, instance) { - if (!this.config.enabled) return - - this.controllerCache.set(className, instance) - - // 缓存清理 - this._evictCache(this.controllerCache, this.config.maxControllerCacheSize) - } - - /** - * 获取中间件组合缓存 - * @param {Array} middlewares - 中间件数组 - * @param {Object} authConfig - 认证配置 - * @returns {Function|null} 缓存的组合中间件 - */ - getMiddlewareComposition(middlewares, authConfig) { - if (!this.config.enabled) return null - - const key = this._getMiddlewareCacheKey(middlewares, authConfig) - const cached = this.middlewareCache.get(key) - - if (cached) { - this.stats.middlewareHits++ - return cached - } - - this.stats.middlewareMisses++ - return null - } - - /** - * 设置中间件组合缓存 - * @param {Array} middlewares - 中间件数组 - * @param {Object} authConfig - 认证配置 - * @param {Function} composed - 组合后的中间件 - */ - setMiddlewareComposition(middlewares, authConfig, composed) { - if (!this.config.enabled) return - - const key = this._getMiddlewareCacheKey(middlewares, authConfig) - this.middlewareCache.set(key, composed) - - // 缓存清理 - this._evictCache(this.middlewareCache, this.config.maxMiddlewareCacheSize) - } - - /** - * 获取路由注册缓存 - * @param {string} filePath - 控制器文件路径 - * @param {number} mtime - 文件修改时间 - * @returns {Array|null} 缓存的路由数组 - */ - getRegistration(filePath, mtime) { - if (!this.config.enabled) return null - - const key = `${filePath}:${mtime}` - const cached = this.registrationCache.get(key) - - if (cached) { - this.stats.registrationHits++ - return cached - } - - this.stats.registrationMisses++ - return null - } - - /** - * 设置路由注册缓存 - * @param {string} filePath - 控制器文件路径 - * @param {number} mtime - 文件修改时间 - * @param {Array} routes - 路由数组 - */ - setRegistration(filePath, mtime, routes) { - if (!this.config.enabled) return - - const key = `${filePath}:${mtime}` - this.registrationCache.set(key, routes) - - // 清理旧的同文件缓存 - for (const cacheKey of this.registrationCache.keys()) { - if (cacheKey.startsWith(filePath + ':') && cacheKey !== key) { - this.registrationCache.delete(cacheKey) - } - } - - // 缓存清理 - this._evictCache(this.registrationCache, this.config.maxRegistrationCacheSize) - } - - /** - * 清除所有缓存 - */ - clearAll() { - this.matchCache.clear() - this.controllerCache.clear() - this.middlewareCache.clear() - this.registrationCache.clear() - - // 重置统计 - Object.keys(this.stats).forEach(key => { - this.stats[key] = 0 - }) - - logger.debug('[路由缓存] 所有缓存已清除') - } - - /** - * 清除路由匹配缓存 - */ - clearRouteMatches() { - this.matchCache.clear() - logger.debug('[路由缓存] 路由匹配缓存已清除') - } - - /** - * 清除控制器实例缓存 - */ - clearControllers() { - this.controllerCache.clear() - logger.debug('[路由缓存] 控制器实例缓存已清除') - } - - /** - * 清除中间件组合缓存 - */ - clearMiddlewares() { - this.middlewareCache.clear() - logger.debug('[路由缓存] 中间件组合缓存已清除') - } - - /** - * 清除路由注册缓存 - */ - clearRegistrations() { - this.registrationCache.clear() - logger.debug('[路由缓存] 路由注册缓存已清除') - } - - /** - * 根据文件路径清除相关缓存 - * @param {string} filePath - 文件路径 - */ - clearByFile(filePath) { - // 清除该文件的注册缓存 - for (const key of this.registrationCache.keys()) { - if (key.startsWith(filePath + ':')) { - this.registrationCache.delete(key) - } - } - - // 清除路由匹配缓存(因为路由可能已变更) - this.clearRouteMatches() - - logger.debug(`[路由缓存] 已清除文件 ${filePath} 相关缓存`) - } - - /** - * 获取缓存统计信息 - * @returns {Object} 统计信息 - */ - getStats() { - const totalHits = this.stats.matchHits + this.stats.controllerHits + - this.stats.middlewareHits + this.stats.registrationHits - const totalMisses = this.stats.matchMisses + this.stats.controllerMisses + - this.stats.middlewareMisses + this.stats.registrationMisses - const hitRate = totalHits + totalMisses > 0 ? (totalHits / (totalHits + totalMisses) * 100).toFixed(2) : 0 - - return { - enabled: this.config.enabled, - hitRate: `${hitRate}%`, - caches: { - routeMatches: { - size: this.matchCache.size, - hits: this.stats.matchHits, - misses: this.stats.matchMisses, - hitRate: this.stats.matchHits + this.stats.matchMisses > 0 ? - `${(this.stats.matchHits / (this.stats.matchHits + this.stats.matchMisses) * 100).toFixed(2)}%` : '0%' - }, - controllers: { - size: this.controllerCache.size, - hits: this.stats.controllerHits, - misses: this.stats.controllerMisses, - hitRate: this.stats.controllerHits + this.stats.controllerMisses > 0 ? - `${(this.stats.controllerHits / (this.stats.controllerHits + this.stats.controllerMisses) * 100).toFixed(2)}%` : '0%' - }, - middlewares: { - size: this.middlewareCache.size, - hits: this.stats.middlewareHits, - misses: this.stats.middlewareMisses, - hitRate: this.stats.middlewareHits + this.stats.middlewareMisses > 0 ? - `${(this.stats.middlewareHits / (this.stats.middlewareHits + this.stats.middlewareMisses) * 100).toFixed(2)}%` : '0%' - }, - registrations: { - size: this.registrationCache.size, - hits: this.stats.registrationHits, - misses: this.stats.registrationMisses, - hitRate: this.stats.registrationHits + this.stats.registrationMisses > 0 ? - `${(this.stats.registrationHits / (this.stats.registrationHits + this.stats.registrationMisses) * 100).toFixed(2)}%` : '0%' - } - } - } - } - - /** - * 更新缓存配置 - * @param {Object} newConfig - 新配置 - */ - updateConfig(newConfig) { - this.config = { ...this.config, ...newConfig } - logger.debug('[路由缓存] 配置已更新', this.config) - } - - /** - * 启用缓存 - */ - enable() { - this.config.enabled = true - logger.debug('[路由缓存] 缓存已启用') - } - - /** - * 禁用缓存 - */ - disable() { - this.config.enabled = false - this.clearAll() - logger.debug('[路由缓存] 缓存已禁用并清除') - } -} - -// 导出单例实例 -export default RouteCache.getInstance() -export { RouteCache } \ No newline at end of file diff --git a/src/utils/error/ApiError.js b/src/utils/error/ApiError.js new file mode 100644 index 0000000..a73b144 --- /dev/null +++ b/src/utils/error/ApiError.js @@ -0,0 +1,19 @@ +import app from "@/global.js" +import BaseError from "./BaseError.js" + +export default class ApiError extends BaseError { + constructor(message, status = ApiError.ERR_CODE.BAD_REQUEST) { + super(message, status) + this.name = "ApiError" + const ctx = app.currentContext + this.ctx = ctx + this.user = ctx?.state?.user || null + this.info = { + path: ctx?.path || "", + method: ctx?.method || "", + query: ctx?.query || {}, + body: ctx?.request?.body || {}, + params: ctx?.params || {}, + } + } +} diff --git a/src/utils/error/CommonError.js b/src/utils/error/CommonError.js index 42ea0c8..446e2b0 100644 --- a/src/utils/error/CommonError.js +++ b/src/utils/error/CommonError.js @@ -5,6 +5,15 @@ export default class CommonError extends BaseError { constructor(message, status = CommonError.ERR_CODE.BAD_REQUEST) { super(message, status) this.name = "CommonError" - this.ctx = app.currentContext + const ctx = app.currentContext + // this.ctx = ctx + this.user = ctx?.state?.user || null + this.info = { + path: ctx?.path || "", + method: ctx?.method || "", + query: ctx?.query || {}, + body: ctx?.request?.body || {}, + params: ctx?.params || {}, + } } } diff --git a/src/utils/router.js b/src/utils/router.js index 6ed3dcc..5ae13f8 100644 --- a/src/utils/router.js +++ b/src/utils/router.js @@ -1,32 +1,8 @@ import { match } from 'path-to-regexp'; import compose from 'koa-compose'; -import routeCache from './cache/RouteCache.js'; import AuthError from './error/AuthError.js'; import CommonError from './error/CommonError.js'; -function RouteAuth(options = {}) { - const { auth = true } = options - return async (ctx, next) => { - // 当 auth 为 false 时,已登录用户不能访问 - if (auth === false) { - if (ctx.state.user) { - throw new CommonError("该接口不能登录查看") - } - } - - // 当 auth 为 true 时,必须登录才能访问 - if (auth === true) { - if (!ctx.state.user) { - throw new AuthError("该接口必须登录查看") - } - } - - // 其他自定义模式(如角色检查等) - return await next() - } -} - - class Router { /** * 初始化路由实例 @@ -106,22 +82,11 @@ class Router { * 生成Koa中间件 * @returns {Function} Koa中间件函数 */ - middleware() { + middleware(beforeMiddleware) { return async (ctx, next) => { const { method, path } = ctx; - - // 尝试从缓存获取路由匹配结果 - let route = routeCache.getRouteMatch(method, path); - - if (!route) { - // 缓存未命中,执行路由匹配 - route = this._matchRoute(method.toLowerCase(), path); - - // 将匹配结果存入缓存 - if (route) { - routeCache.setRouteMatch(method, path, route); - } - } + // 直接进行路由匹配(不使用缓存) + const route = this._matchRoute(method.toLowerCase(), path); // 组合全局中间件、路由专属中间件和 handler const middlewares = [...this.middlewares]; @@ -129,31 +94,13 @@ class Router { // 如果匹配到路由,添加路由专属中间件和处理函数 ctx.params = route.params; - let isAuth = this.options.auth; - if (route.meta && route.meta.auth !== undefined) { - isAuth = route.meta.auth; - } - - // 尝试从缓存获取组合中间件 - const cacheKey = { auth: isAuth, middlewares: this.middlewares.length }; - let composed = routeCache.getMiddlewareComposition(this.middlewares, cacheKey); - - if (!composed) { - // 缓存未命中,重新组合中间件 - middlewares.push(RouteAuth({ auth: isAuth })); - middlewares.push(route.handler); - composed = compose(middlewares); - - // 将组合结果存入缓存 - routeCache.setMiddlewareComposition(this.middlewares, cacheKey, composed); - } else { - // 缓存命中,但仍需添加当前路由的处理器 - const finalMiddlewares = [...middlewares]; - finalMiddlewares.push(RouteAuth({ auth: isAuth })); - finalMiddlewares.push(route.handler); - composed = compose(finalMiddlewares); + if (beforeMiddleware) { + const options = Object.assign({}, this.options, route.meta); + middlewares.push(beforeMiddleware(options)); } + middlewares.push(route.handler); + const composed = compose(middlewares); await composed(ctx, next); } else { // 如果没有匹配到路由,直接调用 next @@ -187,14 +134,6 @@ class Router { } return null; } - - /** - * 清除该路由器的相关缓存 - */ - clearCache() { - routeCache.clearRouteMatches(); - routeCache.clearMiddlewares(); - } } export default Router; \ No newline at end of file diff --git a/src/utils/router/RouteAuth.js b/src/utils/router/RouteAuth.js deleted file mode 100644 index 691952a..0000000 --- a/src/utils/router/RouteAuth.js +++ /dev/null @@ -1,26 +0,0 @@ -import CommonError from '../error/CommonError' -import AuthError from '../error/AuthError' - -export default function RouteAuth(options = {}) { - const { auth = true } = options - return async (ctx, next) => { - // 当 auth 为 false 时,已登录用户不能访问 - if (auth === false) { - if (ctx.state.user) { - throw new CommonError("该接口不能登录查看") - } - return await next() - } - - // 当 auth 为 true 时,必须登录才能访问 - if (auth === true) { - if (!ctx.state.user) { - throw new AuthError("该接口必须登录查看") - } - return await next() - } - - // 其他自定义模式(如角色检查等) - return await next() - } -} diff --git a/src/utils/test/ConfigTest.js b/src/utils/test/ConfigTest.js deleted file mode 100644 index 933379b..0000000 --- a/src/utils/test/ConfigTest.js +++ /dev/null @@ -1,198 +0,0 @@ -import config from '../config/index.js' -import performanceMonitor from '../middlewares/RoutePerformance/index.js' -import { logger } from '@/logger.js' - -/** - * 配置测试工具 - * 验证配置抽离后的功能是否正常 - */ -class ConfigTest { - constructor() { - this.testResults = [] - } - - /** - * 运行配置测试 - */ - async runTests() { - logger.info('[配置测试] 开始测试路由性能监控配置') - - try { - await this.testDefaultConfig() - await this.testConfigUpdate() - await this.testEnvironmentVariables() - await this.testConfigIntegration() - - this.printResults() - } catch (error) { - logger.error('[配置测试] 测试过程中发生错误:', error) - } - } - - /** - * 测试默认配置 - */ - async testDefaultConfig() { - try { - // 验证路由缓存配置 - this.assert(config.routeCache !== undefined, '路由缓存配置应该存在') - this.assert(typeof config.routeCache.enabled === 'boolean', '缓存启用状态应该是布尔值') - - // 验证性能监控配置 - this.assert(config.routePerformance !== undefined, '性能监控配置应该存在') - this.assert(typeof config.routePerformance.windowSize === 'number', '窗口大小应该是数字') - this.assert(typeof config.routePerformance.slowRouteThreshold === 'number', '慢路由阈值应该是数字') - - this.addTestResult('默认配置验证', true, '所有默认配置项正确') - } catch (error) { - this.addTestResult('默认配置验证', false, error.message) - } - } - - /** - * 测试配置更新 - */ - async testConfigUpdate() { - try { - const originalConfig = { ...performanceMonitor.config } - - // 更新配置 - const newConfig = { - windowSize: 200, - slowRouteThreshold: 1000, - enableOptimizationSuggestions: false - } - - performanceMonitor.updateConfig(newConfig) - - // 验证配置是否更新 - this.assert(performanceMonitor.config.windowSize === 200, '窗口大小应该被更新') - this.assert(performanceMonitor.config.slowRouteThreshold === 1000, '慢路由阈值应该被更新') - this.assert(performanceMonitor.config.enableOptimizationSuggestions === false, '优化建议应该被禁用') - - // 恢复原配置 - performanceMonitor.updateConfig(originalConfig) - - this.addTestResult('配置更新', true, '配置更新功能正常') - } catch (error) { - this.addTestResult('配置更新', false, error.message) - } - } - - /** - * 测试环境变量支持 - */ - async testEnvironmentVariables() { - try { - // 测试环境变量解析 - const originalEnv = process.env.PERFORMANCE_MONITOR - - // 设置环境变量 - process.env.PERFORMANCE_MONITOR = 'true' - - // 重新导入配置(模拟) - const testEnabled = process.env.NODE_ENV === 'production' || process.env.PERFORMANCE_MONITOR === 'true' - this.assert(testEnabled === true, '环境变量应该影响配置') - - // 恢复环境变量 - if (originalEnv !== undefined) { - process.env.PERFORMANCE_MONITOR = originalEnv - } else { - delete process.env.PERFORMANCE_MONITOR - } - - this.addTestResult('环境变量支持', true, '环境变量配置正常') - } catch (error) { - this.addTestResult('环境变量支持', false, error.message) - } - } - - /** - * 测试配置集成 - */ - async testConfigIntegration() { - try { - // 测试性能监控使用配置 - const report = performanceMonitor.getPerformanceReport() - this.assert(report.config !== undefined, '性能报告应该包含配置信息') - this.assert(typeof report.config.windowSize === 'number', '窗口大小配置应该存在') - this.assert(typeof report.config.slowRouteThreshold === 'number', '慢路由阈值配置应该存在') - - // 测试配置默认值 - this.assert(config.routePerformance.maxRouteReportCount > 0, '最大路由报告数量应该大于0') - this.assert(config.routePerformance.cacheHitRateWarningThreshold >= 0 && - config.routePerformance.cacheHitRateWarningThreshold <= 1, '缓存命中率阈值应该在0-1之间') - - this.addTestResult('配置集成', true, '配置集成功能正常') - } catch (error) { - this.addTestResult('配置集成', false, error.message) - } - } - - /** - * 断言辅助函数 - */ - assert(condition, message) { - if (!condition) { - throw new Error(`断言失败: ${message}`) - } - } - - /** - * 添加测试结果 - */ - addTestResult(testName, passed, message) { - this.testResults.push({ - name: testName, - passed, - message, - timestamp: new Date().toISOString() - }) - - const status = passed ? '✅ 通过' : '❌ 失败' - logger.info(`[配置测试] ${testName}: ${status} - ${message}`) - } - - /** - * 打印测试结果 - */ - printResults() { - const totalTests = this.testResults.length - const passedTests = this.testResults.filter(r => r.passed).length - const failedTests = totalTests - passedTests - - logger.info('[配置测试] =================== 测试结果摘要 ===================') - logger.info(`[配置测试] 总计测试: ${totalTests}`) - logger.info(`[配置测试] 通过: ${passedTests}`) - logger.info(`[配置测试] 失败: ${failedTests}`) - logger.info(`[配置测试] 成功率: ${((passedTests / totalTests) * 100).toFixed(2)}%`) - - if (failedTests > 0) { - logger.warn('[配置测试] 失败的测试:') - this.testResults.filter(r => !r.passed).forEach(result => { - logger.warn(`[配置测试] - ${result.name}: ${result.message}`) - }) - } - - // 输出当前配置 - logger.info('[配置测试] 当前路由性能监控配置:') - logger.info('[配置测试]', config.routePerformance) - - logger.info('[配置测试] ================================================') - } - - /** - * 显示配置信息 - */ - showConfigInfo() { - logger.info('[配置信息] =================== 配置详情 ===================') - logger.info('[配置信息] 路由缓存配置:', config.routeCache) - logger.info('[配置信息] 性能监控配置:', config.routePerformance) - logger.info('[配置信息] 当前监控器配置:', performanceMonitor.config) - logger.info('[配置信息] ==============================================') - } -} - -// 导出测试类 -export default ConfigTest -export { ConfigTest } \ No newline at end of file diff --git a/src/utils/test/RouteCacheTest.js b/src/utils/test/RouteCacheTest.js deleted file mode 100644 index 4f84f84..0000000 --- a/src/utils/test/RouteCacheTest.js +++ /dev/null @@ -1,222 +0,0 @@ -import routeCache from '../utils/cache/RouteCache.js' -import performanceMonitor from '../middlewares/RoutePerformance/index.js' -import { logger } from '@/logger.js' - -/** - * 路由缓存测试工具 - * 用于验证缓存功能和性能监控 - */ -class RouteCacheTest { - constructor() { - this.testResults = [] - } - - /** - * 运行所有测试 - */ - async runAllTests() { - logger.info('[缓存测试] 开始运行路由缓存测试套件') - - // 清除之前的测试数据 - this.testResults = [] - routeCache.clearAll() - - try { - await this.testBasicCaching() - await this.testControllerCaching() - await this.testPerformanceMonitoring() - await this.testCacheConfiguration() - - this.printTestResults() - } catch (error) { - logger.error('[缓存测试] 测试过程中发生错误:', error) - } - } - - /** - * 测试基本路由缓存功能 - */ - async testBasicCaching() { - logger.info('[缓存测试] 测试基本路由缓存功能') - - try { - // 测试缓存miss - const route1 = routeCache.getRouteMatch('GET', '/api/test') - this.assert(route1 === null, '初始状态应该缓存miss') - - // 测试缓存set - const mockRoute = { path: '/api/test', params: {}, handler: () => {}, meta: {} } - routeCache.setRouteMatch('GET', '/api/test', mockRoute) - - // 测试缓存hit - const route2 = routeCache.getRouteMatch('GET', '/api/test') - this.assert(route2 !== null, '设置缓存后应该命中') - this.assert(route2.path === '/api/test', '缓存内容应该正确') - - this.addTestResult('基本路由缓存', true, '缓存设置和获取功能正常') - } catch (error) { - this.addTestResult('基本路由缓存', false, error.message) - } - } - - /** - * 测试控制器缓存功能 - */ - async testControllerCaching() { - logger.info('[缓存测试] 测试控制器缓存功能') - - try { - // 测试控制器缓存miss - const controller1 = routeCache.getController('TestController') - this.assert(controller1 === null, '初始状态控制器缓存应该miss') - - // 测试控制器缓存set - const mockController = { name: 'TestController', methods: ['test'] } - routeCache.setController('TestController', mockController) - - // 测试控制器缓存hit - const controller2 = routeCache.getController('TestController') - this.assert(controller2 !== null, '设置控制器缓存后应该命中') - this.assert(controller2.name === 'TestController', '控制器缓存内容应该正确') - - this.addTestResult('控制器缓存', true, '控制器缓存功能正常') - } catch (error) { - this.addTestResult('控制器缓存', false, error.message) - } - } - - /** - * 测试性能监控功能 - */ - async testPerformanceMonitoring() { - logger.info('[缓存测试] 测试性能监控功能') - - try { - // 启用性能监控 - performanceMonitor.enable() - - // 模拟记录一些性能数据 - performanceMonitor.recordPerformance('GET', '/api/test', 150, false) - performanceMonitor.recordPerformance('GET', '/api/test', 120, true) - performanceMonitor.recordPerformance('GET', '/api/test', 180, false) - - // 获取性能报告 - const report = performanceMonitor.getPerformanceReport() - this.assert(report.enabled === true, '性能监控应该启用') - this.assert(report.routes.length > 0, '应该有性能数据') - - const testRoute = report.routes.find(r => r.path === '/api/test') - this.assert(testRoute !== undefined, '应该找到测试路由的性能数据') - this.assert(testRoute.requestCount === 3, '请求计数应该正确') - - this.addTestResult('性能监控', true, '性能监控功能正常') - } catch (error) { - this.addTestResult('性能监控', false, error.message) - } - } - - /** - * 测试缓存配置功能 - */ - async testCacheConfiguration() { - logger.info('[缓存测试] 测试缓存配置功能') - - try { - // 获取初始统计 - const initialStats = routeCache.getStats() - this.assert(typeof initialStats.hitRate === 'string', '命中率应该是字符串格式') - this.assert(initialStats.caches !== undefined, '应该有缓存统计信息') - - // 测试禁用缓存 - routeCache.disable() - this.assert(routeCache.config.enabled === false, '缓存应该被禁用') - - // 测试启用缓存 - routeCache.enable() - this.assert(routeCache.config.enabled === true, '缓存应该被启用') - - // 测试配置更新 - const newConfig = { maxMatchCacheSize: 2000 } - routeCache.updateConfig(newConfig) - this.assert(routeCache.config.maxMatchCacheSize === 2000, '配置应该被更新') - - this.addTestResult('缓存配置', true, '缓存配置功能正常') - } catch (error) { - this.addTestResult('缓存配置', false, error.message) - } - } - - /** - * 断言辅助函数 - */ - assert(condition, message) { - if (!condition) { - throw new Error(`断言失败: ${message}`) - } - } - - /** - * 添加测试结果 - */ - addTestResult(testName, passed, message) { - this.testResults.push({ - name: testName, - passed, - message, - timestamp: new Date().toISOString() - }) - - const status = passed ? '✅ 通过' : '❌ 失败' - logger.info(`[缓存测试] ${testName}: ${status} - ${message}`) - } - - /** - * 打印测试结果摘要 - */ - printTestResults() { - const totalTests = this.testResults.length - const passedTests = this.testResults.filter(r => r.passed).length - const failedTests = totalTests - passedTests - - logger.info('[缓存测试] =================== 测试结果摘要 ===================') - logger.info(`[缓存测试] 总计测试: ${totalTests}`) - logger.info(`[缓存测试] 通过: ${passedTests}`) - logger.info(`[缓存测试] 失败: ${failedTests}`) - logger.info(`[缓存测试] 成功率: ${((passedTests / totalTests) * 100).toFixed(2)}%`) - - if (failedTests > 0) { - logger.warn('[缓存测试] 失败的测试:') - this.testResults.filter(r => !r.passed).forEach(result => { - logger.warn(`[缓存测试] - ${result.name}: ${result.message}`) - }) - } - - // 输出缓存统计 - const stats = routeCache.getStats() - logger.info('[缓存测试] 最终缓存统计:', stats) - - logger.info('[缓存测试] ================================================') - } - - /** - * 获取测试结果 - */ - getTestResults() { - return { - summary: { - total: this.testResults.length, - passed: this.testResults.filter(r => r.passed).length, - failed: this.testResults.filter(r => !r.passed).length, - successRate: this.testResults.length > 0 ? - ((this.testResults.filter(r => r.passed).length / this.testResults.length) * 100).toFixed(2) + '%' : '0%' - }, - details: this.testResults, - cacheStats: routeCache.getStats(), - performanceReport: performanceMonitor.getPerformanceReport() - } - } -} - -// 导出测试类 -export default RouteCacheTest -export { RouteCacheTest } \ No newline at end of file diff --git a/src/utils/user.js b/src/utils/user.js deleted file mode 100644 index 42cd922..0000000 --- a/src/utils/user.js +++ /dev/null @@ -1,20 +0,0 @@ -import CommonError from "./error/CommonError" -import jwt from "./jwt" - -function verifyUser() { - return async (ctx, next) => { - if (ctx.session.user) { - ctx.user = ctx.session.user - return next() - } - const authorizationString = ctx.headers["authorization"] - if(!authorizationString) { - throw new CommonError("请登录") - } - const token = ctx.headers["authorization"]?.replace(/^Bearer\s/, "") - ctx.user = jwt.verify(token, process.env.JWT_SECRET) - return next() - } -} - -export default verifyUser \ No newline at end of file diff --git a/src/views/page/login/_ui/password.pug b/src/views/page/login/_ui/password.pug new file mode 100644 index 0000000..c827ccd --- /dev/null +++ b/src/views/page/login/_ui/password.pug @@ -0,0 +1,12 @@ + +div(hx-target="this" hx-swap="outerHTML") + div(class="relative") + label.block.text-sm.font-medium.text-gray-700.mb-2(for="password") 密码 + input(type="password" id="password" value=value name="password" placeholder="请输入密码" hx-indicator="#ind" hx-post="/login/validate/password" hx-trigger="blur delay:500ms" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 ease-in-out" + (error ? ' border-red-500 focus:ring-red-500' : '')) + div(id="ind" class="htmx-indicator absolute right-3 top-9 transform -translate-y-1/2") + div(class="animate-spin h-5 w-5 border-2 border-gray-300 border-t-blue-500 rounded-full") + if error + div(class="error-message text-red-500 text-sm mt-2 flex items-center") + svg.w-4.h-4.mr-1(xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor") + path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z") + | #{error} \ No newline at end of file diff --git a/src/views/page/login/_ui/username.pug b/src/views/page/login/_ui/username.pug index 94f622b..16e48bc 100644 --- a/src/views/page/login/_ui/username.pug +++ b/src/views/page/login/_ui/username.pug @@ -1,4 +1,12 @@ -div(hx-target="this" hx-swap="outerHTML" hx-post="/login/validate/username?aa=1") - input(type="text" id="username" value=value name="username" placeholder="用户名" hx-indicator="#ind") - //- img(id="ind" src="/img/bars.svg" class="htmx-indicator") - div sda \ No newline at end of file + +div(hx-target="this" hx-swap="outerHTML") + div(class="relative") + label.block.text-sm.font-medium.text-gray-700.mb-2(for="username") 用户名 + input(type="text" id="username" value=value name="username" placeholder="请输入用户名" hx-indicator="#ind" hx-post="/login/validate/username" hx-trigger="blur delay:500ms" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 ease-in-out" + (error ? ' border-red-500 focus:ring-red-500' : '')) + div(id="ind" class="htmx-indicator absolute right-3 top-9 transform -translate-y-1/2") + div(class="animate-spin h-5 w-5 border-2 border-gray-300 border-t-blue-500 rounded-full") + if error + div(class="error-message text-red-500 text-sm mt-2 flex items-center") + svg.w-4.h-4.mr-1(xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor") + path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z") + | #{error} \ No newline at end of file diff --git a/src/views/page/login/index.pug b/src/views/page/login/index.pug index 31fae6c..b240718 100644 --- a/src/views/page/login/index.pug +++ b/src/views/page/login/index.pug @@ -1,9 +1,45 @@ extends /layouts/empty.pug block pageHead + //- style. + //- body { + //- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + //- min-height: 100vh; + //- } block pageContent - form(hx-post="/login") - include _ui/username.pug - input(type="password" name="password" placeholder="密码") - button(type="submit") 登录 \ No newline at end of file + .h-full.flex.items-center.justify-end.px-4.bg-red-400(class="sm:px-6 lg:px-8") + .max-w-md.w-full.space-y-8 + .bg-white.py-8.px-4.shadow-xl.rounded-2xl(class="sm:px-10") + .text-center.mb-8 + h2.text-3xl.font-bold.text-gray-900 欢迎回来 + p.text-gray-600.mt-2 请登录您的账户 + form.space-y-6(hx-post="/login") + include _ui/username.pug + include _ui/password.pug + .flex.items-center.justify-between + .flex.items-center + input#remember-me.h-4.w-4.text-blue-600.border-gray-300.rounded(type="checkbox" class="focus:ring-blue-500") + label.ml-2.block.text-sm.text-gray-900(for="remember-me") 记住我 + .text-sm + a.font-medium.text-blue-600(href="#" class="hover:text-blue-500") 忘记密码? + div + button.group.relative.w-full.flex.justify-center.py-3.px-4.border.border-transparent.text-sm.font-medium.rounded-md.text-white.bg-blue-600(type="submit" class="hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150 ease-in-out") + span.absolute.left-0.inset-y-0.flex.items-center.pl-3 + //- svg.h-5.w-5.text-blue-500(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="group-hover:text-blue-400") + //- path(fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd") + span 登录 + .text-center + p.text-sm.text-gray-600 + | 还没有账户? + a.font-medium.text-blue-600(href="/register" class="hover:text-blue-500") 立即注册 + +block pageScripts + script. + document.addEventListener('htmx:error', function(evt) { + if(evt.detail.elt instanceof HTMLElement) { + if(evt.detail.elt.tagName === 'FORM' && evt.detail.xhr) { + window.alert(evt.detail.xhr.response || '请求失败') + } + } + }); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index fcf832d..ef21700 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,7 +19,6 @@ export default defineConfig({ db: resolve(__dirname, "src/db"), config: resolve(__dirname, "src/config"), utils: resolve(__dirname, "src/utils"), - services: resolve(__dirname, "src/services"), }, }, build: {