This commit is contained in:
lv 2025-08-19 20:09:15 +08:00
commit dfccd8cafa
29 changed files with 1291 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -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

19
.idea/compiler.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="mail-service" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="mail-service" options="-parameters" />
</option>
</component>
</project>

6
.idea/encodings.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

20
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

12
.idea/misc.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="corretto-1.8" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

16
logs/mail-service.log Normal file
View File

@ -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)

119
mail-service.iml Normal file
View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="Spring" name="Spring">
<configuration />
</facet>
<facet type="web" name="Web">
<configuration>
<webroots />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-web:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-logging:2.7.5" level="project" />
<orderEntry type="library" name="Maven: ch.qos.logback:logback-classic:1.2.11" level="project" />
<orderEntry type="library" name="Maven: ch.qos.logback:logback-core:1.2.11" level="project" />
<orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-to-slf4j:2.17.2" level="project" />
<orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-api:2.17.2" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:jul-to-slf4j:1.7.36" level="project" />
<orderEntry type="library" name="Maven: jakarta.annotation:jakarta.annotation-api:1.3.5" level="project" />
<orderEntry type="library" name="Maven: org.yaml:snakeyaml:1.30" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-json:2.7.5" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.13.4.2" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.13.4" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-core:2.13.4" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.4" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-tomcat:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-core:9.0.68" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-websocket:9.0.68" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-web:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-beans:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-webmvc:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-aop:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-expression:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-mail:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context-support:5.3.23" level="project" />
<orderEntry type="library" name="Maven: com.sun.mail:jakarta.mail:1.6.7" level="project" />
<orderEntry type="library" name="Maven: com.sun.activation:jakarta.activation:1.2.2" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-data-redis:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework.data:spring-data-redis:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework.data:spring-data-keyvalue:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework.data:spring-data-commons:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-tx:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-oxm:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.36" level="project" />
<orderEntry type="library" name="Maven: io.lettuce:lettuce-core:6.1.10.RELEASE" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-common:4.1.84.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-handler:4.1.84.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-resolver:4.1.84.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-buffer:4.1.84.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-transport-native-unix-common:4.1.84.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-codec:4.1.84.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-transport:4.1.84.Final" level="project" />
<orderEntry type="library" name="Maven: io.projectreactor:reactor-core:3.4.24" level="project" />
<orderEntry type="library" name="Maven: org.reactivestreams:reactive-streams:1.0.4" level="project" />
<orderEntry type="library" name="Maven: org.projectlombok:lombok:1.18.24" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Maven: org.postgresql:postgresql:42.3.7" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Maven: org.checkerframework:checker-qual:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-jdbc:2.7.5" level="project" />
<orderEntry type="library" name="Maven: com.zaxxer:HikariCP:4.0.3" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-jdbc:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.mybatis.spring.boot:mybatis-spring-boot-autoconfigure:2.2.2" level="project" />
<orderEntry type="library" name="Maven: org.mybatis:mybatis:3.5.9" level="project" />
<orderEntry type="library" name="Maven: org.mybatis:mybatis-spring:2.0.7" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-validation:2.7.5" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-el:9.0.68" level="project" />
<orderEntry type="library" name="Maven: org.hibernate.validator:hibernate-validator:6.2.5.Final" level="project" />
<orderEntry type="library" name="Maven: jakarta.validation:jakarta.validation-api:2.0.2" level="project" />
<orderEntry type="library" name="Maven: org.jboss.logging:jboss-logging:3.4.3.Final" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml:classmate:1.5.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-starter-test:2.7.5" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-test:2.7.5" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-test-autoconfigure:2.7.5" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: com.jayway.jsonpath:json-path:2.7.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: net.minidev:json-smart:2.4.8" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: net.minidev:accessors-smart:2.4.8" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.ow2.asm:asm:9.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: jakarta.activation:jakarta.activation-api:1.2.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.assertj:assertj-core:3.22.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest:2.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter:5.8.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.8.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-commons:1.8.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-params:5.8.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-engine:5.8.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-engine:1.8.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-core:4.5.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: net.bytebuddy:byte-buddy:1.12.18" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: net.bytebuddy:byte-buddy-agent:1.12.18" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.objenesis:objenesis:3.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-junit-jupiter:4.5.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.skyscreamer:jsonassert:1.5.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: com.vaadin.external.google:android-json:0.0.20131108.vaadin1" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:5.3.23" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-jcl:5.3.23" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.springframework:spring-test:5.3.23" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.xmlunit:xmlunit-core:2.9.0" level="project" />
</component>
</module>

100
pom.xml Normal file
View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>mail-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mail-service</name>
<description>Spring Boot Mail Service with QQ Mail and Redis Limit</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok (for reducing boilerplate code) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- PostgreSQL -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--&lt;!&ndash; HikariCP 连接池 &ndash;&gt;-->
<!--<dependency>-->
<!--<groupId>com.zaxxer</groupId>-->
<!--<artifactId>HikariCP</artifactId>-->
<!--</dependency>-->
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}

View File

@ -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<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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, "系统异常,请联系管理员");
}
}

View File

@ -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);
}

View File

@ -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<String, Object> 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;
}
}

View File

@ -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);
// 记录保存失败不影响主流程仅记录日志
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
);
}
}
}

View File

@ -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

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志上下文名称 -->
<contextName>mail-service</contextName>
<!-- 日志输出格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n" />
<!-- 日志文件路径 -->
<property name="LOG_PATH" value="logs" />
<property name="LOG_FILE_NAME" value="mail-service" />
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 滚动文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志文件路径 -->
<file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
<!-- 滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 归档文件命名格式 -->
<fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 保留天数 -->
<maxHistory>30</maxHistory>
<!-- 总大小限制 -->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<!-- 输出格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 错误日志输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 只处理ERROR级别日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 错误日志文件路径 -->
<file>${LOG_PATH}/${LOG_FILE_NAME}-error.log</file>
<!-- 滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}-error-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 输出格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 邮件发送相关日志 -->
<logger name="com.example.mailservice.service.MailService" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</logger>
<!-- 数据库操作相关日志 -->
<logger name="com.example.mailservice.mapper" level="INFO" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</logger>
<!-- 根日志配置 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mailservice.mapper.MailSenderRecordMapper">
<!-- 插入邮件发送记录 -->
<insert id="insert" parameterType="com.example.mailservice.entity.MailSenderRecord" useGeneratedKeys="true" keyProperty="id">
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}
)
</insert>
</mapper>