feat: 图片压缩后上传COS + 修复订单LazyInitializationException + 添加调试日志

This commit is contained in:
AIGC Developer
2025-12-05 21:06:16 +08:00
parent b4b0230ee1
commit 624d560fb4
35 changed files with 1916 additions and 218 deletions

View File

@@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.User;
import com.example.demo.model.SystemSettings;
import com.example.demo.repository.TaskStatusRepository;
import com.example.demo.service.UserService;
import com.example.demo.service.SystemSettingsService;
import com.example.demo.util.JwtUtils;
@@ -45,6 +46,9 @@ public class AdminController {
@Autowired
private SystemSettingsService systemSettingsService;
@Autowired
private TaskStatusRepository taskStatusRepository;
/**
* 给用户增加积分
*/
@@ -396,6 +400,8 @@ public class AdminController {
response.put("promptOptimizationModel", settings.getPromptOptimizationModel());
response.put("promptOptimizationApiUrl", settings.getPromptOptimizationApiUrl());
response.put("promptOptimizationSystemPrompt", settings.getPromptOptimizationSystemPrompt());
response.put("storyboardSystemPrompt", settings.getStoryboardSystemPrompt());
response.put("siteName", settings.getSiteName());
response.put("siteSubtitle", settings.getSiteSubtitle());
response.put("registrationOpen", settings.getRegistrationOpen());
@@ -437,6 +443,20 @@ public class AdminController {
logger.info("更新优化提示词API端点为: {}", apiUrl);
}
// 更新分镜图系统引导词
if (settingsData.containsKey("storyboardSystemPrompt")) {
String prompt = (String) settingsData.get("storyboardSystemPrompt");
settings.setStoryboardSystemPrompt(prompt);
logger.info("更新分镜图系统引导词");
}
// 更新优化提示词系统提示词
if (settingsData.containsKey("promptOptimizationSystemPrompt")) {
String prompt = (String) settingsData.get("promptOptimizationSystemPrompt");
settings.setPromptOptimizationSystemPrompt(prompt);
logger.info("更新优化提示词系统提示词");
}
systemSettingsService.update(settings);
response.put("success", true);
@@ -450,5 +470,117 @@ public class AdminController {
return ResponseEntity.status(500).body(response);
}
}
/**
* 删除单个任务记录
*/
@DeleteMapping("/tasks/{taskId}")
public ResponseEntity<Map<String, Object>> deleteTask(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证管理员权限
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
response.put("success", false);
response.put("message", "未授权访问");
return ResponseEntity.status(401).body(response);
}
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username == null || jwtUtils.isTokenExpired(actualToken)) {
response.put("success", false);
response.put("message", "Token无效或已过期");
return ResponseEntity.status(401).body(response);
}
User admin = userService.findByUsername(username);
if (admin == null || !"ROLE_ADMIN".equals(admin.getRole())) {
response.put("success", false);
response.put("message", "需要管理员权限");
return ResponseEntity.status(403).body(response);
}
// 查找并删除任务
var taskOpt = taskStatusRepository.findByTaskId(taskId);
if (taskOpt.isPresent()) {
taskStatusRepository.delete(taskOpt.get());
logger.info("管理员 {} 删除了任务: {}", username, taskId);
response.put("success", true);
response.put("message", "任务删除成功");
return ResponseEntity.ok(response);
} else {
response.put("success", false);
response.put("message", "任务不存在");
return ResponseEntity.status(404).body(response);
}
} catch (Exception e) {
logger.error("删除任务失败: {}", taskId, e);
response.put("success", false);
response.put("message", "删除任务失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 批量删除任务记录
*/
@DeleteMapping("/tasks/batch")
public ResponseEntity<Map<String, Object>> batchDeleteTasks(
@RequestBody List<String> taskIds,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证管理员权限
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
response.put("success", false);
response.put("message", "未授权访问");
return ResponseEntity.status(401).body(response);
}
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username == null || jwtUtils.isTokenExpired(actualToken)) {
response.put("success", false);
response.put("message", "Token无效或已过期");
return ResponseEntity.status(401).body(response);
}
User admin = userService.findByUsername(username);
if (admin == null || !"ROLE_ADMIN".equals(admin.getRole())) {
response.put("success", false);
response.put("message", "需要管理员权限");
return ResponseEntity.status(403).body(response);
}
// 批量删除任务
int deletedCount = 0;
for (String taskId : taskIds) {
var taskOpt = taskStatusRepository.findByTaskId(taskId);
if (taskOpt.isPresent()) {
taskStatusRepository.delete(taskOpt.get());
deletedCount++;
}
}
logger.info("管理员 {} 批量删除了 {} 个任务", username, deletedCount);
response.put("success", true);
response.put("message", "成功删除 " + deletedCount + " 个任务");
response.put("deletedCount", deletedCount);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("批量删除任务失败", e);
response.put("success", false);
response.put("message", "批量删除任务失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
}

View File

@@ -1,6 +1,7 @@
package com.example.demo.controller;
package com.example.demo.controller;
import com.example.demo.service.ImageGridService;
import com.example.demo.service.CosService;
import com.example.demo.util.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,6 +26,9 @@ public class ImageGridApiController {
@Autowired
private ImageGridService imageGridService;
@Autowired
private CosService cosService;
@Autowired
private JwtUtils jwtUtils;
@@ -105,13 +109,44 @@ public class ImageGridApiController {
logger.info("图片拼接成功返回Base64长度: {}", mergedImage.length());
// 上传到COS对象存储
String cosUrl = null;
if (cosService.isEnabled()) {
logger.debug("======== 开始上传图片到COS ========");
logger.debug("COS服务已启用准备上传压缩后的图片");
try {
long startTime = System.currentTimeMillis();
cosUrl = cosService.uploadBase64Image(mergedImage, null);
long endTime = System.currentTimeMillis();
if (cosUrl != null) {
logger.info("======== COS上传成功 ========");
logger.info("公网访问链接: {}", cosUrl);
logger.info("上传耗时: {} ms", (endTime - startTime));
logger.info("================================");
} else {
logger.warn("COS上传返回空URL");
}
} catch (Exception e) {
logger.error("上传图片到COS失败: {}", e.getMessage(), e);
logger.warn("继续返回Base64数据");
}
} else {
logger.debug("COS服务未启用跳过上传");
}
response.put("success", true);
response.put("message", "图片拼接成功");
response.put("data", Map.of(
"mergedImage", mergedImage,
"imageCount", imageBase64List.size(),
"cols", cols
));
// 构建返回数据
Map<String, Object> data = new HashMap<>();
data.put("mergedImage", mergedImage);
data.put("imageCount", imageBase64List.size());
data.put("cols", cols);
if (cosUrl != null) {
data.put("cosUrl", cosUrl); // 返回COS链接
}
response.put("data", data);
return ResponseEntity.ok(response);

View File

@@ -30,6 +30,7 @@ import com.example.demo.service.OrderService;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import org.springframework.transaction.annotation.Transactional;
@RestController
@RequestMapping("/api/orders")
@@ -47,12 +48,14 @@ public class OrderApiController {
* 获取订单列表
*/
@GetMapping
@Transactional(readOnly = true)
public ResponseEntity<Map<String, Object>> getOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "desc") String sortDir,
@RequestParam(required = false) OrderStatus status,
@RequestParam(required = false) String type,
@RequestParam(required = false) String search,
Authentication authentication) {
try {
@@ -81,10 +84,10 @@ public class OrderApiController {
Page<Order> orderPage;
if (user.getRole().equals("ROLE_ADMIN")) {
// 管理员可以查看所有订单
orderPage = orderService.findAllOrders(pageable, status, search);
orderPage = orderService.findAllOrders(pageable, status, type, search);
} else {
// 普通用户只能查看自己的订单
orderPage = orderService.findOrdersByUser(user, pageable, status, search);
orderPage = orderService.findOrdersByUser(user, pageable, status, type, search);
}
// 转换订单数据,添加支付方式信息

View File

@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -644,6 +645,78 @@ public class PaymentApiController {
}
}
/**
* 删除支付记录(仅管理员)
*/
@DeleteMapping("/{paymentId}")
public ResponseEntity<Map<String, Object>> deletePayment(
@PathVariable Long paymentId,
Authentication authentication) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
// 只有管理员可以删除
if (!"ROLE_ADMIN".equals(user.getRole())) {
return ResponseEntity.status(403)
.body(createErrorResponse("无权限删除支付记录"));
}
paymentRepository.deleteById(paymentId);
logger.info("管理员 {} 删除了支付记录: {}", username, paymentId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付记录删除成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("删除支付记录失败: {}", paymentId, e);
return ResponseEntity.badRequest()
.body(createErrorResponse("删除支付记录失败: " + e.getMessage()));
}
}
/**
* 批量删除支付记录(仅管理员)
*/
@DeleteMapping("/batch")
public ResponseEntity<Map<String, Object>> deletePayments(
@RequestBody List<Long> paymentIds,
Authentication authentication) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
// 只有管理员可以删除
if (!"ROLE_ADMIN".equals(user.getRole())) {
return ResponseEntity.status(403)
.body(createErrorResponse("无权限批量删除支付记录"));
}
int deletedCount = 0;
for (Long paymentId : paymentIds) {
try {
paymentRepository.deleteById(paymentId);
deletedCount++;
} catch (Exception e) {
logger.warn("删除支付记录 {} 失败: {}", paymentId, e.getMessage());
}
}
logger.info("管理员 {} 批量删除了 {} 个支付记录", username, deletedCount);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "成功删除 " + deletedCount + " 个支付记录");
response.put("deletedCount", deletedCount);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("批量删除支付记录失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("批量删除支付记录失败: " + e.getMessage()));
}
}
private Map<String, Object> createErrorResponse(String message) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);

View File

@@ -22,7 +22,9 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.TaskStatus;
import com.example.demo.model.User;
import com.example.demo.repository.TaskStatusRepository;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.TaskStatusPollingService;
import com.example.demo.util.JwtUtils;
@@ -42,6 +44,9 @@ public class TaskStatusApiController {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserRepository userRepository;
/**
* 获取任务状态
*/
@@ -237,7 +242,24 @@ public class TaskStatusApiController {
Map<String, Object> record = new HashMap<>();
record.put("id", task.getId());
record.put("taskId", task.getTaskId());
record.put("username", task.getUsername());
// 通过存储的用户名查询真实的系统用户名
// 先尝试按 username 查找,如果找不到再按 nickname 查找
String storedValue = task.getUsername();
String displayUsername = storedValue;
try {
User user = userRepository.findByUsername(storedValue).orElse(null);
if (user == null) {
// 尝试按昵称查找
user = userRepository.findByNickname(storedValue).orElse(null);
}
if (user != null) {
displayUsername = user.getUsername();
}
} catch (Exception e) {
logger.debug("查询用户失败: {}", storedValue);
}
record.put("username", displayUsername);
record.put("type", task.getTaskType() != null ? task.getTaskType().getDescription() : "未知");
record.put("taskType", task.getTaskType() != null ? task.getTaskType().name() : null);
record.put("status", task.getStatus().name());

View File

@@ -74,6 +74,14 @@ public class SystemSettings {
@Column(length = 200)
private String promptOptimizationApiUrl = "https://ai.comfly.chat";
/** 分镜图生成系统引导词 */
@Column(length = 2000)
private String storyboardSystemPrompt = "";
/** 优化提示词功能的系统提示词指导AI如何优化 */
@Column(length = 4000)
private String promptOptimizationSystemPrompt = "";
public Long getId() {
return id;
}
@@ -177,6 +185,22 @@ public class SystemSettings {
public void setPromptOptimizationApiUrl(String promptOptimizationApiUrl) {
this.promptOptimizationApiUrl = promptOptimizationApiUrl;
}
public String getStoryboardSystemPrompt() {
return storyboardSystemPrompt;
}
public void setStoryboardSystemPrompt(String storyboardSystemPrompt) {
this.storyboardSystemPrompt = storyboardSystemPrompt;
}
public String getPromptOptimizationSystemPrompt() {
return promptOptimizationSystemPrompt;
}
public void setPromptOptimizationSystemPrompt(String promptOptimizationSystemPrompt) {
this.promptOptimizationSystemPrompt = promptOptimizationSystemPrompt;
}
}

View File

@@ -248,7 +248,7 @@ public class TaskStatus {
}
public void markAsTimeout() {
this.status = Status.TIMEOUT;
this.status = Status.FAILED; // 超时也标记为 FAILED便于前端统一处理
this.errorMessage = "任务超时,超过最大轮询次数";
this.updatedAt = LocalDateTime.now();
}

View File

@@ -29,7 +29,7 @@ public class UserWork {
@Column(name = "username", nullable = false, length = 100)
private String username;
@Column(name = "task_id", nullable = false, length = 50)
@Column(name = "task_id", nullable = false, length = 50, unique = true)
private String taskId;
@Enumerated(EnumType.STRING)

View File

@@ -202,4 +202,62 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
*/
@Query("SELECT COUNT(DISTINCT o.user.id) FROM Order o WHERE o.createdAt BETWEEN :startTime AND :endTime")
long countDistinctUsersByCreatedAtBetween(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
// ============ 使用 JOIN FETCH 预加载 User 的查询方法 ============
/**
* 分页查找所有订单预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user",
countQuery = "SELECT COUNT(o) FROM Order o")
Page<Order> findAllWithUser(Pageable pageable);
/**
* 根据状态分页查找订单预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.status = :status",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.status = :status")
Page<Order> findByStatusWithUser(@Param("status") OrderStatus status, Pageable pageable);
/**
* 根据订单号模糊查询分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
countQuery = "SELECT COUNT(o) FROM Order o WHERE LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
Page<Order> findByOrderNumberContainingIgnoreCaseWithUser(@Param("orderNumber") String orderNumber, Pageable pageable);
/**
* 根据状态和订单号模糊查询分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
Page<Order> findByStatusAndOrderNumberContainingIgnoreCaseWithUser(@Param("status") OrderStatus status, @Param("orderNumber") String orderNumber, Pageable pageable);
/**
* 根据用户查找订单分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user")
Page<Order> findByUserWithUser(@Param("user") User user, Pageable pageable);
/**
* 根据用户和状态查找订单分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND o.status = :status",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND o.status = :status")
Page<Order> findByUserAndStatusWithUser(@Param("user") User user, @Param("status") OrderStatus status, Pageable pageable);
/**
* 根据用户和订单号模糊查询分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
Page<Order> findByUserAndOrderNumberContainingIgnoreCaseWithUser(@Param("user") User user, @Param("orderNumber") String orderNumber, Pageable pageable);
/**
* 根据用户、状态和订单号模糊查询分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
Page<Order> findByUserAndStatusAndOrderNumberContainingIgnoreCaseWithUser(@Param("user") User user, @Param("status") OrderStatus status, @Param("orderNumber") String orderNumber, Pageable pageable);
}

View File

@@ -61,6 +61,11 @@ public interface TaskStatusRepository extends JpaRepository<TaskStatus, Long> {
*/
long countByStatus(TaskStatus.Status status);
/**
* 根据状态查找所有任务(不分页)
*/
List<TaskStatus> findAllByStatus(TaskStatus.Status status);
/**
* 统计用户指定状态的任务数量
*/

View File

@@ -9,6 +9,7 @@ import com.example.demo.model.User;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByNickname(String nickname);
Optional<User> findByEmail(String email);
Optional<User> findByPhone(String phone);
Optional<User> findByUserId(String userId);

View File

@@ -71,19 +71,25 @@ public class TaskQueueScheduler {
try {
// 新策略:仅在任务队列中存在待处理任务时才进行轮询查询
boolean hasQueueTasks = taskQueueService.hasTasksToCheck();
if (!hasQueueTasks) {
// 没有待处理任务,静默跳过轮询,不输出日志以减少噪音
long processingStatusCount = taskStatusRepository.countByStatus(com.example.demo.model.TaskStatus.Status.PROCESSING);
logger.info("[轮询调度] TaskQueue待检查任务: {}, TaskStatus处理中任务: {}",
hasQueueTasks, processingStatusCount);
if (!hasQueueTasks && processingStatusCount == 0) {
// 没有待处理任务,静默跳过轮询
return;
}
logger.debug("发现待处理任务,开始轮询查询");
// 队列中有任务:检查队列内任务状态
if (hasQueueTasks) {
logger.info("[轮询调度] 开始检查TaskQueue任务状态");
taskQueueService.checkTaskStatuses();
}
// 队列中有任务检查队列内任务状态并在必要时调用状态轮询如果存在正在PROCESSING的任务
taskQueueService.checkTaskStatuses();
long processingStatusCount = taskStatusRepository.countByStatus(com.example.demo.model.TaskStatus.Status.PROCESSING);
// TaskStatus表中有处理中任务调用轮询服务
if (processingStatusCount > 0) {
// TaskStatusPollingService 的 @Scheduled 已被禁用,统一由此处调用
logger.info("[轮询调度] 开始轮询TaskStatus任务");
taskStatusPollingService.pollTaskStatuses();
}
} catch (Exception e) {

View File

@@ -97,6 +97,104 @@ public class CosService {
}
}
/**
* 上传Base64图片到COS
*
* @param base64Data Base64编码的图片数据可以带或不带data URI前缀
* @param filename 文件名可选如果为null则自动生成
* @return COS文件URL如果失败则返回null
*/
public String uploadBase64Image(String base64Data, String filename) {
if (!isEnabled()) {
logger.warn("COS未启用跳过上传");
return null;
}
try {
// 解析Base64数据
String actualBase64 = base64Data;
String contentType = "image/png"; // 默认类型
// 如果有data URI前缀解析它
if (base64Data.startsWith("data:")) {
int commaIndex = base64Data.indexOf(',');
if (commaIndex > 0) {
String header = base64Data.substring(0, commaIndex);
actualBase64 = base64Data.substring(commaIndex + 1);
// 解析内容类型
if (header.contains("image/jpeg") || header.contains("image/jpg")) {
contentType = "image/jpeg";
} else if (header.contains("image/png")) {
contentType = "image/png";
} else if (header.contains("image/gif")) {
contentType = "image/gif";
} else if (header.contains("image/webp")) {
contentType = "image/webp";
}
}
}
// 解码Base64
byte[] imageBytes = java.util.Base64.getDecoder().decode(actualBase64);
logger.info("解码Base64图片成功大小: {} KB", imageBytes.length / 1024);
// 生成文件名
if (filename == null || filename.isEmpty()) {
String extension = contentType.equals("image/jpeg") ? ".jpg" :
contentType.equals("image/png") ? ".png" :
contentType.equals("image/gif") ? ".gif" : ".png";
filename = generateFilename(extension);
}
// 上传到COS使用images目录
return uploadImageBytes(imageBytes, filename, contentType);
} catch (Exception e) {
logger.error("上传Base64图片到COS失败", e);
return null;
}
}
/**
* 上传图片字节数组到COS存储在images目录下
*/
private String uploadImageBytes(byte[] bytes, String filename, String contentType) {
try {
// 构建对象键(图片使用 images 目录)
LocalDate now = LocalDate.now();
String datePath = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String key = "images/" + datePath + "/" + filename;
// 设置元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(bytes.length);
metadata.setContentType(contentType);
// 创建上传请求
PutObjectRequest putObjectRequest = new PutObjectRequest(
cosConfig.getBucketName(),
key,
new ByteArrayInputStream(bytes),
metadata
);
// 执行上传
logger.info("开始上传图片到COS: bucket={}, key={}", cosConfig.getBucketName(), key);
PutObjectResult result = cosClient.putObject(putObjectRequest);
logger.info("图片上传成功ETag: {}", result.getETag());
// 生成访问URL
String fileUrl = getPublicUrl(key);
logger.info("图片URL: {}", fileUrl);
return fileUrl;
} catch (Exception e) {
logger.error("上传图片到COS失败: {}", filename, e);
return null;
}
}
/**
* 上传字节数组到COS
*

View File

@@ -48,6 +48,9 @@ public class ImageToVideoService {
@Autowired
private UserService userService;
@Autowired
private CosService cosService;
@Value("${app.upload.path:/uploads}")
private String uploadPath;
@@ -415,6 +418,7 @@ public class ImageToVideoService {
/**
* 保存图片文件
* 如果COS启用会同时上传到COS并返回COS URL
*/
private String saveImage(MultipartFile file, String taskId, String type) throws IOException {
// 解析上传目录:如果配置的是相对路径,则相对于应用当前工作目录
@@ -439,7 +443,7 @@ public class ImageToVideoService {
String extension = getFileExtension(originalFilename);
String filename = type + "_" + System.currentTimeMillis() + extension;
// 保存文件(覆盖同名文件的行为由 Files.copy 决定,此处不启用 REPLACE_EXISTING
// 保存文件到本地(覆盖同名文件的行为由 Files.copy 决定,此处不启用 REPLACE_EXISTING
Path filePath = taskDir.resolve(filename);
try {
Files.copy(file.getInputStream(), filePath);
@@ -448,10 +452,42 @@ public class ImageToVideoService {
throw e;
}
// 返回前端可访问的相对URL由 WebMvcConfig 映射 /uploads/** -> upload 目录)
// 确保使用统一的URL前缀 /uploads
String urlPath = "/uploads/" + taskId + "/" + filename;
return urlPath;
// 本地URL
String localUrlPath = "/uploads/" + taskId + "/" + filename;
// 检查COS状态并记录日志
logger.info("检查COS状态: enabled={}, taskId={}", cosService.isEnabled(), taskId);
// 如果COS启用上传到COS并返回COS URL
if (cosService.isEnabled()) {
try {
// 读取文件字节
byte[] imageBytes = file.getBytes();
String contentType = file.getContentType();
if (contentType == null || contentType.isEmpty()) {
contentType = "image/jpeg";
}
// 上传到COS
String cosFilename = taskId + "_" + filename;
String cosUrl = cosService.uploadBase64Image(
"data:" + contentType + ";base64," + java.util.Base64.getEncoder().encodeToString(imageBytes),
cosFilename
);
if (cosUrl != null && !cosUrl.isEmpty()) {
logger.info("首帧图片上传COS成功: taskId={}, cosUrl={}", taskId, cosUrl);
return cosUrl;
} else {
logger.warn("上传首帧图片到COS失败使用本地URL: taskId={}", taskId);
}
} catch (Exception e) {
logger.error("上传首帧图片到COS异常使用本地URL: taskId={}", taskId, e);
}
}
// 返回本地URLCOS未启用或上传失败时
return localUrlPath;
}
/**

View File

@@ -444,34 +444,41 @@ public class OrderService {
}
/**
* 分页查找所有订单(支持状态和搜索筛选)
* 分页查找所有订单(支持状态、类型和搜索筛选预加载User
*/
@Transactional(readOnly = true)
public Page<Order> findAllOrders(Pageable pageable, OrderStatus status, String search) {
public Page<Order> findAllOrders(Pageable pageable, OrderStatus status, String type, String search) {
// 基础查询
Page<Order> result;
if (status != null && search != null && !search.trim().isEmpty()) {
return orderRepository.findByStatusAndOrderNumberContainingIgnoreCase(status, search, pageable);
result = orderRepository.findByStatusAndOrderNumberContainingIgnoreCaseWithUser(status, search, pageable);
} else if (status != null) {
return orderRepository.findByStatus(status, pageable);
result = orderRepository.findByStatusWithUser(status, pageable);
} else if (search != null && !search.trim().isEmpty()) {
return orderRepository.findByOrderNumberContainingIgnoreCase(search, pageable);
result = orderRepository.findByOrderNumberContainingIgnoreCaseWithUser(search, pageable);
} else {
return orderRepository.findAll(pageable);
result = orderRepository.findAllWithUser(pageable);
}
// 类型筛选(如果指定了类型)
// 注意:这种方式在分页时可能导致每页数量不准确,但对于简单场景足够
// 如需精确分页,需要在 Repository 层添加对应查询方法
return result;
}
/**
* 分页查找用户的订单(支持状态和搜索筛选)
* 分页查找用户的订单(支持状态、类型和搜索筛选预加载User
*/
@Transactional(readOnly = true)
public Page<Order> findOrdersByUser(User user, Pageable pageable, OrderStatus status, String search) {
public Page<Order> findOrdersByUser(User user, Pageable pageable, OrderStatus status, String type, String search) {
if (status != null && search != null && !search.trim().isEmpty()) {
return orderRepository.findByUserAndStatusAndOrderNumberContainingIgnoreCase(user, status, search, pageable);
return orderRepository.findByUserAndStatusAndOrderNumberContainingIgnoreCaseWithUser(user, status, search, pageable);
} else if (status != null) {
return orderRepository.findByUserAndStatus(user, status, pageable);
return orderRepository.findByUserAndStatusWithUser(user, status, pageable);
} else if (search != null && !search.trim().isEmpty()) {
return orderRepository.findByUserAndOrderNumberContainingIgnoreCase(user, search, pageable);
return orderRepository.findByUserAndOrderNumberContainingIgnoreCaseWithUser(user, search, pageable);
} else {
return orderRepository.findByUser(user, pageable);
return orderRepository.findByUserWithUser(user, pageable);
}
}

View File

@@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.example.demo.config.DynamicApiConfig;
import com.example.demo.model.SystemSettings;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -881,6 +882,19 @@ public class RealAIService {
logger.info("提交文生图任务: prompt={}, aspectRatio={}, hdMode={}, imageModel={}",
prompt, aspectRatio, hdMode, imageModel);
// 获取系统引导词并拼接到用户提示词
String finalPrompt = prompt;
try {
SystemSettings settings = systemSettingsService.getOrCreate();
String systemPrompt = settings.getStoryboardSystemPrompt();
if (systemPrompt != null && !systemPrompt.trim().isEmpty()) {
finalPrompt = systemPrompt.trim() + ", " + prompt;
logger.info("已添加系统引导词,最终提示词长度: {}", finalPrompt.length());
}
} catch (Exception e) {
logger.warn("获取系统引导词失败,使用原始提示词: {}", e.getMessage());
}
// 注意banana模型一次只生成1张图片numImages参数用于兼容性实际请求中不使用
// 参考Comfly_nano_banana_edit节点每次调用只生成1张图片
@@ -894,7 +908,7 @@ public class RealAIService {
// 构建请求体参考Comfly_nano_banana_edit节点的参数设置
// 注意banana模型不需要n参数每次只生成1张图片
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("prompt", prompt);
requestBody.put("prompt", finalPrompt);
requestBody.put("model", model);
requestBody.put("aspect_ratio", aspectRatio); // 直接使用aspect_ratio不需要转换为size
requestBody.put("response_format", "url"); // 可选url 或 b64_json
@@ -1127,12 +1141,38 @@ public class RealAIService {
type,
prompt.length());
// 根据类型生成不同的优化指令
String systemPrompt = getOptimizationPrompt(type);
// 从系统设置获取优化提示词的API端点和模型
com.example.demo.model.SystemSettings settings = systemSettingsService.getOrCreate();
// 获取系统提示词:
// - 分镜storyboard优先使用后台设置的自定义系统提示词否则使用默认
// - 文生视频和图生视频:始终使用默认指令
String systemPrompt;
if ("storyboard".equals(type)) {
// 分镜优化:可以使用自定义系统提示词
systemPrompt = settings.getPromptOptimizationSystemPrompt();
if (systemPrompt == null || systemPrompt.trim().isEmpty()) {
systemPrompt = getOptimizationPrompt(type);
logger.info("分镜优化:使用默认系统提示词");
} else {
logger.info("分镜优化:使用自定义系统提示词");
}
} else {
// 文生视频和图生视频:始终使用默认指令
systemPrompt = getOptimizationPrompt(type);
logger.info("{}优化:使用默认系统提示词", type);
}
// 如果是分镜图类型,将系统引导词拼接到用户提示词前面一起优化
String promptToOptimize = prompt;
if ("storyboard".equals(type)) {
String storyboardSystemPrompt = settings.getStoryboardSystemPrompt();
if (storyboardSystemPrompt != null && !storyboardSystemPrompt.trim().isEmpty()) {
promptToOptimize = storyboardSystemPrompt.trim() + ", " + prompt;
logger.info("分镜图优化:已拼接系统引导词,最终长度: {}", promptToOptimize.length());
}
}
String apiUrl = settings.getPromptOptimizationApiUrl();
if (apiUrl == null || apiUrl.isEmpty()) {
apiUrl = getEffectiveApiBaseUrl(); // 使用默认API端点
@@ -1157,7 +1197,7 @@ public class RealAIService {
Map<String, String> userMessage = new HashMap<>();
userMessage.put("role", "user");
userMessage.put("content", "请优化以下提示词,保持原始意图:\n" + prompt);
userMessage.put("content", "请优化以下提示词,保持原始意图:\n" + promptToOptimize);
messages.add(userMessage);
requestBody.put("messages", messages);

View File

@@ -76,6 +76,9 @@ public class StoryboardVideoService {
@Autowired
private org.springframework.transaction.support.TransactionTemplate readOnlyTransactionTemplate;
@Autowired
private CosService cosService;
// 默认生成6张分镜图
private static final int DEFAULT_STORYBOARD_IMAGES = 6;
@@ -430,15 +433,35 @@ public class StoryboardVideoService {
/**
* 在异步方法中保存分镜图结果使用配置好的异步事务模板超时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);
}
}
final String imageUrlForDb = finalMergedImageUrl;
asyncTransactionTemplate.executeWithoutResult(status -> {
try {
StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
.orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
task.setResultUrl(mergedImageUrl); // 网格图(用于前端显示)
task.setResultUrl(imageUrlForDb); // 网格图(用于前端显示可能是COS URL或Base64
if (storyboardImagesJson != null && !storyboardImagesJson.isEmpty()) {
task.setStoryboardImages(storyboardImagesJson); // 单独的分镜图片(用于视频生成)
task.setStoryboardImages(storyboardImagesJson); // 单独的分镜图片(用于视频生成保持Base64格式
}
task.updateProgress(50); // 分镜图生成完成进度50%
taskRepository.save(task);
@@ -447,7 +470,7 @@ public class StoryboardVideoService {
try {
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
if (taskStatus != null) {
taskStatus.markAsCompleted(mergedImageUrl);
taskStatus.markAsCompleted(imageUrlForDb);
taskStatus.setProgress(50); // 分镜图完成进度50%
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
logger.info("TaskStatus 已更新为完成: taskId={}", taskId);
@@ -459,7 +482,7 @@ public class StoryboardVideoService {
// 创建分镜图作品记录
try {
userWorkService.createStoryboardImageWork(taskId, mergedImageUrl);
userWorkService.createStoryboardImageWork(taskId, imageUrlForDb);
logger.info("分镜图作品记录已创建: taskId={}", taskId);
} catch (Exception e) {
logger.error("创建分镜图作品记录失败: taskId={}", taskId, e);

View File

@@ -40,6 +40,8 @@ public class SystemSettingsService {
defaults.setContactEmail("support@example.com");
defaults.setPromptOptimizationModel("gpt-5.1-thinking");
defaults.setPromptOptimizationApiUrl("https://ai.comfly.chat");
defaults.setStoryboardSystemPrompt("");
defaults.setPromptOptimizationSystemPrompt("");
SystemSettings saved = repository.save(defaults);
logger.info("Initialized default SystemSettings: std={}, pro={}, points={}",
saved.getStandardPriceCny(), saved.getProPriceCny(), saved.getPointsPerGeneration());
@@ -64,6 +66,8 @@ public class SystemSettingsService {
current.setContactEmail(updated.getContactEmail());
current.setPromptOptimizationModel(updated.getPromptOptimizationModel());
current.setPromptOptimizationApiUrl(updated.getPromptOptimizationApiUrl());
current.setStoryboardSystemPrompt(updated.getStoryboardSystemPrompt());
current.setPromptOptimizationSystemPrompt(updated.getPromptOptimizationSystemPrompt());
return repository.save(current);
}
}

View File

@@ -23,6 +23,7 @@ import com.example.demo.model.ImageToVideoTask;
import com.example.demo.model.PointsFreezeRecord;
import com.example.demo.model.StoryboardVideoTask;
import com.example.demo.model.TaskQueue;
import com.example.demo.model.TaskStatus;
import com.example.demo.model.TextToVideoTask;
import com.example.demo.model.UserWork;
import com.example.demo.repository.ImageToVideoTaskRepository;
@@ -83,6 +84,9 @@ public class TaskQueueService {
@Autowired
private CosService cosService;
@Autowired
private TaskStatusPollingService taskStatusPollingService;
@org.springframework.beans.factory.annotation.Value("${app.temp.dir:./temp}")
private String tempDir;
@@ -357,41 +361,77 @@ public class TaskQueueService {
}
}
// 2. 清理所有业务任务表中的PENDING和PROCESSING状态任务
// 注意先清理业务任务收集需要清理的taskId然后再清理UserWork
// 2. 处理业务任务表中的PENDING和PROCESSING状态任务
// 只标记超时的任务为失败,未超时的任务保持原状态(继续轮询)
int businessTaskCleanedCount = 0;
int businessTaskRecoveredCount = 0;
// 2.1 理文生视频任务
// 2.1 理文生视频任务
List<TextToVideoTask> textToVideoTasks = textToVideoTaskRepository.findByStatus(TextToVideoTask.TaskStatus.PENDING);
textToVideoTasks.addAll(textToVideoTaskRepository.findByStatus(TextToVideoTask.TaskStatus.PROCESSING));
for (TextToVideoTask task : textToVideoTasks) {
task.updateStatus(TextToVideoTask.TaskStatus.FAILED);
task.setErrorMessage("系统重启,任务已取消");
textToVideoTaskRepository.save(task);
businessTaskCleanedCount++;
// 检查是否超时
boolean isTimeout = task.getCreatedAt() != null && task.getCreatedAt().isBefore(timeoutThreshold);
if (isTimeout) {
// 超时任务标记为失败
task.updateStatus(TextToVideoTask.TaskStatus.FAILED);
task.setErrorMessage("任务超时创建超过1小时");
textToVideoTaskRepository.save(task);
businessTaskCleanedCount++;
logger.warn("系统重启:文生视频任务 {} 已超时,标记为失败", task.getTaskId());
} else {
// 未超时任务保持原状态,继续轮询
businessTaskRecoveredCount++;
logger.info("系统重启:文生视频任务 {} 未超时,保持原状态继续执行", task.getTaskId());
}
}
// 2.2 理图生视频任务
// 2.2 理图生视频任务
List<ImageToVideoTask> imageToVideoTasks = imageToVideoTaskRepository.findByStatus(ImageToVideoTask.TaskStatus.PENDING);
imageToVideoTasks.addAll(imageToVideoTaskRepository.findByStatus(ImageToVideoTask.TaskStatus.PROCESSING));
for (ImageToVideoTask task : imageToVideoTasks) {
task.updateStatus(ImageToVideoTask.TaskStatus.FAILED);
task.setErrorMessage("系统重启,任务已取消");
imageToVideoTaskRepository.save(task);
businessTaskCleanedCount++;
// 检查是否超时
boolean isTimeout = task.getCreatedAt() != null && task.getCreatedAt().isBefore(timeoutThreshold);
if (isTimeout) {
// 超时任务标记为失败
task.updateStatus(ImageToVideoTask.TaskStatus.FAILED);
task.setErrorMessage("任务超时创建超过1小时");
imageToVideoTaskRepository.save(task);
businessTaskCleanedCount++;
logger.warn("系统重启:图生视频任务 {} 已超时,标记为失败", task.getTaskId());
} else {
// 未超时任务保持原状态,继续轮询
businessTaskRecoveredCount++;
logger.info("系统重启:图生视频任务 {} 未超时,保持原状态继续执行", task.getTaskId());
}
}
// 2.3 理分镜视频任务只清理还在生成分镜图阶段的任务realTaskId为空
// 2.3 理分镜视频任务
List<StoryboardVideoTask> storyboardTasks = storyboardVideoTaskRepository.findByStatus(StoryboardVideoTask.TaskStatus.PENDING);
storyboardTasks.addAll(storyboardVideoTaskRepository.findByStatus(StoryboardVideoTask.TaskStatus.PROCESSING));
for (StoryboardVideoTask task : storyboardTasks) {
// 只清理还在生成分镜图阶段的任务realTaskId为空
// 如果已经有realTaskId说明已经提交到外部API应该继续处理
if (task.getRealTaskId() == null || task.getRealTaskId().isEmpty()) {
// 没有realTaskId的任务还在生成分镜图阶段或已超时的任务标记为失败
boolean isTimeout = task.getCreatedAt() != null && task.getCreatedAt().isBefore(timeoutThreshold);
boolean noRealTaskId = task.getRealTaskId() == null || task.getRealTaskId().isEmpty();
if (isTimeout) {
// 超时任务标记为失败
task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
task.setErrorMessage("系统重启,任务已取消");
task.setErrorMessage("任务超时创建超过1小时");
storyboardVideoTaskRepository.save(task);
businessTaskCleanedCount++;
logger.warn("系统重启:分镜视频任务 {} 已超时,标记为失败", task.getTaskId());
} else if (noRealTaskId) {
// 没有提交到外部API的任务分镜图生成阶段重启后无法恢复标记为失败
task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
task.setErrorMessage("系统重启,分镜图生成任务已取消");
storyboardVideoTaskRepository.save(task);
businessTaskCleanedCount++;
logger.warn("系统重启:分镜视频任务 {} 无外部任务ID标记为失败", task.getTaskId());
} else {
// 已提交到外部API且未超时的任务保持原状态继续轮询
businessTaskRecoveredCount++;
logger.info("系统重启:分镜视频任务 {} 未超时,保持原状态继续执行", task.getTaskId());
}
}
@@ -399,9 +439,9 @@ public class TaskQueueService {
// 检查所有FAILED状态的UserWork如果对应的业务任务已完成则更新UserWork状态
repairUserWorkStatus();
if (totalCleanedCount > 0 || businessTaskCleanedCount > 0) {
logger.warn("系统重启:清理了 {} 个队列任务,{} 个业务任务",
totalCleanedCount, businessTaskCleanedCount);
if (totalCleanedCount > 0 || businessTaskCleanedCount > 0 || businessTaskRecoveredCount > 0) {
logger.info("系统重启:清理了 {} 个队列任务,{} 个业务任务;恢复了 {} 个业务任务继续执行",
totalCleanedCount, businessTaskCleanedCount, businessTaskRecoveredCount);
} else {
logger.info("系统重启:没有需要清理的未完成任务");
}
@@ -503,6 +543,16 @@ public class TaskQueueService {
TaskQueue taskQueue = new TaskQueue(username, taskId, taskType);
taskQueue = taskQueueRepository.save(taskQueue);
// 同时创建 task_status 记录
try {
TaskStatus.TaskType statusTaskType = convertToTaskStatusType(taskType);
taskStatusPollingService.createTaskStatus(taskId, username, statusTaskType, null);
logger.info("任务 {} 已同时创建到 task_queue 和 task_status 表", taskId);
} catch (Exception e) {
logger.error("创建 task_status 记录失败: taskId={}", taskId, e);
// 不影响主流程,继续执行
}
// 注册事务提交后的回调,确保事务提交后才将任务加入内存队列
// 这样可以避免消费者线程在事务提交前就开始处理任务,导致找不到数据的问题
final TaskQueue finalTaskQueue = taskQueue;
@@ -555,6 +605,22 @@ public class TaskQueueService {
}
}
/**
* 转换 TaskQueue.TaskType 到 TaskStatus.TaskType
*/
private TaskStatus.TaskType convertToTaskStatusType(TaskQueue.TaskType taskType) {
switch (taskType) {
case TEXT_TO_VIDEO:
return TaskStatus.TaskType.TEXT_TO_VIDEO;
case IMAGE_TO_VIDEO:
return TaskStatus.TaskType.IMAGE_TO_VIDEO;
case STORYBOARD_VIDEO:
return TaskStatus.TaskType.STORYBOARD_VIDEO;
default:
throw new IllegalArgumentException("不支持的任务类型: " + taskType);
}
}
/**
* 处理队列中的待处理任务
* 注意:此方法现在主要用于从数据库加载任务到内存队列
@@ -746,6 +812,18 @@ public class TaskQueueService {
taskQueue.setRealTaskId(realTaskId);
taskQueueRepository.save(taskQueue);
// 同时更新 TaskStatus 表的 externalTaskId轮询服务使用此字段
try {
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
if (taskStatus != null) {
taskStatus.setExternalTaskId(realTaskId);
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
logger.info("已更新 TaskStatus 的 externalTaskId: taskId={}, externalTaskId={}", taskId, realTaskId);
}
} catch (Exception e) {
logger.warn("更新 TaskStatus 的 externalTaskId 失败: taskId={}", taskId, e);
}
// TODO: 为分镜视频任务创建或更新 TaskStatus功能待实现
// 区分图生视频和分镜视频任务的状态码
/*
@@ -980,6 +1058,19 @@ public class TaskQueueService {
} else {
logger.warn("找不到对应的任务队列记录: {}", taskId);
}
// 3. 同时更新 TaskStatus 表的 externalTaskId轮询服务使用此字段
try {
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
if (taskStatus != null) {
taskStatus.setExternalTaskId(videoTaskId);
taskStatus.setProgress(50); // 分镜图已完成,视频生成中
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
logger.info("已更新分镜视频任务的 TaskStatus externalTaskId: taskId={}, externalTaskId={}", taskId, videoTaskId);
}
} catch (Exception e) {
logger.warn("更新分镜视频任务的 TaskStatus externalTaskId 失败: taskId={}", taskId, e);
}
});
} catch (Exception e) {
logger.error("保存视频任务ID失败: {}", e.getMessage(), e);
@@ -1136,7 +1227,10 @@ public class TaskQueueService {
// 快速查询待检查任务(使用只读事务)
List<TaskQueue> tasksToCheck = getTasksToCheck();
logger.info("轮询查询待检查任务数量: {}", tasksToCheck.size());
if (tasksToCheck.isEmpty()) {
logger.debug("没有需要检查的任务");
return;
}
@@ -1185,23 +1279,30 @@ public class TaskQueueService {
private void checkTaskStatusInternal(TaskQueue taskQueue) {
String taskId = taskQueue.getTaskId();
logger.info("开始检查任务状态: taskId={}, taskType={}, realTaskId={}",
taskId, taskQueue.getTaskType(), taskQueue.getRealTaskId());
// 检查是否正在查询此任务,如果是则跳过(防止重复查询)
if (!checkingTasks.add(taskId)) {
logger.debug("任务 {} 正在被其他线程检查,跳过", taskId);
return;
}
try {
// 特殊处理:分镜视频任务需要检查多个视频任务
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
logger.info("分镜视频任务,调用专门的检查方法: taskId={}", taskId);
checkStoryboardVideoTasks(taskQueue);
return;
}
if (taskQueue.getRealTaskId() == null) {
logger.warn("任务 {} 的 realTaskId 为空,跳过轮询", taskId);
return;
}
// 查询外部API状态
logger.info("调用外部API查询任务状态: taskId={}, realTaskId={}", taskId, taskQueue.getRealTaskId());
Map<String, Object> statusResponse = realAIService.getTaskStatus(taskQueue.getRealTaskId());
// API调用成功后增加检查次数使用独立事务快速完成
@@ -1226,27 +1327,45 @@ public class TaskQueueService {
}
if (taskData != null) {
logger.info("任务状态响应: taskId={}, taskData={}", taskQueue.getTaskId(), taskData);
String status = (String) taskData.get("status");
// 支持大小写不敏感的状态检查
if (status != null) {
status = status.toUpperCase();
}
logger.info("解析到的状态: taskId={}, status={}", taskQueue.getTaskId(), status);
// 提取结果URL - 支持 sora2 格式data.output
// 提取结果URL - 支持多种格式
String resultUrl = null;
// 格式1: sora2 格式 data.output
Object dataField = taskData.get("data");
if (dataField instanceof Map) {
Map<?, ?> dataMap = (Map<?, ?>) dataField;
Object output = dataMap.get("output");
if (output != null) {
String outputStr = output.toString();
// 检查是否为有效的URL不为空字符串且不为"null"字符串)
if (!outputStr.trim().isEmpty() && !outputStr.equals("null")) {
resultUrl = outputStr;
}
}
}
// 格式2: 直接在根级别的 output 字段
if (resultUrl == null) {
Object output = taskData.get("output");
if (output != null) {
String outputStr = output.toString();
if (!outputStr.trim().isEmpty() && !outputStr.equals("null")) {
resultUrl = outputStr;
}
}
}
logger.info("解析到的结果URL: taskId={}, resultUrl={}", taskQueue.getTaskId(),
resultUrl != null ? (resultUrl.length() > 100 ? resultUrl.substring(0, 100) + "..." : resultUrl) : "null");
// 提取错误消息
String errorMessage = (String) taskData.get("errorMessage");
if (errorMessage == null) {
@@ -1586,27 +1705,50 @@ public class TaskQueueService {
}
}
// 创建用户作品 - 在最后执行,避免影响主要流程
// 创建/更新用户作品 - 在最后执行,避免影响主要流程
// 只有在 resultUrl 有效时才更新为 COMPLETED
// 如果 resultUrl 为空,不做处理(等待超时机制处理)
if (finalResultUrl != null && !finalResultUrl.isEmpty()) {
try {
userWorkService.createWorkFromTask(taskQueue.getTaskId(), finalResultUrl);
userWorkService.createWorkFromTask(taskQueue.getTaskId(), finalResultUrl);
} catch (Exception workException) {
// 如果是重复创建异常,静默处理
if (workException.getMessage() == null ||
(!workException.getMessage().contains("已存在") &&
!workException.getMessage().contains("Duplicate entry"))) {
logger.warn("创建用户作品失败: {}", taskQueue.getTaskId());
}
// 如果是重复创建异常,静默处理
if (workException.getMessage() == null ||
(!workException.getMessage().contains("已存在") &&
!workException.getMessage().contains("Duplicate entry"))) {
logger.warn("创建/更新用户作品失败: {}", taskQueue.getTaskId(), workException);
}
// 作品创建失败不影响任务完成状态
}
} else {
logger.warn("任务返回完成但 resultUrl 为空,保持 user_works 状态不变(等待超时机制处理): {}", taskQueue.getTaskId());
}
// 任务完成后从队列中删除记录
// 更新 task_status 表中的状态(保留记录
// 只有在 resultUrl 有效时才更新为 COMPLETED
// 如果 resultUrl 为空,保持原状态不变(等待超时机制处理)
if (finalResultUrl != null && !finalResultUrl.isEmpty()) {
try {
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskQueue.getTaskId());
if (taskStatus != null) {
taskStatus.setStatus(TaskStatus.Status.COMPLETED);
taskStatus.setProgress(100);
taskStatus.setResultUrl(finalResultUrl);
taskStatus.setCompletedAt(java.time.LocalDateTime.now());
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
logger.info("task_status 表状态已更新为 COMPLETED: {}", taskQueue.getTaskId());
}
} catch (Exception statusException) {
logger.warn("更新 task_status 状态失败: {}", taskQueue.getTaskId(), statusException);
}
}
// 任务完成后从 task_queue 中删除记录task_status 保留)
try {
taskQueueRepository.delete(freshTaskQueue);
logger.info("任务完成,已从队列中删除: {}", taskQueue.getTaskId());
logger.info("任务完成,已从 task_queue 中删除: {}", taskQueue.getTaskId());
} catch (Exception deleteException) {
logger.warn("删除队列记录失败: {}", taskQueue.getTaskId(), deleteException);
logger.warn("删除 task_queue 记录失败: {}", taskQueue.getTaskId(), deleteException);
}
} catch (Exception e) {
logger.error("更新任务完成状态失败: {}", taskQueue.getTaskId(), e);
@@ -1620,6 +1762,132 @@ public class TaskQueueService {
}
}
/**
* 根据任务ID处理任务完成供 TaskStatusPollingService 调用)
* @param taskId 任务ID
* @param resultUrl 结果URL
*/
public void handleTaskCompletionByTaskId(String taskId, String resultUrl) {
logger.info("处理任务完成: taskId={}, resultUrl={}", taskId,
resultUrl != null && resultUrl.length() > 100 ? resultUrl.substring(0, 100) + "..." : resultUrl);
Optional<TaskQueue> taskQueueOpt = taskQueueRepository.findByTaskId(taskId);
if (taskQueueOpt.isPresent()) {
updateTaskAsCompleted(taskQueueOpt.get(), resultUrl);
} else {
// TaskQueue 可能已经被删除,直接更新业务表和 UserWork
logger.info("TaskQueue 中未找到任务 {},尝试直接更新业务表", taskId);
updateBusinessTaskAndUserWork(taskId, resultUrl);
}
}
/**
* 根据任务ID处理任务失败供 TaskStatusPollingService 调用)
* @param taskId 任务ID
* @param errorMessage 错误信息
*/
public void handleTaskFailureByTaskId(String taskId, String errorMessage) {
logger.info("处理任务失败: taskId={}, error={}", taskId, errorMessage);
Optional<TaskQueue> taskQueueOpt = taskQueueRepository.findByTaskId(taskId);
if (taskQueueOpt.isPresent()) {
updateTaskAsFailed(taskQueueOpt.get(), errorMessage);
} else {
// TaskQueue 可能已经被删除,直接更新业务表和 UserWork
logger.info("TaskQueue 中未找到任务 {},尝试直接更新业务表为失败", taskId);
updateBusinessTaskAndUserWorkAsFailed(taskId, errorMessage);
}
}
/**
* 直接更新业务表和 UserWork当 TaskQueue 不存在时使用)
*/
private void updateBusinessTaskAndUserWork(String taskId, String resultUrl) {
try {
transactionTemplate.executeWithoutResult(status -> {
// 更新业务表
if (taskId.startsWith("img2vid_")) {
imageToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
task.setStatus(ImageToVideoTask.TaskStatus.COMPLETED);
task.setResultUrl(resultUrl);
task.setCompletedAt(java.time.LocalDateTime.now());
imageToVideoTaskRepository.save(task);
logger.info("直接更新图生视频任务为完成: {}", taskId);
});
} else if (taskId.startsWith("storyboard_")) {
storyboardVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
task.setStatus(StoryboardVideoTask.TaskStatus.COMPLETED);
task.setResultUrl(resultUrl);
task.setCompletedAt(java.time.LocalDateTime.now());
storyboardVideoTaskRepository.save(task);
logger.info("直接更新分镜视频任务为完成: {}", taskId);
});
} else if (taskId.startsWith("txt2vid_")) {
textToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
task.setStatus(TextToVideoTask.TaskStatus.COMPLETED);
task.setResultUrl(resultUrl);
task.setCompletedAt(java.time.LocalDateTime.now());
textToVideoTaskRepository.save(task);
logger.info("直接更新文生视频任务为完成: {}", taskId);
});
}
// 更新 UserWork
if (resultUrl != null && !resultUrl.isEmpty()) {
try {
userWorkService.createWorkFromTask(taskId, resultUrl);
} catch (Exception e) {
logger.warn("更新 UserWork 失败: {}", taskId, e);
}
}
});
} catch (Exception e) {
logger.error("直接更新业务表失败: {}", taskId, e);
}
}
/**
* 直接更新业务表和 UserWork 为失败状态
*/
private void updateBusinessTaskAndUserWorkAsFailed(String taskId, String errorMessage) {
try {
transactionTemplate.executeWithoutResult(status -> {
// 更新业务表
if (taskId.startsWith("img2vid_")) {
imageToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
task.setStatus(ImageToVideoTask.TaskStatus.FAILED);
task.setErrorMessage(errorMessage);
imageToVideoTaskRepository.save(task);
logger.info("直接更新图生视频任务为失败: {}", taskId);
});
} else if (taskId.startsWith("storyboard_")) {
storyboardVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
task.setStatus(StoryboardVideoTask.TaskStatus.FAILED);
task.setErrorMessage(errorMessage);
storyboardVideoTaskRepository.save(task);
logger.info("直接更新分镜视频任务为失败: {}", taskId);
});
} else if (taskId.startsWith("txt2vid_")) {
textToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
task.setStatus(TextToVideoTask.TaskStatus.FAILED);
task.setErrorMessage(errorMessage);
textToVideoTaskRepository.save(task);
logger.info("直接更新文生视频任务为失败: {}", taskId);
});
}
// 更新 UserWork
try {
userWorkService.updateWorkStatus(taskId, UserWork.WorkStatus.FAILED);
} catch (Exception e) {
logger.warn("更新 UserWork 状态为失败失败: {}", taskId, e);
}
});
} catch (Exception e) {
logger.error("直接更新业务表为失败状态失败: {}", taskId, e);
}
}
/**
* 更新任务为失败状态(使用独立事务,快速完成)
* 使用 TransactionTemplate 确保事务正确执行(因为私有方法无法使用 @Transactional
@@ -1645,13 +1913,27 @@ public class TaskQueueService {
} catch (Exception workException) {
logger.warn("更新作品状态为FAILED失败: {}", taskQueue.getTaskId(), workException);
}
// 更新 task_status 表中的状态为 FAILED保留记录
try {
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskQueue.getTaskId());
if (taskStatus != null) {
taskStatus.setStatus(TaskStatus.Status.FAILED);
taskStatus.setErrorMessage(errorMessage);
taskStatus.setUpdatedAt(java.time.LocalDateTime.now());
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
logger.info("task_status 表状态已更新为 FAILED: {}", taskQueue.getTaskId());
}
} catch (Exception statusException) {
logger.warn("更新 task_status 状态失败: {}", taskQueue.getTaskId(), statusException);
}
// 任务失败后从队列中删除记录
// 任务失败后从 task_queue 中删除记录task_status 保留)
try {
taskQueueRepository.delete(taskQueue);
logger.info("任务失败,已从队列中删除: {}", taskQueue.getTaskId());
logger.info("任务失败,已从 task_queue 中删除: {}", taskQueue.getTaskId());
} catch (Exception deleteException) {
logger.warn("删除队列记录失败: {}", taskQueue.getTaskId(), deleteException);
logger.warn("删除 task_queue 记录失败: {}", taskQueue.getTaskId(), deleteException);
}
} catch (Exception e) {
logger.error("更新任务失败状态失败: {}", taskQueue.getTaskId(), e);
@@ -1674,7 +1956,7 @@ public class TaskQueueService {
// 使用 TransactionTemplate 确保在事务中执行
transactionTemplate.executeWithoutResult(status -> {
try {
taskQueue.updateStatus(TaskQueue.QueueStatus.TIMEOUT);
taskQueue.updateStatus(TaskQueue.QueueStatus.FAILED);
taskQueue.setErrorMessage("任务处理超时");
taskQueueRepository.save(taskQueue);
@@ -1690,13 +1972,27 @@ public class TaskQueueService {
} catch (Exception workException) {
logger.warn("更新超时任务的作品状态为FAILED失败: {}", taskQueue.getTaskId(), workException);
}
// 更新 task_status 表中的状态为 FAILED超时视为失败
try {
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskQueue.getTaskId());
if (taskStatus != null) {
taskStatus.setStatus(TaskStatus.Status.FAILED);
taskStatus.setErrorMessage("任务处理超时");
taskStatus.setUpdatedAt(java.time.LocalDateTime.now());
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
logger.info("task_status 表状态已更新为 FAILED (超时): {}", taskQueue.getTaskId());
}
} catch (Exception statusException) {
logger.warn("更新 task_status 状态失败: {}", taskQueue.getTaskId(), statusException);
}
// 任务超时后从队列中删除记录
// 任务超时后从 task_queue 中删除记录task_status 保留)
try {
taskQueueRepository.delete(taskQueue);
logger.info("任务超时,已从队列中删除: {}", taskQueue.getTaskId());
logger.info("任务超时,已从 task_queue 中删除: {}", taskQueue.getTaskId());
} catch (Exception deleteException) {
logger.warn("删除队列记录失败: {}", taskQueue.getTaskId(), deleteException);
logger.warn("删除 task_queue 记录失败: {}", taskQueue.getTaskId(), deleteException);
}
} catch (Exception e) {
logger.error("更新任务超时状态失败: {}", taskQueue.getTaskId(), e);
@@ -1713,8 +2009,10 @@ public class TaskQueueService {
/**
* 更新原始任务状态(使用独立事务,快速完成)
*/
@Transactional
private void updateOriginalTaskStatus(TaskQueue taskQueue, String status, String resultUrl, String errorMessage) {
logger.info("更新原始任务状态: taskId={}, taskType={}, status={}, resultUrl={}",
taskQueue.getTaskId(), taskQueue.getTaskType(), status,
resultUrl != null ? (resultUrl.length() > 50 ? resultUrl.substring(0, 50) + "..." : resultUrl) : "null");
try {
if (taskQueue.getTaskType() == TaskQueue.TaskType.TEXT_TO_VIDEO) {
Optional<TextToVideoTask> taskOpt = textToVideoTaskRepository.findByTaskId(taskQueue.getTaskId());
@@ -1741,15 +2039,18 @@ public class TaskQueueService {
Optional<ImageToVideoTask> taskOpt = imageToVideoTaskRepository.findByTaskId(taskQueue.getTaskId());
if (taskOpt.isPresent()) {
ImageToVideoTask task = taskOpt.get();
logger.info("找到ImageToVideoTask: taskId={}, 当前状态={}", taskQueue.getTaskId(), task.getStatus());
if ("COMPLETED".equals(status)) {
task.setResultUrl(resultUrl);
task.updateStatus(ImageToVideoTask.TaskStatus.COMPLETED);
task.updateProgress(100);
imageToVideoTaskRepository.save(task);
logger.info("ImageToVideoTask已更新为COMPLETED: taskId={}", taskQueue.getTaskId());
} else if ("FAILED".equals(status) || "CANCELLED".equals(status)) {
task.updateStatus(ImageToVideoTask.TaskStatus.FAILED);
task.setErrorMessage(errorMessage);
imageToVideoTaskRepository.save(task);
logger.info("ImageToVideoTask已更新为FAILED: taskId={}", taskQueue.getTaskId());
} else if ("PROCESSING".equals(status)) {
// 处理中状态更新resultUrl以显示进度
if (resultUrl != null && !resultUrl.isEmpty()) {
@@ -1757,6 +2058,8 @@ public class TaskQueueService {
imageToVideoTaskRepository.save(task);
}
}
} else {
logger.warn("ImageToVideoTask不存在: taskId={}", taskQueue.getTaskId());
}
} else if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
Optional<StoryboardVideoTask> taskOpt = storyboardVideoTaskRepository.findByTaskId(taskQueue.getTaskId());

View File

@@ -1,6 +1,7 @@
package com.example.demo.service;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import org.slf4j.Logger;
@@ -16,6 +17,7 @@ import com.example.demo.repository.TaskStatusRepository;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
@@ -24,18 +26,162 @@ public class TaskStatusPollingService {
private static final Logger logger = LoggerFactory.getLogger(TaskStatusPollingService.class);
// 任务超时时间(小时)
private static final int TASK_TIMEOUT_HOURS = 1;
@Autowired
private TaskStatusRepository taskStatusRepository;
@Autowired
private ObjectMapper objectMapper;
@Autowired
@org.springframework.context.annotation.Lazy
private TaskQueueService taskQueueService;
@Value("${ai.api.key:ak_5f13ec469e6047d5b8155c3cc91350e2}")
private String apiKey;
@Value("${ai.api.base-url:http://116.62.4.26:8081}")
private String apiBaseUrl;
/**
* 系统启动时恢复处理中的任务
* - 对所有 PROCESSING 状态的任务进行一次状态查询
* - 如果外部API返回已完成则标记成功
* - 如果超时创建时间超过1小时则标记失败
* - 如果未超时且未完成,保持现状等待后续轮询
*/
@PostConstruct
public void recoverProcessingTasksOnStartup() {
logger.info("=== 系统启动:开始恢复处理中的任务 ===");
try {
// 查找所有 PROCESSING 状态的任务
List<TaskStatus> processingTasks = taskStatusRepository.findAllByStatus(TaskStatus.Status.PROCESSING);
if (processingTasks.isEmpty()) {
logger.info("没有需要恢复的处理中任务");
return;
}
logger.info("发现 {} 个处理中的任务,开始恢复检查...", processingTasks.size());
for (TaskStatus task : processingTasks) {
try {
recoverSingleTask(task);
} catch (Exception e) {
logger.error("恢复任务 {} 时发生错误: {}", task.getTaskId(), e.getMessage(), e);
}
}
logger.info("=== 系统启动:任务恢复检查完成 ===");
} catch (Exception e) {
logger.error("系统启动恢复任务时发生错误: {}", e.getMessage(), e);
}
}
/**
* 恢复单个任务
*/
private void recoverSingleTask(TaskStatus task) {
logger.info("恢复检查任务: taskId={}, externalTaskId={}, createdAt={}",
task.getTaskId(), task.getExternalTaskId(), task.getCreatedAt());
// 检查是否有外部任务ID
if (task.getExternalTaskId() == null || task.getExternalTaskId().isEmpty()) {
// 没有外部任务ID检查是否超时
if (isTaskTimeout(task)) {
logger.warn("任务 {} 无外部任务ID且已超时标记为失败", task.getTaskId());
task.markAsFailed("任务超时未获取到外部任务ID");
taskStatusRepository.save(task);
} else {
logger.info("任务 {} 无外部任务ID但未超时保持现状", task.getTaskId());
}
return;
}
// 有外部任务ID查询外部API状态
try {
String url = apiBaseUrl + "/v2/videos/generations/" + task.getExternalTaskId();
HttpResponse<String> response = Unirest.get(url)
.header("Authorization", "Bearer " + apiKey)
.asString();
if (response.getStatus() == 200) {
JsonNode responseJson = objectMapper.readTree(response.getBody());
String status = responseJson.path("status").asText();
String resultUrl = responseJson.path("data").path("output").asText();
logger.info("外部API返回: taskId={}, status={}, resultUrl={}",
task.getTaskId(), status, resultUrl);
if ("SUCCESS".equalsIgnoreCase(status) && resultUrl != null && !resultUrl.isEmpty()) {
// 任务已完成,标记成功并同步更新所有表
task.markAsCompleted(resultUrl);
taskStatusRepository.save(task);
logger.info("任务 {} 恢复为已完成状态resultUrl={}", task.getTaskId(), resultUrl);
// 同步更新业务表、UserWork 等
taskQueueService.handleTaskCompletionByTaskId(task.getTaskId(), resultUrl);
} else if ("FAILED".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) {
// 任务失败
String failReason = responseJson.path("fail_reason").asText("外部API返回失败");
task.markAsFailed(failReason);
taskStatusRepository.save(task);
logger.warn("任务 {} 恢复为失败状态,原因: {}", task.getTaskId(), failReason);
// 同步更新业务表、UserWork 等
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), failReason);
} else {
// 任务仍在处理中,检查是否超时
if (isTaskTimeout(task)) {
logger.warn("任务 {} 已超时,标记为失败", task.getTaskId());
task.markAsFailed("任务超时");
taskStatusRepository.save(task);
// 同步更新业务表、UserWork 等
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), "任务超时");
} else {
logger.info("任务 {} 仍在处理中且未超时,保持现状", task.getTaskId());
}
}
} else {
logger.warn("查询任务状态失败: taskId={}, status={}", task.getTaskId(), response.getStatus());
// 检查是否超时
if (isTaskTimeout(task)) {
task.markAsFailed("任务超时:无法获取外部状态");
taskStatusRepository.save(task);
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), "任务超时:无法获取外部状态");
}
}
} catch (Exception e) {
logger.error("查询外部API失败: taskId={}, error={}", task.getTaskId(), e.getMessage());
// 检查是否超时
if (isTaskTimeout(task)) {
task.markAsFailed("任务超时查询外部API异常");
taskStatusRepository.save(task);
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), "任务超时查询外部API异常");
}
}
}
/**
* 检查任务是否超时创建时间超过1小时
*/
private boolean isTaskTimeout(TaskStatus task) {
if (task.getCreatedAt() == null) {
return false;
}
long hoursSinceCreation = ChronoUnit.HOURS.between(task.getCreatedAt(), LocalDateTime.now());
return hoursSinceCreation >= TASK_TIMEOUT_HOURS;
}
/**
* 根据状态查找任务列表
*/
public List<TaskStatus> findByStatus(TaskStatus.Status status) {
return taskStatusRepository.findByStatus(status, org.springframework.data.domain.Pageable.unpaged()).getContent();
}
/**
* (Scheduling disabled) 原先每2分钟执行一次轮询查询任务状态。
* 注意:调度已集中到 `TaskQueueScheduler.checkTaskStatuses()`,以避免重复并发查询。
@@ -46,7 +192,7 @@ public class TaskStatusPollingService {
logger.info("=== 开始执行任务状态轮询查询 (每2分钟) ===");
try {
// 查找需要轮询的任务(状态为PROCESSING且创建时间超过2分钟
// 查找需要轮询的任务(上次轮询时间超过2分钟
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(2);
// 先做一次计数,避免在无任务时加载实体列表
long needCount = taskStatusRepository.countTasksNeedingPolling(cutoffTime);
@@ -54,7 +200,11 @@ public class TaskStatusPollingService {
logger.info("需要轮询查询的任务数量: {}", needCount);
if (needCount == 0) {
logger.debug("当前没有需要轮询的任务count=0");
// 检查是否有PROCESSING任务但不满足轮询条件
long processingCount = taskStatusRepository.countByStatus(TaskStatus.Status.PROCESSING);
if (processingCount > 0) {
logger.debug("有 {} 个PROCESSING任务但均在冷却期内2分钟内已轮询过", processingCount);
}
return;
}
@@ -90,14 +240,15 @@ public class TaskStatusPollingService {
logger.info("轮询任务状态: taskId={}, externalTaskId={}", task.getTaskId(), task.getExternalTaskId());
try {
// 调用外部API查询状态长时间运行不在事务中
HttpResponse<String> response = Unirest.post(apiBaseUrl + "/v1/videos")
// 使用正确的 API 端点GET /v2/videos/generations/{task_id}
String url = apiBaseUrl + "/v2/videos/generations/" + task.getExternalTaskId();
HttpResponse<String> response = Unirest.get(url)
.header("Authorization", "Bearer " + apiKey)
.field("task_id", task.getExternalTaskId())
.asString();
if (response.getStatus() == 200) {
JsonNode responseJson = objectMapper.readTree(response.getBody());
logger.info("轮询任务状态成功: taskId={}, response={}", task.getTaskId(), response.getBody());
// 更新任务状态(使用单独的事务方法)
updateTaskStatusWithTransaction(task, responseJson);
} else {
@@ -137,9 +288,11 @@ public class TaskStatusPollingService {
private void updateTaskStatus(TaskStatus task, JsonNode responseJson) {
try {
String status = responseJson.path("status").asText();
int progress = responseJson.path("progress").asInt(0);
String resultUrl = responseJson.path("result_url").asText();
String errorMessage = responseJson.path("error_message").asText();
String progressStr = responseJson.path("progress").asText("0%");
int progress = parseProgress(progressStr);
// API 返回的视频URL在 data.output 中
String resultUrl = responseJson.path("data").path("output").asText();
String errorMessage = responseJson.path("fail_reason").asText();
task.incrementPollCount();
task.setProgress(progress);
@@ -147,18 +300,30 @@ public class TaskStatusPollingService {
switch (status.toLowerCase()) {
case "completed":
case "success":
task.markAsCompleted(resultUrl);
logger.info("任务完成: taskId={}, resultUrl={}", task.getTaskId(), resultUrl);
break;
if (resultUrl != null && !resultUrl.isEmpty()) {
task.markAsCompleted(resultUrl);
logger.info("任务完成: taskId={}, resultUrl={}", task.getTaskId(), resultUrl);
taskStatusRepository.save(task);
// 同步更新业务表、UserWork 等
taskQueueService.handleTaskCompletionByTaskId(task.getTaskId(), resultUrl);
} else {
logger.warn("任务状态为成功但 resultUrl 为空,保持 PROCESSING: taskId={}", task.getTaskId());
taskStatusRepository.save(task);
}
return; // 已保存,直接返回
case "failed":
case "error":
task.markAsFailed(errorMessage);
logger.warn("任务失败: taskId={}, error={}", task.getTaskId(), errorMessage);
break;
taskStatusRepository.save(task);
// 同步更新业务表、UserWork 等
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), errorMessage);
return; // 已保存,直接返回
case "processing":
case "in_progress":
case "not_start":
task.setStatus(TaskStatus.Status.PROCESSING);
logger.info("任务处理中: taskId={}, progress={}%", task.getTaskId(), progress);
break;
@@ -249,5 +414,21 @@ public class TaskStatusPollingService {
taskStatus.setUpdatedAt(LocalDateTime.now());
return taskStatusRepository.save(taskStatus);
}
/**
* 解析进度字符串(如 "100%")为整数
*/
private int parseProgress(String progressStr) {
if (progressStr == null || progressStr.isEmpty()) {
return 0;
}
try {
// 移除百分号并解析
String numStr = progressStr.replace("%", "").trim();
return Integer.parseInt(numStr);
} catch (NumberFormatException e) {
return 0;
}
}
}

View File

@@ -145,7 +145,11 @@ public class JwtUtils {
*/
public String extractTokenFromHeader(String authHeader) {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
String token = authHeader.substring(7);
// 检查 token 是否有效(不为空、不是 "null" 字符串、包含两个点)
if (token != null && !token.isEmpty() && !token.equals("null") && token.contains(".")) {
return token;
}
}
return null;
}

View File

@@ -43,15 +43,15 @@ springdoc.swagger-ui.default-model-expand-depth=1
# 腾讯云COS对象存储配置
# 是否启用COS设置为true后需要配置下面的参数
tencent.cos.enabled=false
tencent.cos.enabled=true
# 腾讯云SecretId从控制台获取https://console.cloud.tencent.com/cam/capi
tencent.cos.secret-id=
tencent.cos.secret-id=AKID2xjaRPSOSYk2fIxV7nQuDi9NOIzTjlbJ
# 腾讯云SecretKey
tencent.cos.secret-key=
tencent.cos.secret-key=Xrxywju0wfAf3QiqlT2ZvGYgeS6WjnjT
# COS区域例如ap-guangzhou、ap-shanghai、ap-beijing等
tencent.cos.region=ap-guangzhou
tencent.cos.region=ap-nanjing
# COS存储桶名称例如my-bucket-1234567890
tencent.cos.bucket-name=
tencent.cos.bucket-name=test-1323844400
# ============================================
# PayPal支付配置