AuthApplicationService.java

package com.yumu.noveltranslator.application.service;

import com.yumu.noveltranslator.port.dto.common.Result;
import com.yumu.noveltranslator.port.dto.auth.LoginRequest;
import com.yumu.noveltranslator.port.dto.auth.RegisterRequest;
import com.yumu.noveltranslator.port.dto.auth.RefreshTokenRequest;
import com.yumu.noveltranslator.port.dto.auth.ChangePasswordRequest;
import com.yumu.noveltranslator.port.dto.auth.ResetPasswordRequest;
import com.yumu.noveltranslator.domain.model.Tenant;
import com.yumu.noveltranslator.domain.model.User;
import com.yumu.noveltranslator.enums.ErrorCodeEnum;
import com.yumu.noveltranslator.port.out.EmailPort;
import com.yumu.noveltranslator.port.out.UserRepositoryPort;
import com.yumu.noveltranslator.port.out.UserDetailsFactoryPort;
import com.yumu.noveltranslator.util.JwtUtils;
import com.yumu.noveltranslator.util.PasswordUtil;
import com.yumu.noveltranslator.domain.service.VerificationCodeService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

/**
 * 用户认证应用服务:登录、注册、验证码、密码管理
 */
@Service
@RequiredArgsConstructor
public class AuthApplicationService implements UserDetailsService, com.yumu.noveltranslator.port.in.AuthPort {

    private final UserRepositoryPort userRepositoryPort;
    private final JwtUtils jwtUtils;
    private final EmailPort emailPort;
    private final UserDetailsFactoryPort userDetailsFactoryPort;
    private final VerificationCodeService verificationCodeService;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepositoryPort.findByEmail(email).orElse(null);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在:" + email);
        }
        return userDetailsFactoryPort.createUserDetails(user);
    }

    public Result<User> login(LoginRequest req) {
        if (req.getEmail() == null || !isValidEmail(req.getEmail().trim())) {
            return Result.error(ErrorCodeEnum.USER_EMAIL_INVALID.getCode(),
                              ErrorCodeEnum.USER_EMAIL_INVALID.getMessage());
        }
        if (req.getPassword() == null || req.getPassword().trim().isEmpty()) {
            return Result.error(ErrorCodeEnum.PARAMETER_ERROR.getCode(),
                              "密码不能为空");
        }

        try {
            User user = userRepositoryPort.findByEmail(req.getEmail().trim()).orElse(null);

            if (user != null) {
                if (PasswordUtil.verifyPassword(req.getPassword(), user.getPassword())) {
                    String token = jwtUtils.createToken(user.getId(), user.getEmail(), user.getTenantId());

                    User userInfo = new User();
                    userInfo.setId(user.getId());
                    userInfo.setEmail(user.getEmail());
                    userInfo.setUsername(user.getUsername());
                    userInfo.setAvatar(user.getAvatar());
                    userInfo.setUserLevel(user.getUserLevel());
                    userInfo.setCreateTime(user.getCreateTime());

                    return Result.okWithToken(userInfo, token);
                } else {
                    return Result.error(ErrorCodeEnum.USER_PASSWORD_ERROR.getCode(),
                                      ErrorCodeEnum.USER_PASSWORD_ERROR.getMessage());
                }
            } else {
                return Result.error(ErrorCodeEnum.USER_NOT_FOUND.getCode(),
                                  ErrorCodeEnum.USER_NOT_FOUND.getMessage());
            }
        } catch (Exception e) {
            return Result.error(ErrorCodeEnum.SYSTEM_ERROR.getCode(),
                              "登录失败:" + e.getMessage());
        }
    }

    /**
     * 发送注册验证码
     */
    public Result sendVerificationCode(String email) {
        if (email == null || !isValidEmail(email.trim())) {
            return Result.error(ErrorCodeEnum.USER_EMAIL_INVALID.getCode(),
                              ErrorCodeEnum.USER_EMAIL_INVALID.getMessage());
        }

        User existingUser = userRepositoryPort.findByEmail(email.trim()).orElse(null);
        if (existingUser != null) {
            return Result.error(ErrorCodeEnum.USER_EMAIL_EXISTS.getCode(),
                              ErrorCodeEnum.USER_EMAIL_EXISTS.getMessage());
        }

        String code = verificationCodeService.generateAndStore(email.trim());
        emailPort.sendVerificationCode(email.trim(), code);
        return Result.ok(null, ErrorCodeEnum.SUCCESS.getCode());
    }

    /**
     * 发送重置密码验证码
     */
    public Result sendResetCode(String email) {
        if (email == null || !isValidEmail(email.trim())) {
            return Result.error(ErrorCodeEnum.USER_EMAIL_INVALID.getCode(),
                              ErrorCodeEnum.USER_EMAIL_INVALID.getMessage());
        }

        User existingUser = userRepositoryPort.findByEmail(email.trim()).orElse(null);
        if (existingUser == null) {
            return Result.error(ErrorCodeEnum.USER_NOT_FOUND.getCode(),
                              ErrorCodeEnum.USER_NOT_FOUND.getMessage());
        }

        String code = verificationCodeService.generateAndStore(email.trim());
        emailPort.sendVerificationCode(email.trim(), code);
        return Result.ok(null, ErrorCodeEnum.SUCCESS.getCode());
    }

    @Transactional
    public Result<User> register(RegisterRequest req) {
        String email = req.getEmail();
        String code = req.getCode();
        String password = req.getPassword();

        if (email == null || !isValidEmail(email.trim())) {
            return Result.error(ErrorCodeEnum.USER_EMAIL_INVALID.getCode(),
                              ErrorCodeEnum.USER_EMAIL_INVALID.getMessage());
        }
        if (code == null || code.trim().isEmpty()) {
            return Result.error(ErrorCodeEnum.PARAMETER_ERROR.getCode(),
                              "验证码不能为空");
        }
        if (password == null || password.length() < 6) {
            return Result.error(ErrorCodeEnum.USER_PASSWORD_TOO_SHORT.getCode(),
                              ErrorCodeEnum.USER_PASSWORD_TOO_SHORT.getMessage());
        }

        if (!verificationCodeService.verifyCode(email.trim(), code)) {
            return Result.error(ErrorCodeEnum.USER_VERIFICATION_CODE_ERROR.getCode(),
                              ErrorCodeEnum.USER_VERIFICATION_CODE_ERROR.getMessage());
        }

        User existingUser = userRepositoryPort.findByEmail(email.trim()).orElse(null);
        if (existingUser != null) {
            return Result.error(ErrorCodeEnum.USER_EMAIL_EXISTS.getCode(),
                              ErrorCodeEnum.USER_EMAIL_EXISTS.getMessage());
        }

        User newUser = new User();
        newUser.setEmail(email.trim());
        newUser.setPassword(PasswordUtil.hashPassword(password));
        newUser.setUserLevel("free");
        newUser.setUsername(req.getUsername());
        newUser.setAvatar(req.getAvatar());

        try {
            // 创建租户
            Tenant tenant = new Tenant();
            tenant.setName(req.getUsername() != null ? req.getUsername() + " 的租户" : "Tenant-" + System.currentTimeMillis());
            tenant.setStatus("ACTIVE");
            tenant.setMaxUsers(1);
            userRepositoryPort.saveTenant(tenant);

            newUser.setTenantId(tenant.getId());

            userRepositoryPort.save(newUser);

            if (newUser.getId() != null) {
                User registeredUser = new User();
                registeredUser.setId(newUser.getId());
                registeredUser.setEmail(newUser.getEmail());
                registeredUser.setUsername(newUser.getUsername());
                registeredUser.setAvatar(newUser.getAvatar());
                registeredUser.setUserLevel(newUser.getUserLevel());
                registeredUser.setCreateTime(newUser.getCreateTime());

                return Result.ok(registeredUser, ErrorCodeEnum.SUCCESS.getCode());
            } else {
                return Result.error(ErrorCodeEnum.SYSTEM_ERROR.getCode(),
                                  "注册失败");
            }
        } catch (Exception e) {
            return Result.error(ErrorCodeEnum.SYSTEM_ERROR.getCode(),
                              "注册过程中发生错误:" + e.getMessage());
        }
    }

    /**
     * 刷新令牌
     */
    public Result refreshToken(RefreshTokenRequest request) {
        if (request.getRefreshToken() == null || request.getRefreshToken().trim().isEmpty()) {
            return Result.error(ErrorCodeEnum.PARAMETER_ERROR.getCode(), "刷新令牌不能为空");
        }

        try {
            var decoded = jwtUtils.verifyToken(request.getRefreshToken());
            Long userId = decoded.getClaim("userId").asLong();
            String email = decoded.getClaim("email").asString();
            Long tenantId = decoded.getClaim("tenantId").asLong();

            // Fallback: old tokens without tenantId
            if (tenantId == null) {
                User user = userRepositoryPort.findByEmail(email).orElse(null);
                if (user != null) {
                    tenantId = user.getTenantId();
                }
            }

            String newToken = jwtUtils.createToken(userId, email, tenantId);

            return Result.okWithToken(null, newToken);
        } catch (Exception e) {
            return Result.error(ErrorCodeEnum.TOKEN_INVALID.getCode(), "刷新令牌无效或已过期");
        }
    }

    /**
     * 修改密码
     */
    public Result changePassword(Long userId, ChangePasswordRequest request) {
        if (request.getOldPassword() == null || request.getNewPassword() == null) {
            return Result.error(ErrorCodeEnum.PARAMETER_ERROR.getCode(), "密码不能为空");
        }

        if (request.getNewPassword().length() < 6) {
            return Result.error(ErrorCodeEnum.USER_PASSWORD_TOO_SHORT.getCode(),
                    ErrorCodeEnum.USER_PASSWORD_TOO_SHORT.getMessage());
        }

        try {
            User user = userRepositoryPort.findById(userId).orElse(null);
            if (user == null) {
                return Result.error(ErrorCodeEnum.USER_NOT_FOUND.getCode(), "用户不存在");
            }

            if (!PasswordUtil.verifyPassword(request.getOldPassword(), user.getPassword())) {
                return Result.error(ErrorCodeEnum.USER_PASSWORD_ERROR.getCode(), "原密码错误");
            }

            user.setPassword(PasswordUtil.hashPassword(request.getNewPassword()));
            userRepositoryPort.update(user);

            return Result.ok(null, ErrorCodeEnum.SUCCESS.getCode());
        } catch (Exception e) {
            return Result.error(ErrorCodeEnum.SYSTEM_ERROR.getCode(), "修改密码失败:" + e.getMessage());
        }
    }

    /**
     * 重置密码
     */
    @Transactional
    public Result resetPassword(ResetPasswordRequest request) {
        if (request.getEmail() == null || !isValidEmail(request.getEmail().trim())) {
            return Result.error(ErrorCodeEnum.USER_EMAIL_INVALID.getCode(),
                    ErrorCodeEnum.USER_EMAIL_INVALID.getMessage());
        }

        if (request.getCode() == null || request.getCode().trim().isEmpty()) {
            return Result.error(ErrorCodeEnum.PARAMETER_ERROR.getCode(), "验证码不能为空");
        }

        if (request.getNewPassword() == null || request.getNewPassword().length() < 6) {
            return Result.error(ErrorCodeEnum.USER_PASSWORD_TOO_SHORT.getCode(),
                    ErrorCodeEnum.USER_PASSWORD_TOO_SHORT.getMessage());
        }

        if (!verificationCodeService.verifyCode(request.getEmail().trim(), request.getCode())) {
            return Result.error(ErrorCodeEnum.USER_VERIFICATION_CODE_ERROR.getCode(),
                    ErrorCodeEnum.USER_VERIFICATION_CODE_ERROR.getMessage());
        }

        try {
            User user = userRepositoryPort.findByEmail(request.getEmail().trim()).orElse(null);
            if (user == null) {
                return Result.error(ErrorCodeEnum.USER_NOT_FOUND.getCode(), "用户不存在");
            }

            user.setPassword(PasswordUtil.hashPassword(request.getNewPassword()));
            userRepositoryPort.update(user);

            return Result.ok(null, ErrorCodeEnum.SUCCESS.getCode());
        } catch (Exception e) {
            return Result.error(ErrorCodeEnum.SYSTEM_ERROR.getCode(), "重置密码失败:" + e.getMessage());
        }
    }

    /**
     * 退出登录
     */
    @Transactional
    public Result logout(Long userId, String refreshToken, String jwt) {
        if (refreshToken != null) {
            try {
                // 清理 device token
                userRepositoryPort.removeDeviceToken(refreshToken);
            } catch (Exception e) {
                // 忽略 token 解析失败
            }
        }

        // 将 JWT 加入黑名单,使其立即失效
        if (jwt != null && !jwt.isEmpty()) {
            try {
                String email = jwtUtils.getEmailFromToken(jwt);
                LocalDateTime expiresAt = jwtUtils.getExpiresAtFromToken(jwt);
                userRepositoryPort.blacklistToken(jwt, email, "logout", expiresAt);
            } catch (Exception e) {
                // 忽略 token 解析失败
            }
        }

        return Result.ok(null, ErrorCodeEnum.SUCCESS.getCode());
    }

    /**
     * 验证邮箱格式
     */
    private boolean isValidEmail(String email) {
        if (!StringUtils.hasText(email)) {
            return false;
        }
        String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" +
                           "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
        return Pattern.matches(emailRegex, email);
    }

    @Override
    public Optional<User> getUserById(Long userId) {
        return userRepositoryPort.findById(userId);
    }
}