优化邮件发送功能和支付宝支付诊断
- 修复邮件服务区域配置(改为ap-hongkong) - 增强支付宝支付错误诊断和日志 - 修复代码质量问题(OrderService、ImageToVideoTask) - 添加支付宝支付问题排查文档 - 增加详细的错误诊断信息
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
* 图生视频任务模型
|
||||
*/
|
||||
@@ -91,7 +99,9 @@ public class ImageToVideoTask {
|
||||
* 计算任务消耗积分
|
||||
*/
|
||||
private Integer calculateCost() {
|
||||
int actualDuration = (duration == null || duration <= 0) ? 5 : duration; // 使用默认时长但不修改字段
|
||||
// 安全处理duration可能为null的情况
|
||||
Integer safeDuration = duration;
|
||||
int actualDuration = (safeDuration == null || safeDuration <= 0) ? 5 : safeDuration; // 使用默认时长但不修改字段
|
||||
|
||||
int baseCost = 10; // 基础消耗
|
||||
int durationCost = actualDuration * 2; // 时长消耗
|
||||
|
||||
@@ -91,14 +91,24 @@ public class AlipayService {
|
||||
* 调用真实的支付宝API
|
||||
*/
|
||||
private Map<String, Object> callRealAlipayAPI(Payment payment) throws Exception {
|
||||
// 创建支付宝客户端,增加超时时间
|
||||
// 记录配置信息
|
||||
logger.info("=== 支付宝API配置信息 ===");
|
||||
logger.info("网关地址: {}", gatewayUrl);
|
||||
logger.info("应用ID: {}", appId);
|
||||
logger.info("字符集: {}", charset);
|
||||
logger.info("签名类型: {}", signType);
|
||||
logger.info("通知URL: {}", notifyUrl);
|
||||
logger.info("返回URL: {}", returnUrl);
|
||||
|
||||
// 设置连接和读取超时时间(60秒),需要在创建客户端之前设置
|
||||
System.setProperty("sun.net.client.defaultConnectTimeout", "60000");
|
||||
System.setProperty("sun.net.client.defaultReadTimeout", "60000");
|
||||
logger.info("超时配置: 连接超时=60秒, 读取超时=60秒");
|
||||
|
||||
// 创建支付宝客户端
|
||||
AlipayClient alipayClient = new DefaultAlipayClient(
|
||||
gatewayUrl, appId, privateKey, "json", charset, publicKey, signType);
|
||||
|
||||
// 设置连接和读取超时时间(30秒)
|
||||
System.setProperty("sun.net.client.defaultConnectTimeout", "30000");
|
||||
System.setProperty("sun.net.client.defaultReadTimeout", "30000");
|
||||
|
||||
// 使用预创建API生成二维码
|
||||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
||||
request.setNotifyUrl(notifyUrl);
|
||||
@@ -127,10 +137,42 @@ public class AlipayService {
|
||||
break; // 成功则跳出循环
|
||||
} catch (Exception e) {
|
||||
retryCount++;
|
||||
logger.warn("支付宝API调用失败,第{}次重试,错误:{}", retryCount, e.getMessage());
|
||||
String errorType = e.getClass().getSimpleName();
|
||||
String errorMessage = e.getMessage();
|
||||
|
||||
logger.warn("支付宝API调用失败,第{}次重试", retryCount);
|
||||
logger.warn("错误类型: {}", errorType);
|
||||
logger.warn("错误信息: {}", errorMessage);
|
||||
|
||||
// 根据错误类型提供诊断建议
|
||||
if (errorMessage != null) {
|
||||
if (errorMessage.contains("Read timed out") || errorMessage.contains("timeout")) {
|
||||
logger.error("=== 网络超时错误诊断 ===");
|
||||
logger.error("可能的原因:");
|
||||
logger.error("1. 网络连接不稳定或延迟过高");
|
||||
logger.error("2. 支付宝沙箱环境响应慢(openapi.alipaydev.com)");
|
||||
logger.error("3. 防火墙或代理服务器阻止连接");
|
||||
logger.error("4. ngrok隧道可能已过期或不可用");
|
||||
logger.error("解决方案:");
|
||||
logger.error("1. 检查网络连接,尝试ping openapi.alipaydev.com");
|
||||
logger.error("2. 检查ngrok是否正常运行: {}", notifyUrl);
|
||||
logger.error("3. 考虑使用代理服务器或VPN");
|
||||
logger.error("4. 增加超时时间或重试次数");
|
||||
} else if (errorMessage.contains("Connection refused") || errorMessage.contains("ConnectException")) {
|
||||
logger.error("=== 连接拒绝错误诊断 ===");
|
||||
logger.error("无法连接到支付宝服务器: {}", gatewayUrl);
|
||||
logger.error("请检查网络连接和防火墙设置");
|
||||
} else if (errorMessage.contains("UnknownHostException")) {
|
||||
logger.error("=== DNS解析错误诊断 ===");
|
||||
logger.error("无法解析域名: {}", gatewayUrl);
|
||||
logger.error("请检查DNS设置和网络连接");
|
||||
}
|
||||
}
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
logger.error("支付宝API调用失败,已达到最大重试次数");
|
||||
throw new RuntimeException("支付宝API调用失败:" + e.getMessage());
|
||||
logger.error("支付宝API调用失败,已达到最大重试次数({}次)", maxRetries);
|
||||
logger.error("最终失败原因: {}", errorMessage);
|
||||
throw new RuntimeException("支付宝API调用失败:" + errorMessage);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(2000); // 等待2秒后重试
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.*;
|
||||
import com.example.demo.repository.OrderItemRepository;
|
||||
import com.example.demo.repository.OrderRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@@ -19,6 +8,22 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.model.Order;
|
||||
import com.example.demo.model.OrderItem;
|
||||
import com.example.demo.model.OrderStatus;
|
||||
import com.example.demo.model.OrderType;
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.repository.OrderItemRepository;
|
||||
import com.example.demo.repository.OrderRepository;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class OrderService {
|
||||
@@ -164,6 +169,15 @@ public class OrderService {
|
||||
// 设置相应的时间戳
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
switch (newStatus) {
|
||||
case PENDING:
|
||||
// 待处理状态,不需要设置时间戳
|
||||
break;
|
||||
case CONFIRMED:
|
||||
// 已确认状态,不需要设置时间戳
|
||||
break;
|
||||
case PROCESSING:
|
||||
// 处理中状态,不需要设置时间戳
|
||||
break;
|
||||
case PAID:
|
||||
order.setPaidAt(now);
|
||||
break;
|
||||
@@ -179,6 +193,12 @@ public class OrderService {
|
||||
case CANCELLED:
|
||||
order.setCancelledAt(now);
|
||||
break;
|
||||
case REFUNDED:
|
||||
// 已退款状态,不需要设置时间戳
|
||||
break;
|
||||
default:
|
||||
// 其他状态,不需要设置时间戳
|
||||
break;
|
||||
}
|
||||
|
||||
Order updatedOrder = orderRepository.save(order);
|
||||
|
||||
@@ -38,7 +38,7 @@ public class TencentSesMailService {
|
||||
@Value("${tencent.ses.secret-key}")
|
||||
private String secretKey;
|
||||
|
||||
@Value("${tencent.ses.region:ap-beijing}")
|
||||
@Value("${tencent.ses.region:ap-hongkong}")
|
||||
private String region;
|
||||
|
||||
@Value("${tencent.ses.from-email}")
|
||||
@@ -109,8 +109,64 @@ public class TencentSesMailService {
|
||||
return true;
|
||||
|
||||
} catch (TencentCloudSDKException e) {
|
||||
logger.error("腾讯云SES API调用失败,收件人: {}, 错误码: {}, 错误信息: {}",
|
||||
toEmail, e.getErrorCode(), e.getMessage(), e);
|
||||
String errorCode = e.getErrorCode();
|
||||
String errorMessage = e.getMessage();
|
||||
|
||||
// 检查是否为权限错误
|
||||
if ("AuthFailure.UnauthorizedOperation".equals(errorCode) ||
|
||||
(errorMessage != null && errorMessage.contains("no permission"))) {
|
||||
logger.error("=== 腾讯云SES权限错误 ===");
|
||||
logger.error("收件人: {}", toEmail);
|
||||
logger.error("错误码: {}", errorCode);
|
||||
logger.error("错误信息: {}", errorMessage);
|
||||
logger.error("解决方案:");
|
||||
logger.error("1. 在腾讯云CAM中为访问密钥添加SES发送邮件权限");
|
||||
logger.error("2. 或临时使用开发模式:设置 tencent.ses.template-id=0");
|
||||
logger.error("详细配置指南请参考: TENCENT_SES_PERMISSION_FIX.md");
|
||||
}
|
||||
// 检查是否为区域不支持错误
|
||||
else if ("UnsupportedRegion".equals(errorCode) ||
|
||||
(errorMessage != null && errorMessage.contains("does not support this region"))) {
|
||||
logger.error("=== 腾讯云SES区域错误 ===");
|
||||
logger.error("收件人: {}", toEmail);
|
||||
logger.error("错误码: {}", errorCode);
|
||||
logger.error("错误信息: {}", errorMessage);
|
||||
logger.error("当前配置的区域: {}", region);
|
||||
logger.error("解决方案:");
|
||||
logger.error("1. 腾讯云SES通常支持以下区域:");
|
||||
logger.error(" - ap-hongkong (中国香港)");
|
||||
logger.error(" - ap-guangzhou (广州)");
|
||||
logger.error(" - ap-shanghai (上海)");
|
||||
logger.error(" - ap-nanjing (南京)");
|
||||
logger.error("2. 修改配置文件中的 tencent.ses.region 为支持的区域");
|
||||
logger.error("3. 当前配置:tencent.ses.region={}", region);
|
||||
}
|
||||
// 检查是否为发送失败错误
|
||||
else if ("FailedOperation.SendEmailErr".equals(errorCode) ||
|
||||
(errorMessage != null && errorMessage.contains("发送遇到问题"))) {
|
||||
logger.error("=== 腾讯云SES发送失败错误 ===");
|
||||
logger.error("收件人: {}", toEmail);
|
||||
logger.error("错误码: {}", errorCode);
|
||||
logger.error("错误信息: {}", errorMessage);
|
||||
logger.error("当前配置:");
|
||||
logger.error(" 区域: {}", region);
|
||||
logger.error(" 发信地址: {}", fromEmail);
|
||||
logger.error(" 模板ID: {}", templateId);
|
||||
logger.error("可能的原因:");
|
||||
logger.error("1. 发信地址 {} 未在腾讯云SES控制台的 {} 区域验证", fromEmail, region);
|
||||
logger.error("2. 模板ID {} 不存在于 {} 区域,或模板状态异常", templateId, region);
|
||||
logger.error("3. 发信地址已验证但未通过审核");
|
||||
logger.error("4. 账户可能未开通SES服务或服务受限");
|
||||
logger.error("解决方案:");
|
||||
logger.error("1. 登录腾讯云SES控制台: https://console.cloud.tencent.com/ses");
|
||||
logger.error("2. 确认选择的地域为: {}", region);
|
||||
logger.error("3. 检查发信地址 {} 是否已在该区域验证通过", fromEmail);
|
||||
logger.error("4. 检查模板ID {} 是否存在于该区域且状态为已审核", templateId);
|
||||
logger.error("5. 如未验证,请先验证发信地址或域名");
|
||||
} else {
|
||||
logger.error("腾讯云SES API调用失败,收件人: {}, 错误码: {}, 错误信息: {}",
|
||||
toEmail, errorCode, errorMessage, e);
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
logger.error("发送邮件异常,收件人: {}", toEmail, e);
|
||||
|
||||
@@ -26,6 +26,19 @@ public class VerificationCodeService {
|
||||
@Value("${tencent.ses.template-id:0}")
|
||||
private Long templateId;
|
||||
|
||||
// 在构造函数中记录配置值,用于调试
|
||||
public VerificationCodeService() {
|
||||
// 构造函数用于初始化
|
||||
}
|
||||
|
||||
// 使用@PostConstruct确保在注入后记录配置
|
||||
@jakarta.annotation.PostConstruct
|
||||
public void init() {
|
||||
logger.info("=== 邮件服务配置初始化 ===");
|
||||
logger.info("模板ID配置值: {}", templateId);
|
||||
logger.info("如果为0或null,将使用开发模式");
|
||||
}
|
||||
|
||||
// 使用内存存储验证码
|
||||
private final ConcurrentHashMap<String, String> verificationCodes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Long> rateLimits = new ConcurrentHashMap<>();
|
||||
@@ -149,6 +162,9 @@ public class VerificationCodeService {
|
||||
*/
|
||||
private boolean sendEmail(String email, String code) {
|
||||
try {
|
||||
// 记录当前模板ID配置
|
||||
logger.info("当前邮件模板ID配置: {}", templateId);
|
||||
|
||||
// 如果没有配置模板ID,使用开发模式(仅记录日志)
|
||||
if (templateId == null || templateId == 0) {
|
||||
logger.warn("未配置邮件模板ID,使用开发模式。验证码: {}, 邮箱: {}", code, email);
|
||||
@@ -156,6 +172,8 @@ public class VerificationCodeService {
|
||||
return true; // 开发模式下返回成功
|
||||
}
|
||||
|
||||
logger.info("使用生产模式发送邮件,收件人: {}, 模板ID: {}", email, templateId);
|
||||
|
||||
// 使用腾讯云SES发送邮件
|
||||
boolean success = tencentSesMailService.sendVerificationCodeEmail(email, code, templateId);
|
||||
|
||||
@@ -163,12 +181,17 @@ public class VerificationCodeService {
|
||||
logger.info("邮件验证码发送成功,邮箱: {}", email);
|
||||
} else {
|
||||
logger.error("邮件验证码发送失败,邮箱: {}", email);
|
||||
logger.error("可能的原因:");
|
||||
logger.error("1. SES权限未配置(检查CAM权限策略)");
|
||||
logger.error("2. 发信地址未验证(检查SES控制台)");
|
||||
logger.error("3. 模板ID错误(当前模板ID: {})", templateId);
|
||||
logger.error("详细排查指南请参考: TENCENT_SES_PERMISSION_FIX.md");
|
||||
}
|
||||
|
||||
return success;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("邮件发送异常,邮箱: {}", email, e);
|
||||
logger.error("邮件发送异常,邮箱: {}, 错误: {}", email, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ jwt.expiration=86400000
|
||||
# 腾讯云SES配置
|
||||
# 主账号ID: 100040185043
|
||||
# 用户名: test
|
||||
tencent.ses.secret-id=AKIDXw8HBtNfjdJm480xljV4QZUDi05wa0DE
|
||||
tencent.ses.secret-key=tZyHMDsKadS4ScZhhU3PYUErGUVIqBIB
|
||||
tencent.ses.region=ap-beijing
|
||||
tencent.ses.from-email=noreply@vionow.com
|
||||
tencent.ses.secret-id=AKIDoaEjFbqxxqZAcv8EE6oZCg2IQPG1fCxm
|
||||
tencent.ses.secret-key=nR83I79FOSpGcqNo7JXkqnU8g7SjsxuG
|
||||
tencent.ses.region=ap-hongkong
|
||||
tencent.ses.from-email=newletter@vionow.com
|
||||
tencent.ses.from-name=AIGC平台
|
||||
# 邮件模板ID(在腾讯云SES控制台创建模板后获取)
|
||||
# 如果未配置或为0,将使用开发模式(仅记录日志)
|
||||
tencent.ses.template-id=0
|
||||
tencent.ses.template-id=154360
|
||||
|
||||
# AI API配置
|
||||
# 文生视频、图生视频、分镜视频都使用Comfly API
|
||||
@@ -50,4 +50,4 @@ alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
|
||||
alipay.charset=UTF-8
|
||||
alipay.sign-type=RSA2
|
||||
alipay.notify-url=https://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/notify
|
||||
alipay.return-url=https://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/return
|
||||
alipay.return-url=https://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/return
|
||||
|
||||
@@ -49,6 +49,16 @@ paypal.cancel-url=${PAYPAL_CANCEL_URL}
|
||||
jwt.secret=${JWT_SECRET}
|
||||
jwt.expiration=${JWT_EXPIRATION:604800000}
|
||||
|
||||
# 腾讯云SES配置 (生产环境)
|
||||
tencent.ses.secret-id=${TENCENT_SES_SECRET_ID}
|
||||
tencent.ses.secret-key=${TENCENT_SES_SECRET_KEY}
|
||||
tencent.ses.region=ap-hongkong
|
||||
tencent.ses.from-email=${TENCENT_SES_FROM_EMAIL}
|
||||
tencent.ses.from-name=AIGC平台
|
||||
# 邮件模板ID(在腾讯云SES控制台创建模板后获取)
|
||||
# 如果未配置或为0,将使用开发模式(仅记录日志)
|
||||
tencent.ses.template-id=${TENCENT_SES_TEMPLATE_ID}
|
||||
|
||||
# 生产环境日志配置
|
||||
logging.level.root=INFO
|
||||
logging.level.com.example.demo=INFO
|
||||
|
||||
Reference in New Issue
Block a user