feat: 图片压缩后上传COS + 修复订单LazyInitializationException + 添加调试日志
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 转换订单数据,添加支付方式信息
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ public interface TaskStatusRepository extends JpaRepository<TaskStatus, Long> {
|
||||
*/
|
||||
long countByStatus(TaskStatus.Status status);
|
||||
|
||||
/**
|
||||
* 根据状态查找所有任务(不分页)
|
||||
*/
|
||||
List<TaskStatus> findAllByStatus(TaskStatus.Status status);
|
||||
|
||||
/**
|
||||
* 统计用户指定状态的任务数量
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回本地URL(COS未启用或上传失败时)
|
||||
return localUrlPath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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支付配置
|
||||
|
||||
Reference in New Issue
Block a user