Browse Source
- 删除 AuthController 和 RouteCacheController 控制器,简化 API 结构 - 移除多余的服务文件,提升代码可维护性 - 更新路由配置,确保新控制器的注册和中间件的使用 - 在登录页面中引入新的用户名和密码验证组件,增强用户体验 - 更新样式文件,改善登录页面的视觉效果pure
51 changed files with 1894 additions and 4903 deletions
@ -1,56 +1,3 @@ |
|||||
export default { |
export default { |
||||
base: "/", |
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 |
|
||||
} |
|
||||
} |
} |
||||
|
|||||
@ -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 |
|
||||
@ -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 |
|
||||
@ -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 |
|
||||
} |
|
||||
} |
|
||||
@ -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) |
||||
|
} |
||||
|
} |
||||
@ -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 } |
|
||||
@ -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", "/") |
||||
|
} |
||||
|
} |
||||
@ -1,5 +1,5 @@ |
|||||
import ContactModel from "../db/models/ContactModel.js" |
import ContactModel from "@/db/models/ContactModel.js" |
||||
import { logger } from "../logger.js" |
import { logger } from "@/logger.js" |
||||
|
|
||||
/** |
/** |
||||
* 联系信息服务类 |
* 联系信息服务类 |
||||
@ -1,4 +1,4 @@ |
|||||
import jobs from "../jobs" |
import jobs from "@/jobs" |
||||
|
|
||||
class JobService { |
class JobService { |
||||
startJob(id) { |
startJob(id) { |
||||
@ -1,5 +1,5 @@ |
|||||
import SiteConfigModel from "../db/models/SiteConfigModel.js" |
import SiteConfigModel from "@/db/models/SiteConfigModel.js" |
||||
import { logger } from "../logger.js" |
import { logger } from "@/logger.js" |
||||
|
|
||||
/** |
/** |
||||
* 站点配置服务类 |
* 站点配置服务类 |
||||
@ -1,563 +0,0 @@ |
|||||
import ArticleModel from "../db/models/ArticleModel.js" |
|
||||
import { logger } from "../logger.js" |
|
||||
|
|
||||
/** |
|
||||
* 文章服务类 |
|
||||
* 提供文章相关的业务逻辑 |
|
||||
*/ |
|
||||
class ArticleService { |
|
||||
/** |
|
||||
* 创建新文章 |
|
||||
* @param {Object} articleData - 文章数据 |
|
||||
* @returns {Promise<Object>} 创建的文章信息 |
|
||||
*/ |
|
||||
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<Object|null>} 文章信息 |
|
||||
*/ |
|
||||
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<Object|null>} 文章信息 |
|
||||
*/ |
|
||||
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<Object>} 更新后的文章信息 |
|
||||
*/ |
|
||||
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<boolean>} 删除结果 |
|
||||
*/ |
|
||||
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<Object>} 发布后的文章信息 |
|
||||
*/ |
|
||||
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<Object>} 取消发布后的文章信息 |
|
||||
*/ |
|
||||
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<Object>} 文章列表和分页信息 |
|
||||
*/ |
|
||||
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<Object>} 文章列表和分页信息 |
|
||||
*/ |
|
||||
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<Object>} 文章列表和分页信息 |
|
||||
*/ |
|
||||
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<Array>} 文章列表 |
|
||||
*/ |
|
||||
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<Array>} 文章列表 |
|
||||
*/ |
|
||||
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<Array>} 搜索结果 |
|
||||
*/ |
|
||||
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<Array>} 最新文章列表 |
|
||||
*/ |
|
||||
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<Array>} 热门文章列表 |
|
||||
*/ |
|
||||
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<Array>} 精选文章列表 |
|
||||
*/ |
|
||||
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<Array>} 相关文章列表 |
|
||||
*/ |
|
||||
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<Object>} 统计信息 |
|
||||
*/ |
|
||||
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<number>} 更新数量 |
|
||||
*/ |
|
||||
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<Array>} 分类统计 |
|
||||
*/ |
|
||||
static async getCategoryStats() { |
|
||||
try { |
|
||||
return await ArticleModel.getArticleCountByCategory() |
|
||||
} catch (error) { |
|
||||
logger.error(`获取文章分类统计失败:`, error) |
|
||||
throw error |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 获取文章标签列表 |
|
||||
* @returns {Promise<Array>} 标签列表 |
|
||||
*/ |
|
||||
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 } |
|
||||
@ -1,492 +0,0 @@ |
|||||
import BookmarkModel from "../db/models/BookmarkModel.js" |
|
||||
import { logger } from "../logger.js" |
|
||||
|
|
||||
/** |
|
||||
* 书签服务类 |
|
||||
* 提供书签相关的业务逻辑 |
|
||||
*/ |
|
||||
class BookmarkService { |
|
||||
/** |
|
||||
* 创建新书签 |
|
||||
* @param {Object} bookmarkData - 书签数据 |
|
||||
* @returns {Promise<Object>} 创建的书签信息 |
|
||||
*/ |
|
||||
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<Object|null>} 书签信息 |
|
||||
*/ |
|
||||
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<Object>} 更新后的书签信息 |
|
||||
*/ |
|
||||
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<boolean>} 删除结果 |
|
||||
*/ |
|
||||
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<Object>} 书签列表和分页信息 |
|
||||
*/ |
|
||||
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<Object>} 书签列表和分页信息 |
|
||||
*/ |
|
||||
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<Array>} 书签列表 |
|
||||
*/ |
|
||||
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<Array>} 热门书签列表 |
|
||||
*/ |
|
||||
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<Array>} 搜索结果 |
|
||||
*/ |
|
||||
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<boolean>} 是否存在 |
|
||||
*/ |
|
||||
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<Object>} 统计信息 |
|
||||
*/ |
|
||||
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<number>} 删除数量 |
|
||||
*/ |
|
||||
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<Object>} 创建结果 |
|
||||
*/ |
|
||||
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<Array>} 分类统计 |
|
||||
*/ |
|
||||
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<Object>} 导入结果 |
|
||||
*/ |
|
||||
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 } |
|
||||
@ -1,415 +0,0 @@ |
|||||
import UserModel from "../db/models/UserModel.js" |
|
||||
import { logger } from "../logger.js" |
|
||||
|
|
||||
/** |
|
||||
* 用户服务类 |
|
||||
* 提供用户相关的业务逻辑 |
|
||||
*/ |
|
||||
class UserService { |
|
||||
/** |
|
||||
* 创建新用户 |
|
||||
* @param {Object} userData - 用户数据 |
|
||||
* @returns {Promise<Object>} 创建的用户信息 |
|
||||
*/ |
|
||||
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<Object|null>} 用户信息 |
|
||||
*/ |
|
||||
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<Object|null>} 用户信息 |
|
||||
*/ |
|
||||
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<Object|null>} 用户信息 |
|
||||
*/ |
|
||||
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<Object>} 更新后的用户信息 |
|
||||
*/ |
|
||||
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<boolean>} 删除结果 |
|
||||
*/ |
|
||||
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<Object>} 用户列表和分页信息 |
|
||||
*/ |
|
||||
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<Object>} 更新后的用户信息 |
|
||||
*/ |
|
||||
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<Object>} 更新后的用户信息 |
|
||||
*/ |
|
||||
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<Array>} 用户列表 |
|
||||
*/ |
|
||||
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<Object>} 统计信息 |
|
||||
*/ |
|
||||
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<Array>} 创建结果 |
|
||||
*/ |
|
||||
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<Array>} 搜索结果 |
|
||||
*/ |
|
||||
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 } |
|
||||
@ -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') |
|
||||
|
|
||||
@ -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<string>} [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}`) |
|
||||
} |
|
||||
} |
|
||||
@ -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 } |
|
||||
@ -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 || {}, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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() |
|
||||
} |
|
||||
} |
|
||||
@ -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 } |
|
||||
@ -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 } |
|
||||
@ -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 |
|
||||
@ -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} |
||||
@ -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") |
div(hx-target="this" hx-swap="outerHTML") |
||||
//- img(id="ind" src="/img/bars.svg" class="htmx-indicator") |
div(class="relative") |
||||
div sda |
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} |
||||
@ -1,9 +1,45 @@ |
|||||
extends /layouts/empty.pug |
extends /layouts/empty.pug |
||||
|
|
||||
block pageHead |
block pageHead |
||||
|
//- style. |
||||
|
//- body { |
||||
|
//- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
//- min-height: 100vh; |
||||
|
//- } |
||||
|
|
||||
block pageContent |
block pageContent |
||||
form(hx-post="/login") |
.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/username.pug |
||||
input(type="password" name="password" placeholder="密码") |
include _ui/password.pug |
||||
button(type="submit") 登录 |
.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 || '请求失败') |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
Loading…
Reference in new issue