一、数据访问层(DAO)

Article detail

后端

2026/4/19 · 79 分钟阅读

一、数据访问层(DAO)

本文档包含用户认证与邮件服务的完整代码实现、功能描述以及 Postman API 测试指南。

邮件网页测试用的网页是 https://www.mailinator.com/v4/public/inboxes.jsp?msgid=pingguomiaomiao20-1776578910-012132816333012#

一、数据访问层(DAO)

1.1 VideoRepository - 视频数据仓库接口

路径com.netflix.clone.demo.dao.VideoRepository
功能:提供视频实体的 CRUD 操作基础接口,继承 Spring Data JPA 的 JpaRepository,无需手动实现。

package com.netflix.clone.demo.dao;

import com.netflix.clone.demo.entity.Video;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * 视频数据仓库接口
 * <p>
 *     继承 JpaRepository,自动获得针对 {@link Video} 实体的基本增删改查、
 *     分页、排序等方法。Spring Data JPA 会在运行时自动生成实现代理类。
 * </p>
 * <p>
 *     泛型参数说明:
 *     <ul>
 *         <li>{@code Video}:实体类型</li>
 *         <li>{@code Long}:实体主键 ID 的类型</li>
 *     </ul>
 * </p>
 */
@Repository
public interface VideoRepository extends JpaRepository<Video, Long> {

    // 目前仅使用 JpaRepository 提供的基础方法,暂无自定义查询。
    // 未来可根据业务需求在此添加方法签名,例如:
    // List<Video> findByPublishedTrue();
    // Page<Video> findByCategoriesContaining(String category, Pageable pageable);
}

二、业务服务接口层(Service)

2.1 AuthService - 认证服务接口

路径com.netflix.clone.demo.service.AuthService
功能:定义用户注册、登录、邮箱验证、密码重置等核心认证业务方法契约。

package com.netflix.clone.demo.service;

import com.netflix.clone.demo.dto.request.UserRequest;
import com.netflix.clone.demo.dto.response.EamilValidationResponse;
import com.netflix.clone.demo.dto.response.LoginResponse;
import com.netflix.clone.demo.dto.response.MessageResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public interface AuthService {
    MessageResponse signup(@Valid UserRequest userRequest);

    LoginResponse login(String email, String password);

    EamilValidationResponse validdateEmail(String email);

    MessageResponse verifyEmail(String token);

    MessageResponse resendVerificationEmail(@NotBlank(message = "Email is required")
                                            @Email(message = "Invalid email format") String email);

    MessageResponse forgotPassword(@NotBlank(message = "Email is required")
                                   @Email(message = "Invalid email format") String email);

    MessageResponse resetPassword(@NotBlank String token,
                                  @NotBlank @Size(min = 6, message = "New password must be at lease 6 characters long") String newPassword);

    MessageResponse changePassword(String email, String currentPassword, String newPassword);

    LoginResponse currentUser(String email);
}

2.2 EmailService - 邮件发送服务接口

路径com.netflix.clone.demo.service.EmailService
功能:定义邮件发送相关方法,包括邮箱验证邮件和密码重置邮件。

package com.netflix.clone.demo.service;

public interface EmailService {

    void sendVerificationEmail(String toEmail, String token);

    void sendPasswordResetEmail(String toEmail, String token);
}

三、业务服务实现层(Service.Impl)

3.1 AuthServiceImpl - 认证服务实现类

路径com.netflix.clone.demo.service.Impl.AuthServiceImpl
功能:实现 AuthService 接口,处理用户注册、登录、邮箱验证、密码重置等具体业务逻辑。

package com.netflix.clone.demo.service.Impl;

import com.netflix.clone.demo.dao.UserRepository;
import com.netflix.clone.demo.dto.request.UserRequest;
import com.netflix.clone.demo.dto.response.EamilValidationResponse;
import com.netflix.clone.demo.dto.response.LoginResponse;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.entity.User;
import com.netflix.clone.demo.enums.Role;
import com.netflix.clone.demo.exception.*;
import com.netflix.clone.demo.security.JwtUtil;
import com.netflix.clone.demo.service.AuthService;
import com.netflix.clone.demo.service.EmailService;
import com.netflix.clone.demo.util.ServiceUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.UUID;

@Service
public class AuthServiceImpl implements AuthService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private EmailService emailService;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private ServiceUtils serviceUtils;

    @Override
    public MessageResponse signup(UserRequest userRequest) {
        if(userRepository.existsByEmail(userRequest.getEmail())){
            throw new EmailAlreadyExistsException("Email already exists.");
        }

        User user = new User();
        user.setEmail(userRequest.getEmail());
        user.setPassword(passwordEncoder.encode(userRequest.getPassword()));
        user.setFullName(userRequest.getFullName());
        user.setRole(Role.USER);
        user.setEmailVerified(false);
        String verificationToken = UUID.randomUUID().toString();
        user.setVerificationToken(verificationToken);
        user.setVerificationTokenExpiry(Instant.now().plusSeconds(86400));
        userRepository.save(user);
        emailService.sendVerificationEmail(userRequest.getEmail(), verificationToken);
        return new MessageResponse("Registration successful! Please check your email to verify your account");
    }

    @Override
    public LoginResponse login(String email, String password) {
        User user = userRepository
                .findByEmail(email)
                .filter(u -> passwordEncoder.matches(password, u.getPassword()))
                .orElseThrow(() -> new BadCredentialsException("Invalid email or password"));
        if(!user.isActive()){
            throw new AccountDeactivatedException("You account has been deactivated. Please contact" +
                    "support for assistance");
        }

        if(!user.isEmailVerified()){
            throw new EmailNotVerifiedException(
                    "Please verify your email address before loggin in.Check your inbox for the" +
                            "verification link."
            );
        }

        final String token = jwtUtil.generateToken(user.getEmail(), user.getRole().name());

        return new LoginResponse(token, user.getEmail(), user.getFullName(), user.getRole().name());

    }

    @Override
    public EamilValidationResponse validdateEmail(String email) {
        boolean exists = userRepository.existsByEmail(email);
        return new EamilValidationResponse(exists, !exists);
    }

    @Override
    public MessageResponse verifyEmail(String token) {
        User user=
                userRepository
                        .findByVerificationToken(token)
                        .orElseThrow(() -> new InvalidTokenException("Invalid or expired verification token"));

        if (user.getVerificationTokenExpiry() == null
                || user.getVerificationTokenExpiry().isBefore(Instant.now())){
            throw  new InvalidTokenException("Verification link has expired. Please request a new one.");
        }

        user.setEmailVerified(true);
        user.setVerificationToken(token);
        user.setVerificationTokenExpiry(null);
        userRepository.save(user);
        return new MessageResponse("Email verified successfully! you can now login!");
    }

    @Override
    public MessageResponse resendVerificationEmail(String email) {
        User user = serviceUtils.getUserByEmailOrThrow(email);

        String verificationToken = UUID.randomUUID().toString();
        user.setVerificationToken(verificationToken);
        user.setVerificationTokenExpiry(Instant.now().plusSeconds(86400));
        userRepository.save(user);
        emailService.sendVerificationEmail(email, verificationToken);

        return new MessageResponse("Verification email resent successfully! Please check your inbox.");
    }

    @Override
    public MessageResponse forgotPassword(String email) {
        User user = serviceUtils.getUserByEmailOrThrow(email);
        String resetToken = UUID.randomUUID().toString();
        user.setPasswordResetToken(resetToken);
        user.setPasswordResetTokenExpiry(Instant.now().plusSeconds(3600));
        userRepository.save(user);
        emailService.sendPasswordResetEmail(email, resetToken);

        return new MessageResponse("Password reset email sent successfully! Please check your inbox.");
    }

    @Override
    public MessageResponse resetPassword(String token, String newPassword) {
        User user =
                userRepository
                        .findByPasswordResetToken(token)
                        .orElseThrow(() -> new InvalidTokenException("Invalid or expired reset token"));

        if (user.getPasswordResetTokenExpiry() == null || user.getPasswordResetTokenExpiry().isBefore(Instant.now())){
            throw new InvalidTokenException("Reset token has expired");
        }

        user.setPassword(passwordEncoder.encode(newPassword));
        user.setPasswordResetToken(null);
        user.setPasswordResetTokenExpiry(null);
        userRepository.save(user);

        return new MessageResponse("Password reset successfully. You can now log in with your new password.");
    }

    @Override
    public MessageResponse changePassword(String email, String currentPassword, String newPassword) {
        User user = serviceUtils.getUserByEmailOrThrow(email);

        if(!passwordEncoder.matches(currentPassword, user.getPassword())){
            throw new InvalidCredentialsException("Current password is incorrect");
        }

        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);
        return new MessageResponse("Password changed successfully.");
    }

    @Override
    public LoginResponse currentUser(String email) {
        User user = serviceUtils.getUserByEmailOrThrow(email);
        return new LoginResponse(null, user.getEmail(), user.getFullName(), user.getRole().name());
    }
}

3.2 EmailServiceImpl - 邮件发送服务实现类

路径com.netflix.clone.demo.service.Impl.EmailServiceImpl
功能:实现 EmailService 接口,使用 Spring Mail 发送纯文本邮件,包括邮箱验证邮件和密码重置邮件。

package com.netflix.clone.demo.service.Impl;

import com.netflix.clone.demo.service.EmailService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

@Service
public class EmailServiceImpl implements EmailService {

    private static final Logger logger = LoggerFactory.getLogger(EmailServiceImpl.class);

    @Autowired
    private JavaMailSender mailSender;

    @Value("${app.frontend.url:http://localhost:4200}")
    private String frontendUrl;

    @Value("${spring.mail.username}")
    private String fromEmail;

    @Override
    public void sendVerificationEmail(String toEmail, String token) {
        try {
            SimpleMailMessage message = new SimpleMailMessage();
            message.setFrom(fromEmail);
            message.setTo(toEmail);
            message.setSubject("Netflix Clone - Verify Your Email");

            String verificationLink = frontendUrl + "/verify-email?token=" + token;

            String emailBody =
                    "Welcome to Netflix Clone!\n\n"
                            + "Thank you for registering. Please verify your email address by clicking the link below:\n\n"
                            + verificationLink
                            + "\n\n"
                            + "This link will expire in 24 hours.\n\n"
                            + "If you didn't create this account, please ignore this email.\n\n"
                            + "Best regards,\n"
                            + "Netflix Clone Team";
            message.setText(emailBody);
            mailSender.send(message);
            logger.info("Verification email sent to {}", toEmail);
        } catch (Exception ex) {
            logger.error("Failed to send verification email to {}: {}", toEmail, ex.getMessage(), ex);
            // 【修改1】统一异常处理行为:抛出运行时异常,让调用方感知失败
            throw new RuntimeException("Failed to send verification email", ex);
        }
    }

    @Override
    public void sendPasswordResetEmail(String toEmail, String token) {
        try {
            SimpleMailMessage message = new SimpleMailMessage();
            message.setFrom(fromEmail);
            // 【修改2】添加缺失的收件人设置
            message.setTo(toEmail);
            message.setSubject("Netflix Clone - Password Reset");

            String resetLink = frontendUrl + "/reset-password?token=" + token;

            // 【修改3】修正邮件正文模板,替换为密码重置专用文案
            String emailBody =
                    "Hi,\n\n"
                            + "We received a request to reset the password for your Netflix Clone account.\n\n"
                            + "Please click the link below to set a new password:\n\n"
                            + resetLink
                            + "\n\n"
                            + "This link will expire in 15 minutes.\n\n"
                            + "If you did not request a password reset, please ignore this email. "
                            + "Your account security will not be affected.\n\n"
                            + "Best regards,\n"
                            + "Netflix Clone Team";
            message.setText(emailBody);
            mailSender.send(message);
            logger.info("Password reset email sent to {}", toEmail);

        } catch (Exception ex) {
            logger.error("Failed to send password reset email to {} : {}", toEmail, ex.getMessage(), ex);
            throw new RuntimeException("Failed to send password reset email", ex);
        }
    }
}

四、控制器层(Controller)

4.1 AuthController - 认证控制器

路径com.netflix.clone.demo.controller.AuthController
功能:暴露 RESTful API,处理来自前端的认证相关 HTTP 请求,包括注册、登录、邮箱验证、密码重置等。

package com.netflix.clone.demo.controller;

import com.netflix.clone.demo.dto.request.*;
import com.netflix.clone.demo.dto.response.EamilValidationResponse;
import com.netflix.clone.demo.dto.response.LoginResponse;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthControler {
    @Autowired
    private AuthService authService;

    @PostMapping("/signup")
    public ResponseEntity<MessageResponse> signup(@Valid @RequestBody UserRequest userRequest){
        return ResponseEntity.ok(authService.signup(userRequest));
    }

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest loginRequest){
        LoginResponse response = authService.login(loginRequest.getEmail(), loginRequest.getPassword());
        return ResponseEntity.ok(response);
    }

    @GetMapping("/validate-email")
    public ResponseEntity<EamilValidationResponse> validateEmail(@RequestParam String email){
        return ResponseEntity.ok(authService.validdateEmail(email));
    }

    @GetMapping("/verify-email")
    public ResponseEntity<MessageResponse> verifyEmail(@RequestParam String token){
        return ResponseEntity.ok(authService.verifyEmail(token));
    }

    @PostMapping("/resend-verification")
    public ResponseEntity<MessageResponse> resendVerification(@Valid @RequestBody EmailRequest emailRequest){
        return ResponseEntity.ok(authService.resendVerificationEmail(emailRequest.getEmail()));
    }

    @PostMapping("/forget-password")
    public ResponseEntity<MessageResponse> forgotPassword(
            @Valid @RequestBody EmailRequest emailRequest
    ){
        return ResponseEntity.ok(authService.forgotPassword(emailRequest.getEmail()));
    }

    @PostMapping("/reset-password")
    public ResponseEntity<MessageResponse> resetPassword(
            @Valid @RequestBody ResetPasswordRequest resetPasswordRequest
            ){
        return ResponseEntity.ok(
                authService.resetPassword(
                        resetPasswordRequest.getToken(), resetPasswordRequest.getNewPassword()
                )
        );
    }

    @PostMapping("/change-password")
    public ResponseEntity<?> changePassword(
            Authentication authentication,
            @Valid @RequestBody ChangePasswordRequest changePasswordRequest
            ){
        String email = authentication.getName();

        return ResponseEntity.ok(
                authService.changePassword(
                        email,
                        changePasswordRequest.getCurrentPassword(),
                        changePasswordRequest.getNewPassword()
                )
        );
    }

    @GetMapping("/current-user")
    public ResponseEntity<LoginResponse> currentUser(
            Authentication authentication
    ){
        String email = authentication.getName();
        return ResponseEntity.ok(authService.currentUser(email));
    }
}

五、Postman API 测试指南

5.1 基础配置

  • Base URLhttp://localhost:8080
  • Content-Typeapplication/json

5.2 接口测试详情

① 用户注册 – POST /api/auth/signup

请求体

{
    "email": "test@mailinator.com",
    "password": "123456",
    "fullName": "Test User"
}

预期响应

{
    "message": "Registration successful! Please check your email to verify your account"
}

② 邮箱验证 – GET /api/auth/verify-email?token=<verification_token>

示例 URL

http://localhost:8080/api/auth/verify-email?token=681103c3-3385-4b05-b5c3-a540e124a888

预期响应

{
    "message": "Email verified successfully! you can now login!"
}

③ 用户登录 – POST /api/auth/login

请求体

{
    "email": "test@mailinator.com",
    "password": "123456"
}

预期响应

{
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "email": "test@mailinator.com",
    "fullName": "Test User",
    "role": "USER"
}

④ 检查邮箱是否已注册 – GET /api/auth/validate-email?email=test@mailinator.com

预期响应

{
    "exists": true,
    "available": false
}

⑤ 重发验证邮件 – POST /api/auth/resend-verification

请求体

{
    "email": "test@mailinator.com"
}

⑥ 忘记密码 – POST /api/auth/forget-password

请求体

{
    "email": "test@mailinator.com"
}

⑦ 重置密码 – POST /api/auth/reset-password

请求体

{
    "token": "从邮件中提取的 password_reset_token",
    "newPassword": "newPassword123"
}

⑧ 修改密码(需认证) – POST /api/auth/change-password

HeadersAuthorization: Bearer <JWT Token>

请求体

{
    "currentPassword": "123456",
    "newPassword": "newSecurePassword"
}

⑨ 获取当前用户信息(需认证) – GET /api/auth/current-user

HeadersAuthorization: Bearer <JWT Token>

预期响应

{
    "token": null,
    "email": "test@mailinator.com",
    "fullName": "Test User",
    "role": "USER"
}

5.3 Postman 环境变量建议

变量名示例值
baseUrlhttp://localhost:8080
token登录成功后获取的 JWT

在请求 URL 中使用 {{baseUrl}}/api/auth/...,在 Authorization 标签页选择 Bearer Token 并填入 {{token}}


六、注意事项

  1. 密码加密:当前 SecurityConfig 使用了 NoOpPasswordEncoder,密码以明文存储,仅适用于开发测试。生产环境必须更换为 BCryptPasswordEncoder
  2. 邮件服务:需在 application.properties 中正确配置 spring.mail.* 参数,推荐使用 QQ 邮箱或 Gmail 应用专用密码。
  3. Token 有效期:邮箱验证 Token 有效期为 24 小时,密码重置 Token 有效期为 1 小时。
  4. 数据库检查:可通过 MySQL 命令行查看 users 表,确认 email_verifiedverification_token 等字段的更新情况。

评论

动作测试