背景痛点:毕业设计为何总被吐槽“像玩具”
每年 3 月,学院 GitLab 上都会冒出 200+ 新仓库,但答辩时老师只看三样东西:README、测试报告、可运行的 jar。结果 70% 的同学卡在第一步——“选题太大、边界不清、功能堆砌”。典型症状如下:
- 需求一句话:“做一个教务系统”,没有角色、没有用例,后续不断拍脑袋加功能。
- 代码一锅粥:controller 里写 SQL,service 里调 Redis,一个类 800 行,分层仅存在于 PPT。
- 测试靠主函数:public static void main 里 new 一个对象,System.out.println 一把梭,连 @Test 都没见过。
- 回滚靠 Ctrl+Z:没有分支、没有 tag,答辩前夜“祖传”final_final_v2.zip 横空出世。
这些问题的根因不是“不会写代码”,而是缺少“工程化思维”。AI 辅助开发的价值,恰恰是把“工程套路”以可复制的形式喂给你,让你在 12 周内跑完完整 SDLC,而不是 11 周都在调前端样式。
技术选型对比:三种开发模式的 24 小时交付实验
为了量化差异,我设计了一个最小实验:实现“课程管理系统”的 5 个核心接口(新增课程、选课、退课、查询课表、成绩录入)。同一人、同一需求,分别用三种方式实现,记录“可运行时间”与“代码质量分”(SonarQube 默认规则)。
| 模式 | 可运行时间 | 代码质量分 | 优点 | 缺点 |
|---|---|---|---|---|
| 纯手动 | 8 h | C (2.1k 技术债) | 思路清晰,完全可控 | 重复代码多,分层靠自觉 |
| Copilot 交互 | 3.5 h | B (0.9k 技术债) | 补全快,命名较规范 | 容易接受“看起来对”的幻觉代码,测试仍需自己写 |
| 自建 Agent 流程(LLM+Prompt+CLI) | 2 h | A (0.3k 技术债) | 一次性生成骨架、单元测试、OpenAPI 文档,目录规范 | 需要提前写 Prompt 模板,对提示词质量敏感 |
结论:AI 不是替代思考,而是把“体力活”压缩到 20% 时间,让你把剩余精力投入到“边界划分、异常流程、安全审查”这些高阶任务。
核心实现:用 5 轮 Prompt 把“课程管理系统”拆成可交付工程
以下流程全部脚本化,仓库初始化后 30 分钟内即可得到可跑通的 Spring Boot 工程。
需求澄清 Prompt
“扮演产品经理,输出一份用例文档,覆盖学生、教师、管理员三类角色,功能包括课程 CRUD、选课、退课、成绩录入,使用 Markdown 表格描述主成功场景与异常流程。”架构草图 Prompt
“基于上述用例,生成 C4 的 Container 图:前端 React,后端 Spring Boot,数据库 Postgres,使用 PlantUML 语法。”模块划分 Prompt
“按 DDD 分层,输出 Maven 多模块结构:course-domain、course-application、course-infrastructure,给出每个模块的 pom 片段与包名约定。”API 设计 Prompt
“为‘选课’用例设计 RESTful 接口,返回统一包装结果 Result ,提供 OpenAPI 3.0 YAML,并生成 SpringDoc 注解。”数据库 Schema Prompt
“根据实体 Course、Student、Enrollment,输出 Postgres DDL,要求:外键级联删除、check 约束保证学分>0、给 enrollment 建联合唯一索引防止重复选课。”
每轮输出后,人工做三件事:
- 用例是否闭环——没有“忘记密码”这类隐形需求。
- 接口是否幂等——选课接口用 POST /enrollments,幂等键(studentId+courseId)放唯一索引。
- DDL 是否可逆——flyway 脚本命名加版本号,本地可 rollback。
Clean Code 示例:让 AI 先生成,再人工精炼
以下代码由 Agent 生成,我仅调整了两处:把 JPA 查询抽象到 Repository,并补充单元测试。亮点:接口与实现分离、领域异常封装、可测试。
// course-domain/src/main/java/course/domain/model/Course.java package course.domain.model; import jakarta.persistence.*; import lombok.*; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "course") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Course { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int credit; @OneToMany(mappedBy = "course", cascade = CascadeType.ALL, orphanRemoval = true) private Set<Enrollment> enrollments = new HashSet<>(); public Course(String name, int credit) { if (credit <= 0) throw new IllegalArgumentException("credit must be positive"); this.name = name; this.credit = credit; } }// course-application/src/main/java/course/application/EnrollService.java package course.application; import course.domain.model.Course; import course.domain.model.Enrollment; import course.domain.model.Student; import course.domain.repo.CourseRepository; import course.domain.repo.EnrollmentRepository; import course.domain.repo.StudentRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class EnrollService { private final CourseRepository courseRepository; private final StudentRepository studentRepository; private final EnrollmentRepository enrollmentRepository; @Transactional public void enroll(Long studentId, Long courseId) { Student student = studentRepository.findById(studentId) .orElseThrow(() -> new NotFoundException("student")); Course course = courseRepository.findById(courseId) .orElseThrow(() -> new NotFoundException("course")); boolean exists = enrollmentRepository.existsByStudentAndCourse(student, course); if (exists) throw new BusinessException("already enrolled"); Enrollment enrollment = new Enrollment(student, course); enrollmentRepository.save(enrollment); } }// course-application/src/test/java/course/application/EnrollServiceTest.java package course.application; import course.domain.model.Course; import course.domain.model.Student; import course.domain.repo.EnrollmentRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.*; @SpringBootTest @Transactional class EnrollServiceTest { @Autowired private EnrollService enrollService; @Autowired private EnrollmentRepository enrollmentRepository; @Autowired private TestDataBuilder builder; @Test void shouldRejectDuplicateEnrollment() { Student s = builder.saveStudent(); Course c = builder.saveCourse(); enrollService.enroll(s.getId(), c.getId()); assertThatThrownBy(() -> enrollService.enroll(s.getId(), c.getId())) .isInstanceOf(BusinessException.class) .hasMessageContaining("already enrolled"); } }要点:
- 领域对象纯 POJO,不依赖框架。
- 应用服务只负责编排,不写 SQL。
- 测试用数据构造器 TestDataBuilder 复用,保证测试独立。
AI 生成内容的风险与人工审查清单
- 安全漏洞:LLM 喜欢在示例里把密码明文存 String,必须提醒它“使用 char[] + BCrypt”。
- 非幂等:批量更新语句忘了加版本号,并发测试必挂。
- SQL 注入:MyBatis XML 里用 ${} 拼接,要替换成 #{}。
- 许可证污染:引入的第三方库需 OSS Review 工具扫描,禁止 GPL 进商业仓库。
- 性能幻觉:AI 给出的“分页+大 in 查询”在 100 万数据量下直接 OOM,需用游标或分页子查询重写。
人工审查三步走:
- 静态扫描:SonarQube + SpotBugs 强制质量门。
- 差异 Review:只审 AI 生成 commit,标记
AI-GEN标签,方便追溯。 - 场景测试:把典型业务路径写成 BDD
.feature,用 Cucumber 每日回归。
生产环境避坑指南:让 Demo 能真正跑在服务器
版本控制
- main 分支保护,PR 至少 1 人 + CI 通过。
- Tag 采用语义版本规范 v1.0.0,打 tag 即触发 CD 到测试环境。
文档同步
- 采用“文档即接口”策略:OpenAPI YAML 由代码注解反向生成,提交时自动更新,拒绝“代码和文档不同步”。
- 架构图用 C4 模型 PlantUML 存 docs/ 目录,CI 渲染成 PNG 后上传到 Wiki,防止“图过时”。
依赖锁定
- Maven/Gradle 使用 .lockfile,Node 使用 package-lock.json;禁止
latest.release。 - 每季度执行
mvn versions:display-dependency-updates,人工评估后批量升级。
- Maven/Gradle 使用 .lockfile,Node 使用 package-lock.json;禁止
配置与环境分离
- 采用 12-Factor,敏感信息进 Vault/K8s Secret,本地只留
.env.example。 - 数据库迁移用 Flyway,baseline-on-migrate=true,防止生产库首次执行失败。
- 采用 12-Factor,敏感信息进 Vault/K8s Secret,本地只留
监控与回滚
- 启动探针:Spring Boot Actuator +
/health/liveness,K8s 就绪探针 5 秒一次。 - 回滚策略:保留前一镜像,helm rollback 1 分钟内完成;数据库迁移若回滚,需准备 down 脚本并提前在预发环境验证。
- 启动探针:Spring Boot Actuator +
结语:如果明天没有 AI,你还能复现这套工程结构吗?
把 AI 当“加速器”而非“拐杖”才是毕业设计真正的收获。试着把本文所有 Prompt 删掉,仅用白板和笔记本,你依旧能徒手画出分层架构、写出幂等 SQL、补全单元测试——那一刻,你交付的已不只是“课程管理系统”,而是一份可迁移到任何技术栈的工程素养。