feat: 添加噜噜支付SDK和前端懒加载指令

This commit is contained in:
AIGC Developer
2026-01-06 14:33:01 +08:00
parent a99cfa28e5
commit a66bd806b2
32 changed files with 1236 additions and 114 deletions

View File

@@ -213,6 +213,7 @@ public class StoryboardVideoApiController {
String aspectRatio = null;
Boolean hdMode = null;
java.util.List<String> referenceImages = null;
String storyboardImage = null; // 前端传递的分镜图URL
if (requestBody != null) {
if (requestBody.containsKey("duration")) {
@@ -239,10 +240,21 @@ public class StoryboardVideoApiController {
logger.info("参考图数量: {}", referenceImages.size());
}
}
if (requestBody.containsKey("storyboardImage")) {
storyboardImage = (String) requestBody.get("storyboardImage");
logger.info("前端传递的分镜图URL: {}", storyboardImage != null ? storyboardImage.substring(0, Math.min(80, storyboardImage.length())) + "..." : "null");
} else {
logger.warn("请求体中没有storyboardImage参数requestBody keys: {}", requestBody.keySet());
}
} else {
logger.warn("请求体为空");
}
// 开始生成视频,传递参数(包括参考图)
storyboardVideoService.startVideoGeneration(taskId, duration, aspectRatio, hdMode, referenceImages);
logger.info("调用startVideoGeneration: taskId={}, storyboardImage={}", taskId,
storyboardImage != null ? "有值(长度:" + storyboardImage.length() + ")" : "null");
// 开始生成视频传递参数包括参考图和分镜图URL
storyboardVideoService.startVideoGeneration(taskId, duration, aspectRatio, hdMode, referenceImages, storyboardImage);
return ResponseEntity.ok(Map.of(
"success", true,

View File

@@ -62,11 +62,11 @@ public class SystemSettings {
@Column(length = 2000)
private String storyboardSystemPrompt = "";
/** Token过期时间小时范围1-720小时默认24小时 */
/** Token过期时间小时范围1-720小时默认720小时(30天) */
@NotNull
@Min(1)
@Column(nullable = false)
private Integer tokenExpireHours = 24;
private Integer tokenExpireHours = 720;
/** AI API密钥视频和图片生成共用 */
@Column(length = 200)
@@ -172,6 +172,13 @@ public class SystemSettings {
// 限制范围在1-720小时1小时到30天
if (tokenExpireHours != null && tokenExpireHours >= 1 && tokenExpireHours <= 720) {
this.tokenExpireHours = tokenExpireHours;
} else if (tokenExpireHours != null) {
// 值超出范围时,强制设置为边界值
if (tokenExpireHours < 1) {
this.tokenExpireHours = 1;
} else {
this.tokenExpireHours = 720;
}
}
}

View File

@@ -92,7 +92,10 @@ public class UserWork {
private String tags; // 标签,用逗号分隔
@Column(name = "uploaded_images", columnDefinition = "LONGTEXT")
private String uploadedImages; // 用户上传的参考图片JSON数组用于"做同款"功能恢复
private String uploadedImages; // 用户上传的参考图片JSON数组用于"做同款"功能恢复(分镜图阶段)
@Column(name = "video_reference_images", columnDefinition = "LONGTEXT")
private String videoReferenceImages; // 视频阶段用户上传的参考图片JSON数组用于分镜视频"做同款"功能恢复
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@@ -390,6 +393,14 @@ public class UserWork {
this.uploadedImages = uploadedImages;
}
public String getVideoReferenceImages() {
return videoReferenceImages;
}
public void setVideoReferenceImages(String videoReferenceImages) {
this.videoReferenceImages = videoReferenceImages;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}

View File

@@ -216,10 +216,42 @@ public class RealAIService {
// 根据参数选择可用的模型
String modelName = selectAvailableImageToVideoModel(aspectRatio, duration, hdMode);
// 处理图片数据:如果是 URL先下载转换为 base64
String processedImageBase64 = imageBase64;
if (imageBase64.startsWith("http://") || imageBase64.startsWith("https://")) {
// 是 URL需要下载图片
logger.info("检测到图片URL开始下载: {}", imageBase64.substring(0, Math.min(100, imageBase64.length())));
try {
java.net.URL imageUrl = new java.net.URL(imageBase64);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) imageUrl.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(60000);
try (java.io.InputStream is = conn.getInputStream();
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
byte[] imageData = baos.toByteArray();
// 添加 data URI 前缀
String mimeType = conn.getContentType();
if (mimeType == null || !mimeType.startsWith("image/")) {
mimeType = "image/png"; // 默认使用 PNG
}
processedImageBase64 = "data:" + mimeType + ";base64," + Base64.getEncoder().encodeToString(imageData);
logger.info("图片下载成功,大小: {} KB", imageData.length / 1024);
}
} catch (Exception e) {
logger.error("下载图片失败: {}", e.getMessage());
throw new RuntimeException("下载参考图片失败: " + e.getMessage());
}
}
// 验证base64数据格式提取纯Base64数据用于验证
String base64DataForValidation = imageBase64;
if (imageBase64.contains(",")) {
base64DataForValidation = imageBase64.substring(imageBase64.indexOf(",") + 1);
String base64DataForValidation = processedImageBase64;
if (processedImageBase64.contains(",")) {
base64DataForValidation = processedImageBase64.substring(processedImageBase64.indexOf(",") + 1);
}
try {
Base64.getDecoder().decode(base64DataForValidation);
@@ -238,7 +270,7 @@ public class RealAIService {
// 图生视频使用 images 数组(即使只有一张图片)
// 验证并规范化图片格式参考sora2实现
List<String> imagesList = new java.util.ArrayList<>();
imagesList.add(imageBase64);
imagesList.add(processedImageBase64);
imagesList = validateImageFormat(imagesList);
Map<String, Object> requestMap = new HashMap<>();

View File

@@ -372,15 +372,52 @@ public class StoryboardVideoService {
logger.info("已保存优化后的提示词到任务: taskId={}", taskId);
} catch (Exception jsonException) {
// JSON 解析失败,使用原始优化结果作为 imagePrompt
// JSON 解析失败,使用原始优化结果作为 imagePrompt 和 videoPrompt
logger.warn("JSON 解析失败,使用原始优化结果: {}", jsonException.getMessage());
finalPrompt = optimizedResult;
// 保存优化结果到 imagePrompt 和 videoPrompt
final String optimizedPrompt = optimizedResult;
asyncTransactionTemplate.executeWithoutResult(status -> {
StoryboardVideoTask t = taskRepository.findByTaskId(taskId).orElse(null);
if (t != null) {
t.setImagePrompt(optimizedPrompt);
t.setVideoPrompt(optimizedPrompt);
taskRepository.save(t);
}
});
logger.info("JSON解析失败已保存优化结果到 imagePrompt 和 videoPrompt: taskId={}", taskId);
}
} else {
logger.info("未配置系统引导词,使用原始提示词");
// 没有系统引导词时imagePrompt 和 videoPrompt 都使用原始提示词
final String originalPrompt = prompt;
asyncTransactionTemplate.executeWithoutResult(status -> {
StoryboardVideoTask t = taskRepository.findByTaskId(taskId).orElse(null);
if (t != null) {
t.setImagePrompt(originalPrompt);
t.setVideoPrompt(originalPrompt);
taskRepository.save(t);
}
});
logger.info("已保存原始提示词到 imagePrompt 和 videoPrompt: taskId={}", taskId);
}
} catch (Exception e) {
logger.warn("提示词优化失败,使用原始提示词: {}", e.getMessage());
// 优化失败时imagePrompt 和 videoPrompt 都使用原始提示词
final String originalPrompt = prompt;
try {
asyncTransactionTemplate.executeWithoutResult(status -> {
StoryboardVideoTask t = taskRepository.findByTaskId(taskId).orElse(null);
if (t != null) {
t.setImagePrompt(originalPrompt);
t.setVideoPrompt(originalPrompt);
taskRepository.save(t);
}
});
logger.info("优化失败,已保存原始提示词到 imagePrompt 和 videoPrompt: taskId={}", taskId);
} catch (Exception saveEx) {
logger.error("保存原始提示词失败: {}", saveEx.getMessage());
}
}
logger.info("任务参数 - 原始提示词: {}, imagePrompt长度: {}, 目标比例: {}, 生成比例: {}, hdMode: {}, 有用户上传: {}, imageModel: {}",
@@ -892,20 +929,55 @@ public class StoryboardVideoService {
* @param aspectRatio 宽高比可选如果为null则使用任务中已有的值
* @param hdMode 高清模式可选如果为null则使用任务中已有的值
* @param referenceImages 参考图列表(可选)
* @param storyboardImage 前端传递的分镜图URL可选用于恢复已被视频URL覆盖的分镜图
*/
@Transactional
public void startVideoGeneration(String taskId, Integer duration, String aspectRatio, Boolean hdMode, java.util.List<String> referenceImages) {
public void startVideoGeneration(String taskId, Integer duration, String aspectRatio, Boolean hdMode, java.util.List<String> referenceImages, String storyboardImage) {
try {
logger.info("开始生成视频: taskId={}, storyboardImage={}", taskId,
storyboardImage != null ? storyboardImage.substring(0, Math.min(80, storyboardImage.length())) + "..." : "null");
// 重新加载任务
StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
.orElseThrow(() -> new RuntimeException("任务不存在: " + taskId));
logger.info("任务当前状态: taskId={}, status={}, resultUrl={}", taskId, task.getStatus(),
task.getResultUrl() != null ? task.getResultUrl().substring(0, Math.min(80, task.getResultUrl().length())) + "..." : "null");
// 检查分镜图是否已生成
if (task.getResultUrl() == null || task.getResultUrl().isEmpty()) {
// 优先使用前端传递的storyboardImage因为任务的resultUrl可能已被视频URL覆盖
String effectiveStoryboardUrl = null;
// 首先检查前端是否传递了分镜图URL
if (storyboardImage != null && !storyboardImage.isEmpty()) {
effectiveStoryboardUrl = storyboardImage;
logger.info("使用前端传递的分镜图URL: taskId={}", taskId);
} else {
// 前端没有传递检查任务中的resultUrl是否是图片
String currentResultUrl = task.getResultUrl();
if (currentResultUrl != null && !currentResultUrl.isEmpty()) {
boolean isImageUrl = currentResultUrl.contains(".png") || currentResultUrl.contains(".jpg")
|| currentResultUrl.contains(".jpeg") || currentResultUrl.contains(".webp")
|| currentResultUrl.startsWith("data:image");
if (isImageUrl) {
effectiveStoryboardUrl = currentResultUrl;
logger.info("使用任务中的分镜图URL: taskId={}", taskId);
} else {
logger.warn("任务的resultUrl不是图片URL: taskId={}, url={}", taskId, currentResultUrl.substring(0, Math.min(50, currentResultUrl.length())));
}
}
}
if (effectiveStoryboardUrl == null || effectiveStoryboardUrl.isEmpty()) {
throw new RuntimeException("分镜图尚未生成,无法生成视频");
}
// 更新任务的resultUrl为有效的分镜图URL确保后续处理能获取到
if (!effectiveStoryboardUrl.equals(task.getResultUrl())) {
task.setResultUrl(effectiveStoryboardUrl);
logger.info("已更新任务的resultUrl为分镜图URL: taskId={}", taskId);
}
// 冻结分镜视频生成积分30积分
try {
userService.freezePoints(task.getUsername(), taskId + "_vid",
@@ -919,12 +991,21 @@ public class StoryboardVideoService {
// 检查任务状态:允许从 PROCESSING、COMPLETED 或 FAILED重试状态生成视频
// 只要分镜图已生成,就允许重试生成视频
// 如果前端传递了storyboardImage也允许PENDING状态从"我的作品"页面跳转的情况)
if (task.getStatus() != StoryboardVideoTask.TaskStatus.PROCESSING &&
task.getStatus() != StoryboardVideoTask.TaskStatus.COMPLETED &&
task.getStatus() != StoryboardVideoTask.TaskStatus.FAILED) {
task.getStatus() != StoryboardVideoTask.TaskStatus.FAILED &&
!(task.getStatus() == StoryboardVideoTask.TaskStatus.PENDING && storyboardImage != null && !storyboardImage.isEmpty())) {
throw new RuntimeException("任务状态不正确,无法生成视频。当前状态: " + task.getStatus());
}
// 如果是PENDING状态且有storyboardImage更新状态为PROCESSING
if (task.getStatus() == StoryboardVideoTask.TaskStatus.PENDING && storyboardImage != null && !storyboardImage.isEmpty()) {
task.setStatus(StoryboardVideoTask.TaskStatus.PROCESSING);
task.setProgress(50);
logger.info("任务状态从PENDING更新为PROCESSING有分镜图开始视频生成: taskId={}", taskId);
}
// 更新任务参数(如果提供了新的参数)
boolean paramsUpdated = false;
if (duration != null && !duration.equals(task.getDuration())) {

View File

@@ -60,9 +60,12 @@ public class SystemSettingsService {
updated.setAiApiKey(existing.getAiApiKey());
}
// 记录保存前的tokenExpireHours值
logger.info("保存前 tokenExpireHours: {}", updated.getTokenExpireHours());
// 保存更新
SystemSettings saved = repository.save(updated);
logger.info("系统设置保存成功: id={}", saved.getId());
logger.info("系统设置保存成功: id={}, tokenExpireHours={}", saved.getId(), saved.getTokenExpireHours());
// 刷新运行时配置
if (saved.getAiApiKey() != null && !saved.getAiApiKey().isEmpty()) {

View File

@@ -294,7 +294,8 @@ public class UserWorkService {
work.setPointsCost(task.getCostPoints());
work.setStatus(UserWork.WorkStatus.COMPLETED);
work.setCompletedAt(LocalDateTime.now());
work.setUploadedImages(task.getUploadedImages()); // 同步用户上传的参考图,用于"做同款"
work.setUploadedImages(task.getUploadedImages()); // 同步分镜图阶段用户上传的参考图
work.setVideoReferenceImages(task.getVideoReferenceImages()); // 同步视频阶段用户上传的参考图,用于"做同款"
work = userWorkRepository.save(work);
logger.info("创建分镜视频作品成功: {}, 用户: {}, 分镜图: {}", work.getId(), work.getUsername(), task.getResultUrl() != null ? "" : "");

View File

@@ -20,7 +20,7 @@ public class JwtUtils {
@Value("${jwt.secret:aigc-demo-secret-key-for-jwt-token-generation}")
private String secret;
@Value("${jwt.expiration:86400000}") // 24小时,单位毫秒
@Value("${jwt.expiration:2592000000}") // 720小时(30天),单位毫秒
private Long expiration;
/**

View File

@@ -16,7 +16,7 @@ alipay.server-url=https\://openapi-sandbox.dl.alipaydev.com/gateway.do
alipay.sign-type=RSA2
app.ffmpeg.path=C\:/Users/UI/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-8.0-full_build/bin/ffmpeg.exe
app.temp.dir=./temp
jwt.expiration=86400000
jwt.expiration=2592000000
jwt.secret=mySecretKey123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
logging.level.com.example.demo=DEBUG
logging.level.org.hibernate.SQL=WARN

View File

@@ -72,7 +72,7 @@ alipay.return-url=https://vionow.com/payment/success
# JWT配置
jwt.secret=mySecretKey123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
jwt.expiration=604800000
jwt.expiration=2592000000
# 腾讯云SES配置 (生产环境)
tencent.ses.secret-id=AKIDoaEjFbqxxqZAcv8EE6oZCg2IQPG1fCxm
@@ -82,10 +82,10 @@ tencent.ses.from-email=newletter@vionow.com
tencent.ses.from-name=AIGC平台
tencent.ses.template-id=154360
# PayPal配置 (生产环境)
paypal.client-id=Adpi67TvppjhyyWhrALWwJhLFzv5S_vXoUHzWQchqZe48NaONSryg7QHKBubf0PRmkeJoaxGEKV5v9lT
paypal.client-secret=EDzZl-hddwtt2pNt5RpBIICdlrUS8QtcmAttU_kuANL8Vd937SC4xel_K2hArTovVqEtyL2ZS5IcQcQV
paypal.mode=sandbox
# PayPal配置 (生产环境 - Live模式)
paypal.client-id=AajQyk5afrKsLuDBpBKYsI3DdCUWC0B9puW3avt5SKJAaBtD73E1hYYCAK3GZFSEzsyLStyIPbuaXya4
paypal.client-secret=EH8mA05ocZkOKRXbNGhAtkP0TgK9VVw7jqQad8SVVC82rChQ47E16SnCWpFISRyzE4xAlraDkfoh-4EB
paypal.mode=live
paypal.success-url=https://vionow.com/api/payment/paypal/success
paypal.cancel-url=https://vionow.com/api/payment/paypal/cancel
@@ -132,9 +132,8 @@ app.ffmpeg.path=${FFMPEG_PATH:ffmpeg}
app.upload.path=${UPLOAD_PATH:./uploads}
# SpringDoc OpenAPI (Swagger) 配置
# 生产环境禁用以提高安全性和性能
springdoc.api-docs.enabled=false
springdoc.swagger-ui.enabled=false
springdoc.api-docs.enabled=true
springdoc.swagger-ui.enabled=true
# ============================================
# Redis 配置(生产环境 - 已禁用)

View File

@@ -39,8 +39,8 @@ app.upload.path=uploads
app.video.output.path=outputs
# JWT配置
jwt.secret=aigc-demo-secret-key-for-jwt-token-generation-2025
jwt.expiration=86400000
jwt.secret=mySecretKey123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
jwt.expiration=2592000000
# ============================================
# Redis配置