一、为什么要有模板
每次新开项目都从零写 .cursorrules 效率低,而且容易遗漏重要约束。
模板解决三个问题:
- 新项目快速启动:复制模板,改几个项目特定的参数就能用
- 约束不遗漏:模板包含了实际项目中反复踩坑总结出来的约束
- 团队统一:所有人用同一套模板,AI 生成的代码风格一致
二、NestJS 后端模板
文件名:node-nestjs.cursorrules
# ===== NestJS 后端项目规范 =====
## 技术栈(必须遵守,不能替换)
- 运行时:Node.js 20+
- 框架:NestJS 10+
- 语言:TypeScript 5+,strict 模式开启
- ORM:Prisma 5+(不使用 TypeORM,不使用 Mongoose)
- 数据库:PostgreSQL 16+
- 鉴权:@nestjs/jwt + @nestjs/passport
- 文档:@nestjs/swagger(所有接口必须有 Swagger 注解)
- 验证:class-validator + class-transformer
- 配置:@nestjs/config(不允许硬编码任何配置值)
## 项目结构规范
src/
├── common/
│ ├── decorators/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── dto/ # 共享 DTO(ResponseDto、PaginationDto)
├── modules/
│ └── user/
│ ├── dto/
│ ├── user.controller.ts
│ ├── user.service.ts
│ └── user.module.ts
├── prisma/
└── main.ts
## 命名规范
- 文件名:kebab-case(user-service.ts,不是 userService.ts)
- 类名:PascalCase(UserService)
- 方法名和变量:camelCase(findAll、userId)
- 常量:SCREAMING_SNAKE_CASE(MAX_LOGIN_ATTEMPTS)
- 数据库表和字段:snake_case(created_at)
- DTO 命名:动作+资源+Dto(CreateUserDto、UpdateUserDto、FindUsersDto)
## 接口返回格式(必须统一,不允许直接返回原始数据)
{
"code": 0,
"data": any,
"message": "ok"
}
## Controller 规范
- 每个 Controller 必须有 @ApiTags('模块名')
- 每个接口必须有 @ApiOperation({ summary: '接口描述' })
- 每个接口必须有 @ApiResponse 定义成功和失败的返回格式
- 路径用 kebab-case(/api/user-profile,不是 /api/userProfile)
## Service 规范
- Service 不能引用任何 HTTP 相关对象(Request、Response、Headers)
- 所有 public 方法必须有 JSDoc 注释
- 复杂查询写在 Service 里,Controller 只做参数接收和返回
## 错误处理规范
- 使用 NestJS 内置异常类:NotFoundException、BadRequestException、
UnauthorizedException、ForbiddenException、ConflictException
- 不允许空的 catch 块
- 错误消息要对用户友好,不能暴露技术细节
## TypeScript 规范
- 禁止使用 any 类型,用 unknown 替代或定义具体类型
- 所有函数参数和返回值必须有明确类型
- 用 interface 定义数据结构(除非需要联合类型才用 type)
- 可选字段用 ?(name?: string,不是 name: string | undefined)
## 不要做的事情
- 不要生成 .spec.ts 测试文件
- 不要在 Entity 或 DTO 里使用 TypeORM 装饰器
- 不要在 Service 层直接操作 HTTP 请求/响应
- 不要硬编码端口号、数据库 URL、Secret Key
- 不要用 console.log,用 NestJS 内置的 Logger
- 不要在单个函数里超过 30 行代码,超过就拆分
每条规则背后的决策逻辑
技术栈声明写"不使用 XX"的原因
单纯写"使用 Prisma"不够,AI 训练数据里 TypeORM 比重大,不明确否定偶尔会混入。
DTO 命名写"动作+资源+Dto"的原因
不规定时 AI 会随机命名:UserInput、UserPayload、NewUserDto、CreateUserRequest 都有可能,同一项目里混用导致混乱。
返回格式给了 JSON 示例而不只是文字描述
只写"统一格式"AI 理解各有不同,有人会生成 { success, result },有人会生成 { status, body }。给了具体示例,没有歧义空间。
不要超过 30 行的原因
AI 倾向于把所有逻辑写在一个大函数里,不主动拆分。30 行限制强制它在生成时就考虑单一职责。
三、Vue3 前端模板
文件名:vue3-frontend.cursorrules
# ===== Vue3 前端项目规范 =====
## 技术栈(必须遵守)
- 框架:Vue 3.4+,使用 Composition API(不使用 Options API)
- 语言:TypeScript 5+,strict 模式
- 构建工具:Vite 5+
- 状态管理:Pinia(不使用 Vuex)
- 路由:Vue Router 4+
- 样式:TailwindCSS(优先使用原子类,减少自定义 CSS)
- 请求:axios(封装成统一的 request.ts,不直接用 axios)
- 图标:@iconify/vue
## 组件规范
- 所有组件使用 <script setup lang="ts"> 语法
- 组件文件名:PascalCase(UserProfile.vue)
- 页面组件放在 src/views/,通用组件放在 src/components/
- 单个组件不超过 200 行,超过就拆子组件
- Props 必须用 defineProps<{}>() 方式定义类型
- Emits 必须用 defineEmits<{}>() 方式定义
## 标准组件结构(生成组件时参照这个格式)
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { User } from '@/types/user'
interface Props {
userId: number
showAvatar?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
update: [user: User]
delete: [id: number]
}>()
</script>
<template>
<div class="flex items-center gap-4">
</div>
</template>
## Pinia Store 规范
- Store 文件放在 src/stores/,命名:useXxxStore.ts
- 每个 Store 只负责一个业务域的状态
- 使用 defineStore 的 setup 函数写法(不用 options 写法)
- 方法命名:动词开头(fetchUsers、updateUser、deleteUser)
## API 请求规范
- 所有 API 调用封装到 src/api/ 目录,按模块划分(user.ts、product.ts)
- 函数命名:动词+名词(getUserList、createUser)
- 统一使用 src/utils/request.ts 里封装的 axios 实例
## 路由规范
- 路由路径:kebab-case(/user-profile)
- 需要登录的路由加 meta: { requiresAuth: true }
- 所有页面组件懒加载:() => import('./views/User.vue')
## TypeScript 规范
- 接口类型定义放在 src/types/ 目录
- API 返回数据必须定义类型,不用 any
- 响应式数据:ref<具体类型>(),不要 ref<any>()
## 不要做的事情
- 不要写 Options API
- 不要使用 Vuex
- 不要在组件里直接写 axios.get()
- 不要写内联样式(style="..."),用 Tailwind 类
- 不要在 template 里写复杂逻辑,提取成 computed 或方法
- 不要在多处重复定义相同类型,放到 src/types/ 统一管理
四、团队共享方案
方案一:直接提交到 Git(推荐)
把 .cursorrules 提交到代码仓库,团队成员 git pull 后自动有这份规范。
注意:.cursorrules 不要加进 .gitignore,它就是要共享的。
git add .cursorrules
git commit -m "chore: 添加 Cursor Rules 项目规范"
git push
方案二:多 Rules 文件(Cursor 新版本支持)
对于全栈项目,可以把 Rules 按模块拆分,放在 .cursor/rules/ 目录下:
.cursor/
rules/
backend.md # NestJS 相关规范
frontend.md # Vue3 相关规范
git.md # Git commit 规范
创建目录结构:
mkdir -p .cursor/rules
touch .cursor/rules/backend.md
touch .cursor/rules/frontend.md
好处:
- 拆分管理,改后端规范不影响前端规范
- 每个文件保持精简,不会触发 Token 超长导致后半段被忽略
- 团队不同成员各自负责各自模块的 Rules 维护
方案三:Rules 模板仓库
建一个专门的 Rules 模板仓库,里面按技术栈分门别类存放模板文件:
company-cursor-rules/
├── nestjs/
│ └── .cursorrules
├── vue3/
│ └── .cursorrules
├── react/
│ └── .cursorrules
└── README.md
新项目开始时,去这个仓库复制对应的模板,根据项目特殊情况微调。
五、Rules 的迭代方式
Rules 是活文档,不是写完就不动的。
触发更新 Rules 的时机:
发现 AI 生成了不符合预期的代码 → 思考这件事 Rules 里有没有说清楚 → 有就是 Rules 描述不够精确,改精确;没有就追加新条目。
一个具体例子:
发现 AI 生成的 Service 方法直接 return prisma.user.findMany(),没有分页。
Rules 里加:
- Service 的 findAll 类方法必须支持分页,参数通过 PaginationDto 传入(page、pageSize)
下次再生成,AI 会自动加分页逻辑。
Rules 迭代的节奏建议:
- 前两周:发现问题就追加,Rules 会增长较快
- 两周后:趋于稳定,只有遇到新类型问题才追加
- 每个月:回顾一次,删掉已经不适用的条目,保持精简
六、演示操作步骤
准备工作
Step 1:初始化两个演示项目
# NestJS 后端
npx @nestjs/cli new nestjs-rules-demo
cd nestjs-rules-demo && npm install
# Vue3 前端
npm create vue@latest vue3-rules-demo
# 选项:TypeScript ✅ Vue Router ✅ Pinia ✅ ESLint ✅
cd vue3-rules-demo && npm install
Step 2:把两份模板文件提前准备好放在桌面(内容就是上面第二、三章的完整内容)
NestJS 模板演示
Step 1:用 Cursor 打开 nestjs-rules-demo
Step 2:在没有 Rules 的状态下,先在 Chat 里发送:
帮我创建一个商品管理模块(ProductModule),包含 CRUD 接口
截图或记录输出结果(用了什么 ORM、有无 Swagger、有无 ResponseDto)
Step 3:在项目根目录创建 .cursorrules,把 NestJS 模板内容粘贴进去
Step 4:新建 Chat(必须新建,不能在原来的 Chat 里继续),发送同样的 Prompt:
帮我创建一个商品管理模块(ProductModule),包含 CRUD 接口
Step 5:逐项对比两次的输出差异,重点检查:
- ORM 是否为 Prisma
- 是否有
@ApiTags、@ApiOperation - 返回值是否是
{ code, data, message }格式 - 是否有生成
.spec.ts文件(不应该有) - 是否有
any类型(不应该有)
Step 6:演示 Rules 迭代——假设发现 AI 生成的 Service 没有 JSDoc 注释
在 .cursorrules 里追加一行:
- 所有 public 方法必须有 JSDoc 注释,格式:/** 方法描述 @param 参数 @returns 返回值 */
新建 Chat,再次发送 Prompt,观察这次的输出是否有 JSDoc 注释。
Vue3 模板演示
Step 1:用 Cursor 打开 vue3-rules-demo
Step 2:在项目根目录创建 .cursorrules,粘贴 Vue3 模板内容
Step 3:在 Chat 里发送:
创建一个 UserCard 组件,展示用户的头像、名字、邮箱,
点击可以触发 select 事件,传递 userId
Step 4:检查生成的组件:
| 检查项 | 期望结果 |
|---|---|
| 脚本语法 | <script setup lang="ts"> |
| Props 定义方式 | defineProps<{ userId: number }>() |
| Emits 定义方式 | defineEmits<{ select: [userId: number] }>() |
| 样式方式 | Tailwind 原子类,无内联样式 |
| 有无 Options API | 不应该有 |
| 有无 any 类型 | 不应该有 |
团队共享演示
Step 1:在 nestjs-rules-demo 里确认 .cursorrules 存在
Step 2:执行 Git 提交:
git init
git add .cursorrules
git status # 确认 .cursorrules 在暂存区里
git commit -m "chore: 添加 NestJS 项目 Cursor Rules 规范"
Step 3:演示多 Rules 文件结构(新版 Cursor)
mkdir -p .cursor/rules
# 把 .cursorrules 的内容拆分成两个文件
cp .cursorrules .cursor/rules/backend.md
touch .cursor/rules/git.md
在 git.md 里写入 Git commit 相关规范(后续第 11 集会详细讲):
## Git Commit 规范
- 使用 Conventional Commits 格式:type(scope): subject
- type:feat / fix / refactor / test / docs / chore
- subject 不超过 50 字,动词开头
展示拆分后的目录结构:
ls -la .cursor/rules/
Spec Coding 实战补充:05 Rules 实战模板
来源:
Spec Coding实战/05 Rules 实战模板.md,已合并到本章节。
1. 模板的价值
好的 .cursorrules 不是随手写几句话,而是把团队实践中踩过的坑沉淀成约束。
这一节给出三套可以直接使用的模板:
- NODE + NESTJS 后端模板(本课重点)
- 前端模板(Vue3 + TypeScript)
- 全栈模板(NestJS + Vue3)
每套模板都可以直接复制到项目根目录,按实际情况调整。
2. NODE + NESTJS 后端模板(完整版)
# ============================================================
# NestJS Backend Cursor Rules
# 适用:Node.js 20 + NestJS 10 + Prisma 5 + PostgreSQL
# ============================================================
# 技术栈
- Runtime: Node.js 20 LTS
- Framework: NestJS 10.x
- Language: TypeScript 5.x(启用严格模式:strict: true)
- ORM: Prisma 5(禁止引入 TypeORM、Sequelize、Mongoose)
- Database: PostgreSQL 16
- 认证: @nestjs/jwt + passport-jwt
- 校验: class-validator + class-transformer
- 日志: nestjs-pino(禁止 console.log,禁止 winston)
- 配置: @nestjs/config(.env 文件)
- HTTP 客户端: axios(如需外部请求)
# 代码风格(与 prettier.config.js 保持一致)
- semi: false(无分号)
- singleQuote: true(单引号)
- tabWidth: 2(2 空格缩进)
- printWidth: 100
- trailingComma: 'all'
- endOfLine: 'lf'
# 目录结构
src/ ├── common/ │ ├── constants/ # 常量(错误码、枚举等) │ ├── decorators/ # 自定义装饰器 │ ├── dto/ # 公共 DTO(分页、排序等) │ │ └── page-query.dto.ts │ ├── filters/ # 全局异常过滤器 │ │ └── all-exceptions.filter.ts │ ├── guards/ # 全局守卫 │ ├── interceptors/ # 全局拦截器 │ │ └── transform.interceptor.ts # 统一响应格式 │ └── utils/ # 工具函数 ├── config/ # 配置 │ └── configuration.ts # 配置工厂函数 ├── modules/ # 业务模块 │ └── [feature]/ │ ├── [feature].controller.ts │ ├── [feature].service.ts │ ├── [feature].module.ts │ └── dto/ │ ├── create-[feature].dto.ts │ ├── update-[feature].dto.ts │ └── query-[feature].dto.ts └── prisma/ └── prisma.service.ts
# 统一响应格式
所有接口通过 TransformInterceptor 自动包装,无需在 Controller 手动包装:
```typescript
// 成功响应结构
{
"code": 0,
"message": "ok",
"data": T | null
}
// 分页响应结构
{
"code": 0,
"message": "ok",
"data": {
"list": T[],
"total": number,
"page": number,
"pageSize": number
}
}
```text
Controller 直接 return 数据,interceptor 自动包装。 异常通过 BusinessException 抛出,filter 自动格式化为统一错误格式。
# 异常处理
使用 BusinessException 统一抛出业务异常:
```typescript
// 不要这样写
throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
// 要这样写
throw new BusinessException(ErrorCode.USER_NOT_FOUND)
```text
ErrorCode 枚举定义在 src/common/constants/error-code.ts
# 命名约定
+ 文件名:kebab-case(create-user.dto.ts,user.service.ts)
+ 类名:PascalCase(CreateUserDto,UserService)
+ 接口/类型:PascalCase,Interface 不加 I 前缀
+ 变量、方法:camelCase
+ 常量:UPPER_SNAKE_CASE
+ 环境变量:UPPER_SNAKE_CASE(DB_HOST,JWT_SECRET)
# DTO 规范
```typescript
// create DTO:所有字段必填,明确类型校验
export class CreateUserDto {
@ApiProperty({ description: '邮箱地址' })
@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty()
email: string
@ApiProperty({ description: '密码,8-32位含大小写和数字' })
@IsString()
@MinLength(8)
@MaxLength(32)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, {
message: '密码必须包含大小写字母和数字',
})
password: string
}
// update DTO:继承 create DTO 的 Partial
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// query DTO:继承公共分页 DTO
export class QueryUserDto extends PageQueryDto {
@IsOptional()
@IsString()
search?: string
}
```text
# Prisma 使用规范
+ 查询时必须过滤软删除:where: { isDeleted: false, ...其他条件 }
+ 密码等敏感字段必须在 select/omit 中排除
+ 分页查询用 findMany + count 组合,禁止用 cursor 分页(除非明确需要)
+ Prisma 模型字段使用 camelCase,数据库列使用 snake_case(通过 @map 映射)
+ 禁止在 Service 外部直接使用 PrismaService,业务 module 通过自己的 Service 操作数据
```typescript
// 正确:分页查询写法
const [list, total] = await this.prisma.$transaction([
this.prisma.user.findMany({
where: { isDeleted: false, ...whereCondition },
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { [sortBy]: order },
omit: { password: true },
}),
this.prisma.user.count({ where: { isDeleted: false, ...whereCondition } }),
])
```text
# 日志规范
```typescript
// 注入 logger
constructor(
private readonly logger: Logger,
) {}
// 结构化日志,不要用字符串拼接
this.logger.log({ userId: user.id, email: user.email }, 'User created successfully')
this.logger.error({ userId, error: err.message }, 'Failed to create user')
// 禁止
console.log('user created:', user)
this.logger.log('user ' + user.id + ' created')
```text
# 安全规范
+ JWT 认证接口统一使用 @Auth() 自定义装饰器(内含 @UseGuards + @ApiBearerAuth)
+ 密码统一用 bcrypt,rounds=10,从配置读取:this.configService.get('BCRYPT_ROUNDS')
+ 禁止在日志、响应、错误信息中暴露 password、secret、token 完整内容
+ 禁止在代码中硬编码任何密钥或敏感配置
# Swagger 文档
+ 所有 Controller 加 @ApiTags()
+ 所有接口加 @ApiOperation({ summary: '...' })
+ 所有 DTO 字段加 @ApiProperty()
+ 需要认证的 Controller 加 @ApiBearerAuth()
# 测试要求
+ 单元测试文件命名:[feature].service.spec.ts
+ 使用 Jest,Mock 所有外部依赖(PrismaService、MailService 等)
+ 测试方法命名:should + 动词 + 场景(should_create_user_successfully)
+ 每个 service 方法至少覆盖:正常流程 + 关键异常场景
# AI 生成代码要求
+ 直接输出完整代码,不需要前置解释
+ 禁止使用 any 类型,遇到类型不确定的地方使用 unknown 或具体类型
+ 生成的代码必须可以直接运行,不留 TODO、FIXME 占位
+ 生成多个文件时,每个文件必须包含完整的 import 语句
+ 如果需要新增环境变量,在代码注释里标注:// 需要添加 ENV: VARIABLE_NAME
```plain
---
### 3. 前端模板(Vue3 + TypeScript)
```markdown
# ============================================================
# Vue3 Frontend Cursor Rules
# 适用:Vue3 + TypeScript + Vite + Pinia
# ============================================================
# 技术栈
- 框架:Vue 3.x(Composition API,禁止 Options API)
- 构建:Vite 5.x
- 语言:TypeScript 5.x(严格模式)
- 状态管理:Pinia(禁止 Vuex)
- 路由:Vue Router 4
- HTTP:axios(封装在 src/api/request.ts)
- UI:[根据项目选择:Element Plus / Naive UI / Ant Design Vue]
- 样式:SCSS(禁止在 <style> 中写无 scoped 的全局样式,全局样式放 src/styles/)
- 包管理:pnpm
# 代码风格
- semi: false
- singleQuote: true
- tabWidth: 2
- printWidth: 100
# 目录结构
src/ ├── api/ # 接口层 │ ├── request.ts # axios 实例和拦截器 │ ├── user.ts # 用户相关接口 │ └── types/ # 接口入参/返回类型定义 ├── assets/ # 静态资源 ├── components/ # 公共组件 │ └── [ComponentName]/ │ ├── index.vue │ └── types.ts # 组件 Props 类型(可选) ├── composables/ # 组合式函数(useXxx 命名) ├── router/ │ └── index.ts ├── stores/ # Pinia stores │ └── useUserStore.ts # use + 名词 + Store 命名 ├── styles/ # 全局样式 ├── utils/ # 工具函数 └── views/ # 页面组件 └── user/ ├── UserList.vue └── UserDetail.vue
# Vue 组件规范
- 使用 <script setup lang="ts"> 语法(禁止 Options API)
- Props 用 defineProps<{...}>() 泛型写法,不用 withDefaults(除非有默认值)
- Emits 用 defineEmits<{...}>() 类型声明写法
- 组件文件名:PascalCase(UserCard.vue)
- 单文件组件顺序:<script setup> → <template> → <style scoped>
```vue
<!-- 正确写法示例 -->
<script setup lang="ts">
interface Props {
userId: string
showAvatar?: boolean
}
interface Emits {
(e: 'update', user: User): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
</script>
```text
# Pinia Store 规范
使用 Setup Store 写法(函数式,不用 Options Store):
```typescript
// stores/useUserStore.ts
export const useUserStore = defineStore('user', () => {
const currentUser = ref<User | null>(null)
const isLoggedIn = computed(() => !!currentUser.value)
async function fetchCurrentUser() {
currentUser.value = await userApi.getMe()
}
return { currentUser, isLoggedIn, fetchCurrentUser }
})
```text
# API 请求规范
+ 所有接口定义在 src/api/ 对应模块文件
+ 接口函数返回 Promise<T>,类型明确
+ 不在组件/store 里直接调用 axios,通过 api 函数调用
+ 响应拦截器统一处理错误,业务代码不需要每次判断 res.code
# 命名约定
+ 组件:PascalCase(UserCard,OrderList)
+ 组合式函数:use 前缀,camelCase(useUserList,usePageQuery)
+ Store:use 前缀 + Store 后缀(useUserStore)
+ 页面路由:kebab-case(/user-list,/order-detail)
+ CSS 类名:BEM 风格(.user-card__avatar--active)
# TypeScript 规范
+ 禁止使用 any,用 unknown 或具体类型
+ 接口用 interface,简单类型别名用 type
+ 不用 namespace
+ 类型文件放在 types/ 目录或与使用文件同目录
# AI 生成代码要求
+ 直接输出完整代码
+ Vue 组件必须包含完整的 script/template/style 三个块
+ 生成的 TypeScript 类型要完整,不留 any 占位
+ 如果用到了尚未安装的包,在注释里注明:// 需要安装:pnpm add xxx
```plain
---
### 4. 全栈模板(NestJS + Vue3)
全栈模板在后端和前端规则的基础上,增加联调和共享类型的约定:
```markdown
# ============================================================
# Full-Stack Rules(NestJS + Vue3)
# ============================================================
# [继承后端模板的所有规则]
# [继承前端模板的所有规则]
# 前后端联调约定
## 共享类型
- 后端 DTO 和前端请求/响应类型保持一致
- 如果有 API 文档生成(Swagger),前端从 swagger.json 生成类型(openapi-typescript)
- 否则手动在 src/api/types/ 维护类型,与后端 DTO 字段名保持完全一致
## 接口路径约定
- 后端接口前缀:/api/v1/
- 前端 axios baseURL:http://localhost:3000(开发),通过 Vite proxy 转发
## 开发环境
- 后端运行在 3000 端口
- 前端运行在 5173 端口
- Vite proxy 配置:/api/* → http://localhost:3000
## 错误处理一致性
前端拦截器处理统一响应格式:
- code === 0:正常,返回 data
- code === 40100(未认证):跳转到登录页
- code !== 0:Toast 提示 message,抛出错误
# 目录结构(Monorepo)
project/ ├── apps/ │ ├── backend/ # NestJS 后端 │ └── frontend/ # Vue3 前端 ├── packages/ │ └── shared/ # 共享类型(可选) ├── package.json # 根 package.json └── pnpm-workspace.yaml
5. 团队协作:共享与版本管理
把 rules 纳入版本控制
# 提交到 git,让所有人共享
git add .cursorrules .cursorignore
git commit -m "chore: init cursor rules for backend development"
团队讨论 rules 变更
rules 文件不应该由一个人随便修改。建议:
1. 提 PR 修改 .cursorrules
2. PR 描述里说明:为什么要加这条规则(踩了什么坑)
3. 团队 review 后合并
rules 不是一次性的
随着项目演进,rules 需要更新:
- 升级了 Prisma 版本,有 Breaking Change?更新 Prisma 使用规范
- 新引入了一个日志库?更新日志规范,删除旧的
- 发现 AI 总是在某个地方犯同样的错?加一条明确的禁止项
6. 自定义模板的最佳实践
原则一:用否定约束效果往往好于正面说明
# 差
使用 Prisma 操作数据库
# 好
使用 Prisma 操作数据库(禁止引入 TypeORM、mongoose、knex)
AI 面对"你可以用任何东西"时,会随机选择。禁止具体的替代项,AI 就没有歧义了。
原则二:代码示例比文字描述有效
# 差
异常处理要统一
# 好
异常统一通过 BusinessException 抛出:
````typescript
throw new BusinessException(ErrorCode.USER_NOT_FOUND)
// 禁止:throw new HttpException('用户不存在', 404)
// 禁止:throw new Error('User not found')
```text
```plain
#### 原则三:分清"必须"和"建议"
```markdown
# 必须(不能违反)
- 所有密码必须 bcrypt 加密
- 禁止在接口返回密码字段
# 建议(有理由可以例外)
- Controller 方法尽量保持简洁,复杂逻辑放 Service
在 rules 里用"必须"、"禁止"等强制词表达约束;用"建议"、"尽量"表达偏好。
#### 原则四:保持简洁,不超过 500 行
超过 500 行的 rules,AI 对后半部分的遵守程度会显著下降。 如果内容很多,拆成多个文件(用 .cursor/rules/ 目录),按场景激活。
---
### 7. 小结
Rules 模板是团队工程规范的数字化。它做的事情,实际上就是把团队 Code Review 中反复提出的意见,提前告诉 AI,让 AI 一次就写对。
三套模板(NestJS 后端 / Vue3 前端 / 全栈)覆盖了大部分场景,可以直接复制使用,按项目实际情况裁剪。
**记住**:最好的 rules 不是在项目开始前一次性写完的,而是在开发过程中不断发现问题、不断往里加约束沉淀出来的。