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时会谈提示是否重置数据
|
|||
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() |
|||
@ -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