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 { |
export default { |
||||
id: "example", |
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 { logger } from "./app/bootstrap/logger.js" |
||||
import "./jobs/index.js" |
import "./infrastructure/jobs/index.js" |
||||
|
|
||||
// 第三方依赖
|
// 第三方依赖
|
||||
import os from "os" |
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 server = app.listen(PORT, () => { |
||||
const port = server.address().port |
const port = server.address().port |
||||
// 获取本地 IP
|
// 获取本地 IP
|
||||
const getLocalIP = () => { |
const getLocalIP = () => { |
||||
const interfaces = os.networkInterfaces() |
const interfaces = os.networkInterfaces() |
||||
for (const name of Object.keys(interfaces)) { |
for (const name of Object.keys(interfaces)) { |
||||
for (const iface of interfaces[name]) { |
for (const iface of interfaces[name]) { |
||||
if (iface.family === "IPv4" && !iface.internal) { |
if (iface.family === "IPv4" && !iface.internal) { |
||||
return iface.address |
return iface.address |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
return "localhost" |
||||
} |
} |
||||
return "localhost" |
const localIP = getLocalIP() |
||||
} |
logger.trace(`──────────────────── 服务器已启动 ────────────────────`) |
||||
const localIP = getLocalIP() |
logger.trace(` `) |
||||
logger.trace(`──────────────────── 服务器已启动 ────────────────────`) |
logger.trace(` 本地访问: http://localhost:${port} `) |
||||
logger.trace(` `) |
logger.trace(` 局域网: http://${localIP}:${port} `) |
||||
logger.trace(` 本地访问: http://localhost:${port} `) |
logger.trace(` `) |
||||
logger.trace(` 局域网: http://${localIP}:${port} `) |
logger.trace(` 服务启动时间: ${new Date().toLocaleString()} `) |
||||
logger.trace(` `) |
logger.trace(`──────────────────────────────────────────────────\n`) |
||||
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 { ArticleModel } from "../models/ArticleModel.js" |
||||
import Router from "utils/router.js" |
import Router from "../../../shared/utils/router.js" |
||||
import { marked } from "marked" |
import { marked } from "marked" |
||||
|
|
||||
class ArticleController { |
class ArticleController { |
||||
@ -1,4 +1,4 @@ |
|||||
import db from "../index.js" |
import db from "../../../infrastructure/database/index.js" |
||||
|
|
||||
class ArticleModel { |
class ArticleModel { |
||||
static async findAll() { |
static async findAll() { |
||||
@ -1,5 +1,5 @@ |
|||||
import ArticleModel from "db/models/ArticleModel.js" |
import ArticleModel from "../models/ArticleModel.js" |
||||
import CommonError from "utils/error/CommonError.js" |
import CommonError from "../../../shared/utils/error/CommonError.js" |
||||
|
|
||||
class ArticleService { |
class ArticleService { |
||||
// 获取所有文章
|
// 获取所有文章
|
||||
@ -1,6 +1,6 @@ |
|||||
import UserService from "services/userService.js" |
import UserService from "../services/userService.js" |
||||
import { R } from "utils/helper.js" |
import { R } from "../../../shared/utils/helper.js" |
||||
import Router from "utils/router.js" |
import Router from "../../../shared/utils/router.js" |
||||
|
|
||||
class AuthController { |
class AuthController { |
||||
constructor() { |
constructor() { |
||||
@ -1,8 +1,8 @@ |
|||||
import Router from "utils/router.js" |
import Router from "../../../shared/utils/router.js" |
||||
import UserService from "services/userService.js" |
import UserService from "../services/userService.js" |
||||
import svgCaptcha from "svg-captcha" |
import svgCaptcha from "svg-captcha" |
||||
import CommonError from "@/utils/error/CommonError" |
import CommonError from "../../../shared/utils/error/CommonError.js" |
||||
import { logger } from "@/logger.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 { |
class UserModel { |
||||
static async findAll() { |
static async findAll() { |
||||
@ -1,8 +1,8 @@ |
|||||
import UserModel from "db/models/UserModel.js" |
import UserModel from "../models/UserModel.js" |
||||
import { hashPassword, comparePassword } from "utils/bcrypt.js" |
import { hashPassword, comparePassword } from "../../../shared/utils/bcrypt.js" |
||||
import CommonError from "utils/error/CommonError.js" |
import CommonError from "../../../shared/utils/error/CommonError.js" |
||||
import { JWT_SECRET } from "@/middlewares/Auth/auth.js" |
import { JWT_SECRET } from "../../../presentation/middlewares/Auth/auth.js" |
||||
import jwt from "@/middlewares/Auth/jwt.js" |
import jwt from "../../../presentation/middlewares/Auth/jwt.js" |
||||
|
|
||||
class UserService { |
class UserService { |
||||
// 根据ID获取用户
|
// 根据ID获取用户
|
||||
@ -1,4 +1,4 @@ |
|||||
import db from "../index.js" |
import db from "../../../infrastructure/database/index.js" |
||||
|
|
||||
class BookmarkModel { |
class BookmarkModel { |
||||
static async findAllByUser(userId) { |
static async findAllByUser(userId) { |
||||
@ -1,5 +1,5 @@ |
|||||
import BookmarkModel from "db/models/BookmarkModel.js" |
import BookmarkModel from "../models/BookmarkModel.js" |
||||
import CommonError from "utils/error/CommonError.js" |
import CommonError from "../../../shared/utils/error/CommonError.js" |
||||
|
|
||||
class BookmarkService { |
class BookmarkService { |
||||
// 获取用户的所有书签
|
// 获取用户的所有书签
|
||||
@ -1,5 +1,5 @@ |
|||||
import { R } from "utils/helper.js" |
import { R } from "../../../shared/utils/helper.js" |
||||
import Router from "utils/router.js" |
import Router from "../../../shared/utils/router.js" |
||||
|
|
||||
class AuthController { |
class AuthController { |
||||
constructor() {} |
constructor() {} |
||||
@ -1,7 +1,7 @@ |
|||||
// Job Controller 示例:如何调用 service 层动态控制和查询定时任务
|
// Job Controller 示例:如何调用 service 层动态控制和查询定时任务
|
||||
import JobService from "services/JobService.js" |
import JobService from "../services/JobService.js" |
||||
import { R } from "utils/helper.js" |
import { R } from "../../../shared/utils/helper.js" |
||||
import Router from "utils/router.js" |
import Router from "../../../shared/utils/router.js" |
||||
|
|
||||
class JobController { |
class JobController { |
||||
constructor() { |
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 { |
class StatusController { |
||||
async status(ctx) { |
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 formidable from "formidable" |
||||
import fs from "fs/promises" |
import fs from "fs/promises" |
||||
import path from "path" |
import path from "path" |
||||
import { fileURLToPath } from "url" |
import { fileURLToPath } from "url" |
||||
import { logger } from "@/logger.js" |
import { logger } from "../../../app/bootstrap/logger.js" |
||||
import { R } from "@/utils/helper" |
import { R } from "../../../shared/utils/helper.js" |
||||
|
|
||||
/** |
/** |
||||
* 文件上传控制器 |
* 文件上传控制器 |
||||
@ -1,4 +1,4 @@ |
|||||
import jobs from "../jobs" |
import jobs from "../../../infrastructure/jobs/index.js" |
||||
|
|
||||
class JobService { |
class JobService { |
||||
startJob(id) { |
startJob(id) { |
||||
@ -1,4 +1,4 @@ |
|||||
import db from "../index.js" |
import db from "../../../infrastructure/database/index.js" |
||||
|
|
||||
class SiteConfigModel { |
class SiteConfigModel { |
||||
// 获取指定key的配置
|
// 获取指定key的配置
|
||||
@ -1,5 +1,5 @@ |
|||||
import SiteConfigModel from "../db/models/SiteConfigModel.js" |
import SiteConfigModel from "../models/SiteConfigModel.js" |
||||
import CommonError from "utils/error/CommonError.js" |
import CommonError from "../../../shared/utils/error/CommonError.js" |
||||
|
|
||||
class SiteConfigService { |
class SiteConfigService { |
||||
// 获取指定key的配置
|
// 获取指定key的配置
|
||||
@ -1,11 +1,11 @@ |
|||||
import Router from "utils/router.js" |
import Router from "../../../shared/utils/router.js" |
||||
import UserService from "services/userService.js" |
import UserService from "../../auth/services/userService.js" |
||||
import formidable from "formidable" |
import formidable from "formidable" |
||||
import fs from "fs/promises" |
import fs from "fs/promises" |
||||
import path from "path" |
import path from "path" |
||||
import { fileURLToPath } from "url" |
import { fileURLToPath } from "url" |
||||
import CommonError from "@/utils/error/CommonError" |
import CommonError from "../../../shared/utils/error/CommonError.js" |
||||
import { logger } from "@/logger.js" |
import { logger } from "../../../app/bootstrap/logger.js" |
||||
import imageThumbnail from "image-thumbnail" |
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 jwt from "./jwt" |
||||
import { minimatch } from "minimatch" |
import { minimatch } from "minimatch" |
||||
|
|
||||
@ -1,4 +1,4 @@ |
|||||
import { logger } from "@/logger" |
import { logger } from "../../../app/bootstrap/logger.js" |
||||
// src/plugins/errorHandler.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"] |
const staticExts = [".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".map", ".woff", ".woff2", ".ttf", ".eot"] |
||||
@ -1,13 +1,13 @@ |
|||||
import { resolve } from "path" |
import { resolve } from "path" |
||||
import { app } from "@/global" |
import { app } from "../../../app/bootstrap/app.js" |
||||
import consolidate from "consolidate" |
import consolidate from "consolidate" |
||||
import send from "../Send" |
import send from "../Send" |
||||
import getPaths from "get-paths" |
import getPaths from "get-paths" |
||||
// import pretty from "pretty"
|
// import pretty from "pretty"
|
||||
import { logger } from "@/logger" |
import { logger } from "../../../app/bootstrap/logger.js" |
||||
import SiteConfigService from "services/SiteConfigService.js" |
import SiteConfigService from "../../../modules/site-config/services/SiteConfigService.js" |
||||
import assign from "lodash/assign" |
import assign from "lodash/assign" |
||||
import config from "config/index.js" |
import config from "../../../app/config/index.js" |
||||
|
|
||||
export default viewsMiddleware |
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