优化邮件发送功能和支付宝支付诊断

- 修复邮件服务区域配置(改为ap-hongkong)
- 增强支付宝支付错误诊断和日志
- 修复代码质量问题(OrderService、ImageToVideoTask)
- 添加支付宝支付问题排查文档
- 增加详细的错误诊断信息
This commit is contained in:
AIGC Developer
2025-11-03 13:20:30 +08:00
parent b5bbd8841e
commit 149b201300
15 changed files with 1264 additions and 121 deletions

View File

@@ -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; // 时长消耗

View File

@@ -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秒后重试

View File

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

View File

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

View File

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

View File

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

View File

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