Browse Source
- 新建 app、modules、shared、presentation、infrastructure 五个顶级目录 - 将配置、核心引导和日志迁移至 app 目录 - 按业务领域划分模块,controllers、services、models 同步重组到 modules 内 - 共享工具、常量和基础类统一放入 shared 目录 - 中间件、路由和视图迁移至 presentation 目录下 - 数据库和定时任务迁移至 infrastructure 目录管理 - 新增数据库服务提供者封装连接和缓存逻辑 - 更新所有相关导入路径适配新目录结构 - 删除废弃的老目录和控制器代码 - 重构 main.js 异步启动服务器逻辑,完成插件注册和日志打印 - 保持原有 MVC 分层与路由自动发现机制,提升代码组织和维护性re
194 changed files with 8381 additions and 8283 deletions
@ -0,0 +1,330 @@ |
|||
# src 目录整理设计 |
|||
|
|||
## 概述 |
|||
|
|||
本设计旨在优化 koa3-demo 项目的 src 目录结构,在保持现有 MVC 架构不变的前提下,通过最小化的代码改动来提升代码组织的清晰度和可维护性。 |
|||
|
|||
## 当前目录结构分析 |
|||
|
|||
### 现状问题 |
|||
- 目录层级较为扁平,业务模块分散 |
|||
- controllers 按类型(Api/Page)分组,但缺乏按业务领域组织 |
|||
- services 和 models 分离在不同的顶级目录 |
|||
- 缺乏统一的模块组织规范 |
|||
|
|||
### 优势保持 |
|||
- MVC 架构清晰,分层明确 |
|||
- 中间件系统完善 |
|||
- 路由自动发现机制完整 |
|||
- 数据库操作层设计合理 |
|||
|
|||
## 整理方案 |
|||
|
|||
### 目标架构 |
|||
|
|||
采用**领域驱动的模块化架构**,在保持 MVC 分层的同时,按业务领域组织代码: |
|||
|
|||
```mermaid |
|||
graph TB |
|||
subgraph "src 目录结构" |
|||
A[src/] --> B[app/] |
|||
A --> C[modules/] |
|||
A --> D[shared/] |
|||
A --> E[presentation/] |
|||
|
|||
B --> B1[config/] |
|||
B --> B2[providers/] |
|||
B --> B3[bootstrap/] |
|||
|
|||
C --> C1[auth/] |
|||
C --> C2[article/] |
|||
C --> C3[user/] |
|||
C --> C4[site-config/] |
|||
|
|||
D --> D1[utils/] |
|||
D --> D2[constants/] |
|||
D --> D3[helpers/] |
|||
|
|||
E --> E1[middlewares/] |
|||
E --> E2[routes/] |
|||
E --> E3[views/] |
|||
|
|||
subgraph "模块内部结构" |
|||
C1 --> C1A[controllers/] |
|||
C1 --> C1B[services/] |
|||
C1 --> C1C[models/] |
|||
C1 --> C1D[validators/] |
|||
end |
|||
end |
|||
``` |
|||
|
|||
### 详细目录规划 |
|||
|
|||
#### 1. app/ - 应用核心 |
|||
``` |
|||
app/ |
|||
├── config/ # 配置管理 |
|||
│ ├── index.js # 当前 src/config/index.js |
|||
│ └── database.js # 数据库配置(从 db/ 提取) |
|||
├── providers/ # 服务提供者 |
|||
│ ├── DatabaseProvider.js # 数据库连接管理 |
|||
│ └── index.js # 提供者注册 |
|||
└── bootstrap/ # 应用引导 |
|||
├── app.js # 应用实例(当前 global.js) |
|||
├── logger.js # 日志配置 |
|||
└── index.js # 引导入口 |
|||
``` |
|||
|
|||
#### 2. modules/ - 业务模块 |
|||
``` |
|||
modules/ |
|||
├── auth/ # 认证模块 |
|||
│ ├── controllers/ |
|||
│ │ ├── AuthController.js # API 控制器 |
|||
│ │ └── AuthPageController.js # 页面控制器 |
|||
│ ├── services/ |
|||
│ │ └── AuthService.js # 从 userService.js 提取认证相关 |
|||
│ ├── models/ |
|||
│ │ └── UserModel.js # 用户模型 |
|||
│ └── validators/ |
|||
│ └── AuthValidator.js # 认证验证规则 |
|||
├── user/ # 用户管理模块 |
|||
│ ├── controllers/ |
|||
│ │ └── ProfileController.js # 用户资料控制器 |
|||
│ ├── services/ |
|||
│ │ └── UserService.js # 用户业务服务 |
|||
│ └── models/ |
|||
│ └── UserModel.js # 用户模型(共享) |
|||
├── article/ # 文章管理模块 |
|||
│ ├── controllers/ |
|||
│ │ └── ArticleController.js |
|||
│ ├── services/ |
|||
│ │ └── ArticleService.js |
|||
│ └── models/ |
|||
│ └── ArticleModel.js |
|||
├── bookmark/ # 书签管理模块 |
|||
│ ├── services/ |
|||
│ │ └── BookmarkService.js |
|||
│ └── models/ |
|||
│ └── BookmarkModel.js |
|||
├── site-config/ # 站点配置模块 |
|||
│ ├── services/ |
|||
│ │ └── SiteConfigService.js |
|||
│ └── models/ |
|||
│ └── SiteConfigModel.js |
|||
└── common/ # 通用模块 |
|||
├── controllers/ |
|||
│ ├── ApiController.js |
|||
│ ├── StatusController.js |
|||
│ └── UploadController.js |
|||
└── services/ |
|||
└── JobService.js |
|||
``` |
|||
|
|||
#### 3. shared/ - 共享资源 |
|||
``` |
|||
shared/ |
|||
├── utils/ # 工具函数 |
|||
│ ├── error/ |
|||
│ ├── router/ |
|||
│ ├── bcrypt.js |
|||
│ ├── helper.js |
|||
│ └── scheduler.js |
|||
├── constants/ # 常量定义 |
|||
│ ├── errors.js |
|||
│ └── status.js |
|||
└── base/ # 基础类 |
|||
├── BaseController.js |
|||
├── BaseService.js |
|||
└── BaseModel.js |
|||
``` |
|||
|
|||
#### 4. presentation/ - 表现层 |
|||
``` |
|||
presentation/ |
|||
├── middlewares/ # 中间件(当前 middlewares/) |
|||
├── routes/ # 路由管理 |
|||
│ ├── api.js # API 路由 |
|||
│ ├── web.js # 页面路由 |
|||
│ └── index.js # 路由注册 |
|||
└── views/ # 视图模板(当前 views/) |
|||
``` |
|||
|
|||
#### 5. infrastructure/ - 基础设施 |
|||
``` |
|||
infrastructure/ |
|||
├── database/ # 数据库相关 |
|||
│ ├── migrations/ # 迁移文件 |
|||
│ ├── seeds/ # 种子数据 |
|||
│ ├── docs/ # 数据库文档 |
|||
│ └── index.js # 数据库连接 |
|||
└── jobs/ # 定时任务 |
|||
├── exampleJob.js |
|||
└── index.js |
|||
``` |
|||
|
|||
## 迁移策略 |
|||
|
|||
### 阶段一:创建新目录结构 |
|||
1. 创建 `app/`, `modules/`, `shared/`, `presentation/`, `infrastructure/` 目录 |
|||
2. 建立各模块的子目录结构 |
|||
|
|||
### 阶段二:文件迁移与重组 |
|||
1. **配置文件迁移** |
|||
- `config/` → `app/config/` |
|||
- 从 `db/index.js` 提取数据库配置到 `app/config/database.js` |
|||
|
|||
2. **核心文件重组** |
|||
- `global.js` → `app/bootstrap/app.js` |
|||
- `logger.js` → `app/bootstrap/logger.js` |
|||
- 创建 `app/bootstrap/index.js` 统一引导 |
|||
|
|||
3. **业务模块化** |
|||
- 按业务领域创建模块目录 |
|||
- 将相关的 controllers, services, models 组织到对应模块 |
|||
|
|||
4. **基础设施迁移** |
|||
- `db/` → `infrastructure/database/` |
|||
- `jobs/` → `infrastructure/jobs/` |
|||
- `middlewares/` → `presentation/middlewares/` |
|||
- `views/` → `presentation/views/` |
|||
|
|||
### 阶段三:更新引用路径 |
|||
1. 更新 `main.js` 中的导入路径 |
|||
2. 更新中间件注册路径 |
|||
3. 更新服务间的依赖引用 |
|||
4. 更新路由自动发现配置 |
|||
|
|||
## 实现细节 |
|||
|
|||
### 模块内 MVC 组织 |
|||
|
|||
每个业务模块内部采用标准的 MVC 分层: |
|||
|
|||
```mermaid |
|||
graph LR |
|||
subgraph "auth 模块" |
|||
AC[AuthController] --> AS[AuthService] |
|||
AS --> UM[UserModel] |
|||
APC[AuthPageController] --> AS |
|||
end |
|||
|
|||
subgraph "共享层" |
|||
BC[BaseController] |
|||
BS[BaseService] |
|||
BM[BaseModel] |
|||
end |
|||
|
|||
AC -.-> BC |
|||
AS -.-> BS |
|||
UM -.-> BM |
|||
``` |
|||
|
|||
### 路径映射表 |
|||
|
|||
| 当前路径 | 新路径 | 说明 | |
|||
|---------|-------|------| |
|||
| `src/config/` | `src/app/config/` | 配置管理 | |
|||
| `src/global.js` | `src/app/bootstrap/app.js` | 应用实例 | |
|||
| `src/logger.js` | `src/app/bootstrap/logger.js` | 日志配置 | |
|||
| `src/controllers/Api/AuthController.js` | `src/modules/auth/controllers/AuthController.js` | 认证API控制器 | |
|||
| `src/controllers/Page/AuthPageController.js` | `src/modules/auth/controllers/AuthPageController.js` | 认证页面控制器 | |
|||
| `src/services/userService.js` | `src/modules/auth/services/AuthService.js` + `src/modules/user/services/UserService.js` | 拆分认证和用户服务 | |
|||
| `src/db/models/UserModel.js` | `src/modules/auth/models/UserModel.js` | 用户模型 | |
|||
| `src/middlewares/` | `src/presentation/middlewares/` | 中间件 | |
|||
| `src/views/` | `src/presentation/views/` | 视图模板 | |
|||
| `src/db/migrations/` | `src/infrastructure/database/migrations/` | 数据库迁移 | |
|||
| `src/jobs/` | `src/infrastructure/jobs/` | 定时任务 | |
|||
|
|||
### 导入路径更新 |
|||
|
|||
#### main.js 更新示例 |
|||
```javascript |
|||
// 原始导入 |
|||
import { app } from "./global" |
|||
import { logger } from "./logger.js" |
|||
import "./jobs/index.js" |
|||
import LoadMiddlewares from "./middlewares/install.js" |
|||
|
|||
// 更新后导入 |
|||
import { app } from "./app/bootstrap/app.js" |
|||
import { logger } from "./app/bootstrap/logger.js" |
|||
import "./infrastructure/jobs/index.js" |
|||
import LoadMiddlewares from "./presentation/middlewares/install.js" |
|||
``` |
|||
|
|||
### 自动路由发现适配 |
|||
|
|||
更新路由扫描配置以适配新的模块结构: |
|||
|
|||
```javascript |
|||
// 扫描路径配置 |
|||
const scanPaths = [ |
|||
'src/modules/*/controllers/**/*.js', |
|||
'src/modules/common/controllers/**/*.js' |
|||
]; |
|||
``` |
|||
|
|||
## 文件操作清单 |
|||
|
|||
### 需要移动的文件 |
|||
- [ ] `src/config/` → `src/app/config/` |
|||
- [ ] `src/global.js` → `src/app/bootstrap/app.js` |
|||
- [ ] `src/logger.js` → `src/app/bootstrap/logger.js` |
|||
- [ ] `src/db/` → `src/infrastructure/database/` |
|||
- [ ] `src/jobs/` → `src/infrastructure/jobs/` |
|||
- [ ] `src/middlewares/` → `src/presentation/middlewares/` |
|||
- [ ] `src/views/` → `src/presentation/views/` |
|||
- [ ] `src/utils/` → `src/shared/utils/` |
|||
|
|||
### 需要按模块重组的文件 |
|||
- [ ] 认证相关:`AuthController.js`, `AuthPageController.js`, `userService.js`(部分), `UserModel.js` |
|||
- [ ] 用户管理:`ProfileController.js`, `userService.js`(部分) |
|||
- [ ] 文章管理:`ArticleController.js`, `ArticleService.js`, `ArticleModel.js` |
|||
- [ ] 书签管理:`BookmarkService.js`, `BookmarkModel.js` |
|||
- [ ] 站点配置:`SiteConfigService.js`, `SiteConfigModel.js` |
|||
- [ ] 通用功能:`ApiController.js`, `StatusController.js`, `UploadController.js`, `JobController.js` |
|||
|
|||
### 需要更新导入的文件 |
|||
- [ ] `src/main.js` |
|||
- [ ] `src/presentation/middlewares/install.js` |
|||
- [ ] 各个控制器文件中的服务导入 |
|||
- [ ] 各个服务文件中的模型导入 |
|||
|
|||
## 预期效果 |
|||
|
|||
### 代码组织改善 |
|||
1. **业务内聚性**:相关的 controller、service、model 集中在同一模块 |
|||
2. **职责清晰**:每个模块负责特定的业务领域 |
|||
3. **依赖明确**:模块间依赖关系更加清晰可见 |
|||
|
|||
### 开发体验提升 |
|||
1. **文件查找**:按业务功能快速定位相关文件 |
|||
2. **代码维护**:修改某个功能时,相关文件集中在一个目录 |
|||
3. **团队协作**:不同开发者可以专注于不同的业务模块 |
|||
|
|||
### 架构扩展性 |
|||
1. **新增模块**:按照统一规范快速创建新的业务模块 |
|||
2. **功能拆分**:大模块可以进一步拆分成子模块 |
|||
3. **微服务准备**:为将来可能的微服务拆分做好准备 |
|||
|
|||
## 风险评估与注意事项 |
|||
|
|||
### 低风险操作 |
|||
- 目录创建和文件移动 |
|||
- 路径引用更新 |
|||
- 配置文件调整 |
|||
|
|||
### 需要谨慎的操作 |
|||
- 路由自动发现逻辑调整 |
|||
- 中间件注册顺序维护 |
|||
- 数据库连接管理迁移 |
|||
|
|||
### 测试验证点 |
|||
1. 应用启动正常 |
|||
2. 路由注册成功 |
|||
3. 数据库连接正常 |
|||
4. 中间件功能完整 |
|||
5. 各业务模块功能正常 |
|||
|
|||
本方案通过最小化改动实现目录结构优化,保持原有架构优势的同时提升代码组织质量,为项目的长期维护和扩展奠定良好基础。 |
|||
@ -0,0 +1,27 @@ |
|||
/** |
|||
* 应用实例 |
|||
* |
|||
* 创建和配置 Koa 应用实例 |
|||
*/ |
|||
|
|||
import Koa from "koa"; |
|||
import { logger } from "./logger.js"; |
|||
import { validateEnvironment } from "../../shared/utils/envValidator.js"; |
|||
|
|||
// 启动前验证环境变量
|
|||
if (!validateEnvironment()) { |
|||
logger.error("环境变量验证失败,应用退出"); |
|||
process.exit(1); |
|||
} |
|||
|
|||
const app = new Koa({ asyncLocalStorage: true }); |
|||
|
|||
app.keys = []; |
|||
|
|||
// SESSION_SECRET 已通过环境变量验证确保存在
|
|||
process.env.SESSION_SECRET.split(",").forEach(secret => { |
|||
app.keys.push(secret.trim()); |
|||
}); |
|||
|
|||
export { app }; |
|||
export default app; |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 应用引导索引文件 |
|||
* |
|||
* 统一导出应用引导相关模块 |
|||
*/ |
|||
|
|||
import app from "./app.js"; |
|||
import { logger, jobLogger, errorLogger } from "./logger.js"; |
|||
|
|||
export { |
|||
app, |
|||
logger, |
|||
jobLogger, |
|||
errorLogger |
|||
}; |
|||
|
|||
export default { |
|||
app, |
|||
logger, |
|||
jobLogger, |
|||
errorLogger |
|||
}; |
|||
@ -0,0 +1,55 @@ |
|||
/** |
|||
* 数据库配置文件 |
|||
* |
|||
* 提供数据库连接配置和相关设置 |
|||
*/ |
|||
|
|||
const environment = process.env.NODE_ENV || "development"; |
|||
|
|||
// 数据库配置映射
|
|||
const databaseConfig = { |
|||
development: { |
|||
client: "sqlite3", |
|||
connection: { |
|||
filename: "./database/development.sqlite3" |
|||
}, |
|||
useNullAsDefault: true, |
|||
migrations: { |
|||
directory: "./src/infrastructure/database/migrations" |
|||
}, |
|||
seeds: { |
|||
directory: "./src/infrastructure/database/seeds" |
|||
} |
|||
}, |
|||
|
|||
test: { |
|||
client: "sqlite3", |
|||
connection: { |
|||
filename: ":memory:" |
|||
}, |
|||
useNullAsDefault: true, |
|||
migrations: { |
|||
directory: "./src/infrastructure/database/migrations" |
|||
}, |
|||
seeds: { |
|||
directory: "./src/infrastructure/database/seeds" |
|||
} |
|||
}, |
|||
|
|||
production: { |
|||
client: "sqlite3", |
|||
connection: { |
|||
filename: process.env.DATABASE_URL || "./database/production.sqlite3" |
|||
}, |
|||
useNullAsDefault: true, |
|||
migrations: { |
|||
directory: "./src/infrastructure/database/migrations" |
|||
}, |
|||
seeds: { |
|||
directory: "./src/infrastructure/database/seeds" |
|||
} |
|||
} |
|||
}; |
|||
|
|||
export default databaseConfig[environment]; |
|||
export { databaseConfig }; |
|||
@ -0,0 +1,216 @@ |
|||
/** |
|||
* 数据库服务提供者 |
|||
* |
|||
* 负责数据库连接管理、迁移管理和查询缓存 |
|||
*/ |
|||
|
|||
import buildKnex from "knex"; |
|||
import databaseConfig from "../config/database.js"; |
|||
|
|||
// 简单内存缓存(支持 TTL 与按前缀清理)
|
|||
const queryCache = new Map(); |
|||
|
|||
const getNow = () => Date.now(); |
|||
|
|||
const computeExpiresAt = (ttlMs) => { |
|||
if (!ttlMs || ttlMs <= 0) return null; |
|||
return getNow() + ttlMs; |
|||
}; |
|||
|
|||
const isExpired = (entry) => { |
|||
if (!entry) return true; |
|||
if (entry.expiresAt == null) return false; |
|||
return entry.expiresAt <= getNow(); |
|||
}; |
|||
|
|||
const getCacheKeyForBuilder = (builder) => { |
|||
if (builder._customCacheKey) return String(builder._customCacheKey); |
|||
return builder.toString(); |
|||
}; |
|||
|
|||
// 全局工具,便于在 QL 外部操作缓存
|
|||
export const DbQueryCache = { |
|||
get(key) { |
|||
const entry = queryCache.get(String(key)); |
|||
if (!entry) return undefined; |
|||
if (isExpired(entry)) { |
|||
queryCache.delete(String(key)); |
|||
return undefined; |
|||
} |
|||
return entry.value; |
|||
}, |
|||
set(key, value, ttlMs) { |
|||
const expiresAt = computeExpiresAt(ttlMs); |
|||
queryCache.set(String(key), { value, expiresAt }); |
|||
return value; |
|||
}, |
|||
has(key) { |
|||
const entry = queryCache.get(String(key)); |
|||
return !!entry && !isExpired(entry); |
|||
}, |
|||
delete(key) { |
|||
return queryCache.delete(String(key)); |
|||
}, |
|||
clear() { |
|||
queryCache.clear(); |
|||
}, |
|||
clearByPrefix(prefix) { |
|||
const p = String(prefix); |
|||
for (const k of queryCache.keys()) { |
|||
if (k.startsWith(p)) queryCache.delete(k); |
|||
} |
|||
}, |
|||
stats() { |
|||
let valid = 0; |
|||
let expired = 0; |
|||
for (const [k, entry] of queryCache.entries()) { |
|||
if (isExpired(entry)) expired++; |
|||
else valid++; |
|||
} |
|||
return { size: queryCache.size, valid, expired }; |
|||
} |
|||
}; |
|||
|
|||
class DatabaseProvider { |
|||
constructor() { |
|||
this.db = null; |
|||
this._isExtended = false; |
|||
} |
|||
|
|||
/** |
|||
* 注册数据库连接 |
|||
*/ |
|||
register() { |
|||
if (this.db) { |
|||
return this.db; |
|||
} |
|||
|
|||
// 创建 Knex 实例
|
|||
this.db = buildKnex(databaseConfig); |
|||
|
|||
// 扩展 QueryBuilder 缓存功能(只在第一次初始化时扩展)
|
|||
if (!this._isExtended) { |
|||
this._extendQueryBuilder(); |
|||
this._isExtended = true; |
|||
} |
|||
|
|||
return this.db; |
|||
} |
|||
|
|||
/** |
|||
* 扩展 QueryBuilder 的缓存功能 |
|||
*/ |
|||
_extendQueryBuilder() { |
|||
// 1) cache(ttlMs?): 读取缓存,不存在则执行并写入
|
|||
buildKnex.QueryBuilder.extend("cache", async function (ttlMs) { |
|||
const key = getCacheKeyForBuilder(this); |
|||
const entry = queryCache.get(key); |
|||
if (entry && !isExpired(entry)) { |
|||
return entry.value; |
|||
} |
|||
const data = await this; |
|||
queryCache.set(key, { value: data, expiresAt: computeExpiresAt(ttlMs) }); |
|||
return data; |
|||
}); |
|||
|
|||
// 2) cacheAs(customKey): 设置自定义 key
|
|||
buildKnex.QueryBuilder.extend("cacheAs", function (customKey) { |
|||
this._customCacheKey = String(customKey); |
|||
return this; |
|||
}); |
|||
|
|||
// 3) cacheSet(value, ttlMs?): 手动设置当前查询 key 的缓存
|
|||
buildKnex.QueryBuilder.extend("cacheSet", function (value, ttlMs) { |
|||
const key = getCacheKeyForBuilder(this); |
|||
queryCache.set(key, { value, expiresAt: computeExpiresAt(ttlMs) }); |
|||
return value; |
|||
}); |
|||
|
|||
// 4) cacheGet(): 仅从缓存读取当前查询 key 的值
|
|||
buildKnex.QueryBuilder.extend("cacheGet", function () { |
|||
const key = getCacheKeyForBuilder(this); |
|||
const entry = queryCache.get(key); |
|||
if (!entry || isExpired(entry)) return undefined; |
|||
return entry.value; |
|||
}); |
|||
|
|||
// 5) cacheInvalidate(): 使当前查询 key 的缓存失效
|
|||
buildKnex.QueryBuilder.extend("cacheInvalidate", function () { |
|||
const key = getCacheKeyForBuilder(this); |
|||
queryCache.delete(key); |
|||
return this; |
|||
}); |
|||
|
|||
// 6) cacheInvalidateByPrefix(prefix): 按前缀清理
|
|||
buildKnex.QueryBuilder.extend("cacheInvalidateByPrefix", function (prefix) { |
|||
const p = String(prefix); |
|||
for (const k of queryCache.keys()) { |
|||
if (k.startsWith(p)) queryCache.delete(k); |
|||
} |
|||
return this; |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 运行数据库迁移 |
|||
*/ |
|||
async runMigrations() { |
|||
if (!this.db) { |
|||
throw new Error("Database not initialized. Call register() first."); |
|||
} |
|||
|
|||
try { |
|||
const [batch, log] = await this.db.migrate.latest(); |
|||
if (log.length === 0) { |
|||
console.log("Database is already up to date"); |
|||
} else { |
|||
console.log(`Migrated ${log.length} files:`, log); |
|||
} |
|||
return { batch, log }; |
|||
} catch (error) { |
|||
console.error("Migration failed:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 运行种子数据 |
|||
*/ |
|||
async runSeeds() { |
|||
if (!this.db) { |
|||
throw new Error("Database not initialized. Call register() first."); |
|||
} |
|||
|
|||
try { |
|||
const [log] = await this.db.seed.run(); |
|||
console.log("Seeds completed:", log); |
|||
return log; |
|||
} catch (error) { |
|||
console.error("Seeding failed:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取数据库实例 |
|||
*/ |
|||
getDatabase() { |
|||
return this.db; |
|||
} |
|||
|
|||
/** |
|||
* 关闭数据库连接 |
|||
*/ |
|||
async close() { |
|||
if (this.db) { |
|||
await this.db.destroy(); |
|||
this.db = null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 导出单例实例
|
|||
const databaseProvider = new DatabaseProvider(); |
|||
|
|||
export default databaseProvider; |
|||
export { DatabaseProvider }; |
|||
@ -0,0 +1,15 @@ |
|||
/** |
|||
* 服务提供者索引文件 |
|||
* |
|||
* 统一导出所有服务提供者 |
|||
*/ |
|||
|
|||
import DatabaseProvider from "./DatabaseProvider.js"; |
|||
|
|||
export { |
|||
DatabaseProvider |
|||
}; |
|||
|
|||
export default { |
|||
DatabaseProvider |
|||
}; |
|||
@ -1,102 +0,0 @@ |
|||
import Router from "utils/router.js" |
|||
import ArticleService from "services/ArticleService.js" |
|||
import { logger } from "@/logger.js" |
|||
|
|||
/** |
|||
* 基础页面控制器 |
|||
* 负责处理首页、静态页面、联系表单等基础功能 |
|||
*/ |
|||
class BasePageController { |
|||
constructor() { |
|||
this.articleService = new ArticleService() |
|||
} |
|||
|
|||
// 首页
|
|||
async indexGet(ctx) { |
|||
const blogs = await this.articleService.getPublishedArticles() |
|||
return await ctx.render( |
|||
"page/index/index", |
|||
{ |
|||
apiList: [ |
|||
{ |
|||
name: "随机图片", |
|||
desc: "随机图片,点击查看。<br> 右键可复制链接", |
|||
url: "https://pic.xieyaxin.top/random.php", |
|||
}, |
|||
], |
|||
blogs: blogs.slice(0, 4), |
|||
}, |
|||
{ includeSite: true, includeUser: true } |
|||
) |
|||
} |
|||
|
|||
// 处理联系表单提交
|
|||
async contactPost(ctx) { |
|||
const { name, email, subject, message } = ctx.request.body |
|||
|
|||
// 简单的表单验证
|
|||
if (!name || !email || !subject || !message) { |
|||
ctx.status = 400 |
|||
ctx.body = { success: false, message: "请填写所有必填字段" } |
|||
return |
|||
} |
|||
|
|||
// 这里可以添加邮件发送逻辑或数据库存储逻辑
|
|||
// 目前只是简单的成功响应
|
|||
logger.info(`收到联系表单: ${name} (${email}) - ${subject}: ${message}`) |
|||
|
|||
ctx.body = { |
|||
success: true, |
|||
message: "感谢您的留言,我们会尽快回复您!", |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 通用页面渲染方法 |
|||
* @param {string} name - 模板名称 |
|||
* @param {Object} data - 页面数据 |
|||
* @returns {Function} 页面渲染函数 |
|||
*/ |
|||
pageGet(name, data) { |
|||
return async ctx => { |
|||
return await ctx.render( |
|||
name, |
|||
{ |
|||
...(data || {}), |
|||
}, |
|||
{ includeSite: true, includeUser: true } |
|||
) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 创建基础页面相关路由 |
|||
* @returns {Router} 路由实例 |
|||
*/ |
|||
static createRoutes() { |
|||
const controller = new BasePageController() |
|||
const router = new Router({ auth: "try" }) |
|||
|
|||
// 首页
|
|||
router.get("/", controller.indexGet.bind(controller), { auth: false }) |
|||
|
|||
// 静态页面
|
|||
router.get("/about", controller.pageGet("page/about/index"), { auth: false }) |
|||
router.get("/terms", controller.pageGet("page/extra/terms"), { auth: false }) |
|||
router.get("/privacy", controller.pageGet("page/extra/privacy"), { auth: false }) |
|||
router.get("/faq", controller.pageGet("page/extra/faq"), { auth: false }) |
|||
router.get("/feedback", controller.pageGet("page/extra/feedback"), { auth: false }) |
|||
router.get("/help", controller.pageGet("page/extra/help"), { auth: false }) |
|||
router.get("/contact", controller.pageGet("page/extra/contact"), { auth: false }) |
|||
|
|||
// 需要登录的页面
|
|||
router.get("/notice", controller.pageGet("page/notice/index"), { auth: true }) |
|||
|
|||
// 联系表单处理
|
|||
router.post("/contact", controller.contactPost.bind(controller), { auth: false }) |
|||
|
|||
return router |
|||
} |
|||
} |
|||
|
|||
export default BasePageController |
|||
@ -1,63 +0,0 @@ |
|||
import Router from "utils/router.js" |
|||
|
|||
class HtmxController { |
|||
async index(ctx) { |
|||
return await ctx.render("index", { name: "bluescurry" }) |
|||
} |
|||
|
|||
page(name, data) { |
|||
return async ctx => { |
|||
return await ctx.render(name, data) |
|||
} |
|||
} |
|||
|
|||
static createRoutes() { |
|||
const controller = new HtmxController() |
|||
const router = new Router({ auth: "try" }) |
|||
router.get("/htmx/timeline", async ctx => { |
|||
return await ctx.render("htmx/timeline", { |
|||
timeLine: [ |
|||
{ |
|||
icon: "第一份工作", |
|||
title: "???", |
|||
desc: `做游戏的。`, |
|||
}, |
|||
{ |
|||
icon: "大学毕业", |
|||
title: "2014年09月", |
|||
desc: `我从<a href="https://www.jxnu.edu.cn/" target="_blank">江西师范大学</a>毕业,
|
|||
获得了软件工程(虚拟现实与技术)专业的学士学位。`,
|
|||
}, |
|||
{ |
|||
icon: "高中", |
|||
title: "???", |
|||
desc: `宜春中学`, |
|||
}, |
|||
{ |
|||
icon: "初中", |
|||
title: "???", |
|||
desc: `宜春实验中学`, |
|||
}, |
|||
{ |
|||
icon: "小学(4-6年级)", |
|||
title: "???", |
|||
desc: `宜春二小`, |
|||
}, |
|||
{ |
|||
icon: "小学(1-3年级)", |
|||
title: "???", |
|||
desc: `丰城市泉港镇小学`, |
|||
}, |
|||
{ |
|||
icon: "出生", |
|||
title: "1996年06月", |
|||
desc: `我出生于江西省丰城市泉港镇`, |
|||
}, |
|||
], |
|||
}) |
|||
}) |
|||
return router |
|||
} |
|||
} |
|||
|
|||
export default HtmxController |
|||
@ -1,149 +0,0 @@ |
|||
import buildKnex from "knex" |
|||
import knexConfig from "../../knexfile.mjs" |
|||
|
|||
// 简单内存缓存(支持 TTL 与按前缀清理)
|
|||
const queryCache = new Map() |
|||
|
|||
const getNow = () => Date.now() |
|||
|
|||
const computeExpiresAt = (ttlMs) => { |
|||
if (!ttlMs || ttlMs <= 0) return null |
|||
return getNow() + ttlMs |
|||
} |
|||
|
|||
const isExpired = (entry) => { |
|||
if (!entry) return true |
|||
if (entry.expiresAt == null) return false |
|||
return entry.expiresAt <= getNow() |
|||
} |
|||
|
|||
const getCacheKeyForBuilder = (builder) => { |
|||
if (builder._customCacheKey) return String(builder._customCacheKey) |
|||
return builder.toString() |
|||
} |
|||
|
|||
// 全局工具,便于在 QL 外部操作缓存
|
|||
export const DbQueryCache = { |
|||
get(key) { |
|||
const entry = queryCache.get(String(key)) |
|||
if (!entry) return undefined |
|||
if (isExpired(entry)) { |
|||
queryCache.delete(String(key)) |
|||
return undefined |
|||
} |
|||
return entry.value |
|||
}, |
|||
set(key, value, ttlMs) { |
|||
const expiresAt = computeExpiresAt(ttlMs) |
|||
queryCache.set(String(key), { value, expiresAt }) |
|||
return value |
|||
}, |
|||
has(key) { |
|||
const entry = queryCache.get(String(key)) |
|||
return !!entry && !isExpired(entry) |
|||
}, |
|||
delete(key) { |
|||
return queryCache.delete(String(key)) |
|||
}, |
|||
clear() { |
|||
queryCache.clear() |
|||
}, |
|||
clearByPrefix(prefix) { |
|||
const p = String(prefix) |
|||
for (const k of queryCache.keys()) { |
|||
if (k.startsWith(p)) queryCache.delete(k) |
|||
} |
|||
}, |
|||
stats() { |
|||
let valid = 0 |
|||
let expired = 0 |
|||
for (const [k, entry] of queryCache.entries()) { |
|||
if (isExpired(entry)) expired++ |
|||
else valid++ |
|||
} |
|||
return { size: queryCache.size, valid, expired } |
|||
} |
|||
} |
|||
|
|||
// QueryBuilder 扩展
|
|||
// 1) cache(ttlMs?): 读取缓存,不存在则执行并写入
|
|||
buildKnex.QueryBuilder.extend("cache", async function (ttlMs) { |
|||
const key = getCacheKeyForBuilder(this) |
|||
const entry = queryCache.get(key) |
|||
if (entry && !isExpired(entry)) { |
|||
return entry.value |
|||
} |
|||
const data = await this |
|||
queryCache.set(key, { value: data, expiresAt: computeExpiresAt(ttlMs) }) |
|||
return data |
|||
}) |
|||
|
|||
// 2) cacheAs(customKey): 设置自定义 key
|
|||
buildKnex.QueryBuilder.extend("cacheAs", function (customKey) { |
|||
this._customCacheKey = String(customKey) |
|||
return this |
|||
}) |
|||
|
|||
// 3) cacheSet(value, ttlMs?): 手动设置当前查询 key 的缓存
|
|||
buildKnex.QueryBuilder.extend("cacheSet", function (value, ttlMs) { |
|||
const key = getCacheKeyForBuilder(this) |
|||
queryCache.set(key, { value, expiresAt: computeExpiresAt(ttlMs) }) |
|||
return value |
|||
}) |
|||
|
|||
// 4) cacheGet(): 仅从缓存读取当前查询 key 的值
|
|||
buildKnex.QueryBuilder.extend("cacheGet", function () { |
|||
const key = getCacheKeyForBuilder(this) |
|||
const entry = queryCache.get(key) |
|||
if (!entry || isExpired(entry)) return undefined |
|||
return entry.value |
|||
}) |
|||
|
|||
// 5) cacheInvalidate(): 使当前查询 key 的缓存失效
|
|||
buildKnex.QueryBuilder.extend("cacheInvalidate", function () { |
|||
const key = getCacheKeyForBuilder(this) |
|||
queryCache.delete(key) |
|||
return this |
|||
}) |
|||
|
|||
// 6) cacheInvalidateByPrefix(prefix): 按前缀清理
|
|||
buildKnex.QueryBuilder.extend("cacheInvalidateByPrefix", function (prefix) { |
|||
const p = String(prefix) |
|||
for (const k of queryCache.keys()) { |
|||
if (k.startsWith(p)) queryCache.delete(k) |
|||
} |
|||
return this |
|||
}) |
|||
|
|||
const environment = process.env.NODE_ENV || "development" |
|||
const db = buildKnex(knexConfig[environment]) |
|||
|
|||
export default db |
|||
|
|||
// async function createDatabase() {
|
|||
// try {
|
|||
// // SQLite会自动创建数据库文件,只需验证连接
|
|||
// await db.raw("SELECT 1")
|
|||
// console.log("SQLite数据库连接成功")
|
|||
|
|||
// // 检查users表是否存在(示例)
|
|||
// const [tableExists] = await db.raw(`
|
|||
// SELECT name
|
|||
// FROM sqlite_master
|
|||
// WHERE type='table' AND name='users'
|
|||
// `)
|
|||
|
|||
// if (tableExists) {
|
|||
// console.log("表 users 已存在")
|
|||
// } else {
|
|||
// console.log("表 users 不存在,需要创建(通过迁移)")
|
|||
// }
|
|||
|
|||
// await db.destroy()
|
|||
// } catch (error) {
|
|||
// console.error("数据库操作失败:", error)
|
|||
// process.exit(1)
|
|||
// }
|
|||
// }
|
|||
|
|||
// createDatabase()
|
|||
@ -1,21 +0,0 @@ |
|||
import Koa from "koa" |
|||
import { logger } from "./logger.js" |
|||
import { validateEnvironment } from "./utils/envValidator.js" |
|||
|
|||
// 启动前验证环境变量
|
|||
if (!validateEnvironment()) { |
|||
logger.error("环境变量验证失败,应用退出") |
|||
process.exit(1) |
|||
} |
|||
|
|||
const app = new Koa({ asyncLocalStorage: true }) |
|||
|
|||
app.keys = [] |
|||
|
|||
// SESSION_SECRET 已通过环境变量验证确保存在
|
|||
process.env.SESSION_SECRET.split(",").forEach(secret => { |
|||
app.keys.push(secret.trim()) |
|||
}) |
|||
|
|||
export { app } |
|||
export default app |
|||
@ -0,0 +1,13 @@ |
|||
/** |
|||
* 数据库基础设施 |
|||
* |
|||
* 统一导出数据库相关功能 |
|||
*/ |
|||
|
|||
import DatabaseProvider from "../../app/providers/DatabaseProvider.js"; |
|||
|
|||
// 初始化数据库连接
|
|||
const db = DatabaseProvider.register(); |
|||
|
|||
export default db; |
|||
export { DatabaseProvider }; |
|||
@ -1,4 +1,4 @@ |
|||
import { jobLogger } from "@/logger" |
|||
import { jobLogger } from "../../app/bootstrap/logger.js" |
|||
|
|||
export default { |
|||
id: "example", |
|||
@ -0,0 +1,62 @@ |
|||
import fs from 'fs'; |
|||
import path from 'path'; |
|||
import { fileURLToPath } from 'url'; |
|||
import scheduler from '../../shared/utils/scheduler.js'; |
|||
|
|||
const __filename = fileURLToPath(import.meta.url); |
|||
const __dirname = path.dirname(__filename); |
|||
const jobsDir = __dirname; |
|||
const jobModules = {}; |
|||
|
|||
// 异步加载所有 job 文件
|
|||
async function loadJobs() { |
|||
const files = fs.readdirSync(jobsDir); |
|||
for (const file of files) { |
|||
if (file === 'index.js' || !file.endsWith('Job.js')) continue; |
|||
try { |
|||
const jobModule = await import(`file://${path.join(jobsDir, file)}`); |
|||
const job = jobModule.default || jobModule; |
|||
if (job && job.id && job.cronTime && typeof job.task === 'function') { |
|||
jobModules[job.id] = job; |
|||
scheduler.add(job.id, job.cronTime, job.task, job.options); |
|||
if (job.autoStart) scheduler.start(job.id); |
|||
} |
|||
} catch (error) { |
|||
console.error(`[Jobs] 加载任务文件 ${file} 失败:`, error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 立即加载所有任务
|
|||
loadJobs().catch(console.error); |
|||
|
|||
function callHook(id, hookName) { |
|||
const job = jobModules[id]; |
|||
if (job && typeof job[hookName] === 'function') { |
|||
try { |
|||
job[hookName](); |
|||
} catch (e) { |
|||
console.error(`[Job:${id}] ${hookName} 执行异常:`, e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export default { |
|||
start: id => { |
|||
callHook(id, 'beforeStart'); |
|||
scheduler.start(id); |
|||
}, |
|||
stop: id => { |
|||
scheduler.stop(id); |
|||
callHook(id, 'afterStop'); |
|||
}, |
|||
updateCronTime: (id, cronTime) => scheduler.updateCronTime(id, cronTime), |
|||
list: () => scheduler.list(), |
|||
reload: id => { |
|||
const job = jobModules[id]; |
|||
if (job) { |
|||
scheduler.remove(id); |
|||
scheduler.add(job.id, job.cronTime, job.task, job.options); |
|||
} |
|||
} |
|||
}; |
|||
@ -1,48 +0,0 @@ |
|||
import fs from 'fs'; |
|||
import path from 'path'; |
|||
import scheduler from 'utils/scheduler.js'; |
|||
|
|||
const jobsDir = __dirname; |
|||
const jobModules = {}; |
|||
|
|||
fs.readdirSync(jobsDir).forEach(file => { |
|||
if (file === 'index.js' || !file.endsWith('Job.js')) return; |
|||
const jobModule = require(path.join(jobsDir, file)); |
|||
const job = jobModule.default || jobModule; |
|||
if (job && job.id && job.cronTime && typeof job.task === 'function') { |
|||
jobModules[job.id] = job; |
|||
scheduler.add(job.id, job.cronTime, job.task, job.options); |
|||
if (job.autoStart) scheduler.start(job.id); |
|||
} |
|||
}); |
|||
|
|||
function callHook(id, hookName) { |
|||
const job = jobModules[id]; |
|||
if (job && typeof job[hookName] === 'function') { |
|||
try { |
|||
job[hookName](); |
|||
} catch (e) { |
|||
console.error(`[Job:${id}] ${hookName} 执行异常:`, e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export default { |
|||
start: id => { |
|||
callHook(id, 'beforeStart'); |
|||
scheduler.start(id); |
|||
}, |
|||
stop: id => { |
|||
scheduler.stop(id); |
|||
callHook(id, 'afterStop'); |
|||
}, |
|||
updateCronTime: (id, cronTime) => scheduler.updateCronTime(id, cronTime), |
|||
list: () => scheduler.list(), |
|||
reload: id => { |
|||
const job = jobModules[id]; |
|||
if (job) { |
|||
scheduler.remove(id); |
|||
scheduler.add(job.id, job.cronTime, job.task, job.options); |
|||
} |
|||
} |
|||
}; |
|||
@ -1,41 +1,52 @@ |
|||
import { app } from "./global" |
|||
import { app } from "./app/bootstrap/app.js" |
|||
// 日志、全局插件、定时任务等基础设施
|
|||
import { logger } from "./logger.js" |
|||
import "./jobs/index.js" |
|||
import { logger } from "./app/bootstrap/logger.js" |
|||
import "./infrastructure/jobs/index.js" |
|||
|
|||
// 第三方依赖
|
|||
import os from "os" |
|||
|
|||
// 应用插件与自动路由
|
|||
import LoadMiddlewares from "./middlewares/install.js" |
|||
import LoadMiddlewares from "./presentation/middlewares/install.js" |
|||
|
|||
// 注册插件
|
|||
LoadMiddlewares(app) |
|||
// 异步启动函数
|
|||
async function startServer() { |
|||
// 注册插件
|
|||
await LoadMiddlewares(app) |
|||
|
|||
const PORT = process.env.PORT || 3000 |
|||
const PORT = process.env.PORT || 3000 |
|||
|
|||
const server = app.listen(PORT, () => { |
|||
const port = server.address().port |
|||
// 获取本地 IP
|
|||
const getLocalIP = () => { |
|||
const interfaces = os.networkInterfaces() |
|||
for (const name of Object.keys(interfaces)) { |
|||
for (const iface of interfaces[name]) { |
|||
if (iface.family === "IPv4" && !iface.internal) { |
|||
return iface.address |
|||
const server = app.listen(PORT, () => { |
|||
const port = server.address().port |
|||
// 获取本地 IP
|
|||
const getLocalIP = () => { |
|||
const interfaces = os.networkInterfaces() |
|||
for (const name of Object.keys(interfaces)) { |
|||
for (const iface of interfaces[name]) { |
|||
if (iface.family === "IPv4" && !iface.internal) { |
|||
return iface.address |
|||
} |
|||
} |
|||
} |
|||
return "localhost" |
|||
} |
|||
return "localhost" |
|||
} |
|||
const localIP = getLocalIP() |
|||
logger.trace(`──────────────────── 服务器已启动 ────────────────────`) |
|||
logger.trace(` `) |
|||
logger.trace(` 本地访问: http://localhost:${port} `) |
|||
logger.trace(` 局域网: http://${localIP}:${port} `) |
|||
logger.trace(` `) |
|||
logger.trace(` 服务启动时间: ${new Date().toLocaleString()} `) |
|||
logger.trace(`──────────────────────────────────────────────────────\n`) |
|||
const localIP = getLocalIP() |
|||
logger.trace(`──────────────────── 服务器已启动 ────────────────────`) |
|||
logger.trace(` `) |
|||
logger.trace(` 本地访问: http://localhost:${port} `) |
|||
logger.trace(` 局域网: http://${localIP}:${port} `) |
|||
logger.trace(` `) |
|||
logger.trace(` 服务启动时间: ${new Date().toLocaleString()} `) |
|||
logger.trace(`──────────────────────────────────────────────────\n`) |
|||
}) |
|||
|
|||
return server |
|||
} |
|||
|
|||
// 启动服务器
|
|||
startServer().catch(error => { |
|||
logger.error('服务器启动失败:', error) |
|||
process.exit(1) |
|||
}) |
|||
|
|||
export default app |
|||
export default app |
|||
@ -1,43 +0,0 @@ |
|||
import { logger } from "@/logger" |
|||
// src/plugins/errorHandler.js
|
|||
// 错误处理中间件插件
|
|||
|
|||
async function formatError(ctx, status, message, stack) { |
|||
const accept = ctx.accepts("json", "html", "text") |
|||
const isDev = process.env.NODE_ENV === "development" |
|||
if (accept === "json") { |
|||
ctx.type = "application/json" |
|||
ctx.body = isDev && stack ? { success: false, error: message, stack } : { success: false, error: message } |
|||
} else if (accept === "html") { |
|||
ctx.type = "html" |
|||
await ctx.render("error/index", { status, message, stack, isDev }) |
|||
} else { |
|||
ctx.type = "text" |
|||
ctx.body = isDev && stack ? `${status} - ${message}\n${stack}` : `${status} - ${message}` |
|||
} |
|||
ctx.status = status |
|||
} |
|||
|
|||
export default function errorHandler() { |
|||
return async (ctx, next) => { |
|||
// 拦截 Chrome DevTools 探测请求,直接返回 204
|
|||
if (ctx.path === "/.well-known/appspecific/com.chrome.devtools.json") { |
|||
ctx.status = 204 |
|||
ctx.body = "" |
|||
return |
|||
} |
|||
try { |
|||
await next() |
|||
if (ctx.status === 404) { |
|||
await formatError(ctx, 404, "Resource not found") |
|||
} |
|||
} catch (err) { |
|||
logger.error(err) |
|||
const isDev = process.env.NODE_ENV === "development" |
|||
if (isDev && err.stack) { |
|||
console.error(err.stack) |
|||
} |
|||
await formatError(ctx, err.statusCode || 500, err.message || err || "Internal server error", isDev ? err.stack : undefined) |
|||
} |
|||
} |
|||
} |
|||
@ -1,5 +1,5 @@ |
|||
import { ArticleModel } from "../../db/models/ArticleModel.js" |
|||
import Router from "utils/router.js" |
|||
import { ArticleModel } from "../models/ArticleModel.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
import { marked } from "marked" |
|||
|
|||
class ArticleController { |
|||
@ -1,4 +1,4 @@ |
|||
import db from "../index.js" |
|||
import db from "../../../infrastructure/database/index.js" |
|||
|
|||
class ArticleModel { |
|||
static async findAll() { |
|||
@ -1,5 +1,5 @@ |
|||
import ArticleModel from "db/models/ArticleModel.js" |
|||
import CommonError from "utils/error/CommonError.js" |
|||
import ArticleModel from "../models/ArticleModel.js" |
|||
import CommonError from "../../../shared/utils/error/CommonError.js" |
|||
|
|||
class ArticleService { |
|||
// 获取所有文章
|
|||
@ -1,6 +1,6 @@ |
|||
import UserService from "services/userService.js" |
|||
import { R } from "utils/helper.js" |
|||
import Router from "utils/router.js" |
|||
import UserService from "../services/userService.js" |
|||
import { R } from "../../../shared/utils/helper.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
|
|||
class AuthController { |
|||
constructor() { |
|||
@ -1,8 +1,8 @@ |
|||
import Router from "utils/router.js" |
|||
import UserService from "services/userService.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
import UserService from "../services/userService.js" |
|||
import svgCaptcha from "svg-captcha" |
|||
import CommonError from "@/utils/error/CommonError" |
|||
import { logger } from "@/logger.js" |
|||
import CommonError from "../../../shared/utils/error/CommonError.js" |
|||
import { logger } from "../../../app/bootstrap/logger.js" |
|||
|
|||
/** |
|||
* 认证相关页面控制器 |
|||
@ -1,4 +1,4 @@ |
|||
import db from "../index.js" |
|||
import db from "../../../infrastructure/database/index.js" |
|||
|
|||
class UserModel { |
|||
static async findAll() { |
|||
@ -1,8 +1,8 @@ |
|||
import UserModel from "db/models/UserModel.js" |
|||
import { hashPassword, comparePassword } from "utils/bcrypt.js" |
|||
import CommonError from "utils/error/CommonError.js" |
|||
import { JWT_SECRET } from "@/middlewares/Auth/auth.js" |
|||
import jwt from "@/middlewares/Auth/jwt.js" |
|||
import UserModel from "../models/UserModel.js" |
|||
import { hashPassword, comparePassword } from "../../../shared/utils/bcrypt.js" |
|||
import CommonError from "../../../shared/utils/error/CommonError.js" |
|||
import { JWT_SECRET } from "../../../presentation/middlewares/Auth/auth.js" |
|||
import jwt from "../../../presentation/middlewares/Auth/jwt.js" |
|||
|
|||
class UserService { |
|||
// 根据ID获取用户
|
|||
@ -1,4 +1,4 @@ |
|||
import db from "../index.js" |
|||
import db from "../../../infrastructure/database/index.js" |
|||
|
|||
class BookmarkModel { |
|||
static async findAllByUser(userId) { |
|||
@ -1,5 +1,5 @@ |
|||
import BookmarkModel from "db/models/BookmarkModel.js" |
|||
import CommonError from "utils/error/CommonError.js" |
|||
import BookmarkModel from "../models/BookmarkModel.js" |
|||
import CommonError from "../../../shared/utils/error/CommonError.js" |
|||
|
|||
class BookmarkService { |
|||
// 获取用户的所有书签
|
|||
@ -1,5 +1,5 @@ |
|||
import { R } from "utils/helper.js" |
|||
import Router from "utils/router.js" |
|||
import { R } from "../../../shared/utils/helper.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
|
|||
class AuthController { |
|||
constructor() {} |
|||
@ -1,7 +1,7 @@ |
|||
// Job Controller 示例:如何调用 service 层动态控制和查询定时任务
|
|||
import JobService from "services/JobService.js" |
|||
import { R } from "utils/helper.js" |
|||
import Router from "utils/router.js" |
|||
import JobService from "../services/JobService.js" |
|||
import { R } from "../../../shared/utils/helper.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
|
|||
class JobController { |
|||
constructor() { |
|||
@ -0,0 +1,25 @@ |
|||
import { R } from "../../../shared/utils/helper.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
|
|||
class Pageontroller { |
|||
constructor() {} |
|||
|
|||
async indexGet () { |
|||
console.log(234); |
|||
|
|||
return R.ResponseSuccess(Math.random()) |
|||
} |
|||
|
|||
/** |
|||
* 路由注册 |
|||
*/ |
|||
static createRoutes() { |
|||
const controller = new Pageontroller() |
|||
const router = new Router() |
|||
router.get("", controller.indexGet.bind(controller), { auth: "try" }) |
|||
router.get("/", controller.indexGet.bind(controller), { auth: "try" }) |
|||
return router |
|||
} |
|||
} |
|||
|
|||
export default Pageontroller |
|||
@ -1,4 +1,4 @@ |
|||
import Router from "utils/router.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
|
|||
class StatusController { |
|||
async status(ctx) { |
|||
@ -1,10 +1,10 @@ |
|||
import Router from "utils/router.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
import formidable from "formidable" |
|||
import fs from "fs/promises" |
|||
import path from "path" |
|||
import { fileURLToPath } from "url" |
|||
import { logger } from "@/logger.js" |
|||
import { R } from "@/utils/helper" |
|||
import { logger } from "../../../app/bootstrap/logger.js" |
|||
import { R } from "../../../shared/utils/helper.js" |
|||
|
|||
/** |
|||
* 文件上传控制器 |
|||
@ -1,4 +1,4 @@ |
|||
import jobs from "../jobs" |
|||
import jobs from "../../../infrastructure/jobs/index.js" |
|||
|
|||
class JobService { |
|||
startJob(id) { |
|||
@ -1,4 +1,4 @@ |
|||
import db from "../index.js" |
|||
import db from "../../../infrastructure/database/index.js" |
|||
|
|||
class SiteConfigModel { |
|||
// 获取指定key的配置
|
|||
@ -1,5 +1,5 @@ |
|||
import SiteConfigModel from "../db/models/SiteConfigModel.js" |
|||
import CommonError from "utils/error/CommonError.js" |
|||
import SiteConfigModel from "../models/SiteConfigModel.js" |
|||
import CommonError from "../../../shared/utils/error/CommonError.js" |
|||
|
|||
class SiteConfigService { |
|||
// 获取指定key的配置
|
|||
@ -1,11 +1,11 @@ |
|||
import Router from "utils/router.js" |
|||
import UserService from "services/userService.js" |
|||
import Router from "../../../shared/utils/router.js" |
|||
import UserService from "../../auth/services/userService.js" |
|||
import formidable from "formidable" |
|||
import fs from "fs/promises" |
|||
import path from "path" |
|||
import { fileURLToPath } from "url" |
|||
import CommonError from "@/utils/error/CommonError" |
|||
import { logger } from "@/logger.js" |
|||
import CommonError from "../../../shared/utils/error/CommonError.js" |
|||
import { logger } from "../../../app/bootstrap/logger.js" |
|||
import imageThumbnail from "image-thumbnail" |
|||
|
|||
/** |
|||
@ -1,4 +1,4 @@ |
|||
import { logger } from "@/logger" |
|||
import { logger } from "../../../app/bootstrap/logger.js" |
|||
import jwt from "./jwt" |
|||
import { minimatch } from "minimatch" |
|||
|
|||
@ -1,4 +1,4 @@ |
|||
import { logger } from "@/logger" |
|||
import { logger } from "../../../app/bootstrap/logger.js" |
|||
// src/plugins/errorHandler.js
|
|||
// 错误处理中间件插件
|
|||
|
|||
@ -1,4 +1,4 @@ |
|||
import { logger } from "@/logger" |
|||
import { logger } from "../../../app/bootstrap/logger.js" |
|||
|
|||
// 静态资源扩展名列表
|
|||
const staticExts = [".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".map", ".woff", ".woff2", ".ttf", ".eot"] |
|||
@ -1,13 +1,13 @@ |
|||
import { resolve } from "path" |
|||
import { app } from "@/global" |
|||
import { app } from "../../../app/bootstrap/app.js" |
|||
import consolidate from "consolidate" |
|||
import send from "../Send" |
|||
import getPaths from "get-paths" |
|||
// import pretty from "pretty"
|
|||
import { logger } from "@/logger" |
|||
import SiteConfigService from "services/SiteConfigService.js" |
|||
import { logger } from "../../../app/bootstrap/logger.js" |
|||
import SiteConfigService from "../../../modules/site-config/services/SiteConfigService.js" |
|||
import assign from "lodash/assign" |
|||
import config from "config/index.js" |
|||
import config from "../../../app/config/index.js" |
|||
|
|||
export default viewsMiddleware |
|||
|
|||
@ -1,222 +0,0 @@ |
|||
# 服务层 (Services) |
|||
|
|||
本目录包含了应用的所有业务逻辑服务层,负责处理业务规则、数据验证和错误处理。 |
|||
|
|||
## 服务列表 |
|||
|
|||
### 1. UserService - 用户服务 |
|||
处理用户相关的所有业务逻辑,包括用户注册、登录、密码管理等。 |
|||
|
|||
**主要功能:** |
|||
- 用户注册和登录 |
|||
- 用户信息管理(增删改查) |
|||
- 密码加密和验证 |
|||
- 用户统计和搜索 |
|||
- 批量操作支持 |
|||
|
|||
**使用示例:** |
|||
```javascript |
|||
import { userService } from '../services/index.js' |
|||
|
|||
// 用户注册 |
|||
const newUser = await userService.register({ |
|||
username: 'testuser', |
|||
email: 'test@example.com', |
|||
password: 'password123' |
|||
}) |
|||
|
|||
// 用户登录 |
|||
const loginResult = await userService.login({ |
|||
username: 'testuser', |
|||
password: 'password123' |
|||
}) |
|||
``` |
|||
|
|||
### 2. ArticleService - 文章服务 |
|||
处理文章相关的所有业务逻辑,包括文章的发布、编辑、搜索等。 |
|||
|
|||
**主要功能:** |
|||
- 文章的增删改查 |
|||
- 文章状态管理(草稿/发布) |
|||
- 文章搜索和分类 |
|||
- 阅读量统计 |
|||
- 相关文章推荐 |
|||
- 分页支持 |
|||
|
|||
**使用示例:** |
|||
```javascript |
|||
import { articleService } from '../services/index.js' |
|||
|
|||
// 创建文章 |
|||
const article = await articleService.createArticle({ |
|||
title: '测试文章', |
|||
content: '文章内容...', |
|||
category: '技术', |
|||
tags: 'JavaScript,Node.js' |
|||
}) |
|||
|
|||
// 获取已发布文章 |
|||
const publishedArticles = await articleService.getPublishedArticles() |
|||
|
|||
// 搜索文章 |
|||
const searchResults = await articleService.searchArticles('JavaScript') |
|||
``` |
|||
|
|||
### 3. BookmarkService - 书签服务 |
|||
处理用户书签的管理,包括添加、编辑、删除和搜索书签。 |
|||
|
|||
**主要功能:** |
|||
- 书签的增删改查 |
|||
- URL格式验证 |
|||
- 批量操作支持 |
|||
- 书签统计和搜索 |
|||
- 分页支持 |
|||
|
|||
**使用示例:** |
|||
```javascript |
|||
import { bookmarkService } from '../services/index.js' |
|||
|
|||
// 添加书签 |
|||
const bookmark = await bookmarkService.createBookmark({ |
|||
user_id: 1, |
|||
title: 'Google', |
|||
url: 'https://www.google.com', |
|||
description: '搜索引擎' |
|||
}) |
|||
|
|||
// 获取用户书签 |
|||
const userBookmarks = await bookmarkService.getUserBookmarks(1) |
|||
|
|||
// 搜索书签 |
|||
const searchResults = await bookmarkService.searchUserBookmarks(1, 'Google') |
|||
``` |
|||
|
|||
### 4. SiteConfigService - 站点配置服务 |
|||
管理站点的各种配置信息,如站点名称、描述、主题等。 |
|||
|
|||
**主要功能:** |
|||
- 配置的增删改查 |
|||
- 配置值验证 |
|||
- 批量操作支持 |
|||
- 默认配置初始化 |
|||
- 配置统计和搜索 |
|||
|
|||
**使用示例:** |
|||
```javascript |
|||
import { siteConfigService } from '../services/index.js' |
|||
|
|||
// 获取配置 |
|||
const siteName = await siteConfigService.get('site_name') |
|||
|
|||
// 设置配置 |
|||
await siteConfigService.set('site_name', '我的新网站') |
|||
|
|||
// 批量设置配置 |
|||
await siteConfigService.setMany({ |
|||
'site_description': '网站描述', |
|||
'posts_per_page': 20 |
|||
}) |
|||
|
|||
// 初始化默认配置 |
|||
await siteConfigService.initializeDefaultConfigs() |
|||
``` |
|||
|
|||
### 5. JobService - 任务服务 |
|||
处理后台任务和定时任务的管理。 |
|||
|
|||
**主要功能:** |
|||
- 任务调度和管理 |
|||
- 任务状态监控 |
|||
- 任务日志记录 |
|||
|
|||
## 错误处理 |
|||
|
|||
所有服务都使用统一的错误处理机制: |
|||
|
|||
```javascript |
|||
import CommonError from 'utils/error/CommonError.js' |
|||
|
|||
try { |
|||
const result = await userService.getUserById(1) |
|||
} catch (error) { |
|||
if (error instanceof CommonError) { |
|||
// 业务逻辑错误 |
|||
console.error(error.message) |
|||
} else { |
|||
// 系统错误 |
|||
console.error('系统错误:', error.message) |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 数据验证 |
|||
|
|||
服务层负责数据验证,确保数据的完整性和正确性: |
|||
|
|||
- **输入验证**:检查必填字段、格式验证等 |
|||
- **业务验证**:检查业务规则,如用户名唯一性 |
|||
- **权限验证**:确保用户只能操作自己的数据 |
|||
|
|||
## 事务支持 |
|||
|
|||
对于涉及多个数据库操作的方法,服务层支持事务处理: |
|||
|
|||
```javascript |
|||
// 在需要事务的方法中使用 |
|||
async createUserWithProfile(userData, profileData) { |
|||
// 这里可以添加事务支持 |
|||
const user = await this.createUser(userData) |
|||
// 创建用户档案... |
|||
return user |
|||
} |
|||
``` |
|||
|
|||
## 缓存策略 |
|||
|
|||
服务层可以集成缓存机制来提高性能: |
|||
|
|||
```javascript |
|||
// 示例:缓存用户信息 |
|||
async getUserById(id) { |
|||
const cacheKey = `user:${id}` |
|||
let user = await cache.get(cacheKey) |
|||
|
|||
if (!user) { |
|||
user = await UserModel.findById(id) |
|||
await cache.set(cacheKey, user, 3600) // 缓存1小时 |
|||
} |
|||
|
|||
return user |
|||
} |
|||
``` |
|||
|
|||
## 使用建议 |
|||
|
|||
1. **控制器层调用服务**:控制器应该调用服务层方法,而不是直接操作模型 |
|||
2. **错误处理**:在控制器中捕获服务层抛出的错误并返回适当的HTTP响应 |
|||
3. **数据转换**:服务层负责数据格式转换,控制器负责HTTP响应格式 |
|||
4. **业务逻辑**:复杂的业务逻辑应该放在服务层,保持控制器的简洁性 |
|||
|
|||
## 扩展指南 |
|||
|
|||
添加新的服务: |
|||
|
|||
1. 创建新的服务文件(如 `NewService.js`) |
|||
2. 继承或实现基础服务接口 |
|||
3. 在 `index.js` 中导出新服务 |
|||
4. 添加相应的测试用例 |
|||
5. 更新文档 |
|||
|
|||
```javascript |
|||
// 新服务示例 |
|||
class NewService { |
|||
async doSomething(data) { |
|||
try { |
|||
// 业务逻辑 |
|||
return result |
|||
} catch (error) { |
|||
throw new CommonError(`操作失败: ${error.message}`) |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
@ -1,36 +0,0 @@ |
|||
// 服务层统一导出
|
|||
import UserService from "./UserService.js" |
|||
import ArticleService from "./ArticleService.js" |
|||
import BookmarkService from "./BookmarkService.js" |
|||
import SiteConfigService from "./SiteConfigService.js" |
|||
import JobService from "./JobService.js" |
|||
|
|||
// 导出所有服务类
|
|||
export { |
|||
UserService, |
|||
ArticleService, |
|||
BookmarkService, |
|||
SiteConfigService, |
|||
JobService |
|||
} |
|||
|
|||
// 导出默认实例(单例模式)
|
|||
export const userService = new UserService() |
|||
export const articleService = new ArticleService() |
|||
export const bookmarkService = new BookmarkService() |
|||
export const siteConfigService = new SiteConfigService() |
|||
export const jobService = new JobService() |
|||
|
|||
// 默认导出
|
|||
export default { |
|||
UserService, |
|||
ArticleService, |
|||
BookmarkService, |
|||
SiteConfigService, |
|||
JobService, |
|||
userService, |
|||
articleService, |
|||
bookmarkService, |
|||
siteConfigService, |
|||
jobService |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue