create-dp-koa 1.1.3 → 1.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dp-koa",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Scaffold a DP-Koa framework project from the official template",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,91 +12,11 @@ alwaysApply: false
12
12
 
13
13
  ## 一、写 Controller 的推荐范式(必须对齐)
14
14
 
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 '@src/framework/decorator/controller';
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 '@src/framework/decorator/controller';
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 '@src/framework/decorator/controller';
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
- ```
15
+ ### 1.1 参考样例(写代码前先对齐)
16
+ - 推荐在实现前先对齐以下文件的写法(不要臆造新风格):
17
+ - `src/controllers/example/NewAnnotationExampleController.ts`
18
+ - `src/controllers/home/ytUser.controller.ts`
19
+ - `src/controllers/base.controller.ts`
100
20
 
101
21
  ### 1.2 Controller 标准骨架(复制这个结构)
102
22
  目标:DTO 入参 → 调用 Service → 统一响应(`success/fail`)→ try/catch + logger
@@ -117,9 +37,9 @@ export class SomeController extends BaseController {
117
37
  @Get('/some/path')
118
38
  @ResponseCode(200)
119
39
  async getSomething(
120
- @Query(SomeQueryDto) query: SomeQueryDto,
40
+ @Query() query: SomeQueryDto,
121
41
  @State('user') user: { userId: number; type?: number },
122
- ): Promise<ControllerResponse<any>> {
42
+ ): Promise<ControllerResponse<SomeResponseDto>> {
123
43
  try {
124
44
  const result = await this.someService.someMethod(query, user.userId);
125
45
  if (result.code !== CommonServiceResultCode.SUCCESS) {
@@ -139,6 +59,7 @@ export class SomeController extends BaseController {
139
59
  - Controller **不直接访问数据库/Repository**
140
60
  - Controller **不 return Service 原始结果**,必须映射 `success/fail`
141
61
  - Controller `async` **必须 try/catch**
62
+ !- Controller 的返回类型 `ControllerResponse<T>` **必须显式指定 T,禁止使用 `any`**
142
63
 
143
64
  ---
144
65
 
@@ -151,15 +72,18 @@ export class SomeController extends BaseController {
151
72
  ### 2.2 参数注解(重点)
152
73
  - **`@Query()`**:
153
74
  - 读取 query 参数
154
- - 推荐写法:`@Query(SomeQueryDto) query: XxxQueryDto`
75
+ - 推荐写法:`@Query() query: XxxQueryDto`
155
76
  - **`@Body()`**:
156
77
  - 读取 body 参数
157
- - 推荐写法:`@Body(SomeBodyDto) body: XxxBodyDto`
78
+ - 推荐写法:`@Body() body: XxxBodyDto`
79
+ - **`@Params()`**:
80
+ - 读取路径参数(如 `/:id`、`/:id/files/:fileId`)
81
+ - 推荐写法:`@Params(ParamsDto) params: XxxParamsDto`
158
82
  - **`@State()`**:
159
- - 读取整个 `ctx.state`(样例:见 **1.1.2**)
83
+ - 读取整个 `ctx.state`(样例:`ytUser.controller.ts`)
160
84
  - 推荐写法:`@State() state: { user: { userId: number; type?: number } }`
161
85
  - **`@State('user')`**:
162
- - 读取 `ctx.state.user`(样例:见 **1.1.3**)
86
+ - 读取 `ctx.state.user`(样例:`NewAnnotationExampleController.ts`)
163
87
  - 推荐写法:`@State('user') user: { userId: number; type?: number }`
164
88
 
165
89
  禁止:
@@ -171,7 +95,7 @@ export class SomeController extends BaseController {
171
95
 
172
96
  ### 2.4 响应校验注解(进阶,按需)
173
97
  - **`@ResponseValidator(DtoClass, objectKey?)`**:对响应数据做校验
174
- - **`@ResponseValidateIf(DtoClass, fn)`**:仅当 fn 返回真时才校验(样例:见 **1.1.4**)
98
+ - **`@ResponseValidateIf(DtoClass, fn)`**:仅当 fn 返回真时才校验(样例:`ytUser.controller.ts`)
175
99
 
176
100
  ---
177
101
 
@@ -181,8 +105,9 @@ export class SomeController extends BaseController {
181
105
  - `@Get('/xxx') + @State('user')`(或 `@State()` 取整段 state)
182
106
 
183
107
  ### 3.2 带 body 的 POST 接口
184
- - `@Post('/xxx') + @Body(SomeBodyDto) body: SomeBodyDto + @ResponseCode(201)`
108
+ - `@Post('/xxx') + @Body() body: Dto + @ResponseCode(201)`
185
109
 
186
110
  ### 3.3 需要自定义 header
187
111
  - `@ResponseHeader('X-XXX', 'value')`
188
112
 
113
+
@@ -199,8 +199,9 @@ async createUser(@Body(CreateUserControllerDto) body: CreateUserControllerDto) {
199
199
  - 常用装饰器:`@IsString()`, `@IsEmail()`, `@IsNotEmpty()`, `@MinLength()`, `@IsOptional()` 等
200
200
 
201
201
  ### 4.2 Controller DTO 特殊处理
202
- - 可以使用 `@Transform` 进行数据转换(如字符串转数字、trim)
203
- - 可以包含 HTTP 层特定的字段(如 `vcodeToken`、`requestId`)
202
+ - **@Transform 必须使用 class-transformer**:Controller 参数绑定走 `plainToInstance`,只有 **class-transformer** 的 `@Transform` 会生效。须 `import { Transform } from 'class-transformer'`,**不要**使用框架 `@src/framework/decorator/controller` 的 Transform(后者不参与 plainToInstance,会不生效)。
203
+ - 使用 `@Transform(({ value }) => yourTransform(value))` 进行数据转换(如逗号分隔字符串转数组、trim、自定义转 number)。
204
+ - 可以包含 HTTP 层特定字段(如 `vcodeToken`、`requestId`)。
204
205
 
205
206
  ### 4.3 Service DTO 要求
206
207
  - **禁止**包含 HTTP 层特性(如 `@Transform`、HTTP 特定字段)
@@ -209,17 +210,30 @@ async createUser(@Body(CreateUserControllerDto) body: CreateUserControllerDto) {
209
210
 
210
211
  ### 4.4 示例对比
211
212
 
212
- #### Controller DTO(包含 HTTP 层转换)
213
+ #### Controller DTO(包含 HTTP 层转换,使用 class-transformer)
213
214
  ```typescript
215
+ import { Transform } from 'class-transformer';
216
+
214
217
  export class CreateUserControllerDto {
215
- @Transform((val) => Trim(String(val)))
218
+ @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
216
219
  @IsString()
217
220
  @IsEmail()
218
221
  email: string;
219
222
 
220
- @Transform((val) => Number(val))
221
- @IsNumber()
222
- age: number;
223
+ // number 可由 enableImplicitConversion 自动转,无需 @Transform;若需自定义再用:
224
+ @Transform(({ value }) => (value != null && value !== '' ? Number(value) : undefined))
225
+ @IsOptional()
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[];
223
237
  }
224
238
  ```
225
239
 
@@ -235,6 +249,14 @@ export class CreateUserServiceDto {
235
249
  }
236
250
  ```
237
251
 
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
+
238
260
  ---
239
261
 
240
262
  ## 五、校验流程
@@ -0,0 +1,48 @@
1
+ ---
2
+ alwaysApply: false
3
+ ---
4
+
5
+ # Skill:后端 VO 规范
6
+
7
+ ## 适用与触发
8
+
9
+ - 当你需要新增/修改 `src/vo/**` 下的 VO 类型定义时启用本 Skill
10
+ - 目标:确保 VO 定义在统一位置、命名和使用方式符合项目约定,方便前端/AI 端稳定索引
11
+
12
+ ---
13
+
14
+ ## 一、VO 定义定位(必须)
15
+
16
+ - VO(View Object)用于跨层返回/展示的“结构类型”
17
+ - VO 不承担运行时校验职责:不使用 `class-validator` 装饰器
18
+ - VO 定义不包含数据库/事务/业务逻辑,只描述字段结构
19
+
20
+ ---
21
+
22
+ ## 二、目录结构(必须遵守)
23
+
24
+ ```
25
+ src/vo/
26
+ └── {module}/
27
+ └── {name}.vo.ts
28
+ ```
29
+
30
+ 示例:
31
+ - `src/vo/im/im.vo.ts`
32
+
33
+ ---
34
+
35
+ ## 三、命名规范(必须)
36
+
37
+ - 类型名后缀:以 `Vo` 结尾,例如 `ImConversationVo`
38
+ - 字段结构:使用 `export type XxxVo = { ... }`(推荐)或 `export interface XxxVo { ... }`
39
+ - 文件命名:以模块语义为主,建议 `{module}.vo.ts`
40
+
41
+ ---
42
+
43
+ ## 四、使用规范
44
+
45
+ - Controller / Service 引用 VO 类型时使用 `import type ...`,避免引入无意义的运行时依赖
46
+ - VO 类型只作为返回结构类型使用(例如 `ControllerResponse<ImMessageVo>`、`CommonServiceResult<ImMessageVo>`)
47
+ - 映射逻辑(Entity/领域模型 -> VO)可以写在 Service 私有方法中;但 VO 的“定义本体”必须在 `src/vo/` 中维护
48
+
@@ -1,65 +1,307 @@
1
- # 后端插件体系规范(dp-koa-framework 模板)
1
+ # 后端插件体系规范(dp-koa-framework
2
2
 
3
- > 目标:让“独立功能体”以插件形式存在,可插拔、易维护、与宿主框架低耦合。
3
+ > 目标:让类似 `weboffice` 的“独立功能体”以插件形式存在,可插拔、易维护、与宿主框架低耦合。
4
4
 
5
5
  ---
6
6
 
7
7
  ## 1. 插件的基本形态
8
8
 
9
+ - 插件是**独立功能域**:可以包含路由、Service、实体/迁移、脚本,但通过统一接口暴露给宿主。
9
10
  - 插件统一放在 `src/plugins/<plugin-id>/` 目录下。
10
- - 插件根目录只保留 `index.ts`(插件入口)。
11
- - 插件入口必须导出一个 `PluginDescriptor`(见 `src/framework/plugins/types.ts`)。
11
+ - 插件入口文件:`src/plugins/<plugin-id>/index.ts`,必须导出一个 `PluginDescriptor`:
12
12
 
13
- 推荐目录结构:
13
+ ```ts
14
+ import type { PluginDescriptor } from "@src/framework/plugins/types";
14
15
 
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/`:种子/运维脚本(可选)
16
+ export const plugin: PluginDescriptor = {
17
+ id: "weboffice",
18
+ displayName: "WPS WebOffice 集成",
19
+ enabled: (env) => env.WEBOFFICE_ENABLED !== "0",
20
+ registerRoutes(router) {
21
+ // 注册本插件路由
22
+ },
23
+ entities: [
24
+ // 插件自有实体
25
+ ],
26
+ };
27
+ ```
21
28
 
22
29
  ---
23
30
 
24
- ## 2. 宿主与插件的依赖方向
31
+ ## 2. PluginDescriptor 约定
25
32
 
26
- ### 2.1 插件 → 宿主
33
+ 定义位置:`src/framework/plugins/types.ts`
27
34
 
28
- - 插件内部业务代码应优先使用相对路径组织。
29
- - 插件调用宿主业务能力(如用户、项目、权限)应通过 **plugin-api 门面层**(建议放在 `src/plugin-api/*`),避免直接 import 宿主 Service/Repository。
35
+ 关键字段:
30
36
 
31
- ### 2.2 宿主 插件
37
+ - `id: string`:插件唯一 ID,建议与目录名一致(如 `weboffice`)。
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 实体(仅包含插件表)。
32
49
 
33
- - 宿主禁止直接 import 插件 Service。
34
- - 宿主感知插件能力应通过:
35
- - 事件扩展点(emit + register handler)
36
- - SPI(可替换实现:getXxxSpi + registerXxxSpi)
50
+ ---
51
+
52
+ ## 3. 插件注册与加载流程
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`。
37
187
 
38
188
  ---
39
189
 
40
- ## 3. 插件注册与加载
190
+ ## 6. 插件生命周期与卸载策略
41
191
 
42
- - 插件注册表:`src/framework/plugins/registry.ts`
43
- - 宿主启动接入点:`src/app.ts`
44
- - beforeBootstrap:计算启用插件、执行插件 before hook、注册插件路由、合并插件实体后初始化 DB
45
- - afterBootstrap:执行插件 after hook(定时任务/订阅等)
192
+ ### 6.1 安装插件
193
+
194
+ 1. 在 `src/plugins/<plugin-id>/` 下创建插件目录和 `index.ts`,导出 `PluginDescriptor`。
195
+ 2. 若有 DB:
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
+ - 具备日志或变更记录,便于审计与回溯。
46
219
 
47
220
  ---
48
221
 
49
- ## 4. 插件数据库规范
222
+ ## 7. 插件测试规范(单元 + 集成)
223
+
224
+ 目标:插件测试要**可复用、稳定、可在 CI 中长期运行**。遵循“测试金字塔”:
225
+
226
+ - **单元测试(默认)**:覆盖插件 `core/*` 的纯函数/解析逻辑,不启动 Koa、不连接真实数据库。
227
+ - **集成测试(少量但关键)**:覆盖插件对外契约(HTTP 路由、DB 交互),使用 Koa 最小实例 + SQLite 内存库。
228
+
229
+ ### 7.1 测试目录与命名
50
230
 
51
- - 插件自建表必须使用插件 ID 作为表名前缀(例如 `weboffice_files`、`weboffice_file_versions`)。
52
- - 插件迁移原则上只操作自身前缀表,避免修改宿主核心表结构。
231
+ - 单元测试:`test/plugins/<plugin-id>/*.test.ts`
232
+ - 集成测试:`test/plugins/<plugin-id>/*.int.test.ts`
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`)
53
280
 
54
281
  ---
55
282
 
56
- ## 5. 示例插件:weboffice
283
+ ## 8. weboffice 插件作为示例
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`。
57
295
 
58
- 模板内置示例插件:`src/plugins/weboffice/`
296
+ ### weboffice 插件目录结构约定(示例)
59
297
 
60
- - 通过 `WEBOFFICE_ENABLED` 环境变量控制启用(默认启用;设为 `0` 禁用)。
61
- - 路由入口:`src/plugins/weboffice/http/routes.ts`
62
- - 插件实体:`src/plugins/weboffice/entities/*`
298
+ - 根目录仅保留 `index.ts`(插件入口)
299
+ - 其余代码按职责归档:
300
+ - `core/`:`types.ts`、`errors.ts`、`context.ts`、`utils.ts`
301
+ - `entities/`:TypeORM 实体
302
+ - `services/`:业务 Service
303
+ - `scripts/`:种子/运维脚本
304
+ - 其他入口文件(如路由)建议放在 `http/` 或保持在根目录 `routes.ts`(按团队偏好统一)
63
305
 
64
- > 注意:模板示例插件的 `getUsers` 为占位实现(不依赖宿主业务用户表)。真实业务接入时应改为通过 plugin-api 调宿主用户体系。
306
+ 新增插件时,强烈建议参考 weboffice 插件的结构与写法,保持一致的工程风格与解耦思路。
65
307
 
@@ -11,7 +11,85 @@ alwaysApply: false
11
11
 
12
12
  ---
13
13
 
14
- ## 二、测试要求
14
+ ## 二、数据库相关测试(强制)
15
+
16
+ **凡涉及数据库操作的 Service、Controller 测试,必须使用内存数据库进行测试。**
17
+
18
+ - 涉及数据库操作:指测试会触发 TypeORM 的 Repository/QueryBuilder 调用(如 list、create、update、delete 等依赖数据库的用例)。
19
+ - 必须通过 `TestDatabaseHelper.initTestDatabase()` 初始化内存库(SQLite `:memory:`),在测试中执行真实 SQL,不得通过 mock `getDataRepository` 或 mock 各个 Repository 的方式绕过真实数据库。
20
+ - 使用 `Provider` 获取 Service 实例,保证 Service 使用真实的 DataSource/Repository,以便暴露列名、约束、SQL 等与真实环境一致的问题。
21
+ - 每个测试前可按需调用 `TestDatabaseHelper.resetTestData()` 或在各用例内自行清理/准备数据;测试套件结束后调用 `TestDatabaseHelper.cleanupTestDatabase()`。
22
+
23
+ 这样可避免「单测全部通过、上线后因列名/方言差异等导致 SQL 报错」的情况。
24
+
25
+ ---
26
+
27
+ ## 三、测试要求
28
+
29
+ - 必须覆盖:
30
+ - 成功场景
31
+ - 失败场景
32
+ - 边界条件
33
+ - 测试必须相互独立
34
+ - 每个测试前重置数据
35
+
36
+ ---
37
+
38
+ ## 四、测试文件结构
39
+
40
+ - Controller:`test/controllers/**`
41
+ - Service:`test/service/**`
42
+ - Framework:`test/framework/**`
43
+
44
+ ---
45
+
46
+ ## 五、Controller 测试专项规范
47
+
48
+ 1. **Query / Body 参数要尽量模拟真实 HTTP 形态**
49
+ - Query 中的数字、布尔、数组,真实请求里通常是字符串,例如:`?page=1&pageSize=20&hasTask=true`。
50
+ - Controller 单测中,涉及这类字段时应优先使用字符串形态构造入参,再交给 DTO/解析逻辑处理,而不是直接传入 `boolean / number`:
51
+ - ✅ `const query = { page: "1", pageSize: "20", hasTask: "true" } as any;`
52
+ - ⛔ `const query = { page: 1, pageSize: 20, hasTask: true } as any;`
53
+
54
+ 2. **新增筛选/查询条件时,必须覆盖“参数解析 + Service 调用”这一整条链路**
55
+ - 对于新增的 Query 字段(例如:`visibility`、`hasTask` 等):
56
+ - Service 层:用 Service 单测验证在 `true / false / 未传` 三种情况下 SQL/查询条件是否正确;
57
+ - Controller 层:用 Controller 单测验证“从字符串 Query 到 Service DTO”的转换是否正确(包括合法值、非法值、不传三种情况)。
58
+
59
+ 3. **典型反例(经验教训)——hasTask 过滤未生效**
60
+ - 曾在项目动态列表接口 `GET /api/projects/:id/activities` 上新增 `hasTask`(是否关联待办)筛选条件:
61
+ - Service 单测直接调用 `list(projectId, { hasTask: true }, userId)`,验证了过滤逻辑本身是正确的;
62
+ - 但 Controller 层没有正确将 `?hasTask=true / ?hasTask=false` 这类字符串转换为布尔值,而是原样把字符串传入 Service,导致运行时 `hasTask` 始终为 `undefined`,过滤条件完全没有生效;
63
+ - 由于 Controller 单测构造的是已经「解析好」的对象(`{ hasTask: true }`),真实 HTTP 请求路径(`string -> boolean`)未被覆盖,问题在上线后才通过前端联调暴露。
64
+ - 结论:凡是需要从 Query/Body 中做类型转换的字段(布尔、数字、数组等),一定要确保至少有一条测试**从原始字符串形态出发**,覆盖到最终调用 Service 的 DTO。
65
+
66
+ ---
67
+ alwaysApply: false
68
+ ---
69
+ # Skill:后端测试规范(Jest)
70
+
71
+ ## 一、测试基础
72
+
73
+ - 使用 Jest
74
+ - 使用 `TestDatabaseHelper` 管理测试数据库
75
+ - 使用 `Provider` 获取 Service 实例
76
+
77
+ ---
78
+
79
+ ## 二、数据库相关测试(强制)
80
+
81
+ **凡涉及数据库操作的 Service、Controller 测试,必须使用内存数据库进行测试。**
82
+
83
+ - 涉及数据库操作:指测试会触发 TypeORM 的 Repository/QueryBuilder 调用(如 list、create、update、delete 等依赖数据库的用例)。
84
+ - 必须通过 `TestDatabaseHelper.initTestDatabase()` 初始化内存库(SQLite `:memory:`),在测试中执行真实 SQL,不得通过 mock `getDataRepository` 或 mock 各个 Repository 的方式绕过真实数据库。
85
+ - 使用 `Provider` 获取 Service 实例,保证 Service 使用真实的 DataSource/Repository,以便暴露列名、约束、SQL 等与真实环境一致的问题。
86
+ - 每个测试前可按需调用 `TestDatabaseHelper.resetTestData()` 或在各用例内自行清理/准备数据;测试套件结束后调用 `TestDatabaseHelper.cleanupTestDatabase()`。
87
+
88
+ 这样可避免「单测全部通过、上线后因列名/方言差异等导致 SQL 报错」的情况。
89
+
90
+ ---
91
+
92
+ ## 三、测试要求
15
93
 
16
94
  - 必须覆盖:
17
95
  - 成功场景
@@ -22,7 +100,7 @@ alwaysApply: false
22
100
 
23
101
  ---
24
102
 
25
- ## 三、测试文件结构
103
+ ## 四、测试文件结构
26
104
 
27
105
  - Controller:`test/controllers/**`
28
106
  - Service:`test/service/**`
@@ -43,13 +43,14 @@ export class BaseController {
43
43
  #### 1.1.2 `@State()`:读取整段 `ctx.state`
44
44
 
45
45
  ```ts
46
- import { Get, State } from '@src/framework/decorator/controller';
46
+ import { Get, State } from 'dp-koa-framework-core';
47
47
  import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
48
+ import type { AppCtxState } from '@src/types/ctxState';
48
49
 
49
50
  export class ExampleStateController extends BaseController {
50
51
  @Get('/example/state-full')
51
52
  async getWithFullState(
52
- @State() state: { user: { userId: number; type?: number } },
53
+ @State() state: AppCtxState,
53
54
  ): Promise<ControllerResponse<{ userId: number }>> {
54
55
  return this.success({ userId: state.user.userId });
55
56
  }
@@ -59,13 +60,14 @@ export class ExampleStateController extends BaseController {
59
60
  #### 1.1.3 `@State('user')`:只读 `ctx.state.user`
60
61
 
61
62
  ```ts
62
- import { Get, State } from '@src/framework/decorator/controller';
63
+ import { Get, State } from 'dp-koa-framework-core';
63
64
  import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
65
+ import type { UserState } from '@src/types/ctxState';
64
66
 
65
67
  export class ExampleUserSliceController extends BaseController {
66
68
  @Get('/example/state-user')
67
69
  async getWithUser(
68
- @State('user') user: { userId: number; type?: number },
70
+ @State('user') user: UserState,
69
71
  ): Promise<ControllerResponse<{ ok: boolean }>> {
70
72
  return this.success({ ok: user.userId > 0 });
71
73
  }
@@ -75,8 +77,9 @@ export class ExampleUserSliceController extends BaseController {
75
77
  #### 1.1.4 `@ResponseValidateIf`:仅在条件成立时校验响应体
76
78
 
77
79
  ```ts
78
- import { Get, State, ResponseValidateIf } from '@src/framework/decorator/controller';
80
+ import { Get, State, ResponseValidateIf } from 'dp-koa-framework-core';
79
81
  import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
82
+ import type { AppCtxState } from '@src/types/ctxState';
80
83
 
81
84
  // 示例 DTO:按实际接口替换
82
85
  class UserProfileResponseDto {
@@ -88,7 +91,7 @@ export class ExampleResponseValidateController extends BaseController {
88
91
  @ResponseValidateIf(UserProfileResponseDto, (data) => Boolean(data && data.data))
89
92
  @Get('/example/profile')
90
93
  async getProfile(
91
- @State() state: { user: { userId: number } },
94
+ @State() state: AppCtxState,
92
95
  ): Promise<ControllerResponse<UserProfileResponseDto | null>> {
93
96
  return this.success({
94
97
  id: state.user.userId,
@@ -102,13 +105,13 @@ export class ExampleResponseValidateController extends BaseController {
102
105
  目标:DTO 入参 → 调用 Service → 统一响应(`success/fail`)→ try/catch + logger
103
106
 
104
107
  ```ts
105
- import { Get, Post, Query, Body, State, ResponseCode, ResponseHeader } from '@src/framework/decorator/controller';
108
+ import { Get, Post, Query, Body, State, ResponseCode, ResponseHeader } from 'dp-koa-framework-core';
106
109
  import { BaseController, ControllerResponse } from '@src/controllers/base.controller';
107
110
  import { Inject } from 'dp-ioc2';
108
- import { logger } from '@src/framework/utils/logger';
111
+ import { logger } from 'dp-koa-framework-core';
109
112
  import { SomeService } from '@src/service/some.service';
110
113
  import { SomeQueryDto, SomeBodyDto } from '@src/dto/controller/xxx/some.controller.dto';
111
- import { CommonServiceResultCode } from '@src/framework/types/ServiceResult';
114
+ import { CommonServiceResultCode } from 'dp-koa-framework-core';
112
115
 
113
116
  export class SomeController extends BaseController {
114
117
  @Inject(SomeService)
@@ -132,4 +132,18 @@ alwaysApply: false
132
132
  ## 六、调用关系约束(必须)
133
133
  - Controller 只能调用 Service 的公开方法,不应调用 Repository
134
134
  - Service 不应调用 Controller,也不应依赖 Koa `Context`
135
- - Service 之间如需调用,应通过依赖注入(`@Inject(OtherService)`),避免静态调用/循环依赖
135
+ - Service 之间如需调用,应通过依赖注入(`@Inject(OtherService)`),避免静态调用/循环依赖
136
+ - 禁止在 Service 内手动创建实例:
137
+ - ❌ 禁止 `new OtherService()` / `new XxxService()`(包括在方法内部临时 new)
138
+ - ❌ 禁止通过手写单例/缓存实例绕过 IoC 容器
139
+ - ✅ 必须依赖 dp-ioc2 在运行期注入(例如字段注入或构造注入)
140
+ - 推荐写法(字段注入示例):
141
+ ```ts
142
+ import { Inject } from "dp-ioc2";
143
+ import { OtherService } from "./other.service";
144
+
145
+ export class MyService extends BaseService {
146
+ @Inject(OtherService)
147
+ private otherService!: OtherService;
148
+ }
149
+ ```
@@ -6,11 +6,15 @@ alwaysApply: false
6
6
  ## 一、错误处理
7
7
 
8
8
  - 所有 async 方法必须 try/catch
9
- - 业务错误:
10
- - 使用 `CommonServiceResult.fail()`
11
- - 系统错误:
12
- - 记录日志
13
- - 返回通用失败响应
9
+ - 业务错误(可预期、可分类):
10
+ - 主要在 **Service 层 catch 中**处理
11
+ - 返回 `CommonServiceResult.fail()/validationError/notFound/...`(由 Service 统一封装)
12
+ - 系统错误(不可预期、疑似异常):
13
+ - **必须记录日志(logger.error)**
14
+ - Service 层返回通用失败的 `CommonServiceResult.error(...)`
15
+ - 映射到 Controller:
16
+ - Controller 不应直接返回 `CommonServiceResult`
17
+ - Controller 必须把 Service 的 `CommonServiceResult` 通过 `this.success()/this.fail()` 映射为 `ControllerResponse`
14
18
 
15
19
  ---
16
20
 
@@ -5,7 +5,7 @@ alwaysApply: false
5
5
 
6
6
  ## 适用与触发(重要)
7
7
  - 当需要新增/调整以下内容时启用本 Skill:
8
- - `src/libs/**` 下的类库/适配层
8
+ - 共享适配/可抽包的代码(优先使用 `dp-koa-framework-libs`;仅临时代码可放业务模块内)
9
9
  - `src/utils/**` 下的工具函数
10
10
  - 业务模块内部的 `xxx.utils.ts`
11
11
 
@@ -19,9 +19,9 @@ alwaysApply: false
19
19
 
20
20
  ---
21
21
 
22
- ## 二、`src/libs/**` 放什么(必须)
22
+ ## 二、共享适配/可抽包代码放什么(必须)
23
23
 
24
- ### 2.1 适合放在 libs 的内容
24
+ ### 2.1 适合的内容
25
25
  - **第三方服务适配/封装**:
26
26
  - 如:短信服务封装、支付网关封装、HTTP SDK 封装等
27
27
  - 对外暴露一个干净的接口:如 `sendSms(phone, templateId, params)`
@@ -30,15 +30,67 @@ alwaysApply: false
30
30
  - **有内部状态或生命周期的组件**:
31
31
  - 连接池包装器、复杂缓存管理器、网关客户端等
32
32
 
33
- ### 2.2 不适合放在 libs 的内容
33
+ ### 2.2 不适合放到共享适配层的内容
34
34
  - 单一的小工具函数(字符串、日期、数字转换等)
35
35
  - 强业务耦合的逻辑(如订单价格计算、业务规则判断)
36
36
 
37
37
  ---
38
38
 
39
- ## 三、`src/utils/**` 放什么(必须)
39
+ ### 2.3 框架库内置工具(缓存/短信/验证码等)(重点)
40
+ 当你需要使用“框架已经内置好的工具”,优先直接从 `dp-koa-framework-libs` 导入稳定导出;其中**缓存**建议按业务通过 `createCache + CacheType` 创建你的“业务缓存”对象。
41
+
42
+ #### 2.3.1 缓存工具:用 `createCache` + `CacheType` 构建你的缓存
43
+ - 核心提供缓存工厂:`createCache(name, type, customConfig?)`(配合 `CacheType`)
44
+ - 你可以完全参考 `dp-koa-framework-libs/src/libs/mCache.ts`(1-8 行)的写法,在你自己的 `libs/` 里创建“业务自定义缓存对象”
45
+ - 注意:`name` 作为全局缓存标识,同名会复用已有缓存实例(避免重复创建)
46
+ - `CacheType` 用于选择默认 TTL/容量策略(来自 `dp-koa-framework-core` 缓存模块默认配置):
47
+ - `CacheType.USER`:用户缓存(默认 `stdTTL=1800` 秒,`maxKeys=500`)
48
+ - `CacheType.CONTROLLER`:控制器结果缓存(默认 `stdTTL=60` 秒,`maxKeys=2000`)
49
+ - `CacheType.SESSION`:会话缓存(默认 `stdTTL=3600` 秒,`maxKeys=1000`)
50
+ - `CacheType.CAPTCHA`:验证码缓存(默认 `stdTTL=300` 秒,`maxKeys=100`)
51
+ - `CacheType.TEMP`:临时缓存(默认 `stdTTL=60` 秒,`maxKeys=100`)
52
+ - `customConfig` 的行为:会在“默认配置(DEFAULT_CACHE_CONFIG)+ CacheType 配置”基础上进行合并,从而覆盖 `stdTTL/maxKeys/checkperiod/...` 等参数
53
+
54
+ ```ts
55
+ import { createCache, CacheType } from "dp-koa-framework-core";
56
+
57
+ // 例:给当前业务域创建一个“用户缓存”
58
+ export const myUserCache = createCache("my-user", CacheType.USER, {
59
+ stdTTL: 600, // 10分钟过期(秒)
60
+ maxKeys: 500, // 用户缓存最大条目数
61
+ });
62
+ ```
63
+
64
+ 使用方式示例(以 `node-cache` 风格的 `get/set` 为准):
65
+ ```ts
66
+ import { myUserCache } from "@src/libs/myUserCache"; // 示例:把 myUserCache 放到你的 libs 文件并导出
67
+
68
+ const cached = myUserCache.get(String(userId));
69
+ if (!cached) {
70
+ const value = await buildValue();
71
+ myUserCache.set(String(userId), value); // TTL 由 createCache 配置决定
72
+ }
73
+ ```
74
+
75
+ #### 2.3.2 其它常用内置工具(快速导入)
76
+ - 验证码:`CaptchaGenerator`
77
+ - 短信:
78
+ - `TencentSms`(类)
79
+ - `tencentSms`(函数/实例)
80
+ - 邮件:`AokEmailSender`
81
+ - COS/文件:
82
+ - `isCosConfigured`
83
+ - `uploadToCos`
84
+ - `getFilePublicUrl`
85
+ - `webofficeCosKey`
86
+ - 通用业务校验:`ServiceValidate`
87
+
88
+ ---
89
+
90
+ ## 三、`src/utils/`** 放什么(必须)
40
91
 
41
92
  ### 3.1 适合放在 utils 的内容
93
+
42
94
  - **与业务无关的纯函数工具**:
43
95
  - 时间、字符串、数字处理
44
96
  - 通用 Promise/重试/节流/防抖等
@@ -70,13 +122,13 @@ alwaysApply: false
70
122
  新增工具/类库时,按以下顺序判断:
71
123
 
72
124
  1. **是否依赖第三方服务/外部系统或有复杂状态?**
73
- - 是 → 优先放 `libs/`(适配层/小 SDK)
125
+ - 是 → 优先放共享适配层(`dp-koa-framework-libs` 或独立 npm 包)
74
126
  2. **是否明显只服务某一个业务模块?**
75
127
  - 是 → 放到该模块自己的 `xxx.utils.ts`
76
128
  3. **是否 100% 纯函数、无状态且可跨多个模块复用?**
77
129
  - 是 → 放到全局 `utils/`
78
130
  4. **未来是否有机会抽为独立 npm 包?**
79
- - 是 → 更倾向放 `libs/`
131
+ - 是 → 更倾向放共享适配层(`dp-koa-framework-libs` 或独立 npm 包)
80
132
 
81
133
  ---
82
134
 
@@ -87,4 +139,21 @@ alwaysApply: false
87
139
  - 导出:优先导出类或工厂函数,如 `export class TencentSmsClient { ... }`
88
140
  - `utils` 目录下:
89
141
  - 文件名:`date.utils.ts` / `string.utils.ts` / `testDataInitializer.ts` 等
90
- - 导出:纯函数 `export function xxx(...) { ... }`
142
+ - 导出:纯函数 `export function xxx(...) { ... }`
143
+
144
+ ---
145
+
146
+ ## 七、运行环境判定(必须)
147
+
148
+ 应统一使用 `dp-koa-framework-core` 导出的**运行环境判定** API,避免散落 `NODE_ENV === 'production'` 判断。
149
+
150
+ | API | 含义 |
151
+ | ------------------------------ | -------------------------------------------------------- |
152
+ | `isDebug()` | `process.argv` 含 `--env=debug` 为 `true` |
153
+ | `getRuntimeEnvironmentLabel()` | `'development'`(debug)或 `'production'`(非 debug),用于日志/元数据 |
154
+
155
+ **约定**:
156
+
157
+ - **非 debug = 生产口径**(迁移、静态缓存策略、对外错误信息是否脱敏等)。
158
+ - **不要**单独用 `NODE_ENV` 替代上述判定;`NODE_ENV=test` 仅用于 Jest 等测试分支。
159
+ - 新增种子脚本若需读 `.env.development`,启动命令应带 `--env=debug`(与 `bootstrap` 一致)。
@@ -20,6 +20,32 @@ alwaysApply: false
20
20
  - 测试必须相互独立
21
21
  - 每个测试前重置数据
22
22
 
23
+ ## 二.2 涉及数据库读写的 Service 测试:必须使用内存数据库
24
+
25
+ - 如果测试中调用的 Service(或其依赖)会发生数据库读写(查询/插入/更新/删除、事务、迁移/初始化等):
26
+ - 必须启用内存数据库模式:通过 `TestDatabaseHelper.initTestDatabase(...)` 初始化测试用 `DataSource`
27
+ - 必须在测试结束后清理:调用 `TestDatabaseHelper.cleanupTestDatabase()`(确保释放资源,并清理缓存)
28
+ - 禁止为了“方便”而只模拟数据:
29
+ - 不允许用简单对象/假 Repository 来替代数据库读写(会掩盖 SQL/实体映射/事务边界问题)
30
+ - 允许 mock 的范围:仅限非数据库行为(如外部 HTTP 调用、消息发送、第三方 SDK 等)
31
+
32
+ ---
33
+
34
+ ## 二.1 测试是否发“真实 HTTP”(需要理解)
35
+
36
+ - `test/controllers/**`:Controller 单元测试
37
+ - 通常是 `new Controller()` 后直接调用 controller 方法
38
+ - 不会触发真实的 `app.listen` 端口监听
39
+ - 这是在验证“编排/返回结构/装饰器效果(mock 情况)”的正确性
40
+
41
+ - `test/plugins/**` / `test/integration/**`:路由/插件集成测试
42
+ - 会创建 `Koa` + `router`
43
+ - 通过 `supertest` 调用 `request(app.callback()).get/post/...`
44
+ - 走完整 Koa 路由链路,但仍是“in-memory callback”,**不需要也不建议**真实 `app.listen`
45
+
46
+ 原则:
47
+ - 测试环境不要 `app.listen` 开端口(避免端口占用、慢、难复现)
48
+
23
49
  ---
24
50
 
25
51
  ## 三、测试文件结构
@@ -0,0 +1,9 @@
1
+ export interface UserState {
2
+ userId: number;
3
+ type?: number;
4
+ }
5
+
6
+ export interface AppCtxState {
7
+ user: UserState;
8
+ }
9
+