From dfccd8cafa5706d1ec72d83ae55c2cdcef83c9fb Mon Sep 17 00:00:00 2001 From: lv Date: Tue, 19 Aug 2025 20:09:15 +0800 Subject: [PATCH] ss --- .idea/.gitignore | 8 ++ .idea/compiler.xml | 19 +++ .idea/encodings.xml | 6 + .idea/jarRepositories.xml | 20 +++ .idea/misc.xml | 12 ++ .idea/vcs.xml | 6 + logs/mail-service-error.log | 0 logs/mail-service.log | 16 +++ mail-service.iml | 119 ++++++++++++++++++ pom.xml | 100 +++++++++++++++ .../mailservice/MailServiceApplication.java | 19 +++ .../example/mailservice/config/LogConfig.java | 29 +++++ .../mailservice/config/MailConfig.java | 69 ++++++++++ .../mailservice/config/RedisConfig.java | 35 ++++++ .../example/mailservice/config/WebConfig.java | 25 ++++ .../controller/MailController.java | 71 +++++++++++ .../example/mailservice/dto/ApiResponse.java | 43 +++++++ .../example/mailservice/dto/MailRequest.java | 40 ++++++ .../mailservice/entity/MailSenderRecord.java | 51 ++++++++ .../exception/EmailErrException.java | 43 +++++++ .../exception/GlobalExceptionHandler.java | 44 +++++++ .../mapper/MailSenderRecordMapper.java | 19 +++ .../mailservice/service/MailLimitService.java | 98 +++++++++++++++ .../service/MailSenderRecordService.java | 54 ++++++++ .../mailservice/service/MailService.java | 70 +++++++++++ .../service/impl/MailServiceImpl.java | 77 ++++++++++++ src/main/resources/application.yml | 83 ++++++++++++ src/main/resources/logback-spring.xml | 89 +++++++++++++ .../mapper/MailSenderRecordMapper.xml | 26 ++++ 29 files changed, 1291 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 logs/mail-service-error.log create mode 100644 logs/mail-service.log create mode 100644 mail-service.iml create mode 100644 pom.xml create mode 100644 src/main/java/com/example/mailservice/MailServiceApplication.java create mode 100644 src/main/java/com/example/mailservice/config/LogConfig.java create mode 100644 src/main/java/com/example/mailservice/config/MailConfig.java create mode 100644 src/main/java/com/example/mailservice/config/RedisConfig.java create mode 100644 src/main/java/com/example/mailservice/config/WebConfig.java create mode 100644 src/main/java/com/example/mailservice/controller/MailController.java create mode 100644 src/main/java/com/example/mailservice/dto/ApiResponse.java create mode 100644 src/main/java/com/example/mailservice/dto/MailRequest.java create mode 100644 src/main/java/com/example/mailservice/entity/MailSenderRecord.java create mode 100644 src/main/java/com/example/mailservice/exception/EmailErrException.java create mode 100644 src/main/java/com/example/mailservice/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/mailservice/mapper/MailSenderRecordMapper.java create mode 100644 src/main/java/com/example/mailservice/service/MailLimitService.java create mode 100644 src/main/java/com/example/mailservice/service/MailSenderRecordService.java create mode 100644 src/main/java/com/example/mailservice/service/MailService.java create mode 100644 src/main/java/com/example/mailservice/service/impl/MailServiceImpl.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/main/resources/mapper/MailSenderRecordMapper.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..267976f --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..88d462c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/logs/mail-service-error.log b/logs/mail-service-error.log new file mode 100644 index 0000000..e69de29 diff --git a/logs/mail-service.log b/logs/mail-service.log new file mode 100644 index 0000000..47cec92 --- /dev/null +++ b/logs/mail-service.log @@ -0,0 +1,16 @@ +2025-08-16 11:15:26.668 [main] INFO com.example.mailservice.MailServiceApplication - Starting MailServiceApplication using Java 1.8.0_452 on kusoul with PID 23664 (F:\work\company-backend\target\classes started by Administrator in F:\work\company-backend) +2025-08-16 11:15:26.669 [main] DEBUG com.example.mailservice.MailServiceApplication - Running with Spring Boot v2.7.5, Spring v5.3.23 +2025-08-16 11:15:26.669 [main] INFO com.example.mailservice.MailServiceApplication - No active profile set, falling back to 1 default profile: "default" +2025-08-16 11:15:26.678 [background-preinit] INFO org.hibernate.validator.internal.util.Version - HV000001: Hibernate Validator 6.2.5.Final +2025-08-16 11:15:27.743 [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode +2025-08-16 11:15:27.745 [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. +2025-08-16 11:15:27.770 [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces. +2025-08-16 11:15:28.374 [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8180 (http) +2025-08-16 11:15:28.387 [main] INFO org.apache.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8180"] +2025-08-16 11:15:28.387 [main] INFO org.apache.catalina.core.StandardService - Starting service [Tomcat] +2025-08-16 11:15:28.388 [main] INFO org.apache.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.68] +2025-08-16 11:15:28.580 [main] INFO o.a.c.core.ContainerBase.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext +2025-08-16 11:15:28.580 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1852 ms +2025-08-16 11:15:29.703 [main] INFO org.apache.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8180"] +2025-08-16 11:15:29.731 [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer - Tomcat started on port(s): 8180 (http) with context path '' +2025-08-16 11:15:29.740 [main] INFO com.example.mailservice.MailServiceApplication - Started MailServiceApplication in 3.738 seconds (JVM running for 7.169) diff --git a/mail-service.iml b/mail-service.iml new file mode 100644 index 0000000..6aa00e9 --- /dev/null +++ b/mail-service.iml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..cda7a04 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.5 + + + com.example + mail-service + 0.0.1-SNAPSHOT + mail-service + Spring Boot Mail Service with QQ Mail and Redis Limit + + + 1.8 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.projectlombok + lombok + true + + + + + + org.postgresql + postgresql + runtime + + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.2.2 + + + + + + + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/src/main/java/com/example/mailservice/MailServiceApplication.java b/src/main/java/com/example/mailservice/MailServiceApplication.java new file mode 100644 index 0000000..2c5abcb --- /dev/null +++ b/src/main/java/com/example/mailservice/MailServiceApplication.java @@ -0,0 +1,19 @@ +package com.example.mailservice; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 邮件服务应用主类 + */ +@SpringBootApplication +// 扫描所有Mapper接口所在的包 +@MapperScan("com.example.mailservice.mapper") +public class MailServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(MailServiceApplication.class, args); + } + +} diff --git a/src/main/java/com/example/mailservice/config/LogConfig.java b/src/main/java/com/example/mailservice/config/LogConfig.java new file mode 100644 index 0000000..649da00 --- /dev/null +++ b/src/main/java/com/example/mailservice/config/LogConfig.java @@ -0,0 +1,29 @@ +package com.example.mailservice.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 日志配置类 + */ +@Configuration +public class LogConfig { + + /** + * 邮件服务专用日志 + */ + @Bean + public Logger mailServiceLogger() { + return LoggerFactory.getLogger("MailServiceLogger"); + } + + /** + * 限制服务专用日志 + */ + @Bean + public Logger limitServiceLogger() { + return LoggerFactory.getLogger("LimitServiceLogger"); + } +} diff --git a/src/main/java/com/example/mailservice/config/MailConfig.java b/src/main/java/com/example/mailservice/config/MailConfig.java new file mode 100644 index 0000000..789dfab --- /dev/null +++ b/src/main/java/com/example/mailservice/config/MailConfig.java @@ -0,0 +1,69 @@ +package com.example.mailservice.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class MailConfig { + + @Value("${spring.mail.host}") + private String host; + + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.username}") + private String username; + + @Value("${spring.mail.password}") + private String password; + + @Value("${spring.mail.protocol:smtp}") + private String protocol; + + @Value("${spring.mail.properties.mail.smtp.auth:true}") + private boolean auth; + + @Value("${spring.mail.properties.mail.smtp.starttls.enable:true}") + private boolean starttlsEnable; + + @Value("${spring.mail.properties.mail.smtp.starttls.required:true}") + private boolean starttlsRequired; + + @Value("${spring.mail.properties.mail.debug:false}") + private boolean debug; + + /** + * 注册JavaMailSender bean + */ + @Bean + public JavaMailSender mailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + mailSender.setProtocol(protocol); + + // 配置邮件发送属性 + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.smtp.auth", auth); + props.put("mail.smtp.starttls.enable", starttlsEnable); + props.put("mail.smtp.starttls.required", starttlsRequired); + props.put("mail.debug", debug); + // 设置超时时间,避免长时间阻塞 + props.put("mail.smtp.connectiontimeout", 5000); + props.put("mail.smtp.timeout", 5000); + props.put("mail.smtp.writetimeout", 5000); + + return mailSender; + } + + + +} diff --git a/src/main/java/com/example/mailservice/config/RedisConfig.java b/src/main/java/com/example/mailservice/config/RedisConfig.java new file mode 100644 index 0000000..4d646ac --- /dev/null +++ b/src/main/java/com/example/mailservice/config/RedisConfig.java @@ -0,0 +1,35 @@ +package com.example.mailservice.config; + +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.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置类 + */ +@Configuration +public class RedisConfig { + + /** + * 配置RedisTemplate + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(factory); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + // 使用GenericJackson2JsonRedisSerializer来序列化和反序列化redis的value值 + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/java/com/example/mailservice/config/WebConfig.java b/src/main/java/com/example/mailservice/config/WebConfig.java new file mode 100644 index 0000000..910e3c9 --- /dev/null +++ b/src/main/java/com/example/mailservice/config/WebConfig.java @@ -0,0 +1,25 @@ +package com.example.mailservice.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + // 1. 配置允许跨域的路径:/** 表示所有接口(可改为特定路径,如 /api/**) + registry.addMapping("/**") + // 2. 允许的来源:* 表示所有前端域名 + .allowedOrigins("*") + // 3. 允许的请求方法:所有 HTTP 方法(GET/POST/PUT/DELETE 等) + .allowedMethods("*") + // 4. 允许的请求头:* 表示所有请求头(包括自定义头) + .allowedHeaders("*") + // 5. 允许携带凭证(可选,默认 false;若前端需传 Cookie/Token,需设为 true,但此时 allowedOrigins 不能为 *,下文有说明) + .allowCredentials(false) + // 6. 预检请求(OPTIONS)的缓存时间:3600 秒(1 小时,避免频繁发送预检请求) + .maxAge(3600); + } +} diff --git a/src/main/java/com/example/mailservice/controller/MailController.java b/src/main/java/com/example/mailservice/controller/MailController.java new file mode 100644 index 0000000..ecc1722 --- /dev/null +++ b/src/main/java/com/example/mailservice/controller/MailController.java @@ -0,0 +1,71 @@ +package com.example.mailservice.controller; + +import com.example.mailservice.dto.ApiResponse; +import com.example.mailservice.dto.MailRequest; +import com.example.mailservice.service.MailLimitService; +import com.example.mailservice.service.MailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * 邮件发送控制器 + */ +@RestController +@RequestMapping("/api/mail") +public class MailController { + + @Autowired + private MailService mailService; + + @Autowired + private MailLimitService mailLimitService; + + /** + * 发送邮件接口 + */ + @PostMapping("/send") + public ApiResponse sendMail(@Validated @RequestBody MailRequest mailRequest, HttpServletRequest request) { + // 获取客户端IP地址 + String ipAddress = getClientIpAddress(request); + + // 检查是否达到发送限制 + if (!mailLimitService.canSendMail(ipAddress)) { + return ApiResponse.fail(403, "您的IP今日发送邮件已达上限,请明天再试"); + } + + // 发送邮件 + boolean sendSuccess = mailService.sendSimpleMail( + mailRequest.getName(), + mailRequest.getEmail(), + mailRequest.getSubject(), + mailRequest.getMessage() + ); + + if (sendSuccess) { + // 增加发送计数 + long count = mailLimitService.incrementSendCount(ipAddress); + int remaining = mailLimitService.getRemainingCount(ipAddress); + return ApiResponse.success("邮件发送成功,今日剩余发送次数: " + remaining); + } else { + return ApiResponse.fail(500, "邮件发送失败,请稍后重试"); + } + } + + /** + * 获取客户端IP地址 + */ + private String getClientIpAddress(HttpServletRequest request) { + String xForwardedForHeader = request.getHeader("X-Forwarded-For"); + if (xForwardedForHeader != null) { + // X-Forwarded-For: client, proxy1, proxy2 + return xForwardedForHeader.split(",")[0].trim(); + } + return request.getRemoteAddr(); + } +} diff --git a/src/main/java/com/example/mailservice/dto/ApiResponse.java b/src/main/java/com/example/mailservice/dto/ApiResponse.java new file mode 100644 index 0000000..75a4f49 --- /dev/null +++ b/src/main/java/com/example/mailservice/dto/ApiResponse.java @@ -0,0 +1,43 @@ +package com.example.mailservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * API响应DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApiResponse { + + /** + * 响应状态码:200表示成功,其他表示失败 + */ + private int code; + + /** + * 响应消息 + */ + private String message; + + /** + * 响应数据 + */ + private Object data; + + /** + * 创建成功响应 + */ + public static ApiResponse success(Object data) { + return new ApiResponse(200, "操作成功", data); + } + + /** + * 创建失败响应 + */ + public static ApiResponse fail(int code, String message) { + return new ApiResponse(code, message, null); + } +} diff --git a/src/main/java/com/example/mailservice/dto/MailRequest.java b/src/main/java/com/example/mailservice/dto/MailRequest.java new file mode 100644 index 0000000..ed76f39 --- /dev/null +++ b/src/main/java/com/example/mailservice/dto/MailRequest.java @@ -0,0 +1,40 @@ +package com.example.mailservice.dto; + +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + +/** + * 邮件请求DTO + */ +@Data +public class MailRequest { + + /** + * 发件人邮箱 + */ + @NotBlank(message = "发件人邮箱不能为空") + @Email(message = "邮箱格式不正确") + private String email; + + + /** + * 发件人 + */ + @NotBlank(message = "发件人不能为空") + private String name; + + + /** + * 邮件主题 + */ + @NotBlank(message = "邮件主题不能为空") + private String subject; + + /** + * 邮件内容 + */ + @NotBlank(message = "邮件内容不能为空") + private String message; +} diff --git a/src/main/java/com/example/mailservice/entity/MailSenderRecord.java b/src/main/java/com/example/mailservice/entity/MailSenderRecord.java new file mode 100644 index 0000000..57bb769 --- /dev/null +++ b/src/main/java/com/example/mailservice/entity/MailSenderRecord.java @@ -0,0 +1,51 @@ +package com.example.mailservice.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 邮件发送记录实体类 + */ +@Data +public class MailSenderRecord { + /** + * 主键ID + */ + private Long id; + + /** + * 发送者邮箱 + */ + private String senderEmail; + + /** + * 发送者IP地址 + */ + private String senderIp; + + /** + * 发送时间 + */ + private LocalDateTime sendTime; + + /** + * 接收者邮箱 + */ + private String recipientEmail; + + /** + * 邮件主题 + */ + private String subject; + + /** + * 发送状态:1-成功,0-失败 + */ + private Integer sendStatus; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; +} + \ No newline at end of file diff --git a/src/main/java/com/example/mailservice/exception/EmailErrException.java b/src/main/java/com/example/mailservice/exception/EmailErrException.java new file mode 100644 index 0000000..2d4aad7 --- /dev/null +++ b/src/main/java/com/example/mailservice/exception/EmailErrException.java @@ -0,0 +1,43 @@ +package com.example.mailservice.exception; + + + +/** + * @auther kuku + * @description 自定义API异常 + * @date 2024/1/17 + * @github https://github.com/macrozheng + */ +public class EmailErrException extends RuntimeException { + private IErrorCode errorCode; + + public EmailErrException(IErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public EmailErrException(String message) { + super(message); + } + + public EmailErrException(Throwable cause) { + super(cause); + } + + public EmailErrException(String message, Throwable cause) { + super(message, cause); + } + + public IErrorCode getErrorCode() { + return errorCode; + } + + public interface IErrorCode { + long getCode(); + + String getMessage(); + } + +} + + diff --git a/src/main/java/com/example/mailservice/exception/GlobalExceptionHandler.java b/src/main/java/com/example/mailservice/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f4026ab --- /dev/null +++ b/src/main/java/com/example/mailservice/exception/GlobalExceptionHandler.java @@ -0,0 +1,44 @@ +package com.example.mailservice.exception; + +import com.example.mailservice.dto.ApiResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理器 + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 处理参数验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ApiResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + BindingResult bindingResult = e.getBindingResult(); + StringBuilder errorMsg = new StringBuilder(); + + for (FieldError fieldError : bindingResult.getFieldErrors()) { + errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(";"); + } + + logger.warn("参数验证失败: {}", errorMsg.toString()); + return ApiResponse.fail(400, errorMsg.toString()); + } + + /** + * 处理其他所有异常 + */ + @ExceptionHandler(Exception.class) + public ApiResponse handleException(Exception e) { + logger.error("系统异常: {}", e.getMessage(), e); + return ApiResponse.fail(500, "系统异常,请联系管理员"); + } +} diff --git a/src/main/java/com/example/mailservice/mapper/MailSenderRecordMapper.java b/src/main/java/com/example/mailservice/mapper/MailSenderRecordMapper.java new file mode 100644 index 0000000..f9fd9ee --- /dev/null +++ b/src/main/java/com/example/mailservice/mapper/MailSenderRecordMapper.java @@ -0,0 +1,19 @@ +package com.example.mailservice.mapper; + +import com.example.mailservice.entity.MailSenderRecord; +import org.apache.ibatis.annotations.Mapper; + +/** + * 邮件发送记录Mapper接口 + */ +@Mapper +public interface MailSenderRecordMapper { + + /** + * 插入邮件发送记录 + * @param record 邮件发送记录 + * @return 影响行数 + */ + int insert(MailSenderRecord record); +} + \ No newline at end of file diff --git a/src/main/java/com/example/mailservice/service/MailLimitService.java b/src/main/java/com/example/mailservice/service/MailLimitService.java new file mode 100644 index 0000000..69003c6 --- /dev/null +++ b/src/main/java/com/example/mailservice/service/MailLimitService.java @@ -0,0 +1,98 @@ +package com.example.mailservice.service; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +/** + * 邮件发送限制服务,用于控制每个IP的每日发送次数 + */ +@Service +@Slf4j +public class MailLimitService { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private Logger limitServiceLogger; + + /** + * Redis中存储IP发送计数的键前缀 + */ + @Value("${mail.redis-key-prefix}") + private String redisKeyPrefix; + + /** + * Redis中IP计数的过期时间(单位:秒) + */ + @Value("${mail.redis-expire-seconds}") + private long redisExpireSeconds; + + /** + * 每日每IP最大发送邮件数量限制 + */ + @Value("${mail.daily-limit}") + private int dailyLimit; + + /** + * 检查IP是否可以发送邮件 + * + * @param ipAddress 客户端IP地址 + * @return 可以发送返回true,否则返回false + */ + public boolean canSendMail(String ipAddress) { + String key = redisKeyPrefix + ipAddress; + + // 获取当前IP的发送次数 + Object countObj = redisTemplate.opsForValue().get(key); + Integer count = countObj != null ? Integer.parseInt(countObj.toString()) : 0; + + // 检查是否超过限制 + if (count >= dailyLimit) { + limitServiceLogger.warn("IP: {} 已达到每日发送上限: {}封", ipAddress, dailyLimit); + return false; + } + + return true; + } + + /** + * 增加IP的发送计数 + * + * @param ipAddress 客户端IP地址 + * @return 增加后的计数 + */ + public long incrementSendCount(String ipAddress) { + String key = redisKeyPrefix + ipAddress; + + // 自增计数,如果是新key则设置过期时间 + Long count = redisTemplate.opsForValue().increment(key); + if (count != null && count == 1) { + redisTemplate.expire(key, redisExpireSeconds, TimeUnit.SECONDS); + } + + limitServiceLogger.info("IP: {} 发送计数增加到: {}次", ipAddress, count); + return count != null ? count : 0; + } + + /** + * 获取IP的剩余发送次数 + * + * @param ipAddress 客户端IP地址 + * @return 剩余发送次数 + */ + public int getRemainingCount(String ipAddress) { + String key = redisKeyPrefix + ipAddress; + + Object countObj = redisTemplate.opsForValue().get(key); + Integer count = countObj != null ? Integer.parseInt(countObj.toString()) : 0; + + return dailyLimit - count; + } +} diff --git a/src/main/java/com/example/mailservice/service/MailSenderRecordService.java b/src/main/java/com/example/mailservice/service/MailSenderRecordService.java new file mode 100644 index 0000000..646e353 --- /dev/null +++ b/src/main/java/com/example/mailservice/service/MailSenderRecordService.java @@ -0,0 +1,54 @@ +package com.example.mailservice.service; + +import com.example.mailservice.entity.MailSenderRecord; +import com.example.mailservice.mapper.MailSenderRecordMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 邮件发送记录服务类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MailSenderRecordService { + + private final MailSenderRecordMapper mailSenderRecordMapper; + + /** + * 保存邮件发送记录 + * @param senderEmail 发送者邮箱 + * @param senderIp 发送者IP + * @param recipientEmail 接收者邮箱 + * @param subject 邮件主题 + * @param sendStatus 发送状态:1-成功,0-失败 + */ + @Transactional(rollbackFor = Exception.class) + public void saveMailRecord(String senderEmail, String senderIp, String recipientEmail, String subject, Integer sendStatus) { + try { + MailSenderRecord record = new MailSenderRecord(); + record.setSenderEmail(senderEmail); + record.setSenderIp(senderIp); + record.setRecipientEmail(recipientEmail); + record.setSubject(subject); + record.setSendStatus(sendStatus); + record.setSendTime(LocalDateTime.now()); + record.setCreatedAt(LocalDateTime.now()); + + int rows = mailSenderRecordMapper.insert(record); + if (rows > 0) { + log.debug("邮件发送记录保存成功,ID: {}", record.getId()); + } else { + log.warn("邮件发送记录保存失败"); + } + } catch (Exception e) { + log.error("保存邮件发送记录异常", e); + // 记录保存失败不影响主流程,仅记录日志 + } + } +} + \ No newline at end of file diff --git a/src/main/java/com/example/mailservice/service/MailService.java b/src/main/java/com/example/mailservice/service/MailService.java new file mode 100644 index 0000000..ce03a15 --- /dev/null +++ b/src/main/java/com/example/mailservice/service/MailService.java @@ -0,0 +1,70 @@ +package com.example.mailservice.service; + +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.MailException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +/** + * 邮件发送服务 + */ +@Service +public class MailService { + + @Autowired + private JavaMailSender mailSender; + + @Autowired + private Logger mailServiceLogger; + + /** + * 发送者邮箱地址指定是用来发送邮件的服务器地址 + */ + @Value("${spring.mail.username}") + private String from; + + + /** + * 接收者邮箱地址 值得是 官网固定用来接收的地址 + */ + @Value("${spring.mail.receive-mail}") + private String receiveMail; + + + /** + * 发送简单文本邮件 + * + * @param to 收件人邮箱 + * @param subject 邮件主题 + * @param content 邮件内容 + * @return 发送成功返回true,否则返回false + */ + public boolean sendSimpleMail(String userName, String to, String subject, String content) { + try { + // 创建简单邮件消息 + SimpleMailMessage message = new SimpleMailMessage(); + // 设置发送者 + message.setFrom(from); + // 设置收件人 + message.setTo(receiveMail); + // 设置邮件主题 + message.setSubject(subject); + String contents = String.format("客户姓名: %s 客户邮箱: %s \r\n %s", userName, to, content); + // 设置邮件内容 + message.setText(contents); + + // 发送邮件 + mailSender.send(message); + + mailServiceLogger.info("邮件发送成功,收件人: {}, 主题: {}", to, subject); + return true; + } catch (MailException e) { + mailServiceLogger.error("邮件发送失败,收件人: {}, 主题: {}, 错误信息: {}", + to, subject, e.getMessage(), e); + return false; + } + } +} diff --git a/src/main/java/com/example/mailservice/service/impl/MailServiceImpl.java b/src/main/java/com/example/mailservice/service/impl/MailServiceImpl.java new file mode 100644 index 0000000..b490b04 --- /dev/null +++ b/src/main/java/com/example/mailservice/service/impl/MailServiceImpl.java @@ -0,0 +1,77 @@ +package com.example.mailservice.service.impl; + +import com.example.mailservice.dto.MailRequest; +import com.example.mailservice.exception.EmailErrException; +import com.example.mailservice.service.MailLimitService; +import com.example.mailservice.service.MailSenderRecordService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.MailException; +import org.springframework.mail.MailSendException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +/** + * 邮件服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MailServiceImpl { + + private final JavaMailSender mailSender; + private final MailLimitService mailLimitService; + private final MailSenderRecordService mailSenderRecordService; + + @Value("${spring.mail.username}") + private String fromEmail; + + @Value("${mail.daily-limit}") + private int dailyMaxCount = 10; + + /** + * 发送邮件 + * @param request 邮件请求参数 + * @param clientIp 客户端IP地址 + */ + public void sendMail(MailRequest request, String clientIp) throws MailException { + // 检查发送限制 + boolean canSend = mailLimitService.canSendMail(clientIp); + if (!canSend) { + // log.warn("IP: {} 达到每日发送上限: {}", clientIp, dailyMaxCount); + throw new EmailErrException( "前IP今日邮件发送已达上限(10封),请明天再试" +// String.format("当前IP今日邮件发送已达上限(%d封),请明天再试", dailyMaxCount) + ); + } + + // 构建邮件消息 + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(fromEmail); + message.setTo(request.getEmail()); + message.setSubject(request.getSubject()); + message.setText(request.getMessage()); + + boolean sendSuccess = false; + + try { + // 发送邮件 + mailSender.send(message); + sendSuccess = true; + log.info("邮件发送成功,从 {} 到 {},主题: {}", fromEmail, request.getEmail(), request.getSubject()); + } catch (MailException e) { + log.error("邮件发送失败,从 {} 到 {},错误: {}", fromEmail, request.getEmail(), e.getMessage(), e); + throw new MailSendException("邮件发送失败: " + e.getMessage(), e); + } finally { + // 保存发送记录,无论成功失败都记录 + mailSenderRecordService.saveMailRecord( + fromEmail, + clientIp, + request.getEmail(), + request.getSubject(), + sendSuccess ? 1 : 0 + ); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..72d0948 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,83 @@ +# 服务器端口配置 +server: + port: 8180 + +spring: + # 数据源配置 + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/maildb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: postgres + password: 123456 + # Hikari连接池配置 + hikari: + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 300000 + connection-timeout: 20000 + max-lifetime: 1800000 + connection-test-query: SELECT 1 + + # Redis配置 + redis: + host: localhost + port: 6379 + password: + database: 0 + timeout: 2000 + lettuce: + pool: + max-active: 8 + max-wait: -1 + max-idle: 8 + min-idle: 0 + + # 邮件配置 + mail: + host: smtp.qq.com + port: 587 + username: 2187434671@qq.com + password: rvujtnksccaudjjf + default-encoding: UTF-8 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + debug: false + receive-mail: 2187434671@qq.com +# MyBatis配置 +mybatis: + mapper-locations: classpath:mapper/*.xml + type-aliases-package: com.example.mailservice.entity + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + + + +# 日志配置 +logging: + config: classpath:logback-spring.xml + file: + name: logs/mail-service.log + level: + root: INFO + com.example.mailservice: DEBUG + org.springframework.web: INFO + org.springframework.mail: INFO + org.mybatis: INFO + com.zaxxer.hikari: INFO + org.postgresql: INFO + redis.clients: INFO + +# 自定义邮件发送配置 +mail: + # 每日每IP最大发送邮件数量限制 + daily-limit: 10 + # Redis中存储IP发送计数的键前缀 + redis-key-prefix: "mail:send:count:" + # Redis中IP计数的过期时间(单位:秒),设置为24小时 + redis-expire-seconds: 86400 diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..d3a6e07 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,89 @@ + + + + mail-service + + + + + + + + + + + + ${LOG_PATTERN} + UTF-8 + + + + + + + ${LOG_PATH}/${LOG_FILE_NAME}.log + + + + + ${LOG_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.log + + 30 + + 1GB + + + + + ${LOG_PATTERN} + UTF-8 + + + + + + + + ERROR + ACCEPT + DENY + + + + ${LOG_PATH}/${LOG_FILE_NAME}-error.log + + + + ${LOG_PATH}/${LOG_FILE_NAME}-error-%d{yyyy-MM-dd}.log + 30 + + + + + ${LOG_PATTERN} + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/MailSenderRecordMapper.xml b/src/main/resources/mapper/MailSenderRecordMapper.xml new file mode 100644 index 0000000..8613b64 --- /dev/null +++ b/src/main/resources/mapper/MailSenderRecordMapper.xml @@ -0,0 +1,26 @@ + + + + + + + INSERT INTO mail_sender_record ( + sender_email, + sender_ip, + send_time, + recipient_email, + subject, + send_status, + created_at + ) VALUES ( + #{senderEmail,jdbcType=VARCHAR}, + #{senderIp,jdbcType=VARCHAR}, + #{sendTime,jdbcType=TIMESTAMP}, + #{recipientEmail,jdbcType=VARCHAR}, + #{subject,jdbcType=VARCHAR}, + #{sendStatus,jdbcType=INTEGER}, + #{createdAt,jdbcType=TIMESTAMP} + ) + + + \ No newline at end of file