在这里插入图片描述

写在前面

三个月前开始用AI编程工具,第一周觉得这玩意儿简直是神——CRUD接口、单元测试、前端页面,点几下就出来了。开发效率直接翻倍。

然后上线第二周出事了。

用户反馈某些操作莫名其妙没有权限。排查了4个小时,定位到是AI写的权限校验代码——角色继承关系理解反了。代码逻辑是天衣无缝的,但业务逻辑是全错的。修复加测试整整花了一天。

从那天起我整理了一份代码审查checklist,每次AI生成的代码对着过一遍。三个月下来,这篇checklist帮我省了至少两周的返工时间。今天全摊给你。

环境说明:本文案例基于Spring Boot 3.x + Spring Security项目,AI工具涵盖多款主流AI编程产品。每个案例均为真实经历,附详细的修复代码。

一、坑一:业务逻辑理解偏差——频次最高、后果最严重

这就是上面提到的权限校验事故。复盘一下细节。

需求很简单:"判断当前用户是否有权限访问某个资源,要考虑角色继承。"比如角色A继承角色B的权限,那么拥有角色A的用户自动拥有角色B的所有权限。

AI写的代码:

// AI生成的权限校验逻辑(有问题)
public boolean hasPermission(String resourcePath) {
    Set<String> userRoles = getCurrentUserRoles();
    Set<String> requiredRoles = getRequiredRoles(resourcePath);
    
    for (String required : requiredRoles) {
        if (userRoles.contains(required)) {
            return true;
        }
    }
    return false;
}

这段代码的问题在哪?它只检查了用户是否直接拥有某个角色,完全没有处理角色继承关系。我们是这样的层级:超级管理员继承管理员的权限,管理员继承编辑的权限。用户A被分配了"超级管理员"角色,他应该也能访问只有"编辑"权限的资源。但上面的代码认为他不可以。

修复后的代码:

// 修复后:包含角色继承的权限校验
public boolean hasPermission(String resourcePath) {
    Set<String> userRoles = getCurrentUserRoles();
    // 展开继承关系,拿到用户拥有的所有角色(直接+继承)
    Set<String> expandedRoles = roleHierarchyService.expandRoles(userRoles);
    Set<String> requiredRoles = getRequiredRoles(resourcePath);
    
    for (String required : requiredRoles) {
        if (expandedRoles.contains(required)) {
            return true;
        }
    }
    return false;
}

修复成本:定位4小时 + 修复1小时 + 回归测试2小时 = 半天没了。

避坑方法: 涉及业务规则的代码,在给AI提需求时显式列出所有规则,不要让AI"猜"。比如:“判断权限时需要通过RoleHierarchyService.expandRoles()展开用户的全部角色(直接分配+继承获取的),然后再判断。”

二、坑二:SQL没有索引意识——开发环境飞一样,线上直接崩

这个坑是隐形的。开发环境数据少,完全测不出来。

需求:商品列表查询,支持按名称模糊搜索、按分类筛选、按价格区间筛选、分页。

AI生成的JPA Repository:

// AI生成的Repository(有问题)
@Repository
public interface ProductRepository extends JpaRepository<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") String name,
                         @Param("category") String category,
                         @Param("minPrice") BigDecimal minPrice,
                         @Param("maxPrice") BigDecimal maxPrice,
                         Pageable pageable);
}

看着没问题对吧?但表上只有一个主键索引。模糊搜索LIKE %:name%根本走不了索引,全表扫描。

500条数据:0.03秒
5万条数据:1.8秒
50万条数据:直接超时

修复:

-- 手动加联合索引
ALTER TABLE product ADD INDEX idx_product_search (category, name);
-- 注意:LIKE '%keyword%' 前缀通配导致索引失效,改用全文索引或ES
-- 如果模糊搜索是核心功能,建议引入Elasticsearch
ALTER TABLE product ADD FULLTEXT INDEX ft_name (name);

避坑方法: AI生成SQL后,EXPLAIN跑一遍。如果type列是ALL(全表扫描),必须处理。另外明确告诉AI表上已有的索引名称。

三、坑三:线程安全完全不考虑——并发场景直接翻车

这个坑最隐蔽,因为单线程测试永远测不出来。

让AI写一个商品浏览计数器:

// AI生成的计数器(有问题)
@Service
public class ViewCountService {
    private Map<Long, Integer> viewCountMap = new HashMap<>();
    
    public void increment(Long productId) {
        Integer count = viewCountMap.getOrDefault(productId, 0);
        viewCountMap.put(productId, count + 1);
    }
    
    public Integer getCount(Long productId) {
        return viewCountMap.getOrDefault(productId, 0);
    }
}

HashMap在并发场景下会出什么问题?两个线程同时put可能导致循环链表,CPU飙到100%。而且即使不发生循环链表,count的值也会因为竞态条件而丢失计数。

修复:

// 修复后:线程安全版本
@Service
public class ViewCountService {
    private final ConcurrentHashMap<Long, AtomicInteger> viewCountMap = new ConcurrentHashMap<>();
    
    public void increment(Long productId) {
        viewCountMap.computeIfAbsent(productId, k -> new AtomicInteger(0))
                     .incrementAndGet();
    }
    
    public Integer getCount(Long productId) {
        AtomicInteger count = viewCountMap.get(productId);
        return count != 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 (Exception e) {
    e.printStackTrace();  // ❌ 在Docker/K8s里根本没人看控制台
}

或者更离谱的:

} catch (Exception e) {
    // TODO: 添加日志  // ← AI真的会这么写
}

到了生产环境(Docker容器 + K8s集群),printStackTrace的内容不会进入任何日志采集系统。线上出错了你看不到任何痕迹。

修复:

// 修复后:规范的异常处理
private static final Logger log = LoggerFactory.getLogger(OrderService.class);

try {
    processOrder(orderId);
} catch (BusinessException e) {
    log.warn("订单处理业务异常,orderId={}, reason={}", orderId, e.getMessage());
    throw e;
} catch (Exception e) {
    log.error("订单处理系统异常,orderId={}", orderId, e);
    throw new ServiceException("订单处理失败,请联系管理员", e);
}

避坑方法: 全文搜索catch块。确保每个catch都有:①日志记录(带业务上下文)②明确的处理策略(抛出自定义异常 / 走补偿逻辑 / 降级处理)。空catch绝对不过审。

五、坑五:配置值硬编码——上线后改一次发一次版

AI从来不把配置值放在配置文件里:

// AI生成的硬编码(到处是魔法数字)
@Service
public class OrderService {
    private static final int MAX_RETRY = 3;
    private static final long TIMEOUT_MS = 30000;
    private static final int PAGE_SIZE = 20;
    
    public void process() {
        // 中间用了这些硬编码的值
    }
}

问题:测试环境30秒超时够用,线上用户多了Connection Pool爆了。调到60秒 → 改代码 → 打包 → 发布 → 重启。一整个发布流程,就为了改个数字。

修复:

# application.yml
order:
  max-retry: ${ORDER_MAX_RETRY:3}
  timeout-ms: ${ORDER_TIMEOUT_MS:30000}
  page-size: ${ORDER_PAGE_SIZE:20}
// 修复后:从配置读取
@Service
public class OrderService {
    @Value("${order.max-retry:3}")
    private int maxRetry;
    
    @Value("${order.timeout-ms:30000}")
    private long timeoutMs;
    
    @Value("${order.page-size:20}")
    private int pageSize;
}

避坑方法: 全文搜索字面量数字。凡是可能改的、环境之间有差异的,全部提取到配置文件。

六、坑六:缺少幂等性设计——超时重试导致重复操作

客户端调用接口 → 网络超时 → 自动重试 → 同一笔订单被扣了两次款。

AI生成的接口从不考虑幂等性:

// AI生成的扣款接口(缺少幂等保护)
@PostMapping("/pay")
public ResponseEntity<String> pay(@RequestBody PayRequest request) {
    // 直接扣款,没有检查是否已经扣过
    accountService.deduct(request.getOrderId(), request.getAmount());
    return ResponseEntity.ok("扣款成功");
}

修复:

// 修复后:加入幂等性校验
@PostMapping("/pay")
public ResponseEntity<String> pay(@RequestBody PayRequest request) {
    String idempotentKey = "pay:idempotent:" + request.getOrderId();
    Boolean success = redisTemplate.opsForValue()
        .setIfAbsent(idempotentKey, "1", Duration.ofHours(24));
    
    if (Boolean.FALSE.equals(success)) {
        log.warn("重复扣款请求,orderId={}", request.getOrderId());
        return ResponseEntity.ok("已处理");
    }
    
    try {
        accountService.deduct(request.getOrderId(), request.getAmount());
        return ResponseEntity.ok("扣款成功");
    } catch (Exception e) {
        redisTemplate.delete(idempotentKey);
        throw e;
    }
}

避坑方法: 涉及金钱、库存、状态变更的接口,必须要求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/Response

AI改完之后用Git diff看变更,确认没问题再合。

写在最后

这7个坑不是让你不要用AI编程——正好相反,用对之后效率提升是实打实的。关键是知道AI的弱点在哪,在弱点上人工把关,在优势上放手让它干。

用AI编程的正确节奏是:AI负责速度,你负责质量。AI写第一版,你做最终审查。AI出覆盖量,你提供判断力。

建议你把这篇收藏下来,每次AI生成代码后对着7个检查项过一遍。坚持一个月,返工时间至少减半。

有问题评论区聊。如果这篇对你有用,点赞收藏让更多人看到,我继续出第二篇《AI生成SQL的9个安全漏洞》。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐