Browse Source
- 视图中间件职责纯粹化,仅负责模板渲染及基础上下文处理 - 新增上下文中间件,注入全局应用配置、站点配置及用户信息,支持缓存优化 - 调整中间件注册流程,区分核心中间件(路由前)与后置中间件(路由后) - 重构数据库初始化脚本,集成DatabaseProvider,支持迁移和数据重置交互 - 移除旧的knex配置文件,统一数据库配置,确保连接配置正确 - 添加数据库工具脚本,支持迁移文件与种子数据文件的生成与管理 - 增加站点配置模块,提供丰富接口支持配置管理和基础配置注入 - 完善静态资源中间件,优化文件路径处理及错误管理refactor
26 changed files with 2336 additions and 175 deletions
@ -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 路由不再是空壳,而是具有完整功能的页面路由系统。 |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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` 常量 |
||||
@ -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 模式提高并发
|
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
@ -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() |
||||
@ -1,23 +1,69 @@ |
|||||
const { execSync } = require('child_process'); |
/** |
||||
const readline = require('readline'); |
* 数据库初始化脚本 |
||||
|
* 直接使用 DatabaseProvider 进行迁移和种子数据操作 |
||||
|
* 避免重复配置 |
||||
|
*/ |
||||
|
|
||||
// 写一个执行npm run migrate && npm run seed的脚本,当执行npm run seed时会谈提示是否重置数据
|
import { createInterface } from 'readline' |
||||
function run(command) { |
import DatabaseProvider from '../src/app/providers/DatabaseProvider.js' |
||||
execSync(command, { stdio: 'inherit' }); |
|
||||
} |
|
||||
|
|
||||
run('npx knex migrate:latest'); |
|
||||
|
|
||||
const rl = readline.createInterface({ |
/** |
||||
input: process.stdin, |
* 询问用户是否重置数据 |
||||
output: process.stdout |
* @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') |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
rl.question('是否重置数据?(y/N): ', (answer) => { |
/** |
||||
if (answer.trim().toLowerCase() === 'y') { |
* 主初始化函数 |
||||
run('npx knex seed:run'); |
*/ |
||||
} else { |
async function init() { |
||||
console.log('已取消数据重置。'); |
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() |
||||
|
|
||||
|
if (shouldReset) { |
||||
|
console.log('\n🌱 运行种子数据...') |
||||
|
await DatabaseProvider.runSeeds() |
||||
|
} 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() |
||||
@ -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() |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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('文章创建功能开发中...'); |
||||
|
}); |
||||
@ -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('文章编辑功能开发中...'); |
||||
|
}); |
||||
Loading…
Reference in new issue