Week 1-8: Spring Boot 学习计划完整项目

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 23:45:17 +08:00
commit f95aa18724
201 changed files with 18595 additions and 0 deletions

444
week4/教案.md Normal file
View File

@@ -0,0 +1,444 @@
# 第四阶段教案:前端基础 + 项目整合(第 4 周)
> **学习周期**7 天
> **每日用时**2-3 小时
> **里程碑**:学生管理系统 v1 — Spring Boot + JPA/MP 双轨 + 原生 JS 全栈应用
---
## 前置准备
1. 执行 `sql/init.sql` 初始化数据库(在你的 MySQL 中)
2. IDEA 打开 `week4/pom.xml`
3. 确认 `application.yml` 中数据库密码正确
---
## 第 1 天JavaScript 基础 —— 让页面"动"起来
### 核心概念
| 概念 | 解释 | 类比 |
|------|------|------|
| **变量** | `let name = "张三"` — 存放数据的容器 | 贴了标签的盒子 |
| **函数** | `function add(a, b) { return a + b; }` — 可复用的代码块 | 微波炉:输入食材,输出热菜 |
| **DOM** | Document Object Model — 把 HTML 文档变成 JS 可操作的对象树 | 遥控器:通过 JS 操控页面元素 |
| **事件** | 用户的操作(点击、输入、提交) | 门铃:按一下触发响应 |
### 阅读 app.js重点部分
打开 `static/js/app.js`,找到以下关键模式:
**1. 选择 DOM 元素并操作它**
```javascript
// 获取元素
const tbody = document.getElementById('table-body');
// 修改内容
tbody.innerHTML = '<tr><td>...</td></tr>';
```
**2. 事件绑定**
```javascript
// HTML 中直接绑定(在按钮上写 onclick="doSomething()"
<button onclick="logout()">退出</button>
// JS 中绑定(更灵活,但在这里我们用上面那种方式入门)
document.addEventListener('DOMContentLoaded', () => { ... });
```
**3. 字符串模板ES6**
```javascript
// 反引号 + ${} 插值,比 + 拼接直观很多
return `
<tr>
<td>${s.id}</td>
<td>${s.name}</td>
</tr>`;
```
### 动手
1. 打开浏览器开发者工具F12 → Console输入
```javascript
document.querySelector('h1').textContent = 'Hello JS!'
```
观察页面标题是否变化
2. 在 `app.js` 的 `renderRow` 函数中,把"年龄"列的显示改成 `${s.age} 岁`(加上"岁"字)
3. 在 Console 中手动创建一个 `<div>`,设置颜色,插入到页面中
---
## 第 2 天fetch API —— 前后端握手
### fetch 是什么?
fetch 是浏览器内置的 HTTP 客户端,让 JS 可以调用后端 API。
```javascript
// GET 请求
const response = await fetch('/api/students');
const result = await response.json(); // 解析 JSON
console.log(result.data); // 使用数据
// POST 请求
const response = await fetch('/api/students', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '新同学', age: 20, email: 'x@mail.com', score: 85 })
});
```
### async / await 是什么?
fetch 是异步操作(需要时间等待服务器响应),`await` 让代码看起来像同步的一样,更好理解。
```javascript
// ❌ 不用 async/await回调地狱
fetch(url).then(resp => resp.json()).then(data => console.log(data));
// ✅ 用 async/await像写同步代码一样
async function load() {
const resp = await fetch(url);
const data = await resp.json();
console.log(data);
}
```
### 本项目中的请求封装
打开 `app.js`,看 `api()` 函数:
```javascript
async function api(url, options) {
options.headers['Authorization'] = 'Bearer ' + token; // 自动带 Token
const resp = await fetch(url, options);
const result = await resp.json();
if (resp.status === 401 || resp.status === 403) {
logout(); // Token 失效,自动退回登录页
}
return result;
}
```
**为什么封装?** 每个请求都需要带 Authorization 头,封装后调用方只需要关心业务数据。
### 动手
1. 在 Console 中手动调用 fetch 测试 API
```javascript
fetch('/api/students/hello', { headers: { 'Authorization': 'Bearer week4-secret-token' } })
.then(r => r.json())
.then(console.log)
```
2. 观察 Network 面板F12 → Network看请求和响应的完整内容
3. 故意发一个错误的 Token观察 401 返回和处理逻辑
---
## 第 3 天Spring MVC 拦截器 —— 请求的"门卫"
### 拦截器原理
```
HTTP 请求
DispatcherServlet前端控制器
[拦截器 preHandle] ← 在这里决定"放行"还是"拦截"
│ 通过
Controller处理业务
[拦截器 postHandle/afterCompletion]
HTTP 响应
```
### 本项目拦截器做了什么?
打开 `controller/AuthInterceptor.java`
```java
@Override
public boolean preHandle(HttpServletRequest request, ...) {
// 1. 白名单直接放行
if (path.startsWith("/api/hello")) return true;
// 2. 校验 Authorization 头
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(401); // 返回 401 Unauthorized
return false; // 拦截!
}
// 3. 验证 Token 是否匹配
String token = authHeader.substring(7);
if (!validToken.equals(token)) {
response.setStatus(403); // 返回 403 Forbidden
return false;
}
return true; // 放行
}
```
### 注册拦截器WebConfig.java
```java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截所有 /api/ 路径
.excludePathPatterns("/api/hello", "/api/login"); // 白名单
}
}
```
### 动手
1. 用 Postman 不带 Authorization 头发一个 `GET /api/students`,观察 401 响应
2. 带正确的 Token `Bearer week4-secret-token`,观察正常返回
3. 把 `auth.token` 改成另一个值,重启后观察旧 Token 变为 403
4. 思考:真正的用户系统会怎么存储和验证 Token预告JWT第 5 周)
---
## 第 4 天:统一异常处理 + 参数校验
### GlobalExceptionHandler 做了什么?
```java
@RestControllerAdvice // 拦截所有 Controller
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) // 参数校验失败
public ResponseEntity<?> handleValidation(...) {
return ResponseEntity.badRequest()
.body(ApiResponse.badRequest("参数校验失败: ..."));
}
@ExceptionHandler(Exception.class) // 兜底:所有未捕获异常
public ResponseEntity<?> handleAll(...) {
return ResponseEntity.status(500)
.body(ApiResponse.error(500, "服务器内部错误"));
}
}
```
### 为什么 Controller 不需要 try-catch
因为 `@RestControllerAdvice` 在所有 Controller 方法的外层织入了一层异常处理网。任何异常抛出后,都会落入对应的 `@ExceptionHandler` 方法。
```java
// Controller 中
@PostMapping
public ApiResponse<Student> add(@Valid @RequestBody Student s) {
return ApiResponse.created(service.add(s));
// 如果 s 的 name 为空 → MethodArgumentNotValidException
// 如果 service.add 中抛异常 → Exception
// 都会被 GlobalExceptionHandler 捕获Controller 不需要处理
}
```
### 统一响应格式ApiResponse
所有接口返回统一结构:
```json
{
"code": 200,
"message": "success",
"data": { ... },
"extra": { "total": 10, "orm": "JPA" }
}
```
这样前端只需要判断 `result.code === 200` 来确定请求是否成功。
### 动手
1. 故意提交空的姓名,观察返回的 400 错误详情
2. 在 Student 实体类中新增一个校验规则:`@Min(0) @Max(100) private int score`
3. 测试传入 score=999观察校验是否生效
---
## 第 5 天RESTful 设计规范 + ORM 切换
### RESTful API 最佳实践
| 原则 | 规则 |
|------|------|
| **用名词,不用动词** | `/api/students` ✅ `/api/getStudents` ❌ |
| **HTTP 方法表达操作** | GET=查 / POST=增 / PUT=改 / DELETE=删 |
| **资源用复数** | `/api/students` 而不是 `/api/student` |
| **层级表示关系** | `/api/students/5/avatar` = 学生 5 的头像 |
| **正确使用状态码** | 200=成功 / 201=创建成功 / 400=参数错误 / 404=未找到 / 500=服务器错误 |
### ORM 模式切换
本项目用 `orm.mode` 配置实现了 JPA 和 MP 的无缝切换:
```yaml
# application.yml
orm:
mode: jpa # 改成 mp 即可切换到 MyBatis-Plus
```
原理:`StudentServiceSelector` 读取 `@Value("${orm.mode}")`,根据值选择调用 `StudentJpaService` 还是 `StudentMpService`。Controller 层完全不感知底层用的是哪个 ORM。
### 动手
1. 修改 `orm.mode` 为 `mp`,重启,观察前端页面显示的 ORM 标签
2. 测试同一个接口的返回数据是否完全一致
3. 对比控制台 SQL 输出JPA 和 MP 生成的 SQL 有何不同
4. 思考:如果要在运行时动态切换(不重启),需要怎么改?
---
## 第 6 天:文件上传 + CORS 跨域
### Spring Boot 文件上传
Controller 中处理文件只需 `@RequestParam("file") MultipartFile file`
```java
@PostMapping("/{id}/avatar")
public ResponseEntity<?> uploadAvatar(
@PathVariable Long id,
@RequestParam("file") MultipartFile file) {
// 1. 文件校验
if (file.isEmpty()) return badRequest("文件为空");
String ext = originalName.substring(originalName.lastIndexOf("."));
// 2. 保存到磁盘
String filename = UUID.randomUUID() + ext; // 随机名防冲突
file.transferTo(new File("./uploads", filename));
// 3. 更新数据库
service.updateAvatar(id, filename);
}
```
### 前端如何上传文件
```javascript
const formData = new FormData();
formData.append('file', fileInput.files[0]); // 从 <input type="file"> 取文件
fetch(`/api/students/${id}/avatar`, {
method: 'POST',
body: formData // 不设 Content-Type浏览器自动设置 multipart/form-data + boundary
});
```
### CORS 跨域
当前前后端同端口,不存在跨域。但项目已经预置了 CORS 配置(`WebConfig.addCorsMappings`),为后续前后端分离做准备。
### 静态资源映射
```java
// 将本地 uploads/ 目录映射到 URL /uploads/**
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:./uploads/");
```
这样通过 `http://localhost:8080/uploads/xxx.jpg` 就能访问上传的头像。
### 动手
1. 新增一个学生,编辑该学生 → 上传一张头像 → 观察表格中头像显示
2. 打开 `./uploads/` 目录,确认文件已保存
3. 故意上传一个超过 2MB 的文件,观察错误提示
4. 修改 `application.yml` 中 `spring.servlet.multipart.max-file-size` 为 5MB
---
## 第 7 天:项目整合与总结
### 本周完整请求流程
```
浏览器
├── 1. GET / → 加载 index.html + style.css + app.js
├── 2. 用户输入 Token → JS 调用 /api/students/hello 验证
├── 3. Token 验证通过 → 进入主面板 → JS 调用 /api/students 加载数据
│ │
│ ▼
│ [AuthInterceptor] 校验 Authorization 头
│ │ 通过
│ ▼
│ [StudentController] 接收请求 → 参数校验(@Valid
│ │
│ ▼
│ [StudentServiceSelector] 根据 orm.mode 选择 JPA 或 MP Service
│ │
│ ▼
│ [StudentJpaService / StudentMpService] 执行业务逻辑
│ │
│ ▼
│ [Repository / Mapper] 操作数据库
│ │
│ ▼
│ 返回 ApiResponse 格式 JSON → JS 渲染到页面
└── 异常 → [GlobalExceptionHandler] 拦截 → 返回统一错误 JSON
```
### 完整验收清单
**后端Postman 逐项测试)**
- [ ] `GET /api/students/hello` 无需 Token返回运行状态
- [ ] `GET /api/students` 需 Token返回全部学生
- [ ] `GET /api/students?keyword=张` 模糊搜索
- [ ] `POST /api/students` 新增一个学生
- [ ] `GET /api/students/1` 查询 ID=1
- [ ] `PUT /api/students/1` 更新学生信息
- [ ] `DELETE /api/students/1` 删除学生
- [ ] `POST /api/students/1/avatar` 上传头像
- [ ] 不带 Token 发请求 → 401
- [ ] 带错误 Token 发请求 → 403
- [ ] 提交空 name → 400 校验错误
- [ ] 修改 `orm.mode: mp` 后功能不变
**前端(浏览器)**
- [ ] 输入正确 Token → 进入主面板
- [ ] 输入错误 Token → 显示错误
- [ ] 表格正确展示全部学生(头像、成绩徽章颜色正确)
- [ ] 新增学生 → 表单提交 → 表格刷新
- [ ] 编辑学生 → 回填数据 → 更新成功
- [ ] 上传头像 → 头像显示在表格中
- [ ] 搜索 → 过滤数据
- [ ] 删除 → 确认弹窗 → 删除成功
- [ ] 退出登录 → 回到登录页
### 本周产出
```
✅ 原生 JS 前后端交互fetch + DOM 操作)
✅ Token 认证与 401/403 处理
✅ Spring MVC 拦截器(登录权限校验)
✅ 全局异常处理(统一 JSON 格式错误返回)
✅ 参数校验(@Valid + Bean Validation + 自定义错误消息)
✅ RESTful API 设计规范
✅ ORM 配置切换JPA ↔ MyBatis-Plus一行配置切换
✅ 文件上传(头像,前端 FormData + 后端 MultipartFile
✅ CORS 跨域预配置
✅ 统一 API 响应格式ApiResponse<T>
```
---
> **本周核心**:你已从 Java 基础走到了能独立交付一个**全栈应用**的水平。后端有三层架构和双 ORM前端有原生 JS 交互和文件上传,系统有拦截器保护、统一异常处理、参数校验。这是你第一个真正的里程碑项目。