create-dp-koa 1.1.11 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.cursor/rules/00-backend-core.skill.md +2 -2
- package/template/.cursor/rules/01-backend-skill-router.skill.md +2 -0
- package/template/.cursor/rules/11-backend-controller-recipes.skill.md +198 -20
- package/template/.cursor/rules/30-backend-validation.skill.md +8 -30
- package/template/.cursor/rules/34-backend-art-template.skill.md +172 -0
- package/template/.cursor/rules/50-backend-bootstrap-lifecycle.skill.md +4 -4
- package/template/.cursor/rules/60-backend-router-registration.skill.md +2 -2
- package/template/.cursor/rules/70-backend-middleware.skill.md +4 -4
- package/template/.cursor/rules/80-backend-utils-and-libs.skill.md +3 -2
- package/template/.cursor/rules/85-backend-plugins.rule.md +34 -276
- package/template/.cursor/rules/README.md +8 -1
- package/template/.trae/skills/00-backend-core.skill.md +1 -1
- package/template/.trae/skills/70-backend-middleware.skill.md +2 -2
- package/template/package.json +1 -1
- package/template/src/app.ts +3 -3
- package/template/src/controllers/example/ExampleController.ts +1 -1
- package/template/src/plugins/registry.ts +4 -4
- package/template/src/plugins/weboffice/http/routes.ts +1 -1
- package/template/src/plugins/weboffice/index.ts +1 -1
- package/template/src/routers/index.ts +1 -1
package/package.json
CHANGED
|
@@ -5,14 +5,14 @@ alwaysApply: true
|
|
|
5
5
|
|
|
6
6
|
## 适用范围
|
|
7
7
|
- 本项目所有 TypeScript 后端代码
|
|
8
|
-
- 技术栈:Koa + TypeORM + dp-ioc2 +
|
|
8
|
+
- 技术栈:Koa + TypeORM + dp-ioc2 + 自定义装饰器体系;框架以 npm 包 **`dp-koa-framework-core`**(运行时与对外稳定 API)与 **`dp-koa-framework-libs`**(共享适配/配套能力)引入,版本以项目 `package.json` 为准。
|
|
9
9
|
- **包管理工具:必须使用 yarn(禁止使用 npm)**
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## 运行环境判定(debug 口径,必须)
|
|
14
14
|
|
|
15
|
-
**唯一标准**:以
|
|
15
|
+
**唯一标准**:以 **`dp-koa-framework-core`** 导出的运行环境判定 API(`isDebug()`、`getRuntimeEnvironmentLabel()` 等)为准,**不要**用 `process.env.NODE_ENV` 作为“是否生产/是否调试”的单一判断。
|
|
16
16
|
|
|
17
17
|
- **`isDebug()`**:当进程启动参数中包含 `--env=debug` 时为 `true`。
|
|
18
18
|
- **非 debug**:一律按**生产口径**处理(例如:迁移、静态资源长缓存、对外错误信息脱敏等)。
|
|
@@ -15,6 +15,7 @@ alwaysApply: true
|
|
|
15
15
|
|
|
16
16
|
### 1) 识别任务类型(多选)
|
|
17
17
|
- Controller/API 开发
|
|
18
|
+
- 服务端 SSR 模板(art-template、`views/`)
|
|
18
19
|
- DTO/参数校验
|
|
19
20
|
- Service/Repository/事务
|
|
20
21
|
- 错误处理/日志
|
|
@@ -29,6 +30,7 @@ alwaysApply: true
|
|
|
29
30
|
- **Controller/API**:
|
|
30
31
|
- `10-backend-api.skill.md`
|
|
31
32
|
-(需要范式/注解速查时)`11-backend-controller-recipes.skill.md`
|
|
33
|
+
- **服务端 SSR 模板(art-template)**:`34-backend-art-template.skill.md`
|
|
32
34
|
- **Service**:
|
|
33
35
|
- `21-backend-service.skill.md`
|
|
34
36
|
- **DTO/校验**:`30-backend-validation.skill.md`
|
|
@@ -12,23 +12,111 @@ alwaysApply: false
|
|
|
12
12
|
|
|
13
13
|
## 一、写 Controller 的推荐范式(必须对齐)
|
|
14
14
|
|
|
15
|
-
### 1.1
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
### 1.1 内嵌参考样例(本 Skill 自包含)
|
|
16
|
+
以下为从本项目常用写法整理的最小示例;**实现新接口时请先对齐本节的装饰器与类型风格**。若仓库中另有 `src/controllers/example/*` 等运行时代码,可作为补充参考,但**不必依赖**特定业务 Controller 文件是否存在。
|
|
17
|
+
|
|
18
|
+
#### 1.1.1 `BaseController` 与 `ControllerResponse`(统一响应)
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
export class ControllerResponse<T> {
|
|
22
|
+
code = 0;
|
|
23
|
+
data: T | null = null;
|
|
24
|
+
message = '';
|
|
25
|
+
|
|
26
|
+
constructor(code: number, data: T | null, message?: string) {
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.data = data;
|
|
29
|
+
if (message) this.message = message;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class BaseController {
|
|
34
|
+
success<T>(data: T, message?: string) {
|
|
35
|
+
return new ControllerResponse<T>(0, data, message);
|
|
36
|
+
}
|
|
37
|
+
fail<T>(code: number, message?: string) {
|
|
38
|
+
return new ControllerResponse<T>(code, null, message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### 1.1.2 `@State()`:读取整段 `ctx.state`
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Get, State } from 'dp-koa-framework-core';
|
|
47
|
+
import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
|
|
48
|
+
|
|
49
|
+
export class ExampleStateController extends BaseController {
|
|
50
|
+
@Get('/example/state-full')
|
|
51
|
+
async getWithFullState(
|
|
52
|
+
@State() state: { user: { userId: number; type?: number } },
|
|
53
|
+
): Promise<ControllerResponse<{ userId: number }>> {
|
|
54
|
+
return this.success({ userId: state.user.userId });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### 1.1.3 `@State('user')`:只读 `ctx.state.user`
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { Get, State } from 'dp-koa-framework-core';
|
|
63
|
+
import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
|
|
64
|
+
|
|
65
|
+
export class ExampleUserSliceController extends BaseController {
|
|
66
|
+
@Get('/example/state-user')
|
|
67
|
+
async getWithUser(
|
|
68
|
+
@State('user') user: { userId: number; type?: number },
|
|
69
|
+
): Promise<ControllerResponse<{ ok: boolean }>> {
|
|
70
|
+
return this.success({ ok: user.userId > 0 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### 1.1.4 `@ResponseValidateIf`:仅在条件成立时校验响应体
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { Get, State, ResponseValidateIf } from 'dp-koa-framework-core';
|
|
79
|
+
import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
|
|
80
|
+
|
|
81
|
+
// 示例 DTO:按实际接口替换
|
|
82
|
+
class UserProfileResponseDto {
|
|
83
|
+
id!: number;
|
|
84
|
+
nickName!: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class ExampleResponseValidateController extends BaseController {
|
|
88
|
+
@ResponseValidateIf(UserProfileResponseDto, (data) => Boolean(data && data.data))
|
|
89
|
+
@Get('/example/profile')
|
|
90
|
+
async getProfile(
|
|
91
|
+
@State() state: { user: { userId: number } },
|
|
92
|
+
): Promise<ControllerResponse<UserProfileResponseDto | null>> {
|
|
93
|
+
return this.success({
|
|
94
|
+
id: state.user.userId,
|
|
95
|
+
nickName: 'demo',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
20
100
|
|
|
21
101
|
### 1.2 Controller 标准骨架(复制这个结构)
|
|
22
102
|
目标:DTO 入参 → 调用 Service → 统一响应(`success/fail`)→ try/catch + logger
|
|
23
103
|
|
|
24
104
|
```ts
|
|
25
|
-
import {
|
|
105
|
+
import {
|
|
106
|
+
Get,
|
|
107
|
+
Post,
|
|
108
|
+
Query,
|
|
109
|
+
Body,
|
|
110
|
+
State,
|
|
111
|
+
ResponseCode,
|
|
112
|
+
ResponseHeader,
|
|
113
|
+
logger,
|
|
114
|
+
CommonServiceResultCode,
|
|
115
|
+
} from 'dp-koa-framework-core';
|
|
26
116
|
import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
|
|
27
117
|
import { Inject } from 'dp-ioc2';
|
|
28
|
-
import { logger } from '@src/framework/utils/logger';
|
|
29
118
|
import { SomeService } from '@src/service/some.service';
|
|
30
119
|
import { SomeQueryDto, SomeBodyDto } from '@src/dto/controller/xxx/some.controller.dto';
|
|
31
|
-
import { CommonServiceResultCode } from '@src/framework/types/ServiceResult';
|
|
32
120
|
|
|
33
121
|
export class SomeController extends BaseController {
|
|
34
122
|
@Inject(SomeService)
|
|
@@ -37,9 +125,9 @@ export class SomeController extends BaseController {
|
|
|
37
125
|
@Get('/some/path')
|
|
38
126
|
@ResponseCode(200)
|
|
39
127
|
async getSomething(
|
|
40
|
-
@Query() query: SomeQueryDto,
|
|
128
|
+
@Query(SomeQueryDto) query: SomeQueryDto,
|
|
41
129
|
@State('user') user: { userId: number; type?: number },
|
|
42
|
-
): Promise<ControllerResponse<
|
|
130
|
+
): Promise<ControllerResponse<any>> {
|
|
43
131
|
try {
|
|
44
132
|
const result = await this.someService.someMethod(query, user.userId);
|
|
45
133
|
if (result.code !== CommonServiceResultCode.SUCCESS) {
|
|
@@ -59,7 +147,6 @@ export class SomeController extends BaseController {
|
|
|
59
147
|
- Controller **不直接访问数据库/Repository**
|
|
60
148
|
- Controller **不 return Service 原始结果**,必须映射 `success/fail`
|
|
61
149
|
- Controller `async` **必须 try/catch**
|
|
62
|
-
!- Controller 的返回类型 `ControllerResponse<T>` **必须显式指定 T,禁止使用 `any`**
|
|
63
150
|
|
|
64
151
|
---
|
|
65
152
|
|
|
@@ -72,19 +159,17 @@ export class SomeController extends BaseController {
|
|
|
72
159
|
### 2.2 参数注解(重点)
|
|
73
160
|
- **`@Query()`**:
|
|
74
161
|
- 读取 query 参数
|
|
75
|
-
- 推荐写法:`@Query() query: XxxQueryDto`
|
|
162
|
+
- 推荐写法:`@Query(SomeQueryDto) query: XxxQueryDto`
|
|
76
163
|
- **`@Body()`**:
|
|
77
164
|
- 读取 body 参数
|
|
78
|
-
- 推荐写法:`@Body() body: XxxBodyDto`
|
|
79
|
-
- **`@Params()`**:
|
|
80
|
-
- 读取路径参数(如 `/:id`、`/:id/files/:fileId`)
|
|
81
|
-
- 推荐写法:`@Params(ParamsDto) params: XxxParamsDto`
|
|
165
|
+
- 推荐写法:`@Body(SomeBodyDto) body: XxxBodyDto`
|
|
82
166
|
- **`@State()`**:
|
|
83
|
-
- 读取整个 `ctx.state
|
|
167
|
+
- 读取整个 `ctx.state`(样例:见 **1.1.2**)
|
|
84
168
|
- 推荐写法:`@State() state: { user: { userId: number; type?: number } }`
|
|
85
169
|
- **`@State('user')`**:
|
|
86
|
-
- 读取 `ctx.state.user
|
|
170
|
+
- 读取 `ctx.state.user`(样例:见 **1.1.3**)
|
|
87
171
|
- 推荐写法:`@State('user') user: { userId: number; type?: number }`
|
|
172
|
+
- **`@Session` / `@Cookie`**:会话与普通 Cookie 的注入规则见 **2.5**(均从 **`dp-koa-framework-core`** 导入)
|
|
88
173
|
|
|
89
174
|
禁止:
|
|
90
175
|
- ❌ `@Query() query: any` / `@Body() body: any` / `@State() state: any`(尽量不要)
|
|
@@ -92,10 +177,97 @@ export class SomeController extends BaseController {
|
|
|
92
177
|
### 2.3 响应元信息注解
|
|
93
178
|
- **`@ResponseCode(200|201|...)`**:设置 HTTP 状态码(样例中 GET 用 200,POST 用 201)
|
|
94
179
|
- **`@ResponseHeader(key, value)`**:为响应添加 header
|
|
180
|
+
- **HTTP 3xx 重定向**:使用 **`return redirect(...)`**(**`dp-koa-framework-core`**),规则见 **§2.6**;与 `this.success` / JSON 体返回**互斥**。
|
|
95
181
|
|
|
96
182
|
### 2.4 响应校验注解(进阶,按需)
|
|
97
183
|
- **`@ResponseValidator(DtoClass, objectKey?)`**:对响应数据做校验
|
|
98
|
-
- **`@ResponseValidateIf(DtoClass, fn)`**:仅当 fn
|
|
184
|
+
- **`@ResponseValidateIf(DtoClass, fn)`**:仅当 fn 返回真时才校验(样例:见 **1.1.4**)
|
|
185
|
+
|
|
186
|
+
### 2.5 `@Session` 与 `@Cookie`(**`dp-koa-framework-core`**)
|
|
187
|
+
|
|
188
|
+
均由 **`dp-koa-framework-core`** 导出(与 `@Get` / `@State` 同源:`import { Session, Cookie, ... } from 'dp-koa-framework-core'`)。
|
|
189
|
+
|
|
190
|
+
#### `@Session`(`ctx.session`)
|
|
191
|
+
|
|
192
|
+
- **何时用**:与 JWT + `ctx.state`(`@State`)**并存**;适合 OAuth2/OIDC/SSO 等需要在服务端存**短期握手数据**的场景,**不替代**鉴权中间件写入的 `ctx.state.user`。
|
|
193
|
+
- **谁负责启用**:**`dp-koa-framework-core`** 在 `bootstrap` 中已注册 `koa-session`,业务 **`src/app.ts` 一般不要再 `app.use(session(...))`**。
|
|
194
|
+
- **语义与绑定数据**:
|
|
195
|
+
- `@Session()`:注入**整段** `ctx.session`(类型建议用 interface/明确结构,避免裸 `any`)。
|
|
196
|
+
- `@Session('uid')`:注入 `ctx.session['uid']`(字符串参数表示 **key**)。
|
|
197
|
+
- `@Session(SomeDtoClass)`:传入 **class** 时,与 `@Body(SomeDto)` 类似,会对**当前取到的 session 对象**(无 key 时为整段 session)做 **class-transformer + class-validator**。
|
|
198
|
+
- **默认行为(框架内建)**:会话 Cookie 名默认 `dpkoa.sid`、`httpOnly`、`signed`、`rolling`、`sameSite: 'lax'` 等由 **`dp-koa-framework-core`** 的 bootstrap 与 `koa-session` 决定;可用环境变量覆盖。
|
|
199
|
+
- **环境变量(摘要)**:
|
|
200
|
+
- `SESSION_KEYS`:逗号分隔多 key(**优先**作签名密钥);
|
|
201
|
+
- `SESSION_SECRET`:单 key(无 `SESSION_KEYS` 时使用);
|
|
202
|
+
- `SESSION_KEY`:Cookie 名;
|
|
203
|
+
- `SESSION_MAX_AGE_MS`:过期毫秒数。
|
|
204
|
+
- **实践要求**:生产**必须**配置 `SESSION_KEYS` 或 `SESSION_SECRET`;Session 仅存短期字段,流程结束**清理**一次性键;跨站回调重点核对 `SameSite`、HTTPS、域名与代理头。
|
|
205
|
+
- **延伸阅读**:`docs/SESSION_DECORATOR_GUIDE.md`(Session 环境变量与更多示例)。
|
|
206
|
+
|
|
207
|
+
#### `@Cookie`(读 / 写 Cookie)
|
|
208
|
+
|
|
209
|
+
- **`@Cookie('name')`**:读取名为 `name` 的单个 Cookie;底层为带 **`signed: true`** 的 `ctx.cookies.get`。
|
|
210
|
+
- **`@Cookie()`**(无参):注入 **`CookieHandle`**:`{ get(name, opts?), set(name, value, opts?) }`,默认读写仍带 **`signed: true`**,可在 `opts` 中按需覆盖。
|
|
211
|
+
- **硬性限制**:**不支持** `@Cookie(SomeDtoClass)`;装饰器要求参数为 **string 或省略**,否则**抛错**。需要多字段时多次 `@Cookie('a')` / `@Cookie('b')`,或极少数场景自行解析(仍保持 Controller 薄)。
|
|
212
|
+
- **前置条件**:依赖 Koa 标准 **`ctx.cookies`**;`set` 时按需传入 `maxAge`、`path`、`httpOnly`、`sameSite` 等安全相关选项。
|
|
213
|
+
|
|
214
|
+
#### 示例(Session / Cookie / State 组合)
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { Get, Session, Cookie, State } from 'dp-koa-framework-core';
|
|
218
|
+
import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
|
|
219
|
+
|
|
220
|
+
export class OauthDemoController extends BaseController {
|
|
221
|
+
@Get('/oauth/callback')
|
|
222
|
+
async callback(
|
|
223
|
+
@Session('oauthState') oauthState: string | undefined,
|
|
224
|
+
@State('user') user: { userId: number } | undefined,
|
|
225
|
+
): Promise<ControllerResponse<{ ok: boolean }>> {
|
|
226
|
+
return this.success({ ok: Boolean(oauthState && user) });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@Get('/prefs')
|
|
230
|
+
async prefs(@Cookie('theme') theme: string | undefined): Promise<ControllerResponse<{ theme: string | null }>> {
|
|
231
|
+
return this.success({ theme: theme ?? null });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@Get('/notice-dismiss')
|
|
235
|
+
async dismiss(
|
|
236
|
+
@Cookie() jar: { get: (name: string, opts?: object) => string | undefined; set: (name: string, value: string | null, opts?: object) => void },
|
|
237
|
+
): Promise<ControllerResponse<null>> {
|
|
238
|
+
jar.set('notice', '', { maxAge: 0 });
|
|
239
|
+
return this.success(null);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 2.6 声明式 HTTP 重定向(`redirect`)
|
|
245
|
+
|
|
246
|
+
- **导入**:`import { redirect } from 'dp-koa-framework-core'`。另导出 `RedirectResponse`、`RedirectStatusCode`、`isRedirectResponse`、`applyRedirectToCtx`(后两者多用于中间件或脱离 `bindRouter` 的手动写入;常规控制器 **`return redirect(...)`** 即可)。
|
|
247
|
+
- **基本用法**:**`return redirect(location)`**;`location` 将作为 **`Location`** 响应头。可选 **`redirect(location, { status?, headers? })`**。
|
|
248
|
+
- **`status`**:仅 **`302` | `303` | `307` | `308`**,默认 **`302`**。
|
|
249
|
+
- **路由层**:识别重定向后写入 **`ctx.status` / `Location` / `Cache-Control: no-store`**,**`ctx.body = ''`** 并短路;**跳过**响应 DTO 校验与 JSON 赋值。
|
|
250
|
+
- **与 `@ControllerCache`**:命中缓存时直接返回缓存体,与本次 `redirect` 二选一;勿在同一方法混用两种出口语义。
|
|
251
|
+
- **不要**与 `this.success` / `ControllerResponse` 混用;跳转方法可省略 `@ResponseCode`(最终以 `redirect` 的 `status` 为准)。
|
|
252
|
+
- **开放重定向**:对用户可控 `next`、回调 URL 等做白名单或站内路径约束,并正确编码 query。
|
|
253
|
+
- **延伸阅读**:`packages/dp-koa-framework-core/README.md` 中「HTTP 重定向(SSO 常用)」。
|
|
254
|
+
|
|
255
|
+
#### 示例(SSO 入口)
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { Get, Query, redirect } from 'dp-koa-framework-core';
|
|
259
|
+
import { BaseController } from '@src/controllers/base.controller';
|
|
260
|
+
|
|
261
|
+
export class SsoController extends BaseController {
|
|
262
|
+
@Get('/sso/login')
|
|
263
|
+
async login(@Query() query: { next?: string }): Promise<ReturnType<typeof redirect>> {
|
|
264
|
+
const next = query?.next;
|
|
265
|
+
const safeNext = next && next.startsWith('/') ? next : '/';
|
|
266
|
+
const url = `https://sso.example.com/authorize?next=${encodeURIComponent(safeNext)}`;
|
|
267
|
+
return redirect(url, { status: 302 });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
99
271
|
|
|
100
272
|
---
|
|
101
273
|
|
|
@@ -105,9 +277,15 @@ export class SomeController extends BaseController {
|
|
|
105
277
|
- `@Get('/xxx') + @State('user')`(或 `@State()` 取整段 state)
|
|
106
278
|
|
|
107
279
|
### 3.2 带 body 的 POST 接口
|
|
108
|
-
- `@Post('/xxx') + @Body() body:
|
|
280
|
+
- `@Post('/xxx') + @Body(SomeBodyDto) body: SomeBodyDto + @ResponseCode(201)`
|
|
109
281
|
|
|
110
282
|
### 3.3 需要自定义 header
|
|
111
283
|
- `@ResponseHeader('X-XXX', 'value')`
|
|
112
284
|
|
|
285
|
+
### 3.4 Session 与登录态 / OAuth
|
|
286
|
+
- 回调或握手:`@Session('state')` / `@Session()` 承载临时字段,与 `@State('user')` 分职责(Session=临时、State=已鉴权用户)。
|
|
287
|
+
- 非 Session 的普通 Cookie:在方法内使用 `@Cookie()` 注入的 `CookieHandle.set(...)`,避免在 Controller 外散落 `ctx.cookies`。
|
|
288
|
+
|
|
289
|
+
### 3.5 HTTP 重定向(SSO / 登录页)
|
|
290
|
+
- **`return redirect(url)`** 或带 **`{ status: 303 }`** 等选项,详见 **§2.6**;与 **`this.success` / `ControllerResponse`** 不要混在同一返回路径。
|
|
113
291
|
|
|
@@ -124,7 +124,7 @@ async createUser(@Body(CreateUserControllerDto) body: CreateUserControllerDto):
|
|
|
124
124
|
#### 响应 DTO 使用
|
|
125
125
|
```typescript
|
|
126
126
|
import { GetUserInfoResponseDto } from '@src/dto/controller/home/ytUser.controller.dto';
|
|
127
|
-
import { ResponseValidateIf } from '
|
|
127
|
+
import { ResponseValidateIf } from 'dp-koa-framework-core';
|
|
128
128
|
|
|
129
129
|
// 使用 @ResponseValidateIf 装饰器校验响应数据
|
|
130
130
|
@ResponseValidateIf(GetUserInfoResponseDto, (data) => data && data.data)
|
|
@@ -199,9 +199,8 @@ async createUser(@Body(CreateUserControllerDto) body: CreateUserControllerDto) {
|
|
|
199
199
|
- 常用装饰器:`@IsString()`, `@IsEmail()`, `@IsNotEmpty()`, `@MinLength()`, `@IsOptional()` 等
|
|
200
200
|
|
|
201
201
|
### 4.2 Controller DTO 特殊处理
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
- 可以包含 HTTP 层特定字段(如 `vcodeToken`、`requestId`)。
|
|
202
|
+
- 可以使用 `@Transform` 进行数据转换(如字符串转数字、trim)
|
|
203
|
+
- 可以包含 HTTP 层特定的字段(如 `vcodeToken`、`requestId`)
|
|
205
204
|
|
|
206
205
|
### 4.3 Service DTO 要求
|
|
207
206
|
- **禁止**包含 HTTP 层特性(如 `@Transform`、HTTP 特定字段)
|
|
@@ -210,30 +209,17 @@ async createUser(@Body(CreateUserControllerDto) body: CreateUserControllerDto) {
|
|
|
210
209
|
|
|
211
210
|
### 4.4 示例对比
|
|
212
211
|
|
|
213
|
-
#### Controller DTO(包含 HTTP
|
|
212
|
+
#### Controller DTO(包含 HTTP 层转换)
|
|
214
213
|
```typescript
|
|
215
|
-
import { Transform } from 'class-transformer';
|
|
216
|
-
|
|
217
214
|
export class CreateUserControllerDto {
|
|
218
|
-
@Transform((
|
|
215
|
+
@Transform((val) => Trim(String(val)))
|
|
219
216
|
@IsString()
|
|
220
217
|
@IsEmail()
|
|
221
218
|
email: string;
|
|
222
219
|
|
|
223
|
-
|
|
224
|
-
@
|
|
225
|
-
|
|
226
|
-
@IsInt()
|
|
227
|
-
age?: number;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Query 中单字符串/逗号分隔转数组示例
|
|
231
|
-
export class QueryDto {
|
|
232
|
-
@Transform(({ value }) => toTagsArray(value)) // 如 "a,b" -> ["a","b"],已是数组则原样
|
|
233
|
-
@IsOptional()
|
|
234
|
-
@IsArray()
|
|
235
|
-
@IsString({ each: true })
|
|
236
|
-
tags?: string[];
|
|
220
|
+
@Transform((val) => Number(val))
|
|
221
|
+
@IsNumber()
|
|
222
|
+
age: number;
|
|
237
223
|
}
|
|
238
224
|
```
|
|
239
225
|
|
|
@@ -249,14 +235,6 @@ export class CreateUserServiceDto {
|
|
|
249
235
|
}
|
|
250
236
|
```
|
|
251
237
|
|
|
252
|
-
### 4.5 class-transformer 与 Controller 参数转换(框架内置)
|
|
253
|
-
|
|
254
|
-
- **框架行为**:对 `@Body(Dto)` / `@Query(Dto)` / `@Params(Dto)`,框架(dp-koa-framework)会使用 **class-transformer** 的 `plainToInstance(Dto, data, { enableImplicitConversion: true })` 从原始请求数据构建 DTO 实例,再经 **class-validator** 校验;校验通过后将该实例写回控制器参数,因此控制器方法收到的是**已转换且已校验的 DTO 实例**,而非原始 `ctx.body` / `ctx.query` / `ctx.params`。
|
|
255
|
-
- **Query/Params 类型**:HTTP Query 与 Params 均为字符串。启用 `enableImplicitConversion` 后,DTO 中声明为 `number` 的字段(如 `page`、`pageSize`)会自动从字符串转为数字,**无需**在 Controller DTO 上为每个字段写 `@Transform`。
|
|
256
|
-
- **适用范围**:仅对 **Body、Query、Params** 做转换与校验;**State、Headers** 不经过 plainToInstance,保持原样传入。
|
|
257
|
-
- **自定义转换**:若需更细粒度控制(如逗号分隔字符串转数组、trim),在 Controller DTO 上使用 **class-transformer** 的 `@Transform`:`import { Transform } from 'class-transformer'`,写法为 `@Transform(({ value }) => yourFn(value))`,在 plainToInstance 时自动应用。
|
|
258
|
-
- **DtoValidator**:在 Service 层手动调用 `DtoValidator.validate` 时,框架同样使用 `plainToInstance` + `enableImplicitConversion`,与上述参数绑定行为一致。
|
|
259
|
-
|
|
260
238
|
---
|
|
261
239
|
|
|
262
240
|
## 五、校验流程
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
alwaysApply: false
|
|
3
|
+
---
|
|
4
|
+
# Skill:服务端模板(art-template)
|
|
5
|
+
|
|
6
|
+
## 适用与触发
|
|
7
|
+
- 新增或修改 **SSR 页面**、`views/**/*.html`、或入口里 **`initArtTemplate`** / Controller 里 **`return template(...)`** 时启用本 Skill。
|
|
8
|
+
- 触发口令建议:「请启用 `34-backend-art-template.skill.md`,按项目约定写模板」。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 一、入口初始化(必须)
|
|
13
|
+
|
|
14
|
+
- 在应用入口链路中(例如 `setBeforeBootstrap`)调用 **`initArtTemplate`**,早于会 `return template(...)` 的路由生效即可。
|
|
15
|
+
- 典型配置:`root` 指向项目根下 **`views/`**,**`extname: '.html'`**,按需 **`enabled`**。
|
|
16
|
+
- **`enabled: false`** 时不得再 `return template(...)`(运行时会抛错)。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 二、Controller 写法(与 redirect 一致)
|
|
21
|
+
|
|
22
|
+
- 使用 **`return template('相对 views 的路径(不含扩展名)', data)`**;由 **`dp-koa-framework-core`** 路由层渲染为 HTML。
|
|
23
|
+
- **不要**在 Controller 里为 SSR 直接改 **`ctx.body`** 或拼接 HTML 字符串(与 `redirect()` 的声明式风格保持一致)。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 三、视图文件拆分约定
|
|
28
|
+
|
|
29
|
+
| 类型 | 内容 | 说明 |
|
|
30
|
+
|------|------|------|
|
|
31
|
+
| **页面模板** | 完整 **`<!DOCTYPE html>` → `<html>` → `<head>` → `<body>` → … → `</html>`** | 页面级文件保留文档骨架;正文宜放在 **`<main>`**(或语义等价区域) |
|
|
32
|
+
| **片段模板** | 仅可嵌入片段,如单个 **`<header>...</header>`**、**`<footer>...</footer>`** | **禁止**在片段内再写一套 `DOCTYPE` / `html` / `head` / `body` 外壳 |
|
|
33
|
+
| **嵌入** | **`{{include './partials/文件名'}}`** | 路径相对**当前模板文件**所在目录;移动文件时需同步修正 include |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 四、art-template 语法要点
|
|
38
|
+
|
|
39
|
+
- **条件**:`{{if cond}} ... {{/if}}`,可选 `{{else}}`。
|
|
40
|
+
- **列表(v4)**:`{{each list item}}` … `{{/each}}`,循环体内用 **`{{ item.field }}`**(避免旧写法 `each list as item` 的升级告警)。
|
|
41
|
+
- **字面量**:模板正文中**不要**出现会被误解析的 **`{{...}}`** 占位展示(例如展示双大括号语法);改用说明文字或 HTML 实体等。
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 五、缓存与性能
|
|
46
|
+
|
|
47
|
+
- **`@ControllerCache` + `template()`** 时,框架缓存的是**渲染后的 HTML**;**`cacheyFn`** 的返回值必须随影响页面内容的入参(`query` / `params` / 用户态等)变化,避免错页。
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 六、依赖
|
|
52
|
+
|
|
53
|
+
- 应用 **`package.json`** 需包含 **`art-template`**(与 `dp-koa-framework-core` 的实现对齐)。
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 七、示例代码(可复制)
|
|
58
|
+
|
|
59
|
+
### 7.1 入口:`initArtTemplate`(如 `setBeforeBootstrap` 内)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import path from "path";
|
|
63
|
+
import { initArtTemplate } from "dp-koa-framework-core";
|
|
64
|
+
|
|
65
|
+
initArtTemplate({
|
|
66
|
+
enabled: true,
|
|
67
|
+
root: path.join(process.cwd(), "views"),
|
|
68
|
+
extname: ".html",
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 7.2 Controller:`return template(...)`(与 `redirect` 同风格)
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { Get, template } from "dp-koa-framework-core";
|
|
76
|
+
import { BaseController } from "@src/controllers/base.controller";
|
|
77
|
+
|
|
78
|
+
export class PageController extends BaseController {
|
|
79
|
+
@Get("/demo/page/hello")
|
|
80
|
+
hello() {
|
|
81
|
+
return template("demo/hello", {
|
|
82
|
+
title: "标题",
|
|
83
|
+
siteName: "站点名",
|
|
84
|
+
footerText: "页脚文案",
|
|
85
|
+
year: new Date().getFullYear(),
|
|
86
|
+
message: "正文说明",
|
|
87
|
+
showNotice: true,
|
|
88
|
+
showVip: false,
|
|
89
|
+
steps: [
|
|
90
|
+
{ name: "步骤一", done: true },
|
|
91
|
+
{ name: "步骤二", done: false },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 7.3 页面模板:`views/demo/hello.html`(整页骨架 + include 片段)
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<!DOCTYPE html>
|
|
102
|
+
<html lang="zh-CN">
|
|
103
|
+
<head>
|
|
104
|
+
<meta charset="utf-8" />
|
|
105
|
+
<title>{{ title }}</title>
|
|
106
|
+
</head>
|
|
107
|
+
<body>
|
|
108
|
+
{{include './partials/site-header'}}
|
|
109
|
+
|
|
110
|
+
<main>
|
|
111
|
+
<h1>{{ title }}</h1>
|
|
112
|
+
<p>{{ message }}</p>
|
|
113
|
+
{{if showNotice}}
|
|
114
|
+
<aside>通知区域</aside>
|
|
115
|
+
{{/if}}
|
|
116
|
+
<ul>
|
|
117
|
+
{{each steps step}}
|
|
118
|
+
<li>{{ step.name }} — {{if step.done}}已完成{{else}}待完成{{/if}}</li>
|
|
119
|
+
{{/each}}
|
|
120
|
+
</ul>
|
|
121
|
+
</main>
|
|
122
|
+
|
|
123
|
+
{{include './partials/site-footer'}}
|
|
124
|
+
</body>
|
|
125
|
+
</html>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 7.4 片段模板(仅片段,无 `DOCTYPE` / `html` / `body` 外壳)
|
|
129
|
+
|
|
130
|
+
`views/demo/partials/site-header.html`:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<header>
|
|
134
|
+
<strong>{{ siteName }}</strong>
|
|
135
|
+
</header>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`views/demo/partials/site-footer.html`:
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<footer>
|
|
142
|
+
<p>{{ footerText }} · {{ year }}</p>
|
|
143
|
+
</footer>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 7.5 可选:`@ControllerCache`(`cacheyFn` 形参与方法入参顺序一致)
|
|
147
|
+
|
|
148
|
+
`cacheyFn` 会收到**与方法参数相同顺序**的注入值;下面示例第一个入参为 `@Query()`,缓存 key 必须带上影响 HTML 的 `lang`。
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { Get, Query, template, ControllerCache } from "dp-koa-framework-core";
|
|
152
|
+
import { BaseController } from "@src/controllers/base.controller";
|
|
153
|
+
|
|
154
|
+
export class CachedPageController extends BaseController {
|
|
155
|
+
@Get("/demo/page/cached")
|
|
156
|
+
@ControllerCache((query: any) => `demo-hello:${String(query?.lang ?? "zh")}`, { ttl: 60 })
|
|
157
|
+
cachedHtml(@Query() query: any) {
|
|
158
|
+
return template("demo/hello", {
|
|
159
|
+
title: "缓存页",
|
|
160
|
+
siteName: "Demo",
|
|
161
|
+
footerText: "footer",
|
|
162
|
+
year: new Date().getFullYear(),
|
|
163
|
+
message: `lang=${query?.lang ?? "zh"}`,
|
|
164
|
+
showNotice: false,
|
|
165
|
+
showVip: false,
|
|
166
|
+
steps: [],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> 说明:多入参时 **`cacheyFn` 的参数列表顺序** 须与 **`@Body` / `@Query` / `@Params` …** 在方法上的声明顺序一致,否则 key 会错位。
|
|
@@ -4,14 +4,14 @@ alwaysApply: false
|
|
|
4
4
|
# Skill:启动生命周期规范(setBeforeBootstrap / setAfterBootstrap)
|
|
5
5
|
|
|
6
6
|
## 适用与触发(重要)
|
|
7
|
-
- 仅当需要修改/新增启动逻辑(`src/app.ts
|
|
7
|
+
- 仅当需要修改/新增启动逻辑(`src/app.ts`)或理解/调整对 **`dp-koa-framework-core`** 暴露的启动 API(`bootstrap`、`setBeforeBootstrap` 等)的接入方式时启用本 Skill;若需修改框架内置启动顺序,应到 **`dp-koa-framework-core`** 包源码修改。
|
|
8
8
|
- 触发口令建议:
|
|
9
9
|
- “请按 `50-backend-bootstrap-lifecycle.skill.md` 调整启动流程”
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## 一、真实执行顺序(必须理解并遵守)
|
|
14
|
-
以 `
|
|
14
|
+
以 **`dp-koa-framework-core`** 的 `bootstrap` 实现为准:
|
|
15
15
|
|
|
16
16
|
1. `bootstrap()` 内部 **先执行** `beforeBootstraps(app)`(如果已注册)
|
|
17
17
|
2. 然后框架挂载:CORS → 日志中间件 → 框架错误处理中间件 → `koaBody()`
|
|
@@ -36,9 +36,9 @@ alwaysApply: false
|
|
|
36
36
|
- 生产环境迁移(如有)
|
|
37
37
|
- 内存库模式初始化测试数据(如有)
|
|
38
38
|
|
|
39
|
-
### 2.1.1 环境变量文件与 `.env` 选择(对齐 `bootstrap
|
|
39
|
+
### 2.1.1 环境变量文件与 `.env` 选择(对齐 **`dp-koa-framework-core`** 的 `bootstrap`)
|
|
40
40
|
|
|
41
|
-
- **与 `NODE_ENV`
|
|
41
|
+
- **与 `NODE_ENV` 解耦**:**`dp-koa-framework-core`** 的 `bootstrap` 最早加载的 `.env` 文件由 **`isDebug()`** 决定:
|
|
42
42
|
- `--env=debug` → `.env.development`
|
|
43
43
|
- 否则 → `.env.production`
|
|
44
44
|
- **本地开发**:`package.json` 的 `dev` 建议在 `node dist/main.js` 后追加 `--env=debug`,避免误加载生产 env。
|
|
@@ -4,7 +4,7 @@ alwaysApply: false
|
|
|
4
4
|
# Skill:路由注册规范(routers/index.ts + bindRouter)
|
|
5
5
|
|
|
6
6
|
## 适用与触发(重要)
|
|
7
|
-
- 仅当需要新增/修改路由绑定(`src/routers/index.ts
|
|
7
|
+
- 仅当需要新增/修改路由绑定(`src/routers/index.ts`)或调整对 **`dp-koa-framework-core`** 导出的路由工具(如 `bindRouter`)的使用方式时启用本 Skill。
|
|
8
8
|
- 触发口令建议:
|
|
9
9
|
- “请按 `60-backend-router-registration.skill.md` 规范注册路由”
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ alwaysApply: false
|
|
|
12
12
|
|
|
13
13
|
## 一、路由注册机制(必须理解)
|
|
14
14
|
本项目路由注册入口:`src/routers/index.ts`
|
|
15
|
-
|
|
15
|
+
路由绑定工具:从 **`dp-koa-framework-core`** 导入 `bindRouter(...)`(业务侧通常在 `src/routers/index.ts` 使用)。
|
|
16
16
|
|
|
17
17
|
### 1.1 bindRouter(...) 的真实行为(以代码为准)
|
|
18
18
|
- 调用形式:
|
|
@@ -4,7 +4,7 @@ alwaysApply: false
|
|
|
4
4
|
# Skill:中间件开发与注册规范(src/middlewares)
|
|
5
5
|
|
|
6
6
|
## 适用与触发(重要)
|
|
7
|
-
- 仅当需要新增/修改中间件(`src/middlewares
|
|
7
|
+
- 仅当需要新增/修改中间件(`src/middlewares/*`)或调整中间件注册顺序(**`dp-koa-framework-core`** 的 `bootstrap` 默认链、`src/app.ts`、`src/routers/index.ts`)时启用本 Skill。
|
|
8
8
|
- 触发口令建议:
|
|
9
9
|
- “请按 `70-backend-middleware.skill.md` 新增/调整中间件”
|
|
10
10
|
|
|
@@ -62,9 +62,9 @@ alwaysApply: false
|
|
|
62
62
|
## 六、注册位置与顺序(必须)
|
|
63
63
|
|
|
64
64
|
### 6.1 全局中间件(bootstrap 阶段)
|
|
65
|
-
在 `
|
|
65
|
+
在 **`dp-koa-framework-core`** 的 `bootstrap` 流程中,框架默认仅挂载 **`loggingMiddleware`**(只记录 **HTTP 4xx/5xx** 与 **未捕获异常**;`ctx.logger` / `ctx.requestId` 仍注入供业务自行打日志):
|
|
66
66
|
- CORS
|
|
67
|
-
- loggingMiddleware
|
|
67
|
+
- loggingMiddleware(请求错误日志)
|
|
68
68
|
- frameworkErrorMiddleware(框架级错误处理中间件)
|
|
69
69
|
- koaBody
|
|
70
70
|
- router.routes/allowedMethods
|
|
@@ -94,7 +94,7 @@ alwaysApply: false
|
|
|
94
94
|
## 八、测试建议(可选但推荐)
|
|
95
95
|
- 单元测试优先覆盖:
|
|
96
96
|
- 鉴权中间件:缺 token / 无效 token / 有效 token 能写入 `ctx.state.user`
|
|
97
|
-
-
|
|
97
|
+
- 日志中间件:是否注入 `ctx.requestId` / `ctx.logger`(访问轨迹、慢请求等由业务自行记录)
|
|
98
98
|
- 静态中间件:prefix 正规化(`/static` vs `/static/`)
|
|
99
99
|
|
|
100
100
|
|
|
@@ -13,6 +13,7 @@ alwaysApply: false
|
|
|
13
13
|
|
|
14
14
|
## 一、总体原则
|
|
15
15
|
|
|
16
|
+
- **框架内置能力**:与框架重叠的通用工具/适配层,优先使用 **`dp-koa-framework-libs`** 或 **`dp-koa-framework-core`** 的稳定导出;业务目录下的 `src/libs` 仅承载**本项目特有**的适配与 SDK 封装。
|
|
16
17
|
- **libs**:更偏向“小框架 / 小 SDK / 第三方服务适配层”
|
|
17
18
|
- **utils**:更偏向“纯函数工具、小颗粒度无状态函数”
|
|
18
19
|
- 业务相关的工具优先放在**各自业务模块内**,而不是全局 `utils/`
|
|
@@ -91,9 +92,9 @@ alwaysApply: false
|
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
94
|
-
##
|
|
95
|
+
## 七、框架级运行环境判定(**`dp-koa-framework-core`**,必须)
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
以下 API 由 **`dp-koa-framework-core`** 提供,全项目应统一使用,避免散落 `NODE_ENV === 'production'` 判断。
|
|
97
98
|
|
|
98
99
|
| API | 含义 |
|
|
99
100
|
|-----|------|
|
|
@@ -1,307 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 后端插件体系规范(**`dp-koa-framework-core`** / 模板)
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 目标:让“独立功能体”以插件形式存在,可插拔、易维护、与宿主框架低耦合。
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 1. 插件的基本形态
|
|
8
8
|
|
|
9
|
-
- 插件是**独立功能域**:可以包含路由、Service、实体/迁移、脚本,但通过统一接口暴露给宿主。
|
|
10
9
|
- 插件统一放在 `src/plugins/<plugin-id>/` 目录下。
|
|
11
|
-
-
|
|
10
|
+
- 插件根目录只保留 `index.ts`(插件入口)。
|
|
11
|
+
- 插件入口必须导出一个 `PluginDescriptor`(类型由 **`dp-koa-framework-core`** 导出)。
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
import type { PluginDescriptor } from "@src/framework/plugins/types";
|
|
13
|
+
推荐目录结构:
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
entities: [
|
|
24
|
-
// 插件自有实体
|
|
25
|
-
],
|
|
26
|
-
};
|
|
27
|
-
```
|
|
15
|
+
- `src/plugins/<plugin-id>/index.ts`
|
|
16
|
+
- `src/plugins/<plugin-id>/core/`:类型、错误、上下文解析、纯函数工具
|
|
17
|
+
- `src/plugins/<plugin-id>/http/`:HTTP 路由入口(Koa Router)
|
|
18
|
+
- `src/plugins/<plugin-id>/entities/`:TypeORM 实体(插件自有表)
|
|
19
|
+
- `src/plugins/<plugin-id>/services/`:插件业务 Service
|
|
20
|
+
- `src/plugins/<plugin-id>/scripts/`:种子/运维脚本(可选)
|
|
28
21
|
|
|
29
22
|
---
|
|
30
23
|
|
|
31
|
-
## 2.
|
|
24
|
+
## 2. 宿主与插件的依赖方向
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
### 2.1 插件 → 宿主
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
- 插件内部业务代码应优先使用相对路径组织。
|
|
29
|
+
- 插件调用宿主业务能力(如用户、项目、权限)应通过 **plugin-api 门面层**(建议放在 `src/plugin-api/*`),避免直接 import 宿主 Service/Repository。
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
- `displayName?: string`:可读名称,用于日志与管理界面。
|
|
39
|
-
- `enabled?: boolean | (env: NodeJS.ProcessEnv) => boolean`:
|
|
40
|
-
- 未设置:默认启用;
|
|
41
|
-
- boolean:固定启/停;
|
|
42
|
-
- function:根据环境变量或配置动态启/停(如 `WEBOFFICE_ENABLED`)。
|
|
43
|
-
- 生命周期钩子:
|
|
44
|
-
- `onBeforeBootstrap?(app: Koa)`:Koa 启动前执行,适合注册中间件、事件等。
|
|
45
|
-
- `onAfterBootstrap?(app: Koa)`:Koa 启动后执行,适合注册定时任务、订阅等。
|
|
46
|
-
- 路由与实体:
|
|
47
|
-
- `registerRoutes?(router: Router)`:注册插件自己的 HTTP 路由。
|
|
48
|
-
- `entities?: Function[]`:插件自有 TypeORM 实体(仅包含插件表)。
|
|
31
|
+
### 2.2 宿主 → 插件
|
|
49
32
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
### 3.1 注册表
|
|
55
|
-
|
|
56
|
-
- 所有内置插件集中注册在 `src/framework/plugins/registry.ts`:
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
import { plugin as webofficePlugin } from "@src/plugins/weboffice";
|
|
60
|
-
|
|
61
|
-
export const plugins: PluginDescriptor[] = [webofficePlugin];
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
- 框架提供辅助方法:
|
|
65
|
-
- `getEnabledPlugins()`:计算启用的插件(考虑 `enabled` 字段)。
|
|
66
|
-
- `collectPluginEntities(enabledPlugins)`:合并所有插件实体。
|
|
67
|
-
- `runBeforeBootstrapHooks(app, enabledPlugins)` / `runAfterBootstrapHooks(app, enabledPlugins)`:
|
|
68
|
-
串行执行插件生命周期钩子。
|
|
69
|
-
- `registerPluginRoutes(router, enabledPlugins)`:批量注册插件路由。
|
|
70
|
-
|
|
71
|
-
### 3.2 与 bootstrap 集成(`src/app.ts`)
|
|
72
|
-
|
|
73
|
-
- 在 `setBeforeBootstrap` 中:
|
|
74
|
-
- 计算启用的插件:`const enabledPlugins = getEnabledPlugins();`
|
|
75
|
-
- 将插件列表挂到 `app`(只读,用于调试或中间件):`(app as any).plugins = enabledPlugins;`
|
|
76
|
-
- 调用 `runBeforeBootstrapHooks(app, enabledPlugins)`。
|
|
77
|
-
- 调用 `Router()` 注册宿主自身路由。
|
|
78
|
-
- 调用 `registerPluginRoutes(router, enabledPlugins)` 注册所有插件路由。
|
|
79
|
-
- 在数据库初始化前,合并实体:
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
const allEntities = [...dbEntities, ...collectPluginEntities(enabledPlugins)];
|
|
83
|
-
const dbconfig = databaseConfigManager.getDatabaseConfig(allEntities);
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
- 在 `setAfterBootstrap` 中:
|
|
87
|
-
- 再次获取启用插件,调用 `runAfterBootstrapHooks(app, enabledPlugins)`。
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## 4. 宿主与插件的依赖方向约束
|
|
92
|
-
|
|
93
|
-
### 4.1 插件 → 宿主:只能通过 `plugin-api`
|
|
94
|
-
|
|
95
|
-
- 插件 **禁止直接 import 项目的业务 Service**(如 `TaskService`、`ProjectService`)。
|
|
96
|
-
- 宿主通过 `src/plugin-api/*` 暴露给插件一组稳定的门面函数/接口:
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
// 例:task 相关 API(只示意)
|
|
100
|
-
export interface TaskPluginApi {
|
|
101
|
-
getById(id: number): Promise<TaskDto | null>;
|
|
102
|
-
addSystemComment(taskId: number, content: string): Promise<void>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export const taskPluginApi: TaskPluginApi = {
|
|
106
|
-
// 内部实际调用 TaskService
|
|
107
|
-
};
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- 插件内部只能 import `plugin-api`,例如:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import { taskPluginApi } from "@src/plugin-api/taskApi";
|
|
114
|
-
await taskPluginApi.addSystemComment(taskId, "来自插件的系统备注");
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### 4.2 宿主 → 插件:只通过“事件”和“SPI”
|
|
118
|
-
|
|
119
|
-
- 宿主禁止直接依赖某个具体插件的 Service。
|
|
120
|
-
- 向插件“要能力”的方式:
|
|
121
|
-
- **事件扩展点**(推荐,用于大多数“附加效果”):
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
export type CoreEvent =
|
|
125
|
-
| { type: "task.created"; payload: { taskId: number } }
|
|
126
|
-
| { type: "task.completed"; payload: { taskId: number } };
|
|
127
|
-
|
|
128
|
-
export type CoreEventHandler = (event: CoreEvent) => Promise<void> | void;
|
|
129
|
-
|
|
130
|
-
export function registerCoreEventHandler(h: CoreEventHandler): void;
|
|
131
|
-
export async function emitCoreEvent(event: CoreEvent): Promise<void>;
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
- 核心 Service:`await emitCoreEvent({ type: "task.completed", payload: { taskId } });`
|
|
135
|
-
- 插件:在 `onAfterBootstrap` 或 `registerTasks` 中调用 `registerCoreEventHandler` 订阅事件。
|
|
136
|
-
|
|
137
|
-
- **SPI(Service Provider Interface,可替换实现)**:
|
|
138
|
-
- 适用于可替换能力(审计、搜索、文件存储等)。
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
export interface AuditSpi {
|
|
142
|
-
writeLog(entry: { type: string; data: any }): Promise<void>;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function registerAuditSpi(impl: AuditSpi): void;
|
|
146
|
-
export function getAuditSpi(): AuditSpi; // 总能返回一个实现(可为 Noop)
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
- 宿主:`await getAuditSpi().writeLog(...);`
|
|
150
|
-
- 插件:实现 `AuditSpi` 并在初始化时 `registerAuditSpi(new MyAuditImpl())`。
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## 5. 插件的数据库规范
|
|
155
|
-
|
|
156
|
-
### 5.1 插件是否必须有数据库能力?
|
|
157
|
-
|
|
158
|
-
否。按能力划分三类插件:
|
|
159
|
-
|
|
160
|
-
1. **纯逻辑/集成插件**:不建表,仅消费宿主事件/HTTP/外部服务。
|
|
161
|
-
2. **只读宿主数据插件**:通过 `plugin-api` 访问宿主表,不自建表。
|
|
162
|
-
3. **带独立数据模型的重插件**:有自己的实体和迁移,但只操作自己的 schema。
|
|
163
|
-
|
|
164
|
-
### 5.2 表与实体命名规范
|
|
165
|
-
|
|
166
|
-
当插件需要自建表时:
|
|
167
|
-
|
|
168
|
-
- 表名必须使用**插件 ID 作为前缀**:
|
|
169
|
-
- 插件 `weboffice`:
|
|
170
|
-
- 表:`weboffice_files`、`weboffice_file_versions`
|
|
171
|
-
- 索引:`IDX_weboffice_files_fileId`
|
|
172
|
-
- 外键:`FK_weboffice_file_versions_webofficeFileId`
|
|
173
|
-
- TypeScript 实体命名也应包含插件前缀:
|
|
174
|
-
- 如:`WebofficeFileEntity`、`WebofficeFileVersionEntity`
|
|
175
|
-
- 迁移文件名包含插件标识:
|
|
176
|
-
- 如:`1731600000000-WebofficeTables.ts`
|
|
177
|
-
|
|
178
|
-
约束:
|
|
179
|
-
|
|
180
|
-
- 插件迁移**原则上只操作自己前缀的表**;
|
|
181
|
-
- 若必须修改宿主核心表结构,迁移应由宿主维护,插件只访问已经存在的字段。
|
|
182
|
-
|
|
183
|
-
### 5.3 实体注册方式
|
|
184
|
-
|
|
185
|
-
- 宿主核心实体集中在 `src/entity/index.ts`,**不包含插件实体**。
|
|
186
|
-
- 插件实体通过 `PluginDescriptor.entities` 暴露,由 `collectPluginEntities(enabledPlugins)` 统一合并后交给 `DatabaseConfigManager`。
|
|
33
|
+
- 宿主禁止直接 import 插件 Service。
|
|
34
|
+
- 宿主感知插件能力应通过:
|
|
35
|
+
- 事件扩展点(emit + register handler)
|
|
36
|
+
- SPI(可替换实现:getXxxSpi + registerXxxSpi)
|
|
187
37
|
|
|
188
38
|
---
|
|
189
39
|
|
|
190
|
-
##
|
|
40
|
+
## 3. 插件注册与加载
|
|
191
41
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
- 在 `entities/` 下定义实体,并加入 `plugin.entities`。
|
|
197
|
-
- 在 `migrations/` 下定义迁移(放在全局 `src/migrations` 或插件子目录中,按项目规范处理)。
|
|
198
|
-
3. 在 `framework/plugins/registry.ts` 将插件加入 `plugins` 数组。
|
|
199
|
-
4. 重启服务,观察日志中的插件加载信息,确认路由与表生效。
|
|
200
|
-
|
|
201
|
-
### 6.2 禁用插件(推荐默认“卸载”方式)
|
|
202
|
-
|
|
203
|
-
- 在 `plugins/registry.ts` 中从 `plugins` 数组移除,或设置 `enabled: false` / 使用环境变量关闭。
|
|
204
|
-
- 重启后:
|
|
205
|
-
- 插件不再注册路由;
|
|
206
|
-
- 不再订阅事件或注册 SPI;
|
|
207
|
-
- 插件相关数据表和数据仍保留,方便未来重新启用或迁移。
|
|
208
|
-
|
|
209
|
-
### 6.3 物理卸载与数据清理(高风险,可选)
|
|
210
|
-
|
|
211
|
-
- 不建议在正常启动流程自动执行“删库”操作。
|
|
212
|
-
- 若必须清理插件数据,应通过**独立 CLI/脚本**实现。
|
|
213
|
-
- 插件可选择在 `PluginDescriptor` 中额外导出卸载辅助方法(例如 `uninstall()`),仅供 CLI 调用,用于:
|
|
214
|
-
- 关闭外部订阅;
|
|
215
|
-
- 提供需要删除/归档的表信息等。
|
|
216
|
-
- 所有物理卸载操作应当:
|
|
217
|
-
- 需要人工确认;
|
|
218
|
-
- 具备日志或变更记录,便于审计与回溯。
|
|
42
|
+
- 插件注册表:**`src/plugins/registry.ts`**(`PluginDescriptor` 来自 **`dp-koa-framework-core`**)
|
|
43
|
+
- 宿主启动接入点:`src/app.ts`
|
|
44
|
+
- beforeBootstrap:计算启用插件、执行插件 before hook、注册插件路由、合并插件实体后初始化 DB
|
|
45
|
+
- afterBootstrap:执行插件 after hook(定时任务/订阅等)
|
|
219
46
|
|
|
220
47
|
---
|
|
221
48
|
|
|
222
|
-
##
|
|
223
|
-
|
|
224
|
-
目标:插件测试要**可复用、稳定、可在 CI 中长期运行**。遵循“测试金字塔”:
|
|
225
|
-
|
|
226
|
-
- **单元测试(默认)**:覆盖插件 `core/*` 的纯函数/解析逻辑,不启动 Koa、不连接真实数据库。
|
|
227
|
-
- **集成测试(少量但关键)**:覆盖插件对外契约(HTTP 路由、DB 交互),使用 Koa 最小实例 + SQLite 内存库。
|
|
228
|
-
|
|
229
|
-
### 7.1 测试目录与命名
|
|
49
|
+
## 4. 插件数据库规范
|
|
230
50
|
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
|
|
234
|
-
示例(weboffice):
|
|
235
|
-
|
|
236
|
-
- `test/plugins/weboffice/core.utils.test.ts`
|
|
237
|
-
- `test/plugins/weboffice/core.context.test.ts`
|
|
238
|
-
- `test/plugins/weboffice/http.routes.int.test.ts`
|
|
239
|
-
|
|
240
|
-
### 7.2 单元测试边界
|
|
241
|
-
|
|
242
|
-
- 仅测试插件内部逻辑(如字段规范化、上下文解析、参数转换)。
|
|
243
|
-
- 禁止依赖:
|
|
244
|
-
- Koa app 启动
|
|
245
|
-
- TypeORM DataSource
|
|
246
|
-
- dp-ioc2 Provider 注入
|
|
247
|
-
- 外部服务(COS、第三方 HTTP)
|
|
248
|
-
|
|
249
|
-
### 7.3 集成测试边界(推荐最小闭环)
|
|
250
|
-
|
|
251
|
-
- 只启动最小 Koa + Router:
|
|
252
|
-
|
|
253
|
-
1. `const app = new Koa(); const router = new Router();`
|
|
254
|
-
2. `plugin.registerRoutes?.(router)`
|
|
255
|
-
3. `app.use(router.routes()).use(router.allowedMethods())`
|
|
256
|
-
4. 使用 `supertest` 对 `app.callback()` 发请求,断言 `code/data` 与关键字段
|
|
257
|
-
|
|
258
|
-
- 数据库使用 SQLite 内存库(`:memory:`):
|
|
259
|
-
- 通过 `TestDatabaseHelper.initTestDatabase({ extraEntities: [...] })` 将**插件实体追加**到宿主实体列表。
|
|
260
|
-
- 集成测试自行准备必要数据(seed 插件自己的表)。
|
|
261
|
-
|
|
262
|
-
### 7.4 外部依赖与 ESM 包处理
|
|
263
|
-
|
|
264
|
-
若插件依赖存在 Jest 解析问题的 ESM 包(典型:`uuid@13`),优先在测试中 mock:
|
|
265
|
-
|
|
266
|
-
```ts
|
|
267
|
-
jest.mock("uuid", () => ({ v4: () => "test-uuid" }));
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
原则:
|
|
271
|
-
|
|
272
|
-
- **测试只关心插件对外行为**,不应被第三方包的模块格式(CJS/ESM)牵连。
|
|
273
|
-
- 若确需验证第三方包行为,再考虑 Jest 转译配置或替换依赖。
|
|
274
|
-
|
|
275
|
-
### 7.5 weboffice 集成测试最小断言建议
|
|
276
|
-
|
|
277
|
-
- 至少覆盖:
|
|
278
|
-
- `GET /v3/3rd/files/:file_id/permission` 返回 `{ code: 0, data: { user_id, read, update, ... } }`
|
|
279
|
-
- `user_id` 应符合 WPS 规范(例如 `test-token` -> `test_token`)
|
|
51
|
+
- 插件自建表必须使用插件 ID 作为表名前缀(例如 `weboffice_files`、`weboffice_file_versions`)。
|
|
52
|
+
- 插件迁移原则上只操作自身前缀表,避免修改宿主核心表结构。
|
|
280
53
|
|
|
281
54
|
---
|
|
282
55
|
|
|
283
|
-
##
|
|
284
|
-
|
|
285
|
-
当前模板框架中已经将 WebOffice 集成改造成首个插件示例:
|
|
286
|
-
|
|
287
|
-
- 插件入口:`src/plugins/weboffice/index.ts`
|
|
288
|
-
- `id: "weboffice"`,`displayName: "WPS WebOffice 集成"`。
|
|
289
|
-
- `enabled: (env) => env.WEBOFFICE_ENABLED !== "0"`,可通过环境变量关闭。
|
|
290
|
-
- `registerRoutes` 复用插件内 `src/plugins/weboffice/http/routes.ts`,对接 WPS 回调网关。
|
|
291
|
-
- `entities` 包含 `WebofficeFileEntity` 与 `WebofficeFileVersionEntity`。
|
|
292
|
-
- 数据表:
|
|
293
|
-
- `weboffice_files`、`weboffice_file_versions`,前缀即插件 ID。
|
|
294
|
-
- 迁移示例:`src/migrations/1731600000000-WebofficeTables.ts`。
|
|
56
|
+
## 5. 示例插件:weboffice
|
|
295
57
|
|
|
296
|
-
|
|
58
|
+
模板内置示例插件:`src/plugins/weboffice/`
|
|
297
59
|
|
|
298
|
-
-
|
|
299
|
-
-
|
|
300
|
-
|
|
301
|
-
- `entities/`:TypeORM 实体
|
|
302
|
-
- `services/`:业务 Service
|
|
303
|
-
- `scripts/`:种子/运维脚本
|
|
304
|
-
- 其他入口文件(如路由)建议放在 `http/` 或保持在根目录 `routes.ts`(按团队偏好统一)
|
|
60
|
+
- 通过 `WEBOFFICE_ENABLED` 环境变量控制启用(默认启用;设为 `0` 禁用)。
|
|
61
|
+
- 路由入口:`src/plugins/weboffice/http/routes.ts`
|
|
62
|
+
- 插件实体:`src/plugins/weboffice/entities/*`
|
|
305
63
|
|
|
306
|
-
|
|
64
|
+
> 注意:模板示例插件的 `getUsers` 为占位实现(不依赖宿主业务用户表)。真实业务接入时应改为通过 plugin-api 调宿主用户体系。
|
|
307
65
|
|
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
- Controller 编排规则:DTO + Service 调用 + 统一响应映射 + try/catch
|
|
21
21
|
- **`11-backend-controller-recipes.skill.md`**
|
|
22
22
|
- Controller 推荐模板(可复制)+ 常用注解速查(@Query/@Body/@State/...)
|
|
23
|
+
- **`34-backend-art-template.skill.md`**
|
|
24
|
+
- 服务端 SSR(**art-template**):入口 **`initArtTemplate`**、`return template(...)`(对齐 **`redirect`**)、`views/` 与 **`.html`**、整页骨架与 **`partials` + `{{include}}`**、**`{{each}}` v4** 写法、**`@ControllerCache`** 与 **`cacheKeyFn`**
|
|
25
|
+
|
|
26
|
+
### 25 - 注释与文档(按需启用)
|
|
27
|
+
- **`25-backend-comments-and-doc.skill.md`**
|
|
28
|
+
- Controller / Service / Entity / DTO 的 **`/** ... */`** 注释粒度与必写项(类注释、对外路由方法注释等)
|
|
29
|
+
- 禁止逐行翻译式注释;可与测试、PRD 交叉引用
|
|
23
30
|
|
|
24
31
|
### 20~40 - 数据/校验/错误(按需启用)
|
|
25
32
|
- **`20-backend-repository.skill.md`**:Repository 使用与事务(`transactionManager.executeInTransaction`)
|
|
@@ -38,7 +45,7 @@
|
|
|
38
45
|
### 80 - 工程结构(按需启用)
|
|
39
46
|
- **`80-backend-utils-and-libs.skill.md`**
|
|
40
47
|
- `src/libs` vs `src/utils` 的放置规则与决策清单
|
|
41
|
-
-
|
|
48
|
+
- **`dp-koa-framework-core`**:`isDebug()` / `getRuntimeEnvironmentLabel()` 约定
|
|
42
49
|
|
|
43
50
|
### 90 - 测试(按需启用)
|
|
44
51
|
- **`90-backend-testing.skill.md`**
|
|
@@ -5,7 +5,7 @@ alwaysApply: true
|
|
|
5
5
|
|
|
6
6
|
## 适用范围
|
|
7
7
|
- 本项目所有 TypeScript 后端代码
|
|
8
|
-
- 技术栈:Koa + TypeORM + dp-ioc2 +
|
|
8
|
+
- 技术栈:Koa + TypeORM + dp-ioc2 + 自定义装饰器体系;框架以 npm 包 **`dp-koa-framework-core`**(运行时与对外稳定 API)与 **`dp-koa-framework-libs`**(共享适配/配套能力)引入,版本以项目 `package.json` 为准。
|
|
9
9
|
- **包管理工具:必须使用 yarn(禁止使用 npm)**
|
|
10
10
|
|
|
11
11
|
---
|
|
@@ -64,7 +64,7 @@ alwaysApply: false
|
|
|
64
64
|
### 6.1 全局中间件(bootstrap 阶段)
|
|
65
65
|
在 `src/framework/utils/bootstrap.ts` 中,框架已注册:
|
|
66
66
|
- CORS
|
|
67
|
-
- loggingMiddleware
|
|
67
|
+
- loggingMiddleware(仅 HTTP 4xx/5xx 与未捕获异常;其余访问日志由业务自行挂载)
|
|
68
68
|
- frameworkErrorMiddleware(框架级错误处理中间件)
|
|
69
69
|
- koaBody
|
|
70
70
|
- router.routes/allowedMethods
|
|
@@ -94,5 +94,5 @@ alwaysApply: false
|
|
|
94
94
|
## 八、测试建议(可选但推荐)
|
|
95
95
|
- 单元测试优先覆盖:
|
|
96
96
|
- 鉴权中间件:缺 token / 无效 token / 有效 token 能写入 `ctx.state.user`
|
|
97
|
-
-
|
|
97
|
+
- 日志中间件:是否注入 `ctx.requestId` / `ctx.logger`(访问轨迹、慢请求等由业务自行记录)
|
|
98
98
|
- 静态中间件:prefix 正规化(`/static` vs `/static/`)
|
package/template/package.json
CHANGED
package/template/src/app.ts
CHANGED
|
@@ -70,7 +70,7 @@ setBeforeBootstrap(async function (app: Koa) {
|
|
|
70
70
|
|
|
71
71
|
// 记录系统状态
|
|
72
72
|
const systemStatus = MigrationHelper.getSystemStatus();
|
|
73
|
-
logger.
|
|
73
|
+
logger.debug(`注解系统状态: ${systemStatus.newSystemEnabled ? '新系统' : '旧系统'}, 环境: ${systemStatus.environment}`);
|
|
74
74
|
|
|
75
75
|
Router();
|
|
76
76
|
registerPluginRoutes(router, enabledPlugins);
|
|
@@ -106,11 +106,11 @@ setBeforeBootstrap(async function (app: Koa) {
|
|
|
106
106
|
throw err;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
logger.
|
|
109
|
+
logger.debug("启动前初始化完毕");
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
setAfterBootstrap(async function (app: Koa) {
|
|
113
|
-
logger.
|
|
113
|
+
logger.debug("启动后执行");
|
|
114
114
|
const enabledPlugins = getEnabledPlugins();
|
|
115
115
|
await runAfterBootstrapHooks(app, enabledPlugins);
|
|
116
116
|
});
|
|
@@ -10,7 +10,7 @@ export function initializeCustomProcessors(): void {
|
|
|
10
10
|
ProcessorManager.registerProcessor(new LoggingProcessor());
|
|
11
11
|
ProcessorManager.registerProcessor(new PermissionProcessor());
|
|
12
12
|
ProcessorManager.registerProcessor(new RateLimitProcessor());
|
|
13
|
-
logger.
|
|
13
|
+
logger.debug('自定义注解处理器已注册');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -18,7 +18,7 @@ function isEnabled(plugin: PluginDescriptor): boolean {
|
|
|
18
18
|
export function getEnabledPlugins(): PluginDescriptor[] {
|
|
19
19
|
const enabled = plugins.filter(isEnabled);
|
|
20
20
|
const disabledIds = plugins.filter((p) => !isEnabled(p)).map((p) => p.id);
|
|
21
|
-
logger.
|
|
21
|
+
logger.debug(
|
|
22
22
|
"Plugin registry initialized. enabled=%s disabled=%s",
|
|
23
23
|
enabled.map((p) => p.id).join(",") || "-",
|
|
24
24
|
disabledIds.join(",") || "-"
|
|
@@ -38,7 +38,7 @@ export async function runBeforeBootstrapHooks(app: Koa, enabled: PluginDescripto
|
|
|
38
38
|
for (const plugin of enabled) {
|
|
39
39
|
if (plugin.onBeforeBootstrap) {
|
|
40
40
|
await plugin.onBeforeBootstrap(app);
|
|
41
|
-
logger.
|
|
41
|
+
logger.debug('Plugin "%s" onBeforeBootstrap executed', plugin.id);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -47,7 +47,7 @@ export async function runAfterBootstrapHooks(app: Koa, enabled: PluginDescriptor
|
|
|
47
47
|
for (const plugin of enabled) {
|
|
48
48
|
if (plugin.onAfterBootstrap) {
|
|
49
49
|
await plugin.onAfterBootstrap(app);
|
|
50
|
-
logger.
|
|
50
|
+
logger.debug('Plugin "%s" onAfterBootstrap executed', plugin.id);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -56,7 +56,7 @@ export function registerPluginRoutes(router: Router, enabled: PluginDescriptor[]
|
|
|
56
56
|
for (const plugin of enabled) {
|
|
57
57
|
if (plugin.registerRoutes) {
|
|
58
58
|
plugin.registerRoutes(router);
|
|
59
|
-
logger.
|
|
59
|
+
logger.debug('Plugin "%s" routes registered', plugin.id);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -174,6 +174,6 @@ export function registerWebOfficeRoutes(router: Router): void {
|
|
|
174
174
|
const prefixFromEnv = (process.env.WEBOFFICE_CALLBACK_PREFIX || "/weboffice").replace(/\/$/, "");
|
|
175
175
|
registerUnderPrefix(router, prefixFromEnv);
|
|
176
176
|
if (prefixFromEnv !== "") registerUnderPrefix(router, "");
|
|
177
|
-
logger.
|
|
177
|
+
logger.debug('WebOffice routes registered (prefix: %s and /v3/3rd)', prefixFromEnv);
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -11,7 +11,7 @@ export const plugin: PluginDescriptor = {
|
|
|
11
11
|
enabled: (env: NodeJS.ProcessEnv) => env.WEBOFFICE_ENABLED !== "0",
|
|
12
12
|
|
|
13
13
|
onBeforeBootstrap(_app: Koa) {
|
|
14
|
-
logger.
|
|
14
|
+
logger.debug('Plugin "weboffice" before bootstrap');
|
|
15
15
|
},
|
|
16
16
|
|
|
17
17
|
registerRoutes(router: Router) {
|