package com.sinata.appointment.service; import com.sinata.appointment.constants.BizErrorCode; import com.sinata.appointment.dto.CaptchaResponse; import com.sinata.appointment.exception.BizException; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.security.SecureRandom; import java.time.Duration; import java.util.Base64; import java.util.UUID; import javax.imageio.ImageIO; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; /** 管理端登录图形验证码:Redis 存储,一次性校验。 */ @Service @RequiredArgsConstructor public class CaptchaService { private static final String KEY_PREFIX = "auth:captcha:"; private static final Duration TTL = Duration.ofMinutes(2); /** 易混淆字符已排除:0/O、1/l/I */ private static final String CHAR_POOL = "23456789ABCDEFGHJKMNPQRSTUVWXYZ"; private static final int CODE_LEN = 4; private static final int W = 112; private static final int H = 40; private final StringRedisTemplate redisTemplate; private final SecureRandom random = new SecureRandom(); public CaptchaResponse generate() { String code = randomCode(); String captchaId = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(KEY_PREFIX + captchaId, code.toLowerCase(), TTL); String imageBase64 = renderPngBase64(code); return CaptchaResponse.builder().captchaId(captchaId).imageBase64(imageBase64).build(); } public void validateAndConsume(String captchaId, String captchaCode) { if (captchaId == null || captchaId.isBlank() || captchaCode == null || captchaCode.isBlank()) { throw new BizException(BizErrorCode.AUTH_CAPTCHA_INVALID, "请填写验证码"); } String key = KEY_PREFIX + captchaId; String expected = redisTemplate.opsForValue().get(key); if (expected == null) { throw new BizException(BizErrorCode.AUTH_CAPTCHA_INVALID, "验证码已失效,请刷新"); } redisTemplate.delete(key); if (!expected.equalsIgnoreCase(captchaCode.trim())) { throw new BizException(BizErrorCode.AUTH_CAPTCHA_INVALID, "验证码错误"); } } private String randomCode() { StringBuilder sb = new StringBuilder(CODE_LEN); for (int i = 0; i < CODE_LEN; i++) { sb.append(CHAR_POOL.charAt(random.nextInt(CHAR_POOL.length()))); } return sb.toString(); } private String renderPngBase64(String code) { BufferedImage image = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); g.setColor(new Color(245, 246, 250)); g.fillRect(0, 0, W, H); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (int i = 0; i < 6; i++) { g.setColor(new Color(random.nextInt(180), random.nextInt(180), random.nextInt(180), 80)); g.setStroke(new BasicStroke(1f)); g.drawLine(random.nextInt(W), random.nextInt(H), random.nextInt(W), random.nextInt(H)); } g.setFont(new Font("SansSerif", Font.BOLD, 28)); for (int i = 0; i < code.length(); i++) { g.setColor(new Color(random.nextInt(80), random.nextInt(80), random.nextInt(80))); int x = 18 + i * 22; int y = 28 + random.nextInt(5) - 2; g.drawString(String.valueOf(code.charAt(i)), x, y); } g.dispose(); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(image, "png", out); return Base64.getEncoder().encodeToString(out.toByteArray()); } catch (Exception e) { throw new IllegalStateException("生成验证码图片失败", e); } } }