Browse Source

重构项目结构,删除不再使用的控制器和服务,优化路由和中间件

- 删除 AuthController 和 RouteCacheController 控制器,简化 API 结构
- 移除多余的服务文件,提升代码可维护性
- 更新路由配置,确保新控制器的注册和中间件的使用
- 在登录页面中引入新的用户名和密码验证组件,增强用户体验
- 更新样式文件,改善登录页面的视觉效果
pure
谢亚昕 2 months ago
parent
commit
f3a9a9b53e
  1. 3
      .vscode/settings.json
  2. 3
      jsconfig.json
  3. 2
      package.json
  4. 4
      src/base/BaseController.js
  5. 53
      src/config/index.js
  6. 25
      src/controllers/Api/AuthController.js
  7. 175
      src/controllers/Api/RouteCacheController.js
  8. 44
      src/controllers/Page/AuthController.js
  9. 140
      src/db/index.js
  10. 172
      src/db/models/BaseModel.js
  11. 35
      src/db/models/UserModel.js
  12. 3
      src/main.js
  13. 69
      src/middlewares/Auth/index.js
  14. 98
      src/middlewares/Controller/index.js
  15. 304
      src/middlewares/RoutePerformance/index.js
  16. 1
      src/middlewares/errorHandler/index.js
  17. 62
      src/middlewares/install.js
  18. 0
      src/modules/Api/controller/index.js
  19. 75
      src/modules/Auth/controller/index.js
  20. 22
      src/modules/Auth/services/index.js
  21. 0
      src/modules/Contact/controller/index.js
  22. 4
      src/modules/Contact/services/index.js
  23. 5
      src/modules/Index/controller/index.js
  24. 0
      src/modules/Index/services/index.js
  25. 12
      src/modules/Job/controller/index.js
  26. 2
      src/modules/Job/services/index.js
  27. 4
      src/modules/SiteConfig/services/index.js
  28. 563
      src/services/ArticleService.js
  29. 492
      src/services/BookmarkService.js
  30. 415
      src/services/UserService.js
  31. 84
      src/services/index.js
  32. 222
      src/utils/ForRegister.js
  33. 388
      src/utils/cache/RouteCache.js
  34. 19
      src/utils/error/ApiError.js
  35. 11
      src/utils/error/CommonError.js
  36. 75
      src/utils/router.js
  37. 26
      src/utils/router/RouteAuth.js
  38. 198
      src/utils/test/ConfigTest.js
  39. 222
      src/utils/test/RouteCacheTest.js
  40. 20
      src/utils/user.js
  41. 12
      src/views/page/login/_ui/password.pug
  42. 16
      src/views/page/login/_ui/username.pug
  43. 42
      src/views/page/login/index.pug
  44. 1
      vite.config.ts

3
.vscode/settings.json

@ -31,5 +31,6 @@
"debug.showBreakpointsInOverviewRuler": true, "debug.showBreakpointsInOverviewRuler": true,
"debug.showInlineBreakpointCandidates": true, "debug.showInlineBreakpointCandidates": true,
"bun.enable": true, "bun.enable": true,
"bun.path": "bun" "bun.path": "bun",
"CodeFree.index": true
} }

3
jsconfig.json

@ -13,9 +13,6 @@
], ],
"utils/*": [ "utils/*": [
"src/utils/*" "src/utils/*"
],
"services/*": [
"src/services/*"
] ]
}, },
"module": "ESNext", "module": "ESNext",

2
package.json

@ -13,9 +13,7 @@
"seed": "npx knex seed:run ", "seed": "npx knex seed:run ",
"dev:init": "bun run scripts/init.js", "dev:init": "bun run scripts/init.js",
"init": "cross-env NODE_ENV=production bun run scripts/init.js", "init": "cross-env NODE_ENV=production bun run scripts/init.js",
"test:env": "bun run scripts/test-env-validation.js",
"test": "bun test", "test": "bun test",
"test:db": "bun test tests/db",
"test:db:run": "bun run scripts/run-db-tests.js", "test:db:run": "bun run scripts/run-db-tests.js",
"test:db:benchmark": "bun run scripts/db-benchmark.js" "test:db:benchmark": "bun run scripts/db-benchmark.js"
}, },

4
src/base/BaseController.js

@ -220,8 +220,8 @@ class BaseController {
*/ */
async render(ctx, template, data = {}, options = {}) { async render(ctx, template, data = {}, options = {}) {
const defaultOptions = { const defaultOptions = {
includeSite: true, // includeSite: true,
includeUser: true, // includeUser: true,
...options ...options
} }

53
src/config/index.js

@ -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
}
} }

25
src/controllers/Api/AuthController.js

@ -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

175
src/controllers/Api/RouteCacheController.js

@ -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

44
src/controllers/Page/AuthController.js

@ -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
}
}

140
src/db/index.js

@ -5,30 +5,30 @@ import { logQuery, logQueryError } from "./monitor.js"
// 简单内存缓存(支持 TTL 与按前缀清理) // 简单内存缓存(支持 TTL 与按前缀清理)
const queryCache = new Map() const queryCache = new Map()
const crypto = await import('crypto') const crypto = await import("crypto")
const getNow = () => Date.now() const getNow = () => Date.now()
const computeExpiresAt = (ttlMs) => { const computeExpiresAt = ttlMs => {
if (!ttlMs || ttlMs <= 0) return null if (!ttlMs || ttlMs <= 0) return null
return getNow() + ttlMs return getNow() + ttlMs
} }
const isExpired = (entry) => { const isExpired = entry => {
if (!entry) return true if (!entry) return true
if (entry.expiresAt == null) return false if (entry.expiresAt == null) return false
return entry.expiresAt <= getNow() return entry.expiresAt <= getNow()
} }
const getCacheKeyForBuilder = (builder) => { const getCacheKeyForBuilder = builder => {
if (builder._customCacheKey) return String(builder._customCacheKey) if (builder._customCacheKey) return String(builder._customCacheKey)
// 改进的缓存键生成策略 // 改进的缓存键生成策略
const sql = builder.toString() const sql = builder.toString()
const tableName = builder._single?.table || 'unknown' const tableName = builder._single?.table || "unknown"
// 使用 MD5 生成简短的哈希值,避免键冲突 // 使用 MD5 生成简短的哈希值,避免键冲突
const hash = crypto.createHash('md5').update(sql).digest('hex') const hash = crypto.createHash("md5").update(sql).digest("hex")
// 缓存键格式:表名:哈希值:时间戳 // 缓存键格式:表名:哈希值:时间戳
const timestamp = Math.floor(Date.now() / 60000) // 每分钟更新一次时间戳 const timestamp = Math.floor(Date.now() / 60000) // 每分钟更新一次时间戳
@ -90,7 +90,7 @@ export const DbQueryCache = {
expired, expired,
totalSize, totalSize,
averageSize: valid > 0 ? Math.round(totalSize / valid) : 0, averageSize: valid > 0 ? Math.round(totalSize / valid) : 0,
hitRate: (hitCount + missCount) > 0 ? (hitCount / (hitCount + missCount)) : 0 hitRate: hitCount + missCount > 0 ? hitCount / (hitCount + missCount) : 0,
} }
}, },
// 缓存一致性管理 // 缓存一致性管理
@ -115,14 +115,14 @@ export const DbQueryCache = {
entryCount: stats.size, entryCount: stats.size,
totalMemoryBytes: stats.totalSize, totalMemoryBytes: stats.totalSize,
averageEntrySize: stats.averageSize, averageEntrySize: stats.averageSize,
estimatedMemoryMB: Math.round(stats.totalSize / 1024 / 1024 * 100) / 100 estimatedMemoryMB: Math.round((stats.totalSize / 1024 / 1024) * 100) / 100,
}
} }
},
} }
// QueryBuilder 扩展 // QueryBuilder 扩展
// 1) cache(ttlMs?): 读取缓存,不存在则执行并写入 // 1) cache(ttlMs?): 读取缓存,不存在则执行并写入
if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === 'function') { if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === "function") {
buildKnex.QueryBuilder.extend("cache", async function (ttlMs) { buildKnex.QueryBuilder.extend("cache", async function (ttlMs) {
const key = getCacheKeyForBuilder(this) const key = getCacheKeyForBuilder(this)
const entry = queryCache.get(key) const entry = queryCache.get(key)
@ -172,7 +172,7 @@ if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === 'function
}) })
// 7) 数据变更时自动清理相关缓存 // 7) 数据变更时自动清理相关缓存
buildKnex.QueryBuilder.extend("invalidateCache", function() { buildKnex.QueryBuilder.extend("invalidateCache", function () {
const tableName = this._single?.table const tableName = this._single?.table
if (tableName) { if (tableName) {
DbQueryCache.invalidateByTable(tableName) DbQueryCache.invalidateByTable(tableName)
@ -184,36 +184,38 @@ if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === 'function
// 8) 为 CUD 操作添加自动缓存失效 // 8) 为 CUD 操作添加自动缓存失效
// 使用更安全的方式扩展 QueryBuilder 方法 // 使用更安全的方式扩展 QueryBuilder 方法
const addCacheInvalidation = (methodName) => { const addCacheInvalidation = methodName => {
if (buildKnex.QueryBuilder && buildKnex.QueryBuilder.prototype && buildKnex.QueryBuilder.prototype[methodName]) { if (buildKnex.QueryBuilder && buildKnex.QueryBuilder.prototype && buildKnex.QueryBuilder.prototype[methodName]) {
const originalMethod = buildKnex.QueryBuilder.prototype[methodName]; const originalMethod = buildKnex.QueryBuilder.prototype[methodName]
buildKnex.QueryBuilder.prototype[methodName] = function(...args) { buildKnex.QueryBuilder.prototype[methodName] = function (...args) {
const result = originalMethod.apply(this, args); const result = originalMethod.apply(this, args)
const tableName = this._single?.table; const tableName = this._single?.table
if (tableName && result && typeof result.then === 'function') { if (tableName && result && typeof result.then === "function") {
// 在操作完成后清理缓存 // 在操作完成后清理缓存
const originalThen = result.then; const originalThen = result.then
result.then = function(...thenArgs) { result.then = function (...thenArgs) {
const promise = originalThen.apply(this, thenArgs); const promise = originalThen.apply(this, thenArgs)
promise.then(() => { promise
DbQueryCache.invalidateByTable(tableName); .then(() => {
}).catch(() => { DbQueryCache.invalidateByTable(tableName)
DbQueryCache.invalidateByTable(tableName); })
}); .catch(() => {
return promise; DbQueryCache.invalidateByTable(tableName)
}; })
return promise
}
} }
return result; return result
}; }
} }
}; }
// 安全地扩展 CUD 方法 // 安全地扩展 CUD 方法
addCacheInvalidation('insert'); addCacheInvalidation("insert")
addCacheInvalidation('update'); addCacheInvalidation("update")
addCacheInvalidation('del'); addCacheInvalidation("del")
const environment = process.env.NODE_ENV || "development" const environment = process.env.NODE_ENV || "development"
const db = buildKnex(knexConfig[environment]) const db = buildKnex(knexConfig[environment])
@ -226,7 +228,7 @@ const connectionStats = {
slowQueries: 0, slowQueries: 0,
errors: 0, errors: 0,
lastHealthCheck: null, lastHealthCheck: null,
uptime: Date.now() uptime: Date.now(),
} }
/** /**
@ -250,9 +252,9 @@ export const checkDatabaseHealth = async () => {
used: db.client.pool.numUsed(), used: db.client.pool.numUsed(),
free: db.client.pool.numFree(), free: db.client.pool.numFree(),
pending: db.client.pool.numPendingAcquires(), pending: db.client.pool.numPendingAcquires(),
pendingCreates: db.client.pool.numPendingCreates() pendingCreates: db.client.pool.numPendingCreates(),
}, },
stats: connectionStats stats: connectionStats,
} }
} catch (error) { } catch (error) {
connectionStats.errors++ connectionStats.errors++
@ -262,7 +264,7 @@ export const checkDatabaseHealth = async () => {
status: "unhealthy", status: "unhealthy",
error: error.message, error: error.message,
timestamp: new Date(), timestamp: new Date(),
stats: connectionStats stats: connectionStats,
} }
} }
} }
@ -280,8 +282,8 @@ export const getDatabaseStats = () => {
used: db.client.pool.numUsed(), used: db.client.pool.numUsed(),
free: db.client.pool.numFree(), free: db.client.pool.numFree(),
pending: db.client.pool.numPendingAcquires(), pending: db.client.pool.numPendingAcquires(),
pendingCreates: db.client.pool.numPendingCreates() pendingCreates: db.client.pool.numPendingCreates(),
} },
} }
} }
@ -309,8 +311,7 @@ export const isDatabaseConnected = async () => {
} }
} }
// 数据库事件监听 function listenQuery(queryData) {
db.on('query', (queryData) => {
connectionStats.totalQueries++ connectionStats.totalQueries++
// 记录查询统计 // 记录查询统计
@ -323,45 +324,66 @@ db.on('query', (queryData) => {
logger.warn("检测到慢查询:", { logger.warn("检测到慢查询:", {
sql: queryData.sql, sql: queryData.sql,
duration: duration, duration: duration,
bindings: queryData.bindings bindings: queryData.bindings,
}) })
} }
}) }
function listenQueryError(error, queryData) {
db.on('query-error', (error, queryData) => {
connectionStats.errors++ connectionStats.errors++
logQueryError(error, queryData?.sql, queryData?.bindings) logQueryError(error, queryData?.sql, queryData?.bindings)
}) }
function listenError(error) {
db.on('error', (error) => {
connectionStats.errors++ connectionStats.errors++
logger.error("数据库错误:", error) logger.error("数据库错误:", error)
}) }
let checkInterval = null
let cleanupInterval = null
let memoryUsageInterval = null
export function dispose() {
db.off("query", listenQuery)
db.off("query-error", listenQueryError)
db.off("error", listenError)
clearInterval(checkInterval)
clearInterval(cleanupInterval)
clearInterval(memoryUsageInterval)
}
export function bootstrapDatabase() {
// 启动前清理之前的事件监听
dispose()
// 数据库事件监听
db.on("query", listenQuery)
db.on("query-error", listenQueryError)
// 定时健康检查(每5分钟) db.on("error", listenError)
setInterval(async () => {
// 定时健康检查(每5分钟)
checkInterval = setInterval(async () => {
const health = await checkDatabaseHealth() const health = await checkDatabaseHealth()
if (health.status === "unhealthy") { if (health.status === "unhealthy") {
logger.error("数据库健康检查失败:", health) logger.error("数据库健康检查失败:", health)
} }
}, 5 * 60 * 1000) }, 5 * 60 * 1000)
// 定时清理过期缓存(每2分钟) // 定时清理过期缓存(每2分钟)
setInterval(() => { cleanupInterval = setInterval(() => {
const cleaned = DbQueryCache.cleanup() const cleaned = DbQueryCache.cleanup()
if (cleaned > 0) { if (cleaned > 0) {
logger.debug(`清理了 ${cleaned} 个过期缓存项`) logger.debug(`清理了 ${cleaned} 个过期缓存项`)
} }
}, 2 * 60 * 1000) }, 2 * 60 * 1000)
// 内存使用监控(每10分钟) // 内存使用监控(每10分钟)
setInterval(() => { memoryUsageInterval = setInterval(() => {
const memoryUsage = DbQueryCache.getMemoryUsage() const memoryUsage = DbQueryCache.getMemoryUsage()
if (memoryUsage.estimatedMemoryMB > 50) { // 如果缓存超过50MB if (memoryUsage.estimatedMemoryMB > 50) {
// 如果缓存超过50MB
logger.warn("缓存内存使用过高:", memoryUsage) logger.warn("缓存内存使用过高:", memoryUsage)
// 可以在这里实现更激进的清理策略 // 可以在这里实现更激进的清理策略
} }
}, 10 * 60 * 1000) }, 10 * 60 * 1000)
}
export default db export default db

172
src/db/models/BaseModel.js

@ -1,5 +1,6 @@
import db from "../index.js" import db from "../index.js"
import { logger } from "../../logger.js" import { logger } from "../../logger.js"
import { BaseSingleton } from "@/utils/BaseSingleton"
/** /**
* 数据库错误类 * 数据库错误类
@ -39,46 +40,58 @@ export const handleDatabaseError = (error, operation = "数据库操作") => {
* 统一的数据库基础模型类 * 统一的数据库基础模型类
* 提供标准化的CRUD操作和错误处理 * 提供标准化的CRUD操作和错误处理
*/ */
export default class BaseModel { export default class BaseModel extends BaseSingleton {
/**
* @returns {BaseModel}
*/
static getInstance() {
return super.getInstance()
}
constructor() {
super()
}
/** /**
* 获取表名必须由子类实现 * 获取表名必须由子类实现
*/ */
static get tableName() { get tableName() {
throw new Error("tableName must be defined in subclass") throw new Error("tableName must be defined in subclass")
} }
/** /**
* 获取默认排序字段 * 获取默认排序字段
*/ */
static get defaultOrderBy() { get defaultOrderBy() {
return "id" return "id"
} }
/** /**
* 获取默认排序方向 * 获取默认排序方向
*/ */
static get defaultOrder() { get defaultOrder() {
return "desc" return "desc"
} }
/** /**
* 获取可搜索字段列表 * 获取可搜索字段列表
*/ */
static get searchableFields() { get searchableFields() {
return [] return []
} }
/** /**
* 获取可过滤字段列表 * 获取可过滤字段列表
*/ */
static get filterableFields() { get filterableFields() {
return [] return []
} }
/** /**
* 根据ID查找单条记录 * 根据ID查找单条记录
*/ */
static async findById(id) { async findById(id) {
try { try {
const result = await db(this.tableName).where("id", id).first() const result = await db(this.tableName).where("id", id).first()
return result || null return result || null
@ -90,16 +103,9 @@ export default class BaseModel {
/** /**
* 查找所有记录支持分页和排序 * 查找所有记录支持分页和排序
*/ */
static async findAll(options = {}) { async findAll(options = {}) {
try { try {
const { const { page = 1, limit = 10, orderBy = this.defaultOrderBy, order = this.defaultOrder, where = {}, select = "*" } = options
page = 1,
limit = 10,
orderBy = this.defaultOrderBy,
order = this.defaultOrder,
where = {},
select = "*"
} = options
const offset = (page - 1) * limit const offset = (page - 1) * limit
@ -122,9 +128,9 @@ export default class BaseModel {
/** /**
* 查找第一条记录 * 查找第一条记录
*/ */
static async findFirst(conditions = {}) { async findFirst(conditions = {}) {
try { try {
return await db(this.tableName).where(conditions).first() || null return (await db(this.tableName).where(conditions).first()) || null
} catch (error) { } catch (error) {
throw handleDatabaseError(error, `查找${this.tableName}第一条记录`) throw handleDatabaseError(error, `查找${this.tableName}第一条记录`)
} }
@ -133,14 +139,9 @@ export default class BaseModel {
/** /**
* 根据条件查找记录 * 根据条件查找记录
*/ */
static async findWhere(conditions, options = {}) { async findWhere(conditions, options = {}) {
try { try {
const { const { orderBy = this.defaultOrderBy, order = this.defaultOrder, limit, select = "*" } = options
orderBy = this.defaultOrderBy,
order = this.defaultOrder,
limit,
select = "*"
} = options
let query = db(this.tableName).select(select).where(conditions) let query = db(this.tableName).select(select).where(conditions)
@ -161,7 +162,7 @@ export default class BaseModel {
/** /**
* 创建新记录 * 创建新记录
*/ */
static async create(data) { async create(data) {
try { try {
const insertData = { const insertData = {
...data, ...data,
@ -169,9 +170,7 @@ export default class BaseModel {
updated_at: db.fn.now(), updated_at: db.fn.now(),
} }
const result = await db(this.tableName) const result = await db(this.tableName).insert(insertData).returning("*")
.insert(insertData)
.returning("*")
// SQLite returning() 总是返回数组,这里统一返回第一个元素 // SQLite returning() 总是返回数组,这里统一返回第一个元素
return Array.isArray(result) ? result[0] : result return Array.isArray(result) ? result[0] : result
@ -183,17 +182,14 @@ export default class BaseModel {
/** /**
* 更新记录 * 更新记录
*/ */
static async update(id, data) { async update(id, data) {
try { try {
const updateData = { const updateData = {
...data, ...data,
updated_at: db.fn.now(), updated_at: db.fn.now(),
} }
const result = await db(this.tableName) const result = await db(this.tableName).where("id", id).update(updateData).returning("*")
.where("id", id)
.update(updateData)
.returning("*")
// SQLite returning() 总是返回数组,这里统一返回第一个元素 // SQLite returning() 总是返回数组,这里统一返回第一个元素
return Array.isArray(result) ? result[0] : result return Array.isArray(result) ? result[0] : result
@ -205,16 +201,14 @@ export default class BaseModel {
/** /**
* 根据条件更新记录 * 根据条件更新记录
*/ */
static async updateWhere(conditions, data) { async updateWhere(conditions, data) {
try { try {
const updateData = { const updateData = {
...data, ...data,
updated_at: db.fn.now(), updated_at: db.fn.now(),
} }
return await db(this.tableName) return await db(this.tableName).where(conditions).update(updateData)
.where(conditions)
.update(updateData)
} catch (error) { } catch (error) {
throw handleDatabaseError(error, `按条件更新${this.tableName}记录`) throw handleDatabaseError(error, `按条件更新${this.tableName}记录`)
} }
@ -223,7 +217,7 @@ export default class BaseModel {
/** /**
* 删除记录 * 删除记录
*/ */
static async delete(id) { async delete(id) {
try { try {
return await db(this.tableName).where("id", id).del() return await db(this.tableName).where("id", id).del()
} catch (error) { } catch (error) {
@ -234,7 +228,7 @@ export default class BaseModel {
/** /**
* 根据条件删除记录 * 根据条件删除记录
*/ */
static async deleteWhere(conditions) { async deleteWhere(conditions) {
try { try {
return await db(this.tableName).where(conditions).del() return await db(this.tableName).where(conditions).del()
} catch (error) { } catch (error) {
@ -245,12 +239,9 @@ export default class BaseModel {
/** /**
* 统计记录数量 * 统计记录数量
*/ */
static async count(conditions = {}) { async count(conditions = {}) {
try { try {
const result = await db(this.tableName) const result = await db(this.tableName).where(conditions).count("id as count").first()
.where(conditions)
.count("id as count")
.first()
return parseInt(result.count) || 0 return parseInt(result.count) || 0
} catch (error) { } catch (error) {
throw handleDatabaseError(error, `统计${this.tableName}记录数量`) throw handleDatabaseError(error, `统计${this.tableName}记录数量`)
@ -260,7 +251,7 @@ export default class BaseModel {
/** /**
* 检查记录是否存在 * 检查记录是否存在
*/ */
static async exists(conditions) { async exists(conditions) {
try { try {
const count = await this.count(conditions) const count = await this.count(conditions)
return count > 0 return count > 0
@ -272,7 +263,7 @@ export default class BaseModel {
/** /**
* 分页查询 * 分页查询
*/ */
static async paginate(options = {}) { async paginate(options = {}) {
try { try {
const { const {
page = 1, page = 1,
@ -282,7 +273,7 @@ export default class BaseModel {
where = {}, where = {},
select = "*", select = "*",
search = "", search = "",
searchFields = this.searchableFields searchFields = this.searchableFields,
} = options } = options
let query = db(this.tableName).select(select) let query = db(this.tableName).select(select)
@ -294,7 +285,7 @@ export default class BaseModel {
// 添加搜索条件 // 添加搜索条件
if (search && searchFields.length > 0) { if (search && searchFields.length > 0) {
query = query.where(function() { query = query.where(function () {
searchFields.forEach((field, index) => { searchFields.forEach((field, index) => {
if (index === 0) { if (index === 0) {
this.where(field, "like", `%${search}%`) this.where(field, "like", `%${search}%`)
@ -312,10 +303,7 @@ export default class BaseModel {
// 分页查询 // 分页查询
const offset = (page - 1) * limit const offset = (page - 1) * limit
const data = await query const data = await query.orderBy(orderBy, order).limit(limit).offset(offset)
.orderBy(orderBy, order)
.limit(limit)
.offset(offset)
return { return {
data, data,
@ -325,8 +313,8 @@ export default class BaseModel {
total, total,
totalPages: Math.ceil(total / limit), totalPages: Math.ceil(total / limit),
hasNext: page * limit < total, hasNext: page * limit < total,
hasPrev: page > 1 hasPrev: page > 1,
} },
} }
} catch (error) { } catch (error) {
throw handleDatabaseError(error, `分页查询${this.tableName}记录`) throw handleDatabaseError(error, `分页查询${this.tableName}记录`)
@ -336,7 +324,7 @@ export default class BaseModel {
/** /**
* 批量创建记录 * 批量创建记录
*/ */
static async createMany(dataArray, batchSize = 100) { async createMany(dataArray, batchSize = 100) {
try { try {
const results = [] const results = []
@ -347,9 +335,7 @@ export default class BaseModel {
updated_at: db.fn.now(), updated_at: db.fn.now(),
})) }))
const batchResults = await db(this.tableName) const batchResults = await db(this.tableName).insert(batch).returning("*")
.insert(batch)
.returning("*")
results.push(...(Array.isArray(batchResults) ? batchResults : [batchResults])) results.push(...(Array.isArray(batchResults) ? batchResults : [batchResults]))
} }
@ -363,16 +349,14 @@ export default class BaseModel {
/** /**
* 批量更新记录 * 批量更新记录
*/ */
static async updateMany(conditions, data) { async updateMany(conditions, data) {
try { try {
const updateData = { const updateData = {
...data, ...data,
updated_at: db.fn.now(), updated_at: db.fn.now(),
} }
return await db(this.tableName) return await db(this.tableName).where(conditions).update(updateData)
.where(conditions)
.update(updateData)
} catch (error) { } catch (error) {
throw handleDatabaseError(error, `批量更新${this.tableName}记录`) throw handleDatabaseError(error, `批量更新${this.tableName}记录`)
} }
@ -381,7 +365,7 @@ export default class BaseModel {
/** /**
* 获取表结构信息 * 获取表结构信息
*/ */
static async getTableInfo() { async getTableInfo() {
try { try {
return await db.raw(`PRAGMA table_info(${this.tableName})`) return await db.raw(`PRAGMA table_info(${this.tableName})`)
} catch (error) { } catch (error) {
@ -392,7 +376,7 @@ export default class BaseModel {
/** /**
* 清空表数据 * 清空表数据
*/ */
static async truncate() { async truncate() {
try { try {
return await db(this.tableName).del() return await db(this.tableName).del()
} catch (error) { } catch (error) {
@ -403,11 +387,9 @@ export default class BaseModel {
/** /**
* 获取随机记录 * 获取随机记录
*/ */
static async findRandom(limit = 1) { async findRandom(limit = 1) {
try { try {
return await db(this.tableName) return await db(this.tableName).orderByRaw("RANDOM()").limit(limit)
.orderByRaw("RANDOM()")
.limit(limit)
} catch (error) { } catch (error) {
throw handleDatabaseError(error, `获取${this.tableName}随机记录`) throw handleDatabaseError(error, `获取${this.tableName}随机记录`)
} }
@ -416,41 +398,41 @@ export default class BaseModel {
/** /**
* 关联查询基础方法 - 左连接 * 关联查询基础方法 - 左连接
*/ */
static leftJoin(joinTable, leftKey, rightKey) { leftJoin(joinTable, leftKey, rightKey) {
return db(this.tableName).leftJoin(joinTable, leftKey, rightKey) return db(this.tableName).leftJoin(joinTable, leftKey, rightKey)
} }
/** /**
* 关联查询基础方法 - 内连接 * 关联查询基础方法 - 内连接
*/ */
static innerJoin(joinTable, leftKey, rightKey) { innerJoin(joinTable, leftKey, rightKey) {
return db(this.tableName).innerJoin(joinTable, leftKey, rightKey) return db(this.tableName).innerJoin(joinTable, leftKey, rightKey)
} }
/** /**
* 关联查询基础方法 - 右连接 * 关联查询基础方法 - 右连接
*/ */
static rightJoin(joinTable, leftKey, rightKey) { rightJoin(joinTable, leftKey, rightKey) {
return db(this.tableName).rightJoin(joinTable, leftKey, rightKey) return db(this.tableName).rightJoin(joinTable, leftKey, rightKey)
} }
/** /**
* 构建复杂关联查询 * 构建复杂关联查询
*/ */
static buildRelationQuery(relations = []) { buildRelationQuery(relations = []) {
let query = db(this.tableName) let query = db(this.tableName)
relations.forEach(relation => { relations.forEach(relation => {
const { type, table, on, select } = relation const { type, table, on, select } = relation
switch (type) { switch (type) {
case 'left': case "left":
query = query.leftJoin(table, on[0], on[1]) query = query.leftJoin(table, on[0], on[1])
break break
case 'inner': case "inner":
query = query.innerJoin(table, on[0], on[1]) query = query.innerJoin(table, on[0], on[1])
break break
case 'right': case "right":
query = query.rightJoin(table, on[0], on[1]) query = query.rightJoin(table, on[0], on[1])
break break
} }
@ -466,14 +448,9 @@ export default class BaseModel {
/** /**
* 通用关联查询方法 * 通用关联查询方法
*/ */
static async findWithRelations(conditions = {}, relations = [], options = {}) { async findWithRelations(conditions = {}, relations = [], options = {}) {
try { try {
const { const { orderBy = this.defaultOrderBy, order = this.defaultOrder, limit, select = [`${this.tableName}.*`] } = options
orderBy = this.defaultOrderBy,
order = this.defaultOrder,
limit,
select = [`${this.tableName}.*`]
} = options
let query = this.buildRelationQuery(relations) let query = this.buildRelationQuery(relations)
@ -504,7 +481,7 @@ export default class BaseModel {
/** /**
* 在事务中创建记录 * 在事务中创建记录
*/ */
static async createInTransaction(trx, data) { async createInTransaction(trx, data) {
try { try {
const insertData = { const insertData = {
...data, ...data,
@ -512,9 +489,7 @@ export default class BaseModel {
updated_at: trx.fn.now(), updated_at: trx.fn.now(),
} }
const result = await trx(this.tableName) const result = await trx(this.tableName).insert(insertData).returning("*")
.insert(insertData)
.returning("*")
return Array.isArray(result) ? result[0] : result return Array.isArray(result) ? result[0] : result
} catch (error) { } catch (error) {
@ -525,17 +500,14 @@ export default class BaseModel {
/** /**
* 在事务中更新记录 * 在事务中更新记录
*/ */
static async updateInTransaction(trx, id, data) { async updateInTransaction(trx, id, data) {
try { try {
const updateData = { const updateData = {
...data, ...data,
updated_at: trx.fn.now(), updated_at: trx.fn.now(),
} }
const result = await trx(this.tableName) const result = await trx(this.tableName).where("id", id).update(updateData).returning("*")
.where("id", id)
.update(updateData)
.returning("*")
return Array.isArray(result) ? result[0] : result return Array.isArray(result) ? result[0] : result
} catch (error) { } catch (error) {
@ -546,7 +518,7 @@ export default class BaseModel {
/** /**
* 在事务中删除记录 * 在事务中删除记录
*/ */
static async deleteInTransaction(trx, id) { async deleteInTransaction(trx, id) {
try { try {
return await trx(this.tableName).where("id", id).del() return await trx(this.tableName).where("id", id).del()
} catch (error) { } catch (error) {
@ -557,7 +529,7 @@ export default class BaseModel {
/** /**
* 在事务中批量创建记录 * 在事务中批量创建记录
*/ */
static async createManyInTransaction(trx, dataArray, batchSize = 100) { async createManyInTransaction(trx, dataArray, batchSize = 100) {
try { try {
const results = [] const results = []
@ -568,9 +540,7 @@ export default class BaseModel {
updated_at: trx.fn.now(), updated_at: trx.fn.now(),
})) }))
const batchResults = await trx(this.tableName) const batchResults = await trx(this.tableName).insert(batch).returning("*")
.insert(batch)
.returning("*")
results.push(...(Array.isArray(batchResults) ? batchResults : [batchResults])) results.push(...(Array.isArray(batchResults) ? batchResults : [batchResults]))
} }
@ -584,16 +554,14 @@ export default class BaseModel {
/** /**
* 在事务中批量更新记录 * 在事务中批量更新记录
*/ */
static async updateManyInTransaction(trx, conditions, data) { async updateManyInTransaction(trx, conditions, data) {
try { try {
const updateData = { const updateData = {
...data, ...data,
updated_at: trx.fn.now(), updated_at: trx.fn.now(),
} }
return await trx(this.tableName) return await trx(this.tableName).where(conditions).update(updateData)
.where(conditions)
.update(updateData)
} catch (error) { } catch (error) {
throw handleDatabaseError(error, `在事务中批量更新${this.tableName}记录`) throw handleDatabaseError(error, `在事务中批量更新${this.tableName}记录`)
} }
@ -602,7 +570,7 @@ export default class BaseModel {
/** /**
* 在事务中执行原生 SQL * 在事务中执行原生 SQL
*/ */
static async rawInTransaction(trx, query, bindings = []) { async rawInTransaction(trx, query, bindings = []) {
try { try {
return await trx.raw(query, bindings) return await trx.raw(query, bindings)
} catch (error) { } catch (error) {

35
src/db/models/UserModel.js

@ -1,29 +1,40 @@
import BaseModel from "./BaseModel.js" import BaseModel from "./BaseModel.js"
class UserModel extends BaseModel { class UserModel extends BaseModel {
static get tableName() { /**
* @returns {UserModel}
*/
static getInstance() {
return super.getInstance()
}
constructor() {
super()
}
get tableName() {
return "users" return "users"
} }
static get searchableFields() { get searchableFields() {
return ["username", "email", "name"] return ["username", "email", "name"]
} }
static get filterableFields() { get filterableFields() {
return ["role", "status"] return ["role", "status"]
} }
// 特定业务方法 // 特定业务方法
static async findByUsername(username) { async findByUsername(username) {
return this.findFirst({ username }) return this.findFirst({ username })
} }
static async findByEmail(email) { async findByEmail(email) {
return this.findFirst({ email }) return this.findFirst({ email })
} }
// 重写create方法添加验证 // 重写create方法添加验证
static async create(data) { async create(data) {
// 验证唯一性 // 验证唯一性
if (data.username) { if (data.username) {
const existingUser = await this.findByUsername(data.username) const existingUser = await this.findByUsername(data.username)
@ -43,7 +54,7 @@ class UserModel extends BaseModel {
} }
// 重写update方法添加验证 // 重写update方法添加验证
static async update(id, data) { async update(id, data) {
// 验证唯一性(排除当前用户) // 验证唯一性(排除当前用户)
if (data.username) { if (data.username) {
const existingUser = await this.findFirst({ username: data.username }) const existingUser = await this.findFirst({ username: data.username })
@ -63,21 +74,21 @@ class UserModel extends BaseModel {
} }
// 用户状态管理 // 用户状态管理
static async activate(id) { async activate(id) {
return this.update(id, { status: "active" }) return this.update(id, { status: "active" })
} }
static async deactivate(id) { async deactivate(id) {
return this.update(id, { status: "inactive" }) return this.update(id, { status: "inactive" })
} }
// 按角色查找用户 // 按角色查找用户
static async findByRole(role) { async findByRole(role) {
return this.findWhere({ role }) return this.findWhere({ role })
} }
// 获取用户统计 // 获取用户统计
static async getUserStats() { async getUserStats() {
const total = await this.count() const total = await this.count()
const active = await this.count({ status: "active" }) const active = await this.count({ status: "active" })
const inactive = await this.count({ status: "inactive" }) const inactive = await this.count({ status: "inactive" })
@ -85,7 +96,7 @@ class UserModel extends BaseModel {
return { return {
total, total,
active, active,
inactive inactive,
} }
} }
} }

3
src/main.js

@ -1,4 +1,5 @@
import { app } from "./global" import { app } from "./global"
import { bootstrapDatabase } from "@/db/index.js"
// 日志、全局插件、定时任务等基础设施 // 日志、全局插件、定时任务等基础设施
import { logger } from "./logger.js" import { logger } from "./logger.js"
import "./jobs/index.js" import "./jobs/index.js"
@ -16,6 +17,8 @@ const PORT = process.env.PORT || 3001;
// 注册插件 // 注册插件
await LoadMiddlewares(app) await LoadMiddlewares(app)
bootstrapDatabase()
const server = app.listen(PORT, () => { const server = app.listen(PORT, () => {
const port = server.address().port const port = server.address().port
// 获取本地 IP // 获取本地 IP

69
src/middlewares/Auth/index.js

@ -7,10 +7,10 @@ export const JWT_SECRET = process.env.JWT_SECRET
function matchList(list, path) { function matchList(list, path) {
for (const item of list) { for (const item of list) {
if (typeof item === "string" && minimatch(path, item)) { if (typeof item === "string" && minimatch(path, item)) {
return { matched: true, auth: false } return { matched: true }
} }
if (typeof item === "object" && minimatch(path, item.pattern)) { if (typeof item === "object" && minimatch(path, item.pattern)) {
return { matched: true, auth: item.auth } return { matched: true }
} }
} }
return { matched: false } return { matched: false }
@ -30,45 +30,36 @@ export function AuthMiddleware(options = {
} }
// 白名单处理 // 白名单处理
const white = matchList(options.whiteList, ctx.path) const white = matchList(options.whiteList, ctx.path)
if (white.matched) { if (!white.matched) {
if (white.auth === false) { throw new CommonError("禁止访问", CommonError.ERR_CODE.FORBIDDEN)
ctx.authType = false
} else if (white.auth === "try") {
ctx.authType = "try"
} else {
ctx.authType = true
}
} else {
// 默认需要登录
ctx.authType = true
} }
return next() return next()
} }
} }
export function VerifyUserMiddleware() { // export function VerifyUserMiddleware() {
return (ctx, next) => { // return (ctx, next) => {
if (ctx.session.user) { // if (ctx.session.user) {
ctx.state.user = ctx.session.user // ctx.state.user = ctx.session.user
} else { // } else {
const authorizationString = ctx.headers["authorization"] // const authorizationString = ctx.headers["authorization"]
if (authorizationString) { // if (authorizationString) {
const token = authorizationString.replace(/^Bearer\s/, "") // const token = authorizationString.replace(/^Bearer\s/, "")
ctx.state.user = jwt.verify(token, process.env.JWT_SECRET) // ctx.state.user = jwt.verify(token, process.env.JWT_SECRET)
} // }
} // }
if (ctx.authType === false) { // if (ctx.authType === false) {
if (ctx.state.user) { // if (ctx.state.user) {
throw new CommonError("该接口不能登录查看") // throw new CommonError("该接口不能登录查看")
} // }
return next() // return next()
} // }
if (ctx.authType === "try") { // if (ctx.authType === "try") {
return next() // return next()
} // }
if (!ctx.state.user && ctx.authType === true) { // if (!ctx.state.user && ctx.authType === true) {
throw new CommonError("请登录") // throw new CommonError("请登录")
} // }
return next() // return next()
} // }
} // }

98
src/middlewares/Controller/index.js

@ -0,0 +1,98 @@
import fs from "fs"
import path from "path"
import { logger } from "@/logger.js"
import compose from "koa-compose"
async function scanControllers(rootDir) {
const routers = []
const stack = [rootDir]
while (stack.length) {
const dir = stack.pop()
let files
try {
files = fs.readdirSync(dir)
} catch (error) {
logger.error(`[控制器注册] ❌ 读取目录失败 ${dir}: ${error.message}`)
continue
}
for (const file of files) {
if (file.startsWith("_")) continue
const fullPath = path.join(dir, file)
let stat
try {
stat = fs.statSync(fullPath)
} catch (error) {
logger.error(`[控制器注册] ❌ 读取文件信息失败 ${fullPath}: ${error.message}`)
continue
}
if (stat.isDirectory()) {
stack.push(fullPath)
continue
}
if (!fullPath.replace(/\\/g, "/").includes("/controller/")) continue
let fileName = fullPath.replace(rootDir+path.sep, "")
try {
const controllerModule = await import(fullPath)
const controller = controllerModule.default || controllerModule
if (!controller) {
logger.warn(`[控制器注册] ${fileName} - 缺少默认导出,跳过注册`)
continue
}
let routesFactory = controller.createRoutes || controller.default?.createRoutes || controller.default || controller
if(typeof routesFactory === "function") {
routesFactory = routesFactory.bind(controller)
}
if (typeof routesFactory !== "function") {
logger.warn(`[控制器注册] ⚠️ ${fileName} - 未找到 createRoutes 方法或导出对象`)
continue
}
let routerResult
try {
routerResult = routesFactory()
} catch (error) {
logger.error(`[控制器注册] ❌ ${fileName} - createRoutes() 执行失败: ${error.message}`)
continue
}
const list = Array.isArray(routerResult) ? routerResult : [routerResult]
let added = 0
for (const r of list) {
if (r && typeof r.middleware === "function") {
routers.push(r)
added++
} else {
logger.warn(`[控制器注册] ⚠️ ${fileName} - createRoutes() 返回的部分路由器对象无效`)
}
}
if (added > 0) logger.debug(`[控制器注册] ✅ ${fileName} - 创建成功 (${added})`)
} catch (importError) {
logger.error(`[控制器注册] ❌ ${fileName} - 模块导入失败: ${importError.message}`)
logger.error(importError)
}
}
}
return routers
}
export default async function (options = {}) {
const { root, handleBeforeEachRequest } = options
if (!root) {
throw new Error("controller root is required")
}
const routers = await scanControllers(root)
const allRouters = []
for (let i = 0; i < routers.length; i++) {
const router = routers[i]
allRouters.push(router.middleware((options = {}) => handleBeforeEachRequest(options)))
}
return async function (ctx, next) {
return await compose(allRouters)(ctx, next)
}
}

304
src/middlewares/RoutePerformance/index.js

@ -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 }

1
src/middlewares/errorHandler/index.js

@ -90,7 +90,6 @@ export default function () {
// 确保状态码在合理范围内 // 确保状态码在合理范围内
status = status >= 100 && status < 600 ? status : 500 status = status >= 100 && status < 600 ? status : 500
await formatError( await formatError(
ctx, ctx,
status, status,

62
src/middlewares/install.js

@ -4,21 +4,22 @@ import { resolve } from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import path from "path" import path from "path"
import ErrorHandler from "./ErrorHandler" import ErrorHandler from "./ErrorHandler"
import { VerifyUserMiddleware, AuthMiddleware } from "./Auth" import { AuthMiddleware } from "./Auth"
import bodyParser from "koa-bodyparser" import bodyParser from "koa-bodyparser"
import Views from "./Views" import Views from "./Views"
import Session from "./Session" import Session from "./Session"
import etag from "@koa/etag" import etag from "@koa/etag"
import conditional from "koa-conditional-get" import conditional from "koa-conditional-get"
import { autoRegisterControllers } from "@/utils/ForRegister.js" import Controller from "./Controller/index.js"
import performanceMonitor from "./RoutePerformance/index.js"
import app from "@/global" import app from "@/global"
import fs from "fs" import fs from "fs"
import helmet from "koa-helmet" import helmet from "koa-helmet"
import ratelimit from "koa-ratelimit" import ratelimit from "koa-ratelimit"
import { render } from "./PugHelper/sass.js" import { render } from "./PugHelper/sass.js"
import AuthError from "@/utils/error/AuthError.js"
import CommonError from "@/utils/error/CommonError.js"
import { SiteConfigService } from "services/SiteConfigService.js" import { SiteConfigService } from "@/modules/SiteConfig/services/index.js"
import config from "config/index.js" import config from "config/index.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
@ -40,6 +41,7 @@ export default async app => {
} }
return next() return next()
}) })
// 跨域设置
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", "*") ctx.set("Access-Control-Allow-Origin", "*")
ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS") ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
@ -51,6 +53,7 @@ export default async app => {
return await next() return await next()
}) })
// 安全设置
app.use( app.use(
helmet({ helmet({
contentSecurityPolicy: { contentSecurityPolicy: {
@ -61,7 +64,7 @@ export default async app => {
}) })
) )
// apply rate limit // 应用限流
const db = new Map() const db = new Map()
app.use( app.use(
ratelimit({ ratelimit({
@ -85,17 +88,14 @@ export default async app => {
}, },
}) })
) )
app.use(async (ctx, next) => {
// 提供全局数据 // 提供全局数据
app.use(async (ctx, next) => {
ctx.state.siteConfig = await SiteConfigService.getAll() ctx.state.siteConfig = await SiteConfigService.getAll()
ctx.state.$config = config ctx.state.$config = config
return await next() return await next()
}) })
// 错误处理,主要处理运行中抛出的错误 // 错误处理,主要处理运行中抛出的错误
app.use(ErrorHandler()) app.use(ErrorHandler())
// 路由性能监控(在路由处理之前)
app.use(performanceMonitor.middleware())
// session设置 // session设置
app.use(Session(app)) app.use(Session(app))
// 视图设置 // 视图设置
@ -105,6 +105,7 @@ export default async app => {
options: { options: {
basedir: resolve(__dirname, "../views"), basedir: resolve(__dirname, "../views"),
filters: { filters: {
// 处理scss
scss: function (text, options) { scss: function (text, options) {
//- process.env.SASS_PATH = "D:/@code/demo/koa3-demo/src/views/page/index" //- process.env.SASS_PATH = "D:/@code/demo/koa3-demo/src/views/page/index"
const root = path.resolve(__dirname, "../views") const root = path.resolve(__dirname, "../views")
@ -126,8 +127,8 @@ export default async app => {
AuthMiddleware({ AuthMiddleware({
whiteList: [ whiteList: [
// 所有请求放行 // 所有请求放行
{ pattern: "/", auth: "try" }, { pattern: "/" },
{ pattern: "/**/*", auth: "try" }, { pattern: "/**/*" },
], ],
blackList: [ blackList: [
// 禁用api请求 // 禁用api请求
@ -139,11 +140,44 @@ export default async app => {
) )
// 验证用户 // 验证用户
// 注入全局变量:ctx.state.user // 注入全局变量:ctx.state.user
app.use(VerifyUserMiddleware()) // app.use(VerifyUserMiddleware())
// 请求体解析 // 请求体解析
app.use(bodyParser()) app.use(bodyParser())
// 自动注册控制器 app.use(
await autoRegisterControllers(app, path.resolve(__dirname, "../controllers")) await Controller({
root: path.resolve(__dirname, "../modules"),
handleBeforeEachRequest: options => {
const { auth = true } = options || {}
return async (ctx, next) => {
if (ctx.session && ctx.session.user) {
ctx.state.user = ctx.session.user
} else {
const authorizationString = ctx.headers && ctx.headers["authorization"]
if (authorizationString) {
const token = authorizationString.replace(/^Bearer\s/, "")
try {
ctx.state.user = jwt.verify(token, process.env.JWT_SECRET)
} catch (_) {
// 无效token忽略
}
}
}
if (auth === false && ctx.state.user) {
throw new CommonError("不能登录查看")
}
if (auth === "try") {
return next()
}
if (auth === true && !ctx.state.user) {
throw new AuthError("需要登录才能访问")
}
return await next()
}
},
})
)
// 注册完成之后静态资源设置 // 注册完成之后静态资源设置
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
if (ctx.body) return await next() if (ctx.body) return await next()

0
src/controllers/Api/ApiController.js → src/modules/Api/controller/index.js

75
src/modules/Auth/controller/index.js

@ -0,0 +1,75 @@
import Router from "utils/router.js"
import { logger } from "@/logger.js"
import BaseController from "@/base/BaseController.js"
import AuthService from "../services"
export default class AuthController extends BaseController {
/**
* 创建基础页面相关路由
* @returns {Router} 路由实例
*/
static createRoutes() {
const controller = new this()
const router = new Router({ auth: false })
router.get("/login", controller.handleRequest(controller.loginGet))
router.post("/login", controller.handleRequest(controller.loginPost))
router.post("/login/validate/username", controller.handleRequest(controller.validateUsername))
router.post("/login/validate/password", controller.handleRequest(controller.validatePassword))
router.post("/logout", controller.handleRequest(controller.logout), { auth: true })
return router
}
constructor() {
super()
}
// 首页
async loginGet(ctx) {
return this.render(ctx, "page/login/index", {})
}
async loginPost(ctx) {
const res = await AuthService.login(ctx.request.body)
ctx.session.user = res.user
ctx.set("HX-Redirect", "/")
}
async validateUsername(ctx) {
const { username } = ctx.request.body
const uiPath = "page/login/_ui/username"
if (username === "") {
return this.render(ctx, uiPath, {
value: username,
error: "用户名不能为空",
})
}
return this.render(ctx, uiPath, {
value: username,
error: undefined,
})
}
async validatePassword(ctx) {
const { password } = ctx.request.body
const uiPath = "page/login/_ui/password"
if (password === "") {
return this.render(ctx, uiPath, {
value: password,
error: "密码不能为空",
})
}
return this.render(ctx, uiPath, {
value: password,
error: undefined,
})
}
async logout(ctx) {
ctx.session.user = null
ctx.set("HX-Redirect", "/")
}
}

22
src/services/AuthService.js → src/modules/Auth/services/index.js

@ -1,12 +1,14 @@
import UserModel from "../db/models/UserModel.js" import UserModel from "@/db/models/UserModel.js"
import CommonError from "utils/error/CommonError.js" import CommonError from "@/utils/error/CommonError.js"
import { comparePassword } from "utils/bcrypt.js" import { comparePassword } from "@/utils/bcrypt.js"
import { JWT_SECRET } from "@/middlewares/Auth/index.js"
import jwt from "jsonwebtoken"
/** /**
* 认证服务类 * 认证服务类
* 提供认证相关的业务逻辑 * 提供认证相关的业务逻辑
*/ */
class AuthService { export class AuthService {
// 注册新用户 // 注册新用户
static async register(data) { static async register(data) {
try { try {
@ -15,14 +17,14 @@ class AuthService {
} }
// 检查用户名是否已存在 // 检查用户名是否已存在
const existUser = await UserModel.findByUsername(data.username) const existUser = await UserModel.getInstance().findByUsername(data.username)
if (existUser) { if (existUser) {
throw new CommonError(`用户名${data.username}已存在`) throw new CommonError(`用户名${data.username}已存在`)
} }
// 检查邮箱是否已存在 // 检查邮箱是否已存在
if (data.email) { if (data.email) {
const existEmail = await UserModel.findByEmail(data.email) const existEmail = await UserModel.getInstance().findByEmail(data.email)
if (existEmail) { if (existEmail) {
throw new CommonError(`邮箱${data.email}已被使用`) throw new CommonError(`邮箱${data.email}已被使用`)
} }
@ -31,7 +33,7 @@ class AuthService {
// 密码加密 // 密码加密
const hashed = await hashPassword(data.password) const hashed = await hashPassword(data.password)
const user = await UserModel.create({ ...data, password: hashed }) const user = await UserModel.getInstance().create({ ...data, password: hashed })
// 返回脱敏信息 // 返回脱敏信息
const { password, ...userInfo } = Array.isArray(user) ? user[0] : user const { password, ...userInfo } = Array.isArray(user) ? user[0] : user
@ -55,9 +57,9 @@ class AuthService {
let user let user
if (username) { if (username) {
user = await UserModel.findByUsername(username) user = await UserModel.getInstance().findByUsername(username)
} else if (email) { } else if (email) {
user = await UserModel.findByEmail(email) user = await UserModel.getInstance().findByEmail(email)
} }
if (!user) { if (!user) {
@ -82,3 +84,5 @@ class AuthService {
} }
} }
} }
export default AuthService

0
src/modules/Contact/controller/index.js

4
src/services/ContactService.js → src/modules/Contact/services/index.js

@ -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"
/** /**
* 联系信息服务类 * 联系信息服务类

5
src/controllers/Page/CommonController.js → src/modules/Index/controller/index.js

@ -1,7 +1,6 @@
import Router from "utils/router.js" import Router from "utils/router.js"
import { logger } from "@/logger.js" import { logger } from "@/logger.js"
import BaseController from "@/base/BaseController.js" import BaseController from "@/base/BaseController.js"
import SiteConfigModel from '@/db/models/SiteConfigModel.js'
export default class CommonController extends BaseController { export default class CommonController extends BaseController {
constructor() { constructor() {
@ -46,8 +45,8 @@ export default class CommonController extends BaseController {
const router = new Router({ auth: "try" }) const router = new Router({ auth: "try" })
// 首页 // 首页
router.get("", controller.handleRequest(controller.indexGet)) router.get("", controller.handleRequest(controller.indexGet), { auth: "try" })
router.get("/", controller.handleRequest(controller.indexGet)) router.get("/", controller.handleRequest(controller.indexGet), { auth: "try" })
// router.get("/about", controller.handleRequest(controller.pageGet("page/about/index"))) // router.get("/about", controller.handleRequest(controller.pageGet("page/about/index")))
router.get("/contact", controller.handleRequest(controller.pageGet("page/extra/contact"))) router.get("/contact", controller.handleRequest(controller.pageGet("page/extra/contact")))
router.get("/faq", controller.handleRequest(controller.pageGet("page/extra/faq"))) router.get("/faq", controller.handleRequest(controller.pageGet("page/extra/faq")))

0
src/modules/Index/services/index.js

12
src/controllers/Api/JobController.js → src/modules/Job/controller/index.js

@ -1,7 +1,7 @@
// Job Controller 示例:如何调用 service 层动态控制和查询定时任务 // Job Controller 示例:如何调用 service 层动态控制和查询定时任务
import JobService from "services/JobService.js" import JobService from "../services"
import { R } from "utils/helper.js" import { R } from "@/utils/helper.js"
import Router from "utils/router.js" import Router from "@/utils/router.js"
class JobController { class JobController {
constructor() { constructor() {
@ -33,9 +33,9 @@ class JobController {
} }
static createRoutes() { static createRoutes() {
const controller = new JobController() const controller = new this()
const router = new Router({ prefix: "/api/jobs", auth: true }) const router = new Router({ prefix: "/api/jobs", auth: "try" })
router.get("/", controller.list.bind(controller)) router.get("", controller.list.bind(controller))
router.get("/", controller.list.bind(controller)) router.get("/", controller.list.bind(controller))
router.post("/start/:id", controller.start.bind(controller)) router.post("/start/:id", controller.start.bind(controller))
router.post("/stop/:id", controller.stop.bind(controller)) router.post("/stop/:id", controller.stop.bind(controller))

2
src/services/JobService.js → src/modules/Job/services/index.js

@ -1,4 +1,4 @@
import jobs from "../jobs" import jobs from "@/jobs"
class JobService { class JobService {
startJob(id) { startJob(id) {

4
src/services/SiteConfigService.js → src/modules/SiteConfig/services/index.js

@ -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"
/** /**
* 站点配置服务类 * 站点配置服务类

563
src/services/ArticleService.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 }

492
src/services/BookmarkService.js

@ -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 }

415
src/services/UserService.js

@ -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 }

84
src/services/index.js

@ -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')

222
src/utils/ForRegister.js

@ -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}`)
}
}

388
src/utils/cache/RouteCache.js

@ -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 }

19
src/utils/error/ApiError.js

@ -0,0 +1,19 @@
import app from "@/global.js"
import BaseError from "./BaseError.js"
export default class ApiError extends BaseError {
constructor(message, status = ApiError.ERR_CODE.BAD_REQUEST) {
super(message, status)
this.name = "ApiError"
const ctx = app.currentContext
this.ctx = ctx
this.user = ctx?.state?.user || null
this.info = {
path: ctx?.path || "",
method: ctx?.method || "",
query: ctx?.query || {},
body: ctx?.request?.body || {},
params: ctx?.params || {},
}
}
}

11
src/utils/error/CommonError.js

@ -5,6 +5,15 @@ export default class CommonError extends BaseError {
constructor(message, status = CommonError.ERR_CODE.BAD_REQUEST) { constructor(message, status = CommonError.ERR_CODE.BAD_REQUEST) {
super(message, status) super(message, status)
this.name = "CommonError" this.name = "CommonError"
this.ctx = app.currentContext const ctx = app.currentContext
// this.ctx = ctx
this.user = ctx?.state?.user || null
this.info = {
path: ctx?.path || "",
method: ctx?.method || "",
query: ctx?.query || {},
body: ctx?.request?.body || {},
params: ctx?.params || {},
}
} }
} }

75
src/utils/router.js

@ -1,32 +1,8 @@
import { match } from 'path-to-regexp'; import { match } from 'path-to-regexp';
import compose from 'koa-compose'; import compose from 'koa-compose';
import routeCache from './cache/RouteCache.js';
import AuthError from './error/AuthError.js'; import AuthError from './error/AuthError.js';
import CommonError from './error/CommonError.js'; import CommonError from './error/CommonError.js';
function RouteAuth(options = {}) {
const { auth = true } = options
return async (ctx, next) => {
// 当 auth 为 false 时,已登录用户不能访问
if (auth === false) {
if (ctx.state.user) {
throw new CommonError("该接口不能登录查看")
}
}
// 当 auth 为 true 时,必须登录才能访问
if (auth === true) {
if (!ctx.state.user) {
throw new AuthError("该接口必须登录查看")
}
}
// 其他自定义模式(如角色检查等)
return await next()
}
}
class Router { class Router {
/** /**
* 初始化路由实例 * 初始化路由实例
@ -106,22 +82,11 @@ class Router {
* 生成Koa中间件 * 生成Koa中间件
* @returns {Function} Koa中间件函数 * @returns {Function} Koa中间件函数
*/ */
middleware() { middleware(beforeMiddleware) {
return async (ctx, next) => { return async (ctx, next) => {
const { method, path } = ctx; const { method, path } = ctx;
// 直接进行路由匹配(不使用缓存)
// 尝试从缓存获取路由匹配结果 const route = this._matchRoute(method.toLowerCase(), path);
let route = routeCache.getRouteMatch(method, path);
if (!route) {
// 缓存未命中,执行路由匹配
route = this._matchRoute(method.toLowerCase(), path);
// 将匹配结果存入缓存
if (route) {
routeCache.setRouteMatch(method, path, route);
}
}
// 组合全局中间件、路由专属中间件和 handler // 组合全局中间件、路由专属中间件和 handler
const middlewares = [...this.middlewares]; const middlewares = [...this.middlewares];
@ -129,31 +94,13 @@ class Router {
// 如果匹配到路由,添加路由专属中间件和处理函数 // 如果匹配到路由,添加路由专属中间件和处理函数
ctx.params = route.params; ctx.params = route.params;
let isAuth = this.options.auth; if (beforeMiddleware) {
if (route.meta && route.meta.auth !== undefined) { const options = Object.assign({}, this.options, route.meta);
isAuth = route.meta.auth; middlewares.push(beforeMiddleware(options));
} }
// 尝试从缓存获取组合中间件
const cacheKey = { auth: isAuth, middlewares: this.middlewares.length };
let composed = routeCache.getMiddlewareComposition(this.middlewares, cacheKey);
if (!composed) {
// 缓存未命中,重新组合中间件
middlewares.push(RouteAuth({ auth: isAuth }));
middlewares.push(route.handler); middlewares.push(route.handler);
composed = compose(middlewares); const composed = compose(middlewares);
// 将组合结果存入缓存
routeCache.setMiddlewareComposition(this.middlewares, cacheKey, composed);
} else {
// 缓存命中,但仍需添加当前路由的处理器
const finalMiddlewares = [...middlewares];
finalMiddlewares.push(RouteAuth({ auth: isAuth }));
finalMiddlewares.push(route.handler);
composed = compose(finalMiddlewares);
}
await composed(ctx, next); await composed(ctx, next);
} else { } else {
// 如果没有匹配到路由,直接调用 next // 如果没有匹配到路由,直接调用 next
@ -187,14 +134,6 @@ class Router {
} }
return null; return null;
} }
/**
* 清除该路由器的相关缓存
*/
clearCache() {
routeCache.clearRouteMatches();
routeCache.clearMiddlewares();
}
} }
export default Router; export default Router;

26
src/utils/router/RouteAuth.js

@ -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()
}
}

198
src/utils/test/ConfigTest.js

@ -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 }

222
src/utils/test/RouteCacheTest.js

@ -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 }

20
src/utils/user.js

@ -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

12
src/views/page/login/_ui/password.pug

@ -0,0 +1,12 @@
div(hx-target="this" hx-swap="outerHTML")
div(class="relative")
label.block.text-sm.font-medium.text-gray-700.mb-2(for="password") 密码
input(type="password" id="password" value=value name="password" placeholder="请输入密码" hx-indicator="#ind" hx-post="/login/validate/password" hx-trigger="blur delay:500ms" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 ease-in-out" + (error ? ' border-red-500 focus:ring-red-500' : ''))
div(id="ind" class="htmx-indicator absolute right-3 top-9 transform -translate-y-1/2")
div(class="animate-spin h-5 w-5 border-2 border-gray-300 border-t-blue-500 rounded-full")
if error
div(class="error-message text-red-500 text-sm mt-2 flex items-center")
svg.w-4.h-4.mr-1(xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor")
path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z")
| #{error}

16
src/views/page/login/_ui/username.pug

@ -1,4 +1,12 @@
div(hx-target="this" hx-swap="outerHTML" hx-post="/login/validate/username?aa=1")
input(type="text" id="username" value=value name="username" placeholder="用户名" hx-indicator="#ind") 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}

42
src/views/page/login/index.pug

@ -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 || '请求失败')
}
}
});

1
vite.config.ts

@ -19,7 +19,6 @@ export default defineConfig({
db: resolve(__dirname, "src/db"), db: resolve(__dirname, "src/db"),
config: resolve(__dirname, "src/config"), config: resolve(__dirname, "src/config"),
utils: resolve(__dirname, "src/utils"), utils: resolve(__dirname, "src/utils"),
services: resolve(__dirname, "src/services"),
}, },
}, },
build: { build: {

Loading…
Cancel
Save