diff --git a/pom.xml b/pom.xml index 37afbde5..354f7dc3 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,11 @@ org.springframework.boot spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-validation + mysql @@ -118,7 +123,7 @@ 5.8.11 - + cn.afterturn @@ -126,11 +131,14 @@ 4.3.0 + + com.alibaba + fastjson + 1.2.31 + - - diff --git a/src/main/java/com/achobeta/common/constants/GlobalServiceStatusCode.java b/src/main/java/com/achobeta/common/constants/GlobalServiceStatusCode.java index 138fc874..e4614287 100644 --- a/src/main/java/com/achobeta/common/constants/GlobalServiceStatusCode.java +++ b/src/main/java/com/achobeta/common/constants/GlobalServiceStatusCode.java @@ -27,6 +27,7 @@ public enum GlobalServiceStatusCode { PARAM_IS_BLANK(1002, "参数为空"), PARAM_TYPE_ERROR(1003, "参数类型错误"), PARAM_NOT_COMPLETE(1004, "参数缺失"), + PARAM_FAILED_VALIDATE(1005, "参数未通过验证"), /* 用户错误 2001-3000 */ USER_NOT_LOGIN(2001, "用户未登录"), @@ -42,6 +43,13 @@ public enum GlobalServiceStatusCode { USER_NO_PERMISSION(2403, "用户无权限"), USER_NO_PHONE_CODE(2500, "验证码错误"), + /* 邮箱错误 3001-4000 */ + EMAIL_PATTERN_ERROR(3001, "邮箱格式错误"), + EMAIL_SEND_FAIL(3002, "邮箱发送失败"), + + EMAIL_NOT_EXIST_RECORD(3101, "邮箱不存在记录"), + EMAIL_CODE_NOT_CONSISTENT(3102, "邮箱验证码不一致"), + EMAIL_CODE_OPPORTUNITIES_EXHAUST(3103, "验证次数达到上限"), /* -------------- */; @@ -53,7 +61,6 @@ public enum GlobalServiceStatusCode { this.message = message; } - /** * 根据code获取message * diff --git a/src/main/java/com/achobeta/domain/email/component/EmailSender.java b/src/main/java/com/achobeta/domain/email/component/EmailSender.java index 8031cd2e..3f3676f7 100644 --- a/src/main/java/com/achobeta/domain/email/component/EmailSender.java +++ b/src/main/java/com/achobeta/domain/email/component/EmailSender.java @@ -1,9 +1,9 @@ package com.achobeta.domain.email.component; import cn.hutool.core.bean.BeanUtil; -import com.achobeta.domain.email.component.po.Email; -import com.achobeta.exception.ParameterValidateException; -import com.achobeta.exception.SendMailException; +import com.achobeta.common.constants.GlobalServiceStatusCode; +import com.achobeta.domain.email.component.po.EmailMessage; +import com.achobeta.exception.GlobalServiceException; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; @@ -20,79 +20,77 @@ import java.util.Objects; import java.util.function.Function; - @Component @Slf4j @RequiredArgsConstructor public class EmailSender { + private final JavaMailSender javaMailSender; private final TemplateEngine templateEngine; - public SimpleMailMessage emailToSimpleMailMessage(Email email) { + public SimpleMailMessage emailToSimpleMailMessage(EmailMessage emailMessage) { SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); - simpleMailMessage.setFrom(email.getSender()); - simpleMailMessage.setTo(email.getRecipient()); - simpleMailMessage.setCc(email.getCarbonCopy()); - simpleMailMessage.setSubject(email.getTitle()); - simpleMailMessage.setText(email.getContent()); + simpleMailMessage.setFrom(emailMessage.getSender()); + simpleMailMessage.setTo(emailMessage.getRecipient()); + simpleMailMessage.setCc(emailMessage.getCarbonCopy()); + simpleMailMessage.setSubject(emailMessage.getTitle()); + simpleMailMessage.setText(emailMessage.getContent()); return simpleMailMessage; } - - public MimeMessageHelper emailIntoMimeMessageByHelper(MimeMessage mimeMessage, Email email) { + public MimeMessageHelper emailIntoMimeMessageByHelper(MimeMessage mimeMessage, EmailMessage emailMessage) { try { MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); - mimeMessageHelper.setFrom(email.getSender()); - mimeMessageHelper.setCc(email.getCarbonCopy()); - mimeMessageHelper.setSubject(email.getTitle()); - mimeMessageHelper.setTo(email.getRecipient()); + mimeMessageHelper.setFrom(emailMessage.getSender()); + mimeMessageHelper.setCc(emailMessage.getCarbonCopy()); + mimeMessageHelper.setSubject(emailMessage.getTitle()); + mimeMessageHelper.setTo(emailMessage.getRecipient()); return mimeMessageHelper; } catch (MessagingException e) { - throw new SendMailException(e.getMessage()); + throw new GlobalServiceException(e.getMessage(), GlobalServiceStatusCode.EMAIL_SEND_FAIL); } } - public void sendSimpleMailMessage(Email email) { - if(Objects.isNull(email)) { - throw new ParameterValidateException("email不能为空"); + public void sendSimpleMailMessage(EmailMessage emailMessage) { + if (Objects.isNull(emailMessage)) { + throw new GlobalServiceException("email不能为空", GlobalServiceStatusCode.PARAM_IS_BLANK); } // 封装simpleMailMessage对象 - SimpleMailMessage simpleMailMessage = emailToSimpleMailMessage(email); + SimpleMailMessage simpleMailMessage = emailToSimpleMailMessage(emailMessage); // 发送 javaMailSender.send(simpleMailMessage); } - - public void sendMailWithFile(Email email, File... files) { - if(Objects.isNull(email)) { - throw new ParameterValidateException("email不能为空"); + public void sendMailWithFile(EmailMessage emailMessage, File... files) { + if (Objects.isNull(emailMessage)) { + throw new GlobalServiceException("email不能为空", GlobalServiceStatusCode.PARAM_IS_BLANK); } // 封装对象 try { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); - MimeMessageHelper mimeMessageHelper = emailIntoMimeMessageByHelper(mimeMessage, email); + MimeMessageHelper mimeMessageHelper = emailIntoMimeMessageByHelper(mimeMessage, emailMessage); // 添加附件 - for(File file : files) { - if(Objects.nonNull(file)) { + for (File file : files) { + if (Objects.nonNull(file)) { mimeMessageHelper.addAttachment(file.getName(), file); } } - mimeMessageHelper.setText(email.getContent(), false); + mimeMessageHelper.setText(emailMessage.getContent(), false); javaMailSender.send(mimeMessage); } catch (MessagingException e) { - throw new SendMailException(e.getMessage()); + throw new GlobalServiceException(e.getMessage(), GlobalServiceStatusCode.EMAIL_SEND_FAIL); } } - public void sendModelMail(Email email, String template, Object modelMessage) { - if(Objects.isNull(email)) { - throw new ParameterValidateException("email不能为空"); + public void sendModelMail(EmailMessage emailMessage, String template, Object modelMessage) { + if (Objects.isNull(emailMessage)) { + throw new GlobalServiceException("email不能为空", GlobalServiceStatusCode.PARAM_IS_BLANK); } // 封装对象 try { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); - MimeMessageHelper mimeMessageHelper = emailIntoMimeMessageByHelper(mimeMessage, email); + MimeMessageHelper mimeMessageHelper = emailIntoMimeMessageByHelper(mimeMessage, emailMessage); // 构造模板消息 Context context = new Context(); context.setVariables(BeanUtil.beanToMap(modelMessage)); @@ -101,17 +99,18 @@ public void sendModelMail(Email email, String template, Object modelMessage) { mimeMessageHelper.setText(content, true); javaMailSender.send(mimeMessage); } catch (MessagingException e) { - throw new SendMailException(e.getMessage()); + throw new GlobalServiceException(e.getMessage(), GlobalServiceStatusCode.EMAIL_SEND_FAIL); } } - public void sendModelMailWithFile(Email email, String template, Object modelMessage, File... files) { - if(Objects.isNull(email)) { - throw new ParameterValidateException("email不能为空"); + + public void sendModelMailWithFile(EmailMessage emailMessage, String template, Object modelMessage, File... files) { + if (Objects.isNull(emailMessage)) { + throw new GlobalServiceException("email不能为空", GlobalServiceStatusCode.PARAM_IS_BLANK); } // 封装对象 try { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); - MimeMessageHelper mimeMessageHelper = emailIntoMimeMessageByHelper(mimeMessage, email); + MimeMessageHelper mimeMessageHelper = emailIntoMimeMessageByHelper(mimeMessage, emailMessage); // 构造模板消息 Context context = new Context(); context.setVariables(BeanUtil.beanToMap(modelMessage)); @@ -119,25 +118,25 @@ public void sendModelMailWithFile(Email email, String template, Object modelMess String content = templateEngine.process(template, context); mimeMessageHelper.setText(content, true); // 添加附件 - for(File file : files) { - if(Objects.nonNull(file)) { + for (File file : files) { + if (Objects.nonNull(file)) { mimeMessageHelper.addAttachment(file.getName(), file); } } javaMailSender.send(mimeMessage); } catch (MessagingException e) { - throw new SendMailException(e.getMessage()); + throw new GlobalServiceException(e.getMessage(), GlobalServiceStatusCode.EMAIL_SEND_FAIL); } } - public void customizedSendEmail(Email email, String template, Function function, File... files) { - if(Objects.isNull(email)) { - throw new ParameterValidateException("email不能为空"); + public void customizedSendEmail(EmailMessage emailMessage, String template, Function function, File... files) { + if (Objects.isNull(emailMessage)) { + throw new GlobalServiceException("email不能为空", GlobalServiceStatusCode.PARAM_IS_BLANK); } - String sender = email.getSender(); - String[] carbonCopy = email.getCarbonCopy(); - String title = email.getTitle(); - Arrays.stream(email.getRecipient()) + String sender = emailMessage.getSender(); + String[] carbonCopy = emailMessage.getCarbonCopy(); + String title = emailMessage.getTitle(); + Arrays.stream(emailMessage.getRecipient()) .parallel() .distinct() .forEach(s -> { @@ -150,8 +149,8 @@ public void customizedSendEmail(Email email, String template, Function void customizedSendEmail(Email email, String template, Function email:{}", code, email); + return SystemJsonResponse.SYSTEM_SUCCESS(); + } + + @PostMapping("/check/{code}") + public SystemJsonResponse checkCode(@RequestParam("email") @Email String email, + @PathVariable("code") @NonNull String code) { + // 验证 + emailService.checkIdentifyingCode(email, code); + // 成功 + log.info("email:{}, 验证码:{} 验证成功", email, code); + return SystemJsonResponse.SYSTEM_SUCCESS(); + } + +} diff --git a/src/main/java/com/achobeta/domain/email/model/vo/VerificationCodeTemplate.java b/src/main/java/com/achobeta/domain/email/model/vo/VerificationCodeTemplate.java new file mode 100644 index 00000000..7101c4d6 --- /dev/null +++ b/src/main/java/com/achobeta/domain/email/model/vo/VerificationCodeTemplate.java @@ -0,0 +1,17 @@ +package com.achobeta.domain.email.model.vo; + +import lombok.Builder; +import lombok.Getter; + +/** + * 生命周期只有一次,成员变量对修改关闭,直接通过 builder 构建对象 + */ +@Getter +@Builder +public class VerificationCodeTemplate { + + private String code; // 验证码 + + private int minutes; // 过期时间分钟数 + +} diff --git a/src/main/java/com/achobeta/domain/email/repository/EmailRepository.java b/src/main/java/com/achobeta/domain/email/repository/EmailRepository.java new file mode 100644 index 00000000..fa159f76 --- /dev/null +++ b/src/main/java/com/achobeta/domain/email/repository/EmailRepository.java @@ -0,0 +1,50 @@ +package com.achobeta.domain.email.repository; + +import com.achobeta.domain.email.util.IdentifyingCodeValidator; +import com.achobeta.redis.RedisCache; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + + +@Repository +@RequiredArgsConstructor +public class EmailRepository { + + private final RedisCache redisCache; + + /** + * 设置验证码到 redis 里(携带超时时间点,与验证机会数) + * + * @param redisKey 用户邮箱在 redis 对应的key + * @param code 验证码 + * @param timeout 超时时间 + * @param opportunities 验证机会 + */ + public void setIdentifyingCode(String redisKey, String code, long timeout, int opportunities) { + Map data = new HashMap<>(); + data.put(IdentifyingCodeValidator.IDENTIFYING_CODE, code); // 验证码 + data.put(IdentifyingCodeValidator.IDENTIFYING_OPPORTUNITIES, opportunities); // 有效次数 + redisCache.setCacheMap(redisKey, data); + redisCache.expire(redisKey, timeout); // 设置超时时间 + } + + public long getTTLOfCode(String redisKey) { + return redisCache.getKeyTTL(redisKey); + } + + public long decrementOpportunities(String redisKey) { + return redisCache.decrementCacheMapNumber(redisKey, IdentifyingCodeValidator.IDENTIFYING_OPPORTUNITIES); + } + + public Optional> getIdentifyingCode(String redisKey) { + return redisCache.getCacheMap(redisKey); + } + + public void deleteIdentifyingCodeRecord(String redisKey) { + redisCache.deleteObject(redisKey); + } +} diff --git a/src/main/java/com/achobeta/domain/email/service/EmailService.java b/src/main/java/com/achobeta/domain/email/service/EmailService.java new file mode 100644 index 00000000..2f8e9727 --- /dev/null +++ b/src/main/java/com/achobeta/domain/email/service/EmailService.java @@ -0,0 +1,20 @@ +package com.achobeta.domain.email.service; + +public interface EmailService { + + /** + * 向用户邮箱发送验证码 + * + * @param email 用户的邮箱 + * @param code 验证码 + */ + void sendIdentifyingCode(String email, String code); + + /** + * 校验当前邮箱用户输入的验证码是否正确 + * + * @param email 用户的邮箱 + * @param code 验证码 + */ + void checkIdentifyingCode(String email, String code); +} diff --git a/src/main/java/com/achobeta/domain/email/service/impl/EmailServiceImpl.java b/src/main/java/com/achobeta/domain/email/service/impl/EmailServiceImpl.java new file mode 100644 index 00000000..85ce7114 --- /dev/null +++ b/src/main/java/com/achobeta/domain/email/service/impl/EmailServiceImpl.java @@ -0,0 +1,97 @@ +package com.achobeta.domain.email.service.impl; + +import com.achobeta.common.constants.GlobalServiceStatusCode; +import com.achobeta.domain.email.component.EmailSender; +import com.achobeta.domain.email.component.po.EmailMessage; +import com.achobeta.domain.email.model.vo.VerificationCodeTemplate; +import com.achobeta.domain.email.repository.EmailRepository; +import com.achobeta.domain.email.service.EmailService; +import com.achobeta.domain.email.util.IdentifyingCodeValidator; +import com.achobeta.exception.GlobalServiceException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.Map; + + +@Slf4j +@Service +@RequiredArgsConstructor +public class EmailServiceImpl implements EmailService { + + // todo: 将配置写于配置文件 + private static final int IDENTIFYING_CODE_MINUTES = 5;//过期分钟数 + + private static final long IDENTIFYING_CODE_INTERVAL_Limit = 1 * 60 * 1000; // 两次发送验证码的最短时间间隔 + + private static final long IDENTIFYING_CODE_TIMEOUT = IDENTIFYING_CODE_MINUTES * 60 * 1000; //单位为毫秒 + + private static final int IDENTIFYING_CODE_INTERVAL_LIMIT = 5; // 只有五次验证机会 + + private static final String EMAIL_MODEL_HTML = "identifying-code-model.html"; // Email 验证码通知 -模板 + + @Value("${spring.mail.username}") + private String achobetaEmail; + + private final EmailSender emailSender; + + private final EmailRepository emailRepository; + + @Override + public void sendIdentifyingCode(String email, String code) { + final String redisKey = IdentifyingCodeValidator.REDIS_EMAIL_IDENTIFYING_CODE + email; + // 验证一下一分钟以内发过了没有 + long ttl = emailRepository.getTTLOfCode(redisKey); // 小于 0 则代表没有到期时间或者不存在,允许发送 + if(ttl > IDENTIFYING_CODE_TIMEOUT - IDENTIFYING_CODE_INTERVAL_Limit) { + String message = String.format("请在 %d 分钟后再重新申请", IDENTIFYING_CODE_INTERVAL_Limit / (60 * 1000L)); + throw new GlobalServiceException(message, GlobalServiceStatusCode.EMAIL_SEND_FAIL); + } + // 封装 Email + EmailMessage emailMessage = new EmailMessage(); + emailMessage.setContent(code); + emailMessage.setCreateTime(new Date()); + emailMessage.setTitle(IdentifyingCodeValidator.IDENTIFYING_CODE_PURPOSE); + emailMessage.setRecipient(email); + emailMessage.setCarbonCopy(); + emailMessage.setSender(achobetaEmail); + // 存到 redis 中 + emailRepository.setIdentifyingCode(redisKey, code, IDENTIFYING_CODE_TIMEOUT, IDENTIFYING_CODE_INTERVAL_LIMIT); + // 构造模板消息 + VerificationCodeTemplate verificationCodeTemplate = VerificationCodeTemplate.builder() + .code(code) + .minutes(IDENTIFYING_CODE_MINUTES) + .build(); + // 发送模板消息 + emailSender.sendModelMail(emailMessage, EMAIL_MODEL_HTML, verificationCodeTemplate); + } + + @Override + public void checkIdentifyingCode(String email, String code) { + String redisKey = IdentifyingCodeValidator.REDIS_EMAIL_IDENTIFYING_CODE + email; + Map map = emailRepository.getIdentifyingCode(redisKey) + .map(value -> (Map)value) + .orElseThrow(() -> { + String message = String.format("Redis 中不存在邮箱[%s]的相关记录", email); + return new GlobalServiceException(message, GlobalServiceStatusCode.EMAIL_NOT_EXIST_RECORD); + }); + // 取出验证码和过期时间点 + String codeValue = (String) map.get(IdentifyingCodeValidator.IDENTIFYING_CODE); + int opportunities = (int) map.get(IdentifyingCodeValidator.IDENTIFYING_OPPORTUNITIES); + // 还有没有验证机会 + if (opportunities < 1) { + throw new GlobalServiceException(GlobalServiceStatusCode.EMAIL_CODE_OPPORTUNITIES_EXHAUST); + } + // 验证是否正确 + if (!codeValue.equals(code)) { + // 次数减一 + opportunities = (int)emailRepository.decrementOpportunities(redisKey); + String message = String.format("验证码错误,剩余%d次机会", opportunities); + throw new GlobalServiceException(message, GlobalServiceStatusCode.EMAIL_CODE_NOT_CONSISTENT); + } + // 验证成功 + emailRepository.deleteIdentifyingCodeRecord(redisKey); + } +} diff --git a/src/main/java/com/achobeta/domain/email/util/IdentifyingCodeValidator.java b/src/main/java/com/achobeta/domain/email/util/IdentifyingCodeValidator.java new file mode 100644 index 00000000..b9260e92 --- /dev/null +++ b/src/main/java/com/achobeta/domain/email/util/IdentifyingCodeValidator.java @@ -0,0 +1,24 @@ +package com.achobeta.domain.email.util; + +import cn.hutool.core.util.RandomUtil; + +import java.util.Map; + +public class IdentifyingCodeValidator { + + public static final String IDENTIFYING_CODE_PURPOSE = "验证用户身份"; + + public static final int IDENTIFYING_CODE_SIZE = 6; // 验证码长度 + + public static final String IDENTIFYING_CODE = "IdentifyingCode"; // 验证码 + + public static final String IDENTIFYING_OPPORTUNITIES = "IdentifyingOpportunities"; // 剩余验证验证机会 + +// public static final String REDIS_EMAIL_IDENTIFYING_CODE = "REDIS_EMAIL_IDENTIFYING_CODE_"; + public static final String REDIS_EMAIL_IDENTIFYING_CODE = "redis_email_identifying_code:"; + + public static String getIdentifyingCode() { + return RandomUtil.randomNumbers(IDENTIFYING_CODE_SIZE); + } + +} diff --git a/src/main/java/com/achobeta/domain/package-info.java b/src/main/java/com/achobeta/domain/package-info.java index de8f64e9..405a91d8 100644 --- a/src/main/java/com/achobeta/domain/package-info.java +++ b/src/main/java/com/achobeta/domain/package-info.java @@ -5,6 +5,7 @@ * 比如招新系统有注册登录模块 users,有通知模块 notice * 每个领域模块里面的内容应当包含四个(controller、model、repository、service) * 暂时以 users 包为例,大家可以点击浏览一下,每个包的具体含义和职责请查看 package-info + * * @author BanTanger 半糖 * @date 2024/1/11 15:40 */ diff --git a/src/main/java/com/achobeta/domain/shortlink/component/RedisCache.java b/src/main/java/com/achobeta/domain/shortlink/component/RedisCache.java deleted file mode 100644 index bb64ec61..00000000 --- a/src/main/java/com/achobeta/domain/shortlink/component/RedisCache.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.achobeta.domain.shortlink.component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import net.sf.jsqlparser.statement.select.KSQLWindow; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@Component -@RequiredArgsConstructor -@Slf4j -public class RedisCache { - - private final RedisTemplate redisTemplate; - - private final RedisBloomFilter redisBloomFilter; - - /** - * 缓存基本的对象,Integer、String、实体类等 - * - * @param key 缓存的键值 - * @param value 缓存的值 - */ - public void setCacheObject(final String key, final T value) { - log.info("存入Redis\t[{}]-[{}]", key, value); - // todo: 设置超时时间 - redisTemplate.opsForValue().set(key, value); - } - - public Optional getCacheObject(final String key) { - T value = (T) redisTemplate.opsForValue().get(key); - log.info("查询Redis\t[{}]-[{}]", key, value); - return Optional.ofNullable(value); - } - - /** - * 删除单个对象 - * - * @param key - */ - public boolean deleteObject(final String key) { - log.info("删除Redis键值\tkey[{}]", key); - return redisTemplate.delete(key); - } - - /** - * 加入布隆过滤器 - * @param key 键值 - * @return - */ - public void addToBloomFilter(final String key) { - log.info("加入布隆过滤器\tkey[{}]", key); - redisBloomFilter.add(key); - } - - /** - * 布隆过滤器是否存在该键值 - * @param key 键值 - * @return - */ - public boolean containsInBloomFilter(final String key) { - boolean flag = redisBloomFilter.contains(key); - log.info("key[{}]\t是否存在于布隆过滤器:\t{}", key, flag); - return flag; - } - -} \ No newline at end of file diff --git a/src/main/java/com/achobeta/domain/shortlink/controller/ShortLinkController.java b/src/main/java/com/achobeta/domain/shortlink/controller/ShortLinkController.java index 24ce3d36..d637071a 100644 --- a/src/main/java/com/achobeta/domain/shortlink/controller/ShortLinkController.java +++ b/src/main/java/com/achobeta/domain/shortlink/controller/ShortLinkController.java @@ -1,11 +1,11 @@ package com.achobeta.domain.shortlink.controller; import com.achobeta.common.SystemJsonResponse; +import com.achobeta.common.constants.GlobalServiceStatusCode; import com.achobeta.domain.shortlink.service.ShortLinkService; import com.achobeta.domain.shortlink.util.HttpUrlValidator; import com.achobeta.domain.shortlink.util.ShortLinkUtils; -import com.achobeta.exception.IllegalUrlException; -import com.achobeta.exception.ShortLinkGenerateException; +import com.achobeta.exception.GlobalServiceException; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -21,29 +21,32 @@ public class ShortLinkController { private final ShortLinkService shortLinkService; - /** * 重定向短链接 + * * @param code 短链code * @return 重定向到原链接 */ @GetMapping("/{code}") - public RedirectView getShortLink(@PathVariable("code")String code) { + public RedirectView getShortLink(@PathVariable("code") String code) { String originUrl = shortLinkService.getOriginUrl(code); + log.info("短链code:{} -> 原链接:{}", code, originUrl); return new RedirectView(originUrl); } /** * 长转短 + * * @param request 用来获取host - * @param url 原链接 + * @param url 原链接 * @return 短链接 */ @PostMapping("/trans") - public SystemJsonResponse transferAndSaveShortLink(HttpServletRequest request, @RequestParam("url")String url) { + public SystemJsonResponse transferAndSaveShortLink(HttpServletRequest request, @RequestParam("url") String url) { //验证url - if(!HttpUrlValidator.isHttpUrl(url) || !HttpUrlValidator.isUrlAccessible(url)) { - throw new IllegalUrlException(String.format("url:'%s' 无效", url)); + if (!HttpUrlValidator.isHttpUrl(url) || !HttpUrlValidator.isUrlAccessible(url)) { + throw new GlobalServiceException(String.format("url:'%s' 无效", url), + GlobalServiceStatusCode.PARAM_NOT_VALID); } // 拼接出基础的url String baseUrl = ShortLinkUtils.getBaseUrl(request.getHeader("host")); @@ -52,6 +55,4 @@ public SystemJsonResponse transferAndSaveShortLink(HttpServletRequest request, @ log.info("原链接:{} -> 短链接:{}", url, shortLinkURL); return SystemJsonResponse.SYSTEM_SUCCESS(shortLinkURL); } - - } diff --git a/src/main/java/com/achobeta/domain/shortlink/mapper/ShortLinkMapper.java b/src/main/java/com/achobeta/domain/shortlink/mapper/ShortLinkMapper.java index a15ffa5f..30612c92 100644 --- a/src/main/java/com/achobeta/domain/shortlink/mapper/ShortLinkMapper.java +++ b/src/main/java/com/achobeta/domain/shortlink/mapper/ShortLinkMapper.java @@ -4,11 +4,11 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** -* @author 马拉圈 -* @description 针对表【short_link】的数据库操作Mapper -* @createDate 2024-01-12 19:48:07 -* @Entity com.macaku.domain.po.ShortLink -*/ + * @author 马拉圈 + * @description 针对表【short_link】的数据库操作Mapper + * @createDate 2024-01-12 19:48:07 + * @Entity com.macaku.domain.po.ShortLink + */ public interface ShortLinkMapper extends BaseMapper { } diff --git a/src/main/java/com/achobeta/domain/shortlink/po/ShortLink.java b/src/main/java/com/achobeta/domain/shortlink/po/ShortLink.java index 80a0716f..c20a9516 100644 --- a/src/main/java/com/achobeta/domain/shortlink/po/ShortLink.java +++ b/src/main/java/com/achobeta/domain/shortlink/po/ShortLink.java @@ -1,16 +1,16 @@ package com.achobeta.domain.shortlink.po; import com.achobeta.domain.users.model.dao.BaseIncrIDEntity; -import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; -import java.util.Date; /** * @TableName short_link */ -@TableName(value ="short_link") +@TableName(value = "short_link") @Data public class ShortLink extends BaseIncrIDEntity implements Serializable { diff --git a/src/main/java/com/achobeta/domain/shortlink/service/ShortLinkService.java b/src/main/java/com/achobeta/domain/shortlink/service/ShortLinkService.java index 5b0fdffc..2920e08d 100644 --- a/src/main/java/com/achobeta/domain/shortlink/service/ShortLinkService.java +++ b/src/main/java/com/achobeta/domain/shortlink/service/ShortLinkService.java @@ -2,28 +2,31 @@ import com.achobeta.domain.shortlink.po.ShortLink; import com.baomidou.mybatisplus.extension.service.IService; + /** -* @author 马拉圈 -* @description 针对表【short_link】的数据库操作Service -* @createDate 2024-01-12 19:48:07 -*/ + * @author 马拉圈 + * @description 针对表【short_link】的数据库操作Service + * @createDate 2024-01-12 19:48:07 + */ public interface ShortLinkService extends IService { /** * url生成唯一的code - * 1. 将code和源链接的关系保存到数据库里 - * 2. 将code对应的key和源链接作为键值对保存到redis里 - * 3. 将code对应的key加入到redis布隆过滤器中 + * 1. 将code和源链接的关系保存到数据库里 + * 2. 将code对应的key和源链接作为键值对保存到redis里 + * 3. 将code对应的key加入到redis布隆过滤器中 + * * @param baseUrl 基础路径 - * @param url 原链接 + * @param url 原链接 * @return 短链接 */ String transShortLinkURL(String baseUrl, String url); /** * 通过code获取原链接 - * 1. redis中存在短链接code对应的键值对,直接返回重定向原链接 - * 2. 否则,查数据库再重定向,如果查不到就抛异常 + * 1. redis中存在短链接code对应的键值对,直接返回重定向原链接 + * 2. 否则,查数据库再重定向,如果查不到就抛异常 + * * @param code 短链code * @return 原链接 */ diff --git a/src/main/java/com/achobeta/domain/shortlink/service/impl/ShortLinkServiceImpl.java b/src/main/java/com/achobeta/domain/shortlink/service/impl/ShortLinkServiceImpl.java index fb5d6e4c..78fca2e4 100644 --- a/src/main/java/com/achobeta/domain/shortlink/service/impl/ShortLinkServiceImpl.java +++ b/src/main/java/com/achobeta/domain/shortlink/service/impl/ShortLinkServiceImpl.java @@ -1,32 +1,33 @@ package com.achobeta.domain.shortlink.service.impl; -import com.achobeta.domain.shortlink.component.RedisCache; +import com.achobeta.common.constants.GlobalServiceStatusCode; import com.achobeta.domain.shortlink.mapper.ShortLinkMapper; import com.achobeta.domain.shortlink.po.ShortLink; import com.achobeta.domain.shortlink.service.ShortLinkService; import com.achobeta.domain.shortlink.util.ShortLinkUtils; -import com.achobeta.exception.ShortLinkGenerateException; +import com.achobeta.exception.GlobalServiceException; +import com.achobeta.redis.RedisCache; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.Date; import java.util.Objects; import java.util.Optional; /** -* @author 马拉圈 -* @description 针对表【short_link】的数据库操作Service实现 -* @createDate 2024-01-12 19:48:07 -*/ + * @author 马拉圈 + * @description 针对表【short_link】的数据库操作Service实现 + * @createDate 2024-01-12 19:48:07 + */ @Service @Slf4j @RequiredArgsConstructor public class ShortLinkServiceImpl extends ServiceImpl - implements ShortLinkService { + implements ShortLinkService { + private static final long SHORT_LINK_TIMEOUT = 1 * 7 * 24 * 3600 * 1000L; // 超时时间 (默认七天) private final RedisCache redisCache; @@ -39,7 +40,7 @@ public String transShortLinkURL(String baseUrl, String url) { do { code = ShortLinkUtils.getShortCodeByURL(code); redisKey = ShortLinkUtils.REDIS_SHORT_LINK + code; - } while (redisCache.containsInBloomFilter(redisKey));//误判为存在也无所谓,无非就是再重新生成一个 + } while (redisCache.containsInBloomFilter(ShortLinkUtils.BLOOM_FILTER_NAME, redisKey));//误判为存在也无所谓,无非就是再重新生成一个 // 保存 ShortLink shortLink = new ShortLink(); shortLink.setOriginUrl(url); @@ -51,8 +52,8 @@ public String transShortLinkURL(String baseUrl, String url) { log.info("原链接:{} -> redisKey:{}", url, redisKey); this.save(shortLink); // 缓存到Redis,加入布隆过滤器 - redisCache.setCacheObject(redisKey, url); - redisCache.addToBloomFilter(redisKey); + redisCache.setCacheObject(redisKey, url, SHORT_LINK_TIMEOUT); + redisCache.addToBloomFilter(ShortLinkUtils.BLOOM_FILTER_NAME, redisKey); // 返回完整的短链接 return baseUrl + code; } @@ -65,13 +66,13 @@ public String getOriginUrl(String code) { return originUrlCache.orElseGet(() -> { //否则查MySQL ShortLink shortLink = this.lambdaQuery().eq(ShortLink::getShortCode, code).one(); - if(Objects.isNull(shortLink)) { - throw new ShortLinkGenerateException("不存在此短链接code:" + code); + if (Objects.isNull(shortLink)) { + throw new GlobalServiceException("不存在此短链接code:" + code, GlobalServiceStatusCode.PARAM_NOT_VALID); } String originUrl = shortLink.getOriginUrl(); // 缓存到Redis里 - redisCache.setCacheObject(redisKey, originUrl); - redisCache.addToBloomFilter(redisKey); + redisCache.setCacheObject(redisKey, originUrl, SHORT_LINK_TIMEOUT); + redisCache.addToBloomFilter(ShortLinkUtils.BLOOM_FILTER_NAME, redisKey); return originUrl; }); } diff --git a/src/main/java/com/achobeta/domain/shortlink/util/HttpUrlValidator.java b/src/main/java/com/achobeta/domain/shortlink/util/HttpUrlValidator.java index c0e09d16..5f30dd5d 100644 --- a/src/main/java/com/achobeta/domain/shortlink/util/HttpUrlValidator.java +++ b/src/main/java/com/achobeta/domain/shortlink/util/HttpUrlValidator.java @@ -1,6 +1,5 @@ package com.achobeta.domain.shortlink.util; -import cn.hutool.log.Log; import com.achobeta.common.constants.GlobalServiceStatusCode; import lombok.extern.slf4j.Slf4j; @@ -18,6 +17,7 @@ public class HttpUrlValidator { /** * 是否格式正确 + * * @param url * @return */ @@ -27,6 +27,7 @@ public static boolean isHttpUrl(String url) { /** * 是否是有效地址 + * * @param urlString 链接 * @return 是否有效 */ diff --git a/src/main/java/com/achobeta/domain/shortlink/util/ShortLinkUtils.java b/src/main/java/com/achobeta/domain/shortlink/util/ShortLinkUtils.java index d80d2672..5bf80406 100644 --- a/src/main/java/com/achobeta/domain/shortlink/util/ShortLinkUtils.java +++ b/src/main/java/com/achobeta/domain/shortlink/util/ShortLinkUtils.java @@ -1,7 +1,8 @@ package com.achobeta.domain.shortlink.util; -import com.achobeta.exception.ShortLinkGenerateException; +import com.achobeta.common.constants.GlobalServiceStatusCode; +import com.achobeta.exception.GlobalServiceException; import org.apache.commons.codec.digest.DigestUtils; import java.util.UUID; @@ -11,7 +12,10 @@ public class ShortLinkUtils { private static final String CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; private static final int LINK_LENGTH = 6; - public static final String REDIS_SHORT_LINK = "REDIS_SHORT_LINK_";//前缀 +// public static final String REDIS_SHORT_LINK = "REDIS_SHORT_LINK_";//前缀 + public static final String REDIS_SHORT_LINK = "redis_short_link:";//前缀 + + public static final String BLOOM_FILTER_NAME = "LINK_CODE_BLOOM_FILTER"; // 布隆过滤器名 public static final int FETCH_RADIX = 16; @@ -19,7 +23,6 @@ public class ShortLinkUtils { public static final int FETCH_SIZE = 4; - // 获取盐值 public static String getSalt() { return UUID.randomUUID().toString().replace("-", ""); @@ -33,9 +36,10 @@ public static String md5(String normal) { public static String subCodeByString(String str) { int strLength = str.length(); int gap = strLength / LINK_LENGTH;//取值间隔 - if(gap < FETCH_SIZE) { + if (gap < FETCH_SIZE) { // 代表无法取出6个十六进制数 - throw new ShortLinkGenerateException(String.format("哈希字符串%s,无法取出%d个%d进制数", str, LINK_LENGTH, FETCH_RADIX)); + String message = String.format("哈希字符串%s,无法取出%d个%d进制数", str, LINK_LENGTH, FETCH_RADIX); + throw new GlobalServiceException(message, GlobalServiceStatusCode.PARAM_NOT_VALID); } StringBuilder subCode = new StringBuilder(); for (int i = 0; i < LINK_LENGTH; i++) { @@ -55,5 +59,4 @@ public static String getBaseUrl(String host) { } - } diff --git a/src/main/java/com/achobeta/domain/users/controller/UserController.java b/src/main/java/com/achobeta/domain/users/controller/UserController.java index 19fe214d..f273e15c 100644 --- a/src/main/java/com/achobeta/domain/users/controller/UserController.java +++ b/src/main/java/com/achobeta/domain/users/controller/UserController.java @@ -3,7 +3,9 @@ import com.achobeta.common.SystemJsonResponse; import com.achobeta.domain.users.service.UserService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -12,13 +14,16 @@ * @date 2024/1/11 15:58 */ @RestController +@Slf4j @RequiredArgsConstructor +@RequestMapping("/api/v1/user") public class UserController { - private final UserService userService; + private UserService userService; /** * http://localhost:9001/login?name=bantanger + * * @param name * @return */ diff --git a/src/main/java/com/achobeta/domain/users/controller/package-info.java b/src/main/java/com/achobeta/domain/users/controller/package-info.java index c01d2d15..951aa366 100644 --- a/src/main/java/com/achobeta/domain/users/controller/package-info.java +++ b/src/main/java/com/achobeta/domain/users/controller/package-info.java @@ -1,6 +1,7 @@ /** * 这个不用多说,就是 controller 层 * 这层主要做参数过滤,校验以及调用 service + * * @author BanTanger 半糖 * @date 2024/1/11 15:48 */ diff --git a/src/main/java/com/achobeta/domain/users/model/dao/package-info.java b/src/main/java/com/achobeta/domain/users/model/dao/package-info.java index 73e7c135..f156fb8a 100644 --- a/src/main/java/com/achobeta/domain/users/model/dao/package-info.java +++ b/src/main/java/com/achobeta/domain/users/model/dao/package-info.java @@ -1,6 +1,7 @@ /** * 数据库表与 Java 实体的 ORM 映射 * mapper 包存放增删改查(也就是 mybatis plus 的 mapper 类) + * * @author BanTanger 半糖 * @date 2024/1/11 16:10 */ diff --git a/src/main/java/com/achobeta/domain/users/model/package-info.java b/src/main/java/com/achobeta/domain/users/model/package-info.java index d88cdb8a..2c30b9fc 100644 --- a/src/main/java/com/achobeta/domain/users/model/package-info.java +++ b/src/main/java/com/achobeta/domain/users/model/package-info.java @@ -1,6 +1,7 @@ /** * model 存储数据库表映射的实体对象,请求 request 以及响应 response * 更通俗一点的说法就是 dao、dto、vo + * * @author BanTanger 半糖 * @date 2024/1/11 15:48 */ diff --git a/src/main/java/com/achobeta/domain/users/repository/package-info.java b/src/main/java/com/achobeta/domain/users/repository/package-info.java index ab546b30..fe063e97 100644 --- a/src/main/java/com/achobeta/domain/users/repository/package-info.java +++ b/src/main/java/com/achobeta/domain/users/repository/package-info.java @@ -2,6 +2,7 @@ * 仓储层,不单只是数据库,也可以是 es、redis 这种与底层数据打交道的底层 * 它的上层是 service 层,下层是 mysql、nosql 等等 * 也就是 service 调用 repository, repository 以接口方式调用具体实现类与底层数据交互 + * * @author BanTanger 半糖 * @date 2024/1/11 15:49 */ diff --git a/src/main/java/com/achobeta/domain/users/service/impl/UserServiceImpl.java b/src/main/java/com/achobeta/domain/users/service/impl/UserServiceImpl.java index 92441ae7..30a2c698 100644 --- a/src/main/java/com/achobeta/domain/users/service/impl/UserServiceImpl.java +++ b/src/main/java/com/achobeta/domain/users/service/impl/UserServiceImpl.java @@ -1,7 +1,7 @@ package com.achobeta.domain.users.service.impl; import com.achobeta.domain.users.service.UserService; -import com.achobeta.exception.NotPermissionException; +import com.achobeta.exception.GlobalServiceException; import org.springframework.stereotype.Service; /** @@ -15,7 +15,7 @@ public void test_NotPermissionExceptionHandler() { boolean loginStatus = false; if (!loginStatus) { // 减少 try-catch 语句, 直接抛出异常将会被全局异常处理器 GlobalExceptionHandler 接收处理 - throw new NotPermissionException(); + throw new GlobalServiceException(); } } diff --git a/src/main/java/com/achobeta/domain/users/service/package-info.java b/src/main/java/com/achobeta/domain/users/service/package-info.java index 5ce636a8..d8f17aaf 100644 --- a/src/main/java/com/achobeta/domain/users/service/package-info.java +++ b/src/main/java/com/achobeta/domain/users/service/package-info.java @@ -1,6 +1,7 @@ /** * service 层,真正的业务逻辑层 * repository 提供最基础的增删改查能力,而 service 层则封装业务逻辑 + * * @author BanTanger 半糖 * @date 2024/1/11 15:52 */ diff --git a/src/main/java/com/achobeta/exception/GlobalServiceException.java b/src/main/java/com/achobeta/exception/GlobalServiceException.java new file mode 100644 index 00000000..91c464a4 --- /dev/null +++ b/src/main/java/com/achobeta/exception/GlobalServiceException.java @@ -0,0 +1,39 @@ +package com.achobeta.exception; + +import com.achobeta.common.constants.GlobalServiceStatusCode; +import lombok.Getter; + +/** + * Created With Intellij IDEA + * Description: + * User: 马拉圈 + * Date: 2024-01-18 + * Time: 15:44 + */ +@Getter +public class GlobalServiceException extends RuntimeException{ + + private final GlobalServiceStatusCode statusCode; + + private final String message; + + public GlobalServiceException(String message, GlobalServiceStatusCode statusCode) { + this.message = message; + this.statusCode = statusCode; + } + + public GlobalServiceException(String message) { + this.message = message; + this.statusCode = GlobalServiceStatusCode.SYSTEM_SERVICE_FAIL; + } + + public GlobalServiceException(GlobalServiceStatusCode statusCode) { + this.message = statusCode.getMessage(); + this.statusCode = statusCode; + } + + public GlobalServiceException() { + this.message = GlobalServiceStatusCode.SYSTEM_SERVICE_FAIL.getMessage(); + this.statusCode = GlobalServiceStatusCode.SYSTEM_SERVICE_FAIL; + } +} diff --git a/src/main/java/com/achobeta/exception/IllegalUrlException.java b/src/main/java/com/achobeta/exception/IllegalUrlException.java deleted file mode 100644 index 1a888b56..00000000 --- a/src/main/java/com/achobeta/exception/IllegalUrlException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.achobeta.exception; - -/** - * Created with IntelliJ IDEA. - * Description: - * User: 马拉圈 - * Date: 2024-01-17 - * Time: 0:42 - */ -public class IllegalUrlException extends IllegalArgumentException { - public IllegalUrlException(String s) { - super(s); - } -} diff --git a/src/main/java/com/achobeta/exception/NotPermissionException.java b/src/main/java/com/achobeta/exception/NotPermissionException.java deleted file mode 100644 index 6b05b0f2..00000000 --- a/src/main/java/com/achobeta/exception/NotPermissionException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.achobeta.exception; - -/** - * @author BanTanger 半糖 - * @date 2024/1/11 19:53 - */ -public class NotPermissionException extends RuntimeException { -} diff --git a/src/main/java/com/achobeta/exception/ParameterValidateException.java b/src/main/java/com/achobeta/exception/ParameterValidateException.java deleted file mode 100644 index 8390c6cb..00000000 --- a/src/main/java/com/achobeta/exception/ParameterValidateException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.achobeta.exception; - -/** - * Created with IntelliJ IDEA. - * Description: - * User: 马拉圈 - * Date: 2024-01-16 - * Time: 18:51 - */ -public class ParameterValidateException extends RuntimeException { - - public ParameterValidateException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/src/main/java/com/achobeta/exception/SendMailException.java b/src/main/java/com/achobeta/exception/SendMailException.java deleted file mode 100644 index d1d3b6ff..00000000 --- a/src/main/java/com/achobeta/exception/SendMailException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.achobeta.exception; - -/** - * Created with IntelliJ IDEA. - * Description: - * User: 马拉圈 - * Date: 2024-01-16 - * Time: 18:26 - */ -public class SendMailException extends RuntimeException { - public SendMailException(String message) { - super(message); - } -} diff --git a/src/main/java/com/achobeta/exception/ShortLinkGenerateException.java b/src/main/java/com/achobeta/exception/ShortLinkGenerateException.java deleted file mode 100644 index 11b37229..00000000 --- a/src/main/java/com/achobeta/exception/ShortLinkGenerateException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.achobeta.exception; - -/** - * Created with IntelliJ IDEA. - * Description: - * User: 马拉圈 - * Date: 2024-01-16 - * Time: 19:10 - */ -public class ShortLinkGenerateException extends RuntimeException{ - - public ShortLinkGenerateException(String message) { - super(message); - } -} diff --git a/src/main/java/com/achobeta/handler/GlobalExceptionHandler.java b/src/main/java/com/achobeta/handler/GlobalExceptionHandler.java index 0dce4dc3..abdeeef5 100644 --- a/src/main/java/com/achobeta/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/achobeta/handler/GlobalExceptionHandler.java @@ -1,13 +1,19 @@ package com.achobeta.handler; import com.achobeta.common.SystemJsonResponse; -import com.achobeta.exception.*; +import com.achobeta.common.constants.GlobalServiceStatusCode; +import com.achobeta.exception.GlobalServiceException; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import static com.achobeta.common.constants.GlobalServiceStatusCode.*; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.achobeta.common.constants.GlobalServiceStatusCode.PARAM_FAILED_VALIDATE; /** * 全局异常处理器,减少 try-catch 语句 @@ -22,39 +28,26 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(NotPermissionException.class) - public SystemJsonResponse handleNotPermissionException(NotPermissionException e, HttpServletRequest request) { - String requestURI = request.getRequestURI(); - log.error("请求地址'{}', 权限码校验失败'{}'", requestURI, e.getMessage()); - return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(USER_NO_PERMISSION, "没有访问权限, 请联系管理员授权"); - } - - @ExceptionHandler(SendMailException.class) - public SystemJsonResponse handleSendMailException(SendMailException e, HttpServletRequest request) { + @ExceptionHandler(GlobalServiceException.class) + public SystemJsonResponse handleGlobalServiceException(GlobalServiceException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); - log.error("请求地址'{}', 邮箱发送失败'{}'", requestURI, e.getMessage()); - return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(SYSTEM_SERVICE_FAIL, "邮箱发送失败"); + String message = e.getMessage(); + GlobalServiceStatusCode statusCode = e.getStatusCode(); + log.error("请求地址'{}', {}: '{}'", requestURI, statusCode, message); + return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(statusCode, message); } - @ExceptionHandler(ParameterValidateException.class) - public SystemJsonResponse handleParameterValidateException(ParameterValidateException e, HttpServletRequest request) { - String requestURI = request.getRequestURI(); - log.error("请求地址'{}', 参数校验不通过'{}'", requestURI, e.getMessage()); - return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(PARAM_NOT_VALID, "参数校验不通过"); - } - - @ExceptionHandler(ShortLinkGenerateException.class) - public SystemJsonResponse handleShortLinkGenerateException(ShortLinkGenerateException e, HttpServletRequest request) { - String requestURI = request.getRequestURI(); - log.error("请求地址'{}', 短链生成失败'{}'", requestURI, e.getMessage()); - return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(SYSTEM_SERVICE_FAIL, "短链生成失败"); - } - - @ExceptionHandler(IllegalUrlException.class) - public SystemJsonResponse handleIllegalUrlException(IllegalUrlException e, HttpServletRequest request) { - String requestURI = request.getRequestURI(); - log.error("请求地址'{}', 非法的url'{}'", requestURI, e.getMessage()); - return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(SYSTEM_SERVICE_FAIL, "url非法"); + /** + * 自定义验证异常 + */ + @ExceptionHandler(ConstraintViolationException.class) + public SystemJsonResponse constraintViolationException(ConstraintViolationException e) { + log.error("自定义验证异常'{}'", e.getMessage()); + String message = e.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .filter(Objects::nonNull) + .collect(Collectors.joining("\n")); + return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(PARAM_FAILED_VALIDATE, message); } } diff --git a/src/main/java/com/achobeta/redis/RedisCache.java b/src/main/java/com/achobeta/redis/RedisCache.java new file mode 100644 index 00000000..9e6c8abc --- /dev/null +++ b/src/main/java/com/achobeta/redis/RedisCache.java @@ -0,0 +1,233 @@ +package com.achobeta.redis; + +import com.achobeta.redis.component.RedisBloomFilter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +@Slf4j +@SuppressWarnings(value = { "unchecked", "rawtypes" }) +public class RedisCache { + + private final RedisTemplate redisTemplate; + + private final RedisBloomFilter redisBloomFilter; + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) { + log.info("为 Redis 的键值设置超时时间\t[{}]-[{}s]", key, timeout / 1000L); + return redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS); + } + + /** + * 获得对象的剩余存活时间 + * @param key 键 + * @return 剩余存活时间 + */ + public long getKeyTTL(final String key) { + int ttl = Math.toIntExact(redisTemplate.opsForValue().getOperations().getExpire(key)); + String message = null; + switch (ttl) { + case -1: + message = "没有设置过期时间"; + break; + case -2: + message = "key不存在"; + break; + default: + message = ttl + "s"; + break; + } + log.info("查询 Redis key[{}] 剩余存活时间:{}", key, message); + return ttl * 1000L; // 统一单位为 ms + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) { + log.info("存入 Redis\t[{}]-[{}]", key, value); + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timout 超时时间 + */ + public void setCacheObject(final String key, final T value, final long timout) { + log.info("存入 Redis\t[{}]-[{}],超时时间:[{}s]", key, value, timout / 1000L); + redisTemplate.opsForValue().set(key, value, timout, TimeUnit.MILLISECONDS); + } + + /** + * 获取键值 + * @param key 键 + * @return 键对应的值,并封装成 Optional 对象 + * @param + */ + public Optional getCacheObject(final String key) { + T value = (T) redisTemplate.opsForValue().get(key); + log.info("查询 Redis\t[{}]-[{}]", key, value); + return Optional.ofNullable(value); + } + + /** + * 让指定 Redis 键值进行自减 + * @param key 键 + * @return 自减后的值 + */ + public long decrementCacheNumber(final String key) { + long number = redisTemplate.opsForValue().decrement(key); + log.info("Redis key[{}] 自减后:{}", key, number); + return number; + } + + /** + * 让指定 Redis 键值进行自增 + * @param key 键 + * @return 自增后的值 + */ + public long incrementCacheNumber(final String key) { + long number = redisTemplate.opsForValue().increment(key); + log.info("Redis key[{}] 自增后:{}", key, number); + return number; + } + + /** + * 加入布隆过滤器 + * @param bloomFilterName 隆过滤器的名字 + * @param key key 键 + */ + public void addToBloomFilter(final String bloomFilterName, final String key) { + log.info("加入布隆过滤器[{}]\tkey[{}]", bloomFilterName, key); + redisBloomFilter.add(bloomFilterName, key); + } + + /** + * 布隆过滤器是否存在该键值 + * @param bloomFilterName 布隆过滤器的名字 + * @param key 键 + * @return 键是否存在 + */ + public boolean containsInBloomFilter(final String bloomFilterName, final String key) { + boolean flag = redisBloomFilter.contains(bloomFilterName, key); + log.info("key[{}]\t是否存在于布隆过滤器[{}]:\t{}", key, bloomFilterName, flag); + return flag; + } + + /** + * 缓存Map + * + * @param key + * @param data + */ + public void setCacheMap(final String key, final Map data) { + if (Objects.nonNull(data)) { + log.info("Map 存入 Redis\t[{}]-[{}]", key, data); + redisTemplate.opsForHash().putAll(key, data); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Optional> getCacheMap(final String key) { + Map data = redisTemplate.opsForHash().entries(key); + data = data.size() == 0 ? null: data; + log.info("获取 Redis 中的 Map 缓存\t[{}]-[{}]", key, data); + return Optional.ofNullable(data); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hashKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hashKey, final T value) { + log.info("存入 Redis 的某个 Map\t[{}.{}]-[{}]", key, hashKey, value); + redisTemplate.opsForHash().put(key, hashKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hashKey Hash键 + * @return Hash中的对象 + */ + public Optional getCacheMapValue(final String key, final String hashKey) { + T value = (T) redisTemplate.opsForHash().get(key, hashKey); + log.info("获取 Redis 中的 Map 的键值\t[{}.{}]-[{}]", key, hashKey, value); + return Optional.ofNullable(value); + } + + /** + * 删除Hash中的数据 + * + * @param key + * @param hashKey + */ + public void delCacheMapValue(final String key, final String hashKey) { + log.info("删除 Redis 中的 Map 的键值\tkey[{}.{}]", key, hashKey); + redisTemplate.opsForHash().delete(key, hashKey); + } + + /** + * 让指定 HashMap 的键值进行自减 + * @param key HashMap的名字 + * @param hashKey HashMap的一个键 + * @return 自减后的值 + */ + public long decrementCacheMapNumber(final String key, final String hashKey) { + long number = redisTemplate.opsForHash().increment(key, hashKey, -1); + log.info("Redis key[{}.{}] 自减后:{}", key, hashKey, number); + return number; + } + + /** + * 让指定 HashMap 的键值进行自增 + * @param key HashMap的名字 + * @param hashKey HashMap的一个键 + * @return 自增后的值 + */ + public long incrementCacheMapNumber(final String key, final String hashKey) { + long number = redisTemplate.opsForHash().increment(key, hashKey, +1); + log.info("Redis key[{}.{}] 自增后:{}", key, hashKey, number); + return number; + } + + /** + * 删除单个对象 + * @param key + */ + public boolean deleteObject(final String key) { + log.info("删除 Redis 的键值\tkey[{}]", key); + return redisTemplate.delete(key); + } + +} \ No newline at end of file diff --git a/src/main/java/com/achobeta/domain/shortlink/component/BloomFilterHelper.java b/src/main/java/com/achobeta/redis/component/BloomFilterHelper.java similarity index 96% rename from src/main/java/com/achobeta/domain/shortlink/component/BloomFilterHelper.java rename to src/main/java/com/achobeta/redis/component/BloomFilterHelper.java index c0ed116c..1f4f8375 100644 --- a/src/main/java/com/achobeta/domain/shortlink/component/BloomFilterHelper.java +++ b/src/main/java/com/achobeta/redis/component/BloomFilterHelper.java @@ -1,10 +1,11 @@ -package com.achobeta.domain.shortlink.component; +package com.achobeta.redis.component; import com.google.common.hash.Funnel; import com.google.common.hash.Hashing; import com.google.common.hash.PrimitiveSink; import org.assertj.core.util.Preconditions; import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.stereotype.Component; /** * 算法过程: @@ -13,9 +14,9 @@ * 3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1 * 4. 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。 **/ +@Component public class BloomFilterHelper { - private int numHashFunctions; private int bitSize; @@ -54,7 +55,6 @@ public int[] murmurHashOffset(T value) { } offset[i - 1] = nextHash % bitSize; } - return offset; } diff --git a/src/main/java/com/achobeta/redis/component/FastJsonRedisSerializer.java b/src/main/java/com/achobeta/redis/component/FastJsonRedisSerializer.java new file mode 100644 index 00000000..913ad574 --- /dev/null +++ b/src/main/java/com/achobeta/redis/component/FastJsonRedisSerializer.java @@ -0,0 +1,52 @@ +package com.achobeta.redis.component; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.ParserConfig; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +import java.nio.charset.Charset; + +/* + Redis使用FastJson序列化 + */ +public class FastJsonRedisSerializer implements RedisSerializer { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private Class clazz; + + static { + ParserConfig.getGlobalInstance().setAutoTypeSupport(true); + } + + public FastJsonRedisSerializer(Class clazz) { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException { + if (t == null) { + return new byte[0]; + } + return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException { + if (bytes == null || bytes.length <= 0) { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + return JSON.parseObject(str, clazz); + } + + protected JavaType getJavaType(Class clazz) + { + return TypeFactory.defaultInstance().constructType(clazz); + } +} \ No newline at end of file diff --git a/src/main/java/com/achobeta/domain/shortlink/component/RedisBloomFilter.java b/src/main/java/com/achobeta/redis/component/RedisBloomFilter.java similarity index 66% rename from src/main/java/com/achobeta/domain/shortlink/component/RedisBloomFilter.java rename to src/main/java/com/achobeta/redis/component/RedisBloomFilter.java index 75127b0f..8dae3124 100644 --- a/src/main/java/com/achobeta/domain/shortlink/component/RedisBloomFilter.java +++ b/src/main/java/com/achobeta/redis/component/RedisBloomFilter.java @@ -1,4 +1,4 @@ -package com.achobeta.domain.shortlink.component; +package com.achobeta.redis.component; import lombok.RequiredArgsConstructor; import org.assertj.core.util.Preconditions; @@ -9,33 +9,29 @@ @RequiredArgsConstructor public class RedisBloomFilter { - private static final String BLOOM_FILTER_NAME = "LINK-CODE-LIST"; - - private final RedisTemplate redisTemplate; - private BloomFilterHelper bloomFilterHelper = new BloomFilterHelper(); + private final BloomFilterHelper bloomFilterHelper; /** * 根据给定的布隆过滤器添加值 */ - public void add(T value) { + public void add(String bloomFilterName, T value) { Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空"); int[] offset = bloomFilterHelper.murmurHashOffset(value); for (int i : offset) { - // todo: 设置超时时间 - redisTemplate.opsForValue().setBit(BLOOM_FILTER_NAME, i, true); + redisTemplate.opsForValue().setBit(bloomFilterName, i, true); } } /** * 根据给定的布隆过滤器判断值是否存在 */ - public boolean contains(T value) { + public boolean contains(String bloomFilterName, T value) { Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空"); int[] offset = bloomFilterHelper.murmurHashOffset(value); for (int i : offset) { - if (!redisTemplate.opsForValue().getBit(BLOOM_FILTER_NAME, i)) { + if (!redisTemplate.opsForValue().getBit(bloomFilterName, i)) { return false; } } diff --git a/src/main/java/com/achobeta/redis/config/RedisConfig.java b/src/main/java/com/achobeta/redis/config/RedisConfig.java new file mode 100644 index 00000000..639a82bd --- /dev/null +++ b/src/main/java/com/achobeta/redis/config/RedisConfig.java @@ -0,0 +1,29 @@ +package com.achobeta.redis.config; + +import com.achobeta.redis.component.FastJsonRedisSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(connectionFactory); + FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + template.afterPropertiesSet(); + return template; + } +} \ No newline at end of file diff --git a/src/main/java/com/achobeta/domain/excel/util/ExcelUtil.java b/src/main/java/com/achobeta/util/ExcelUtil.java similarity index 97% rename from src/main/java/com/achobeta/domain/excel/util/ExcelUtil.java rename to src/main/java/com/achobeta/util/ExcelUtil.java index 08161362..c283be1c 100644 --- a/src/main/java/com/achobeta/domain/excel/util/ExcelUtil.java +++ b/src/main/java/com/achobeta/util/ExcelUtil.java @@ -1,8 +1,7 @@ -package com.achobeta.domain.excel.util; +package com.achobeta.util; import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; -import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Workbook; @@ -17,20 +16,6 @@ public class ExcelUtil { private static final String DEFAULT_SUFFIX = ".xlsx"; - private static String tryCreateFile(String filePath, String fileName) throws IOException { - // 文件夹是否存在,若没有对应文件夹直接根据路径生成文件会报错 - File directory = new File(filePath); - if (!directory.exists() && !directory.isDirectory()) { - directory.mkdirs(); - } - // 文件是否存在 - String path = filePath + fileName + DEFAULT_SUFFIX; - File file = new File(path); - if (!file.exists()){ - file.createNewFile(); - } - return path; - } /** * 打印表格 * @param title 表格标题 @@ -63,4 +48,20 @@ public static void exportXlsxFile(String title, String sheetName, log.warn(e.getMessage()); } } + + private static String tryCreateFile(String filePath, String fileName) throws IOException { + // 文件夹是否存在,若没有对应文件夹直接根据路径生成文件会报错 + File directory = new File(filePath); + if (!directory.exists() && !directory.isDirectory()) { + directory.mkdirs(); + } + // 文件是否存在 + String path = filePath + fileName + DEFAULT_SUFFIX; + File file = new File(path); + if (!file.exists()){ + file.createNewFile(); + } + return path; + } + } diff --git a/src/main/java/com/achobeta/util/ValidatorUtils.java b/src/main/java/com/achobeta/util/ValidatorUtils.java new file mode 100644 index 00000000..2ad95aaf --- /dev/null +++ b/src/main/java/com/achobeta/util/ValidatorUtils.java @@ -0,0 +1,21 @@ +package com.achobeta.util; + +import cn.hutool.extra.spring.SpringUtil; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +import java.util.Set; + +public class ValidatorUtils { + + private static final Validator validator = SpringUtil.getBean(Validator.class); + + public static void validate(T object, Class... groups) { + Set> validate = validator.validate(object, groups); + if (!validate.isEmpty()) { + throw new ConstraintViolationException("参数校验异常", validate); + } + } + +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml deleted file mode 100644 index 7b93b919..00000000 --- a/src/main/resources/application-dev.yml +++ /dev/null @@ -1,21 +0,0 @@ -ab: - db: - host: 127.0.0.1 - port: 3306 - database: achobeta_recruitment - username: root - password: 123456 - -server: - port: 9001 - ---- # 数据源配置 -spring: - datasource: - url: jdbc:mysql://${ab.db.host}:${ab.db.port}/${ab.db.database}?serverTimezone=UTC&useSSL=false&characterEncoding=UTF8&allowPublicKeyRetrieval=true - username: ${ab.db.username} - password: ${ab.db.password} - type: com.zaxxer.hikari.HikariDataSource - hikari: - driver-class-name: com.mysql.cj.jdbc.Driver - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a40e0217..324757ff 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,8 +3,6 @@ spring: name: achobeta-recruitment profiles: active: @profiles.active@ - mail: - host: smtp.yeah.net # 日志配置 logging: level: diff --git a/src/main/resources/templates/identifying-code-model.html b/src/main/resources/templates/identifying-code-model.html new file mode 100644 index 00000000..7b106c63 --- /dev/null +++ b/src/main/resources/templates/identifying-code-model.html @@ -0,0 +1,23 @@ + + + + + + 邮箱验证 + + + +  感谢您参与AchoBeta实验室的招新!下面是您的身份验证码: + +
+ +   + +
+  请在  + + + +  分钟内完成验证,如非本人操作,请忽略! + +