package com.example.demo.service;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.imageio.ImageIO;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import com.example.demo.model.StoryboardVideoTask;
import com.example.demo.model.TaskStatus;
import com.example.demo.model.enums.CommonTaskType;
import com.example.demo.repository.StoryboardVideoTaskRepository;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 分镜视频服务类
 * 注意：不在类级别使用 @Transactional，因为某些方法需要禁用事务（如长时间运行的外部API调用）
 */
@Service
public class StoryboardVideoService {

    private static final Logger logger = LoggerFactory.getLogger(StoryboardVideoService.class);

    @Autowired
    private StoryboardVideoTaskRepository taskRepository;

    @Autowired
    private RealAIService realAIService;

    @Autowired
    private ImageGridService imageGridService;

    @Autowired
    private TaskQueueService taskQueueService;

    @Autowired
    private UserWorkService userWorkService;

    @Autowired
    private UserService userService;

    @Autowired
    private TaskStatusPollingService taskStatusPollingService;

    @Autowired
    private ApplicationContext applicationContext;
    
    @Autowired
    private org.springframework.transaction.support.TransactionTemplate asyncTransactionTemplate;
    
    @Autowired
    private org.springframework.transaction.support.TransactionTemplate readOnlyTransactionTemplate;
    
    @Autowired
    private CosService cosService;
    
    @Autowired
    private SystemSettingsService systemSettingsService;
    
    @Autowired
    private java.util.concurrent.Executor taskExecutor;
    
    // 默认生成1张分镜图
    private static final int DEFAULT_STORYBOARD_IMAGES = 1;
    
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 创建分镜视频任务
     * 注意：使用 REQUIRES_NEW 确保事务快速提交，避免长时间占用连接
     * 事务提交后，异步方法在事务外执行
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public StoryboardVideoTask createTask(String username, String prompt, String aspectRatio, boolean hdMode, String imageUrl, Integer duration, String imageModel, List<String> uploadedImages) {
        // 验证参数
        if (username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        if (prompt == null || prompt.trim().isEmpty()) {
            throw new IllegalArgumentException("文本描述不能为空");
        }
        
        // 检查用户所有类型任务的总数（统一检查）
        userWorkService.checkMaxConcurrentTasks(username);
        
        // 生成任务ID
        String taskId = generateTaskId();
        
        // 冻结分镜图生成积分
        int storyboardImagePts = systemSettingsService.getOrCreate().getStoryboardImagePoints();
        try {
            userService.freezePoints(username, taskId + "_img", 
                CommonTaskType.STORYBOARD_IMAGE, 
                storyboardImagePts, "分镜图生成");
            logger.info("分镜图积分冻结成功: taskId={}, username={}, points={}", taskId, username, storyboardImagePts);
        } catch (Exception e) {
            logger.error("分镜图积分冻结失败: taskId={}, username={}", taskId, username, e);
            throw new RuntimeException("积分不足，无法创建任务: " + e.getMessage());
        }
        
        // 创建任务
        StoryboardVideoTask task = new StoryboardVideoTask(username, prompt.trim(), aspectRatio, hdMode, duration);
        task.setTaskId(taskId);
        task.setStatus(StoryboardVideoTask.TaskStatus.PROCESSING);  // 直接设为生成中
        task.setProgress(0);
        
        // 设置图像生成模型
        if (imageModel != null && !imageModel.isEmpty()) {
            task.setImageModel(imageModel);
        }
        
        // 保存用户上传的多张图片（上传到COS后存储URL）
        if (uploadedImages != null && !uploadedImages.isEmpty()) {
            try {
                List<String> processedImages = new ArrayList<>();
                int imageIndex = 0;
                for (String img : uploadedImages) {
                    if (img == null || img.isEmpty() || "null".equals(img)) {
                        continue;
                    }
                    // 如果是Base64图片且COS启用，上传到COS
                    if (img.startsWith("data:image") && cosService.isEnabled()) {
                        try {
                            String cosUrl = cosService.uploadBase64Image(img, "uploaded_" + taskId + "_" + imageIndex + ".png");
                            if (cosUrl != null && !cosUrl.isEmpty()) {
                                processedImages.add(cosUrl);
                                logger.info("用户上传图片{}上传COS成功: taskId={}", imageIndex, taskId);
                            } else {
                                // COS上传失败，保留原始Base64
                                processedImages.add(img);
                                logger.warn("用户上传图片{}上传COS失败，保留Base64: taskId={}", imageIndex, taskId);
                            }
                        } catch (Exception e) {
                            // 异常时保留原始Base64
                            processedImages.add(img);
                            logger.error("用户上传图片{}上传COS异常: taskId={}", imageIndex, taskId, e);
                        }
                    } else {
                        // 非Base64或COS未启用，直接保留
                        processedImages.add(img);
                    }
                    imageIndex++;
                }
                
                if (!processedImages.isEmpty()) {
                    String uploadedImagesJson = objectMapper.writeValueAsString(processedImages);
                    task.setUploadedImages(uploadedImagesJson);
                    logger.info("保存用户上传的图片: taskId={}, 数量={}", taskId, processedImages.size());
                }
            } catch (Exception e) {
                logger.error("处理上传图片失败: taskId={}", taskId, e);
            }
        }
        
        // 上传用户参考图片到COS
        if (imageUrl != null && !imageUrl.isEmpty()) {
            String finalImageUrl = imageUrl;
            // 如果是Base64图片且COS启用，上传到COS
            if (imageUrl.startsWith("data:image") && cosService.isEnabled()) {
                try {
                    logger.info("开始上传用户参考图片到COS: taskId={}", taskId);
                    String cosUrl = cosService.uploadBase64Image(imageUrl, "ref_" + taskId + ".png");
                    if (cosUrl != null && !cosUrl.isEmpty()) {
                        finalImageUrl = cosUrl;
                        logger.info("用户参考图片上传COS成功: taskId={}, url={}", taskId, cosUrl);
                    } else {
                        logger.warn("用户参考图片上传COS失败，使用原始URL: taskId={}", taskId);
                    }
                } catch (Exception e) {
                    logger.error("用户参考图片上传COS异常: taskId={}", taskId, e);
                }
            }
            task.setImageUrl(finalImageUrl);
        }
        
        // 保存任务（快速完成，事务立即提交）
        task = taskRepository.save(task);
        
        logger.info("分镜视频任务创建成功: {}, 用户: {}", taskId, username);
        
        // 创建PROCESSING状态的UserWork，以便用户刷新页面后能恢复任务，同时在"我的作品"中显示
        try {
            userWorkService.createProcessingStoryboardVideoWork(task);
            logger.info("创建PROCESSING状态分镜视频作品成功: {}", taskId);
        } catch (Exception e) {
            logger.warn("创建PROCESSING状态作品失败（不影响任务执行）: {}", taskId, e);
        }
        
        // 注意：异步方法调用必须在事务提交后执行，避免占用连接
        // 使用 TransactionSynchronizationManager 确保在事务提交后再调用异步方法
        // 通过 ApplicationContext 获取代理对象，确保 @Async 生效
        // 注意：获取代理对象的操作在事务内，但这是轻量级操作，不会长时间占用连接
        final String finalTaskId = taskId;
        final StoryboardVideoService self = applicationContext.getBean(StoryboardVideoService.class);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 事务提交后，在事务外执行异步方法
                // 使用代理对象调用，确保 @Async 生效
                self.processTaskAsync(finalTaskId);
            }
        });
        
        // 方法立即返回，事务快速提交
        return task;
    }

    /**
     * 直接使用上传的分镜图创建视频任务（跳过分镜图生成）
     * 用户在STEP2上传分镜图后直接生成视频时调用
     * 只冻结视频生成积分（30积分），不冻结分镜图生成积分
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public StoryboardVideoTask createVideoDirectTask(String username, String prompt, String storyboardImage, 
            String aspectRatio, boolean hdMode, Integer duration, List<String> referenceImages) {
        // 验证参数
        if (username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        if (storyboardImage == null || storyboardImage.trim().isEmpty()) {
            throw new IllegalArgumentException("分镜图不能为空");
        }
        
        // 检查用户所有类型任务的总数
        userWorkService.checkMaxConcurrentTasks(username);
        
        // 生成任务ID
        String taskId = generateTaskId();
        
        // 只冻结视频生成积分，不冻结分镜图生成积分
        int storyboardVideoPts = systemSettingsService.getOrCreate().getStoryboardVideoPoints();
        try {
            userService.freezePoints(username, taskId + "_vid", 
                CommonTaskType.STORYBOARD_VIDEO, 
                storyboardVideoPts, "分镜视频生成（直接上传分镜图）");
            logger.info("视频积分冻结成功: taskId={}, username={}, points={}", taskId, username, storyboardVideoPts);
        } catch (Exception e) {
            logger.error("视频积分冻结失败: taskId={}, username={}", taskId, username, e);
            throw new RuntimeException("积分不足，无法创建任务: " + e.getMessage());
        }
        
        // 创建任务
        StoryboardVideoTask task = new StoryboardVideoTask(username, prompt != null ? prompt.trim() : "根据分镜图生成视频", aspectRatio, hdMode, duration);
        task.setTaskId(taskId);
        task.setStatus(StoryboardVideoTask.TaskStatus.PROCESSING);  // 直接设为生成中
        task.setProgress(50);  // 分镜图已完成，从50%开始
        
        // 处理分镜图：如果是Base64，上传到COS
        String finalStoryboardImage = storyboardImage;
        if (storyboardImage.startsWith("data:image") && cosService.isEnabled()) {
            try {
                String cosUrl = cosService.uploadBase64Image(storyboardImage, "storyboard_" + taskId + ".png");
                if (cosUrl != null && !cosUrl.isEmpty()) {
                    finalStoryboardImage = cosUrl;
                    logger.info("分镜图上传COS成功: taskId={}", taskId);
                }
            } catch (Exception e) {
                logger.error("分镜图上传COS异常: taskId={}", taskId, e);
            }
        }
        task.setResultUrl(finalStoryboardImage);  // 设置分镜图URL
        
        // 处理视频参考图
        if (referenceImages != null && !referenceImages.isEmpty()) {
            try {
                List<String> processedImages = new ArrayList<>();
                int imageIndex = 0;
                for (String img : referenceImages) {
                    if (img == null || img.isEmpty() || "null".equals(img)) {
                        continue;
                    }
                    if (img.startsWith("data:image") && cosService.isEnabled()) {
                        try {
                            String cosUrl = cosService.uploadBase64Image(img, "videoref_" + taskId + "_" + imageIndex + ".png");
                            if (cosUrl != null && !cosUrl.isEmpty()) {
                                processedImages.add(cosUrl);
                            } else {
                                processedImages.add(img);
                            }
                        } catch (Exception e) {
                            processedImages.add(img);
                        }
                    } else {
                        processedImages.add(img);
                    }
                    imageIndex++;
                }
                
                if (!processedImages.isEmpty()) {
                    String videoRefImagesJson = objectMapper.writeValueAsString(processedImages);
                    task.setVideoReferenceImages(videoRefImagesJson);
                    logger.info("保存视频参考图: taskId={}, 数量={}", taskId, processedImages.size());
                }
            } catch (Exception e) {
                logger.error("处理视频参考图失败: taskId={}", taskId, e);
            }
        }
        
        // 保存任务
        task = taskRepository.save(task);
        logger.info("直接视频任务创建成功: {}, 用户: {}", taskId, username);
        
        // 创建PROCESSING状态的UserWork
        try {
            userWorkService.createProcessingStoryboardVideoWork(task);
            logger.info("创建PROCESSING状态分镜视频作品成功: {}", taskId);
        } catch (Exception e) {
            logger.warn("创建PROCESSING状态作品失败（不影响任务执行）: {}", taskId, e);
        }
        
        // 事务提交后，直接开始视频生成（跳过分镜图生成）
        final String finalTaskId = taskId;
        final StoryboardVideoService self = applicationContext.getBean(StoryboardVideoService.class);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 直接调用视频生成，跳过分镜图生成
                self.processVideoGenerationAsync(finalTaskId);
            }
        });
        
        return task;
    }

    /**
     * 直接处理视频生成（异步）- 跳过分镜图生成
     * 用于用户直接上传分镜图后生成视频的场景
     */
    @Async("taskExecutor")
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void processVideoGenerationAsync(String taskId) {
        try {
            logger.info("开始直接视频生成（跳过分镜图生成）: taskId={}", taskId);
            
            // 短暂等待确保数据库同步完成
            Thread.sleep(500);
            
            // 将任务添加到队列并直接处理
            StoryboardVideoTask task = loadTaskInfoWithTransactionTemplate(taskId);
            if (task == null) {
                logger.error("任务不存在: taskId={}", taskId);
                return;
            }
            
            // 添加到任务队列
            try {
                taskQueueService.addStoryboardVideoTask(task.getUsername(), taskId);
                logger.info("视频任务已添加到队列: taskId={}", taskId);
            } catch (Exception e) {
                logger.error("添加视频任务到队列失败: taskId={}", taskId, e);
                // 返还积分
                try {
                    userService.returnFrozenPoints(taskId + "_vid");
                } catch (Exception re) {
                    logger.warn("返还积分失败: taskId={}_vid", taskId);
                }
                throw e;
            }
            
            // 直接处理视频生成
            taskQueueService.processStoryboardVideoTaskDirectly(taskId);
            
        } catch (Exception e) {
            logger.error("直接视频生成失败: taskId={}", taskId, e);
            // 更新任务状态为失败
            try {
                updateTaskStatusToFailed(taskId, "视频生成失败: " + e.getMessage());
            } catch (Exception ue) {
                logger.error("更新任务状态失败: taskId={}", taskId, ue);
            }
        }
    }

    /**
     * 使用真实API处理任务（异步）
     * 注意：此方法明确禁用事务，因为长时间运行的外部API调用会占用数据库连接
     * 只在需要数据库操作时使用单独的事务方法
     */
    @Async("taskExecutor")
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void processTaskAsync(String taskId) {
        try {
            logger.info("开始使用真实API处理分镜视频任务: {}", taskId);
            
            // 在异步方法中使用 TransactionTemplate 手动管理事务，确保事务正确关闭
            StoryboardVideoTask taskInfo = loadTaskInfoWithTransactionTemplate(taskId);
            String prompt = taskInfo.getPrompt();
            String targetAspectRatio = taskInfo.getAspectRatio(); // 用户选择的目标比例
            boolean hdMode = taskInfo.isHdMode();
            String imageUrl = taskInfo.getImageUrl(); // 获取参考图片（兼容旧逻辑）
            String imageModel = taskInfo.getImageModel(); // 获取图像生成模型
            
            // 解析用户上传的多张图片
            List<String> userUploadedImages = new ArrayList<>();
            String uploadedImagesJson = taskInfo.getUploadedImages();
            if (uploadedImagesJson != null && !uploadedImagesJson.isEmpty()) {
                try {
                    List<String> parsedImages = objectMapper.readValue(uploadedImagesJson, 
                        objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
                    // 过滤掉 null 和空字符串
                    for (String img : parsedImages) {
                        if (img != null && !img.isEmpty() && !"null".equals(img)) {
                            userUploadedImages.add(img);
                        }
                    }
                    logger.info("解析用户上传图片: 有效数量={}", userUploadedImages.size());
                } catch (Exception e) {
                    logger.error("解析上传图片失败: taskId={}", taskId, e);
                }
            }
            
            // 判断是否有用户上传的图片
            boolean hasUserUploads = !userUploadedImages.isEmpty();
            
            // 判断是否有参考图片（兼容旧逻辑）
            boolean hasReferenceImage = imageUrl != null && !imageUrl.isEmpty();
            
            // 根据是否有上传图片，决定生成分镜图使用的比例
            // 有上传图片：使用4:3；无上传图片：使用目标比例
            String generationAspectRatio = imageGridService.getGenerationAspectRatio(targetAspectRatio, hasUserUploads);
            
            // 第一步：调用提示词优化 API，获取 shotList、imagePrompt、videoPrompt
            // 系统引导词配置为返回 JSON 格式数据
            String finalPrompt = prompt; // 用于生成分镜图的提示词
            String shotListResult = "";
            String videoPromptResult = "";
            
            try {
                com.example.demo.model.SystemSettings settings = systemSettingsService.getOrCreate();
                String storyboardSystemPrompt = settings.getStoryboardSystemPrompt();
                
                if (storyboardSystemPrompt != null && !storyboardSystemPrompt.trim().isEmpty()) {
                    // 使用后台配置的系统引导词作为系统消息，用户原始提示词作为用户消息
                    logger.info("开始调用提示词优化API，获取 JSON 格式结果...");
                    
                    // 调用多模态提示词优化（传递用户上传的图片和自定义系统提示词）
                    // 期望返回 JSON（包含 shotList、imagePrompt、videoPrompt）
                    String userPrompt = "用户需求：" + prompt;
                    String optimizedResult = realAIService.optimizePromptWithImages(userPrompt, "storyboard", userUploadedImages, storyboardSystemPrompt);
                    
                    // 尝试解析 JSON 响应
                    try {
                        // 清理可能的 markdown 代码块
                        String jsonContent = optimizedResult.trim();
                        if (jsonContent.startsWith("```json")) {
                            jsonContent = jsonContent.substring(7);
                        } else if (jsonContent.startsWith("```")) {
                            jsonContent = jsonContent.substring(3);
                        }
                        if (jsonContent.endsWith("```")) {
                            jsonContent = jsonContent.substring(0, jsonContent.length() - 3);
                        }
                        jsonContent = jsonContent.trim();
                        
                        @SuppressWarnings("unchecked")
                        Map<String, Object> jsonResult = objectMapper.readValue(jsonContent, Map.class);
                        
                        // 提取字段（按照系统引导词返回的 JSON 格式）
                        logger.info("解析 JSON 字段，可用 keys: {}", jsonResult.keySet());
                        
                        // shotList（可能是数组或字符串）或 shotListMarkdown
                        if (jsonResult.containsKey("shotList")) {
                            Object shotListObj = jsonResult.get("shotList");
                            if (shotListObj instanceof List) {
                                // 如果是数组，转换为 JSON 字符串保存
                                shotListResult = objectMapper.writeValueAsString(shotListObj);
                            } else {
                                shotListResult = String.valueOf(shotListObj);
                            }
                            logger.info("提取 shotList 成功，长度: {}", shotListResult.length());
                        }
                        if (jsonResult.containsKey("shotListMarkdown")) {
                            String markdown = String.valueOf(jsonResult.get("shotListMarkdown"));
                            if (shotListResult.isEmpty()) {
                                shotListResult = markdown;
                            }
                            logger.info("提取 shotListMarkdown 成功，长度: {}", markdown.length());
                        }
                        
                        // imagePrompt（优先使用，用于生成分镜图）
                        if (jsonResult.containsKey("imagePrompt")) {
                            finalPrompt = String.valueOf(jsonResult.get("imagePrompt"));
                            logger.info("提取 imagePrompt 成功，长度: {}", finalPrompt.length());
                        }
                        
                        // videoPrompt（用于生成视频）
                        if (jsonResult.containsKey("videoPrompt")) {
                            Object videoPromptObj = jsonResult.get("videoPrompt");
                            if (videoPromptObj != null && !"null".equals(String.valueOf(videoPromptObj))) {
                                videoPromptResult = String.valueOf(videoPromptObj);
                                logger.info("提取 videoPrompt 成功，长度: {}", videoPromptResult.length());
                            }
                        }
                        
                        // 如果没有 videoPrompt，使用 imagePrompt 作为备选
                        if (videoPromptResult.isEmpty() && !finalPrompt.equals(prompt)) {
                            videoPromptResult = finalPrompt;
                            logger.info("videoPrompt 为空，使用 imagePrompt 作为备选");
                        }
                        
                        // 保存到任务（使用事务模板避免连接泄漏）
                        final String finalShotList = shotListResult;
                        final String finalImagePrompt = finalPrompt;
                        final String finalVideoPrompt = videoPromptResult;
                        
                        // 调试日志：检查即将保存的提示词
                        logger.info("【调试】即将保存提示词: taskId={}, shotList长度={}, imagePrompt长度={}, videoPrompt长度={}", 
                            taskId,
                            finalShotList != null ? finalShotList.length() : 0,
                            finalImagePrompt != null ? finalImagePrompt.length() : 0,
                            finalVideoPrompt != null ? finalVideoPrompt.length() : 0);
                        
                        asyncTransactionTemplate.executeWithoutResult(status -> {
                            StoryboardVideoTask t = taskRepository.findByTaskId(taskId).orElse(null);
                            if (t != null) {
                                t.setShotList(finalShotList);
                                t.setImagePrompt(finalImagePrompt);
                                t.setVideoPrompt(finalVideoPrompt);
                                taskRepository.save(t);
                                logger.info("【调试】提示词已保存到数据库: taskId={}", taskId);
                            } else {
                                logger.error("【调试】保存提示词失败：任务不存在: taskId={}", taskId);
                            }
                        });
                        logger.info("已保存优化后的提示词到任务: taskId={}", taskId);
                        
                    } catch (Exception jsonException) {
                        // JSON 解析失败，使用用户原始提示词（不是AI返回的完整响应）
                        logger.warn("JSON 解析失败，使用用户原始提示词: {}", jsonException.getMessage());
                        // 不要使用 optimizedResult，因为它可能包含系统引导词
                        // 使用用户的原始提示词
                        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("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: {}", 
                prompt, finalPrompt.length(), targetAspectRatio, generationAspectRatio, hdMode, hasUserUploads, imageModel);
            
            // 更新任务状态为处理中（使用 TransactionTemplate 确保事务正确关闭）
            updateTaskStatusWithTransactionTemplate(taskId);
            
            // 调用AI生图API，生成分镜图
            logger.info("开始生成分镜图（{}模式）...", hasReferenceImage || hasUserUploads ? "图生图" : "文生图");
            
            // 收集所有图片URL
            List<String> imageUrls = new ArrayList<>();
            
            // 参考Comfly项目：多次调用API生成多张图片（因为Comfly API可能不支持一次生成多张）
            // 添加重试机制，确保每张图片都生成成功
            int maxRetriesPerImage = 10; // 每张图片最多重试10次，确保生成成功
            long startTime = System.currentTimeMillis();
            
            for (int i = 0; i < DEFAULT_STORYBOARD_IMAGES; i++) {
                boolean imageGenerated = false;
                int retryCount = 0;
                
                // 重试机制：如果单张图片生成失败，持续重试直到成功（最多重试10次）
                while (!imageGenerated && retryCount <= maxRetriesPerImage) {
                    try {
                        if (retryCount > 0) {
                            logger.info("重试生成第{}张分镜图（第{}次重试）...", i + 1, retryCount);
                            Thread.sleep(1000 * retryCount); // 重试时延迟递增
                        }
                        
                        // 根据是否有参考图片，调用不同的API
                        // 使用系统引导词 + 用户提示词 (finalPrompt)
                        Map<String, Object> apiResponse;
                        if (hasReferenceImage || hasUserUploads) {
                            // 有参考图片，使用图生图API（bananaAPI）
                            String refImage = hasUserUploads ? userUploadedImages.get(0) : imageUrl;
                            apiResponse = realAIService.submitImageToImageTask(
                                finalPrompt, 
                                refImage,
                                generationAspectRatio,
                                hdMode,
                                imageModel  // 使用用户选择的图像生成模型
                            );
                        } else {
                            // 无参考图片，使用文生图API
                            apiResponse = realAIService.submitTextToImageTask(
                                finalPrompt, 
                                generationAspectRatio,
                                1,  // 每次生成1张图片
                                hdMode,
                                imageModel  // 使用用户选择的图像生成模型
                            );
                        }
                        
                        // 检查API响应是否为空
                        if (apiResponse == null) {
                            logger.warn("第{}张分镜图API响应为空（重试: {}/{}）", i + 1, retryCount, maxRetriesPerImage);
                            retryCount++;
                            continue;
                        }
                        
                        // 从API响应中提取图片URL
                        Object dataObj = apiResponse.get("data");
                        if (dataObj instanceof List) {
                            @SuppressWarnings("unchecked")
                            List<Map<String, Object>> data = (List<Map<String, Object>>) dataObj;
                            if (!data.isEmpty()) {
                                // 提取第一张图片的URL（因为每次只生成1张）
                                Map<String, Object> imageData = data.get(0);
                                if (imageData == null) {
                                    logger.warn("第{}张分镜图data[0]为空（重试: {}/{}）", i + 1, retryCount, maxRetriesPerImage);
                                    retryCount++;
                                    continue;
                                }
                                
                                // 提取图片数据（优先使用Base64，其次使用URL）
                                String imageBase64 = extractImageAsBase64(imageData);
                                
                                if (imageBase64 != null && !imageBase64.isEmpty()) {
                                    imageUrls.add(imageBase64);
                                    imageGenerated = true;
                                    long elapsed = System.currentTimeMillis() - startTime;
                                    int progress = (int) ((i + 1) * 100.0 / DEFAULT_STORYBOARD_IMAGES);
                                    logger.info("✓ 成功生成第{}/{}张分镜图（进度: {}%, 耗时: {}ms, Base64长度: {}）", 
                                        i + 1, DEFAULT_STORYBOARD_IMAGES, progress, elapsed, imageBase64.length());
                                } else {
                                    logger.warn("未能提取第{}张分镜图的数据，imageBase64为空（重试: {}/{}）", i + 1, retryCount, maxRetriesPerImage);
                                    retryCount++;
                                }
                            } else {
                                logger.warn("第{}张分镜图data列表为空（重试: {}/{}）", i + 1, retryCount, maxRetriesPerImage);
                                retryCount++;
                            }
                        } else {
                            logger.warn("第{}张分镜图响应格式异常，data不是List类型: {}（重试: {}/{}）", 
                                i + 1, dataObj != null ? dataObj.getClass().getName() : "null", retryCount, maxRetriesPerImage);
                            retryCount++;
                        }
                        
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        logger.error("生成分镜图被中断: {}", taskId, e);
                        throw new RuntimeException("生成分镜图被中断", e);
                    } catch (Exception e) {
                        logger.warn("生成第{}张分镜图异常（重试: {}/{}）: {}", i + 1, retryCount, maxRetriesPerImage, e.getMessage());
                        retryCount++;
                    }
                }
                
                // 检查该张图片是否生成成功，如果失败则整个任务失败
                if (!imageGenerated) {
                    logger.error("生成第{}张分镜图失败，已达最大重试次数{}，任务失败", i + 1, maxRetriesPerImage);
                    throw new RuntimeException("生成第" + (i + 1) + "张分镜图失败，已重试" + maxRetriesPerImage + "次");
                }
                
                // 在多次调用之间添加短暂延迟，避免API限流
                if (i < DEFAULT_STORYBOARD_IMAGES - 1) {
                    try {
                        Thread.sleep(500); // 延迟500ms
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException("生成分镜图被中断", e);
                    }
                }
            }
            
            // 严格检查：必须生成指定数量的图片
            if (imageUrls.size() < DEFAULT_STORYBOARD_IMAGES) {
                String errorMsg = String.format("只生成了%d张图片，需要%d张", 
                    imageUrls.size(), DEFAULT_STORYBOARD_IMAGES);
                logger.error(errorMsg);
                throw new RuntimeException(errorMsg);
            }
            
            logger.info("成功生成{}张分镜图", imageUrls.size());
            
            // 确保不超过指定数量
            if (imageUrls.size() > DEFAULT_STORYBOARD_IMAGES) {
                logger.warn("生成了{}张图片，多于预期的{}张，只取前{}张", 
                    imageUrls.size(), DEFAULT_STORYBOARD_IMAGES, DEFAULT_STORYBOARD_IMAGES);
                imageUrls = imageUrls.subList(0, DEFAULT_STORYBOARD_IMAGES);
            }
            
            long totalTime = System.currentTimeMillis() - startTime;
            logger.info("成功获取{}张图片（总耗时: {}ms），开始验证并规范化图片格式...", imageUrls.size(), totalTime);
            
            // 验证所有图片都是Base64格式（带data URI前缀）
            // 参考sora2实现：确保所有图片格式一致
            List<String> validatedImages = validateAndNormalizeImages(imageUrls);
            
            // 验证后的图片数量也要满足要求
            if (validatedImages.size() < DEFAULT_STORYBOARD_IMAGES) {
                String errorMsg = String.format("验证后只有%d张图片，需要%d张", 
                    validatedImages.size(), DEFAULT_STORYBOARD_IMAGES);
                logger.error(errorMsg);
                throw new RuntimeException(errorMsg);
            }
            
            // 生成分镜图阶段：不做拼图处理，直接返回原始 AI 生成的分镜图给用户预览
            // 拼图处理（分镜图 + 用户上传图片）在生成视频时进行（TaskQueueService）
            String generatedImage = validatedImages.get(0);
            logger.info("分镜图生成完成，直接返回原始图片供用户预览（拼图在生成视频时进行）");
            
            // 检查生成的图片是否有效
            if (generatedImage == null || generatedImage.isEmpty()) {
                throw new RuntimeException("分镜图生成失败: 返回的图片为空");
            }
            
            // 直接使用原始分镜图作为预览图
            String mergedImageUrl = generatedImage;
            
            // 保存单独的分镜图片（Base64数组，参考sora2实现）
            // validatedImages 已在上面定义并验证
            // 将图片列表转换为JSON数组（每张图片都是Base64格式，带data URI前缀）
            String storyboardImagesJson = null;
            try {
                storyboardImagesJson = objectMapper.writeValueAsString(validatedImages);
            } catch (Exception e) {
                logger.error("转换分镜图片为JSON失败: {}", taskId, e);
                // 如果转换失败，继续使用网格图
            }
            
            // 只有在6张图片都生成并拼接完成后，才保存结果图片URL
            // 使用 TransactionTemplate 确保事务正确关闭
            long saveStartTime = System.currentTimeMillis();
            saveStoryboardImageResultWithTransactionTemplate(taskId, mergedImageUrl, storyboardImagesJson, validatedImages.size());
            long saveTime = System.currentTimeMillis() - saveStartTime;
            
            long totalElapsed = System.currentTimeMillis() - startTime;
            logger.info("✓ 分镜图生成完成: taskId={}, 共{}张图片，总耗时: {}ms (生成: {}ms, 保存: {}ms)", 
                taskId, validatedImages.size(), totalElapsed, totalTime, saveTime);
            
            // 不再自动生成视频，等待用户点击"开始生成"按钮
            
        } catch (Exception e) {
            logger.error("处理分镜视频任务失败: {}", taskId, e);
            // 更新任务失败状态（使用 TransactionTemplate 确保事务正确关闭）
            updateTaskStatusToFailedWithTransactionTemplate(taskId, e.getMessage());
        }
    }
    
    /**
     * 在异步方法中加载任务信息（使用配置好的只读事务模板，超时2秒，确保快速完成）
     */
    private StoryboardVideoTask loadTaskInfoWithTransactionTemplate(String taskId) {
        // 使用配置好的只读事务模板，超时2秒，确保快速完成
        return readOnlyTransactionTemplate.execute(status -> {
            return taskRepository.findByTaskId(taskId)
                .orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
        });
    }
    
    /**
     * 在异步方法中更新任务状态为处理中（使用配置好的异步事务模板，超时3秒，确保快速完成）
     */
    private void updateTaskStatusWithTransactionTemplate(String taskId) {
        // 使用配置好的异步事务模板，超时3秒，确保快速完成
        asyncTransactionTemplate.executeWithoutResult(status -> {
            try {
                StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
                    .orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
                task.updateStatus(StoryboardVideoTask.TaskStatus.PROCESSING);
                taskRepository.save(task);
                logger.info("任务状态已更新为处理中: {}", taskId);

                // 创建 TaskStatus 记录用于跟踪分镜图生成状态
                try {
                    // 分镜图生成是同步的，不需要 externalTaskId
                    TaskStatus taskStatus = taskStatusPollingService.createTaskStatus(
                        taskId,
                        task.getUsername(),
                        CommonTaskType.STORYBOARD_VIDEO,
                        null  // 分镜图生成是同步API，不需要外部任务ID
                    );
                    logger.info("TaskStatus 记录已创建: taskId={}, status={}", taskId, taskStatus.getStatus());
                } catch (Exception e) {
                    logger.error("创建 TaskStatus 记录失败: {}", taskId, e);
                    // 不抛出异常，避免影响主流程
                }

                // 注意：不在此处创建 STORYBOARD_VIDEO 的 UserWork 记录
                // 只有在视频真正生成完成后才创建，避免显示没有视频的空作品
                // 分镜图作品在 saveStoryboardImageResultWithTransactionTemplate 中创建
            } catch (Exception e) {
                logger.error("更新任务状态失败: {}", taskId, e);
                status.setRollbackOnly();
                throw e;
            }
        });
    }
    
    /**
     * 在异步方法中保存分镜图结果（使用配置好的异步事务模板，超时3秒，确保快速完成）
     * 参考sora2实现：保存网格图和单独的分镜图片
     * 如果COS启用，会将网格图上传到COS
     */
    private void saveStoryboardImageResultWithTransactionTemplate(String taskId, String mergedImageUrl, String storyboardImagesJson, int validatedImageCount) {
        // 如果COS启用，上传网格图到COS
        String finalMergedImageUrl = mergedImageUrl;
        if (cosService.isEnabled() && mergedImageUrl != null && mergedImageUrl.startsWith("data:image")) {
            try {
                logger.info("开始上传分镜网格图到COS: taskId={}", taskId);
                String cosUrl = cosService.uploadBase64Image(mergedImageUrl, "storyboard_" + taskId + ".png");
                if (cosUrl != null && !cosUrl.isEmpty()) {
                    finalMergedImageUrl = cosUrl;
                    logger.info("分镜网格图上传COS成功: taskId={}, url={}", taskId, cosUrl);
                } else {
                    logger.warn("上传分镜网格图到COS失败，使用Base64: taskId={}", taskId);
                }
            } catch (Exception e) {
                logger.error("上传分镜网格图到COS异常，使用Base64: taskId={}", taskId, e);
            }
        }
        
        // 如果COS启用，将 storyboardImages JSON 数组中的每张 Base64 图片也上传到 COS
        String finalStoryboardImagesJson = uploadStoryboardImagesToCos(taskId, storyboardImagesJson);
        
        final String imageUrlForDb = finalMergedImageUrl;
        final String storyboardImagesForDb = finalStoryboardImagesJson;
        
        asyncTransactionTemplate.executeWithoutResult(status -> {
            try {
                StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
                    .orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
                task.setResultUrl(imageUrlForDb); // 网格图（COS URL 或 Base64 回退）
                if (storyboardImagesForDb != null && !storyboardImagesForDb.isEmpty()) {
                    task.setStoryboardImages(storyboardImagesForDb); // 单独的分镜图片（COS URL 或 Base64 回退）
                }
                task.updateProgress(50); // 分镜图生成完成，进度50%
                task.setStatus(StoryboardVideoTask.TaskStatus.COMPLETED); // 分镜图完成
                taskRepository.save(task);
                logger.info("分镜图生成完成，任务状态已更新为 COMPLETED: taskId={}", taskId);
                
                // 扣除分镜图冻结的积分
                try {
                    userService.deductFrozenPoints(taskId + "_img");
                    logger.info("分镜图积分扣除成功: taskId={}", taskId);
                } catch (Exception e) {
                    logger.error("分镜图积分扣除失败（不影响任务完成）: taskId={}", taskId, e);
                }

                // 更新 TaskStatus 为 COMPLETED（分镜图生成完成，停止轮询）
                // 用户点击"生成视频"时，会重新将状态改为 PROCESSING 并添加 externalTaskId
                try {
                    TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
                    if (taskStatus != null) {
                        taskStatus.setResultUrl(imageUrlForDb);
                        taskStatus.setProgress(50); // 分镜图完成，进度50%
                        taskStatus.setStatus(TaskStatus.Status.COMPLETED); // 标记为完成，停止轮询
                        taskStatus.setCompletedAt(LocalDateTime.now());
                        taskStatus.setUpdatedAt(LocalDateTime.now());
                        taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
                        logger.info("TaskStatus 已标记为 COMPLETED（分镜图完成）: taskId={}", taskId);
                    }
                } catch (Exception e) {
                    logger.error("更新 TaskStatus 状态失败: {}", taskId, e);
                    // 不抛出异常，避免影响主流程
                }

                // 创建分镜图作品记录
                try {
                    userWorkService.createStoryboardImageWork(taskId, imageUrlForDb);
                    logger.info("分镜图作品记录已创建: taskId={}", taskId);
                } catch (Exception e) {
                    logger.error("创建分镜图作品记录失败: taskId={}", taskId, e);
                    // 不抛出异常，避免影响主流程
                }
                
                // 更新分镜视频 UserWork 状态为 COMPLETED（分镜图阶段完成）
                // 这样主页不会显示"生成中"，用户点击"生成视频"后会重新创建 PROCESSING 状态的记录
                try {
                    userWorkService.updateStoryboardVideoWorkToImageCompleted(taskId, imageUrlForDb);
                    logger.info("分镜视频作品状态已更新为分镜图完成: taskId={}", taskId);
                } catch (Exception e) {
                    logger.warn("更新分镜视频作品状态失败: taskId={}, error={}", taskId, e.getMessage());
                    // 不抛出异常，避免影响主流程
                }
                
                // 分镜图生成完成，从任务队列中移除（用户点击"生成视频"时会重新添加）
                try {
                    taskQueueService.removeTaskFromQueue(taskId);
                    logger.info("分镜图生成完成，已从任务队列移除: taskId={}", taskId);
                } catch (Exception e) {
                    logger.warn("从任务队列移除失败: taskId={}, error={}", taskId, e.getMessage());
                    // 不抛出异常，避免影响主流程
                }
            } catch (Exception e) {
                logger.error("保存分镜图结果失败: {}", taskId, e);
                status.setRollbackOnly();
                throw e;
            }
        });
    }
    
    /**
     * 在异步方法中更新任务状态为失败（通过 task_status 触发级联更新）
     */
    private void updateTaskStatusToFailedWithTransactionTemplate(String taskId, String errorMessage) {
        try {
            // 通过更新 task_status 触发级联更新（自动同步到业务表和 user_works）
            // 注意：积分返还由 updateTaskStatusWithCascade 自动处理，不要在这里重复返还
            boolean updated = taskStatusPollingService.markTaskFailed(taskId, errorMessage);
            if (updated) {
                logger.info("任务状态已更新为失败（级联更新已触发）: taskId={}", taskId);
            } else {
                // 如果 task_status 记录不存在，回退到直接更新业务表
                logger.warn("task_status 记录不存在，直接更新业务表: taskId={}", taskId);
                asyncTransactionTemplate.executeWithoutResult(status -> {
                    try {
                        StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
                            .orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
                        task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
                        task.setErrorMessage(errorMessage);
                        task.setCompletedAt(LocalDateTime.now());
                        taskRepository.save(task);
                        // 更新 UserWork
                        try {
                            userWorkService.updateWorkStatusByTaskId(taskId, 
                                com.example.demo.model.UserWork.WorkStatus.FAILED);
                        } catch (Exception ue) {
                            logger.warn("回退更新UserWork状态失败: taskId={}", taskId);
                        }
                        // 回退时需要返还分镜图积分（因为这个方法只在分镜图生成失败时调用）
                        try {
                            userService.returnFrozenPoints(taskId + "_img");
                            logger.info("回退更新时已返还分镜图积分: taskId={}_img", taskId);
                        } catch (Exception e) {
                            logger.debug("回退更新时分镜图积分返还失败（可能未冻结或已返还）: taskId={}_img", taskId);
                        }
                    } catch (Exception e) {
                        logger.error("直接更新业务表失败: {}", taskId, e);
                        status.setRollbackOnly();
                    }
                });
            }
        } catch (Exception e) {
            logger.error("执行更新失败状态事务失败: {}", taskId, e);
        }
    }

    /**
     * 获取任务信息用于处理（只读事务，快速完成）
     */
    @Transactional(readOnly = true)
    public StoryboardVideoTask getTaskInfoForProcessing(String taskId) {
        return taskRepository.findByTaskId(taskId)
            .orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
    }

    /**
     * 更新任务状态为处理中（单独的事务方法）
     */
    @Transactional
    public void updateTaskStatusToProcessing(String taskId) {
        StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
            .orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
        task.updateStatus(StoryboardVideoTask.TaskStatus.PROCESSING);
        taskRepository.save(task);
    }

    /**
     * 保存分镜图结果（单独的事务方法）
     */
    @Transactional
    public void saveStoryboardImageResult(String taskId, String mergedImageUrl, String storyboardImagesJson, int validatedImageCount) {
        // 上传网格图到 COS
        String finalMergedImageUrl = mergedImageUrl;
        if (cosService.isEnabled() && mergedImageUrl != null && mergedImageUrl.startsWith("data:image")) {
            try {
                String cosUrl = cosService.uploadBase64Image(mergedImageUrl, "storyboard_" + taskId + ".png");
                if (cosUrl != null && !cosUrl.isEmpty()) {
                    finalMergedImageUrl = cosUrl;
                }
            } catch (Exception e) {
                logger.warn("上传网格图到COS失败，保留Base64: taskId={}", taskId);
            }
        }
        
        // 上传单独分镜图片到 COS
        String finalStoryboardImagesJson = uploadStoryboardImagesToCos(taskId, storyboardImagesJson);
        
        StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
            .orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
        task.setResultUrl(finalMergedImageUrl);
        if (finalStoryboardImagesJson != null && !finalStoryboardImagesJson.isEmpty()) {
            task.setStoryboardImages(finalStoryboardImagesJson);
        }
        task.updateProgress(50);
        taskRepository.save(task);

        // 创建分镜图作品记录（使用 COS URL）
        try {
            userWorkService.createStoryboardImageWork(taskId, finalMergedImageUrl);
            logger.info("分镜图作品记录已创建: taskId={}", taskId);
        } catch (Exception e) {
            logger.error("创建分镜图作品记录失败: taskId={}", taskId, e);
        }
    }

    /**
     * 更新任务状态为失败（通过 task_status 触发级联更新）
     */
    @Transactional
    public void updateTaskStatusToFailed(String taskId, String errorMessage) {
        try {
            // 通过更新 task_status 触发级联更新
            boolean updated = taskStatusPollingService.markTaskFailed(taskId, errorMessage);
            if (!updated) {
                // 如果 task_status 记录不存在，回退到直接更新业务表
                logger.warn("task_status 记录不存在，直接更新业务表: taskId={}", taskId);
                StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
                    .orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
                task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
                task.setErrorMessage(errorMessage);
                task.setCompletedAt(LocalDateTime.now());
                taskRepository.save(task);
                // 更新 UserWork
                try {
                    userWorkService.updateWorkStatusByTaskId(taskId, 
                        com.example.demo.model.UserWork.WorkStatus.FAILED);
                } catch (Exception ue) {
                    logger.warn("回退更新UserWork状态失败: taskId={}", taskId);
                }
                // 回退时也需要返还积分
                try {
                    userService.returnFrozenPoints(taskId + "_vid");
                    logger.info("回退更新时已返还分镜视频积分: taskId={}_vid", taskId);
                } catch (Exception e) {
                    logger.debug("回退更新时分镜视频积分返还失败（可能未冻结或已返还）: taskId={}_vid", taskId);
                }
                try {
                    userService.returnFrozenPoints(taskId + "_img");
                    logger.info("回退更新时已返还分镜图积分: taskId={}_img", taskId);
                } catch (Exception e) {
                    logger.debug("回退更新时分镜图积分返还失败（可能未冻结或已返还）: taskId={}_img", taskId);
                }
            }
        } catch (Exception ex) {
            logger.error("更新任务失败状态失败: {}", taskId, ex);
        }
    }

    /**
     * 获取任务详情
     */
    @Transactional(readOnly = true)
    public StoryboardVideoTask getTask(String taskId) {
        return taskRepository.findByTaskId(taskId)
            .orElseThrow(() -> new RuntimeException("任务不存在: " + taskId));
    }

    /**
     * 获取用户任务列表
     */
    @Transactional(readOnly = true)
    public List<StoryboardVideoTask> getUserTasks(String username, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        Page<StoryboardVideoTask> taskPage = taskRepository.findByUsernameOrderByCreatedAtDesc(username, pageable);
        return taskPage.getContent();
    }

    /**
     * 开始生成视频（从分镜图生成视频）
     * 用户点击"开始生成"按钮后调用
     * @param duration 视频时长（可选，如果为null则使用任务中已有的值）
     * @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, 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");
            
            // 检查分镜图是否已生成
            // 优先使用前端传递的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()) {
                // 根据任务状态给出更友好的提示
                if (task.getStatus() == StoryboardVideoTask.TaskStatus.PROCESSING) {
                    throw new RuntimeException("分镜图正在生成中，请等待生成完成后再生成视频");
                } else {
                    throw new RuntimeException("请先上传或生成分镜图，再生成视频");
                }
            }
            
            // 更新任务的resultUrl为有效的分镜图URL（确保后续处理能获取到）
            if (!effectiveStoryboardUrl.equals(task.getResultUrl())) {
                task.setResultUrl(effectiveStoryboardUrl);
                logger.info("已更新任务的resultUrl为分镜图URL: taskId={}", taskId);
            }
            
            // 冻结分镜视频生成积分
            int sbVideoPts = systemSettingsService.getOrCreate().getStoryboardVideoPoints();
            try {
                userService.freezePoints(task.getUsername(), taskId + "_vid", 
                    CommonTaskType.STORYBOARD_VIDEO, 
                    sbVideoPts, "分镜视频生成");
                logger.info("分镜视频积分冻结成功: taskId={}, username={}, points={}", taskId, task.getUsername(), sbVideoPts);
            } catch (Exception e) {
                logger.error("分镜视频积分冻结失败: taskId={}, username={}", taskId, task.getUsername(), e);
                throw new RuntimeException("积分不足，无法生成视频: " + e.getMessage());
            }
            
            // 检查任务状态：允许从 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.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())) {
                logger.info("更新任务 {} 的时长参数: {} -> {}", taskId, task.getDuration(), duration);
                task.setDuration(duration);
                paramsUpdated = true;
            }
            if (aspectRatio != null && !aspectRatio.equals(task.getAspectRatio())) {
                logger.info("更新任务 {} 的宽高比参数: {} -> {}", taskId, task.getAspectRatio(), aspectRatio);
                task.setAspectRatio(aspectRatio);
                paramsUpdated = true;
            }
            if (hdMode != null && !hdMode.equals(task.getHdMode())) {
                logger.info("更新任务 {} 的高清模式参数: {} -> {}", taskId, task.getHdMode(), hdMode);
                task.setHdMode(hdMode);
                paramsUpdated = true;
            }
            
            // 更新视频阶段的参考图（如果提供了新的参考图）
            if (referenceImages != null && !referenceImages.isEmpty()) {
                try {
                    com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
                    String videoRefImagesJson = mapper.writeValueAsString(referenceImages);
                    task.setVideoReferenceImages(videoRefImagesJson);
                    logger.info("更新任务 {} 的视频参考图: {} 张", taskId, referenceImages.size());
                    paramsUpdated = true;
                } catch (Exception e) {
                    logger.warn("序列化视频参考图失败: {}", e.getMessage());
                }
            } else {
                // 如果没有提供新的参考图，清空视频参考图字段
                task.setVideoReferenceImages(null);
                logger.info("任务 {} 未提供视频参考图，清空该字段", taskId);
            }
            
            // 如果是 COMPLETED 或 FAILED 状态，更新为 PROCESSING（表示正在生成视频）
            if (task.getStatus() == StoryboardVideoTask.TaskStatus.COMPLETED ||
                task.getStatus() == StoryboardVideoTask.TaskStatus.FAILED) {
                task.setStatus(StoryboardVideoTask.TaskStatus.PROCESSING);
                task.setProgress(50); // 分镜图完成，视频生成中
                task.setErrorMessage(null); // 清除之前的错误信息
                paramsUpdated = true;
                logger.info("任务状态已更新: {} -> PROCESSING (开始视频生成)", taskId);
            }
            
            // 无论任务状态如何，都确保 TaskStatus 为 PROCESSING，启用轮询
            try {
                TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
                if (taskStatus != null && taskStatus.getStatus() != TaskStatus.Status.PROCESSING) {
                    taskStatus.setStatus(TaskStatus.Status.PROCESSING);
                    taskStatus.setProgress(50);
                    taskStatus.setCompletedAt(null); // 清除完成时间
                    taskStatus.setUpdatedAt(LocalDateTime.now());
                    taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
                    logger.info("TaskStatus 已更新为 PROCESSING（开始视频生成）: taskId={}", taskId);
                }
            } catch (Exception e) {
                logger.error("更新 TaskStatus 状态失败: {}", taskId, e);
            }
            
            // 更新 UserWork 状态为 PROCESSING（视频生成中）
            try {
                userWorkService.updateStoryboardVideoWorkToProcessing(taskId);
                logger.info("UserWork 已更新为 PROCESSING（开始视频生成）: taskId={}", taskId);
            } catch (Exception e) {
                logger.warn("更新 UserWork 状态失败: taskId={}, error={}", taskId, e.getMessage());
            }
            
            // 如果有任何参数更新，保存任务
            if (paramsUpdated) {
                taskRepository.save(task);
                logger.info("任务参数已更新并保存: {}", taskId);
            }
            
            // 将视频生成任务添加到任务队列
            try {
                taskQueueService.addStoryboardVideoTask(task.getUsername(), taskId);
            } catch (Exception e) {
                logger.error("添加分镜视频任务到队列失败: {}", taskId, e);
                // 返还刚刚冻结的视频积分
                try {
                    userService.returnFrozenPoints(taskId + "_vid");
                    logger.info("添加队列失败，已返还分镜视频积分: taskId={}_vid", taskId);
                } catch (Exception re) {
                    logger.warn("返还分镜视频积分失败: taskId={}_vid, error={}", taskId, re.getMessage());
                }
                throw new RuntimeException("添加视频生成任务失败: " + e.getMessage());
            }
            
            // 在事务提交后异步触发视频生成
            // 队列消费者已禁用对分镜视频的处理，由这里直接执行
            final String finalTaskId = taskId;
            TransactionSynchronizationManager.registerSynchronization(
                new TransactionSynchronization() {
                    @Override
                    public void afterCommit() {
                        taskExecutor.execute(() -> {
                            try {
                                // 短暂等待确保数据库同步完成
                                Thread.sleep(500);
                                logger.info("开始异步视频生成: taskId={}", finalTaskId);
                                taskQueueService.processStoryboardVideoTaskDirectly(finalTaskId);
                            } catch (Exception e) {
                                logger.error("异步视频生成失败: taskId={}", finalTaskId, e);
                            }
                        });
                    }
                }
            );
            
        } catch (Exception e) {
            logger.error("开始生成视频失败: {}", taskId, e);
            throw new RuntimeException("开始生成视频失败: " + e.getMessage());
        }
    }

    /**
     * 从图片数据中提取Base64格式的图片（参考sora2实现）
     * 优先使用b64_json，如果不存在则下载URL并转换为Base64
     * 确保返回的格式为：data:image/png;base64,{base64_str}
     */
    private String extractImageAsBase64(Map<String, Object> imageData) {
        if (imageData == null) {
            return null;
        }
        
        // 优先使用b64_json（参考banana实现）
        Object b64JsonObj = imageData.get("b64_json");
        if (b64JsonObj != null) {
            String base64Data = b64JsonObj.toString();
            // 确保有data URI前缀（参考Comfly.py 6236行）
            if (!base64Data.startsWith("data:")) {
                return "data:image/png;base64," + base64Data;
            } else {
                return base64Data;
            }
        }
        
        // 如果没有b64_json，尝试下载URL并转换为Base64
        Object urlObj = imageData.get("url");
        if (urlObj != null) {
            String imageUrl = urlObj.toString();
            try {
                // 下载图片并转换为Base64（参考sora2实现）
                String base64Image = downloadImageAndConvertToBase64(imageUrl);
                if (base64Image != null && !base64Image.isEmpty()) {
                    return base64Image;
                }
            } catch (Exception e) {
                logger.warn("下载并转换图片失败: {}, 错误: {}", imageUrl, e.getMessage());
            }
        }
        
        return null;
    }
    
    /**
     * 压缩图片以减小体积
     * @param originalImage 原始图片
     * @param maxSize 最大尺寸（宽度或高度）
     * @param quality 压缩质量（0.0-1.0，未使用，保留用于未来扩展）
     * @return 压缩后的图片
     */
    private BufferedImage compressImage(BufferedImage originalImage, int maxSize, float quality) {
        int originalWidth = originalImage.getWidth();
        int originalHeight = originalImage.getHeight();
        
        // 如果图片尺寸小于等于最大尺寸，直接返回
        if (originalWidth <= maxSize && originalHeight <= maxSize) {
            return originalImage;
        }
        
        // 计算缩放比例
        double scale = Math.min((double) maxSize / originalWidth, (double) maxSize / originalHeight);
        int newWidth = (int) (originalWidth * scale);
        int newHeight = (int) (originalHeight * scale);
        
        // 创建缩放后的图片
        BufferedImage compressedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = compressedImage.createGraphics();
        
        // 设置高质量缩放
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        
        g.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
        g.dispose();
        
        return compressedImage;
    }
    
    /**
     * 压缩Base64图片（如果图片过大）
     * @param base64Image Base64编码的图片（带data URI前缀）
     * @param maxSize 最大尺寸（宽度或高度）
     * @param quality JPEG压缩质量（0.0-1.0）
     * @return 压缩后的Base64图片
     */
    private String compressBase64Image(String base64Image, int maxSize, float quality) {
        if (base64Image == null || !base64Image.startsWith("data:image")) {
            return base64Image;
        }
        
        try {
            // 提取Base64数据
            String base64Data = base64Image.substring(base64Image.indexOf(",") + 1);
            byte[] imageBytes = Base64.getDecoder().decode(base64Data);
            BufferedImage image = ImageIO.read(new java.io.ByteArrayInputStream(imageBytes));
            
            if (image == null) {
                return base64Image;
            }
            
            // 检查是否需要压缩
            int originalWidth = image.getWidth();
            int originalHeight = image.getHeight();
            int originalSize = imageBytes.length;
            
            if (originalWidth <= maxSize && originalHeight <= maxSize && originalSize < 500 * 1024) {
                // 图片已经足够小，不需要压缩
                return base64Image;
            }
            
            // 压缩图片
            BufferedImage compressedImage = compressImage(image, maxSize, quality);
            
            // 转换为JPEG格式的Base64
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            javax.imageio.ImageWriter writer = javax.imageio.ImageIO.getImageWritersByFormatName("jpg").next();
            javax.imageio.ImageWriteParam param = writer.getDefaultWriteParam();
            if (param.canWriteCompressed()) {
                param.setCompressionMode(javax.imageio.ImageWriteParam.MODE_EXPLICIT);
                param.setCompressionQuality(quality);
            }
            javax.imageio.IIOImage iioImage = new javax.imageio.IIOImage(compressedImage, null, null);
            writer.setOutput(javax.imageio.ImageIO.createImageOutputStream(baos));
            writer.write(null, iioImage, param);
            writer.dispose();
            
            byte[] compressedBytes = baos.toByteArray();
            String compressedBase64 = Base64.getEncoder().encodeToString(compressedBytes);
            
            double compressionRatio = (1.0 - (double) compressedBytes.length / originalSize) * 100;
            logger.info("图片压缩完成: {}x{} -> {}x{}, 大小: {} KB -> {} KB (压缩率: {}%)", 
                originalWidth, originalHeight, 
                compressedImage.getWidth(), compressedImage.getHeight(),
                originalSize / 1024, compressedBytes.length / 1024,
                String.format("%.1f", compressionRatio));
            
            return "data:image/jpeg;base64," + compressedBase64;
            
        } catch (Exception e) {
            logger.warn("压缩图片失败，使用原始图片: {}", e.getMessage());
            return base64Image;
        }
    }
    
    /**
     * 验证并规范化图片格式（参考sora2实现）
     * 确保所有图片都是Base64格式，带data URI前缀
     * 同时压缩过大的图片以减小请求体积
     */
    private List<String> validateAndNormalizeImages(List<String> imageUrls) {
        List<String> validatedImages = new ArrayList<>();
        for (String img : imageUrls) {
            if (img == null || img.isEmpty()) {
                continue;
            }
            
            // 确保有data URI前缀（参考Comfly.py 6236行）
            String normalizedImg = img;
            if (!img.startsWith("data:")) {
                // 如果没有前缀，尝试添加
                if (img.contains(",")) {
                    // 如果已经有逗号，提取Base64数据并添加前缀
                    String pureBase64Data = img.substring(img.indexOf(",") + 1);
                    normalizedImg = "data:image/png;base64," + pureBase64Data;
                } else {
                    // 假设是纯Base64数据
                    normalizedImg = "data:image/png;base64," + img;
                }
            }
            
            // 压缩图片以减小体积（最大1024px，JPEG质量85%）
            // 这样可以显著减小请求体大小，从17MB降低到几MB
            normalizedImg = compressBase64Image(normalizedImg, 1024, 0.85f);
            
            validatedImages.add(normalizedImg);
        }
        
        return validatedImages;
    }
    
    /**
     * 下载图片并转换为Base64格式（参考sora2实现）
     * 返回格式：data:image/png;base64,{base64_str}
     * 添加超时控制和重试机制
     */
    private String downloadImageAndConvertToBase64(String imageUrl) {
        if (imageUrl == null || imageUrl.isEmpty()) {
            return null;
        }
        
        // 如果已经是Base64格式，直接返回
        if (imageUrl.startsWith("data:image")) {
            return imageUrl;
        }
        
        // 重试机制：最多重试2次
        int maxRetries = 2;
        int retryCount = 0;
        long connectTimeout = 10000; // 10秒连接超时
        long readTimeout = 30000;    // 30秒读取超时
        
        while (retryCount <= maxRetries) {
            try {
                if (retryCount > 0) {
                    Thread.sleep(1000 * retryCount); // 重试延迟递增
                }
                
                // 下载图片（使用URI避免deprecated警告）
                URI uri = new URI(imageUrl);
                URL url = uri.toURL();
                
                // 使用HttpURLConnection以便设置超时
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setConnectTimeout((int) connectTimeout);
                connection.setReadTimeout((int) readTimeout);
                connection.setRequestMethod("GET");
                connection.setRequestProperty("User-Agent", "Mozilla/5.0");
                connection.setRequestProperty("Accept", "image/*");
                
                BufferedImage image;
                try (InputStream in = connection.getInputStream()) {
                    image = ImageIO.read(in);
                } finally {
                    connection.disconnect();
                }
                
                if (image == null) {
                    logger.warn("无法读取图片: {}", imageUrl);
                    retryCount++;
                    continue;
                }
                
                // 压缩图片以减小体积（限制最大尺寸和压缩质量）
                BufferedImage compressedImage = compressImage(image, 1024, 0.85f); // 最大1024px，质量85%
                
                // 转换为JPEG格式的Base64（JPEG压缩率更高，体积更小）
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                javax.imageio.ImageWriter writer = javax.imageio.ImageIO.getImageWritersByFormatName("jpg").next();
                javax.imageio.ImageWriteParam param = writer.getDefaultWriteParam();
                if (param.canWriteCompressed()) {
                    param.setCompressionMode(javax.imageio.ImageWriteParam.MODE_EXPLICIT);
                    param.setCompressionQuality(0.85f); // JPEG质量85%
                }
                javax.imageio.IIOImage iioImage = new javax.imageio.IIOImage(compressedImage, null, null);
                writer.setOutput(javax.imageio.ImageIO.createImageOutputStream(baos));
                writer.write(null, iioImage, param);
                writer.dispose();
                
                byte[] imageBytes = baos.toByteArray();
                String base64 = Base64.getEncoder().encodeToString(imageBytes);
                
                // 返回带data URI前缀的Base64字符串（使用JPEG格式以减小体积）
                return "data:image/jpeg;base64," + base64;
                
            } catch (java.net.SocketTimeoutException | java.net.ConnectException e) {
                retryCount++;
                if (retryCount > maxRetries) {
                    logger.error("下载图片超时或连接失败（已重试{}次）: {}", maxRetries, imageUrl, e);
                    return null;
                }
            } catch (IOException e) {
                retryCount++;
                if (retryCount > maxRetries) {
                    logger.error("下载图片失败（已重试{}次）: {}", maxRetries, imageUrl, e);
                    return null;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.error("下载图片被中断: {}", imageUrl, e);
                return null;
            } catch (Exception e) {
                logger.error("转换图片为Base64失败: {}", imageUrl, e);
                return null;
            }
        }
        
        return null;
    }
    
    /**
     * 生成任务ID
     */
    private String generateTaskId() {
        return "sb_" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
    
    /**
     * 检查并处理超时的分镜视频任务
     * 分两个阶段：
     * 1. 分镜图生成阶段（无resultUrl）：10分钟超时
     * 2. 视频生成阶段（有resultUrl，progress > 50 但 < 100）：1小时超时
     */
    @Transactional
    public int checkAndHandleTimeoutTasks() {
        try {
            int totalHandledCount = 0;
            
            // ========== 阶段1：检查分镜图生成超时（10分钟） ==========
            LocalDateTime storyboardTimeoutTime = LocalDateTime.now().minusMinutes(10);
            List<StoryboardVideoTask> storyboardTimeoutTasks = taskRepository.findTimeoutTasks(
                StoryboardVideoTask.TaskStatus.PROCESSING,
                storyboardTimeoutTime
            );
            
            int storyboardHandledCount = 0;
            int storyboardSkippedCount = 0;
            
            for (StoryboardVideoTask task : storyboardTimeoutTasks) {
                try {
                    // 如果有resultUrl，说明分镜图已生成
                    if (task.getResultUrl() != null && !task.getResultUrl().isEmpty()) {
                        // 检查是否请求了视频生成
                        String videoTaskIds = task.getVideoTaskIds();
                        if (videoTaskIds == null || videoTaskIds.isEmpty() || "[]".equals(videoTaskIds)) {
                            // 用户只生成了分镜图，未请求视频，标记为完成
                            task.updateStatus(StoryboardVideoTask.TaskStatus.COMPLETED);
                            task.setProgress(100);
                            task.setCompletedAt(LocalDateTime.now());
                            taskRepository.save(task);
                            // 更新 task_status
                            taskStatusPollingService.updateTaskStatusWithCascade(
                                task.getTaskId(), 
                                com.example.demo.model.TaskStatus.Status.COMPLETED, 
                                task.getResultUrl(), 
                                null
                            );
                            logger.info("分镜图任务完成（用户未请求视频）: taskId={}", task.getTaskId());
                        }
                        storyboardSkippedCount++;
                        continue;
                    }
                    
                    // 分镜图生成超时，尝试通过 task_status 触发级联更新
                    // 注意：积分返还由 updateTaskStatusWithCascade 自动处理，不要在这里重复返还
                    boolean updated = taskStatusPollingService.markTaskFailed(task.getTaskId(), "分镜图生成超时：任务创建后超过10分钟仍未完成");
                    if (!updated) {
                        // 回退：直接更新业务表
                        task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
                        task.setErrorMessage("分镜图生成超时：任务创建后超过10分钟仍未完成");
                        task.setCompletedAt(LocalDateTime.now());
                        taskRepository.save(task);
                        // 更新 UserWork
                        try {
                            userWorkService.updateWorkStatusByTaskId(task.getTaskId(), 
                                com.example.demo.model.UserWork.WorkStatus.FAILED);
                        } catch (Exception ue) {
                            logger.warn("回退更新UserWork状态失败: taskId={}", task.getTaskId());
                        }
                        // 回退时需要返还分镜图积分
                        try {
                            userService.returnFrozenPoints(task.getTaskId() + "_img");
                            logger.info("回退更新时已返还分镜图积分: taskId={}_img", task.getTaskId());
                        } catch (Exception e) {
                            logger.debug("回退更新时分镜图积分返还失败（可能未冻结或已返还）: taskId={}_img", task.getTaskId());
                        }
                    }
                    
                    logger.warn("分镜图生成任务超时，已标记为失败: taskId={}", task.getTaskId());
                    storyboardHandledCount++;
                } catch (Exception e) {
                    logger.error("处理超时分镜图生成任务失败: taskId={}", task.getTaskId(), e);
                }
            }
            
            if (storyboardHandledCount > 0 || storyboardSkippedCount > 0) {
                logger.info("分镜图生成超时检查完成，失败: {}/{}，跳过（已生成分镜图）: {}",
                    storyboardHandledCount, storyboardTimeoutTasks.size(), storyboardSkippedCount);
            }
            totalHandledCount += storyboardHandledCount;
            
            // ========== 阶段2：检查视频生成超时（1小时） ==========
            LocalDateTime videoTimeoutTime = LocalDateTime.now().minusMinutes(60);
            List<StoryboardVideoTask> videoTimeoutTasks = taskRepository.findTimeoutTasks(
                StoryboardVideoTask.TaskStatus.PROCESSING,
                videoTimeoutTime
            );
            
            int videoHandledCount = 0;
            
            for (StoryboardVideoTask task : videoTimeoutTasks) {
                try {
                    // 只处理有resultUrl（分镜图已生成）但视频未完成的任务
                    if (task.getResultUrl() == null || task.getResultUrl().isEmpty()) {
                        continue; // 无分镜图，由阶段1处理
                    }
                    
                    // 检查progress：如果已完成（>= 100）则跳过
                    int progress = task.getProgress();
                    if (progress >= 100) {
                        continue; // 已完成，不处理
                    }
                    
                    // 只有当用户确实请求了视频生成（videoTaskIds不为空）时才检查超时
                    // 如果只生成了分镜图但未请求视频，不算超时
                    String videoTaskIds = task.getVideoTaskIds();
                    if (videoTaskIds == null || videoTaskIds.isEmpty() || "[]".equals(videoTaskIds)) {
                        continue; // 用户未请求视频生成，跳过
                    }
                    
                    // 视频生成超时，尝试通过 task_status 触发级联更新
                    // 注意：积分返还由 updateTaskStatusWithCascade 自动处理，不要在这里重复返还
                    boolean updated = taskStatusPollingService.markTaskFailed(task.getTaskId(), "视频生成超时：任务创建后超过1小时仍未完成");
                    if (!updated) {
                        // 回退：直接更新业务表
                        task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
                        task.setErrorMessage("视频生成超时：任务创建后超过1小时仍未完成");
                        task.setCompletedAt(LocalDateTime.now());
                        taskRepository.save(task);
                        // 更新 UserWork
                        try {
                            userWorkService.updateWorkStatusByTaskId(task.getTaskId(), 
                                com.example.demo.model.UserWork.WorkStatus.FAILED);
                        } catch (Exception ue) {
                            logger.warn("回退更新UserWork状态失败: taskId={}", task.getTaskId());
                        }
                        // 回退时需要返还视频积分
                        try {
                            userService.returnFrozenPoints(task.getTaskId() + "_vid");
                            logger.info("回退更新时已返还分镜视频积分: taskId={}_vid", task.getTaskId());
                        } catch (Exception e) {
                            logger.debug("回退更新时分镜视频积分返还失败（可能未冻结或已返还）: taskId={}_vid", task.getTaskId());
                        }
                    }
                    
                    logger.warn("视频生成任务超时，已标记为失败: taskId={}, progress={}", task.getTaskId(), progress);
                    videoHandledCount++;
                } catch (Exception e) {
                    logger.error("处理超时视频生成任务失败: taskId={}", task.getTaskId(), e);
                }
            }
            
            if (videoHandledCount > 0) {
                logger.info("视频生成超时检查完成，失败: {}", videoHandledCount);
            }
            totalHandledCount += videoHandledCount;
            
            return totalHandledCount;

        } catch (Exception e) {
            logger.error("检查超时分镜视频任务失败", e);
            return 0;
        }
    }

    /**
     * 根据用户名获取用户ID
     */
    private Long getUserIdByUsername(String username) {
        try {
            com.example.demo.model.User user = userService.findByUsername(username);
            if (user != null) {
                return user.getId();
            }
            logger.warn("找不到用户: {}", username);
            return null;
        } catch (Exception e) {
            logger.error("获取用户ID失败: {}", username, e);
            return null;
        }
    }

    /**
     * 生成作品标题
     */
    private String generateTitle(String prompt) {
        if (prompt == null || prompt.trim().isEmpty()) {
            return "未命名作品";
        }

        // 取提示词的前20个字符作为标题
        String title = prompt.trim();
        if (title.length() > 20) {
            title = title.substring(0, 20) + "...";
        }
        return title;
    }

    /**
     * 重试失败的任务
     * 复用原来的 task_id，重新提交生成任务
     */
    @Transactional
    public void retryTask(String taskId) {
        logger.info("重试失败任务: {}", taskId);
        
        // 获取任务
        StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
            .orElseThrow(() -> new RuntimeException("任务不存在: " + taskId));
        
        // 验证任务状态
        if (task.getStatus() != StoryboardVideoTask.TaskStatus.FAILED) {
            throw new RuntimeException("只能重试失败的任务");
        }
        
        // 判断失败阶段，重新冻结对应的积分
        String resultUrl = task.getResultUrl();
        String videoTaskIds = task.getVideoTaskIds();
        
        boolean hasStoryboard = (resultUrl != null && !resultUrl.isEmpty());
        boolean hasVideoTask = (videoTaskIds != null && !videoTaskIds.isEmpty() && !"[]".equals(videoTaskIds));
        
        com.example.demo.model.SystemSettings retrySettings = systemSettingsService.getOrCreate();
        try {
            if (!hasStoryboard) {
                // 分镜图阶段失败，重新冻结分镜图积分
                userService.freezePoints(task.getUsername(), taskId + "_img", 
                    CommonTaskType.STORYBOARD_IMAGE, 
                    retrySettings.getStoryboardImagePoints(), "分镜图生成（重试）");
                logger.info("重试任务：已重新冻结分镜图积分: taskId={}_img", taskId);
            } else if (hasVideoTask) {
                // 视频生成阶段失败，重新冻结视频积分
                userService.freezePoints(task.getUsername(), taskId + "_vid", 
                    CommonTaskType.STORYBOARD_VIDEO, 
                    retrySettings.getStoryboardVideoPoints(), "分镜视频生成（重试）");
                logger.info("重试任务：已重新冻结视频积分: taskId={}_vid", taskId);
            } else {
                // 只生成了分镜图但未开始视频生成，重新冻结分镜图积分
                userService.freezePoints(task.getUsername(), taskId + "_img", 
                    CommonTaskType.STORYBOARD_IMAGE, 
                    retrySettings.getStoryboardImagePoints(), "分镜图生成（重试）");
                logger.info("重试任务：已重新冻结分镜图积分: taskId={}_img", taskId);
            }
        } catch (Exception e) {
            logger.error("重试任务时冻结积分失败: taskId={}", taskId, e);
            throw new RuntimeException("积分不足，无法重试任务: " + e.getMessage());
        }
        
        // 重置任务状态为 PROCESSING
        task.setStatus(StoryboardVideoTask.TaskStatus.PROCESSING);
        task.setErrorMessage(null);
        task.setUpdatedAt(java.time.LocalDateTime.now());
        taskRepository.save(task);
        
        // 使用事务钩子在事务提交后异步处理任务
        final String finalTaskId = taskId;
        final StoryboardVideoService self = applicationContext.getBean(StoryboardVideoService.class);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 事务提交后，在事务外执行异步方法
                self.processTaskAsync(finalTaskId);
            }
        });
        
        logger.info("任务 {} 已重新提交", taskId);
    }
    
    /**
     * 将 storyboardImages JSON 数组中的 Base64 图片上传到 COS，返回替换后的 JSON。
     * COS 未启用或上传失败时，返回原始 JSON（不丢数据）。
     */
    private String uploadStoryboardImagesToCos(String taskId, String storyboardImagesJson) {
        if (!cosService.isEnabled() || storyboardImagesJson == null || storyboardImagesJson.isEmpty()) {
            return storyboardImagesJson;
        }
        try {
            List<String> images = objectMapper.readValue(storyboardImagesJson,
                objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
            List<String> result = new ArrayList<>();
            for (int i = 0; i < images.size(); i++) {
                String img = images.get(i);
                if (img != null && img.startsWith("data:image")) {
                    try {
                        String cosUrl = cosService.uploadBase64Image(img, "storyboard_" + taskId + "_" + i + ".png");
                        if (cosUrl != null && !cosUrl.isEmpty()) {
                            result.add(cosUrl);
                            continue;
                        }
                    } catch (Exception e) {
                        logger.warn("上传单张分镜图到COS失败，保留原值: taskId={}, index={}", taskId, i);
                    }
                }
                result.add(img); // 非 Base64 或上传失败时保留原值
            }
            String json = objectMapper.writeValueAsString(result);
            logger.info("分镜图片上传COS完成: taskId={}, 共{}张", taskId, result.size());
            return json;
        } catch (Exception e) {
            logger.error("解析分镜图片JSON失败，保留原始数据: taskId={}", taskId, e);
            return storyboardImagesJson;
        }
    }
}
