返回 AiToEarn
project-standards.md
根目录 / project / aitoearn-backend / .claude / rules / project-standards.md
1 # Project Development Standards
2
3 ## General Principles
4
5 - Type safety with input/output separation: Use DTOs only for request validation and transformation; use VOs only for response encapsulation.
6 - Clear layer responsibilities: Controller handles routing/parameter binding/response transformation only; Service handles business orchestration and permission filtering; Repository handles data access only, without business logic or permission checks.
7 - Unified exceptions: Business errors use AppException + ResponseCode; protocol-level errors (for example rate limit/auth transport semantics) may use standard HttpException; global filter ensures unified response format.
8
9 ## Naming & Files
10
11 - Classes/Interfaces/Enums use PascalCase; variables/functions use camelCase; constants use UPPER_SNAKE_CASE.
12 - File suffixes: `*.controller.ts` / `*.service.ts` / `*.module.ts` / `*.dto.ts` / `*.vo.ts` / `*.repository.ts`.
13 - File names must use kebab-case; camelCase is prohibited: `platform-rules.constants.ts`, not `platformRules.config.ts`.
14 - `*.config.ts` files are only for zod configuration; not allowed for constants, utils, or interfaces.
15 - Do not arbitrarily nest folders like `dto/`; create `*.dto.ts` files directly in the module root directory.
16
17 ## Repository Method Naming
18
19 - Methods must start with these prefixes: `get` / `list` / `create` / `update` / `delete` / `count`
20 - Aggregation methods may start with aggregation keywords: `aggregate` / `sum` / `avg` etc.
21 - Batch operations use `createMany` / `updateMany` / `deleteMany` format
22 - Methods returning a single value must start with `get`, format: `getByXxx`
23 - Methods returning arrays must start with `list`, format: `listByXxx`
24 - Methods returning counts must start with `count`, format: `countByXxx`
25 - Pagination methods must end with `WithPagination`, e.g., `listWithPagination`
26 - Non-standard prefixes are prohibited: `find` / `del` / `add` / `set` / `check` etc.
27 - Business verbs as prefixes are prohibited: `verify` / `mark` / `publish` etc.
28
29 ### Examples
30
31 - ✅ `getById` / `getByUserId` / `getByIdAndStatus`
32 - ✅ `listByUserId` / `listWithPagination` / `listByStatus`
33 - ✅ `create` / `createMany` / `createByUser`
34 - ✅ `updateById` / `updateByStatus` / `updateManyByIds`
35 - ✅ `deleteById` / `deleteByUserId` / `deleteManyByIds`
36 - ✅ `countByUserId` / `countByStatus` / `aggregateByDate`
37 - ❌ `findById` → `getById`
38 - ❌ `findList` → `listWithPagination`
39 - ❌ `delOne` → `deleteById`
40 - ❌ `addUseCount` → `updateUseCountById`
41 - ❌ `verify` → `updateVerifyById`
42 - ❌ `markAsRead` → `updateAsReadByIds`
43
44 ## DTO (Input)
45
46 - Write zod schema first, then use `createZodDto(schema, 'IdString')` to generate DTO; using entities as input is prohibited.
47 - Pagination input must use `PaginationDtoSchema` (page ≥1, pageSize ∈[1,1000], string numbers auto-convert).
48
49 ```ts
50 import { createZodDto, PaginationDtoSchema } from '@yikart/common';
51 import { z } from 'zod';
52
53 export const CreateOrderDtoSchema = z.object({
54 productId: z.string(),
55 quantity: z.number().int().positive().default(1),
56 returnTo: z.url().optional(),
57 });
58 export class CreateOrderDto extends createZodDto(CreateOrderDtoSchema, 'CreateOrderDto') {}
59 ```
60
61 ## VO (Output)
62
63 - VOs expose only stable external fields; mapping is done in Service, Controller outputs using `VoClass.create(data)` (regular VO); returning database entities directly is prohibited.
64 - Pagination responses must use `createPaginationVo` (pagination VO instantiation uses `new`), with fields: page, pageSize, totalPages, total, list.
65
66 ```ts
67 import { createPaginationVo, createZodDto } from '@yikart/common';
68 import { z } from 'zod';
69
70 export const OrderDetailVoSchema = z.object({ id: z.string(), amount: z.number(), createdAt: z.coerce.date() });
71 export class OrderDetailVo extends createZodDto(OrderDetailVoSchema, 'OrderDetailVo') {}
72 export class OrderListVo extends createPaginationVo(OrderDetailVoSchema, 'OrderListVo') {}
73 ```
74
75 ## API Documentation (Swagger)
76
77 - Every Controller must have `@ApiTags('Category/Module')` decorator for grouping
78 - Every Controller method must have `@ApiDoc` decorator for documentation
79 - DTO/VO schema fields must use `.describe('description')` for field documentation
80
81 ### Controller Documentation
82
83 ```ts
84 import { ApiTags } from '@nestjs/swagger'
85 import { ApiDoc } from '@yikart/common'
86
87 @ApiTags('AI/Material-Adaptation')
88 @Controller('/material-adaptation')
89 export class MaterialAdaptationController {
90
91 @ApiDoc({
92 summary: '适配素材到多个平台',
93 description: '使用 AI 将素材内容适配到指定的社交媒体平台',
94 body: AdaptMaterialDtoSchema,
95 response: [MaterialAdaptationVo],
96 })
97 @Post('/')
98 async adaptMaterial(...): Promise<MaterialAdaptationVo[]> { ... }
99 }
100 ```
101
102 ### DTO/VO Field Documentation
103
104 ```ts
105 export const CreateOrderDtoSchema = z.object({
106 productId: z.string().describe('产品 ID'),
107 quantity: z.number().int().positive().default(1).describe('购买数量'),
108 returnTo: z.url().optional().describe('回调地址'),
109 });
110
111 export const OrderDetailVoSchema = z.object({
112 id: z.string().describe('订单 ID'),
113 amount: z.number().describe('订单金额'),
114 createdAt: z.coerce.date().describe('创建时间'),
115 });
116 ```
117
118 ### ApiDoc Options
119
120 - `summary` (required): Brief description of the endpoint
121 - `description` (optional): Detailed description
122 - `body` (optional): Request body Zod schema (use `XxxDtoSchema`, not the class)
123 - `query` (optional): Query parameters Zod schema
124 - `response` (optional): Response VO class or `[VoClass]` for array response
125
126 ## Exceptions & Error Codes
127
128 - Business exceptions use `new AppException(code)` or `new AppException(code, data)`; messages are generated from code→message mapping, custom overrides are prohibited.
129
130 ```ts
131 import { AppException, ResponseCode } from '@yikart/common';
132
133 throw new AppException(ResponseCode.MaterialGroupNotFound, { groupId: 'group_xxx' });
134 ```
135
136 ## ResponseCode Standards
137
138 - Success code is fixed at `Success = 0`; business error codes start from `10000` and are allocated by module range, cross-module reuse is prohibited.
139 - Naming uses PascalCase and must specify the concrete resource: e.g., `ContractNotFound`, `CommentNotFound`; generic permission names (`Unauthorized`/`AccessDenied` etc.) are prohibited.
140 - Define and export only in the common package; all services reference the same source to avoid scattered definitions.
141 - Must maintain code→default message mapping; use "Unknown error" for unmatched codes.
142 - Collaboration with AppException: pass only `code` or `code+data`, custom messages are not allowed (messages are generated from mapping).
143 - Addition workflow: Add constant to `ResponseCode` → Add default message to message mapping → Use in business code.
144
145 ## Permissions & Data Access
146
147 - Permissions are filtered through query conditions; prefer specific resource NotFound over generic permission exceptions; permission logic belongs in Service layer.
148 - Repository only accesses its own data model; cross-model operations, permission checks, and extra existence queries are prohibited (existence checks are done by Service first).
149
150 ## Data Filtering & Aggregation
151
152 - All filtering, counting, and aggregation MUST be done at database layer (Repository), not in application code.
153 - Prohibited: fetching a list from DB then using `.filter()` / `.some()` / `.find()` / `.reduce()` to narrow results in memory.
154 - Use Repository methods with proper query conditions instead.
155
156 ```typescript
157 // WRONG — filtering in memory
158 const records = await this.repo.listByUserId(userId)
159 const pending = records.filter(r => r.status === 'pending')
160 const total = pending.reduce((sum, r) => sum + r.amount, 0)
161
162 // CORRECT — filtering at database layer
163 const pending = await this.repo.listByUserIdAndStatus(userId, 'pending')
164 const total = await this.repo.sumAmountByUserIdAndStatus(userId, 'pending')
165 ```
166
167 - Exception: filtering on external API responses is acceptable since the data is not from our DB.
168
169 ## Logging & Lint
170
171 - Using console is prohibited; use dependency-injected Logger instance (e.g., `this.logger.log()`).
172 - Strictly follow ESLint (root `eslint.config.mjs`); must pass `pnpm lint -w` and type checking before commit.
173
174 ## Mandatory Rules (Hard Requirements)
175
176 - Controller handles routing/parameter binding/response transformation only; Service handles business orchestration and permission filtering; Repository handles data access only.
177 - Controller output must use VOs uniformly (regular VO uses `VoClass.create(data)`; pagination VO uses `new`); Service returns entity data, Controller converts to VO, not the reverse.
178 - Pagination Service methods must return `[list, total]` tuple directly from Repository (transparent pass-through); `new ListVo()` is only called in Controller; field mapping, default value filling, and field renaming are done inside `new ListVo(list.map(...), total, pagination)` in Controller; if no mapping is needed, pass `list` directly.
179 - VO schema optional fields must use `.optional()`; `.nullable()` is only for fields that can be explicitly set to `null` in business logic; DB `required: false` / `field?: Type` must map to `.optional()`, not `.nullable()`.
180 - DTO/VO must be defined with zod schema and generated using `createZodDto` / `createPaginationVo`.
181 - Pagination: input uses `PaginationDtoSchema`; output fields are fixed as page, pageSize, totalPages, total, list.
182 - AppException is constructed only with code or code+data, messages come from mapping; standard HttpException is reserved for protocol-layer errors.
183 - Permissions are filtered through query conditions; unmatched results are represented as specific resource NotFound; permission logic is in Service layer.
184 - Prefer soft delete (`deletedAt`); state transitions are handled in Service layer; avoid complex state enums.
185 - Statistics/counts are implemented at database layer, application-layer iteration aggregation is prohibited.
186 - HTTP decorator paths must start with `/`, empty parameters are prohibited; methods with pagination must end with `WithPagination`.
187 - Every Controller must have `@ApiTags` decorator; every Controller method must have `@ApiDoc` decorator.
188 - All DTO/VO schema fields must have `.describe()` for API documentation.
189
190 ## Prohibited Practices (Hard Requirements)
191
192 - Writing business logic in Controller or directly accessing database; returning database entities directly; skipping DTO/VO.
193 - Injecting Repository in Controller; data must be accessed through Service layer; Controller must not contain complex logic.
194 - Performing permission checks, cross-model operations, or extra existence queries in Repository; Repository methods containing unnecessary parameters.
195 - Using generic permission exception names (Unauthorized/PermissionDenied/AccessDenied) instead of specific resource NotFound.
196 - Overriding AppException default messages or creating custom business exception types; customizing HTTP status codes to represent business errors.
197 - Using `throw new Error()` instead of `AppException + ResponseCode` for business errors; unexpected infrastructure exceptions may be propagated to the global filter directly.
198 - Using Logger static methods or `console`; reading environment variables directly (must go through config module).
199 - Using `as any` or explicitly bypassing type checking; disabling/ignoring Lint during development.
200 - Using `z.nativeEnum`; must use `z.enum` and explicitly list enum values.
201 - Arbitrarily wrapping business logic with try-catch; exceptions should be handled uniformly by the framework.
202 - Delete operations returning meaningless objects like `{ success: true }`; should return `void`.
203 - Using wrong HTTP methods: delete uses `@Delete()` not `@Put()`; update uses `@Patch()` not `@Put()`.
204 - Designing complex state enums; doing statistics/aggregation iteration at application layer; pagination methods not following naming conventions.
205 - HTTP decorators not starting with `/` or being empty; Controller methods not following naming conventions.
206 - Writing redundant comments; code should be self-explanatory, avoid unnecessary comments explaining obvious logic.
207 - Creating Controller methods without `@ApiDoc` decorator; creating DTO/VO schema fields without `.describe()`.
208 - Writing data migration logic as NestJS Service/Controller; migrations must be pure MongoDB shell scripts placed in `migrations/` directory, executed via `mongosh`.
209 - Using `@InjectModel` or `@InjectConnection` in Service files; data access must go through Repository layer.
210 - Pagination Service methods doing `list.map(item => ({...}))` entity mapping for response fields; Service method return values containing `pageNo`/`pageSize` pagination parameters (Controller already has these).
211 - Using `.nullable()` for DB optional fields (`required: false` / `field?: Type`) instead of `.optional()`; this causes type mismatch and forces Controller to add `?? null` conversions.
212
213 ## Build Verification
214
215 - Must use `pnpm nx build <project>` to verify builds
216 - Using `tsc` directly for compilation is prohibited
217 - After renaming methods, must run `pnpm nx build` to ensure all references are updated
218 - Before merging, ensure all builds pass: `pnpm nx run-many --target=build --all`
219
219 lines MARKDOWN