Files
gc-plan/week4/教案.md
2026-04-29 23:45:17 +08:00

445 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第四阶段教案:前端基础 + 项目整合(第 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 交互和文件上传,系统有拦截器保护、统一异常处理、参数校验。这是你第一个真正的里程碑项目。