Browse Source

feat(core): 重构视图和上下文中间件及数据库初始化流程

- 视图中间件职责纯粹化,仅负责模板渲染及基础上下文处理
- 新增上下文中间件,注入全局应用配置、站点配置及用户信息,支持缓存优化
- 调整中间件注册流程,区分核心中间件(路由前)与后置中间件(路由后)
- 重构数据库初始化脚本,集成DatabaseProvider,支持迁移和数据重置交互
- 移除旧的knex配置文件,统一数据库配置,确保连接配置正确
- 添加数据库工具脚本,支持迁移文件与种子数据文件的生成与管理
- 增加站点配置模块,提供丰富接口支持配置管理和基础配置注入
- 完善静态资源中间件,优化文件路径处理及错误管理
refactor
谢亚昕 3 months ago
parent
commit
d36ce7ee14
  1. 3
      .env.example
  2. 109
      WEB_ROUTES_IMPROVEMENT_REPORT.md
  3. BIN
      data/database.db
  4. BIN
      data/database.db-shm
  5. BIN
      data/database.db-wal
  6. 100
      docs/views-refactor.md
  7. 61
      knexfile.mjs
  8. 8
      package.json
  9. 237
      scripts/db-tools.js
  10. 80
      scripts/init.js
  11. 37
      src/app/bootstrap/middleware.js
  12. 6
      src/app/bootstrap/routes.js
  13. 3
      src/app/config/index.js
  14. 51
      src/app/providers/DatabaseProvider.js
  15. 112
      src/infrastructure/http/middleware/context.js
  16. 8
      src/infrastructure/http/middleware/static.js
  17. 57
      src/infrastructure/http/middleware/views.js
  18. 34
      src/main.js
  19. 274
      src/modules/site-config/controllers/SiteConfigController.js
  20. 166
      src/modules/site-config/models/SiteConfigModel.js
  21. 57
      src/modules/site-config/routes.js
  22. 361
      src/modules/site-config/services/SiteConfigService.js
  23. 651
      src/presentation/routes/web.js
  24. 37
      src/presentation/views/page/articles/create.pug
  25. 37
      src/presentation/views/page/articles/edit.pug
  26. 6
      src/shared/helpers/routeHelper.js

3
.env.example

@ -27,6 +27,9 @@ NODE_ENV=development
# Server port
PORT=3000
# 数据库
DB_PATH=./data/database.db
# 日志文件目录
# Log files directory
LOG_DIR=logs

109
WEB_ROUTES_IMPROVEMENT_REPORT.md

@ -0,0 +1,109 @@
# Web 路由改进报告
## 问题描述
原来的 `src/presentation/routes/web.js` 文件只是静态的路由定义,缺乏真实的数据,所有页面都只是返回空数据或模拟数据。
## 改进内容
### 1. 引入服务层
添加了所有必要的服务类:
- `ArticleService` - 文章管理服务
- `UserService` - 用户管理服务
- `SiteConfigService` - 站点配置服务
- `AuthService` - 认证服务
### 2. 首页路由改进
- 获取最新文章数据
- 获取用户统计信息
- 获取文章统计信息
- 获取站点基本配置
- 提供真实的 API 列表
### 3. 关于页面改进
- 集成站点配置数据
- 动态显示站点信息
### 4. 用户资料页面改进
- 获取用户详细信息
- 显示用户的文章列表
- 提供用户统计数据
### 5. 文章功能完善
- **文章列表页**:支持分页、搜索、分类筛选
- **文章详情页**:显示完整文章内容、相关文章推荐、自动增加阅读量
- **创建文章页**:验证用户权限
- **编辑文章页**:权限验证、文章作者检查
### 6. 新增搜索功能
- 支持文章搜索
- 支持用户搜索
- 分页显示搜索结果
### 7. 管理后台功能
- **管理员仪表板**:统计数据展示、权限验证
- **用户管理页**:用户列表、搜索、状态管理
- **文章管理页**:文章列表、状态管理、搜索筛选
- **站点设置页**:配置管理
- **系统监控页**:系统运行状态监控
### 8. 错误处理优化
- 统一的错误处理机制
- 友好的错误页面显示
- 详细的错误日志记录
## 技术特点
### 数据获取
- 所有路由都集成了真实的数据服务
- 支持异步数据加载
- 错误回退机制
### 权限控制
- 登录状态检查
- 管理员权限验证
- 文章作者权限验证
### 用户体验
- 分页支持
- 搜索功能
- 相关内容推荐
- 统计信息展示
### 架构整合
- 完全集成了模块化架构
- 使用了项目的服务层
- 遵循了项目的编码规范
## 路由列表
### 公共路由
- `GET /` - 首页(集成真实数据)
- `GET /about` - 关于页面
- `GET /articles` - 文章列表(支持搜索、分页)
- `GET /articles/:id` - 文章详情
- `GET /search` - 搜索页面
### 用户路由
- `GET /profile` - 用户资料页
- `GET /articles/create` - 创建文章页
- `GET /articles/:id/edit` - 编辑文章页
### 管理员路由
- `GET /admin` - 管理后台首页
- `GET /admin/users` - 用户管理
- `GET /admin/articles` - 文章管理
- `GET /admin/settings` - 站点设置
- `GET /admin/monitor` - 系统监控
## 性能优化
- 使用 `Promise.all()` 并行获取数据
- 错误容错机制避免页面崩溃
- 分页减少数据传输量
## 注意事项
1. 所有路由都已集成真实的服务层数据
2. 包含完整的错误处理和权限验证
3. 支持动态配置和用户个性化
4. 遵循项目的模块化架构设计
这样的改进使得 Web 路由不再是空壳,而是具有完整功能的页面路由系统。

BIN
data/database.db

Binary file not shown.

BIN
data/database.db-shm

Binary file not shown.

BIN
data/database.db-wal

Binary file not shown.

100
docs/views-refactor.md

@ -0,0 +1,100 @@
# 视图系统重构说明
## 重构目标
解决原有 `views.js` 中间件职责混乱的问题,实现更清晰的关注点分离。
## 问题分析
### 原有架构问题
1. **职责混乱**:视图中间件承担了数据获取、全局变量注入等非渲染职责
2. **层级耦合**:基础设施层直接依赖应用层服务
3. **性能问题**:每次渲染都重新获取站点配置,无缓存机制
4. **可测试性差**:强耦合导致单元测试困难
## 重构方案
### 1. 视图中间件 (`views.js`)
**新职责:**
- 纯粹的模板渲染功能
- 基础上下文处理(路径、登录状态等框架级数据)
- 错误处理和日志记录
**移除的职责:**
- 业务数据获取(站点配置、用户信息等)
- 复杂的上下文构建
- 服务层调用
### 2. 上下文中间件 (`context.js`)
**新增职责:**
- 全局数据注入($config、$site、$user等)
- 站点配置管理(带缓存)
- 通用上下文变量设置
**特性:**
- 5分钟内存缓存机制
- 优雅的错误降级
- 可配置的数据注入选项
## 架构改进
### 中间件执行顺序
```
1. 错误处理
2. 响应时间统计
3. 会话管理
4. 上下文数据注入 ← 新增
5. 请求体解析
6. 视图渲染
7. HTTP缓存
```
### 数据流向
```
Context Middleware → ctx.state → Views Middleware → Templates
```
## 使用方式
### 在控制器中渲染视图
```javascript
// 基础渲染(使用全局上下文)
await ctx.render('index/home')
// 传递额外数据
await ctx.render('user/profile', {
title: '个人资料',
activeTab: 'profile'
})
// 所有模板都可以访问:
// - currentPath: 当前路径
// - isLogin: 登录状态
// - $config: 应用配置
// - $site: 站点配置
// - $user: 用户信息
```
### 缓存管理
```javascript
import { clearSiteConfigCache } from '@/infrastructure/http/middleware/context.js'
// 在站点配置更新后清空缓存
await siteConfigService.updateConfig(data)
clearSiteConfigCache()
```
## 优势
1. **职责清晰**:每个中间件只负责自己的核心功能
2. **性能优化**:站点配置缓存减少数据库查询
3. **可维护性**:代码结构更清晰,便于维护和测试
4. **可扩展性**:上下文中间件可以轻松扩展新的全局数据
5. **错误隔离**:即使某个数据获取失败,也不会影响整个渲染流程
## 注意事项
1. 上下文中间件必须在认证中间件之后注册,以便正确获取用户信息
2. 视图中间件必须在上下文中间件之后注册,以便使用注入的数据
3. 站点配置缓存会在应用重启时自动清空
4. 如需修改缓存TTL,可在 `context.js` 中调整 `CACHE_TTL` 常量

61
knexfile.mjs

@ -1,61 +0,0 @@
// knexfile.mjs (ESM格式)
console.log(process.env.DB_PATH);
export default {
development: {
client: "sqlite3",
connection: {
filename: process.env.DB_PATH || "./database/development.sqlite3",
},
migrations: {
directory: "./src/infrastructure/database/migrations", // 迁移文件目录
// 启用ES模块支持
extension: "mjs",
loadExtensions: [".mjs", ".js"],
},
seeds: {
directory: "./src/infrastructure/database/seeds", // 种子数据目录,
// 启用ES模块支持
extension: "mjs",
loadExtensions: [".mjs", ".js"],
timestampFilenamePrefix: true,
},
useNullAsDefault: true, // SQLite需要这一选项
pool: {
min: 1,
max: 1, // SQLite 建议设为 1,避免并发问题
afterCreate: (conn, done) => {
conn.run("PRAGMA journal_mode = WAL", done) // 启用 WAL 模式提高并发
},
},
},
// 生产环境、测试环境配置可按需添加
production: {
client: "sqlite3",
connection: {
filename: process.env.DB_PATH || "./database/db.sqlite3",
},
migrations: {
directory: "./src/infrastructure/database/migrations", // 迁移文件目录
// 启用ES模块支持
extension: "mjs",
loadExtensions: [".mjs", ".js"],
},
seeds: {
directory: "./src/infrastructure/database/seeds", // 种子数据目录,
// 启用ES模块支持
extension: "mjs",
loadExtensions: [".mjs", ".js"],
timestampFilenamePrefix: true,
},
useNullAsDefault: true, // SQLite需要这一选项
pool: {
min: 1,
max: 1, // SQLite 建议设为 1,避免并发问题
afterCreate: (conn, done) => {
conn.run("PRAGMA journal_mode = WAL", done) // 启用 WAL 模式提高并发
},
},
},
}

8
package.json

@ -7,10 +7,10 @@
"dev": "bun --hot src/main.js",
"start": "cross-env NODE_ENV=production bun run src/main.js",
"build": "vite build",
"migrate:make": "npx knex migrate:make ",
"migrate": "npx knex migrate:latest",
"seed:make": "npx knex seed:make ",
"seed": "npx knex seed:run ",
"db:init": "bun run scripts/init.js",
"db:migrate": "bun run scripts/db-tools.js migrate",
"db:seed": "bun run scripts/db-tools.js seed",
"db:help": "bun run scripts/db-tools.js help",
"dev:init": "bun run scripts/init.js",
"init": "cross-env NODE_ENV=production bun run scripts/init.js",
"test:env": "bun run scripts/test-env-validation.js"

237
scripts/db-tools.js

@ -0,0 +1,237 @@
/**
* 数据库工具脚本
* 用于创建迁移文件和种子数据文件
*/
import { writeFileSync, mkdirSync, existsSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
// 获取当前文件目录
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const projectRoot = resolve(__dirname, '..')
// 配置路径
const MIGRATIONS_DIR = 'src/infrastructure/database/migrations'
const SEEDS_DIR = 'src/infrastructure/database/seeds'
/**
* 生成时间戳
* @returns {string} 格式: YYYYMMDDHHMMSS
*/
function generateTimestamp() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
return `${year}${month}${day}${hours}${minutes}${seconds}`
}
/**
* 创建迁移文件模板
* @param {string} migrationName 迁移名称
* @returns {string} 迁移文件内容
*/
function createMigrationTemplate(migrationName) {
return `/**
* ${migrationName} 迁移文件
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export const up = async knex => {
// TODO: 在此处编写数据库结构变更逻辑
// 例如:创建表、添加列、创建索引等
/*
return knex.schema.createTable('table_name', table => {
table.increments('id').primary()
table.string('name').notNullable()
table.timestamps(true, true)
})
*/
}
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export const down = async knex => {
// TODO: 在此处编写回滚逻辑
// 例如:删除表、删除列、删除索引等
/*
return knex.schema.dropTable('table_name')
*/
}
`
}
/**
* 创建种子数据文件模板
* @param {string} seedName 种子数据名称
* @returns {string} 种子数据文件内容
*/
function createSeedTemplate(seedName) {
return `/**
* ${seedName} 种子数据
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export const seed = async knex => {
// 清空现有数据(可选)
// await knex('table_name').del()
// 插入种子数据
/*
await knex('table_name').insert([
{
name: '示例数据1',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
name: '示例数据2',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
}
])
console.log('✅ ${seedName} seeded successfully!')
*/
}
`
}
/**
* 创建迁移文件
* @param {string} migrationName 迁移名称
*/
function createMigration(migrationName) {
if (!migrationName) {
console.error('❌ 请提供迁移名称')
console.log('用法: bun run scripts/db-tools.js migrate <migration_name>')
process.exit(1)
}
// 确保目录存在
const migrationsPath = resolve(projectRoot, MIGRATIONS_DIR)
if (!existsSync(migrationsPath)) {
mkdirSync(migrationsPath, { recursive: true })
}
// 生成文件名
const timestamp = generateTimestamp()
const fileName = `${timestamp}_${migrationName}.mjs`
const filePath = resolve(migrationsPath, fileName)
// 检查文件是否已存在
if (existsSync(filePath)) {
console.error(`❌ 迁移文件已存在: ${fileName}`)
process.exit(1)
}
// 创建文件
const content = createMigrationTemplate(migrationName)
writeFileSync(filePath, content, 'utf8')
console.log(`✅ 迁移文件创建成功: ${fileName}`)
console.log(`📁 路径: ${filePath}`)
console.log(`📝 请编辑文件并实现 up() 和 down() 方法`)
}
/**
* 创建种子数据文件
* @param {string} seedName 种子数据名称
*/
function createSeed(seedName) {
if (!seedName) {
console.error('❌ 请提供种子数据名称')
console.log('用法: bun run scripts/db-tools.js seed <seed_name>')
process.exit(1)
}
// 确保目录存在
const seedsPath = resolve(projectRoot, SEEDS_DIR)
if (!existsSync(seedsPath)) {
mkdirSync(seedsPath, { recursive: true })
}
// 生成文件名
const timestamp = generateTimestamp()
const fileName = `${timestamp}_${seedName}.mjs`
const filePath = resolve(seedsPath, fileName)
// 检查文件是否已存在
if (existsSync(filePath)) {
console.error(`❌ 种子数据文件已存在: ${fileName}`)
process.exit(1)
}
// 创建文件
const content = createSeedTemplate(seedName)
writeFileSync(filePath, content, 'utf8')
console.log(`✅ 种子数据文件创建成功: ${fileName}`)
console.log(`📁 路径: ${filePath}`)
console.log(`📝 请编辑文件并实现 seed() 方法`)
}
/**
* 显示帮助信息
*/
function showHelp() {
console.log(`
🗄 数据库工具 - 迁移和种子数据管理
用法:
bun run scripts/db-tools.js <command> [options]
命令:
migrate <name> 创建新的迁移文件
seed <name> 创建新的种子数据文件
help 显示帮助信息
示例:
bun run scripts/db-tools.js migrate create_posts_table
bun run scripts/db-tools.js seed posts_seed
💡 提示:
- 迁移文件用于管理数据库结构变更
- 种子数据文件用于插入测试或初始数据
- 文件名会自动添加时间戳前缀
- 所有文件使用 ES 模块格式 (.mjs)
`)
}
// 主函数
function main() {
const args = process.argv.slice(2)
const command = args[0]
const name = args[1]
switch (command) {
case 'migrate':
createMigration(name)
break
case 'seed':
createSeed(name)
break
case 'help':
case '--help':
case '-h':
showHelp()
break
default:
console.error(`❌ 未知命令: ${command}`)
showHelp()
process.exit(1)
}
}
// 运行主函数
main()

80
scripts/init.js

@ -1,23 +1,69 @@
const { execSync } = require('child_process');
const readline = require('readline');
/**
* 数据库初始化脚本
* 直接使用 DatabaseProvider 进行迁移和种子数据操作
* 避免重复配置
*/
// 写一个执行npm run migrate && npm run seed的脚本,当执行npm run seed时会谈提示是否重置数据
function run(command) {
execSync(command, { stdio: 'inherit' });
import { createInterface } from 'readline'
import DatabaseProvider from '../src/app/providers/DatabaseProvider.js'
/**
* 询问用户是否重置数据
* @returns {Promise<boolean>} 用户选择结果
*/
function askForReset() {
return new Promise((resolve) => {
const rl = createInterface({
input: process.stdin,
output: process.stdout
})
rl.question('是否重置数据?(y/N): ', (answer) => {
rl.close()
resolve(answer.trim().toLowerCase() === 'y')
})
})
}
run('npx knex migrate:latest');
/**
* 主初始化函数
*/
async function init() {
try {
console.log('🚀 开始数据库初始化...')
console.log(`运行环境: ${typeof Bun !== 'undefined' ? 'Bun' : 'Node.js'}`)
// 1. 初始化数据库连接
console.log('\n🔗 初始化数据库连接...')
await DatabaseProvider.register()
// 2. 运行数据库迁移
console.log('\n📦 运行数据库迁移...')
await DatabaseProvider.runMigrations()
console.log('✅ 数据库迁移完成')
// 3. 询问是否重置数据
const shouldReset = await askForReset()
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
if (shouldReset) {
console.log('\n🌱 运行种子数据...')
await DatabaseProvider.runSeeds()
} else {
console.log('⏭️ 已取消数据重置')
}
rl.question('是否重置数据?(y/N): ', (answer) => {
if (answer.trim().toLowerCase() === 'y') {
run('npx knex seed:run');
} else {
console.log('已取消数据重置。');
console.log('\n🎉 数据库初始化完成!')
// 关闭数据库连接
await DatabaseProvider.close()
process.exit(0)
} catch (error) {
console.error('\n❌ 数据库初始化失败:', error.message)
console.error(error.stack)
process.exit(1)
}
rl.close();
});
}
// 运行初始化
init()

37
src/app/bootstrap/middleware.js

@ -14,6 +14,7 @@ import ValidationMiddleware from '../../core/middleware/validation/index.js'
// 第三方和基础设施中间件
import bodyParser from 'koa-bodyparser'
import Views from '../../infrastructure/http/middleware/views.js'
import ContextMiddleware from '../../infrastructure/http/middleware/context.js'
import Session from '../../infrastructure/http/middleware/session.js'
import etag from '@koa/etag'
import conditional from 'koa-conditional-get'
@ -21,9 +22,9 @@ import { resolve } from 'path'
import staticMiddleware from '../../infrastructure/http/middleware/static.js'
/**
* 注册全局中间件
* 注册核心中间件路由之前
*/
export function registerGlobalMiddleware() {
export function registerCoreMiddleware() {
// 错误处理中间件(最先注册)
app.use(ErrorHandlerMiddleware())
@ -33,6 +34,13 @@ export function registerGlobalMiddleware() {
// 会话管理
app.use(Session(app))
// 上下文数据注入(在身份验证之后,视图渲染之前)
app.use(ContextMiddleware({
includeSiteConfig: true,
includeAppConfig: true,
includeUserInfo: true
}))
// 请求体解析
app.use(bodyParser())
@ -45,12 +53,8 @@ export function registerGlobalMiddleware() {
// HTTP 缓存
app.use(conditional())
app.use(etag())
}
/**
* 注册认证中间件
*/
export function registerAuthMiddleware() {
// 认证中间件(在路由之前注册,但全局放行)
app.use(AuthMiddleware({
whiteList: [
{ pattern: "/", auth: false },
@ -61,12 +65,16 @@ export function registerAuthMiddleware() {
}
/**
* 注册静态资源中间件
* 注册后置中间件路由之后
*/
export function registerStaticMiddleware() {
export function registerPostMiddleware() {
// 静态资源中间件(在路由之后注册,作为回退处理)
app.use(async (ctx, next) => {
// 如果已有响应体或状态码不是200,跳过静态资源处理
if (ctx.body) return await next()
if (ctx.status === 200) return await next()
if (ctx.status !== 404) return await next()
// 只处理 GET 请求
if (ctx.method.toLowerCase() === "get") {
try {
await staticMiddleware(ctx, ctx.path, {
@ -75,6 +83,7 @@ export function registerStaticMiddleware() {
immutable: config.static.immutable
})
} catch (err) {
// 如果静态资源也没找到,保持404状态
if (err.status !== 404) throw err
}
}
@ -83,12 +92,12 @@ export function registerStaticMiddleware() {
}
/**
* 注册所有中间件
* 注册所有中间件兼容旧版本
* @deprecated 请使用 registerCoreMiddleware registerPostMiddleware
*/
export function registerMiddleware() {
registerGlobalMiddleware()
registerAuthMiddleware()
registerStaticMiddleware()
registerCoreMiddleware()
// 注意:这里不调用 registerPostMiddleware,因为后置中间件应该在路由之后
}
export default registerMiddleware

6
src/app/bootstrap/routes.js

@ -13,14 +13,14 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
/**
* 注册所有路由
*/
export function registerRoutes() {
export async function registerRoutes() {
// 自动注册控制器路由
const controllersPath = resolve(__dirname, '../../modules')
autoRegisterControllers(app, controllersPath)
await autoRegisterControllers(app, controllersPath)
// 注册共享控制器
const sharedControllersPath = resolve(__dirname, '../../modules/shared/controllers')
autoRegisterControllers(app, sharedControllersPath)
await autoRegisterControllers(app, sharedControllersPath)
}
export default registerRoutes

3
src/app/config/index.js

@ -6,6 +6,7 @@
// 移除循环依赖,在应用启动时验证环境变量
const config = {
base: "/",
// 服务器配置
server: {
port: process.env.PORT || 3000,
@ -24,7 +25,7 @@ const config = {
database: {
client: 'sqlite3',
connection: {
filename: process.env.DB_PATH || './database/development.sqlite3'
filename: process.env.DB_PATH
},
useNullAsDefault: true,
migrations: {

51
src/app/providers/DatabaseProvider.js

@ -16,15 +16,36 @@ class DatabaseProvider {
*/
async register() {
try {
this.db = knex(databaseConfig)
// 创建 knex 实例时使用正确的配置结构
const knexConfig = {
client: databaseConfig.client,
connection: databaseConfig.connection,
useNullAsDefault: databaseConfig.useNullAsDefault,
migrations: {
directory: databaseConfig.migrations.directory,
extension: 'mjs',
loadExtensions: ['.mjs', '.js']
},
seeds: {
directory: databaseConfig.seeds.directory,
extension: 'mjs',
loadExtensions: ['.mjs', '.js']
},
pool: {
min: 1,
max: 1,
afterCreate: (conn, done) => {
conn.run('PRAGMA journal_mode = WAL', done)
}
}
}
this.db = knex(knexConfig)
// 测试数据库连接
await this.db.raw('SELECT 1')
console.log('✓ 数据库连接成功')
// 运行待处理的迁移
await this.runMigrations()
return this.db
} catch (error) {
console.error('✗ 数据库连接失败:', error.message)
@ -34,9 +55,14 @@ class DatabaseProvider {
/**
* 运行数据库迁移
* 注意这个方法应该在需要时手动调用而不是在每次连接时自动运行
*/
async runMigrations() {
try {
if (!this.db) {
throw new Error('数据库连接未初始化')
}
await this.db.migrate.latest()
console.log('✓ 数据库迁移完成')
} catch (error) {
@ -46,6 +72,23 @@ class DatabaseProvider {
}
/**
* 运行种子数据
*/
async runSeeds() {
try {
if (!this.db) {
throw new Error('数据库连接未初始化')
}
await this.db.seed.run()
console.log('✓ 种子数据执行完成')
} catch (error) {
console.error('✗ 种子数据执行失败:', error.message)
throw error
}
}
/**
* 获取数据库实例
*/
getConnection() {

112
src/infrastructure/http/middleware/context.js

@ -0,0 +1,112 @@
/**
* 上下文数据注入中间件
*
* 职责
* 1. 为视图模板注入全局数据
* 2. 管理站点配置用户信息等通用上下文
* 3. 提供缓存机制以优化性能
*/
import { logger } from '@/logger'
import config from '../../../app/config/index.js'
import SiteConfigService from '../../../modules/site-config/services/SiteConfigService.js'
// 简单的内存缓存
let siteConfigCache = null
let lastCacheTime = 0
const CACHE_TTL = 5 * 60 * 1000 // 5分钟缓存
/**
* 获取站点配置带缓存
*/
async function getSiteConfig() {
const now = Date.now()
// 检查缓存是否有效
if (siteConfigCache && (now - lastCacheTime) < CACHE_TTL) {
return siteConfigCache
}
try {
const siteConfigService = new SiteConfigService()
siteConfigCache = await siteConfigService.getBasicConfig()
lastCacheTime = now
return siteConfigCache
} catch (error) {
logger.error('获取站点配置失败:', error)
// 返回默认配置
const defaultConfig = {
site_title: '我的网站',
site_author: '站点管理员',
site_description: '一个基于Koa3的现代化网站'
}
// 如果是首次获取失败,也缓存默认配置
if (!siteConfigCache) {
siteConfigCache = defaultConfig
lastCacheTime = now
}
return siteConfigCache
}
}
/**
* 清空站点配置缓存用于配置更新后
*/
export function clearSiteConfigCache() {
siteConfigCache = null
lastCacheTime = 0
logger.debug('站点配置缓存已清空')
}
/**
* 上下文数据中间件
*/
export default function contextMiddleware(options = {}) {
const {
includeSiteConfig = true,
includeAppConfig = true,
includeUserInfo = true
} = options
return async function context(ctx, next) {
// 确保 ctx.state 存在
if (!ctx.state) {
ctx.state = {}
}
try {
// 注入应用配置
if (includeAppConfig) {
ctx.state.$config = config
}
// 注入站点配置
if (includeSiteConfig) {
ctx.state.$site = await getSiteConfig()
}
// 注入用户相关信息
if (includeUserInfo) {
ctx.state.$user = ctx.state.user || null
ctx.state.isLogin = !!(ctx.state && ctx.state.user)
}
// 注入当前路径
ctx.state.currentPath = ctx.path
logger.debug('上下文数据注入完成', {
path: ctx.path,
hasUser: !!ctx.state.user,
hasSiteConfig: !!ctx.state.$site
})
} catch (error) {
logger.error('上下文数据注入失败:', error)
// 即使失败也要继续,避免阻塞请求
}
await next()
}
}

8
src/infrastructure/http/middleware/static.js

@ -2,7 +2,7 @@
* 静态资源中间件 - 简化版本
*/
import fs from 'fs'
import { resolve, extname } from 'path'
import NPath from 'path'
import { promisify } from 'util'
const stat = promisify(fs.stat)
@ -16,13 +16,13 @@ export default function staticMiddleware(ctx, path, options = {}) {
return new Promise(async (resolve, reject) => {
try {
const fullPath = resolve(root, path.startsWith('/') ? path.slice(1) : path)
const fullPath = NPath.resolve(root, path.startsWith('/') ? path.slice(1) : path)
// 检查文件是否存在
const stats = await stat(fullPath)
if (!stats.isFile()) {
return reject(new Error('Not a file'))
return resolve() // reject(new Error('Not a file'))
}
// 设置响应头
@ -34,7 +34,7 @@ export default function staticMiddleware(ctx, path, options = {}) {
ctx.set('Cache-Control', directives.join(','))
// 设置内容类型
const ext = extname(fullPath)
const ext = NPath.extname(fullPath)
if (ext) {
ctx.type = ext
}

57
src/infrastructure/http/middleware/views.js

@ -1,21 +1,56 @@
/**
* 视图引擎中间件 - 简化版本
* 视图引擎中间件 - 纯粹的模板渲染职责
*
* 职责
* 1. 提供模板渲染能力
* 2. 处理基础的渲染上下文
* 3. 错误处理和日志记录
*
* 不负责
* - 业务数据获取
* - 复杂的上下文构建
* - 服务层调用
*/
import consolidate from 'consolidate'
import { resolve } from 'path'
import { logger } from '@/logger'
export default function viewsMiddleware(viewPath, options = {}) {
const {
extension = 'pug',
engineOptions = {}
options: renderOptions = {}
} = options
return async function views(ctx, next) {
if (ctx.render) return await next()
ctx.response.render = ctx.render = function(templatePath, locals = {}) {
/**
* 渲染模板
* @param {string} templatePath - 模板路径
* @param {object} locals - 本地变量
* @param {object} options - 渲染选项
*/
ctx.response.render = ctx.render = async function(templatePath, locals = {}, options = {}) {
const fullPath = resolve(viewPath, `${templatePath}.${extension}`)
const state = Object.assign({}, locals, ctx.state || {})
// 基础上下文数据(只包含框架级别的数据)
const baseContext = {
currentPath: ctx.path,
isLogin: !!(ctx.state && ctx.state.user),
}
// 合并上下文:基础上下文 + locals + ctx.state + 渲染选项
const renderContext = Object.assign(
{},
baseContext,
locals,
renderOptions,
ctx.state || {},
options
)
// 添加 partials 支持
renderContext.partials = Object.assign({}, renderOptions.partials || {})
ctx.type = 'text/html'
@ -24,9 +59,19 @@ export default function viewsMiddleware(viewPath, options = {}) {
throw new Error(`Template engine not found for ".${extension}" files`)
}
return render(fullPath, state).then(html => {
try {
const html = await render(fullPath, renderContext)
ctx.body = html
})
return html
} catch (err) {
logger.error('View rendering error:', {
template: templatePath,
fullPath,
error: err.message,
stack: err.stack
})
throw err
}
}
return await next()

34
src/main.js

@ -52,16 +52,19 @@ class Application {
// 2. 初始化数据库
await this.initializeDatabase()
// 3. 注册中间件
await this.registerMiddleware()
// 3. 注册核心中间件(在路由之前)
await this.registerCoreMiddleware()
// 4. 注册路由
await this.registerRoutes()
// 5. 初始化任务调度
// 5. 注册后置中间件(在路由之后)
await this.registerPostMiddleware()
// 6. 初始化任务调度
await this.initializeJobs()
// 6. 启动 HTTP 服务器
// 7. 启动 HTTP 服务器
await this.startServer()
this.isStarted = true
@ -105,12 +108,23 @@ class Application {
}
/**
* 注册中间件
* 注册核心中间件路由之前
*/
async registerCoreMiddleware() {
this.logger.info('🔧 注册核心中间件...')
const { registerCoreMiddleware } = await import('./app/bootstrap/middleware.js')
registerCoreMiddleware()
this.logger.info('核心中间件注册完成')
}
/**
* 注册后置中间件路由之后
*/
async registerMiddleware() {
this.logger.info('🔧 注册应用中间件...')
registerMiddleware()
this.logger.info('中间件注册完成')
async registerPostMiddleware() {
this.logger.info('🔧 注册后置中间件...')
const { registerPostMiddleware } = await import('./app/bootstrap/middleware.js')
registerPostMiddleware()
this.logger.info('后置中间件注册完成')
}
/**
@ -124,7 +138,7 @@ class Application {
// 注册模块路由(业务模块)
const { registerRoutes: registerModuleRoutes } = await import('./app/bootstrap/routes.js')
registerModuleRoutes()
await registerModuleRoutes()
this.logger.info('路由注册完成')
}

274
src/modules/site-config/controllers/SiteConfigController.js

@ -0,0 +1,274 @@
/**
* 站点配置控制器
* 处理站点配置相关的请求
*/
import BaseController from '../../../core/base/BaseController.js'
import SiteConfigService from '../services/SiteConfigService.js'
class SiteConfigController extends BaseController {
constructor() {
super()
this.siteConfigService = new SiteConfigService()
}
/**
* 获取单个配置
*/
async get(ctx) {
try {
const { key } = this.getParams(ctx)
if (!key) {
return this.error(ctx, '配置键不能为空', 400)
}
const value = await this.siteConfigService.get(key)
if (value === null) {
return this.error(ctx, '配置不存在', 404)
}
this.success(ctx, { key, value }, '获取配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 设置单个配置
*/
async set(ctx) {
try {
const { key } = this.getParams(ctx)
const { value } = this.getBody(ctx)
this.validateRequired({ key, value }, ['key', 'value'])
const result = await this.siteConfigService.set(key, value)
this.success(ctx, result, '设置配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 获取多个配置
*/
async getMany(ctx) {
try {
const { keys } = this.getQuery(ctx)
if (!keys) {
return this.error(ctx, '请提供配置键列表', 400)
}
// 支持逗号分隔的字符串或数组
const keyList = Array.isArray(keys) ? keys : keys.split(',').map(k => k.trim())
const configs = await this.siteConfigService.getMany(keyList)
this.success(ctx, configs, '获取配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 获取所有配置
*/
async getAll(ctx) {
try {
const configs = await this.siteConfigService.getAll()
this.success(ctx, configs, '获取所有配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 批量设置配置
*/
async setMany(ctx) {
try {
const configs = this.getBody(ctx)
if (!configs || typeof configs !== 'object' || Object.keys(configs).length === 0) {
return this.error(ctx, '配置数据不能为空', 400)
}
const result = await this.siteConfigService.setMany(configs)
this.success(ctx, result, '批量设置配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 删除配置
*/
async delete(ctx) {
try {
const { key } = this.getParams(ctx)
if (!key) {
return this.error(ctx, '配置键不能为空', 400)
}
const result = await this.siteConfigService.delete(key)
this.success(ctx, result, '删除配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 搜索配置
*/
async search(ctx) {
try {
const { keyword } = this.getQuery(ctx)
const configs = await this.siteConfigService.search(keyword)
this.success(ctx, configs, '搜索配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 获取配置统计信息
*/
async stats(ctx) {
try {
const stats = await this.siteConfigService.getStats()
this.success(ctx, stats, '获取配置统计成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 获取基础站点配置
*/
async getBasic(ctx) {
try {
const config = await this.siteConfigService.getBasicConfig()
this.success(ctx, config, '获取基础站点配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 初始化默认配置
*/
async initDefaults(ctx) {
try {
const result = await this.siteConfigService.initializeDefaults()
this.success(ctx, result, '初始化默认配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 获取默认配置不操作数据库
*/
async getDefaults(ctx) {
try {
const defaults = this.siteConfigService.getDefaultConfigs()
this.success(ctx, defaults, '获取默认配置成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 重置配置到默认值
*/
async reset(ctx) {
try {
const { keys } = this.getBody(ctx)
const defaults = this.siteConfigService.getDefaultConfigs()
if (keys && Array.isArray(keys)) {
// 重置指定的配置
const configsToReset = {}
keys.forEach(key => {
if (defaults[key] !== undefined) {
configsToReset[key] = defaults[key]
}
})
if (Object.keys(configsToReset).length === 0) {
return this.error(ctx, '没有找到可重置的配置', 400)
}
const result = await this.siteConfigService.setMany(configsToReset)
this.success(ctx, {
...result,
resetKeys: Object.keys(configsToReset)
}, '配置重置成功')
} else {
// 重置所有配置到默认值
const result = await this.siteConfigService.setMany(defaults)
this.success(ctx, {
...result,
resetKeys: Object.keys(defaults)
}, '所有配置重置成功')
}
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 导出配置
*/
async export(ctx) {
try {
const configs = await this.siteConfigService.getAll()
// 设置下载响应头
ctx.set('Content-Type', 'application/json')
ctx.set('Content-Disposition', `attachment; filename=site-config-${Date.now()}.json`)
ctx.body = JSON.stringify(configs, null, 2)
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
/**
* 导入配置
*/
async import(ctx) {
try {
const configs = this.getBody(ctx)
if (!configs || typeof configs !== 'object') {
return this.error(ctx, '导入的配置数据格式不正确', 400)
}
const result = await this.siteConfigService.setMany(configs)
this.success(ctx, result, '配置导入成功')
} catch (error) {
this.error(ctx, error.message, error.status || 400)
}
}
}
export default SiteConfigController

166
src/modules/site-config/models/SiteConfigModel.js

@ -0,0 +1,166 @@
/**
* 站点配置模型
* 处理站点配置数据的持久化操作
*/
import BaseModel from '../../../core/base/BaseModel.js'
class SiteConfigModel extends BaseModel {
constructor() {
super('site_config')
}
/**
* 根据配置键获取配置值
*/
async getByKey(key) {
const config = await this.findOne({ key })
return config ? config.value : null
}
/**
* 根据配置键设置配置值
*/
async setByKey(key, value) {
const existingConfig = await this.findOne({ key })
if (existingConfig) {
// 更新现有配置
return await this.updateById(existingConfig.id, { value, key })
} else {
// 创建新配置
return await this.create({ key, value })
}
}
/**
* 批量获取多个配置
*/
async getMany(keys) {
const configs = await this.query()
.whereIn('key', keys)
.select(['key', 'value'])
const result = {}
configs.forEach(config => {
result[config.key] = config.value
})
return result
}
/**
* 获取所有配置返回对象格式
*/
async getAllAsObject() {
const configs = await this.query()
.select(['key', 'value'])
const result = {}
configs.forEach(config => {
result[config.key] = config.value
})
return result
}
/**
* 批量设置配置
*/
async setMany(configData) {
const results = []
for (const [key, value] of Object.entries(configData)) {
const result = await this.setByKey(key, value)
results.push(result)
}
return results
}
/**
* 删除配置
*/
async deleteByKey(key) {
return await this.query()
.where('key', key)
.del()
}
/**
* 检查配置键是否存在
*/
async keyExists(key) {
return await this.exists({ key })
}
/**
* 搜索配置
*/
async searchConfigs(keyword) {
return await this.query()
.where('key', 'like', `%${keyword}%`)
.orWhere('value', 'like', `%${keyword}%`)
.select(['key', 'value', 'created_at', 'updated_at'])
}
/**
* 获取配置统计信息
*/
async getConfigStats() {
const [total, keyStats] = await Promise.all([
this.count(),
this.query()
.select('key')
.then(configs => {
const stats = {
byType: {},
byLength: {
short: 0, // 0-50字符
medium: 0, // 51-200字符
long: 0 // 200+字符
}
}
configs.forEach(config => {
const valueLength = String(config.value).length
// 按长度统计
if (valueLength <= 50) {
stats.byLength.short++
} else if (valueLength <= 200) {
stats.byLength.medium++
} else {
stats.byLength.long++
}
})
return stats
})
])
return {
total,
...keyStats
}
}
/**
* 获取基础站点配置
*/
async getBasicSiteConfig() {
const basicKeys = [
'site_title',
'site_author',
'site_author_avatar',
'site_description',
'site_logo',
'site_bg',
'keywords'
]
return await this.getMany(basicKeys)
}
}
export default SiteConfigModel

57
src/modules/site-config/routes.js

@ -0,0 +1,57 @@
/**
* 站点配置模块路由
* 定义站点配置相关的路由规则
*/
import Router from 'koa-router'
import SiteConfigController from './controllers/SiteConfigController.js'
const router = new Router({
prefix: '/api/site-config'
})
const siteConfigController = new SiteConfigController()
// 获取单个配置
router.get('/:key', siteConfigController.get.bind(siteConfigController))
// 设置单个配置
router.put('/:key', siteConfigController.set.bind(siteConfigController))
// 删除配置
router.delete('/:key', siteConfigController.delete.bind(siteConfigController))
// 获取多个配置
router.get('/', siteConfigController.getMany.bind(siteConfigController))
// 获取所有配置
router.get('/all/configs', siteConfigController.getAll.bind(siteConfigController))
// 批量设置配置
router.post('/', siteConfigController.setMany.bind(siteConfigController))
// 搜索配置
router.get('/search/configs', siteConfigController.search.bind(siteConfigController))
// 获取配置统计信息
router.get('/stats/info', siteConfigController.stats.bind(siteConfigController))
// 获取基础站点配置
router.get('/basic/config', siteConfigController.getBasic.bind(siteConfigController))
// 初始化默认配置
router.post('/init/defaults', siteConfigController.initDefaults.bind(siteConfigController))
// 获取默认配置(不操作数据库)
router.get('/defaults/config', siteConfigController.getDefaults.bind(siteConfigController))
// 重置配置到默认值
router.post('/reset/configs', siteConfigController.reset.bind(siteConfigController))
// 导出配置
router.get('/export/configs', siteConfigController.export.bind(siteConfigController))
// 导入配置
router.post('/import/configs', siteConfigController.import.bind(siteConfigController))
export default router

361
src/modules/site-config/services/SiteConfigService.js

@ -0,0 +1,361 @@
/**
* 站点配置服务
* 处理站点配置相关的业务逻辑
*/
import BaseService from '../../../core/base/BaseService.js'
import SiteConfigModel from '../models/SiteConfigModel.js'
import ValidationException from '../../../core/exceptions/ValidationException.js'
class SiteConfigService extends BaseService {
constructor() {
super()
this.siteConfigModel = new SiteConfigModel()
}
/**
* 获取单个配置值
*/
async get(key) {
try {
if (!key || key.trim() === '') {
throw new ValidationException('配置键不能为空')
}
const value = await this.siteConfigModel.getByKey(key.trim())
this.log('获取配置', { key, found: value !== null })
return value
} catch (error) {
this.log('获取配置失败', { key, error: error.message })
throw error
}
}
/**
* 设置单个配置值
*/
async set(key, value) {
try {
if (!key || key.trim() === '') {
throw new ValidationException('配置键不能为空')
}
if (value === undefined || value === null) {
throw new ValidationException('配置值不能为空')
}
// 验证配置值
this.validateConfigValue(key, value)
const result = await this.siteConfigModel.setByKey(key.trim(), value)
this.log('设置配置', { key, value })
return result
} catch (error) {
this.log('设置配置失败', { key, value, error: error.message })
throw error
}
}
/**
* 批量获取多个配置
*/
async getMany(keys) {
try {
if (!Array.isArray(keys) || keys.length === 0) {
throw new ValidationException('配置键列表不能为空')
}
// 过滤空值并去重
const validKeys = [...new Set(keys.filter(key => key && key.trim() !== ''))]
if (validKeys.length === 0) {
throw new ValidationException('没有有效的配置键')
}
const configs = await this.siteConfigModel.getMany(validKeys)
this.log('批量获取配置', { keysCount: validKeys.length, resultCount: Object.keys(configs).length })
return configs
} catch (error) {
this.log('批量获取配置失败', { keys, error: error.message })
throw error
}
}
/**
* 获取所有配置
*/
async getAll() {
try {
const configs = await this.siteConfigModel.getAllAsObject()
this.log('获取所有配置', { count: Object.keys(configs).length })
return configs
} catch (error) {
this.log('获取所有配置失败', { error: error.message })
throw error
}
}
/**
* 批量设置配置
*/
async setMany(configs) {
try {
if (!configs || typeof configs !== 'object') {
throw new ValidationException('配置数据格式不正确')
}
const keys = Object.keys(configs)
if (keys.length === 0) {
throw new ValidationException('配置数据不能为空')
}
const results = []
const errors = []
for (const [key, value] of Object.entries(configs)) {
try {
await this.set(key, value)
results.push(key)
} catch (error) {
errors.push({
key,
value,
error: error.message
})
}
}
this.log('批量设置配置', {
total: keys.length,
success: results.length,
errors: errors.length
})
return {
success: results,
errors,
total: keys.length,
successCount: results.length,
errorCount: errors.length
}
} catch (error) {
this.log('批量设置配置失败', { configs, error: error.message })
throw error
}
}
/**
* 删除配置
*/
async delete(key) {
try {
if (!key || key.trim() === '') {
throw new ValidationException('配置键不能为空')
}
// 检查配置是否存在
const exists = await this.siteConfigModel.keyExists(key.trim())
if (!exists) {
throw new ValidationException('配置不存在')
}
const result = await this.siteConfigModel.deleteByKey(key.trim())
this.log('删除配置', { key, deleted: result > 0 })
return { message: '配置删除成功', deleted: result > 0 }
} catch (error) {
this.log('删除配置失败', { key, error: error.message })
throw error
}
}
/**
* 搜索配置
*/
async search(keyword) {
try {
if (!keyword || keyword.trim() === '') {
return await this.getAll()
}
const configs = await this.siteConfigModel.searchConfigs(keyword.trim())
// 转换为对象格式
const result = {}
configs.forEach(config => {
result[config.key] = config.value
})
this.log('搜索配置', { keyword, resultCount: configs.length })
return result
} catch (error) {
this.log('搜索配置失败', { keyword, error: error.message })
throw error
}
}
/**
* 获取配置统计信息
*/
async getStats() {
try {
const stats = await this.siteConfigModel.getConfigStats()
this.log('获取配置统计', stats)
return stats
} catch (error) {
this.log('获取配置统计失败', { error: error.message })
throw error
}
}
/**
* 获取基础站点配置
*/
async getBasicConfig() {
try {
const config = await this.siteConfigModel.getBasicSiteConfig()
this.log('获取基础站点配置', { configCount: Object.keys(config).length })
return config
} catch (error) {
this.log('获取基础站点配置失败', { error: error.message })
throw error
}
}
/**
* 验证配置值
*/
validateConfigValue(key, value) {
try {
// 根据不同的配置键进行不同的验证
switch (key) {
case 'site_title':
case 'site_author':
if (typeof value !== 'string' || value.trim().length === 0) {
throw new ValidationException(`${key} 必须是有效的字符串`)
}
break
case 'site_description':
case 'keywords':
if (typeof value !== 'string') {
throw new ValidationException(`${key} 必须是字符串`)
}
break
case 'site_url':
try {
new URL(value)
} catch {
throw new ValidationException('站点URL格式不正确')
}
break
case 'posts_per_page':
const num = parseInt(value)
if (isNaN(num) || num < 1 || num > 100) {
throw new ValidationException('每页文章数必须是1-100之间的数字')
}
break
case 'enable_comments':
if (typeof value !== 'boolean' && !['true', 'false', '1', '0'].includes(String(value))) {
throw new ValidationException('评论开关必须是布尔值')
}
break
default:
// 对于其他配置,只做基本类型检查
if (value === undefined || value === null) {
throw new ValidationException('配置值不能为空')
}
}
return true
} catch (error) {
throw error
}
}
/**
* 获取默认配置
*/
getDefaultConfigs() {
return {
site_title: '我的网站',
site_author: '站点管理员',
site_description: '一个基于Koa3的现代化网站',
site_url: 'http://localhost:3000',
site_logo: '/static/logo.png',
site_bg: '/static/bg.jpg',
keywords: 'blog,koa3,javascript',
posts_per_page: 10,
enable_comments: true,
theme: 'default',
language: 'zh-CN',
timezone: 'Asia/Shanghai'
}
}
/**
* 初始化默认配置
*/
async initializeDefaults() {
try {
const defaultConfigs = this.getDefaultConfigs()
const existingConfigs = await this.getAll()
const configsToSet = {}
Object.entries(defaultConfigs).forEach(([key, value]) => {
if (!(key in existingConfigs)) {
configsToSet[key] = value
}
})
if (Object.keys(configsToSet).length > 0) {
await this.setMany(configsToSet)
this.log('初始化默认配置', { initialized: Object.keys(configsToSet) })
return {
message: '默认配置初始化成功',
initialized: Object.keys(configsToSet)
}
}
return {
message: '所有默认配置已存在',
initialized: []
}
} catch (error) {
this.log('初始化默认配置失败', { error: error.message })
throw error
}
}
}
export default SiteConfigService

651
src/presentation/routes/web.js

@ -7,24 +7,129 @@ import Router from 'koa-router'
const router = new Router()
// 服务懒加载函数
let _articleService, _userService, _siteConfigService, _authService
const getArticleService = async () => {
if (!_articleService) {
const { default: ArticleService } = await import('../../modules/article/services/ArticleService.js')
_articleService = new ArticleService()
}
return _articleService
}
const getUserService = async () => {
if (!_userService) {
const { default: UserService } = await import('../../modules/user/services/UserService.js')
_userService = new UserService()
}
return _userService
}
const getSiteConfigService = async () => {
if (!_siteConfigService) {
const { default: SiteConfigService } = await import('../../modules/site-config/services/SiteConfigService.js')
_siteConfigService = new SiteConfigService()
}
return _siteConfigService
}
const getAuthService = async () => {
if (!_authService) {
const { default: AuthService } = await import('../../modules/auth/services/AuthService.js')
_authService = new AuthService()
}
return _authService
}
/**
* 首页
*/
router.get('/', async (ctx) => {
await ctx.render('page/index', {
title: 'Koa3 Demo - 首页',
message: '欢迎使用 Koa3 Demo 应用'
})
try {
// 按需获取服务
const articleService = await getArticleService()
const siteConfigService = await getSiteConfigService()
// 获取最新文章
const articles = await articleService.getRecentArticles({ limit: 6 })
// 获取用户统计
// const userStats = await getUserService().then(service => service.getUserStats())
// 获取文章统计
// const articleStats = await articleService.getStats()
// 获取站点基本配置
const siteConfig = await siteConfigService.getBasicConfig()
await ctx.render('page/index/index', {
title: `${siteConfig.site_name || 'Koa3 Demo'} - 首页`,
message: siteConfig.site_description || '欢迎使用 Koa3 Demo 应用',
articles: articles || [],
// userStats,
// articleStats,
siteConfig,
apiList: [
{
name: '用户管理 API',
desc: '用户注册、登录、信息管理',
url: '/api/users'
},
{
name: '文章管理 API',
desc: '文章创建、编辑、发布管理',
url: '/api/articles'
},
{
name: '站点配置 API',
desc: '站点设置和配置管理',
url: '/api/site-config'
}
],
blogs: articles.slice(0, 4) || [],
collections: []
})
} catch (error) {
console.error('首页数据加载失败:', error)
await ctx.render('page/index/index', {
title: 'Koa3 Demo - 首页',
message: '欢迎使用 Koa3 Demo 应用',
articles: [],
userStats: {},
articleStats: {},
siteConfig: {},
apiList: [],
blogs: [],
collections: []
})
}
})
/**
* 关于页面
*/
router.get('/about', async (ctx) => {
await ctx.render('page/about', {
title: 'Koa3 Demo - 关于',
description: '这是一个基于 Koa3 的示例应用'
})
try {
// 按需获取服务
const siteConfigService = await getSiteConfigService()
// 获取站点基本配置
const siteConfig = await siteConfigService.getBasicConfig()
await ctx.render('page/about/index', {
title: `${siteConfig.site_name || 'Koa3 Demo'} - 关于`,
description: siteConfig.site_description || '这是一个基于 Koa3 的示例应用',
siteConfig
})
} catch (error) {
console.error('关于页面数据加载失败:', error)
await ctx.render('page/about/index', {
title: 'Koa3 Demo - 关于',
description: '这是一个基于 Koa3 的示例应用',
siteConfig: {}
})
}
})
/**
@ -37,7 +142,7 @@ router.get('/login', async (ctx) => {
return
}
await ctx.render('page/login', {
await ctx.render('page/login/index', {
title: 'Koa3 Demo - 登录'
})
})
@ -52,7 +157,7 @@ router.get('/register', async (ctx) => {
return
}
await ctx.render('page/register', {
await ctx.render('page/register/index', {
title: 'Koa3 Demo - 注册'
})
})
@ -67,31 +172,137 @@ router.get('/profile', async (ctx) => {
return
}
await ctx.render('page/profile', {
title: 'Koa3 Demo - 个人资料',
user: ctx.session.user
})
try {
// 按需获取服务
const authService = await getAuthService()
const articleService = await getArticleService()
// 获取用户详细信息
const userProfile = await authService.getProfile(ctx.session.user.id)
// 获取用户的文章
const userArticles = await articleService.getArticles({
author: ctx.session.user.id,
limit: 10,
page: 1
})
await ctx.render('page/profile/index', {
title: 'Koa3 Demo - 个人资料',
user: userProfile,
userArticles: userArticles.data || [],
stats: {
articleCount: userArticles.pagination?.total || 0,
publishedCount: userArticles.data?.filter(a => a.status === 'published').length || 0
}
})
} catch (error) {
console.error('用户资料页面数据加载失败:', error)
await ctx.render('page/profile/index', {
title: 'Koa3 Demo - 个人资料',
user: ctx.session.user,
userArticles: [],
stats: {
articleCount: 0,
publishedCount: 0
}
})
}
})
/**
* 文章列表页面
*/
router.get('/articles', async (ctx) => {
await ctx.render('page/articles', {
title: 'Koa3 Demo - 文章列表'
})
try {
const { page = 1, category, search, status = 'published' } = ctx.query
// 按需获取服务
const articleService = await getArticleService()
// 获取文章列表
const result = await articleService.getArticles({
page: parseInt(page),
limit: 12,
category,
search,
status // 只显示已发布的文章
})
// 获取热门文章
const popularArticles = await articleService.getPopularArticles({ limit: 5 })
// 获取文章统计
const articleStats = await articleService.getStats()
await ctx.render('page/articles/index', {
title: 'Koa3 Demo - 文章列表',
articles: result.data || [],
pagination: result.pagination,
popularArticles: popularArticles.data || [],
articleStats,
currentPage: parseInt(page),
category,
search
})
} catch (error) {
console.error('文章列表页面数据加载失败:', error)
await ctx.render('page/articles/index', {
title: 'Koa3 Demo - 文章列表',
articles: [],
pagination: { total: 0, pages: 0, current: 1, limit: 12 },
popularArticles: [],
articleStats: {},
currentPage: 1,
category: null,
search: null
})
}
})
/**
* 文章详情页面
*/
router.get('/articles/:id', async (ctx) => {
const { id } = ctx.params
router.get('/articles/:slug', async (ctx) => {
try {
const { slug } = ctx.params
await ctx.render('page/article-detail', {
title: 'Koa3 Demo - 文章详情',
articleId: id
})
// 按需获取服务
const articleService = await getArticleService()
// 获取文章详情
const article = await articleService.getArticleBySlug(slug)
if (!article) {
ctx.status = 404
await ctx.render('error/404', {
title: 'Koa3 Demo - 文章未找到',
message: '您访问的文章不存在或已被删除'
})
return
}
const { id } = article
// 增加阅读量
await articleService.incrementViewCount(id)
// 获取相关文章
const relatedArticles = await articleService.getRelatedArticles(id, { limit: 5 })
await ctx.render('page/articles/article', {
title: `${article.title} - Koa3 Demo`,
article,
relatedArticles: relatedArticles.data || [],
articleId: id
})
} catch (error) {
console.error('文章详情页面数据加载失败:', error)
ctx.status = 500
await ctx.render('error/500', {
title: 'Koa3 Demo - 服务器错误',
message: '文章加载失败,请稍后再试'
})
}
})
/**
@ -104,10 +315,24 @@ router.get('/articles/create', async (ctx) => {
return
}
await ctx.render('page/article-create', {
title: 'Koa3 Demo - 创建文章',
user: ctx.session.user
})
try {
// 按需获取服务
const authService = await getAuthService()
// 获取用户信息
const userProfile = await authService.getProfile(ctx.session.user.id)
await ctx.render('page/articles/create', {
title: 'Koa3 Demo - 创建文章',
user: userProfile
})
} catch (error) {
console.error('创建文章页面数据加载失败:', error)
await ctx.render('page/articles/create', {
title: 'Koa3 Demo - 创建文章',
user: ctx.session.user
})
}
})
/**
@ -120,13 +345,52 @@ router.get('/articles/:id/edit', async (ctx) => {
return
}
const { id } = ctx.params
try {
const { id } = ctx.params
await ctx.render('page/article-edit', {
title: 'Koa3 Demo - 编辑文章',
articleId: id,
user: ctx.session.user
})
// 按需获取服务
const articleService = await getArticleService()
const authService = await getAuthService()
// 获取文章详情
const article = await articleService.getArticleById(id)
if (!article) {
ctx.status = 404
await ctx.render('error/404', {
title: 'Koa3 Demo - 文章未找到',
message: '您要编辑的文章不存在或已被删除'
})
return
}
// 检查是否为文章作者
if (article.author_id !== ctx.session.user.id) {
ctx.status = 403
await ctx.render('error/403', {
title: 'Koa3 Demo - 无权访问',
message: '您没有权限编辑这篇文章'
})
return
}
// 获取用户信息
const userProfile = await authService.getProfile(ctx.session.user.id)
await ctx.render('page/articles/edit', {
title: `编辑: ${article.title} - Koa3 Demo`,
article,
articleId: id,
user: userProfile
})
} catch (error) {
console.error('编辑文章页面数据加载失败:', error)
ctx.status = 500
await ctx.render('error/500', {
title: 'Koa3 Demo - 服务器错误',
message: '文章加载失败,请稍后再试'
})
}
})
/**
@ -139,12 +403,54 @@ router.get('/admin', async (ctx) => {
return
}
// 这里可以添加管理员权限检查
try {
// 按需获取服务
const authService = await getAuthService()
const userService = await getUserService()
const articleService = await getArticleService()
const siteConfigService = await getSiteConfigService()
await ctx.render('page/admin', {
title: 'Koa3 Demo - 管理后台',
user: ctx.session.user
})
// 获取用户信息
const userProfile = await authService.getProfile(ctx.session.user.id)
// 检查是否为管理员(可以根据实际需要调整权限检查逻辑)
if (userProfile.role !== 'admin') {
ctx.status = 403
await ctx.render('error/403', {
title: 'Koa3 Demo - 无权访问',
message: '您没有管理员权限'
})
return
}
// 获取统计数据
const [userStats, articleStats, siteConfig] = await Promise.all([
userService.getUserStats(),
articleService.getStats(),
siteConfigService.getBasicConfig()
])
await ctx.render('page/admin/dashboard', {
title: 'Koa3 Demo - 管理后台',
user: userProfile,
userStats,
articleStats,
siteConfig,
adminMenus: [
{ name: '用户管理', url: '/admin/users', icon: 'users' },
{ name: '文章管理', url: '/admin/articles', icon: 'article' },
{ name: '站点配置', url: '/admin/settings', icon: 'settings' },
{ name: '系统监控', url: '/admin/monitor', icon: 'monitor' }
]
})
} catch (error) {
console.error('管理后台页面数据加载失败:', error)
await ctx.render('page/index/index', {
title: 'Koa3 Demo - 管理后台',
message: '管理后台功能开发中...',
user: ctx.session.user
})
}
})
/**
@ -157,10 +463,269 @@ router.get('/admin/monitor', async (ctx) => {
return
}
await ctx.render('page/monitor', {
title: 'Koa3 Demo - 系统监控',
user: ctx.session.user
})
try {
// 按需获取服务
const authService = await getAuthService()
const userService = await getUserService()
const articleService = await getArticleService()
// 获取用户信息
const userProfile = await authService.getProfile(ctx.session.user.id)
// 检查管理员权限
if (userProfile.role !== 'admin') {
ctx.status = 403
await ctx.render('error/403', {
title: 'Koa3 Demo - 无权访问',
message: '您没有管理员权限'
})
return
}
// 获取系统监控数据
const [userStats, articleStats] = await Promise.all([
userService.getUserStats(),
articleService.getStats()
])
// 模拟系统监控数据
const systemMonitor = {
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.version,
platform: process.platform,
timestamp: new Date().toISOString()
}
await ctx.render('page/admin/monitor', {
title: 'Koa3 Demo - 系统监控',
user: userProfile,
userStats,
articleStats,
systemMonitor
})
} catch (error) {
console.error('系统监控页面数据加载失败:', error)
await ctx.render('page/index/index', {
title: 'Koa3 Demo - 系统监控',
message: '系统监控功能开发中...',
user: ctx.session.user
})
}
})
/**
* 文章搜索页面
*/
router.get('/search', async (ctx) => {
try {
const { q: query, page = 1, type = 'article' } = ctx.query
if (!query) {
await ctx.render('page/search/index', {
title: 'Koa3 Demo - 搜索',
query: '',
results: [],
pagination: { total: 0, pages: 0, current: 1, limit: 10 },
type
})
return
}
let results = { data: [], pagination: { total: 0, pages: 0, current: 1, limit: 10 } }
if (type === 'article') {
const articleService = await getArticleService()
results = await articleService.searchArticles(query, {
page: parseInt(page),
limit: 10,
status: 'published'
})
} else if (type === 'user') {
const userService = await getUserService()
results = await userService.searchUsers(query, {
page: parseInt(page),
limit: 10
})
}
await ctx.render('page/search/index', {
title: `搜索: ${query} - Koa3 Demo`,
query,
results: results.data || [],
pagination: results.pagination,
type,
currentPage: parseInt(page)
})
} catch (error) {
console.error('搜索页面数据加载失败:', error)
await ctx.render('page/search/index', {
title: 'Koa3 Demo - 搜索',
query: ctx.query.q || '',
results: [],
pagination: { total: 0, pages: 0, current: 1, limit: 10 },
type: ctx.query.type || 'article',
currentPage: 1
})
}
})
/**
* 管理员 - 用户管理页面
*/
router.get('/admin/users', async (ctx) => {
// 检查登录状态和权限
if (!ctx.session.user) {
ctx.redirect('/login')
return
}
try {
// 按需获取服务
const authService = await getAuthService()
const userService = await getUserService()
const userProfile = await authService.getProfile(ctx.session.user.id)
if (userProfile.role !== 'admin') {
ctx.status = 403
await ctx.render('error/403', {
title: 'Koa3 Demo - 无权访问',
message: '您没有管理员权限'
})
return
}
const { page = 1, search, status } = ctx.query
const result = await userService.getUsers({
page: parseInt(page),
limit: 20,
search,
status
})
const userStats = await userService.getUserStats()
await ctx.render('page/admin/users', {
title: 'Koa3 Demo - 用户管理',
users: result.data || [],
pagination: result.pagination,
userStats,
currentPage: parseInt(page),
search,
status
})
} catch (error) {
console.error('用户管理页面数据加载失败:', error)
ctx.status = 500
await ctx.render('error/500', {
title: 'Koa3 Demo - 服务器错误',
message: '页面加载失败'
})
}
})
/**
* 管理员 - 文章管理页面
*/
router.get('/admin/articles', async (ctx) => {
// 检查登录状态和权限
if (!ctx.session.user) {
ctx.redirect('/login')
return
}
try {
// 按需获取服务
const authService = await getAuthService()
const articleService = await getArticleService()
const userProfile = await authService.getProfile(ctx.session.user.id)
if (userProfile.role !== 'admin') {
ctx.status = 403
await ctx.render('error/403', {
title: 'Koa3 Demo - 无权访问',
message: '您没有管理员权限'
})
return
}
const { page = 1, search, status, category } = ctx.query
const result = await articleService.getArticles({
page: parseInt(page),
limit: 20,
search,
status,
category
})
const articleStats = await articleService.getStats()
await ctx.render('page/admin/articles', {
title: 'Koa3 Demo - 文章管理',
articles: result.data || [],
pagination: result.pagination,
articleStats,
currentPage: parseInt(page),
search,
status,
category
})
} catch (error) {
console.error('文章管理页面数据加载失败:', error)
ctx.status = 500
await ctx.render('error/500', {
title: 'Koa3 Demo - 服务器错误',
message: '页面加载失败'
})
}
})
/**
* 管理员 - 站点设置页面
*/
router.get('/admin/settings', async (ctx) => {
// 检查登录状态和权限
if (!ctx.session.user) {
ctx.redirect('/login')
return
}
try {
// 按需获取服务
const authService = await getAuthService()
const siteConfigService = await getSiteConfigService()
const userProfile = await authService.getProfile(ctx.session.user.id)
if (userProfile.role !== 'admin') {
ctx.status = 403
await ctx.render('error/403', {
title: 'Koa3 Demo - 无权访问',
message: '您没有管理员权限'
})
return
}
const siteConfigs = await siteConfigService.getAll()
const configStats = await siteConfigService.getStats()
await ctx.render('page/admin/settings', {
title: 'Koa3 Demo - 站点设置',
configs: siteConfigs,
configStats
})
} catch (error) {
console.error('站点设置页面数据加载失败:', error)
ctx.status = 500
await ctx.render('error/500', {
title: 'Koa3 Demo - 服务器错误',
message: '页面加载失败'
})
}
})
/**
@ -168,7 +733,7 @@ router.get('/admin/monitor', async (ctx) => {
*/
router.get('/404', async (ctx) => {
ctx.status = 404
await ctx.render('error/404', {
await ctx.render('error/index', {
title: 'Koa3 Demo - 页面未找到'
})
})

37
src/presentation/views/page/articles/create.pug

@ -0,0 +1,37 @@
extends ../../layouts/page
block content
.container
.row
.col-12
h1.mb-4 创建文章
.card
.card-body
form#articleForm
.mb-3
label(for="title").form-label 文章标题
input#title.form-control(type="text" required)
.mb-3
label(for="content").form-label 文章内容
textarea#content.form-control(rows="10" required)
.mb-3
label(for="category").form-label 分类
select#category.form-select
option(value="") 请选择分类
option(value="tech") 技术
option(value="life") 生活
option(value="other") 其他
.mb-3
label(for="tags").form-label 标签
input#tags.form-control(type="text" placeholder="用逗号分隔多个标签")
.d-flex.gap-2
button.btn.btn-primary(type="submit") 发布文章
a.btn.btn-secondary(href="/articles") 取消
block scripts
script.
document.getElementById('articleForm').addEventListener('submit', async (e) => {
e.preventDefault();
// TODO: 实现文章创建功能
alert('文章创建功能开发中...');
});

37
src/presentation/views/page/articles/edit.pug

@ -0,0 +1,37 @@
extends ../../layouts/page
block content
.container
.row
.col-12
h1.mb-4 编辑文章
.card
.card-body
form#articleEditForm
.mb-3
label(for="title").form-label 文章标题
input#title.form-control(type="text" value="示例文章标题" required)
.mb-3
label(for="content").form-label 文章内容
textarea#content.form-control(rows="10" required) 这里是示例文章内容...
.mb-3
label(for="category").form-label 分类
select#category.form-select
option(value="") 请选择分类
option(value="tech" selected) 技术
option(value="life") 生活
option(value="other") 其他
.mb-3
label(for="tags").form-label 标签
input#tags.form-control(type="text" value="技术,编程" placeholder="用逗号分隔多个标签")
.d-flex.gap-2
button.btn.btn-primary(type="submit") 更新文章
a.btn.btn-secondary(href="/articles") 取消
block scripts
script.
document.getElementById('articleEditForm').addEventListener('submit', async (e) => {
e.preventDefault();
// TODO: 实现文章编辑功能
alert('文章编辑功能开发中...');
});

6
src/shared/helpers/routeHelper.js

@ -71,6 +71,12 @@ export async function autoRegisterControllers(app, controllersDir) {
*/
async function scanDirectory(dir, registeredRoutes, log, modulePrefix = '') {
try {
// 检查目录是否存在
if (!fs.existsSync(dir)) {
log.debug(`[目录扫描] 📁 跳过不存在的目录: ${dir}`)
return
}
const files = fs.readdirSync(dir)
for (const file of files) {

Loading…
Cancel
Save