VerificationCodeService.java

package com.yumu.noveltranslator.domain.service;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * Verification code generation, storage, and verification domain service.
 * Extracted from EmailVerificationCodeUtil to separate concerns:
 * this handles code lifecycle, while EmailPort handles email delivery.
 */
@Service
@Slf4j
public class VerificationCodeService {

    private Cache<String, String> verificationCodeCache;
    private Cache<String, Long> lastSendTimeCache;

    @Value("${email.verification.code.validity:1}")
    private long validity;

    @Value("${email.verification.code.length:6}")
    private int codeLength;

    private static final Random RANDOM = new Random();

    @PostConstruct
    public void init() {
        verificationCodeCache = Caffeine.newBuilder()
                .expireAfterWrite(validity, TimeUnit.MINUTES)
                .maximumSize(10000)
                .build();
        lastSendTimeCache = Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .maximumSize(10000)
                .build();
    }

    /**
     * Generate a random numeric code and store it in cache.
     * @return the generated code
     */
    public String generateAndStore(String email) {
        String code = generateCode();
        verificationCodeCache.put(email + ":" + code, code);
        lastSendTimeCache.put(email, System.currentTimeMillis());
        log.info("验证码已生成: {}", email);
        return code;
    }

    /**
     * Verify the code for the given email.
     * @return true if valid and not expired
     */
    public boolean verifyCode(String email, String code) {
        String cachedCode = verificationCodeCache.getIfPresent(email + ":" + code);
        if (cachedCode == null) {
            log.warn("验证码已过期或不存在,邮箱: {}", email);
            return false;
        }
        verificationCodeCache.invalidate(email + ":" + code);
        log.info("邮箱验证成功: {}", email);
        return true;
    }

    /**
     * Check if a code was sent recently (within 60 seconds).
     * @return true if rate-limited
     */
    public boolean isRateLimited(String email) {
        Long lastSendTime = lastSendTimeCache.getIfPresent(email);
        if (lastSendTime != null) {
            long elapsed = System.currentTimeMillis() - lastSendTime;
            return elapsed < 60000;
        }
        return false;
    }

    public Long getLastSendTime(String email) {
        return lastSendTimeCache.getIfPresent(email);
    }

    private String generateCode() {
        StringBuilder code = new StringBuilder();
        for (int i = 0; i < codeLength; i++) {
            code.append(RANDOM.nextInt(10));
        }
        return code.toString();
    }
}