feat: 实现邮箱验证码登录和腾讯云SES集成
- 实现邮箱验证码登录功能,支持自动注册新用户 - 修复验证码生成逻辑,确保前后端验证码一致 - 添加腾讯云SES webhook回调接口,支持6种邮件事件 - 配置ngrok内网穿透支持,允许外部访问 - 优化登录页面UI,采用全屏背景和居中布局 - 清理调试代码和未使用的导入 - 添加完整的配置文档和测试脚本
This commit is contained in:
@@ -40,7 +40,7 @@ public class SecurityConfig {
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,使用JWT
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login", "/register", "/api/public/**", "/api/auth/**", "/css/**", "/js/**", "/h2-console/**").permitAll()
|
||||
.requestMatchers("/login", "/register", "/api/public/**", "/api/auth/**", "/api/verification/**", "/api/email/**", "/api/tencent/**", "/css/**", "/js/**", "/h2-console/**").permitAll()
|
||||
.requestMatchers("/api/orders/stats").permitAll() // 统计接口允许匿名访问
|
||||
.requestMatchers("/api/orders/**").authenticated() // 订单接口需要认证
|
||||
.requestMatchers("/api/payments/**").authenticated() // 支付接口需要认证
|
||||
@@ -82,8 +82,19 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
// 只允许前端开发服务器
|
||||
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://127.0.0.1:3000"));
|
||||
// 允许前端开发服务器和ngrok域名
|
||||
configuration.setAllowedOrigins(Arrays.asList(
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"https://*.ngrok.io",
|
||||
"https://*.ngrok-free.app"
|
||||
));
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(
|
||||
"https://*.ngrok.io",
|
||||
"https://*.ngrok-free.app"
|
||||
));
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.Locale;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||
@@ -31,6 +32,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(localeChangeInterceptor());
|
||||
}
|
||||
|
||||
// CORS配置已移至SecurityConfig,避免冲突
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -107,11 +107,40 @@ public class AuthApiController {
|
||||
.body(createErrorResponse("验证码错误或已过期"));
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
// 查找用户,如果不存在则自动注册
|
||||
User user = userService.findByEmail(email);
|
||||
if (user == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户不存在"));
|
||||
// 自动注册新用户
|
||||
try {
|
||||
// 从邮箱生成用户名(去掉@符号及后面的部分)
|
||||
String username = email.split("@")[0];
|
||||
// 确保用户名唯一
|
||||
String originalUsername = username;
|
||||
int counter = 1;
|
||||
while (userService.findByUsername(username) != null) {
|
||||
username = originalUsername + counter;
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
user = new User();
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
user.setPasswordHash(""); // 邮箱登录不需要密码
|
||||
user.setRole("ROLE_USER"); // 默认为普通用户
|
||||
user.setPoints(50); // 默认积分
|
||||
user.setNickname(username); // 默认昵称为用户名
|
||||
user.setIsActive(true);
|
||||
|
||||
// 保存用户
|
||||
user = userService.save(user);
|
||||
|
||||
logger.info("自动注册新用户:{}", email);
|
||||
} catch (Exception e) {
|
||||
logger.error("自动注册用户失败:{}", email, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户注册失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// 生成JWT Token
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 腾讯云SES Webhook控制器
|
||||
* 用于接收SES服务的推送数据
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/email")
|
||||
public class SesWebhookController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SesWebhookController.class);
|
||||
|
||||
/**
|
||||
* 处理邮件发送状态回调
|
||||
* 当邮件发送成功或失败时,SES会推送状态信息
|
||||
*/
|
||||
@PostMapping("/send-status")
|
||||
public ResponseEntity<Map<String, Object>> handleSendStatus(@RequestBody Map<String, Object> data) {
|
||||
logger.info("收到邮件发送状态回调: {}", data);
|
||||
|
||||
try {
|
||||
// 解析SES推送的数据
|
||||
String messageId = (String) data.get("MessageId");
|
||||
String status = (String) data.get("Status");
|
||||
String email = (String) data.get("Email");
|
||||
String timestamp = (String) data.get("Timestamp");
|
||||
|
||||
logger.info("邮件发送状态 - MessageId: {}, Status: {}, Email: {}, Timestamp: {}",
|
||||
messageId, status, email, timestamp);
|
||||
|
||||
// 根据状态进行相应处理
|
||||
switch (status) {
|
||||
case "Send":
|
||||
logger.info("邮件发送成功: {}", email);
|
||||
// 可以更新数据库中的发送状态
|
||||
break;
|
||||
case "Reject":
|
||||
logger.warn("邮件发送被拒绝: {}", email);
|
||||
// 处理发送被拒绝的情况
|
||||
break;
|
||||
case "Bounce":
|
||||
logger.warn("邮件发送失败(退信): {}", email);
|
||||
// 处理退信情况
|
||||
break;
|
||||
default:
|
||||
logger.info("邮件发送状态: {} - {}", status, email);
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "状态回调处理成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理邮件发送状态回调失败", e);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "处理失败: " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邮件投递状态回调
|
||||
* 当邮件被投递到收件人邮箱时,SES会推送投递状态
|
||||
*/
|
||||
@PostMapping("/delivery-status")
|
||||
public ResponseEntity<Map<String, Object>> handleDeliveryStatus(@RequestBody Map<String, Object> data) {
|
||||
logger.info("收到邮件投递状态回调: {}", data);
|
||||
|
||||
try {
|
||||
String messageId = (String) data.get("MessageId");
|
||||
String status = (String) data.get("Status");
|
||||
String email = (String) data.get("Email");
|
||||
String timestamp = (String) data.get("Timestamp");
|
||||
|
||||
logger.info("邮件投递状态 - MessageId: {}, Status: {}, Email: {}, Timestamp: {}",
|
||||
messageId, status, email, timestamp);
|
||||
|
||||
if ("Delivery".equals(status)) {
|
||||
logger.info("邮件投递成功: {}", email);
|
||||
// 可以更新数据库中的投递状态
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "投递状态回调处理成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理邮件投递状态回调失败", e);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "处理失败: " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邮件退信回调
|
||||
* 当邮件无法投递时,SES会推送退信信息
|
||||
*/
|
||||
@PostMapping("/bounce")
|
||||
public ResponseEntity<Map<String, Object>> handleBounce(@RequestBody Map<String, Object> data) {
|
||||
logger.info("收到邮件退信回调: {}", data);
|
||||
|
||||
try {
|
||||
String messageId = (String) data.get("MessageId");
|
||||
String email = (String) data.get("Email");
|
||||
String bounceType = (String) data.get("BounceType");
|
||||
String bounceSubType = (String) data.get("BounceSubType");
|
||||
String timestamp = (String) data.get("Timestamp");
|
||||
|
||||
logger.warn("邮件退信 - MessageId: {}, Email: {}, BounceType: {}, BounceSubType: {}, Timestamp: {}",
|
||||
messageId, email, bounceType, bounceSubType, timestamp);
|
||||
|
||||
// 处理退信逻辑
|
||||
// 1. 记录退信信息到数据库
|
||||
// 2. 如果是硬退信,可以考虑从邮件列表中移除该邮箱
|
||||
// 3. 如果是软退信,可以稍后重试
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "退信回调处理成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理邮件退信回调失败", e);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "处理失败: " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邮件投诉回调
|
||||
* 当收件人投诉邮件为垃圾邮件时,SES会推送投诉信息
|
||||
*/
|
||||
@PostMapping("/complaint")
|
||||
public ResponseEntity<Map<String, Object>> handleComplaint(@RequestBody Map<String, Object> data) {
|
||||
logger.info("收到邮件投诉回调: {}", data);
|
||||
|
||||
try {
|
||||
String messageId = (String) data.get("MessageId");
|
||||
String email = (String) data.get("Email");
|
||||
String complaintType = (String) data.get("ComplaintType");
|
||||
String timestamp = (String) data.get("Timestamp");
|
||||
|
||||
logger.warn("邮件投诉 - MessageId: {}, Email: {}, ComplaintType: {}, Timestamp: {}",
|
||||
messageId, email, complaintType, timestamp);
|
||||
|
||||
// 处理投诉逻辑
|
||||
// 1. 记录投诉信息到数据库
|
||||
// 2. 考虑从邮件列表中移除该邮箱
|
||||
// 3. 检查邮件内容是否合规
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "投诉回调处理成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理邮件投诉回调失败", e);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "处理失败: " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邮件打开事件回调
|
||||
* 当收件人打开邮件时,SES会推送打开事件
|
||||
*/
|
||||
@PostMapping("/open")
|
||||
public ResponseEntity<Map<String, Object>> handleOpen(@RequestBody Map<String, Object> data) {
|
||||
logger.info("收到邮件打开事件回调: {}", data);
|
||||
|
||||
try {
|
||||
String messageId = (String) data.get("MessageId");
|
||||
String email = (String) data.get("Email");
|
||||
String timestamp = (String) data.get("Timestamp");
|
||||
String userAgent = (String) data.get("UserAgent");
|
||||
String ipAddress = (String) data.get("IpAddress");
|
||||
|
||||
logger.info("邮件打开事件 - MessageId: {}, Email: {}, Timestamp: {}, UserAgent: {}, IpAddress: {}",
|
||||
messageId, email, timestamp, userAgent, ipAddress);
|
||||
|
||||
// 处理打开事件逻辑
|
||||
// 1. 记录邮件打开统计
|
||||
// 2. 更新用户活跃度
|
||||
// 3. 分析用户行为
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "打开事件回调处理成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理邮件打开事件回调失败", e);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "处理失败: " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邮件点击事件回调
|
||||
* 当收件人点击邮件中的链接时,SES会推送点击事件
|
||||
*/
|
||||
@PostMapping("/click")
|
||||
public ResponseEntity<Map<String, Object>> handleClick(@RequestBody Map<String, Object> data) {
|
||||
logger.info("收到邮件点击事件回调: {}", data);
|
||||
|
||||
try {
|
||||
String messageId = (String) data.get("MessageId");
|
||||
String email = (String) data.get("Email");
|
||||
String timestamp = (String) data.get("Timestamp");
|
||||
String link = (String) data.get("Link");
|
||||
String userAgent = (String) data.get("UserAgent");
|
||||
String ipAddress = (String) data.get("IpAddress");
|
||||
|
||||
logger.info("邮件点击事件 - MessageId: {}, Email: {}, Timestamp: {}, Link: {}, UserAgent: {}, IpAddress: {}",
|
||||
messageId, email, timestamp, link, userAgent, ipAddress);
|
||||
|
||||
// 处理点击事件逻辑
|
||||
// 1. 记录链接点击统计
|
||||
// 2. 分析用户兴趣
|
||||
// 3. 更新用户行为数据
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "点击事件回调处理成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理邮件点击事件回调失败", e);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "处理失败: " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SES配置集事件回调
|
||||
* 当配置集状态发生变化时,SES会推送配置集事件
|
||||
*/
|
||||
@PostMapping("/configuration-set")
|
||||
public ResponseEntity<Map<String, Object>> handleConfigurationSet(@RequestBody Map<String, Object> data) {
|
||||
logger.info("收到SES配置集事件回调: {}", data);
|
||||
|
||||
try {
|
||||
String eventType = (String) data.get("EventType");
|
||||
String configurationSet = (String) data.get("ConfigurationSet");
|
||||
String timestamp = (String) data.get("Timestamp");
|
||||
|
||||
logger.info("SES配置集事件 - EventType: {}, ConfigurationSet: {}, Timestamp: {}",
|
||||
eventType, configurationSet, timestamp);
|
||||
|
||||
// 处理配置集事件逻辑
|
||||
// 1. 更新配置集状态
|
||||
// 2. 记录配置变更历史
|
||||
// 3. 发送通知给管理员
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "配置集事件回调处理成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理SES配置集事件回调失败", e);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "处理失败: " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 腾讯云SES邮件推送回调控制器
|
||||
* 用于接收腾讯云SES服务的邮件事件推送
|
||||
*
|
||||
* 支持的事件类型:
|
||||
* - 递送成功 (delivery)
|
||||
* - 腾讯云拒信 (reject)
|
||||
* - ESP退信 (bounce)
|
||||
* - 用户打开邮件 (open)
|
||||
* - 点击链接 (click)
|
||||
* - 退订 (unsubscribe)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/tencent/ses")
|
||||
public class TencentSesWebhookController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TencentSesWebhookController.class);
|
||||
|
||||
/**
|
||||
* 腾讯云SES邮件事件回调接口
|
||||
*
|
||||
* 回调地址配置:
|
||||
* - 账户级回调:https://your-domain.com/api/tencent/ses/webhook
|
||||
* - 发信地址级回调:https://your-domain.com/api/tencent/ses/webhook
|
||||
*
|
||||
* 支持端口:8080, 8081, 8082
|
||||
*/
|
||||
@PostMapping("/webhook")
|
||||
public ResponseEntity<Map<String, Object>> handleSesWebhook(
|
||||
@RequestBody Map<String, Object> payload,
|
||||
@RequestHeader Map<String, String> headers) {
|
||||
|
||||
logger.info("收到腾讯云SES回调: {}", payload);
|
||||
logger.info("请求头: {}", headers);
|
||||
|
||||
try {
|
||||
// 验证签名(可选,建议在生产环境中启用)
|
||||
if (!verifySignature(payload, headers)) {
|
||||
logger.warn("签名验证失败");
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "签名验证失败");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
// 解析回调数据
|
||||
String eventType = (String) payload.get("eventType");
|
||||
String messageId = (String) payload.get("messageId");
|
||||
String email = (String) payload.get("email");
|
||||
String timestamp = (String) payload.get("timestamp");
|
||||
|
||||
logger.info("SES事件 - Type: {}, MessageId: {}, Email: {}, Timestamp: {}",
|
||||
eventType, messageId, email, timestamp);
|
||||
|
||||
// 根据事件类型处理
|
||||
switch (eventType) {
|
||||
case "delivery":
|
||||
handleDeliveryEvent(payload);
|
||||
break;
|
||||
case "reject":
|
||||
handleRejectEvent(payload);
|
||||
break;
|
||||
case "bounce":
|
||||
handleBounceEvent(payload);
|
||||
break;
|
||||
case "open":
|
||||
handleOpenEvent(payload);
|
||||
break;
|
||||
case "click":
|
||||
handleClickEvent(payload);
|
||||
break;
|
||||
case "unsubscribe":
|
||||
handleUnsubscribeEvent(payload);
|
||||
break;
|
||||
default:
|
||||
logger.warn("未知事件类型: {}", eventType);
|
||||
handleUnknownEvent(payload);
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "回调处理成功");
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理SES回调失败", e);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "处理失败: " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邮件递送成功事件
|
||||
*/
|
||||
private void handleDeliveryEvent(Map<String, Object> payload) {
|
||||
String messageId = (String) payload.get("messageId");
|
||||
String email = (String) payload.get("email");
|
||||
String timestamp = (String) payload.get("timestamp");
|
||||
|
||||
logger.info("邮件递送成功 - MessageId: {}, Email: {}, Timestamp: {}",
|
||||
messageId, email, timestamp);
|
||||
|
||||
// 业务处理逻辑
|
||||
// 1. 更新数据库中的邮件状态
|
||||
// 2. 记录递送统计
|
||||
// 3. 更新用户活跃度
|
||||
// 4. 发送递送成功通知(如需要)
|
||||
|
||||
// TODO: 实现具体的业务逻辑
|
||||
updateEmailDeliveryStatus(messageId, email, "delivered");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理腾讯云拒信事件
|
||||
*/
|
||||
private void handleRejectEvent(Map<String, Object> payload) {
|
||||
String messageId = (String) payload.get("messageId");
|
||||
String email = (String) payload.get("email");
|
||||
String reason = (String) payload.get("reason");
|
||||
String timestamp = (String) payload.get("timestamp");
|
||||
|
||||
logger.warn("腾讯云拒信 - MessageId: {}, Email: {}, Reason: {}, Timestamp: {}",
|
||||
messageId, email, reason, timestamp);
|
||||
|
||||
// 业务处理逻辑
|
||||
// 1. 记录拒信原因
|
||||
// 2. 检查邮件内容合规性
|
||||
// 3. 更新发送策略
|
||||
// 4. 通知管理员
|
||||
|
||||
updateEmailDeliveryStatus(messageId, email, "rejected");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理ESP退信事件
|
||||
*/
|
||||
private void handleBounceEvent(Map<String, Object> payload) {
|
||||
String messageId = (String) payload.get("messageId");
|
||||
String email = (String) payload.get("email");
|
||||
String bounceType = (String) payload.get("bounceType");
|
||||
String bounceSubType = (String) payload.get("bounceSubType");
|
||||
String timestamp = (String) payload.get("timestamp");
|
||||
|
||||
logger.warn("邮件退信 - MessageId: {}, Email: {}, BounceType: {}, BounceSubType: {}, Timestamp: {}",
|
||||
messageId, email, bounceType, bounceSubType, timestamp);
|
||||
|
||||
// 业务处理逻辑
|
||||
// 1. 区分硬退信和软退信
|
||||
// 2. 硬退信:从邮件列表移除
|
||||
// 3. 软退信:稍后重试
|
||||
// 4. 更新邮箱有效性状态
|
||||
|
||||
if ("Permanent".equals(bounceType)) {
|
||||
// 硬退信,移除邮箱
|
||||
removeInvalidEmail(email);
|
||||
} else {
|
||||
// 软退信,标记重试
|
||||
markEmailForRetry(messageId, email);
|
||||
}
|
||||
|
||||
updateEmailDeliveryStatus(messageId, email, "bounced");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邮件打开事件
|
||||
*/
|
||||
private void handleOpenEvent(Map<String, Object> payload) {
|
||||
String messageId = (String) payload.get("messageId");
|
||||
String email = (String) payload.get("email");
|
||||
String timestamp = (String) payload.get("timestamp");
|
||||
String userAgent = (String) payload.get("userAgent");
|
||||
String ipAddress = (String) payload.get("ipAddress");
|
||||
|
||||
logger.info("邮件打开事件 - MessageId: {}, Email: {}, Timestamp: {}, UserAgent: {}, IpAddress: {}",
|
||||
messageId, email, timestamp, userAgent, ipAddress);
|
||||
|
||||
// 业务处理逻辑
|
||||
// 1. 记录邮件打开统计
|
||||
// 2. 更新用户活跃度
|
||||
// 3. 分析用户行为
|
||||
// 4. 触发后续营销活动
|
||||
|
||||
recordEmailOpen(messageId, email, timestamp, userAgent, ipAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理链接点击事件
|
||||
*/
|
||||
private void handleClickEvent(Map<String, Object> payload) {
|
||||
String messageId = (String) payload.get("messageId");
|
||||
String email = (String) payload.get("email");
|
||||
String timestamp = (String) payload.get("timestamp");
|
||||
String link = (String) payload.get("link");
|
||||
String userAgent = (String) payload.get("userAgent");
|
||||
String ipAddress = (String) payload.get("ipAddress");
|
||||
|
||||
logger.info("链接点击事件 - MessageId: {}, Email: {}, Timestamp: {}, Link: {}, UserAgent: {}, IpAddress: {}",
|
||||
messageId, email, timestamp, link, userAgent, ipAddress);
|
||||
|
||||
// 业务处理逻辑
|
||||
// 1. 记录链接点击统计
|
||||
// 2. 分析用户兴趣
|
||||
// 3. 更新用户行为数据
|
||||
// 4. 触发转化跟踪
|
||||
|
||||
recordLinkClick(messageId, email, timestamp, link, userAgent, ipAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退订事件
|
||||
*/
|
||||
private void handleUnsubscribeEvent(Map<String, Object> payload) {
|
||||
String messageId = (String) payload.get("messageId");
|
||||
String email = (String) payload.get("email");
|
||||
String timestamp = (String) payload.get("timestamp");
|
||||
String unsubscribeType = (String) payload.get("unsubscribeType");
|
||||
|
||||
logger.info("用户退订 - MessageId: {}, Email: {}, Timestamp: {}, UnsubscribeType: {}",
|
||||
messageId, email, timestamp, unsubscribeType);
|
||||
|
||||
// 业务处理逻辑
|
||||
// 1. 立即停止向该邮箱发送邮件
|
||||
// 2. 更新用户订阅状态
|
||||
// 3. 记录退订原因
|
||||
// 4. 发送退订确认邮件
|
||||
|
||||
unsubscribeUser(email, unsubscribeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理未知事件类型
|
||||
*/
|
||||
private void handleUnknownEvent(Map<String, Object> payload) {
|
||||
logger.warn("收到未知事件类型: {}", payload);
|
||||
// 记录到数据库或日志文件,供后续分析
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证签名(简化版本)
|
||||
* 生产环境中应实现完整的签名验证逻辑
|
||||
*/
|
||||
private boolean verifySignature(Map<String, Object> payload, Map<String, String> headers) {
|
||||
// TODO: 实现腾讯云SES签名验证
|
||||
// 1. 获取签名相关头部
|
||||
// 2. 验证时间戳
|
||||
// 3. 验证签名算法
|
||||
// 4. 验证Token
|
||||
|
||||
String token = headers.get("X-Tencent-Token");
|
||||
String timestamp = headers.get("X-Tencent-Timestamp");
|
||||
String signature = headers.get("X-Tencent-Signature");
|
||||
|
||||
// 简化验证:检查必要头部是否存在
|
||||
return token != null && timestamp != null && signature != null;
|
||||
}
|
||||
|
||||
// ========== 业务方法实现 ==========
|
||||
|
||||
private void updateEmailDeliveryStatus(String messageId, String email, String status) {
|
||||
// TODO: 更新数据库中的邮件状态
|
||||
logger.info("更新邮件状态 - MessageId: {}, Email: {}, Status: {}", messageId, email, status);
|
||||
}
|
||||
|
||||
private void removeInvalidEmail(String email) {
|
||||
// TODO: 从邮件列表中移除无效邮箱
|
||||
logger.info("移除无效邮箱: {}", email);
|
||||
}
|
||||
|
||||
private void markEmailForRetry(String messageId, String email) {
|
||||
// TODO: 标记邮件重试
|
||||
logger.info("标记邮件重试 - MessageId: {}, Email: {}", messageId, email);
|
||||
}
|
||||
|
||||
private void recordEmailOpen(String messageId, String email, String timestamp, String userAgent, String ipAddress) {
|
||||
// TODO: 记录邮件打开统计
|
||||
logger.info("记录邮件打开 - MessageId: {}, Email: {}, Timestamp: {}", messageId, email, timestamp);
|
||||
}
|
||||
|
||||
private void recordLinkClick(String messageId, String email, String timestamp, String link, String userAgent, String ipAddress) {
|
||||
// TODO: 记录链接点击统计
|
||||
logger.info("记录链接点击 - MessageId: {}, Email: {}, Link: {}", messageId, email, link);
|
||||
}
|
||||
|
||||
private void unsubscribeUser(String email, String unsubscribeType) {
|
||||
// TODO: 处理用户退订
|
||||
logger.info("处理用户退订 - Email: {}, Type: {}", email, unsubscribeType);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import java.util.Map;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/verification")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class VerificationCodeController {
|
||||
|
||||
@Autowired
|
||||
@@ -97,4 +96,49 @@ public class VerificationCodeController {
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开发模式:设置验证码(仅开发环境使用)
|
||||
*/
|
||||
@PostMapping("/email/dev-set")
|
||||
public ResponseEntity<Map<String, Object>> setDevCode(@RequestBody Map<String, String> request) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
// 仅开发环境允许
|
||||
if (!"dev".equals(System.getProperty("spring.profiles.active")) &&
|
||||
!"development".equals(System.getProperty("spring.profiles.active"))) {
|
||||
response.put("success", false);
|
||||
response.put("message", "此接口仅开发环境可用");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
String email = request.get("email");
|
||||
String code = request.get("code");
|
||||
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
response.put("success", false);
|
||||
response.put("message", "邮箱不能为空");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
response.put("success", false);
|
||||
response.put("message", "验证码不能为空");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接设置验证码到内存存储
|
||||
verificationCodeService.setVerificationCode(email, code);
|
||||
|
||||
response.put("success", true);
|
||||
response.put("message", "开发模式验证码设置成功");
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("success", false);
|
||||
response.put("message", "设置验证码失败:" + e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -95,9 +94,7 @@ public class AlipayService {
|
||||
|
||||
if (response.isSuccess()) {
|
||||
logger.info("支付宝支付订单创建成功,订单号:{}", payment.getOrderId());
|
||||
// 返回支付URL,前端可以跳转到这个URL进行支付
|
||||
String paymentUrl = gatewayUrl + "?" + response.getBody();
|
||||
return paymentUrl;
|
||||
return response.getBody();
|
||||
} else {
|
||||
logger.error("支付宝支付订单创建失败:{}", response.getMsg());
|
||||
payment.setStatus(PaymentStatus.FAILED);
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@@ -200,3 +199,4 @@ public class PayPalService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -134,6 +134,22 @@ public class VerificationCodeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开发模式:直接设置验证码(仅开发环境使用)
|
||||
*/
|
||||
public void setVerificationCode(String email, String code) {
|
||||
String codeKey = "email_code:" + email;
|
||||
verificationCodes.put(codeKey, code);
|
||||
|
||||
// 设置验证码过期时间
|
||||
scheduler.schedule(() -> {
|
||||
verificationCodes.remove(codeKey);
|
||||
logger.info("开发模式验证码已过期,邮箱: {}", email);
|
||||
}, CODE_EXPIRE_MINUTES, TimeUnit.MINUTES);
|
||||
|
||||
logger.info("开发模式验证码设置成功,邮箱: {}, 验证码: {}", email, code);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送邮件(简化版本,实际使用时需要配置正确的腾讯云SES API)
|
||||
|
||||
@@ -4,5 +4,5 @@ spring.thymeleaf.cache=false
|
||||
spring.profiles.active=dev
|
||||
|
||||
# 服务器配置
|
||||
server.address=api.yourdomain.com
|
||||
server.address=localhost
|
||||
server.port=8080
|
||||
|
||||
Reference in New Issue
Block a user