更新配置: 支付和邮件登录模块配置优化, 删除临时文档
This commit is contained in:
@@ -7,8 +7,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -35,9 +33,6 @@ public class AuthApiController {
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
@@ -45,48 +40,16 @@ public class AuthApiController {
|
||||
private VerificationCodeService verificationCodeService;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* 用户登录(已禁用,仅支持邮箱验证码登录)
|
||||
* 为了向后兼容,保留此接口但返回提示信息
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> credentials,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
try {
|
||||
String username = credentials.get("username");
|
||||
String password = credentials.get("password");
|
||||
|
||||
if (username == null || password == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户名和密码不能为空"));
|
||||
}
|
||||
|
||||
// 使用Spring Security进行认证
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(username, password);
|
||||
Authentication authentication = authenticationManager.authenticate(authToken);
|
||||
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 生成JWT Token
|
||||
String token = jwtUtils.generateToken(username, user.getRole(), user.getId());
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("success", true);
|
||||
body.put("message", "登录成功");
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("user", user);
|
||||
data.put("token", token);
|
||||
body.put("data", data);
|
||||
|
||||
logger.info("用户登录成功:{}", username);
|
||||
return ResponseEntity.ok(body);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("登录失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户名或密码错误"));
|
||||
}
|
||||
logger.warn("尝试使用用户名密码登录,但系统已禁用此方式");
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("系统已禁用用户名密码登录,请使用邮箱验证码登录"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
69
demo/src/main/java/com/example/demo/dto/MailMessage.java
Normal file
69
demo/src/main/java/com/example/demo/dto/MailMessage.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 邮件消息DTO
|
||||
* 用于封装邮件发送请求
|
||||
*/
|
||||
public class MailMessage {
|
||||
|
||||
private String toEmail;
|
||||
private String subject;
|
||||
private Long templateId;
|
||||
private Map<String, Object> templateData;
|
||||
|
||||
public MailMessage() {
|
||||
this.templateData = new HashMap<>();
|
||||
}
|
||||
|
||||
public MailMessage(String toEmail, String subject, Long templateId) {
|
||||
this();
|
||||
this.toEmail = toEmail;
|
||||
this.subject = subject;
|
||||
this.templateId = templateId;
|
||||
}
|
||||
|
||||
public String getToEmail() {
|
||||
return toEmail;
|
||||
}
|
||||
|
||||
public void setToEmail(String toEmail) {
|
||||
this.toEmail = toEmail;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public Long getTemplateId() {
|
||||
return templateId;
|
||||
}
|
||||
|
||||
public void setTemplateId(Long templateId) {
|
||||
this.templateId = templateId;
|
||||
}
|
||||
|
||||
public Map<String, Object> getTemplateData() {
|
||||
return templateData;
|
||||
}
|
||||
|
||||
public void setTemplateData(Map<String, Object> templateData) {
|
||||
this.templateData = templateData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加模板参数
|
||||
*/
|
||||
public MailMessage addParam(String key, Object value) {
|
||||
this.templateData.put(key, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,3 +197,7 @@ public class PointsFreezeRecord {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -265,3 +265,7 @@ public class TaskQueue {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -257,3 +257,7 @@ public class TaskStatus {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -65,3 +65,7 @@ public interface TaskStatusRepository extends JpaRepository<TaskStatus, Long> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ public class PlainTextPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 图片网格拼接服务
|
||||
* 用于将多张分镜图片拼接成一个网格布局
|
||||
* 参考Comfly项目的图片处理方式
|
||||
*/
|
||||
@Service
|
||||
public class ImageGridService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ImageGridService.class);
|
||||
|
||||
/**
|
||||
* 将多张图片URL拼接成一个网格图片
|
||||
*
|
||||
* @param imageUrls 图片URL列表
|
||||
* @param gridCols 网格列数,默认3列(适合6张或9张图)
|
||||
* @return Base64编码的拼接后图片
|
||||
*/
|
||||
public String mergeImagesToGrid(List<String> imageUrls, int gridCols) {
|
||||
if (imageUrls == null || imageUrls.isEmpty()) {
|
||||
throw new IllegalArgumentException("图片URL列表不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
int imageCount = imageUrls.size();
|
||||
// 自动计算网格布局:如果未指定列数,根据图片数量自动计算
|
||||
if (gridCols <= 0) {
|
||||
gridCols = calculateOptimalColumns(imageCount);
|
||||
}
|
||||
int gridRows = (int) Math.ceil((double) imageCount / gridCols);
|
||||
|
||||
logger.info("开始拼接图片网格: 图片数量={}, 列数={}, 行数={}", imageCount, gridCols, gridRows);
|
||||
|
||||
// 读取所有图片
|
||||
BufferedImage[] images = new BufferedImage[imageCount];
|
||||
int maxWidth = 512; // 默认最小宽度,避免网格尺寸为0
|
||||
int maxHeight = 512; // 默认最小高度,避免网格尺寸为0
|
||||
|
||||
for (int i = 0; i < imageCount; i++) {
|
||||
BufferedImage img = loadImageFromUrl(imageUrls.get(i));
|
||||
if (img != null) {
|
||||
images[i] = img;
|
||||
maxWidth = Math.max(maxWidth, img.getWidth());
|
||||
maxHeight = Math.max(maxHeight, img.getHeight());
|
||||
} else {
|
||||
logger.warn("无法加载图片: {}", imageUrls.get(i));
|
||||
// 创建一个空白图片占位
|
||||
BufferedImage placeholder = new BufferedImage(512, 512, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = placeholder.createGraphics();
|
||||
g.setColor(java.awt.Color.WHITE);
|
||||
g.fillRect(0, 0, 512, 512);
|
||||
g.dispose();
|
||||
images[i] = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建网格图片,每张图片使用统一尺寸
|
||||
int cellWidth = maxWidth;
|
||||
int cellHeight = maxHeight;
|
||||
BufferedImage gridImage = new BufferedImage(
|
||||
gridCols * cellWidth,
|
||||
gridRows * cellHeight,
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
|
||||
Graphics2D g = gridImage.createGraphics();
|
||||
g.setColor(java.awt.Color.WHITE);
|
||||
g.fillRect(0, 0, gridImage.getWidth(), gridImage.getHeight());
|
||||
|
||||
// 将图片放置到网格中
|
||||
for (int i = 0; i < imageCount; i++) {
|
||||
int row = i / gridCols;
|
||||
int col = i % gridCols;
|
||||
int x = col * cellWidth;
|
||||
int y = row * cellHeight;
|
||||
|
||||
BufferedImage img = images[i];
|
||||
// 如果图片尺寸不同,居中放置
|
||||
int imgX = x + (cellWidth - img.getWidth()) / 2;
|
||||
int imgY = y + (cellHeight - img.getHeight()) / 2;
|
||||
|
||||
g.drawImage(img, imgX, imgY, null);
|
||||
}
|
||||
|
||||
g.dispose();
|
||||
|
||||
// 转换为Base64
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(gridImage, "PNG", baos);
|
||||
byte[] imageBytes = baos.toByteArray();
|
||||
String base64 = Base64.getEncoder().encodeToString(imageBytes);
|
||||
|
||||
logger.info("图片网格拼接完成: 总尺寸={}x{}", gridImage.getWidth(), gridImage.getHeight());
|
||||
|
||||
return "data:image/png;base64," + base64;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("拼接图片网格失败", e);
|
||||
throw new RuntimeException("图片拼接失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算最优列数
|
||||
*/
|
||||
private int calculateOptimalColumns(int imageCount) {
|
||||
// 根据图片数量选择最佳列数
|
||||
if (imageCount <= 2) return 2;
|
||||
if (imageCount <= 4) return 2;
|
||||
if (imageCount <= 6) return 3;
|
||||
if (imageCount <= 9) return 3;
|
||||
if (imageCount <= 12) return 4;
|
||||
return 4; // 默认4列
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL加载图片
|
||||
*/
|
||||
private BufferedImage loadImageFromUrl(String imageUrl) {
|
||||
try {
|
||||
// 处理base64格式的图片
|
||||
if (imageUrl.startsWith("data:image")) {
|
||||
String base64Data = imageUrl.substring(imageUrl.indexOf(",") + 1);
|
||||
byte[] imageBytes = Base64.getDecoder().decode(base64Data);
|
||||
return ImageIO.read(new java.io.ByteArrayInputStream(imageBytes));
|
||||
}
|
||||
|
||||
// 处理普通URL
|
||||
URI uri = new URI(imageUrl);
|
||||
URL url = uri.toURL();
|
||||
return ImageIO.read(url);
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
logger.error("加载图片失败: {}", imageUrl, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,25 +388,40 @@ public class RealAIService {
|
||||
|
||||
/**
|
||||
* 提交文生图任务(分镜视频使用)
|
||||
* 调用Qwen文生图API
|
||||
* 调用Comfly API的文生图接口,参考Comfly项目的Comfly_qwen_image节点实现
|
||||
* 支持分镜图生成,可以生成多张图片用于拼接成分镜图网格
|
||||
*
|
||||
* @param prompt 提示词
|
||||
* @param aspectRatio 宽高比
|
||||
* @param numImages 生成图片数量,默认6张用于分镜图
|
||||
* @return API响应,包含多张图片的URL
|
||||
*/
|
||||
public Map<String, Object> submitTextToImageTask(String prompt, String aspectRatio) {
|
||||
public Map<String, Object> submitTextToImageTask(String prompt, String aspectRatio, int numImages) {
|
||||
try {
|
||||
logger.info("提交文生图任务: prompt={}, aspectRatio={}", prompt, aspectRatio);
|
||||
logger.info("提交文生图任务: prompt={}, aspectRatio={}, numImages={}", prompt, aspectRatio, numImages);
|
||||
|
||||
// 根据aspectRatio转换尺寸
|
||||
// 限制生成图片数量在1-12之间,参考Comfly项目的限制
|
||||
if (numImages < 1) {
|
||||
numImages = 1;
|
||||
} else if (numImages > 12) {
|
||||
numImages = 12;
|
||||
}
|
||||
|
||||
// 根据aspectRatio转换尺寸,参考Comfly项目的尺寸映射
|
||||
String size = convertAspectRatioToImageSize(aspectRatio);
|
||||
|
||||
// 使用文生图的API端点(Comfly API)
|
||||
String url = aiImageApiBaseUrl + "/v1/images/generations";
|
||||
|
||||
// 构建请求体
|
||||
// 构建请求体,参考Comfly_qwen_image节点的参数设置
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("prompt", prompt);
|
||||
requestBody.put("size", size);
|
||||
requestBody.put("model", "qwen-image");
|
||||
requestBody.put("n", 1);
|
||||
requestBody.put("n", numImages); // 支持生成多张图片
|
||||
requestBody.put("response_format", "url");
|
||||
// 添加guidance_scale参数以提高图片质量(参考Comfly项目)
|
||||
requestBody.put("guidance_scale", 2.5);
|
||||
|
||||
String requestBodyJson = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
@@ -451,15 +466,16 @@ public class RealAIService {
|
||||
|
||||
/**
|
||||
* 将宽高比转换为图片尺寸
|
||||
* 参考Comfly项目的尺寸映射,支持分镜图生成的各种常用尺寸
|
||||
*/
|
||||
private String convertAspectRatioToImageSize(String aspectRatio) {
|
||||
return switch (aspectRatio) {
|
||||
case "16:9" -> "1024x576";
|
||||
case "9:16" -> "576x1024";
|
||||
case "4:3" -> "1024x768";
|
||||
case "3:4" -> "768x1024";
|
||||
case "1:1" -> "1024x1024";
|
||||
default -> "1024x768";
|
||||
case "16:9" -> "1024x576"; // 横屏,适合宽屏分镜
|
||||
case "9:16" -> "576x1024"; // 竖屏,适合手机视频分镜
|
||||
case "4:3" -> "1024x768"; // 标准横屏
|
||||
case "3:4" -> "768x1024"; // 标准竖屏
|
||||
case "1:1" -> "1024x1024"; // 正方形
|
||||
default -> "1024x768"; // 默认标准横屏
|
||||
};
|
||||
}
|
||||
|
||||
@@ -654,12 +670,14 @@ public class RealAIService {
|
||||
优化要求:
|
||||
1. 将中文描述翻译成流畅的英文,确保语义准确
|
||||
2. 关注镜头构图、画面布局、视觉元素(如:composition, framing, visual hierarchy等)
|
||||
3. 适合生成12格黑白分镜图风格,强调构图和画面元素
|
||||
3. 适合生成专业分镜图风格,强调构图和画面元素,包含清晰的场景描述和视觉细节
|
||||
4. 确保提示词清晰描述每个镜头的关键视觉元素和构图方式
|
||||
5. 使用专业的电影分镜术语(如:establishing shot, medium shot, close-up等)
|
||||
6. 如果原始提示词已经是英文,直接优化,保持语言一致
|
||||
7. 输出优化后的提示词,不要添加额外说明、引号或其他格式标记
|
||||
8. 优化后的提示词应该直接可用,长度控制在合理范围内
|
||||
5. 使用专业的电影分镜术语(如:establishing shot, medium shot, close-up, wide shot, over-the-shoulder等)
|
||||
6. 添加画面细节描述:场景环境、人物/物体的位置、光线、氛围等
|
||||
7. 如果原始提示词已经是英文,直接优化,保持语言一致
|
||||
8. 输出优化后的提示词,不要添加额外说明、引号或其他格式标记
|
||||
9. 优化后的提示词应该直接可用,长度控制在合理范围内(200-500词)
|
||||
10. 参考格式:"(scene description), (shot type), (composition details), (visual elements), (lighting and atmosphere), professional storyboard style, cinematic framing"
|
||||
""";
|
||||
default -> """
|
||||
你是一个专业的提示词优化专家。请将用户提供的简单描述优化为详细、专业的英文提示词。
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -32,6 +33,12 @@ public class StoryboardVideoService {
|
||||
@Autowired
|
||||
private RealAIService realAIService;
|
||||
|
||||
@Autowired
|
||||
private ImageGridService imageGridService;
|
||||
|
||||
// 默认生成6张分镜图
|
||||
private static final int DEFAULT_STORYBOARD_IMAGES = 6;
|
||||
|
||||
/**
|
||||
* 创建分镜视频任务
|
||||
*/
|
||||
@@ -92,43 +99,57 @@ public class StoryboardVideoService {
|
||||
task.updateStatus(StoryboardVideoTask.TaskStatus.PROCESSING);
|
||||
taskRepository.flush(); // 强制刷新到数据库
|
||||
|
||||
// 调用真实文生图API
|
||||
logger.info("分镜视频任务已提交,正在调用文生图API生成分镜图...");
|
||||
// 调用真实文生图API,生成多张分镜图
|
||||
logger.info("分镜视频任务已提交,正在调用文生图API生成{}张分镜图...", DEFAULT_STORYBOARD_IMAGES);
|
||||
|
||||
Map<String, Object> apiResponse = realAIService.submitTextToImageTask(
|
||||
task.getPrompt(),
|
||||
task.getAspectRatio()
|
||||
task.getAspectRatio(),
|
||||
DEFAULT_STORYBOARD_IMAGES // 生成多张图片用于分镜图
|
||||
);
|
||||
|
||||
// 从API响应中提取图片URL
|
||||
// 从API响应中提取所有图片URL
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> data = (List<Map<String, Object>>) apiResponse.get("data");
|
||||
if (data != null && !data.isEmpty()) {
|
||||
String imageUrl = null;
|
||||
Map<String, Object> firstImage = data.get(0);
|
||||
|
||||
// 检查是否有url或b64_json字段
|
||||
if (firstImage.get("url") != null) {
|
||||
imageUrl = (String) firstImage.get("url");
|
||||
} else if (firstImage.get("b64_json") != null) {
|
||||
// base64编码的图片
|
||||
String base64Data = (String) firstImage.get("b64_json");
|
||||
imageUrl = "data:image/png;base64," + base64Data;
|
||||
// 收集所有图片URL
|
||||
List<String> imageUrls = new ArrayList<>();
|
||||
for (Map<String, Object> imageData : data) {
|
||||
String imageUrl = null;
|
||||
if (imageData.get("url") != null) {
|
||||
imageUrl = (String) imageData.get("url");
|
||||
} else if (imageData.get("b64_json") != null) {
|
||||
// base64编码的图片
|
||||
String base64Data = (String) imageData.get("b64_json");
|
||||
imageUrl = "data:image/png;base64," + base64Data;
|
||||
}
|
||||
if (imageUrl != null) {
|
||||
imageUrls.add(imageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrls.isEmpty()) {
|
||||
throw new RuntimeException("未能从API响应中提取任何图片URL");
|
||||
}
|
||||
|
||||
logger.info("成功获取{}张图片,开始拼接成分镜图网格...", imageUrls.size());
|
||||
|
||||
// 拼接多张图片成网格
|
||||
String mergedImageUrl = imageGridService.mergeImagesToGrid(imageUrls, 0); // 0表示自动计算列数
|
||||
|
||||
// 重新加载任务(因为之前的flush可能使实体detached)
|
||||
task = taskRepository.findByTaskId(taskId)
|
||||
.orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
|
||||
|
||||
// 设置结果
|
||||
task.setResultUrl(imageUrl);
|
||||
// 设置拼接后的结果图片URL
|
||||
task.setResultUrl(mergedImageUrl);
|
||||
task.setRealTaskId(taskId + "_image");
|
||||
task.updateStatus(StoryboardVideoTask.TaskStatus.COMPLETED);
|
||||
task.updateProgress(100);
|
||||
|
||||
taskRepository.save(task);
|
||||
|
||||
logger.info("分镜图生成完成,任务ID: {}, 图片URL: {}", taskId, imageUrl);
|
||||
logger.info("分镜图生成并拼接完成,任务ID: {}, 共生成{}张图片", taskId, imageUrls.size());
|
||||
} else {
|
||||
throw new RuntimeException("API返回的图片数据为空");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
import com.tencentcloudapi.ses.v20201002.SesClient;
|
||||
import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
|
||||
import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
|
||||
import com.tencentcloudapi.ses.v20201002.models.Template;
|
||||
|
||||
/**
|
||||
* 腾讯云SES邮件推送服务
|
||||
* 基于 mails-over-tencent-cloud 模块实现
|
||||
* 参考:https://github.com/starter-go/mails-over-tencent-cloud
|
||||
*/
|
||||
@Service
|
||||
public class TencentSesMailService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TencentSesMailService.class);
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Value("${tencent.ses.secret-id}")
|
||||
private String secretId;
|
||||
|
||||
@Value("${tencent.ses.secret-key}")
|
||||
private String secretKey;
|
||||
|
||||
@Value("${tencent.ses.region:ap-beijing}")
|
||||
private String region;
|
||||
|
||||
@Value("${tencent.ses.from-email}")
|
||||
private String fromEmail;
|
||||
|
||||
@Value("${tencent.ses.from-name:AIGC平台}")
|
||||
private String fromName;
|
||||
|
||||
/**
|
||||
* 获取腾讯云SES客户端
|
||||
*/
|
||||
private SesClient getClient() {
|
||||
Credential cred = new Credential(secretId, secretKey);
|
||||
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
httpProfile.setEndpoint("ses.tencentcloudapi.com");
|
||||
|
||||
ClientProfile clientProfile = new ClientProfile();
|
||||
clientProfile.setHttpProfile(httpProfile);
|
||||
|
||||
return new SesClient(cred, region, clientProfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件(使用模板)
|
||||
*
|
||||
* @param toEmail 收件人邮箱
|
||||
* @param subject 邮件主题
|
||||
* @param templateId 模板ID(在腾讯云SES控制台创建)
|
||||
* @param templateData 模板数据(JSON格式的Map)
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendEmailWithTemplate(String toEmail, String subject, Long templateId, Map<String, Object> templateData) {
|
||||
try {
|
||||
logger.info("开始发送邮件,收件人: {}, 主题: {}, 模板ID: {}", toEmail, subject, templateId);
|
||||
|
||||
SesClient client = getClient();
|
||||
SendEmailRequest req = new SendEmailRequest();
|
||||
|
||||
// 设置发件人
|
||||
req.setFromEmailAddress(fromEmail);
|
||||
|
||||
// 设置收件人(仅支持单个收件人)
|
||||
req.setDestination(new String[]{toEmail});
|
||||
|
||||
// 设置邮件主题
|
||||
req.setSubject(subject);
|
||||
|
||||
// 设置模板
|
||||
Template template = new Template();
|
||||
template.setTemplateID(templateId);
|
||||
|
||||
// 将模板数据转换为JSON字符串
|
||||
String templateDataJson = objectMapper.writeValueAsString(templateData);
|
||||
template.setTemplateData(templateDataJson);
|
||||
|
||||
req.setTemplate(template);
|
||||
|
||||
// 设置触发类型:1-批量触发(模板邮件)
|
||||
req.setTriggerType(1L);
|
||||
|
||||
// 调用API发送邮件
|
||||
SendEmailResponse resp = client.SendEmail(req);
|
||||
|
||||
String messageId = resp.getMessageId();
|
||||
logger.info("邮件发送成功,收件人: {}, 消息ID: {}", toEmail, messageId);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (TencentCloudSDKException e) {
|
||||
logger.error("腾讯云SES API调用失败,收件人: {}, 错误码: {}, 错误信息: {}",
|
||||
toEmail, e.getErrorCode(), e.getMessage(), e);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
logger.error("发送邮件异常,收件人: {}", toEmail, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码邮件(使用默认模板)
|
||||
*
|
||||
* @param toEmail 收件人邮箱
|
||||
* @param code 验证码
|
||||
* @param templateId 验证码邮件模板ID(从配置读取)
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendVerificationCodeEmail(String toEmail, String code, Long templateId) {
|
||||
Map<String, Object> templateData = new HashMap<>();
|
||||
templateData.put("code", code);
|
||||
|
||||
return sendEmailWithTemplate(toEmail, "验证码", templateId, templateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通用模板邮件
|
||||
*
|
||||
* @param toEmail 收件人邮箱
|
||||
* @param subject 邮件主题
|
||||
* @param templateId 模板ID
|
||||
* @param params 模板参数(键值对)
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendTemplateEmail(String toEmail, String subject, Long templateId, Map<String, Object> params) {
|
||||
return sendEmailWithTemplate(toEmail, subject, templateId, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送JSON格式模板数据的邮件
|
||||
*
|
||||
* @param toEmail 收件人邮箱
|
||||
* @param subject 邮件主题
|
||||
* @param templateId 模板ID
|
||||
* @param jsonData JSON格式的模板数据字符串
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendEmailWithJsonData(String toEmail, String subject, Long templateId, String jsonData) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> templateData = objectMapper.readValue(jsonData, Map.class);
|
||||
return sendEmailWithTemplate(toEmail, subject, templateId, templateData);
|
||||
} catch (Exception e) {
|
||||
logger.error("解析JSON模板数据失败: {}", jsonData, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
// import com.example.demo.config.TencentCloudConfig;
|
||||
// import com.tencentcloudapi.common.Credential;
|
||||
// import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||
// import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
// import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
// import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||
// import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||
// import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||
// import com.tencentcloudapi.ses.v20201002.SesClient;
|
||||
// import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
|
||||
// import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
// import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 验证码服务
|
||||
*/
|
||||
@@ -30,8 +20,11 @@ public class VerificationCodeService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VerificationCodeService.class);
|
||||
|
||||
// @Autowired
|
||||
// private TencentCloudConfig tencentCloudConfig;
|
||||
@Autowired
|
||||
private TencentSesMailService tencentSesMailService;
|
||||
|
||||
@Value("${tencent.ses.template-id:0}")
|
||||
private Long templateId;
|
||||
|
||||
// 使用内存存储验证码
|
||||
private final ConcurrentHashMap<String, String> verificationCodes = new ConcurrentHashMap<>();
|
||||
@@ -152,20 +145,30 @@ public class VerificationCodeService {
|
||||
|
||||
|
||||
/**
|
||||
* 发送邮件(简化版本,实际使用时需要配置正确的腾讯云SES API)
|
||||
* 发送邮件(使用腾讯云SES邮件推送服务)
|
||||
*/
|
||||
private boolean sendEmail(String email, String code) {
|
||||
try {
|
||||
// TODO: 实现腾讯云SES邮件发送
|
||||
// 这里暂时使用日志输出,实际部署时需要配置正确的腾讯云SES API
|
||||
logger.info("发送邮件验证码到: {}, 验证码: {}", email, code);
|
||||
// 如果没有配置模板ID,使用开发模式(仅记录日志)
|
||||
if (templateId == null || templateId == 0) {
|
||||
logger.warn("未配置邮件模板ID,使用开发模式。验证码: {}, 邮箱: {}", code, email);
|
||||
logger.info("开发模式:邮件验证码发送到: {}, 验证码: {}", email, code);
|
||||
return true; // 开发模式下返回成功
|
||||
}
|
||||
|
||||
// 在实际环境中,这里应该调用腾讯云SES API
|
||||
// 由于腾讯云SES API配置较复杂,这里先返回true进行测试
|
||||
return true;
|
||||
// 使用腾讯云SES发送邮件
|
||||
boolean success = tencentSesMailService.sendVerificationCodeEmail(email, code, templateId);
|
||||
|
||||
if (success) {
|
||||
logger.info("邮件验证码发送成功,邮箱: {}", email);
|
||||
} else {
|
||||
logger.error("邮件验证码发送失败,邮箱: {}", email);
|
||||
}
|
||||
|
||||
return success;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("邮件发送失败", e);
|
||||
logger.error("邮件发送异常,邮箱: {}", email, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,16 @@ jwt.secret=mySecretKey1234567890123456789012345678901234567890123456789012345678
|
||||
jwt.expiration=86400000
|
||||
|
||||
# 腾讯云SES配置
|
||||
tencent.ses.secret-id=AKIDz8krbsJ5yKBZQpn74WFkmLPx3gnPhESA
|
||||
tencent.ses.secret-key=Gu5t9xGARNpqDXcd3I4WZkFpAALPVZ7pKbNScfFsj
|
||||
# 主账号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.from-name=AIGC平台
|
||||
# 邮件模板ID(在腾讯云SES控制台创建模板后获取)
|
||||
# 如果未配置或为0,将使用开发模式(仅记录日志)
|
||||
tencent.ses.template-id=0
|
||||
|
||||
# AI API配置
|
||||
# 文生视频、图生视频、分镜视频都使用Comfly API
|
||||
|
||||
@@ -24,3 +24,7 @@ CREATE TABLE IF NOT EXISTS task_queue (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -23,3 +23,7 @@ CREATE TABLE IF NOT EXISTS points_freeze_records (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,3 +26,7 @@ CREATE TABLE task_status (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -568,6 +568,10 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -484,6 +484,10 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -523,6 +523,10 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user