Browse Source

refactor(src): 重构项目目录结构实现模块化管理

- 新建 app、modules、shared、presentation、infrastructure 五个顶级目录
- 将配置、核心引导和日志迁移至 app 目录
- 按业务领域划分模块,controllers、services、models 同步重组到 modules 内
- 共享工具、常量和基础类统一放入 shared 目录
- 中间件、路由和视图迁移至 presentation 目录下
- 数据库和定时任务迁移至 infrastructure 目录管理
- 新增数据库服务提供者封装连接和缓存逻辑
- 更新所有相关导入路径适配新目录结构
- 删除废弃的老目录和控制器代码
- 重构 main.js 异步启动服务器逻辑,完成插件注册和日志打印
- 保持原有 MVC 分层与路由自动发现机制,提升代码组织和维护性
re
谢亚昕 4 months ago
parent
commit
d5c46fcb33
  1. 330
      .qoder/quests/clean-src-directory.md
  2. 27
      src/app/bootstrap/app.js
  3. 22
      src/app/bootstrap/index.js
  4. 7
      src/app/bootstrap/logger.js
  5. 55
      src/app/config/database.js
  6. 0
      src/app/config/index.js
  7. 216
      src/app/providers/DatabaseProvider.js
  8. 15
      src/app/providers/index.js
  9. 102
      src/controllers/Page/BasePageController.js
  10. 63
      src/controllers/Page/_Demo/HtmxController.js
  11. 149
      src/db/index.js
  12. 21
      src/global.js
  13. 0
      src/infrastructure/database/docs/ArticleModel.md
  14. 0
      src/infrastructure/database/docs/BookmarkModel.md
  15. 0
      src/infrastructure/database/docs/README.md
  16. 0
      src/infrastructure/database/docs/SiteConfigModel.md
  17. 0
      src/infrastructure/database/docs/UserModel.md
  18. 13
      src/infrastructure/database/index.js
  19. 0
      src/infrastructure/database/migrations/20250616065041_create_users_table.mjs
  20. 0
      src/infrastructure/database/migrations/20250621013128_site_config.mjs
  21. 0
      src/infrastructure/database/migrations/20250830014825_create_articles_table.mjs
  22. 0
      src/infrastructure/database/migrations/20250830015422_create_bookmarks_table.mjs
  23. 0
      src/infrastructure/database/migrations/20250830020000_add_article_fields.mjs
  24. 0
      src/infrastructure/database/migrations/20250901000000_add_profile_fields.mjs
  25. 0
      src/infrastructure/database/seeds/20250616071157_users_seed.mjs
  26. 0
      src/infrastructure/database/seeds/20250621013324_site_config_seed.mjs
  27. 0
      src/infrastructure/database/seeds/20250830020000_articles_seed.mjs
  28. 2
      src/infrastructure/jobs/exampleJob.js
  29. 62
      src/infrastructure/jobs/index.js
  30. 48
      src/jobs/index.js
  31. 65
      src/main.js
  32. 43
      src/middlewares/ErrorHandler/index.js
  33. 4
      src/modules/article/controllers/ArticleController.js
  34. 2
      src/modules/article/models/ArticleModel.js
  35. 4
      src/modules/article/services/ArticleService.js
  36. 6
      src/modules/auth/controllers/AuthController.js
  37. 8
      src/modules/auth/controllers/AuthPageController.js
  38. 2
      src/modules/auth/models/UserModel.js
  39. 10
      src/modules/auth/services/userService.js
  40. 2
      src/modules/bookmark/models/BookmarkModel.js
  41. 4
      src/modules/bookmark/services/BookmarkService.js
  42. 4
      src/modules/common/controllers/ApiController.js
  43. 6
      src/modules/common/controllers/JobController.js
  44. 25
      src/modules/common/controllers/PageController.js
  45. 2
      src/modules/common/controllers/StatusController.js
  46. 6
      src/modules/common/controllers/UploadController.js
  47. 2
      src/modules/common/services/JobService.js
  48. 2
      src/modules/site-config/models/SiteConfigModel.js
  49. 4
      src/modules/site-config/services/SiteConfigService.js
  50. 8
      src/modules/user/controllers/ProfileController.js
  51. 2
      src/presentation/middlewares/Auth/auth.js
  52. 0
      src/presentation/middlewares/Auth/index.js
  53. 0
      src/presentation/middlewares/Auth/jwt.js
  54. 2
      src/presentation/middlewares/ErrorHandler/index.js
  55. 2
      src/presentation/middlewares/ResponseTime/index.js
  56. 0
      src/presentation/middlewares/Send/index.js
  57. 0
      src/presentation/middlewares/Send/resolve-path.js
  58. 0
      src/presentation/middlewares/Session/index.js
  59. 0
      src/presentation/middlewares/Toast/index.js
  60. 8
      src/presentation/middlewares/Views/index.js
  61. 9
      src/presentation/middlewares/install.js
  62. 0
      src/presentation/views/error/index.pug
  63. 0
      src/presentation/views/htmx/footer.pug
  64. 0
      src/presentation/views/htmx/login.pug
  65. 0
      src/presentation/views/htmx/navbar.pug
  66. 0
      src/presentation/views/htmx/timeline.pug
  67. 0
      src/presentation/views/layouts/base.pug
  68. 0
      src/presentation/views/layouts/bg-page.pug
  69. 0
      src/presentation/views/layouts/empty.pug
  70. 0
      src/presentation/views/layouts/page.pug
  71. 0
      src/presentation/views/layouts/pure.pug
  72. 0
      src/presentation/views/layouts/root.pug
  73. 0
      src/presentation/views/layouts/utils.pug
  74. 0
      src/presentation/views/page/about/index.pug
  75. 0
      src/presentation/views/page/articles/article.pug
  76. 0
      src/presentation/views/page/articles/category.pug
  77. 0
      src/presentation/views/page/articles/index.pug
  78. 0
      src/presentation/views/page/articles/search.pug
  79. 0
      src/presentation/views/page/articles/tag.pug
  80. 0
      src/presentation/views/page/auth/no-auth.pug
  81. 0
      src/presentation/views/page/extra/contact.pug
  82. 0
      src/presentation/views/page/extra/faq.pug
  83. 0
      src/presentation/views/page/extra/feedback.pug
  84. 0
      src/presentation/views/page/extra/help.pug
  85. 0
      src/presentation/views/page/extra/privacy.pug
  86. 0
      src/presentation/views/page/extra/terms.pug
  87. 0
      src/presentation/views/page/index copy/index.pug
  88. 0
      src/presentation/views/page/index/index copy 2.pug
  89. 0
      src/presentation/views/page/index/index copy.pug
  90. 0
      src/presentation/views/page/index/index.pug
  91. 0
      src/presentation/views/page/index/person.pug
  92. 0
      src/presentation/views/page/login/index.pug
  93. 0
      src/presentation/views/page/notice/index.pug
  94. 0
      src/presentation/views/page/profile/index.pug
  95. 0
      src/presentation/views/page/register/index.pug
  96. 222
      src/services/README.md
  97. 36
      src/services/index.js
  98. 0
      src/shared/utils/BaseSingleton.js
  99. 14
      src/shared/utils/ForRegister.js
  100. 0
      src/shared/utils/bcrypt.js

330
.qoder/quests/clean-src-directory.md

@ -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. 各业务模块功能正常
本方案通过最小化改动实现目录结构优化,保持原有架构优势的同时提升代码组织质量,为项目的长期维护和扩展奠定良好基础。

27
src/app/bootstrap/app.js

@ -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;

22
src/app/bootstrap/index.js

@ -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
};

7
src/logger.js → src/app/bootstrap/logger.js

@ -1,3 +1,8 @@
/**
* 日志配置
*
* 配置和导出日志记录器
*/
import log4js from "log4js";
@ -60,4 +65,4 @@ log4js.configure({
// 导出常用 logger 实例,便于直接引用
export const logger = log4js.getLogger(); // default
export const jobLogger = log4js.getLogger('jobs');
export const errorLogger = log4js.getLogger('error');
export const errorLogger = log4js.getLogger('error');

55
src/app/config/database.js

@ -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
src/config/index.js → src/app/config/index.js

216
src/app/providers/DatabaseProvider.js

@ -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 };

15
src/app/providers/index.js

@ -0,0 +1,15 @@
/**
* 服务提供者索引文件
*
* 统一导出所有服务提供者
*/
import DatabaseProvider from "./DatabaseProvider.js";
export {
DatabaseProvider
};
export default {
DatabaseProvider
};

102
src/controllers/Page/BasePageController.js

@ -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

63
src/controllers/Page/_Demo/HtmxController.js

@ -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

149
src/db/index.js

@ -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()

21
src/global.js

@ -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
src/db/docs/ArticleModel.md → src/infrastructure/database/docs/ArticleModel.md

0
src/db/docs/BookmarkModel.md → src/infrastructure/database/docs/BookmarkModel.md

0
src/db/docs/README.md → src/infrastructure/database/docs/README.md

0
src/db/docs/SiteConfigModel.md → src/infrastructure/database/docs/SiteConfigModel.md

0
src/db/docs/UserModel.md → src/infrastructure/database/docs/UserModel.md

13
src/infrastructure/database/index.js

@ -0,0 +1,13 @@
/**
* 数据库基础设施
*
* 统一导出数据库相关功能
*/
import DatabaseProvider from "../../app/providers/DatabaseProvider.js";
// 初始化数据库连接
const db = DatabaseProvider.register();
export default db;
export { DatabaseProvider };

0
src/db/migrations/20250616065041_create_users_table.mjs → src/infrastructure/database/migrations/20250616065041_create_users_table.mjs

0
src/db/migrations/20250621013128_site_config.mjs → src/infrastructure/database/migrations/20250621013128_site_config.mjs

0
src/db/migrations/20250830014825_create_articles_table.mjs → src/infrastructure/database/migrations/20250830014825_create_articles_table.mjs

0
src/db/migrations/20250830015422_create_bookmarks_table.mjs → src/infrastructure/database/migrations/20250830015422_create_bookmarks_table.mjs

0
src/db/migrations/20250830020000_add_article_fields.mjs → src/infrastructure/database/migrations/20250830020000_add_article_fields.mjs

0
src/db/migrations/20250901000000_add_profile_fields.mjs → src/infrastructure/database/migrations/20250901000000_add_profile_fields.mjs

0
src/db/seeds/20250616071157_users_seed.mjs → src/infrastructure/database/seeds/20250616071157_users_seed.mjs

0
src/db/seeds/20250621013324_site_config_seed.mjs → src/infrastructure/database/seeds/20250621013324_site_config_seed.mjs

0
src/db/seeds/20250830020000_articles_seed.mjs → src/infrastructure/database/seeds/20250830020000_articles_seed.mjs

2
src/jobs/exampleJob.js → src/infrastructure/jobs/exampleJob.js

@ -1,4 +1,4 @@
import { jobLogger } from "@/logger"
import { jobLogger } from "../../app/bootstrap/logger.js"
export default {
id: "example",

62
src/infrastructure/jobs/index.js

@ -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);
}
}
};

48
src/jobs/index.js

@ -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);
}
}
};

65
src/main.js

@ -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

43
src/middlewares/ErrorHandler/index.js

@ -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)
}
}
}

4
src/controllers/Page/ArticleController.js → src/modules/article/controllers/ArticleController.js

@ -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 {

2
src/db/models/ArticleModel.js → src/modules/article/models/ArticleModel.js

@ -1,4 +1,4 @@
import db from "../index.js"
import db from "../../../infrastructure/database/index.js"
class ArticleModel {
static async findAll() {

4
src/services/ArticleService.js → src/modules/article/services/ArticleService.js

@ -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 {
// 获取所有文章

6
src/controllers/Api/AuthController.js → src/modules/auth/controllers/AuthController.js

@ -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() {

8
src/controllers/Page/AuthPageController.js → src/modules/auth/controllers/AuthPageController.js

@ -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"
/**
* 认证相关页面控制器

2
src/db/models/UserModel.js → src/modules/auth/models/UserModel.js

@ -1,4 +1,4 @@
import db from "../index.js"
import db from "../../../infrastructure/database/index.js"
class UserModel {
static async findAll() {

10
src/services/userService.js → src/modules/auth/services/userService.js

@ -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获取用户

2
src/db/models/BookmarkModel.js → src/modules/bookmark/models/BookmarkModel.js

@ -1,4 +1,4 @@
import db from "../index.js"
import db from "../../../infrastructure/database/index.js"
class BookmarkModel {
static async findAllByUser(userId) {

4
src/services/BookmarkService.js → src/modules/bookmark/services/BookmarkService.js

@ -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 {
// 获取用户的所有书签

4
src/controllers/Api/ApiController.js → src/modules/common/controllers/ApiController.js

@ -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() {}

6
src/controllers/Api/JobController.js → src/modules/common/controllers/JobController.js

@ -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() {

25
src/modules/common/controllers/PageController.js

@ -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

2
src/controllers/Api/StatusController.js → src/modules/common/controllers/StatusController.js

@ -1,4 +1,4 @@
import Router from "utils/router.js"
import Router from "../../../shared/utils/router.js"
class StatusController {
async status(ctx) {

6
src/controllers/Page/UploadController.js → src/modules/common/controllers/UploadController.js

@ -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"
/**
* 文件上传控制器

2
src/services/JobService.js → src/modules/common/services/JobService.js

@ -1,4 +1,4 @@
import jobs from "../jobs"
import jobs from "../../../infrastructure/jobs/index.js"
class JobService {
startJob(id) {

2
src/db/models/SiteConfigModel.js → src/modules/site-config/models/SiteConfigModel.js

@ -1,4 +1,4 @@
import db from "../index.js"
import db from "../../../infrastructure/database/index.js"
class SiteConfigModel {
// 获取指定key的配置

4
src/services/SiteConfigService.js → src/modules/site-config/services/SiteConfigService.js

@ -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的配置

8
src/controllers/Page/ProfileController.js → src/modules/user/controllers/ProfileController.js

@ -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"
/**

2
src/middlewares/Auth/auth.js → src/presentation/middlewares/Auth/auth.js

@ -1,4 +1,4 @@
import { logger } from "@/logger"
import { logger } from "../../../app/bootstrap/logger.js"
import jwt from "./jwt"
import { minimatch } from "minimatch"

0
src/middlewares/Auth/index.js → src/presentation/middlewares/Auth/index.js

0
src/middlewares/Auth/jwt.js → src/presentation/middlewares/Auth/jwt.js

2
src/middlewares/errorHandler/index.js → src/presentation/middlewares/ErrorHandler/index.js

@ -1,4 +1,4 @@
import { logger } from "@/logger"
import { logger } from "../../../app/bootstrap/logger.js"
// src/plugins/errorHandler.js
// 错误处理中间件插件

2
src/middlewares/ResponseTime/index.js → src/presentation/middlewares/ResponseTime/index.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"]

0
src/middlewares/Send/index.js → src/presentation/middlewares/Send/index.js

0
src/middlewares/Send/resolve-path.js → src/presentation/middlewares/Send/resolve-path.js

0
src/middlewares/Session/index.js → src/presentation/middlewares/Session/index.js

0
src/middlewares/Toast/index.js → src/presentation/middlewares/Toast/index.js

8
src/middlewares/Views/index.js → src/presentation/middlewares/Views/index.js

@ -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

9
src/middlewares/install.js → src/presentation/middlewares/install.js

@ -10,12 +10,12 @@ import Views from "./Views"
import Session from "./Session"
import etag from "@koa/etag"
import conditional from "koa-conditional-get"
import { autoRegisterControllers } from "@/utils/ForRegister.js"
import { autoRegisterControllers } from "../../shared/utils/ForRegister.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const publicPath = resolve(__dirname, "../../public")
const publicPath = resolve(__dirname, "../../../public")
export default app => {
export default async app => {
// 错误处理
app.use(ErrorHandler())
// 响应时间
@ -50,9 +50,10 @@ export default app => {
// 请求体解析
app.use(bodyParser())
// 自动注册控制器
autoRegisterControllers(app, path.resolve(__dirname, "../controllers"))
await autoRegisterControllers(app, path.resolve(__dirname, "../../modules"))
// 注册完成之后静态资源设置
app.use(async (ctx, next) => {
console.log(11, ctx.body);
if (ctx.body) return await next()
if (ctx.status === 200) return await next()
if (ctx.method.toLowerCase() === "get") {

0
src/views/error/index.pug → src/presentation/views/error/index.pug

0
src/views/htmx/footer.pug → src/presentation/views/htmx/footer.pug

0
src/views/htmx/login.pug → src/presentation/views/htmx/login.pug

0
src/views/htmx/navbar.pug → src/presentation/views/htmx/navbar.pug

0
src/views/htmx/timeline.pug → src/presentation/views/htmx/timeline.pug

0
src/views/layouts/base.pug → src/presentation/views/layouts/base.pug

0
src/views/layouts/bg-page.pug → src/presentation/views/layouts/bg-page.pug

0
src/views/layouts/empty.pug → src/presentation/views/layouts/empty.pug

0
src/views/layouts/page.pug → src/presentation/views/layouts/page.pug

0
src/views/layouts/pure.pug → src/presentation/views/layouts/pure.pug

0
src/views/layouts/root.pug → src/presentation/views/layouts/root.pug

0
src/views/layouts/utils.pug → src/presentation/views/layouts/utils.pug

0
src/views/page/about/index.pug → src/presentation/views/page/about/index.pug

0
src/views/page/articles/article.pug → src/presentation/views/page/articles/article.pug

0
src/views/page/articles/category.pug → src/presentation/views/page/articles/category.pug

0
src/views/page/articles/index.pug → src/presentation/views/page/articles/index.pug

0
src/views/page/articles/search.pug → src/presentation/views/page/articles/search.pug

0
src/views/page/articles/tag.pug → src/presentation/views/page/articles/tag.pug

0
src/views/page/auth/no-auth.pug → src/presentation/views/page/auth/no-auth.pug

0
src/views/page/extra/contact.pug → src/presentation/views/page/extra/contact.pug

0
src/views/page/extra/faq.pug → src/presentation/views/page/extra/faq.pug

0
src/views/page/extra/feedback.pug → src/presentation/views/page/extra/feedback.pug

0
src/views/page/extra/help.pug → src/presentation/views/page/extra/help.pug

0
src/views/page/extra/privacy.pug → src/presentation/views/page/extra/privacy.pug

0
src/views/page/extra/terms.pug → src/presentation/views/page/extra/terms.pug

0
src/views/page/index copy/index.pug → src/presentation/views/page/index copy/index.pug

0
src/views/page/index/index copy 2.pug → src/presentation/views/page/index/index copy 2.pug

0
src/views/page/index/index copy.pug → src/presentation/views/page/index/index copy.pug

0
src/views/page/index/index.pug → src/presentation/views/page/index/index.pug

0
src/views/page/index/person.pug → src/presentation/views/page/index/person.pug

0
src/views/page/login/index.pug → src/presentation/views/page/login/index.pug

0
src/views/page/notice/index.pug → src/presentation/views/page/notice/index.pug

0
src/views/page/profile/index.pug → src/presentation/views/page/profile/index.pug

0
src/views/page/register/index.pug → src/presentation/views/page/register/index.pug

222
src/services/README.md

@ -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}`)
}
}
}
```

36
src/services/index.js

@ -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
}

0
src/utils/BaseSingleton.js → src/shared/utils/BaseSingleton.js

14
src/utils/ForRegister.js → src/shared/utils/ForRegister.js

@ -2,7 +2,7 @@
// 兼容传统 routes 方式和自动注册 controller 方式
import fs from "fs"
import path from "path"
import { logger } from "@/logger.js"
import { logger } from "../../app/bootstrap/logger.js"
// 保证不会被摇树(tree-shaking),即使在生产环境也会被打包
if (import.meta.env.PROD) {
@ -20,10 +20,10 @@ if (import.meta.env.PROD) {
* @param {string} prefix - 路由前缀
* @param {Set<string>} [manualControllers] - 可选手动传入已注册 controller 文件名集合优先于自动扫描
*/
export function autoRegisterControllers(app, controllersDir) {
export async function autoRegisterControllers(app, controllersDir) {
let allRouter = []
function scan(dir, routePrefix = "") {
async function scan(dir, routePrefix = "") {
try {
for (const file of fs.readdirSync(dir)) {
const fullPath = path.join(dir, file)
@ -31,12 +31,12 @@ export function autoRegisterControllers(app, controllersDir) {
if (stat.isDirectory()) {
if (!file.startsWith("_")) {
scan(fullPath, routePrefix + "/" + file)
await scan(fullPath, routePrefix + "/" + file)
}
} else if (file.endsWith("Controller.js") && !file.startsWith("_")) {
try {
// 使用同步的import方式,确保ES模块兼容性
const controllerModule = require(fullPath)
// 使用动态import方式,确保ES模块兼容性
const controllerModule = await import(`file://${fullPath}`)
const controller = controllerModule.default || controllerModule
if (!controller) {
@ -72,7 +72,7 @@ export function autoRegisterControllers(app, controllersDir) {
}
try {
scan(controllersDir)
await scan(controllersDir)
if (allRouter.length === 0) {
logger.warn("[路由注册] ⚠️ 未发现任何可注册的控制器")

0
src/utils/bcrypt.js → src/shared/utils/bcrypt.js

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save