文章目录
-
- [一、 为什么 AI 写的代码总像"外包"?------认识 Skills 规则文件](#一、 为什么 AI 写的代码总像“外包”?——认识 Skills 规则文件)
-
- [新手常遇的痛点:AI 写的代码总像"外包"](#新手常遇的痛点:AI 写的代码总像“外包”)
- [什么是 Skills(规则文件)?](#什么是 Skills(规则文件)?)
- [二、 划定技术栈边界:别让 AI 用老黄历写新代码](#二、 划定技术栈边界:别让 AI 用老黄历写新代码)
-
- [明确 JDK 版本与依赖库:告别"跨版本穿越"](#明确 JDK 版本与依赖库:告别“跨版本穿越”)
- 指定构建工具与插件限制:统一"施工流程"
- [禁用清单:明确告诉 AI "不要用什么"](#禁用清单:明确告诉 AI “不要用什么”)
- [三、 绘制项目的"建筑蓝图":用架构约束锁定代码流向](#三、 绘制项目的“建筑蓝图”:用架构约束锁定代码流向)
-
- [为什么 AI 总是把业务逻辑写到 Controller 里?](#为什么 AI 总是把业务逻辑写到 Controller 里?)
- [使用 Mermaid 流程图向 AI 描绘架构](#使用 Mermaid 流程图向 AI 描绘架构)
- [四、 统一团队"黑话":让 AI 听懂你的业务领域语言](#四、 统一团队“黑话”:让 AI 听懂你的业务领域语言)
-
- [避免"翻译腔":别让 AI 当生硬的同声传译](#避免“翻译腔”:别让 AI 当生硬的同声传译)
- 将业务缩写与特定名词注入规则
- 规定枚举类和常量类的定义与使用场景
- [五、 设定不可逾越的"红线":异常处理与日志规范](#五、 设定不可逾越的“红线”:异常处理与日志规范)
-
- [告别 `e.printStackTrace()`:统一日志框架与占位符用法](#告别
e.printStackTrace():统一日志框架与占位符用法) - 异常分类哲学:业务异常与系统异常的严格区分
- [校验规则的统一:何时用 `@Valid`,何时手动抛异常](#校验规则的统一:何时用
@Valid,何时手动抛异常)
- [告别 `e.printStackTrace()`:统一日志框架与占位符用法](#告别
- [六、 摆脱说教,直接上"满分试卷":注入标杆代码示例](#六、 摆脱说教,直接上“满分试卷”:注入标杆代码示例)
-
- 千言万语不如一段真实代码
- 如何在规则文件中引用项目中的"模范类"
- [标杆 Java 代码展示](#标杆 Java 代码展示)
- 为什么这段"满分试卷"威力巨大?
一、 为什么 AI 写的代码总像"外包"?------认识 Skills 规则文件
在大多数程序员刚开始用 Cursor、Claude Code 或者 Trae 这类 AI 编程助手时,通常都会经历这样一个心路历程:
"哇,一下就跑通了!" ➡️ "等等,这代码怎么写的?" ➡️ "删掉,我自己重写吧。"
新手常遇的痛点:AI 写的代码总像"外包"
为什么会有这种感觉?因为默认状态下的 AI,就像是一个精通百门语言的"自由职业者"。你让它写一个保存用户的功能,它确实能帮你实现,但它会暴露出以下问题:
- 乱用工具包 :你项目里明明统一用了
org.apache.commons.lang3的字符串处理,它偏要给你手写一段java.util的底层逻辑,或者引入你项目里根本不存在的包。 - 毫无章法的分层:你明明规划了严格的 Controller、Service、Mapper 三层架构,它为了图省事,直接在 Controller 里写了一大堆 SQL 查询逻辑。
- 变量命名随意 :你团队里统一定义了用户余额叫
balance,它给你来个money或者userMoney,导致后续维护时到处都是同义词。
它能干活,但它不遵守你们团队的"江湖规矩"。这种代码一旦合并到主干,就像一件精心定制的西装上打了个五颜六色的补丁。
什么是 Skills(规则文件)?
要解决这个问题,就不能把 AI 当成"工具",而要把它当成一个刚入职的新人程序员 。
想象一下,一个新员工第一天来上班,你肯定不会直接把他按在工位上说:"开始写代码"。你会给他发一份**《员工手册》和 《项目开发规范》。
在 AI 编程工具里,Skills 规则文件就是这份 员工手册**。
它是一个纯文本文件(通常是 Markdown 格式),你在里面写下硬性规定。当 AI 准备生成代码或者修改代码时,它会先"翻阅"一遍这份手册,然后严格按照手册里的规矩来敲键盘。
比如,你在手册里写了一条:"所有时间类型必须使用 java.time.LocalDateTime,禁止使用 java.util.Date。" 那么 AI 就像一个背熟了条文的员工,绝不敢在你的代码里写出一个 Date 来。
二、 划定技术栈边界:别让 AI 用老黄历写新代码
AI 是个刚入职的新人。但如果这个新人虽然看了《员工手册》,却拿着十年前的技术栈来写代码,那同样是一场灾难。
很多开发者会疑惑:"我明明用的是最新的 Spring Boot 3,为什么 AI 还在给我写 javax.servlet 的代码?"
这是因为,AI 的肚子里装了从 Java 1.5 到 Java 21 的所有知识,它不知道你当前项目的"时间线"在哪里。如果你不划定技术栈的边界,它就会凭概率输出它见过最多的"老黄历"代码。
明确 JDK 版本与依赖库:告别"跨版本穿越"
在 Java 世界里,版本的跨越往往意味着不兼容。最典型的就是 Spring Boot 2.x 到 3.x 的升级,底层从 Java EE 迁移到了 Jakarta EE,导致大量的包名发生了变化。
如果你不告诉 AI 你的版本,它可能会在你的 Spring Boot 3 项目里写出这样的导入:
java
// 错误示例:这是 Spring Boot 2.x (老黄历) 的写法
import javax.validation.Valid;
import javax.servlet.http.HttpServletRequest;
当这段代码粘进你的 IDE 里,满屏飘红。为了防止这种"穿越",我们需要在 Skills 规则文件里直接把版本和依赖锁死:
markdown
## 技术栈版本约束
- JDK 版本:严格使用 JDK 17,禁止使用 JDK 8 特性(如不使用 java.util.Date,统一用 java.time)。
- 核心框架:Spring Boot 3.2.0。
- 依赖包命名空间:由于是 Spring Boot 3.x,所有 Web 和校验相关的包名必须是 Jakarta 命名空间。
有了这段话,AI 再写代码时,就会自动纠正为:
java
// 正确示例:符合 Spring Boot 3.x 规范
import jakarta.validation.Valid;
import jakarta.servlet.http.HttpServletRequest;
指定构建工具与插件限制:统一"施工流程"
现在的 Java 项目大多用 Maven 或 Gradle 构建。虽然它们都能把项目跑起来,但在写法和插件使用上大有不同。特别是现在越来越多的项目开始引入 Lombok 或者 MapStruct 这类注解处理器。
如果不加限制,AI 可能会在 POM 文件里乱加一些你团队根本不用的插件,或者漏掉关键的配置。
你可以在规则文件中这样规定:
markdown
## 构建工具规范 (Maven)
- 本项目使用 Maven 作为构建工具,禁止生成 build.gradle 相关文件。
- 依赖管理:所有的版本号必须在 <dependencyManagement> 中统一管理,子模块禁止直接写死版本号。
- 必须包含的编译插件:
- maven-compiler-plugin(配置 source 和 target 为 17)
- lombok(必须在 maven-compiler-plugin 的 annotationProcessorPaths 中配置,不要指望全局依赖生效)
当你要求 AI 帮你新增一个模块或者引入 MyBatis-Plus 时,它就不会简单地扔给你一个 <dependency> 标签,而是会严格按照你规定的格式,把版本号放在正确的地方,并处理好 Lombok 的编译依赖。
禁用清单:明确告诉 AI "不要用什么"
在约束人的时候,我们常说"要做什么",但在约束 AI 时,"不要做什么"往往比"要做什么"更有效力 。AI 很聪明,但也喜欢走捷径。有些技术在语法上完全正确,但被你所在的团队明令禁止。
这时候,一份"黑名单"就非常重要:
markdown
## 绝对禁用清单(红灯区)
遇到以下情况,即使代码能跑通,也绝对不允许生成:
1. 禁用 JPA 的自动建表功能(禁止出现 `spring.jpa.hibernate.ddl-auto=update` 或 `create`)。
2. 禁止使用 `System.out.println()` 打印日志,必须使用 Slf4j。
3. 禁止使用 `fastjson`(存在安全漏洞),所有 JSON 序列化统一使用 `Jackson` 或 `com.alibaba.fastjson2`。
4. 禁止在代码中捕获 Exception 后空处理(即 `catch (Exception e) {}` 这种毫无意义的吞异常行为)。
为什么需要这么强硬?因为当你问 AI "帮我把这个对象转成 JSON" 时,如果没设禁令,它大概率会因为它训练数据里 fastjson 的 API 最短、最简单,就直接给你 JSON.toJSONString()。一旦引入,不仅违反团队规范,还可能有很大的安全隐患。
总结一下,划定技术栈边界,本质上就是在告诉 AI:"这是我们的武器库,你可以用这里面最新的枪炮,但绝对不准用外面捡来的土铳。" 只有边界清晰了,AI 写出来的代码才能直接无缝融入你的项目。
三、 绘制项目的"建筑蓝图":用架构约束锁定代码流向
你肯定遇到过这种让人抓狂的场景:你让 AI "写一个根据用户 ID 查询订单的接口",结果它洋洋洒洒给你返回了一段代码,所有的查库逻辑、数据组装、甚至金额计算,全塞在了一个 Controller 的方法里,方法长度眼看就要突破一百行。
为什么 AI 总是把业务逻辑写到 Controller 里?
这其实不能全怪 AI,我们要理解它的"求生欲"。
AI 的底层逻辑是预测下一个最可能出现的词 。在它看过的海量开源代码、个人博客和 StackOverflow 问答中,为了"快速演示如何调用一个接口",无数开发者都习惯把所有逻辑写在 Controller 里。
对 AI 来说,把逻辑写在 Controller 里是"最保险、最常见"的输出方式,因为它无法通过肉眼看出你的项目其实有着严格的架构规范。它没有"全局观",就像一个盲人摸象的泥瓦匠,你让他砌一堵墙,他就真的一直砌,根本不管这堵墙会不会把大厦的承重墙给堵死。
要解决这个问题,我们不能只用文字去干巴巴地解释"请遵守三层架构",我们需要直接把项目的建筑蓝图贴在它的工位上。
使用 Mermaid 流程图向 AI 描绘架构
人类是视觉动物,其实 AI 也是。现代的编程助手(如 Claude Code、Cursor)对 Mermaid 流程图的理解能力极强。在 Skills 规则文件中,画一张清晰的架构图,胜过你写一千字的解释。
假设我们使用的是经典的 Java 三层架构,你可以在规则文件里直接写入以下内容:
markdown
## 项目架构蓝图
本项目严格遵循以下分层架构,任何代码生成必须符合此流向:
```mermaid
graph TD
Client((客户端)) --> Controller[表现层 Controller]
Controller -->|传输对象 DTO| Service[业务逻辑层 Service]
Service -->|领域对象 DO| Mapper[数据访问层 Mapper/Repository]
Mapper --> Database[(MySQL 数据库)]
classDef controller fill:#f9f,stroke:#333,stroke-width:2px;
classDef service fill:#bbf,stroke:#333,stroke-width:2px;
classDef mapper fill:#bfb,stroke:#333,stroke-width:2px;
class Controller controller;
class Service service;
class Mapper mapper;
架构职责定义:
-
Controller 层:仅负责接收请求、参数校验(@Valid)、调用 Service、返回响应。禁止包含任何业务计算和数据库查询。
-
Service 层:核心业务逻辑所在。负责事务管理(@Transactional)、对象转换(DTO 与 DO 互转)、业务规则校验。
-
Mapper 层:仅负责与数据库交互,禁止出现业务逻辑判断(比如不要在 SQL 的 WHERE 里写复杂的业务状态机判断)。
当你把这段图表丢给 AI 后,它的脑子里就不再是模糊的文字,而是一张清晰的"地图"。当你再让它写功能时,它会自动对号入座,知道哪段代码该放在哪个"房间"里。
规定层与层之间的数据流转与依赖方向
光有架构图还不够,AI 经常会在"数据流转"上犯错。比如它可能会在 Service 层直接返回数据库实体类(DO)给 Controller,导致敏感字段(如用户密码)被直接暴露给前端;或者它为了省事,在 Controller 里直接注入 Mapper 跳过 Service。
我们需要在规则文件中制定严格的**"交通规则"**:markdown## 层级依赖与数据流转红线 1. **单向依赖原则**:只能从上层调用下层(Controller -> Service -> Mapper)。 - ❌ 绝对禁止:Controller 直接注入 Mapper。 - ❌ 绝对禁止:Mapper 反向调用 Service。 2. **对象隔离原则**:不同层之间传递数据,必须使用对应的对象,严禁混用。 - Controller 接收和返回:`xxxDTO` (Data Transfer Object) - Service 内部处理:`xxxDO` (Data Object,对应数据库表结构) - 转换工具:DTO 与 DO 的转换必须使用 `MapStruct` 生成的 Converter,禁止手写 Getter/Setter 循环赋值。
有了这两条"红线",我们来看看当遇到实际需求时,AI 的表现会发生怎样脱胎换骨的变化。
错误示范(没有架构约束时的 AI 输出):
java
import org.apache.ibatis.session.SqlSessionFactory; // 违反红线:Controller直接引入了底层数据库组件
import com.example.demo.entity.UserDO; // 违反红线:直接返回了数据库实体
@RestController
public class UserController {
@Autowired
private SqlSessionFactory sqlSessionFactory; // 违反红线:跳过了Service和Mapper
@GetMapping("/user/{id}")
public UserDO getUser(@PathVariable Long id) {
// 违反红线:业务逻辑写在了Controller里
try (var session = sqlSessionFactory.openSession()) {
var user = session.selectOne("com.example.demo.mapper.UserMapper.selectById", id);
if (user.getAge() < 18) { // 违反红线:业务判断混入Controller
throw new RuntimeException("未成年人禁止访问");
}
return user; // 违反红线:数据库实体直接暴露给前端,密码等字段会泄露
}
}
}
正确示范(加入架构蓝图与流转规则后的 AI 输出):
java
import com.example.demo.dto.UserDTO;
import com.example.demo.dto.UserInfoResponse;
import com.example.demo.service.UserService;
import com.example.demo.converter.UserConverter;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class UserController {
// 符合规范:只依赖 Service 层
private final UserService userService;
// 符合规范:使用 MapStruct 生成的转换器
private final UserConverter userConverter;
@GetMapping("/user/{id}")
public UserInfoResponse getUser(@PathVariable @Valid Long id) {
// 符合规范:Controller 极度轻量,只做调用和转换
UserDTO userDTO = userService.queryUserById(id);
return userConverter.toResponse(userDTO);
}
}
通过给 AI 提供一张架构图和两条简单的依赖红线,你其实就是在告诉它:"在这个工地上,搬砖的去搬砖,和水泥的去和水泥,谁也别越界干别人的活。" 这样生成的代码,才真正具备企业级项目的可维护性。
四、 统一团队"黑话":让 AI 听懂你的业务领域语言
在真实的业务开发中,每个公司、每个团队都有一套自己的"黑话"。
比如,你们公司做的是电商,系统里的"商品"可能不叫 Product,而叫 SKU 或者 SPU;做金融系统的,"账户"可能不叫 Account,而叫 Ledger(账本)或者 Wallet(钱包)。
当你对着 AI 说:"帮我写一个接口,冻结这个用户的账户",如果 AI 不懂你们的黑话,它就会展现出浓烈的**"翻译腔"**。
避免"翻译腔":别让 AI 当生硬的同声传译
"翻译腔"是 AI 写业务代码时最容易被忽略,却也最让人难受的毛病。
假设你们团队的实体类叫 MerchantFundDetail(商户资金明细),你让 AI 写一段查询逻辑。不懂业务黑话的 AI 会怎么写注释和方法名?
java
// 糟糕的"翻译腔":AI 只是在做字面翻译
/**
* 获取商户钱的细节
*/
public List<MerchantFundDetail> getMerchantMoneyDetails(String merchantId) {
...
}
这段代码能跑,但放到你们团队的代码库里就像个异类。团队里大家都管这个叫"流水",管扣钱叫"冻扣",结果 AI 给你来了个"钱的细节"。
要解决这个问题,我们需要在 Skills 规则文件里,给 AI 建立一个**"业务术语词典"**,直接把英文类名和业务黑话死死绑定在一起:
markdown
## 核心业务术语词典
在生成代码注释、方法名、变量名时,必须严格使用以下业务术语,禁止使用通俗化翻译:
- `MerchantFundDetail` 对应业务词:**资金流水**(禁止翻译为:资金细节、商户钱记录)
- 冻结资金动作:**冻扣**(禁止翻译为:冻结扣除、freeze money)
- `UserAccount` 对应业务词:**会员资产**(禁止翻译为:用户账号、用户钱)
- 分发优惠券动作:**发券**(禁止翻译为:分发折扣票)
当 AI 看到了这个词典,你再让它写同样的逻辑,它就会输出非常地道的"内部代码":
java
// 地道的业务代码:完全符合团队语境
/**
* 根据商户ID查询资金流水
*/
public List<MerchantFundDetail> getMerchantFundFlows(String merchantId) {
...
}
将业务缩写与特定名词注入规则
除了业务动作,业务名词的缩写也是重灾区。特别是对于一些特定行业的系统,AI 很难猜到两个字母的缩写到底代表什么。
比如在物流系统中,TO 可能不是 To(去往),而是 Transfer Order(转运单);DO 不是 Data Object,而是 Delivery Order(配送单)。
如果你不告诉 AI,它可能会在方法里写出 getTo() 这种让人摸不着头脑,甚至和 Java 关键字 to 混淆的代码。
在规则文件中,我们需要明确缩写的全称和适用范围:
markdown
## 业务缩写与专有名词规范
- `TO`:仅代表 Transfer Order(转运单),不要作为介词 to 的缩写使用。
- `DO`:在 `com.example.logistics.domain` 包下,仅代表 Delivery Order(配送单)。
- `COD`:Cash On Delivery(货到付款),在枚举或常量中必须大写。
- 禁止使用拼音缩写作为变量名(如禁止出现 `kch` 代表库存号)。
规定枚举类和常量类的定义与使用场景
业务"黑话"在代码中最具体的载体,其实就是枚举 和常量 。
新手用 AI 时,AI 特别喜欢在业务逻辑里直接写死 1、2、3,或者直接写 if (status.equals("SUCCESS"))。这种写法一旦业务状态发生变化,修改起来就是灾难。
我们需要在规则里强制 AI 使用枚举,并规定它怎么写枚举:
markdown
## 枚举与常量使用铁律
1. **禁止魔法值**:业务代码中绝对不允许出现表示状态的数字(如 0, 1, 2)或未经定义的字符串(如 "SUCCESS", "FAIL")。
2. **枚举统一定义位置**:所有业务枚举必须放在 `com.example.common.enums` 包下。
3. **枚举规范模板**:枚举类必须包含 `code`(存入数据库的 int 或 String)、`desc`(给前端看的中文描述)两个属性,并提供根据 code 获取枚举的静态方法。
我们可以直接在规则文件里给 AI 贴一个**"满分试卷"**(样板代码),AI 的模仿能力极强,看到模板它就会照着抄:
java
// 【规则文件中的样板代码展示】
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 订单状态枚举
*/
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {
INIT(10, "待发券"),
FROZEN(20, "已冻扣"),
CANCELLED(90, "已作废");
private final Integer code;
private final String desc;
/**
* 根据 code 获取枚举(AI 必须按照此格式生成其他枚举的查询逻辑)
*/
public static OrderStatusEnum getByCode(Integer code) {
return Arrays.stream(values())
.filter(e -> e.getCode().equals(code))
.findFirst()
.orElse(null);
}
}
有了这个规范和模板,当你要求 AI:"写一个判断订单是否已冻扣的逻辑"时,它再也不会写出 if (order.getStatus() == 20) 这种魔法值代码了,而是会乖乖输出:
java
import com.example.common.enums.OrderStatusEnum;
// AI 生成的逻辑:使用了规范的枚举进行判断
if (OrderStatusEnum.FROZEN.getCode().equals(order.getStatus())) {
// 执行业务逻辑...
}
统一团队黑话,本质上就是把 AI 从一个"懂 Java 语法的机器",驯化为一个"懂你们公司业务的资深员工"。当它写出来的变量名、注释和状态判断跟你身边的同事一模一样时,这才是 AI 辅助编程真正的价值所在。
五、 设定不可逾越的"红线":异常处理与日志规范
如果在代码审查时,只能挑出一个最让人血压升高的点,很多老程序员会把票投给:糟糕的异常处理和日志打印 。
当你让默认状态的 AI 去"处理一下可能出现的错误"时,它最喜欢干两件事:要么甩给你一个 e.printStackTrace(),要么来一句 System.out.println("出错了:" + e.getMessage())。
在生产环境中,前者会把错误信息直接吐到黑洞般的标准输出里,任何日志收集系统都抓不到;后者不仅抓不到,还会因为大量的字符串拼接拖慢系统性能。这就好比家里进了贼,AI 不打 110 报警,而是自己躲在角落里写日记。
我们必须在 Skills 规则文件中,为异常和日志画上不可逾越的"红线"。
告别 e.printStackTrace():统一日志框架与占位符用法
首先要做的就是"禁用老古董",并确立唯一的发声渠道。在 Java 生态里,这几乎是毫无争议的标准答案:Slf4j 配合 Logback。
但仅仅告诉 AI "用 Slf4j"是不够的,AI 在拼接日志时依然会犯错。我们需要在规则中明确规定日志的书写语法:
markdown
## 日志规范红线
1. **绝对禁令**:严禁出现 `e.printStackTrace()`、`System.out.println()`、`System.err.println()`。
2. **日志门面**:统一使用 Lombok 的 `@Slf4j` 注解注入日志对象,禁止手动声明 `LoggerFactory.getLogger()`。
3. **占位符法则**:日志输出必须使用 `{}` 作为占位符,**绝对禁止**使用字符串拼接(`+`)。
- ❌ 错误:`log.info("处理用户:" + userId + "失败,原因:" + msg);`
- ✅ 正确:`log.info("处理用户:{}失败,原因:{}", userId, msg);`
4. **异常堆栈打印**:在 log.error 中打印异常时,必须把异常对象作为最后一个参数传入,不要在 message 里解析异常。
- ❌ 错误:`log.error("出错:" + e.getMessage());` (丢失了完整堆栈!)
- ✅ 正确:`log.error("处理用户:{}发生异常", userId, e);`
有了这条红线,当你让 AI 补全一段捕获异常的代码时,它就能输出非常标准的生产级代码:
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrderService {
public void processOrder(Long orderId) {
try {
// 业务逻辑...
} catch (Exception e) {
// 完美符合规范:使用 @Slf4j,使用 {} 占位符,保留完整异常堆栈
log.error("处理订单:{}时发生系统异常", orderId, e);
}
}
}
异常分类哲学:业务异常与系统异常的严格区分
AI 处理异常时另一个大坑是"一锅炖":不管是因为用户没填参数,还是因为数据库连不上,统统抛出一个 RuntimeException 或者直接返回 null。这会导致上层的全局异常拦截器无法区分错误类型,前端也没法给用户展示友好的提示。
我们需要在规则里给 AI 灌输**"异常分类哲学"**,就像医院分诊一样,把感冒发烧和骨折大出血分开处理:
markdown
## 异常分类与抛出规范
系统只允许存在两种异常类型,必须严格区分:
1. **业务异常**:
- 场景:用户输入错误、业务规则不满足(如余额不足、订单已取消)。
- 处理:必须抛出自定义的业务异常 `com.example.common.exception.BizException`。
- 特点:这种异常不需要打印 error 日志(因为它是预期内的正常业务拦截),全局拦截器会提取其中的 message 直接返回给前端展示。
2. **系统异常**:
- 场景:数据库宕机、网络超时、空指针等非预期故障。
- 处理:不要手动抛出,交由框架向上抛出,由全局异常拦截器捕获。
- 特点:必须打印 error 级别日志及完整堆栈,用于告警排查;前端统一展示"系统开小差了"。
配合这段文字,我们最好在规则里给出 BizException 的实际样例,让 AI 知道该怎么调用:
java
// 【规则文件中的样板代码展示】
import lombok.Getter;
@Getter
public class BizException extends RuntimeException {
private final Integer code;
public BizException(String message) {
super(message);
this.code = 400; // 默认业务错误码
}
public BizException(Integer code, String message) {
super(message);
this.code = code;
}
}
现在,你让 AI 写一段"扣减库存"的逻辑,它的表现会变得极其专业:
java
import com.example.common.exception.BizException;
public void deductStock(Long skuId, Integer deductCount) {
Integer currentStock = mapper.getStock(skuId);
if (currentStock == null) {
// 系统异常:没查到数据,属于非预期故障,向上抛出,由全局拦截器打印error日志
throw new NullPointerException("SKU数据不存在");
}
if (currentStock < deductCount) {
// 业务异常:库存不足,属于预期内的业务规则拦截,只抛 BizException,不打 error 日志
throw new BizException("库存不足,当前库存:" + currentStock);
}
mapper.updateStock(skuId, deductCount);
}
校验规则的统一:何时用 @Valid,何时手动抛异常
在接口入参阶段,AI 经常陷入纠结:有时它会在 Controller 里写一堆 if (name == null),有时它又在 Service 层试图用 @NotNull 去校验业务字段。
我们必须帮 AI 理清"防线"的概念:
markdown
## 参数校验防线规范
1. **第一道防线(Controller 层 - 格式校验)**:
- 仅用于校验基础格式:非空、长度、邮箱正则等。
- 必须使用 `@Valid` 配合 JSR 303 注解(如 `@NotBlank`, `@Size`)。
- 禁止在 Controller 里写 `if-else` 判断请求参数格式。
2. **第二道防线(Service 层 - 业务校验)**:
- 用于校验业务规则:查数据库判断唯一性、判断账户状态、判断金额是否足够等。
- 必须使用上面定义的 `throw new BizException(...)`。
- 禁止在 Service 层使用 `@Valid` 去做业务规则校验。
通过这三条红线的约束(日志怎么打、异常怎么分、校验在哪做),你相当于给 AI 配备了一套标准的"警报系统"。它写出来的代码将不再是一个漏风的破房子,而是一座有着严密安防体系的堡垒。
六、 摆脱说教,直接上"满分试卷":注入标杆代码示例
在前面的五个章节里,我们给 AI 立了规矩、画了架构图、定了业务黑话。这相当于给新入职的程序员发了一本厚厚的《员工手册》。
但现实情况是,绝大多数人拿到员工手册后是不会从头读到尾的。当遇到具体任务时,他们最快的学习方式是什么?------看老员工是怎么写代码的。
AI 也是一样。虽然它能理解长篇大论的 Markdown 文本,但模型底层最擅长的是模式匹配 。你在规则里写了"请使用 Lombok 和 Slf4j",它可能转头就忘;但如果你直接甩给它一段完美符合规范的代码,它会像照葫芦画瓢一样,精准地复刻出每一个细节。
在 Skills 规则文件中,这一招被称为注入标杆代码。
千言万语不如一段真实代码
很多开发者在写规则时,喜欢用否定句:"不要用 java.util.Date"、"不要在 Controller 里写业务逻辑"、"不要手写 Getter"。这种"说教式"的规则有两个致命弱点:
- 消耗 Token 注意力:AI 在生成代码时,注意力权重是分散的,它可能在生成到一半时,已经忘记了二十行前那个"不要"。
- 没有给出明确出路 :你告诉它"不要手写 Getter",但没告诉它"你要用
@Data",它可能就会换成另一种奇怪的写法。
相反,如果你直接把一段"满分试卷"拍在它面前,告诉它"以后就照着这个样子写",效果是摧枯拉朽的。在 AI 的眼里,这段代码就是最高权重的"黄金模板"。
如何在规则文件中引用项目中的"模范类"
你不需要把项目里几万行代码全塞进规则文件里(那样会超出上下文限制且浪费算力),你只需要摘取最具代表性的**"代码片段"**。
在引用时,有一个极其关键的技巧:必须包含完整的包名导入 。
如果不写 import,AI 就只能靠猜。它看到 @GetMapping,可能会给你导入一个老版本的包;看到 @NotNull,可能会导入 javax.validation 而不是你们项目用的 jakarta.validation。完整的 import 列表,就是在替 AI 锁定它背后的类库版本。
你可以这样在规则文件里引入模范类:
markdown
## 代码标杆模板
在生成任何 Controller、Service 及其相关对象时,必须严格模仿以下代码的风格、注解使用和导入规范。
这是本项目最标准的"满分试卷",违背以下风格的代码将被拒绝。
标杆 Java 代码展示
下面这段代码,浓缩了我们前五章讲过的所有核心要点(技术栈边界、分层架构、业务黑话、日志异常红线)。你可以直接把这段代码原封不动地贴进你的 .cursorrules 或 CLAUDE.md 中。
java
// ---------------------------------------------------------
// 标杆 1:表现层
// ---------------------------------------------------------
package com.example.mall.controller;
import com.example.mall.common.result.Result; // 统一响应封装
import com.example.mall.dto.req.OrderCreateReq; // 请求入参 DTO
import com.example.mall.dto.resp.OrderDetailResp; // 响应出参 DTO
import com.example.mall.service.OrderService;
import com.example.mall.converter.OrderConverter; // MapStruct 转换器
import jakarta.validation.Valid; // 划定技术栈:Jakarta 命名空间
import lombok.RequiredArgsConstructor; // 划定技术栈:使用 Lombok
import lombok.extern.slf4j.Slf4j; // 划定技术栈:使用 Slf4j
import org.springframework.web.bind.annotation.*;
// 规范体现:使用 @Slf4j,不手动声明 Logger;使用 @RequiredArgsConstructor,不使用 @Autowired
@Slf4j
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
// 规范体现:单向依赖,Controller 只能注入 Service,绝对不能注入 Mapper
private final OrderService orderService;
private final OrderConverter orderConverter;
// 规范体现:参数校验第一道防线,使用 @Valid,不写 if-else 判断格式
@PostMapping("/create")
public Result<OrderDetailResp> createOrder(@RequestBody @Valid OrderCreateReq req) {
log.info("收到创建订单请求, 业务线:{}, 用户:{}", req.getBizLine(), req.getUserId());
// 规范体现:Controller 极度轻量,只做调用和对象转换(DTO 转 DO 隔离)
OrderDetailResp resp = orderConverter.toResp(orderService.createOrder(req));
return Result.success(resp);
}
}
// ---------------------------------------------------------
// 标杆 2:业务逻辑层
// ---------------------------------------------------------
package com.example.mall.service.impl;
import com.example.mall.common.exception.BizException; // 自定义业务异常
import com.example.mall.dto.req.OrderCreateReq;
import com.example.mall.domain.entity.OrderDO; // 领域对象 DO
import com.example.mall.mapper.OrderMapper;
import com.example.mall.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
@Override
// 规范体现:涉及到数据修改,必须明确标注事务传播机制
@Transactional(rollbackFor = Exception.class)
public OrderDO createOrder(OrderCreateReq req) {
// 规范体现:第二道防线,业务规则校验失败,直接抛 BizException,不打 error 日志
if (req.getItemCount() > 50) {
throw new BizException("单次购买商品数量不能超过50个");
}
try {
OrderDO orderDO = new OrderDO();
// 规范体现:使用业务黑话命名,不写直白的翻译
orderDO.setBizLine(req.getBizLine());
orderDO.setOrderStatus(OrderStatusEnum.INIT.getCode());
orderMapper.insert(orderDO);
return orderDO;
} catch (Exception e) {
// 规范体现:系统异常防线,使用 {} 占位符,异常对象放最后保留完整堆栈
log.error("创建订单数据库异常, 业务线:{}", req.getBizLine(), e);
throw e; // 继续向上抛出,交由全局拦截器处理
}
}
}
// ---------------------------------------------------------
// 标杆 3:对象转换器 ------ 重点提醒 AI
// ---------------------------------------------------------
package com.example.mall.converter;
import com.example.mall.domain.entity.OrderDO;
import com.example.mall.dto.resp.OrderDetailResp;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
// 规范体现:严禁手写 get/set 进行 DTO 与 DO 的互转,必须使用 MapStruct
@Mapper
public interface OrderConverter {
OrderConverter INSTANCE = Mappers.getMapper(OrderConverter.class);
OrderDetailResp toResp(OrderDO orderDO);
}
为什么这段"满分试卷"威力巨大?
当你把上面这段代码放进 Skills 规则文件后,AI 的行为模式会发生根本性的改变。它不再是在浩如烟海的 GitHub 开源库里随机抽样,而是把这段代码当作了概率分布的锚点。
- 自动解决包名冲突 :它看到
import jakarta.validation.Valid;,以后全项目都不会再出现javax。 - 自动规避禁用语法 :它看到注入用的是
构造器 + final,以后全项目都不会再出现@Autowired字段注入。 - 自动复制分层习惯 :它看到 Controller 里调了 Service 又调了 Converter,以后它生成代码时,哪怕你不提醒,它也会自觉地把这两层给拼装好。
写规则文件的终极心法就是:你想要什么样的代码,就亲手写一段最完美的给它看。 把抽象的规范具象化为一段几十行的样板代码,是让 Cursor、Claude Code 等工具彻底"长"成你想要的样子,最快、最暴力、也最有效的手段。