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 對照
@GetMappingGETdef get(self, request)
@PostMappingPOSTdef post(self, request)
@PutMappingPUTdef put(self, request)
@DeleteMappingDELETEdef delete(self, request)
@PatchMappingPATCHdef 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不能是 nullallow_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/registerPOST註冊
/api/auth/loginPOST登入
/api/auth/googlePOSTGoogle 登入
/api/coursesGET課程列表
/api/courses/{id}GET課程詳情
/api/courses/{id}/enrollPOST報名課程
/api/missions/{id}GET任務詳情
/api/missions/{id}/progressPOST更新進度
/api/missions/{id}/claimPOST領取獎勵
/api/users/meGET當前用戶
/api/leaderboardGET排行榜

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 認證機制。