【AI踩坑实录】让AI写了3个月代码,这7个坑让我返工了整整2周——附完整避坑清单
文章目录
- 写在前面
- 一、坑一:业务逻辑理解偏差——频次最高、后果最严重
- 二、坑二:SQL没有索引意识——开发环境飞一样,线上直接崩
- 三、坑三:线程安全完全不考虑——并发场景直接翻车
- 四、坑四:异常处理只打e.printStackTrace()——上线后日志失踪
- 五、坑五:配置值硬编码——上线后改一次发一次版
- 六、坑六:缺少幂等性设计——超时重试导致重复操作
- 七、坑七:代码能跑但无法维护——技术债从第一行开始
- 写在最后
写在前面
三个月前开始用AI编程工具,第一周觉得这玩意儿简直是神——CRUD接口、单元测试、前端页面,点几下就出来了。开发效率直接翻倍。
然后上线第二周出事了。
用户反馈某些操作莫名其妙没有权限。排查了4个小时,定位到是AI写的权限校验代码——角色继承关系理解反了。代码逻辑是天衣无缝的,但业务逻辑是全错的。修复加测试整整花了一天。
从那天起我整理了一份代码审查checklist,每次AI生成的代码对着过一遍。三个月下来,这篇checklist帮我省了至少两周的返工时间。今天全摊给你。
环境说明:本文案例基于Spring Boot 3.x + Spring Security项目,AI工具涵盖多款主流AI编程产品。每个案例均为真实经历,附详细的修复代码。
一、坑一:业务逻辑理解偏差——频次最高、后果最严重
这就是上面提到的权限校验事故。复盘一下细节。
需求很简单:"判断当前用户是否有权限访问某个资源,要考虑角色继承。"比如角色A继承角色B的权限,那么拥有角色A的用户自动拥有角色B的所有权限。
AI写的代码:
// AI生成的权限校验逻辑(有问题)publicbooleanhasPermission(StringresourcePath){Set<String>userRoles=getCurrentUserRoles();Set<String>requiredRoles=getRequiredRoles(resourcePath);for(Stringrequired:requiredRoles){if(userRoles.contains(required)){returntrue;}}returnfalse;}这段代码的问题在哪?它只检查了用户是否直接拥有某个角色,完全没有处理角色继承关系。我们是这样的层级:超级管理员继承管理员的权限,管理员继承编辑的权限。用户A被分配了"超级管理员"角色,他应该也能访问只有"编辑"权限的资源。但上面的代码认为他不可以。
修复后的代码:
// 修复后:包含角色继承的权限校验publicbooleanhasPermission(StringresourcePath){Set<String>userRoles=getCurrentUserRoles();// 展开继承关系,拿到用户拥有的所有角色(直接+继承)Set<String>expandedRoles=roleHierarchyService.expandRoles(userRoles);Set<String>requiredRoles=getRequiredRoles(resourcePath);for(Stringrequired:requiredRoles){if(expandedRoles.contains(required)){returntrue;}}returnfalse;}修复成本:定位4小时 + 修复1小时 + 回归测试2小时 = 半天没了。
避坑方法:涉及业务规则的代码,在给AI提需求时显式列出所有规则,不要让AI"猜"。比如:“判断权限时需要通过RoleHierarchyService.expandRoles()展开用户的全部角色(直接分配+继承获取的),然后再判断。”
二、坑二:SQL没有索引意识——开发环境飞一样,线上直接崩
这个坑是隐形的。开发环境数据少,完全测不出来。
需求:商品列表查询,支持按名称模糊搜索、按分类筛选、按价格区间筛选、分页。
AI生成的JPA Repository:
// AI生成的Repository(有问题)@RepositorypublicinterfaceProductRepositoryextendsJpaRepository<Product,Long>{@Query("SELECT p FROM Product p WHERE "+"(:name IS NULL OR p.name LIKE %:name%) AND "+"(:category IS NULL OR p.category = :category) AND "+"(:minPrice IS NULL OR p.price >= :minPrice) AND "+"(:maxPrice IS NULL OR p.price <= :maxPrice)")Page<Product>search(@Param("name")Stringname,@Param("category")Stringcategory,@Param("minPrice")BigDecimalminPrice,@Param("maxPrice")BigDecimalmaxPrice,Pageablepageable);}看着没问题对吧?但表上只有一个主键索引。模糊搜索LIKE %:name%根本走不了索引,全表扫描。
500条数据:0.03秒
5万条数据:1.8秒
50万条数据:直接超时
修复:
-- 手动加联合索引ALTERTABLEproductADDINDEXidx_product_search(category,name);-- 注意:LIKE '%keyword%' 前缀通配导致索引失效,改用全文索引或ES-- 如果模糊搜索是核心功能,建议引入ElasticsearchALTERTABLEproductADDFULLTEXTINDEXft_name(name);避坑方法:AI生成SQL后,EXPLAIN跑一遍。如果type列是ALL(全表扫描),必须处理。另外明确告诉AI表上已有的索引名称。
三、坑三:线程安全完全不考虑——并发场景直接翻车
这个坑最隐蔽,因为单线程测试永远测不出来。
让AI写一个商品浏览计数器:
// AI生成的计数器(有问题)@ServicepublicclassViewCountService{privateMap<Long,Integer>viewCountMap=newHashMap<>();publicvoidincrement(LongproductId){Integercount=viewCountMap.getOrDefault(productId,0);viewCountMap.put(productId,count+1);}publicIntegergetCount(LongproductId){returnviewCountMap.getOrDefault(productId,0);}}HashMap在并发场景下会出什么问题?两个线程同时put可能导致循环链表,CPU飙到100%。而且即使不发生循环链表,count的值也会因为竞态条件而丢失计数。
修复:
// 修复后:线程安全版本@ServicepublicclassViewCountService{privatefinalConcurrentHashMap<Long,AtomicInteger>viewCountMap=newConcurrentHashMap<>();publicvoidincrement(LongproductId){viewCountMap.computeIfAbsent(productId,k->newAtomicInteger(0)).incrementAndGet();}publicIntegergetCount(LongproductId){AtomicIntegercount=viewCountMap.get(productId);returncount!=null?count.get():0;}}修复成本:写代码5分钟,但线上排查花了一个下午(CPU飙升 -> 线程dump -> 定位循环链表 -> 修bug)。
避坑方法:代码里有共享状态(Map、List、计数器),强制AI使用并发安全容器。HashMap → ConcurrentHashMap,int → AtomicInteger,ArrayList → CopyOnWriteArrayList(读多写少场景)。
四、坑四:异常处理只打e.printStackTrace()——上线后日志失踪
AI最喜欢的一种写法:
// AI生成的异常处理(完全无用)try{processOrder(orderId);}catch(Exceptione){e.printStackTrace();// ❌ 在Docker/K8s里根本没人看控制台}或者更离谱的:
}catch(Exceptione){// TODO: 添加日志 // ← AI真的会这么写}到了生产环境(Docker容器 + K8s集群),printStackTrace的内容不会进入任何日志采集系统。线上出错了你看不到任何痕迹。
修复:
// 修复后:规范的异常处理privatestaticfinalLoggerlog=LoggerFactory.getLogger(OrderService.class);try{processOrder(orderId);}catch(BusinessExceptione){log.warn("订单处理业务异常,orderId={}, reason={}",orderId,e.getMessage());throwe;}catch(Exceptione){log.error("订单处理系统异常,orderId={}",orderId,e);thrownewServiceException("订单处理失败,请联系管理员",e);}避坑方法:全文搜索catch块。确保每个catch都有:①日志记录(带业务上下文)②明确的处理策略(抛出自定义异常 / 走补偿逻辑 / 降级处理)。空catch绝对不过审。
五、坑五:配置值硬编码——上线后改一次发一次版
AI从来不把配置值放在配置文件里:
// AI生成的硬编码(到处是魔法数字)@ServicepublicclassOrderService{privatestaticfinalintMAX_RETRY=3;privatestaticfinallongTIMEOUT_MS=30000;privatestaticfinalintPAGE_SIZE=20;publicvoidprocess(){// 中间用了这些硬编码的值}}问题:测试环境30秒超时够用,线上用户多了Connection Pool爆了。调到60秒 → 改代码 → 打包 → 发布 → 重启。一整个发布流程,就为了改个数字。
修复:
# application.ymlorder:max-retry:${ORDER_MAX_RETRY:3}timeout-ms:${ORDER_TIMEOUT_MS:30000}page-size:${ORDER_PAGE_SIZE:20}// 修复后:从配置读取@ServicepublicclassOrderService{@Value("${order.max-retry:3}")privateintmaxRetry;@Value("${order.timeout-ms:30000}")privatelongtimeoutMs;@Value("${order.page-size:20}")privateintpageSize;}避坑方法:全文搜索字面量数字。凡是可能改的、环境之间有差异的,全部提取到配置文件。
六、坑六:缺少幂等性设计——超时重试导致重复操作
客户端调用接口 → 网络超时 → 自动重试 → 同一笔订单被扣了两次款。
AI生成的接口从不考虑幂等性:
// AI生成的扣款接口(缺少幂等保护)@PostMapping("/pay")publicResponseEntity<String>pay(@RequestBodyPayRequestrequest){// 直接扣款,没有检查是否已经扣过accountService.deduct(request.getOrderId(),request.getAmount());returnResponseEntity.ok("扣款成功");}修复:
// 修复后:加入幂等性校验@PostMapping("/pay")publicResponseEntity<String>pay(@RequestBodyPayRequestrequest){StringidempotentKey="pay:idempotent:"+request.getOrderId();Booleansuccess=redisTemplate.opsForValue().setIfAbsent(idempotentKey,"1",Duration.ofHours(24));if(Boolean.FALSE.equals(success)){log.warn("重复扣款请求,orderId={}",request.getOrderId());returnResponseEntity.ok("已处理");}try{accountService.deduct(request.getOrderId(),request.getAmount());returnResponseEntity.ok("扣款成功");}catch(Exceptione){redisTemplate.delete(idempotentKey);throwe;}}避坑方法:涉及金钱、库存、状态变更的接口,必须要求AI加入幂等校验。
七、坑七:代码能跑但无法维护——技术债从第一行开始
这个坑没有上面那些那么炸裂,但日积月累是最伤元气的。
AI生成的代码通常:
- 方法长度300行+
- 变量名:a, b, c, temp, data
- 零注释
- 业务逻辑和数据库访问混在一起
- 同一个判断逻辑复制了5处
这些代码今天能跑,下个月你自己都看不懂。
解决方式不是不让AI写代码,是让AI自己审查自己写的代码。
// 给AI的代码审查指令(放在prompt最后)// 请按照以下标准审查你生成的代码:// 1. 找出所有超过50行的public方法,建议拆分// 2. 找出所有含义不明的变量名(如temp、data、result),建议重命名// 3. 找出可以提取为私有方法的重复代码块// 4. 给所有public方法添加JavaDoc注释// 5. 确保Service层不直接操作HttpServletRequest/ResponseAI改完之后用Git diff看变更,确认没问题再合。
写在最后
这7个坑不是让你不要用AI编程——正好相反,用对之后效率提升是实打实的。关键是知道AI的弱点在哪,在弱点上人工把关,在优势上放手让它干。
用AI编程的正确节奏是:AI负责速度,你负责质量。AI写第一版,你做最终审查。AI出覆盖量,你提供判断力。
建议你把这篇收藏下来,每次AI生成代码后对着7个检查项过一遍。坚持一个月,返工时间至少减半。
有问题评论区聊。如果这篇对你有用,点赞收藏让更多人看到,我继续出第二篇《AI生成SQL的9个安全漏洞》。