Controller 層詳解#
Controller 層負責定義 REST API 端點,接收 HTTP 請求並回傳響應。等同於 Django 的 views.py + urls.py。
檔案位置#
backend/src/main/java/tw/waterballsa/academy/controller/
├── AuthController.java # 認證 API
├── UserController.java # 用戶 API
├── CourseController.java # 課程 API
├── EnrollmentController.java # 報名 API
├── MissionController.java # 任務 API
├── LeaderboardController.java # 排行榜 API
├── GymController.java # 道場 API
├── ChallengeController.java # 挑戰 API
├── GradeController.java # 評分 API
└── AdminController.java # 管理員 API核心概念#
1. 基本 Controller 結構#
@RestController // 標記為 REST Controller(回傳 JSON)
@RequestMapping("/api/auth") // 基礎路徑
@RequiredArgsConstructor // Lombok: 自動注入依賴
public class AuthController {
private final AuthService authService; // 注入 Service
@PostMapping("/register") // POST /api/auth/register
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}
@PostMapping("/login") // POST /api/auth/login
public ResponseEntity<AuthResponse> login(@Valid @RequestBody AuthRequest request) {
return ResponseEntity.ok(authService.login(request));
}
}Django 對照:
# urls.py
urlpatterns = [
path('api/auth/register', RegisterView.as_view()),
path('api/auth/login', LoginView.as_view()),
]
# views.py
class RegisterView(APIView):
def post(self, request):
serializer = RegisterSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
result = AuthService().register(serializer.validated_data)
return Response(result)2. HTTP 方法註解#
| 註解 | HTTP 方法 | Django 對照 |
|---|---|---|
@GetMapping | GET | def get(self, request) |
@PostMapping | POST | def post(self, request) |
@PutMapping | PUT | def put(self, request) |
@DeleteMapping | DELETE | def delete(self, request) |
@PatchMapping | PATCH | def patch(self, request) |
@RestController
@RequestMapping("/api/courses")
public class CourseController {
@GetMapping // GET /api/courses
public List<Course> getAllCourses() { ... }
@GetMapping("/{id}") // GET /api/courses/123
public Course getCourse(@PathVariable Long id) { ... }
@PostMapping // POST /api/courses
public Course createCourse(@RequestBody CourseDto dto) { ... }
@PutMapping("/{id}") // PUT /api/courses/123
public Course updateCourse(@PathVariable Long id, @RequestBody CourseDto dto) { ... }
@DeleteMapping("/{id}") // DELETE /api/courses/123
public void deleteCourse(@PathVariable Long id) { ... }
}參數接收#
@PathVariable - 路徑參數#
// GET /api/courses/123
@GetMapping("/{courseId}")
public ResponseEntity<CourseDetailDto> getCourseDetail(
@PathVariable Long courseId) { // courseId = 123
return ResponseEntity.ok(courseService.getCourseDetail(courseId));
}
// GET /api/courses/123/chapters/456
@GetMapping("/{courseId}/chapters/{chapterId}")
public ResponseEntity<Chapter> getChapter(
@PathVariable Long courseId,
@PathVariable Long chapterId) {
// ...
}Django 對照:
# urls.py
path('api/courses/<int:course_id>/', CourseDetailView.as_view())
# views.py
class CourseDetailView(APIView):
def get(self, request, course_id): # course_id 來自 URL
pass@RequestBody - 請求體 (JSON)#
// POST /api/auth/register
// Body: {"email": "test@example.com", "password": "123456", "name": "Test"}
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(
@Valid @RequestBody RegisterRequest request) {
// request.getEmail() = "test@example.com"
// request.getPassword() = "123456"
// request.getName() = "Test"
return ResponseEntity.ok(authService.register(request));
}Django 對照:
class RegisterView(APIView):
def post(self, request):
email = request.data.get('email')
password = request.data.get('password')
name = request.data.get('name')@RequestParam - 查詢參數#
// GET /api/courses?page=1&size=10&category=programming
@GetMapping
public ResponseEntity<Page<Course>> getCourses(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String category) {
// page = 1, size = 10, category = "programming"
return ResponseEntity.ok(courseService.getCourses(page, size, category));
}Django 對照:
def get(self, request):
page = request.query_params.get('page', 0)
size = request.query_params.get('size', 10)
category = request.query_params.get('category')Authentication - 取得當前用戶#
@GetMapping("/me")
public ResponseEntity<UserDto> getCurrentUser(Authentication authentication) {
// authentication.getName() 回傳 JWT 中的 subject(通常是 email)
String email = authentication.getName();
// 查詢用戶
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found"));
return ResponseEntity.ok(convertToDto(user));
}
// 封裝成共用方法
private Long getUserId(Authentication authentication) {
String email = authentication.getName();
return userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found"))
.getId();
}Django 對照:
from rest_framework.permissions import IsAuthenticated
class CurrentUserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user # Django 自動注入
return Response(UserSerializer(user).data)DTO (Data Transfer Object)#
Request DTO - 接收請求資料#
// dto/RegisterRequest.java
@Data // Lombok: 自動產生 getter/setter/toString/equals/hashCode
public class RegisterRequest {
@Email // 驗證: 必須是 email 格式
@NotBlank // 驗證: 不能是空白
private String email;
@NotBlank
@Size(min = 6) // 驗證: 最少 6 字元
private String password;
@NotBlank
private String name;
}Django 對照:
class RegisterSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(min_length=6)
name = serializers.CharField()Response DTO - 回傳響應資料#
// dto/AuthResponse.java
@Data
@AllArgsConstructor
public class AuthResponse {
private String token;
private Long userId;
private String name;
private String email;
private Integer level;
private Integer exp;
private String role;
private String avatarUrl;
}驗證註解#
| 註解 | 說明 | Django 對照 |
|---|---|---|
@NotBlank | 不能是空白 | required=True |
@NotNull | 不能是 null | allow_null=False |
@Email | 必須是 email 格式 | EmailField() |
@Size(min=6, max=20) | 長度限制 | CharField(min_length=6, max_length=20) |
@Min(0) | 最小值 | IntegerField(min_value=0) |
@Max(100) | 最大值 | IntegerField(max_value=100) |
@Pattern(regexp="...") | 正則表達式 | RegexValidator |
public class UpdateProgressRequest {
@Min(0)
private Integer watchedPosition;
@Min(1)
private Integer duration;
@Min(0)
private Integer watchedTime;
}響應處理#
ResponseEntity#
ResponseEntity 讓你控制 HTTP 狀態碼和響應頭:
// 200 OK
return ResponseEntity.ok(data);
// 201 Created
return ResponseEntity.status(HttpStatus.CREATED).body(data);
// 204 No Content
return ResponseEntity.noContent().build();
// 404 Not Found
return ResponseEntity.notFound().build();
// 400 Bad Request
return ResponseEntity.badRequest().body(errorMessage);
// 自訂狀態碼
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(data);直接回傳物件#
如果只需要 200 OK,可以直接回傳物件:
@GetMapping
public List<Course> getAllCourses() { // 自動包成 200 OK
return courseService.getAllCourses();
}實際範例分析#
AuthController - 認證 API#
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
// POST /api/auth/register
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}
// POST /api/auth/login
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody AuthRequest request) {
return ResponseEntity.ok(authService.login(request));
}
// POST /api/auth/google (Google OAuth)
@PostMapping("/google")
public ResponseEntity<AuthResponse> googleLogin(@Valid @RequestBody GoogleLoginRequest request) {
return ResponseEntity.ok(authService.googleLogin(request.getIdToken()));
}
}CourseController - 課程 API#
@RestController
@RequestMapping("/api/courses")
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
private final EnrollmentService enrollmentService;
private final UserRepository userRepository;
// GET /api/courses - 取得所有課程
@GetMapping
public ResponseEntity<List<CourseListDto>> getAllCourses(Authentication authentication) {
// authentication 可能是 null(未登入用戶)
Long userId = authentication != null ? getUserId(authentication) : null;
return ResponseEntity.ok(courseService.getAllCourses(userId));
}
// GET /api/courses/{courseId} - 取得課程詳情
@GetMapping("/{courseId}")
public ResponseEntity<CourseDetailDto> getCourseDetail(
@PathVariable Long courseId,
Authentication authentication) {
Long userId = authentication != null ? getUserId(authentication) : null;
return ResponseEntity.ok(courseService.getCourseDetail(courseId, userId));
}
// POST /api/courses/{courseId}/enroll - 報名課程
@PostMapping("/{courseId}/enroll")
public ResponseEntity<EnrollmentDto> enrollCourse(
@PathVariable Long courseId,
@Valid @RequestBody EnrollCourseRequest request,
Authentication authentication) {
Long userId = getUserId(authentication); // 必須登入
return ResponseEntity.ok(enrollmentService.enrollCourse(userId, courseId, request.getPaymentMethod()));
}
// GET /api/courses/{courseId}/enrollment-status - 檢查是否已報名
@GetMapping("/{courseId}/enrollment-status")
public ResponseEntity<Map<String, Boolean>> checkEnrollmentStatus(
@PathVariable Long courseId,
Authentication authentication) {
Long userId = getUserId(authentication);
boolean isEnrolled = enrollmentService.isEnrolled(userId, courseId);
return ResponseEntity.ok(Map.of("isEnrolled", isEnrolled));
}
private Long getUserId(Authentication authentication) {
String email = authentication.getName();
return userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found"))
.getId();
}
}MissionController - 任務 API#
@RestController
@RequestMapping("/api/missions")
@RequiredArgsConstructor
public class MissionController {
private final MissionService missionService;
private final UserRepository userRepository;
// GET /api/missions/{missionId} - 取得任務詳情
@GetMapping("/{missionId}")
public ResponseEntity<MissionDetailDto> getMissionDetail(
@PathVariable Long missionId,
Authentication authentication) {
Long userId = authentication != null ? getUserId(authentication) : null;
MissionDetailDto mission = missionService.getMissionDetail(missionId, userId);
return ResponseEntity.ok(mission);
}
// POST /api/missions/{missionId}/progress - 更新觀看進度
@PostMapping("/{missionId}/progress")
public ResponseEntity<MissionProgressDto> updateProgress(
@PathVariable Long missionId,
@Valid @RequestBody UpdateProgressRequest request,
Authentication authentication) {
Long userId = getUserId(authentication);
MissionProgressDto progress = missionService.updateProgress(
userId,
missionId,
request.getWatchedPosition(),
request.getDuration(),
request.getWatchedTime()
);
return ResponseEntity.ok(progress);
}
// POST /api/missions/{missionId}/claim - 領取獎勵
@PostMapping("/{missionId}/claim")
public ResponseEntity<ClaimRewardResponse> claimReward(
@PathVariable Long missionId,
Authentication authentication) {
Long userId = getUserId(authentication);
ClaimRewardResponse response = missionService.claimReward(userId, missionId);
return ResponseEntity.ok(response);
}
private Long getUserId(Authentication authentication) {
String email = authentication.getName();
return userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found"))
.getId();
}
}API 端點一覽#
| 端點 | 方法 | 說明 | 需登入 |
|---|---|---|---|
/api/auth/register | POST | 註冊 | ❌ |
/api/auth/login | POST | 登入 | ❌ |
/api/auth/google | POST | Google 登入 | ❌ |
/api/courses | GET | 課程列表 | ❌ |
/api/courses/{id} | GET | 課程詳情 | ❌ |
/api/courses/{id}/enroll | POST | 報名課程 | ✅ |
/api/missions/{id} | GET | 任務詳情 | ❌ |
/api/missions/{id}/progress | POST | 更新進度 | ✅ |
/api/missions/{id}/claim | POST | 領取獎勵 | ✅ |
/api/users/me | GET | 當前用戶 | ✅ |
/api/leaderboard | GET | 排行榜 | ❌ |
Controller 設計原則#
1. Controller 只做轉發#
Controller 不應該包含業務邏輯,只做:
- 接收請求參數
- 呼叫 Service
- 回傳響應
// 好的寫法
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request)); // 只呼叫 Service
}
// 不好的寫法(業務邏輯寫在 Controller)
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
if (userRepository.existsByEmail(request.getEmail())) { // ❌ 業務邏輯不應該在這
throw new RuntimeException("Email exists");
}
User user = User.builder()...build(); // ❌ 應該在 Service 裡
userRepository.save(user);
// ...
}2. 統一使用 ResponseEntity#
便於統一處理響應格式和狀態碼。
3. 使用 @Valid 驗證請求#
自動驗證 DTO 欄位,減少手動檢查。
常見問題#
Q: @RestController vs @Controller?#
@RestController=@Controller+@ResponseBody@RestController預設回傳 JSON@Controller用於回傳 HTML 頁面(MVC)
Q: 為什麼有些 API 可以不登入存取?#
在 SecurityConfig 中設定的公開端點。詳見 Security 配置。
Q: Authentication 什麼時候是 null?#
當使用者未帶 JWT Token,或請求的端點是公開的(permitAll)。
下一步#
理解 Controller 後,接下來看 Security 配置 了解 JWT 認證機制。