优化: Safari下载兼容、禁用生产Swagger、前端构建优化移除console、更新COS配置
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
/**
|
||||
* 单独的密码编码器配置类
|
||||
* 避免与 SecurityConfig 产生循环依赖
|
||||
*/
|
||||
@Configuration
|
||||
public class PasswordEncoderConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -20,79 +20,74 @@ import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import com.example.demo.security.JwtAuthenticationFilter;
|
||||
import com.example.demo.security.PlainTextPasswordEncoder;
|
||||
import com.example.demo.service.RedisTokenService;
|
||||
import com.example.demo.service.UserService;
|
||||
import com.example.demo.util.JwtUtils;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder();
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
|
||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtils jwtUtils, UserService userService,
|
||||
RedisTokenService redisTokenService) throws Exception {
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 允许基于表单的账号密码登录后保持会话
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Swagger/OpenAPI 路径 - 必须放在最前面,完全公开访问
|
||||
.requestMatchers(
|
||||
"/swagger-ui/**",
|
||||
"/swagger-ui.html",
|
||||
"/v3/api-docs/**",
|
||||
"/v3/api-docs",
|
||||
"/api-docs/**",
|
||||
"/swagger-resources/**",
|
||||
"/webjars/**",
|
||||
"/configuration/ui",
|
||||
"/configuration/security",
|
||||
"/swagger-config",
|
||||
"/api/swagger-config"
|
||||
).permitAll()
|
||||
// 公共路径
|
||||
.requestMatchers(
|
||||
"/login",
|
||||
"/register",
|
||||
"/api/public/**",
|
||||
"/api/auth/**",
|
||||
"/api/verification/**",
|
||||
"/api/email/**",
|
||||
"/api/tencent/**",
|
||||
"/api/test/**",
|
||||
"/api/polling/**",
|
||||
"/api/diagnostic/**",
|
||||
"/api/polling-diagnostic/**",
|
||||
"/api/monitor/**",
|
||||
"/api/health/**",
|
||||
"/css/**",
|
||||
"/js/**",
|
||||
"/h2-console/**"
|
||||
).permitAll()
|
||||
.requestMatchers("/api/orders/stats").permitAll() // 统计接口允许匿名访问
|
||||
.requestMatchers("/api/orders/**").authenticated() // 订单接口需要认证
|
||||
.requestMatchers("/api/payments/alipay/notify", "/api/payments/alipay/return").permitAll() // 支付宝回调接口允许匿名访问(外部调用)
|
||||
.requestMatchers("/api/payments/**").authenticated() // 其他支付接口需要认证
|
||||
.requestMatchers("/api/image-to-video/**").authenticated() // 图生视频接口需要认证
|
||||
.requestMatchers("/api/text-to-video/**").authenticated() // 文生视频接口需要认证
|
||||
.requestMatchers("/api/storyboard-video/**").authenticated() // 分镜视频接口需要认证
|
||||
.requestMatchers("/api/dashboard/**").hasAnyRole("ADMIN", "SUPER_ADMIN") // 仪表盘API需要管理员权限
|
||||
.requestMatchers("/api/admin/**").hasAnyRole("ADMIN", "SUPER_ADMIN") // 管理员API需要管理员权限
|
||||
.requestMatchers("/settings", "/settings/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
|
||||
.requestMatchers("/users/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
|
||||
.anyRequest().permitAll()
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Swagger/OpenAPI 路径
|
||||
.requestMatchers(
|
||||
"/swagger-ui/**",
|
||||
"/swagger-ui.html",
|
||||
"/v3/api-docs/**",
|
||||
"/v3/api-docs",
|
||||
"/api-docs/**",
|
||||
"/swagger-resources/**",
|
||||
"/webjars/**",
|
||||
"/configuration/ui",
|
||||
"/configuration/security",
|
||||
"/swagger-config",
|
||||
"/api/swagger-config"
|
||||
).permitAll()
|
||||
// 公共路径
|
||||
.requestMatchers(
|
||||
"/login",
|
||||
"/register",
|
||||
"/api/public/**",
|
||||
"/api/auth/**",
|
||||
"/api/verification/**",
|
||||
"/api/email/**",
|
||||
"/api/tencent/**",
|
||||
"/api/test/**",
|
||||
"/api/polling/**",
|
||||
"/api/diagnostic/**",
|
||||
"/api/polling-diagnostic/**",
|
||||
"/api/monitor/**",
|
||||
"/api/health/**",
|
||||
"/css/**",
|
||||
"/js/**",
|
||||
"/h2-console/**"
|
||||
).permitAll()
|
||||
.requestMatchers("/api/orders/stats").permitAll()
|
||||
.requestMatchers("/api/orders/**").authenticated()
|
||||
.requestMatchers("/api/payments/alipay/notify", "/api/payments/alipay/return").permitAll()
|
||||
.requestMatchers("/api/payments/**").authenticated()
|
||||
.requestMatchers("/api/image-to-video/**").authenticated()
|
||||
.requestMatchers("/api/text-to-video/**").authenticated()
|
||||
.requestMatchers("/api/storyboard-video/**").authenticated()
|
||||
.requestMatchers("/api/dashboard/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
|
||||
.requestMatchers("/api/admin/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
|
||||
.requestMatchers("/settings", "/settings/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
|
||||
.requestMatchers("/users/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login")
|
||||
// 使用email作为表单登录的用户名参数(账号即邮箱)
|
||||
.usernameParameter("email")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/", true)
|
||||
@@ -101,15 +96,13 @@ public class SecurityConfig {
|
||||
.logout(Customizer.withDefaults());
|
||||
|
||||
// 添加JWT过滤器
|
||||
http.addFilterBefore(jwtAuthenticationFilter(jwtUtils, userService, redisTokenService), UsernamePasswordAuthenticationFilter.class);
|
||||
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
// H2 控制台需要以下设置
|
||||
http.headers(headers -> headers.frameOptions(frame -> frame.disable()));
|
||||
return http.build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
@@ -119,75 +112,32 @@ public class SecurityConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config, DaoAuthenticationProvider authenticationProvider) throws Exception {
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// 面向海外用户,使用最宽松的CORS配置
|
||||
// 使用allowedOriginPatterns支持通配符(当allowCredentials=true时必须使用patterns而不是origins)
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(
|
||||
// 允许所有HTTP和HTTPS来源(任意域名、任意IP、任意端口)
|
||||
"http://*:*",
|
||||
"https://*:*",
|
||||
"http://*",
|
||||
"https://*",
|
||||
"http://*.*",
|
||||
"https://*.*",
|
||||
"http://*.*.*",
|
||||
"https://*.*.*",
|
||||
|
||||
// 本地开发环境
|
||||
"http://localhost:*",
|
||||
"http://127.0.0.1:*",
|
||||
"http://0.0.0.0:*",
|
||||
|
||||
// 常见开发工具和隧道服务
|
||||
"https://*.ngrok.io",
|
||||
"https://*.ngrok-free.app",
|
||||
"https://*.cloudflare.com",
|
||||
"https://*.vercel.app",
|
||||
"https://*.netlify.app",
|
||||
"https://*.herokuapp.com",
|
||||
"https://*.github.io",
|
||||
"https://*.gitlab.io",
|
||||
|
||||
// 生产域名(如果有)
|
||||
"https://vionow.com",
|
||||
"https://www.vionow.com",
|
||||
"http://vionow.com",
|
||||
"http://www.vionow.com",
|
||||
"https://*.vionow.com"
|
||||
"http://*:*", "https://*:*", "http://*", "https://*",
|
||||
"http://*.*", "https://*.*", "http://*.*.*", "https://*.*.*",
|
||||
"http://localhost:*", "http://127.0.0.1:*", "http://0.0.0.0:*",
|
||||
"https://*.ngrok.io", "https://*.ngrok-free.app",
|
||||
"https://*.cloudflare.com", "https://*.vercel.app",
|
||||
"https://*.netlify.app", "https://*.herokuapp.com",
|
||||
"https://*.github.io", "https://*.gitlab.io",
|
||||
"https://vionow.com", "https://www.vionow.com",
|
||||
"http://vionow.com", "http://www.vionow.com", "https://*.vionow.com"
|
||||
));
|
||||
|
||||
// 允许所有HTTP方法
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD", "TRACE", "CONNECT"));
|
||||
|
||||
// 允许所有请求头
|
||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||
|
||||
// 允许携带凭证(cookies, authorization headers等)
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
// 暴露所有响应头给客户端
|
||||
configuration.setExposedHeaders(Arrays.asList("*"));
|
||||
|
||||
// 预检请求缓存时间(1小时,减少预检请求频率)
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtUtils jwtUtils, UserService userService,
|
||||
com.example.demo.service.RedisTokenService redisTokenService) {
|
||||
return new JwtAuthenticationFilter(jwtUtils, userService, redisTokenService);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public class StoryboardVideoApiController {
|
||||
String aspectRatio = (String) request.getOrDefault("aspectRatio", "16:9");
|
||||
Boolean hdMode = (Boolean) request.getOrDefault("hdMode", false);
|
||||
String imageUrl = (String) request.get("imageUrl");
|
||||
String imageModel = (String) request.getOrDefault("imageModel", "nano-banana");
|
||||
String imageModel = (String) request.getOrDefault("imageModel", "nano-banana-2");
|
||||
|
||||
// 提取用户上传的多张图片(新增)
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -129,6 +129,7 @@ public class StoryboardVideoApiController {
|
||||
taskData.put("status", task.getStatus());
|
||||
taskData.put("progress", task.getProgress());
|
||||
taskData.put("resultUrl", task.getResultUrl());
|
||||
taskData.put("videoUrls", task.getVideoUrls()); // 视频URL(JSON数组)
|
||||
taskData.put("imageUrl", task.getImageUrl()); // 参考图片(旧字段)
|
||||
taskData.put("uploadedImages", task.getUploadedImages()); // 用户上传的参考图(JSON数组)
|
||||
taskData.put("prompt", task.getPrompt());
|
||||
|
||||
@@ -115,6 +115,12 @@ public class UserWorkApiController {
|
||||
Page<UserWork> works;
|
||||
if (includeProcessing) {
|
||||
works = userWorkService.getAllUserWorks(username, page, size);
|
||||
// 调试日志:检查是否有 PROCESSING 状态的作品
|
||||
long processingCount = works.getContent().stream()
|
||||
.filter(w -> w.getStatus() == UserWork.WorkStatus.PROCESSING || w.getStatus() == UserWork.WorkStatus.PENDING)
|
||||
.count();
|
||||
logger.info("获取作品列表: username={}, total={}, processing/pending={}",
|
||||
username, works.getTotalElements(), processingCount);
|
||||
} else {
|
||||
works = userWorkService.getUserWorks(username, page, size);
|
||||
}
|
||||
|
||||
@@ -44,10 +44,13 @@ public class StoryboardVideoTask {
|
||||
private Integer duration; // 视频时长(秒):5, 10, 15
|
||||
|
||||
@Column(name = "image_model", length = 50)
|
||||
private String imageModel = "nano-banana"; // 图像生成模型:nano-banana, nano-banana2
|
||||
private String imageModel = "nano-banana-2"; // 图像生成模型:nano-banana, nano-banana-2
|
||||
|
||||
@Column(name = "uploaded_images", columnDefinition = "LONGTEXT")
|
||||
private String uploadedImages; // 用户上传的多张参考图片(JSON数组,最多3张Base64格式)
|
||||
private String uploadedImages; // 用户上传的多张参考图片(JSON数组,最多3张Base64格式)- 生成分镜图时使用
|
||||
|
||||
@Column(name = "video_reference_images", columnDefinition = "LONGTEXT")
|
||||
private String videoReferenceImages; // 视频阶段用户上传的参考图片(JSON数组)- 生成视频时使用
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 20)
|
||||
@@ -188,6 +191,8 @@ public class StoryboardVideoTask {
|
||||
public void setImageModel(String imageModel) { this.imageModel = imageModel; }
|
||||
public String getUploadedImages() { return uploadedImages; }
|
||||
public void setUploadedImages(String uploadedImages) { this.uploadedImages = uploadedImages; }
|
||||
public String getVideoReferenceImages() { return videoReferenceImages; }
|
||||
public void setVideoReferenceImages(String videoReferenceImages) { this.videoReferenceImages = videoReferenceImages; }
|
||||
public TaskStatus getStatus() { return status; }
|
||||
public void setStatus(TaskStatus status) { this.status = status; }
|
||||
public int getProgress() { return progress; }
|
||||
|
||||
@@ -91,6 +91,9 @@ public class UserWork {
|
||||
@Column(name = "tags", length = 500)
|
||||
private String tags; // 标签,用逗号分隔
|
||||
|
||||
@Column(name = "uploaded_images", columnDefinition = "LONGTEXT")
|
||||
private String uploadedImages; // 用户上传的参考图片(JSON数组),用于"做同款"功能恢复
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@@ -124,6 +127,7 @@ public class UserWork {
|
||||
* 作品状态枚举
|
||||
*/
|
||||
public enum WorkStatus {
|
||||
PENDING("排队中"),
|
||||
PROCESSING("处理中"),
|
||||
COMPLETED("已完成"),
|
||||
FAILED("失败"),
|
||||
@@ -378,6 +382,14 @@ public class UserWork {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public String getUploadedImages() {
|
||||
return uploadedImages;
|
||||
}
|
||||
|
||||
public void setUploadedImages(String uploadedImages) {
|
||||
this.uploadedImages = uploadedImages;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -79,6 +80,7 @@ public interface CompletedTaskArchiveRepository extends JpaRepository<CompletedT
|
||||
/**
|
||||
* 删除超过指定天数的归档任务
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM CompletedTaskArchive c WHERE c.archivedAt < :cutoffDate")
|
||||
int deleteOlderThan(@Param("cutoffDate") LocalDateTime cutoffDate);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -79,6 +80,7 @@ public interface FailedTaskCleanupLogRepository extends JpaRepository<FailedTask
|
||||
/**
|
||||
* 删除超过指定天数的清理日志
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM FailedTaskCleanupLog f WHERE f.cleanedAt < :cutoffDate")
|
||||
int deleteOlderThan(@Param("cutoffDate") LocalDateTime cutoffDate);
|
||||
}
|
||||
|
||||
@@ -86,18 +86,18 @@ public class TaskQueueScheduler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("[轮询调度] TaskQueue待检查任务: {}, TaskStatus处理中任务: {}",
|
||||
logger.debug("[轮询调度] TaskQueue待检查任务: {}, TaskStatus处理中任务: {}",
|
||||
hasQueueTasks, processingStatusCount);
|
||||
|
||||
// 队列中有任务:检查队列内任务状态
|
||||
if (hasQueueTasks) {
|
||||
logger.info("[轮询调度] 开始检查TaskQueue任务状态");
|
||||
logger.debug("[轮询调度] 开始检查TaskQueue任务状态");
|
||||
taskQueueService.checkTaskStatuses();
|
||||
}
|
||||
|
||||
// TaskStatus表中有处理中任务:调用轮询服务
|
||||
if (processingStatusCount > 0) {
|
||||
logger.info("[轮询调度] 开始轮询TaskStatus任务");
|
||||
logger.debug("[轮询调度] 开始轮询TaskStatus任务");
|
||||
taskStatusPollingService.pollTaskStatuses();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -16,8 +16,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.service.RedisTokenService;
|
||||
import com.example.demo.service.UserService;
|
||||
import com.example.demo.repository.UserRepository;
|
||||
import com.example.demo.util.JwtUtils;
|
||||
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
@@ -26,19 +25,21 @@ import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
* 注意:直接使用 UserRepository 而不是 UserService,避免循环依赖
|
||||
*/
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
||||
|
||||
private final JwtUtils jwtUtils;
|
||||
private final UserService userService;
|
||||
private final RedisTokenService redisTokenService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserService userService, RedisTokenService redisTokenService) {
|
||||
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserRepository userRepository) {
|
||||
this.jwtUtils = jwtUtils;
|
||||
this.userService = userService;
|
||||
this.redisTokenService = redisTokenService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,7 +72,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是公开路径,直接放行,不进行JWT验证
|
||||
// 如果是公开路径,直接放行
|
||||
if (isPublicPath) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
@@ -79,16 +80,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
try {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
|
||||
String token = jwtUtils.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (token != null && !token.equals("null") && !token.trim().isEmpty()) {
|
||||
String username = jwtUtils.getUsernameFromToken(token);
|
||||
|
||||
if (username != null && jwtUtils.validateToken(token, username)) {
|
||||
// Redis 验证已降级:isTokenValid 总是返回 true
|
||||
// 主要依赖 JWT 本身的有效性验证
|
||||
User user = userService.findByUsernameOrNull(username);
|
||||
// 直接使用 Repository 查询用户
|
||||
User user = userRepository.findByUsername(username).orElse(null);
|
||||
|
||||
if (user != null) {
|
||||
// 检查用户是否被封禁
|
||||
@@ -101,12 +100,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建用户权限列表
|
||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||
authorities.add(new SimpleGrantedAuthority(user.getRole()));
|
||||
|
||||
// 使用用户名字符串作为Principal,而不是User对象
|
||||
// 这样 authentication.getName() 会返回用户名字符串
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(user.getUsername(), null, authorities);
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
@@ -117,11 +113,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
}
|
||||
} catch (ExpiredJwtException e) {
|
||||
// Token过期,返回401让前端跳转登录页
|
||||
logger.warn("JWT已过期: 过期时间={}, 当前时间={}, token前30字符={}",
|
||||
e.getClaims().getExpiration(),
|
||||
new java.util.Date(),
|
||||
e.getClaims().getSubject());
|
||||
logger.warn("JWT已过期: {}", e.getMessage());
|
||||
SecurityContextHolder.clearContext();
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
@@ -129,7 +121,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
logger.warn("JWT认证失败: {}", e.getMessage());
|
||||
// 清除可能存在的认证信息
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.alipay.api.domain.AlipayTradePagePayModel;
|
||||
import com.alipay.api.domain.AlipayTradePrecreateModel;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.example.demo.model.Payment;
|
||||
@@ -65,7 +66,7 @@ public class AlipayService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付宝支付订单并生成二维码
|
||||
* 创建支付宝支付订单(电脑网站支付)
|
||||
*/
|
||||
public Map<String, Object> createPayment(Payment payment) {
|
||||
try {
|
||||
@@ -84,8 +85,8 @@ public class AlipayService {
|
||||
paymentRepository.save(payment);
|
||||
logger.info("支付记录已保存,ID:{}", payment.getId());
|
||||
|
||||
// 调用真实的支付宝API
|
||||
return callRealAlipayAPI(payment);
|
||||
// 调用电脑网站支付API
|
||||
return callPagePayAPI(payment);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("创建支付订单时发生异常,订单号:{},错误:{}", payment.getOrderId(), e.getMessage(), e);
|
||||
@@ -108,6 +109,60 @@ public class AlipayService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用电脑网页支付API(alipay.trade.page.pay)
|
||||
* 返回支付页面的HTML表单,前端直接渲染即可跳转到支付宝
|
||||
*/
|
||||
private Map<String, Object> callPagePayAPI(Payment payment) throws Exception {
|
||||
logger.info("=== 使用IJPay调用支付宝电脑网页支付API ===");
|
||||
logger.info("网关地址: {}", gatewayUrl);
|
||||
logger.info("应用ID: {}", appId);
|
||||
logger.info("通知URL: {}", notifyUrl);
|
||||
logger.info("返回URL: {}", returnUrl);
|
||||
|
||||
// 在调用前确保配置已设置
|
||||
ensureAliPayConfigSet();
|
||||
|
||||
// 设置业务参数
|
||||
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
||||
model.setOutTradeNo(payment.getOrderId());
|
||||
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 电脑网站支付固定值
|
||||
|
||||
// 如果是美元,按汇率转换为人民币(支付宝只支持CNY)
|
||||
java.math.BigDecimal amount = payment.getAmount();
|
||||
String currency = payment.getCurrency();
|
||||
if ("USD".equalsIgnoreCase(currency)) {
|
||||
java.math.BigDecimal exchangeRate = new java.math.BigDecimal("7.2");
|
||||
amount = amount.multiply(exchangeRate).setScale(2, java.math.RoundingMode.HALF_UP);
|
||||
logger.info("货币转换: {} USD -> {} CNY (汇率: 7.2)", payment.getAmount(), amount);
|
||||
}
|
||||
model.setTotalAmount(amount.toString());
|
||||
model.setSubject(payment.getDescription() != null ? payment.getDescription() : "AIGC会员订阅");
|
||||
model.setBody(payment.getDescription() != null ? payment.getDescription() : "AIGC平台会员订阅服务");
|
||||
model.setTimeoutExpress("30m"); // 订单超时时间
|
||||
|
||||
logger.info("调用支付宝电脑网页支付API,订单号:{},金额:{},商品名称:{}",
|
||||
model.getOutTradeNo(), model.getTotalAmount(), model.getSubject());
|
||||
|
||||
// 使用IJPay调用电脑网页支付API,返回HTML表单
|
||||
String form = AliPayApi.tradePage(model, notifyUrl, returnUrl);
|
||||
|
||||
if (form == null || form.isEmpty()) {
|
||||
logger.error("支付宝电脑网页支付API返回为空");
|
||||
throw new RuntimeException("支付宝支付页面生成失败");
|
||||
}
|
||||
|
||||
logger.info("支付宝电脑网页支付表单生成成功,订单号:{}", payment.getOrderId());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("payForm", form); // HTML表单,前端直接渲染
|
||||
result.put("outTradeNo", payment.getOrderId());
|
||||
result.put("success", true);
|
||||
result.put("payType", "PAGE_PAY"); // 标识支付类型
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保AliPayApiConfigKit中已设置配置
|
||||
* 如果未设置,从AliPayApiConfigHolder获取并设置
|
||||
|
||||
@@ -173,9 +173,10 @@ public class ImageToVideoService {
|
||||
if (page < 0) {
|
||||
page = 0;
|
||||
}
|
||||
if (size <= 0 || size > 100) {
|
||||
size = 10; // 默认每页10条,最大100条
|
||||
if (size <= 0) {
|
||||
size = 10; // 默认每页10条
|
||||
}
|
||||
// 移除size上限限制,允许获取全部历史记录
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size);
|
||||
Page<ImageToVideoTask> taskPage = taskRepository.findByUsernameOrderByCreatedAtDesc(username, pageable);
|
||||
@@ -345,10 +346,11 @@ public class ImageToVideoService {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
} catch (Exception saveException) {
|
||||
logger.error("保存失败状态时出错: {}", task.getTaskId(), saveException);
|
||||
} finally {
|
||||
// 确保无论如何都返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +377,8 @@ public class ImageToVideoService {
|
||||
ImageToVideoTask currentTask = taskRepository.findByTaskId(task.getTaskId()).orElse(null);
|
||||
if (currentTask != null && currentTask.getStatus() == ImageToVideoTask.TaskStatus.CANCELLED) {
|
||||
logger.info("任务 {} 已被取消,停止轮询", task.getTaskId());
|
||||
// 任务取消时返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -496,6 +500,8 @@ public class ImageToVideoService {
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("轮询任务状态被中断: {}", task.getTaskId(), e);
|
||||
// 线程中断时返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
logger.error("轮询任务状态异常: {}", task.getTaskId(), e);
|
||||
@@ -503,6 +509,8 @@ public class ImageToVideoService {
|
||||
if (e.getCause() != null) {
|
||||
logger.error("异常原因: {}", e.getCause().getMessage());
|
||||
}
|
||||
// 轮询异常时返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -853,12 +853,12 @@ public class RealAIService {
|
||||
* 参考Comfly项目的Comfly_nano_banana_edit节点实现
|
||||
*/
|
||||
public Map<String, Object> submitTextToImageTask(String prompt, String aspectRatio, int numImages, boolean hdMode) {
|
||||
return submitTextToImageTask(prompt, aspectRatio, numImages, hdMode, "nano-banana");
|
||||
return submitTextToImageTask(prompt, aspectRatio, numImages, hdMode, "nano-banana-2");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交文生图任务(使用指定的模型)
|
||||
* @param imageModel 图像生成模型:nano-banana 或 nano-banana2
|
||||
* @param imageModel 图像生成模型:nano-banana 或 nano-banana-2
|
||||
*/
|
||||
public Map<String, Object> submitTextToImageTask(String prompt, String aspectRatio, int numImages, boolean hdMode, String imageModel) {
|
||||
try {
|
||||
@@ -885,8 +885,8 @@ public class RealAIService {
|
||||
// 参考Comfly_nano_banana_edit节点:使用 /v1/images/generations 端点
|
||||
String url = getEffectiveImageApiBaseUrl() + "/v1/images/generations";
|
||||
|
||||
// 使用指定的模型,默认为 nano-banana
|
||||
String model = (imageModel != null && !imageModel.isEmpty()) ? imageModel : "nano-banana";
|
||||
// 强制使用 nano-banana-2 模型,忽略用户选择
|
||||
String model = "nano-banana-2";
|
||||
|
||||
// 构建请求体,参考Comfly_nano_banana_edit节点的参数设置
|
||||
// 注意:banana模型不需要n参数,每次只生成1张图片
|
||||
@@ -978,6 +978,14 @@ public class RealAIService {
|
||||
* @return API响应
|
||||
*/
|
||||
public Map<String, Object> submitImageToImageTask(String prompt, String imageBase64, String aspectRatio, boolean hdMode) {
|
||||
return submitImageToImageTask(prompt, imageBase64, aspectRatio, hdMode, "nano-banana-2");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交图生图任务(使用指定的模型)
|
||||
* @param imageModel 图像生成模型参数(忽略,强制使用 nano-banana-2)
|
||||
*/
|
||||
public Map<String, Object> submitImageToImageTask(String prompt, String imageBase64, String aspectRatio, boolean hdMode, String imageModel) {
|
||||
// 参数验证:图生图必须有参考图片
|
||||
if (imageBase64 == null || imageBase64.isEmpty()) {
|
||||
logger.error("图生图任务失败:缺少参考图片");
|
||||
@@ -985,15 +993,15 @@ public class RealAIService {
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info("提交图生图任务(banana模型): prompt={}, aspectRatio={}, hdMode={}, imageBase64长度={}",
|
||||
prompt, aspectRatio, hdMode, imageBase64.length());
|
||||
// 强制使用 nano-banana-2 模型,忽略用户选择
|
||||
String model = "nano-banana-2";
|
||||
|
||||
logger.info("提交图生图任务: prompt={}, aspectRatio={}, hdMode={}, imageModel={}, imageBase64长度={}",
|
||||
prompt, aspectRatio, hdMode, model, imageBase64.length());
|
||||
|
||||
// 图生图使用 /v1/images/edits 端点(参考 Comfly_nano_banana_edit)
|
||||
String url = getEffectiveImageApiBaseUrl() + "/v1/images/edits";
|
||||
|
||||
// 模型选择
|
||||
String model = "nano-banana";
|
||||
|
||||
// 处理图片数据:如果是 URL,先下载转换为 base64
|
||||
String base64Data = imageBase64;
|
||||
if (imageBase64.startsWith("http://") || imageBase64.startsWith("https://")) {
|
||||
|
||||
@@ -133,14 +133,46 @@ public class StoryboardVideoService {
|
||||
task.setImageModel(imageModel);
|
||||
}
|
||||
|
||||
// 保存用户上传的多张图片(JSON数组)
|
||||
// 保存用户上传的多张图片(上传到COS后存储URL)
|
||||
if (uploadedImages != null && !uploadedImages.isEmpty()) {
|
||||
try {
|
||||
String uploadedImagesJson = objectMapper.writeValueAsString(uploadedImages);
|
||||
task.setUploadedImages(uploadedImagesJson);
|
||||
logger.info("保存用户上传的图片: taskId={}, 数量={}", taskId, uploadedImages.size());
|
||||
List<String> processedImages = new ArrayList<>();
|
||||
int imageIndex = 0;
|
||||
for (String img : uploadedImages) {
|
||||
if (img == null || img.isEmpty() || "null".equals(img)) {
|
||||
continue;
|
||||
}
|
||||
// 如果是Base64图片且COS启用,上传到COS
|
||||
if (img.startsWith("data:image") && cosService.isEnabled()) {
|
||||
try {
|
||||
String cosUrl = cosService.uploadBase64Image(img, "uploaded_" + taskId + "_" + imageIndex + ".png");
|
||||
if (cosUrl != null && !cosUrl.isEmpty()) {
|
||||
processedImages.add(cosUrl);
|
||||
logger.info("用户上传图片{}上传COS成功: taskId={}", imageIndex, taskId);
|
||||
} else {
|
||||
// COS上传失败,保留原始Base64
|
||||
processedImages.add(img);
|
||||
logger.warn("用户上传图片{}上传COS失败,保留Base64: taskId={}", imageIndex, taskId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 异常时保留原始Base64
|
||||
processedImages.add(img);
|
||||
logger.error("用户上传图片{}上传COS异常: taskId={}", imageIndex, taskId, e);
|
||||
}
|
||||
} else {
|
||||
// 非Base64或COS未启用,直接保留
|
||||
processedImages.add(img);
|
||||
}
|
||||
imageIndex++;
|
||||
}
|
||||
|
||||
if (!processedImages.isEmpty()) {
|
||||
String uploadedImagesJson = objectMapper.writeValueAsString(processedImages);
|
||||
task.setUploadedImages(uploadedImagesJson);
|
||||
logger.info("保存用户上传的图片: taskId={}, 数量={}", taskId, processedImages.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("序列化上传图片失败: taskId={}", taskId, e);
|
||||
logger.error("处理上传图片失败: taskId={}", taskId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +422,8 @@ public class StoryboardVideoService {
|
||||
finalPrompt,
|
||||
refImage,
|
||||
generationAspectRatio,
|
||||
hdMode
|
||||
hdMode,
|
||||
imageModel // 使用用户选择的图像生成模型
|
||||
);
|
||||
} else {
|
||||
// 无参考图片,使用文生图API
|
||||
@@ -673,6 +706,16 @@ public class StoryboardVideoService {
|
||||
// 不抛出异常,避免影响主流程
|
||||
}
|
||||
|
||||
// 更新分镜视频 UserWork 状态为 COMPLETED(分镜图阶段完成)
|
||||
// 这样主页不会显示"生成中",用户点击"生成视频"后会重新创建 PROCESSING 状态的记录
|
||||
try {
|
||||
userWorkService.updateStoryboardVideoWorkToImageCompleted(taskId, imageUrlForDb);
|
||||
logger.info("分镜视频作品状态已更新为分镜图完成: taskId={}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("更新分镜视频作品状态失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
// 不抛出异常,避免影响主流程
|
||||
}
|
||||
|
||||
// 分镜图生成完成,从任务队列中移除(用户点击"生成视频"时会重新添加)
|
||||
try {
|
||||
taskQueueService.removeTaskFromQueue(taskId);
|
||||
@@ -804,6 +847,19 @@ public class StoryboardVideoService {
|
||||
} catch (Exception ue) {
|
||||
logger.warn("回退更新UserWork状态失败: taskId={}", taskId);
|
||||
}
|
||||
// 回退时也需要返还积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_vid");
|
||||
logger.info("回退更新时已返还分镜视频积分: taskId={}_vid", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("回退更新时分镜视频积分返还失败(可能未冻结或已返还): taskId={}_vid", taskId);
|
||||
}
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_img");
|
||||
logger.info("回退更新时已返还分镜图积分: taskId={}_img", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("回退更新时分镜图积分返还失败(可能未冻结或已返还): taskId={}_img", taskId);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("更新任务失败状态失败: {}", taskId, ex);
|
||||
@@ -887,17 +943,21 @@ public class StoryboardVideoService {
|
||||
paramsUpdated = true;
|
||||
}
|
||||
|
||||
// 更新参考图(如果提供了新的参考图)
|
||||
// 更新视频阶段的参考图(如果提供了新的参考图)
|
||||
if (referenceImages != null && !referenceImages.isEmpty()) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
String uploadedImagesJson = mapper.writeValueAsString(referenceImages);
|
||||
task.setUploadedImages(uploadedImagesJson);
|
||||
logger.info("更新任务 {} 的参考图: {} 张", taskId, referenceImages.size());
|
||||
String videoRefImagesJson = mapper.writeValueAsString(referenceImages);
|
||||
task.setVideoReferenceImages(videoRefImagesJson);
|
||||
logger.info("更新任务 {} 的视频参考图: {} 张", taskId, referenceImages.size());
|
||||
paramsUpdated = true;
|
||||
} catch (Exception e) {
|
||||
logger.warn("序列化参考图失败: {}", e.getMessage());
|
||||
logger.warn("序列化视频参考图失败: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 如果没有提供新的参考图,清空视频参考图字段
|
||||
task.setVideoReferenceImages(null);
|
||||
logger.info("任务 {} 未提供视频参考图,清空该字段", taskId);
|
||||
}
|
||||
|
||||
// 如果是 COMPLETED 或 FAILED 状态,更新为 PROCESSING(表示正在生成视频)
|
||||
@@ -925,6 +985,14 @@ public class StoryboardVideoService {
|
||||
logger.error("更新 TaskStatus 状态失败: {}", taskId, e);
|
||||
}
|
||||
|
||||
// 更新 UserWork 状态为 PROCESSING(视频生成中)
|
||||
try {
|
||||
userWorkService.updateStoryboardVideoWorkToProcessing(taskId);
|
||||
logger.info("UserWork 已更新为 PROCESSING(开始视频生成): taskId={}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("更新 UserWork 状态失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
}
|
||||
|
||||
// 如果有任何参数更新,保存任务
|
||||
if (paramsUpdated) {
|
||||
taskRepository.save(task);
|
||||
@@ -936,6 +1004,13 @@ public class StoryboardVideoService {
|
||||
taskQueueService.addStoryboardVideoTask(task.getUsername(), taskId);
|
||||
} catch (Exception e) {
|
||||
logger.error("添加分镜视频任务到队列失败: {}", taskId, e);
|
||||
// 返还刚刚冻结的视频积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_vid");
|
||||
logger.info("添加队列失败,已返还分镜视频积分: taskId={}_vid", taskId);
|
||||
} catch (Exception re) {
|
||||
logger.warn("返还分镜视频积分失败: taskId={}_vid, error={}", taskId, re.getMessage());
|
||||
}
|
||||
throw new RuntimeException("添加视频生成任务失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
@@ -1313,11 +1388,12 @@ public class StoryboardVideoService {
|
||||
}
|
||||
}
|
||||
|
||||
// 返还冻结积分
|
||||
// 返还冻结积分(分镜图阶段失败,返还 _img 积分)
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId());
|
||||
userService.returnFrozenPoints(task.getTaskId() + "_img");
|
||||
logger.info("已返还分镜图积分: taskId={}_img", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.warn("返还冻结积分失败(可能已处理): taskId={}", task.getTaskId());
|
||||
logger.warn("返还冻结积分失败(可能已处理): taskId={}_img", task.getTaskId());
|
||||
}
|
||||
|
||||
logger.warn("分镜图生成任务超时,已标记为失败: taskId={}", task.getTaskId());
|
||||
@@ -1379,11 +1455,12 @@ public class StoryboardVideoService {
|
||||
}
|
||||
}
|
||||
|
||||
// 返还冻结积分
|
||||
// 返还冻结积分(视频生成阶段失败,返还 _vid 积分)
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId());
|
||||
userService.returnFrozenPoints(task.getTaskId() + "_vid");
|
||||
logger.info("已返还分镜视频积分: taskId={}_vid", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.warn("返还冻结积分失败(可能已处理): taskId={}", task.getTaskId());
|
||||
logger.warn("返还冻结积分失败(可能已处理): taskId={}_vid", task.getTaskId());
|
||||
}
|
||||
|
||||
logger.warn("视频生成任务超时,已标记为失败: taskId={}, progress={}", task.getTaskId(), progress);
|
||||
|
||||
@@ -362,7 +362,23 @@ public class TaskQueueService {
|
||||
|
||||
// 返还冻结的积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId());
|
||||
// 分镜视频任务需要同时返还 _vid 和 _img 积分
|
||||
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||
try {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId() + "_vid");
|
||||
logger.debug("系统重启:已返还分镜视频积分: taskId={}_vid", taskQueue.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("系统重启:分镜视频积分返还失败(可能未冻结): taskId={}_vid", taskQueue.getTaskId());
|
||||
}
|
||||
try {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId() + "_img");
|
||||
logger.debug("系统重启:已返还分镜图积分: taskId={}_img", taskQueue.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("系统重启:分镜图积分返还失败(可能未冻结): taskId={}_img", taskQueue.getTaskId());
|
||||
}
|
||||
} else {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("返还积分失败(可能未冻结): taskId={}", taskQueue.getTaskId());
|
||||
}
|
||||
@@ -483,6 +499,19 @@ public class TaskQueueService {
|
||||
logger.warn("记录错误日志失败: {}", task.getTaskId());
|
||||
}
|
||||
}
|
||||
// 返还分镜任务冻结的积分(_img 和 _vid)
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId() + "_vid");
|
||||
logger.debug("系统重启:已返还分镜视频积分: taskId={}_vid", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("系统重启:分镜视频积分返还失败(可能未冻结): taskId={}_vid", task.getTaskId());
|
||||
}
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId() + "_img");
|
||||
logger.debug("系统重启:已返还分镜图积分: taskId={}_img", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("系统重启:分镜图积分返还失败(可能未冻结): taskId={}_img", task.getTaskId());
|
||||
}
|
||||
businessTaskCleanedCount++;
|
||||
logger.warn("系统重启:分镜视频任务 {} 已超时,标记为失败", task.getTaskId());
|
||||
} else if (noRealTaskId) {
|
||||
@@ -508,6 +537,13 @@ public class TaskQueueService {
|
||||
logger.warn("记录错误日志失败: {}", task.getTaskId());
|
||||
}
|
||||
}
|
||||
// 返还分镜图冻结的积分(分镜图阶段失败,只有 _img 积分)
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId() + "_img");
|
||||
logger.debug("系统重启:已返还分镜图积分: taskId={}_img", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("系统重启:分镜图积分返还失败(可能未冻结): taskId={}_img", task.getTaskId());
|
||||
}
|
||||
businessTaskCleanedCount++;
|
||||
logger.warn("系统重启:分镜视频任务 {} 无外部任务ID,标记为失败", task.getTaskId());
|
||||
} else {
|
||||
@@ -636,10 +672,16 @@ public class TaskQueueService {
|
||||
Integer requiredPoints = calculateRequiredPoints(taskType);
|
||||
|
||||
// 冻结积分
|
||||
PointsFreezeRecord.TaskType freezeTaskType = convertTaskType(taskType);
|
||||
|
||||
userService.freezePoints(username, taskId, freezeTaskType, requiredPoints,
|
||||
"任务提交冻结积分 - " + taskType.getDescription());
|
||||
// 注意:分镜视频任务的积分已在 StoryboardVideoService 中使用 _img 和 _vid 后缀冻结
|
||||
// 这里跳过分镜视频任务的积分冻结,避免重复冻结
|
||||
if (taskType != TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||
PointsFreezeRecord.TaskType freezeTaskType = convertTaskType(taskType);
|
||||
|
||||
userService.freezePoints(username, taskId, freezeTaskType, requiredPoints,
|
||||
"任务提交冻结积分 - " + taskType.getDescription());
|
||||
} else {
|
||||
logger.info("分镜视频任务 {} 跳过积分冻结(已在业务层冻结)", taskId);
|
||||
}
|
||||
|
||||
// 创建新的队列任务
|
||||
TaskQueue taskQueue = new TaskQueue(username, taskId, taskType);
|
||||
@@ -790,8 +832,22 @@ public class TaskQueueService {
|
||||
// 返还冻结的积分
|
||||
try {
|
||||
// 分镜视频使用 taskId + "_vid" 作为积分冻结ID
|
||||
// 分镜图使用 taskId + "_img" 作为积分冻结ID
|
||||
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||
userService.returnFrozenPoints(taskId + "_vid");
|
||||
// 尝试返还视频积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_vid");
|
||||
logger.info("已返还分镜视频积分: taskId={}_vid", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("分镜视频积分返还失败(可能未冻结): taskId={}_vid", taskId);
|
||||
}
|
||||
// 同时尝试返还分镜图积分(如果分镜图阶段失败)
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_img");
|
||||
logger.info("已返还分镜图积分: taskId={}_img", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("分镜图积分返还失败(可能未冻结或已返还): taskId={}_img", taskId);
|
||||
}
|
||||
} else {
|
||||
userService.returnFrozenPoints(taskId);
|
||||
}
|
||||
@@ -1135,29 +1191,30 @@ public class TaskQueueService {
|
||||
// 获取分镜图(取第一张)
|
||||
String storyboardImage = images.get(0);
|
||||
|
||||
// 解析用户上传的图片
|
||||
// 解析视频阶段用户上传的参考图(优先使用 videoReferenceImages 字段)
|
||||
List<String> userUploadedImages = new ArrayList<>();
|
||||
String uploadedImagesJson = task.getUploadedImages();
|
||||
if (uploadedImagesJson != null && !uploadedImagesJson.isEmpty()) {
|
||||
String videoRefImagesJson = task.getVideoReferenceImages();
|
||||
if (videoRefImagesJson != null && !videoRefImagesJson.isEmpty()) {
|
||||
try {
|
||||
List<String> parsedUploads = objectMapper.readValue(uploadedImagesJson,
|
||||
List<String> parsedUploads = objectMapper.readValue(videoRefImagesJson,
|
||||
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||
for (String img : parsedUploads) {
|
||||
if (img != null && !img.isEmpty() && !"null".equals(img)) {
|
||||
userUploadedImages.add(img);
|
||||
}
|
||||
}
|
||||
logger.info("解析用户上传图片,有效数量: {}", userUploadedImages.size());
|
||||
logger.info("解析视频阶段参考图,有效数量: {}", userUploadedImages.size());
|
||||
} catch (Exception e) {
|
||||
logger.warn("解析用户上传图片失败: {}", e.getMessage());
|
||||
logger.warn("解析视频阶段参考图失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
// 注意:不再使用 uploadedImages(分镜图阶段的参考图),只使用 videoReferenceImages
|
||||
|
||||
// 生成视频时进行拼图处理(分镜图 + 用户上传图片)
|
||||
// 生成视频时进行拼图处理(分镜图 + 视频阶段用户上传的参考图)
|
||||
String imageForVideo;
|
||||
if (!userUploadedImages.isEmpty()) {
|
||||
// 有用户上传图片,创建复合布局
|
||||
logger.info("创建复合布局(分镜图 + {}张用户图片): 目标比例={}", userUploadedImages.size(), task.getAspectRatio());
|
||||
logger.info("创建复合布局(分镜图 + {}张视频参考图): 目标比例={}", userUploadedImages.size(), task.getAspectRatio());
|
||||
try {
|
||||
imageForVideo = imageGridService.createStoryboardLayout(storyboardImage, userUploadedImages, task.getAspectRatio());
|
||||
} catch (Exception e) {
|
||||
@@ -1166,7 +1223,7 @@ public class TaskQueueService {
|
||||
}
|
||||
} else {
|
||||
// 无用户上传图片,创建简单布局(分镜图适配目标比例)
|
||||
logger.info("创建简单布局(无用户图片): 目标比例={}", task.getAspectRatio());
|
||||
logger.info("创建简单布局(无视频参考图): 目标比例={}", task.getAspectRatio());
|
||||
try {
|
||||
imageForVideo = imageGridService.createStoryboardLayout(storyboardImage, null, task.getAspectRatio());
|
||||
} catch (Exception e) {
|
||||
@@ -1440,20 +1497,22 @@ public class TaskQueueService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待检查任务列表(只读事务,快速完成)
|
||||
* 获取待检查任务列表(使用 TransactionTemplate 手动管理事务,快速完成)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<TaskQueue> getTasksToCheck() {
|
||||
return taskQueueRepository.findTasksToCheck();
|
||||
return readOnlyTransactionTemplate.execute(status -> {
|
||||
return taskQueueRepository.findTasksToCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的任务(快速检查,只统计数量)
|
||||
* @return true 如果有待处理任务,false 否则
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public boolean hasTasksToCheck() {
|
||||
return taskQueueRepository.countTasksToCheck() > 0;
|
||||
return readOnlyTransactionTemplate.execute(status -> {
|
||||
return taskQueueRepository.countTasksToCheck() > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1898,7 +1957,8 @@ public class TaskQueueService {
|
||||
private void updateTaskAsCompleted(TaskQueue taskQueue, String resultUrl) {
|
||||
String taskId = taskQueue.getTaskId();
|
||||
|
||||
// 第一步:在事务中完成数据库更新(不包含COS上传)
|
||||
// 第一步:在事务中完成核心数据库更新
|
||||
boolean transactionSuccess = false;
|
||||
try {
|
||||
transactionTemplate.executeWithoutResult(status -> {
|
||||
try {
|
||||
@@ -1915,6 +1975,7 @@ public class TaskQueueService {
|
||||
}
|
||||
|
||||
if (freshTaskQueue.getStatus() == TaskQueue.QueueStatus.COMPLETED) {
|
||||
logger.info("任务已完成,跳过重复处理: {}", taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1934,39 +1995,6 @@ public class TaskQueueService {
|
||||
logger.warn("积分扣除失败(不影响任务完成): taskId={}, error={}", taskId, e.getMessage());
|
||||
}
|
||||
|
||||
// 更新原始任务状态(先用原始URL)
|
||||
updateOriginalTaskStatus(taskQueue, "COMPLETED", resultUrl, null);
|
||||
|
||||
// 创建/更新用户作品
|
||||
if (resultUrl != null && !resultUrl.isEmpty()) {
|
||||
try {
|
||||
userWorkService.createWorkFromTask(taskId, resultUrl);
|
||||
} catch (Exception workException) {
|
||||
if (workException.getMessage() == null ||
|
||||
(!workException.getMessage().contains("已存在") &&
|
||||
!workException.getMessage().contains("Duplicate entry"))) {
|
||||
logger.warn("创建/更新用户作品失败: {}", taskId, workException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 task_status 表中的状态
|
||||
if (resultUrl != null && !resultUrl.isEmpty()) {
|
||||
try {
|
||||
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
|
||||
if (taskStatus != null) {
|
||||
taskStatus.setStatus(TaskStatus.Status.COMPLETED);
|
||||
taskStatus.setProgress(100);
|
||||
taskStatus.setResultUrl(resultUrl);
|
||||
taskStatus.setCompletedAt(java.time.LocalDateTime.now());
|
||||
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||
logger.info("task_status 表状态已更新为 COMPLETED: {}", taskId);
|
||||
}
|
||||
} catch (Exception statusException) {
|
||||
logger.warn("更新 task_status 状态失败: {}", taskId, statusException);
|
||||
}
|
||||
}
|
||||
|
||||
// 任务完成后从 task_queue 中删除记录
|
||||
try {
|
||||
taskQueueRepository.delete(freshTaskQueue);
|
||||
@@ -1980,20 +2008,20 @@ public class TaskQueueService {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
transactionSuccess = true;
|
||||
} catch (Exception e) {
|
||||
logger.error("执行更新任务完成状态事务失败: {}", taskId, e);
|
||||
return; // 事务失败,不继续COS上传
|
||||
}
|
||||
|
||||
// 第二步:在事务外执行COS上传(避免事务超时)
|
||||
String finalResultUrl = resultUrl;
|
||||
if (resultUrl != null && !resultUrl.isEmpty() && cosService.isEnabled()) {
|
||||
try {
|
||||
logger.info("开始上传视频到COS(事务外): taskId={}", taskId);
|
||||
String cosUrl = cosService.uploadVideoFromUrl(resultUrl, null);
|
||||
if (cosUrl != null && !cosUrl.isEmpty()) {
|
||||
logger.info("COS上传成功: taskId={}, cosUrl={}", taskId, cosUrl);
|
||||
// 用新事务更新URL为COS URL
|
||||
updateResultUrlToCos(taskQueue, cosUrl);
|
||||
finalResultUrl = cosUrl;
|
||||
} else {
|
||||
logger.warn("COS上传返回空URL,使用原始URL: taskId={}", taskId);
|
||||
}
|
||||
@@ -2001,24 +2029,75 @@ public class TaskQueueService {
|
||||
logger.error("上传视频到COS失败,使用原始URL: taskId={}", taskId, cosException);
|
||||
}
|
||||
}
|
||||
|
||||
// 第三步:更新业务表和UserWork(使用级联更新,避免锁竞争)
|
||||
if (finalResultUrl != null && !finalResultUrl.isEmpty()) {
|
||||
try {
|
||||
// 通过 TaskStatusPollingService 的级联更新来更新所有相关表
|
||||
// 这样可以避免多个事务同时更新同一条记录
|
||||
boolean updated = taskStatusPollingService.updateTaskStatusWithCascade(
|
||||
taskId,
|
||||
TaskStatus.Status.COMPLETED,
|
||||
finalResultUrl,
|
||||
null
|
||||
);
|
||||
if (updated) {
|
||||
logger.info("任务完成状态已通过级联更新: taskId={}", taskId);
|
||||
} else {
|
||||
// 级联更新失败,回退到直接更新
|
||||
logger.warn("级联更新失败,回退到直接更新: taskId={}", taskId);
|
||||
updateOriginalTaskStatus(taskQueue, "COMPLETED", finalResultUrl, null);
|
||||
try {
|
||||
userWorkService.createWorkFromTask(taskId, finalResultUrl);
|
||||
} catch (Exception e) {
|
||||
logger.warn("创建/更新用户作品失败: {}", taskId, e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("更新任务完成状态失败: {}", taskId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新结果URL为COS URL(独立事务)
|
||||
* 只更新URL字段,不重复更新状态
|
||||
*/
|
||||
private void updateResultUrlToCos(TaskQueue taskQueue, String cosUrl) {
|
||||
String taskId = taskQueue.getTaskId();
|
||||
try {
|
||||
transactionTemplate.executeWithoutResult(status -> {
|
||||
try {
|
||||
// 更新原始任务的resultUrl
|
||||
updateOriginalTaskStatus(taskQueue, "COMPLETED", cosUrl, null);
|
||||
// 直接更新业务表的resultUrl(不通过updateOriginalTaskStatus,避免重复更新)
|
||||
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||
storyboardVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||
task.setResultUrl(cosUrl);
|
||||
// 同时更新videoUrls
|
||||
if (!cosUrl.startsWith("data:image")) {
|
||||
task.setVideoUrls("[\"" + cosUrl + "\"]");
|
||||
}
|
||||
storyboardVideoTaskRepository.save(task);
|
||||
logger.info("已更新StoryboardVideoTask的COS URL: taskId={}", taskId);
|
||||
});
|
||||
} else if (taskQueue.getTaskType() == TaskQueue.TaskType.IMAGE_TO_VIDEO) {
|
||||
imageToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||
task.setResultUrl(cosUrl);
|
||||
imageToVideoTaskRepository.save(task);
|
||||
logger.info("已更新ImageToVideoTask的COS URL: taskId={}", taskId);
|
||||
});
|
||||
} else if (taskQueue.getTaskType() == TaskQueue.TaskType.TEXT_TO_VIDEO) {
|
||||
textToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||
task.setResultUrl(cosUrl);
|
||||
textToVideoTaskRepository.save(task);
|
||||
logger.info("已更新TextToVideoTask的COS URL: taskId={}", taskId);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户作品的resultUrl
|
||||
// 更新用户作品的resultUrl(使用简单更新,不创建新记录)
|
||||
try {
|
||||
userWorkService.createWorkFromTask(taskId, cosUrl);
|
||||
userWorkService.updateWorkResultUrl(taskId, cosUrl);
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
logger.warn("更新UserWork COS URL失败: {}", taskId, e);
|
||||
}
|
||||
|
||||
// 更新task_status的resultUrl
|
||||
@@ -2113,10 +2192,15 @@ public class TaskQueueService {
|
||||
imageToVideoTaskRepository.save(task);
|
||||
logger.info("回退更新图生视频任务为完成: {}", taskId);
|
||||
});
|
||||
} else if (taskId.startsWith("storyboard_")) {
|
||||
} else if (taskId.startsWith("storyboard_") || taskId.startsWith("sb_")) {
|
||||
storyboardVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||
task.setStatus(StoryboardVideoTask.TaskStatus.COMPLETED);
|
||||
task.setResultUrl(resultUrl);
|
||||
// 如果是视频URL(不是Base64图片),同时设置videoUrls
|
||||
if (resultUrl != null && !resultUrl.startsWith("data:image")) {
|
||||
task.setVideoUrls("[\"" + resultUrl + "\"]");
|
||||
logger.info("回退更新时设置videoUrls: taskId={}", taskId);
|
||||
}
|
||||
task.setProgress(100);
|
||||
task.setCompletedAt(java.time.LocalDateTime.now());
|
||||
storyboardVideoTaskRepository.save(task);
|
||||
@@ -2177,7 +2261,14 @@ public class TaskQueueService {
|
||||
imageToVideoTaskRepository.save(task);
|
||||
logger.info("回退更新图生视频任务为失败: {}", taskId);
|
||||
});
|
||||
} else if (taskId.startsWith("storyboard_")) {
|
||||
// 返还冻结的积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId);
|
||||
logger.info("回退更新时已返还积分: taskId={}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("回退更新时积分返还失败(可能未冻结): taskId={}", taskId);
|
||||
}
|
||||
} else if (taskId.startsWith("storyboard_") || taskId.startsWith("sb_")) {
|
||||
storyboardVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||
task.setStatus(StoryboardVideoTask.TaskStatus.FAILED);
|
||||
task.setErrorMessage(errorMessage);
|
||||
@@ -2185,6 +2276,19 @@ public class TaskQueueService {
|
||||
storyboardVideoTaskRepository.save(task);
|
||||
logger.info("回退更新分镜视频任务为失败: {}", taskId);
|
||||
});
|
||||
// 分镜任务需要同时返还 _vid 和 _img 积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_vid");
|
||||
logger.info("回退更新时已返还分镜视频积分: taskId={}_vid", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("回退更新时分镜视频积分返还失败(可能未冻结): taskId={}_vid", taskId);
|
||||
}
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_img");
|
||||
logger.info("回退更新时已返还分镜图积分: taskId={}_img", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("回退更新时分镜图积分返还失败(可能未冻结): taskId={}_img", taskId);
|
||||
}
|
||||
} else if (taskId.startsWith("txt2vid_")) {
|
||||
textToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||
task.setStatus(TextToVideoTask.TaskStatus.FAILED);
|
||||
@@ -2193,6 +2297,13 @@ public class TaskQueueService {
|
||||
textToVideoTaskRepository.save(task);
|
||||
logger.info("回退更新文生视频任务为失败: {}", taskId);
|
||||
});
|
||||
// 返还冻结的积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId);
|
||||
logger.info("回退更新时已返还积分: taskId={}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("回退更新时积分返还失败(可能未冻结): taskId={}", taskId);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 UserWork
|
||||
@@ -2211,7 +2322,7 @@ public class TaskQueueService {
|
||||
taskType = "IMAGE_TO_VIDEO";
|
||||
username = imageToVideoTaskRepository.findByTaskId(taskId)
|
||||
.map(t -> t.getUsername()).orElse(null);
|
||||
} else if (taskId.startsWith("storyboard_")) {
|
||||
} else if (taskId.startsWith("storyboard_") || taskId.startsWith("sb_")) {
|
||||
taskType = "STORYBOARD_VIDEO";
|
||||
username = storyboardVideoTaskRepository.findByTaskId(taskId)
|
||||
.map(t -> t.getUsername()).orElse(null);
|
||||
@@ -2254,8 +2365,22 @@ public class TaskQueueService {
|
||||
// 返还冻结的积分
|
||||
try {
|
||||
// 分镜视频使用 taskId + "_vid" 作为积分冻结ID
|
||||
// 分镜图使用 taskId + "_img" 作为积分冻结ID
|
||||
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId() + "_vid");
|
||||
// 尝试返还视频积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId() + "_vid");
|
||||
logger.info("已返还分镜视频积分: taskId={}_vid", taskQueue.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("分镜视频积分返还失败(可能未冻结): taskId={}_vid", taskQueue.getTaskId());
|
||||
}
|
||||
// 同时尝试返还分镜图积分(如果分镜图阶段失败)
|
||||
try {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId() + "_img");
|
||||
logger.info("已返还分镜图积分: taskId={}_img", taskQueue.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("分镜图积分返还失败(可能未冻结或已返还): taskId={}_img", taskQueue.getTaskId());
|
||||
}
|
||||
} else {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId());
|
||||
}
|
||||
@@ -2334,7 +2459,23 @@ public class TaskQueueService {
|
||||
taskQueueRepository.save(taskQueue);
|
||||
|
||||
// 返还冻结的积分
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId());
|
||||
// 分镜视频任务需要同时返还 _vid 和 _img 积分
|
||||
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||
try {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId() + "_vid");
|
||||
logger.info("已返还分镜视频积分: taskId={}_vid", taskQueue.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("分镜视频积分返还失败(可能未冻结): taskId={}_vid", taskQueue.getTaskId());
|
||||
}
|
||||
try {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId() + "_img");
|
||||
logger.info("已返还分镜图积分: taskId={}_img", taskQueue.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.debug("分镜图积分返还失败(可能未冻结): taskId={}_img", taskQueue.getTaskId());
|
||||
}
|
||||
} else {
|
||||
userService.returnFrozenPoints(taskQueue.getTaskId());
|
||||
}
|
||||
|
||||
// 更新原始任务状态
|
||||
updateOriginalTaskStatus(taskQueue, "FAILED", null, "任务处理超时");
|
||||
@@ -2480,6 +2621,20 @@ public class TaskQueueService {
|
||||
if (taskOpt.isPresent()) {
|
||||
StoryboardVideoTask task = taskOpt.get();
|
||||
if ("COMPLETED".equals(status)) {
|
||||
// 保存视频URL到videoUrls字段(JSON数组格式)
|
||||
if (resultUrl != null && !resultUrl.isEmpty() && !resultUrl.startsWith("data:image")) {
|
||||
// 视频URL,保存到videoUrls
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String videoUrlsJson = mapper.writeValueAsString(java.util.List.of(resultUrl));
|
||||
task.setVideoUrls(videoUrlsJson);
|
||||
logger.info("已设置videoUrls: taskId={}, videoUrls={}", taskId, videoUrlsJson);
|
||||
} catch (Exception e) {
|
||||
// 如果JSON序列化失败,直接保存原始URL
|
||||
task.setVideoUrls("[\"" + resultUrl + "\"]");
|
||||
logger.warn("JSON序列化失败,使用简单格式: taskId={}", taskId);
|
||||
}
|
||||
}
|
||||
// resultUrl 现在存储的是视频URL(替换之前的分镜图Base64)
|
||||
task.setResultUrl(resultUrl);
|
||||
task.setRealTaskId(taskQueue.getRealTaskId());
|
||||
@@ -2540,7 +2695,23 @@ public class TaskQueueService {
|
||||
taskQueueRepository.save(taskQueue);
|
||||
|
||||
// 返还冻结的积分
|
||||
userService.returnFrozenPoints(taskId);
|
||||
// 分镜视频任务需要同时返还 _vid 和 _img 积分
|
||||
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_vid");
|
||||
logger.info("已返还分镜视频积分: taskId={}_vid", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("分镜视频积分返还失败(可能未冻结): taskId={}_vid", taskId);
|
||||
}
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_img");
|
||||
logger.info("已返还分镜图积分: taskId={}_img", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("分镜图积分返还失败(可能未冻结): taskId={}_img", taskId);
|
||||
}
|
||||
} else {
|
||||
userService.returnFrozenPoints(taskId);
|
||||
}
|
||||
|
||||
// 更新原始任务状态
|
||||
updateOriginalTaskStatus(taskQueue, "CANCELLED", null, "用户取消了任务");
|
||||
@@ -2651,10 +2822,26 @@ public class TaskQueueService {
|
||||
updateRelatedTaskStatus(task.getTaskId(), task.getTaskType());
|
||||
|
||||
// 返还冻结积分
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.warn("返还冻结积分失败(可能已处理): taskId={}", task.getTaskId());
|
||||
// 分镜视频任务需要同时返还 _vid 和 _img 积分
|
||||
if (task.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId() + "_vid");
|
||||
logger.info("已返还分镜视频积分: taskId={}_vid", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.warn("分镜视频积分返还失败(可能已处理): taskId={}_vid", task.getTaskId());
|
||||
}
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId() + "_img");
|
||||
logger.info("已返还分镜图积分: taskId={}_img", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.warn("分镜图积分返还失败(可能已处理): taskId={}_img", task.getTaskId());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
userService.returnFrozenPoints(task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.warn("返还冻结积分失败(可能已处理): taskId={}", task.getTaskId());
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn("队列任务超时,已标记为失败: taskId={}, taskType={}, createdAt={}",
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.model.TaskStatus;
|
||||
import com.example.demo.model.UserWork;
|
||||
import com.example.demo.repository.TaskStatusRepository;
|
||||
import com.example.demo.config.DynamicApiConfig;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -63,6 +64,9 @@ public class TaskStatusPollingService {
|
||||
@Autowired
|
||||
private DynamicApiConfig dynamicApiConfig;
|
||||
|
||||
@Autowired
|
||||
private UserWorkService userWorkService;
|
||||
|
||||
/**
|
||||
* 系统启动时恢复处理中的任务
|
||||
* - 对所有 PROCESSING 状态的任务进行一次状态查询
|
||||
@@ -611,6 +615,11 @@ public class TaskStatusPollingService {
|
||||
}
|
||||
if (resultUrl != null) {
|
||||
task.setResultUrl(resultUrl);
|
||||
// 如果是视频URL(不是Base64图片),同时设置videoUrls
|
||||
if (status == TaskStatus.Status.COMPLETED && !resultUrl.startsWith("data:image")) {
|
||||
task.setVideoUrls("[\"" + resultUrl + "\"]");
|
||||
logger.info("已设置videoUrls: taskId={}", taskId);
|
||||
}
|
||||
}
|
||||
if (status == TaskStatus.Status.COMPLETED) {
|
||||
task.setProgress(100);
|
||||
@@ -683,42 +692,92 @@ public class TaskStatusPollingService {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是失败状态,记录错误日志
|
||||
if (status == TaskStatus.Status.FAILED && errorMessage != null) {
|
||||
// 如果是失败状态,返还冻结的积分并记录错误日志
|
||||
if (status == TaskStatus.Status.FAILED) {
|
||||
// 返还冻结的积分
|
||||
try {
|
||||
String username = null;
|
||||
String taskType = "UNKNOWN";
|
||||
|
||||
if (taskId.startsWith("img2vid_")) {
|
||||
taskType = "IMAGE_TO_VIDEO";
|
||||
username = imageToVideoTaskRepository.findByTaskId(taskId)
|
||||
.map(t -> t.getUsername()).orElse(null);
|
||||
} else if (taskId.startsWith("storyboard_") || taskId.startsWith("sb_")) {
|
||||
taskType = "STORYBOARD_VIDEO";
|
||||
username = storyboardVideoTaskRepository.findByTaskId(taskId)
|
||||
.map(t -> t.getUsername()).orElse(null);
|
||||
} else if (taskId.startsWith("txt2vid_")) {
|
||||
taskType = "TEXT_TO_VIDEO";
|
||||
username = textToVideoTaskRepository.findByTaskId(taskId)
|
||||
.map(t -> t.getUsername()).orElse(null);
|
||||
}
|
||||
|
||||
if (username != null) {
|
||||
userErrorLogService.logErrorAsync(
|
||||
username,
|
||||
com.example.demo.model.UserErrorLog.ErrorType.TASK_PROCESSING_ERROR,
|
||||
errorMessage,
|
||||
"TaskStatusPollingService.updateTaskStatusWithCascade",
|
||||
taskId,
|
||||
taskType
|
||||
);
|
||||
if (taskId.startsWith("sb_") || taskId.startsWith("storyboard_")) {
|
||||
// 分镜任务需要同时返还 _vid 和 _img 积分
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_vid");
|
||||
logger.info("级联更新时已返还分镜视频积分: taskId={}_vid", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("级联更新时分镜视频积分返还失败(可能未冻结或已返还): taskId={}_vid", taskId);
|
||||
}
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId + "_img");
|
||||
logger.info("级联更新时已返还分镜图积分: taskId={}_img", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.debug("级联更新时分镜图积分返还失败(可能未冻结或已返还): taskId={}_img", taskId);
|
||||
}
|
||||
} else {
|
||||
logger.warn("无法记录错误日志,未找到用户名: taskId={}", taskId);
|
||||
// 其他任务类型直接使用 taskId
|
||||
userService.returnFrozenPoints(taskId);
|
||||
logger.info("级联更新时已返还积分: taskId={}", taskId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("级联更新时积分返还失败(可能未冻结或已返还): taskId={}", taskId);
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
if (errorMessage != null) {
|
||||
try {
|
||||
String username = null;
|
||||
String taskType = "UNKNOWN";
|
||||
|
||||
if (taskId.startsWith("img2vid_")) {
|
||||
taskType = "IMAGE_TO_VIDEO";
|
||||
username = imageToVideoTaskRepository.findByTaskId(taskId)
|
||||
.map(t -> t.getUsername()).orElse(null);
|
||||
} else if (taskId.startsWith("storyboard_") || taskId.startsWith("sb_")) {
|
||||
taskType = "STORYBOARD_VIDEO";
|
||||
username = storyboardVideoTaskRepository.findByTaskId(taskId)
|
||||
.map(t -> t.getUsername()).orElse(null);
|
||||
} else if (taskId.startsWith("txt2vid_")) {
|
||||
taskType = "TEXT_TO_VIDEO";
|
||||
username = textToVideoTaskRepository.findByTaskId(taskId)
|
||||
.map(t -> t.getUsername()).orElse(null);
|
||||
}
|
||||
|
||||
if (username != null) {
|
||||
userErrorLogService.logErrorAsync(
|
||||
username,
|
||||
com.example.demo.model.UserErrorLog.ErrorType.TASK_PROCESSING_ERROR,
|
||||
errorMessage,
|
||||
"TaskStatusPollingService.updateTaskStatusWithCascade",
|
||||
taskId,
|
||||
taskType
|
||||
);
|
||||
} else {
|
||||
logger.warn("无法记录错误日志,未找到用户名: taskId={}", taskId);
|
||||
}
|
||||
} catch (Exception logException) {
|
||||
logger.warn("记录错误日志失败: taskId={}, error={}", taskId, logException.getMessage());
|
||||
}
|
||||
} catch (Exception logException) {
|
||||
logger.warn("记录错误日志失败: taskId={}, error={}", taskId, logException.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 同步 UserWork 状态
|
||||
try {
|
||||
UserWork.WorkStatus workStatus = convertToUserWorkStatus(status);
|
||||
userWorkService.updateWorkStatusWithResult(taskId, workStatus, resultUrl);
|
||||
logger.info("已同步 UserWork 状态: taskId={}, status={}", taskId, workStatus);
|
||||
} catch (Exception e) {
|
||||
logger.warn("同步 UserWork 状态失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 TaskStatus.Status 转换为 UserWork.WorkStatus
|
||||
*/
|
||||
private UserWork.WorkStatus convertToUserWorkStatus(TaskStatus.Status status) {
|
||||
return switch (status) {
|
||||
case PENDING -> UserWork.WorkStatus.PENDING;
|
||||
case PROCESSING -> UserWork.WorkStatus.PROCESSING;
|
||||
case COMPLETED -> UserWork.WorkStatus.COMPLETED;
|
||||
case FAILED -> UserWork.WorkStatus.FAILED;
|
||||
default -> UserWork.WorkStatus.PROCESSING;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,6 +41,9 @@ public class TextToVideoService {
|
||||
@Autowired
|
||||
private UserWorkService userWorkService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private TaskStatusPollingService taskStatusPollingService;
|
||||
|
||||
@@ -196,6 +199,8 @@ public class TextToVideoService {
|
||||
logger.warn("回退更新UserWork状态失败: taskId={}", task.getTaskId());
|
||||
}
|
||||
}
|
||||
// 返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
return CompletableFuture.completedFuture(null); // 直接返回,不进行轮询
|
||||
}
|
||||
|
||||
@@ -231,6 +236,9 @@ public class TextToVideoService {
|
||||
}
|
||||
} catch (Exception saveException) {
|
||||
logger.error("保存失败状态时出错: {}", task.getTaskId(), saveException);
|
||||
} finally {
|
||||
// 确保无论如何都返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +265,8 @@ public class TextToVideoService {
|
||||
TextToVideoTask currentTask = taskRepository.findByTaskId(task.getTaskId()).orElse(null);
|
||||
if (currentTask != null && currentTask.getStatus() == TextToVideoTask.TaskStatus.CANCELLED) {
|
||||
logger.info("任务 {} 已被取消,停止轮询", task.getTaskId());
|
||||
// 任务取消时返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -329,6 +339,8 @@ public class TextToVideoService {
|
||||
logger.warn("回退更新UserWork状态失败: taskId={}", task.getTaskId());
|
||||
}
|
||||
}
|
||||
// 返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
logger.error("文生视频任务失败: {}", task.getTaskId());
|
||||
return;
|
||||
} else if ("processing".equals(status) || "pending".equals(status) || "running".equals(status)) {
|
||||
@@ -370,10 +382,14 @@ public class TextToVideoService {
|
||||
logger.warn("回退更新UserWork状态失败: taskId={}", task.getTaskId());
|
||||
}
|
||||
}
|
||||
// 返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
logger.error("文生视频任务超时: {}", task.getTaskId());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("轮询任务状态被中断: {}", task.getTaskId(), e);
|
||||
// 线程中断时返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
logger.error("轮询任务状态异常: {}", task.getTaskId(), e);
|
||||
@@ -381,6 +397,8 @@ public class TextToVideoService {
|
||||
if (e.getCause() != null) {
|
||||
logger.error("异常原因: {}", e.getCause().getMessage());
|
||||
}
|
||||
// 轮询异常时返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,9 +439,10 @@ public class TextToVideoService {
|
||||
if (page < 0) {
|
||||
page = 0;
|
||||
}
|
||||
if (size <= 0 || size > 100) {
|
||||
size = 10; // 默认每页10条,最大100条
|
||||
if (size <= 0) {
|
||||
size = 10; // 默认每页10条
|
||||
}
|
||||
// 移除size上限限制,允许获取全部历史记录
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size);
|
||||
Page<TextToVideoTask> taskPage = taskRepository.findByUsernameOrderByCreatedAtDesc(username, pageable);
|
||||
@@ -547,6 +566,9 @@ public class TextToVideoService {
|
||||
}
|
||||
}
|
||||
|
||||
// 返还冻结积分
|
||||
returnFrozenPointsSafely(task.getTaskId());
|
||||
|
||||
logger.warn("文生视频任务超时,已标记为失败: taskId={}", task.getTaskId());
|
||||
|
||||
handledCount++;
|
||||
@@ -631,4 +653,16 @@ public class TextToVideoService {
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全返还冻结积分(捕获异常,避免影响主流程)
|
||||
*/
|
||||
private void returnFrozenPointsSafely(String taskId) {
|
||||
try {
|
||||
userService.returnFrozenPoints(taskId);
|
||||
logger.info("已返还冻结积分: taskId={}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("返还冻结积分失败(可能未冻结): taskId={}, error={}", taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
@@ -36,7 +37,7 @@ public class UserService {
|
||||
private final CacheManager cacheManager;
|
||||
private final MembershipLevelRepository membershipLevelRepository;
|
||||
|
||||
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder,
|
||||
public UserService(UserRepository userRepository, @Lazy PasswordEncoder passwordEncoder,
|
||||
PointsFreezeRecordRepository pointsFreezeRecordRepository,
|
||||
com.example.demo.repository.OrderRepository orderRepository,
|
||||
com.example.demo.repository.PaymentRepository paymentRepository,
|
||||
@@ -364,15 +365,20 @@ public class UserService {
|
||||
|
||||
/**
|
||||
* 返还冻结的积分(任务失败)
|
||||
* 使用悲观锁防止并发重复返还
|
||||
* 使用 REQUIRES_NEW 传播行为,防止异常导致外部事务回滚
|
||||
* 如果积分已经返还或扣除,则静默返回(幂等操作)
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void returnFrozenPoints(String taskId) {
|
||||
PointsFreezeRecord record = pointsFreezeRecordRepository.findByTaskId(taskId)
|
||||
// 使用悲观写锁查询,防止并发重复返还
|
||||
PointsFreezeRecord record = pointsFreezeRecordRepository.findByTaskIdWithLock(taskId)
|
||||
.orElseThrow(() -> new RuntimeException("找不到冻结记录: " + taskId));
|
||||
|
||||
// 如果状态不是 FROZEN,说明已经处理过,静默返回(幂等操作)
|
||||
if (record.getStatus() != PointsFreezeRecord.FreezeStatus.FROZEN) {
|
||||
throw new RuntimeException("冻结记录状态不正确: " + record.getStatus());
|
||||
logger.debug("积分记录状态为 {},跳过返还: taskId={}", record.getStatus(), taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
User user = userRepository.findByUsername(record.getUsername())
|
||||
|
||||
@@ -294,6 +294,7 @@ public class UserWorkService {
|
||||
work.setPointsCost(task.getCostPoints());
|
||||
work.setStatus(UserWork.WorkStatus.COMPLETED);
|
||||
work.setCompletedAt(LocalDateTime.now());
|
||||
work.setUploadedImages(task.getUploadedImages()); // 同步用户上传的参考图,用于"做同款"
|
||||
|
||||
work = userWorkRepository.save(work);
|
||||
logger.info("创建分镜视频作品成功: {}, 用户: {}, 分镜图: {}", work.getId(), work.getUsername(), task.getResultUrl() != null ? "有" : "无");
|
||||
@@ -334,12 +335,59 @@ public class UserWorkService {
|
||||
work.setPointsCost(0); // 分镜图不单独扣费
|
||||
work.setStatus(UserWork.WorkStatus.COMPLETED);
|
||||
work.setCompletedAt(LocalDateTime.now());
|
||||
work.setUploadedImages(task.getUploadedImages()); // 同步用户上传的参考图,用于"做同款"
|
||||
|
||||
work = userWorkRepository.save(work);
|
||||
logger.info("创建分镜图作品成功: {}, 用户: {}", work.getId(), work.getUsername());
|
||||
return work;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分镜视频作品状态为分镜图完成
|
||||
* 分镜图生成完成后调用,将 PROCESSING 状态改为 COMPLETED
|
||||
* 这样主页不会显示"生成中"的轮询状态
|
||||
*/
|
||||
@Transactional
|
||||
public void updateStoryboardVideoWorkToImageCompleted(String taskId, String imageUrl) {
|
||||
Optional<UserWork> workOpt = userWorkRepository.findByTaskId(taskId);
|
||||
if (workOpt.isPresent()) {
|
||||
UserWork work = workOpt.get();
|
||||
// 只更新 PROCESSING 状态的分镜视频作品
|
||||
if (work.getWorkType() == UserWork.WorkType.STORYBOARD_VIDEO
|
||||
&& work.getStatus() == UserWork.WorkStatus.PROCESSING) {
|
||||
work.setStatus(UserWork.WorkStatus.COMPLETED);
|
||||
work.setResultUrl(imageUrl); // 暂时设置为分镜图URL
|
||||
work.setThumbnailUrl(imageUrl);
|
||||
work.setCompletedAt(LocalDateTime.now());
|
||||
userWorkRepository.save(work);
|
||||
logger.info("分镜视频作品状态已更新为分镜图完成: taskId={}, workId={}", taskId, work.getId());
|
||||
}
|
||||
} else {
|
||||
logger.warn("未找到分镜视频作品记录: taskId={}", taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分镜视频作品状态为视频生成中
|
||||
* 用户点击"生成视频"后调用,将 COMPLETED 状态改回 PROCESSING
|
||||
*/
|
||||
@Transactional
|
||||
public void updateStoryboardVideoWorkToProcessing(String taskId) {
|
||||
Optional<UserWork> workOpt = userWorkRepository.findByTaskId(taskId);
|
||||
if (workOpt.isPresent()) {
|
||||
UserWork work = workOpt.get();
|
||||
// 只更新分镜视频作品
|
||||
if (work.getWorkType() == UserWork.WorkType.STORYBOARD_VIDEO) {
|
||||
work.setStatus(UserWork.WorkStatus.PROCESSING);
|
||||
work.setCompletedAt(null); // 清除完成时间
|
||||
userWorkRepository.save(work);
|
||||
logger.info("分镜视频作品状态已更新为视频生成中: taskId={}, workId={}", taskId, work.getId());
|
||||
}
|
||||
} else {
|
||||
logger.warn("未找到分镜视频作品记录: taskId={}", taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名获取用户ID
|
||||
*/
|
||||
@@ -451,6 +499,7 @@ public class UserWorkService {
|
||||
|
||||
/**
|
||||
* 删除作品(软删除)
|
||||
* 如果是分镜图作品,同时删除对应的分镜视频作品
|
||||
*/
|
||||
@Transactional
|
||||
public boolean deleteWork(Long workId, String username) {
|
||||
@@ -466,6 +515,30 @@ public class UserWorkService {
|
||||
|
||||
int result = userWorkRepository.softDeleteWork(workId, username, LocalDateTime.now());
|
||||
logger.info("删除作品成功: {}, 用户: {}", workId, username);
|
||||
|
||||
// 如果是分镜图作品(task_id 以 _image 结尾),同时删除对应的分镜视频作品
|
||||
String taskId = work.getTaskId();
|
||||
if (taskId != null && taskId.endsWith("_image")) {
|
||||
String videoTaskId = taskId.replace("_image", "");
|
||||
Optional<UserWork> videoWorkOpt = userWorkRepository.findByTaskId(videoTaskId);
|
||||
if (videoWorkOpt.isPresent()) {
|
||||
UserWork videoWork = videoWorkOpt.get();
|
||||
userWorkRepository.softDeleteWork(videoWork.getId(), username, LocalDateTime.now());
|
||||
logger.info("同时删除分镜视频作品: {}, taskId: {}", videoWork.getId(), videoTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是分镜视频作品,同时删除对应的分镜图作品
|
||||
if (taskId != null && work.getWorkType() == UserWork.WorkType.STORYBOARD_VIDEO) {
|
||||
String imageTaskId = taskId + "_image";
|
||||
Optional<UserWork> imageWorkOpt = userWorkRepository.findByTaskId(imageTaskId);
|
||||
if (imageWorkOpt.isPresent()) {
|
||||
UserWork imageWork = imageWorkOpt.get();
|
||||
userWorkRepository.softDeleteWork(imageWork.getId(), username, LocalDateTime.now());
|
||||
logger.info("同时删除分镜图作品: {}, taskId: {}", imageWork.getId(), imageTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
return result > 0;
|
||||
}
|
||||
|
||||
@@ -684,6 +757,27 @@ public class UserWorkService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新作品状态和结果URL(级联更新时使用)
|
||||
*/
|
||||
@Transactional
|
||||
public void updateWorkStatusWithResult(String taskId, UserWork.WorkStatus status, String resultUrl) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime completedAt = (status == UserWork.WorkStatus.COMPLETED || status == UserWork.WorkStatus.FAILED) ? now : null;
|
||||
|
||||
// 先更新状态
|
||||
int result = userWorkRepository.updateStatusByTaskId(taskId, status, now, completedAt);
|
||||
|
||||
// 如果有结果URL且状态为完成,同时更新URL
|
||||
if (resultUrl != null && !resultUrl.isEmpty() && status == UserWork.WorkStatus.COMPLETED) {
|
||||
userWorkRepository.updateResultUrlByTaskId(taskId, resultUrl, now);
|
||||
}
|
||||
|
||||
if (result > 0) {
|
||||
logger.info("更新作品状态和结果成功: taskId={}, status={}, hasResultUrl={}", taskId, status, resultUrl != null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新作品结果URL
|
||||
*/
|
||||
|
||||
@@ -21,10 +21,10 @@ spring.h2.console.enabled=false
|
||||
# DB_PASSWORD=your_secure_password_here
|
||||
# ============================================
|
||||
|
||||
spring.datasource.url=jdbc:mysql://43.156.12.172:3306/aigc_platform?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.url=jdbc:mysql://localhost:3306/aigc_platform?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.username=aigc_platform
|
||||
spring.datasource.password=jRbHPZbbkdm24yTT
|
||||
spring.datasource.username=root
|
||||
spring.datasource.password=177615
|
||||
|
||||
# 数据库连接池配置 (生产环境 - 支持50人并发)
|
||||
spring.datasource.hikari.maximum-pool-size=30
|
||||
@@ -41,57 +41,75 @@ spring.datasource.hikari.connection-test-query=SELECT 1
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.properties.hibernate.format_sql=false
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
|
||||
|
||||
# 禁用 SQL 脚本自动运行
|
||||
spring.sql.init.mode=never
|
||||
spring.sql.init.continue-on-error=true
|
||||
|
||||
# Thymeleaf 可启用缓存
|
||||
spring.thymeleaf.cache=true
|
||||
|
||||
# AI API配置 (生产环境)
|
||||
# 文生视频、图生视频、分镜视频都使用Comfly API
|
||||
ai.api.base-url=${AI_API_BASE_URL:https://ai.comfly.chat}
|
||||
ai.api.key=${AI_API_KEY}
|
||||
ai.api.base-url=https://ai.comfly.chat
|
||||
ai.api.key=sk-J9A9c7rr7Y2suarAudmLG1J722ozIIHOweIhsI8QXX68sjMW
|
||||
# 文生图使用Comfly API
|
||||
ai.image.api.base-url=${AI_IMAGE_API_BASE_URL:https://ai.comfly.chat}
|
||||
ai.image.api.key=${AI_IMAGE_API_KEY}
|
||||
ai.image.api.base-url=https://ai.comfly.chat
|
||||
ai.image.api.key=sk-J9A9c7rr7Y2suarAudmLG1J722ozIIHOweIhsI8QXX68sjMW
|
||||
|
||||
# 支付宝配置 (生产环境)
|
||||
alipay.app-id=${ALIPAY_APP_ID}
|
||||
alipay.private-key=${ALIPAY_PRIVATE_KEY}
|
||||
alipay.public-key=${ALIPAY_PUBLIC_KEY}
|
||||
alipay.app-id=2021006103624219
|
||||
alipay.private-key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCFsCSu7FVwLVCsqbZAaxv0jVrIErE45aYahKXutFeDOs7IWvUOzugL3RKMsh5Ndx0mNO6nbcL4AxEFCa4EfZIMgyyCeFnG29o0E8N7zpxH0VAES92yLpwQZHV2M1LraMsfhW7Hk9I0EkC+cElUEMBQL0LrcdjfZpspIC1utKQPpepSRZ5GpYADtgnyu+B6aOSmZk5j6+lZmn2K06H8PMZjop029uN4HfSNBNdl1NIBTEs5Kk4hw/PQm5KmC5u0CkKgmqGXt6hV4zRb3USrEYLGBvLVpCkXzpQXWTACWxy3qqVeE5OiQWkbpWIhhWqybq0gmmOJCv4cusdutEkiFdgfAgMBAAECggEAeaUCbAxt3anOG549ULZldIvey+h+S+hi0QRcPCzq6GTtXU+uZnAMoybgxxcYDaLR6j8F3WE5pBSeOvhI2Jst9qaxLHK4NgM8tGA7Yv9oIs0pww8JRiW1KhFO9GPVEpGDKkZuu7kc7vag5OglQRIQ+6VVfglUrkqd6rj1viMumXEgRKAken+g39lC7/pzkS+6J4/hpD55XZ1jEJq9mj0DTOozlbImg8y1RiyZ/Te3RVsvmF1EgggA7Z5R8+/HvFlh3KZdWWfZHvzYeu4DWJhJu1RNmskCfMIF07O1wkl1+RrrGIDTtWtVN7/Gayirx3w40LmOUrb6FSTfvUsPKN03OQKBgQD0euD2tc4CXtfQYddnvWAZXvhVPteScgigRqB9xqyv0lwASZiH313Mbf0JdQId+7glmyleXPwvDpKzfs2yl9ZUCn5LtFtZkyGMeXf39hTpSyeFf2dLVJd1VFAM53GjMXw5kWi6OUL5lFewE0rLkIqr2oYF5UwlZIDWXaNwmXAQzQKBgQCL/MzKuNsMS733/8XTjopr+HYI0nLgKopclgT7p3BJw+VWY/8BuXmA4dpAZBBIersMtRDa9acxrOUtiEwknj5fGF/NmOqGlRamP+2Gna1+DdqRWqgmSuMAiEJKiSCsCVxXdktHfP6UMa1FwhxaOvLNKGnx7dGRofAKGi9RwfpcmwKBgAEW2xG+VaClE4kWJoOL0HXMeobGtOcuIuOz7Nsim3pdEZPewBM654wVoV79anj/uh5Qxqpo96auBfFOy1PUYVwWf+GOeCm6AhhCIkq0iftQHmj13Fv1kIcxTPoBvfvgKJGJGFJcFvRNuOZL77Vge32wh5BXKTOxcvGBkUzbIiixAoGAO31sXn5egIQzsA/fNz+tLaNCLg+ZSBBsClqqtXN7sa1xadxHA6mZrB7PDGw5y0N0+Dp+dj7NFbw/DLGOgkVJhkoqdIoWqKj1HiOuwnWBxD8I8pqPOO68N36whVJvMw0rU/Pum+vPmJTf6PRL7kB87JjPJUQGupgSFYj5MQp5Zh8CgYEAvNGL16QNKdAHadefwfZaGKNc9Yw66BsKEoG2HshjD2BBUABwmrF2DFB6n5erBChyQM9t2DBnxy4py7zAIArxFWhuDRDvP66D3kZR10r7PuIFzlBomEQDQ8JBHW1m4JEfpR3xlNGqemKsWTliKYMx3kHATAsq+XHw/anMGQq1J1c=
|
||||
alipay.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlVXTDpQUaXKlYY3980lSH/4O6p/dSu71upk8SF9FKB5FCJgDgoOiIm4QJdAF1JSXNj11Q0CE+JKCc3e0Dq1Scc4pPL93SBGbGckukddQbRBLmblKtkTBnFc4zxakE2moJuGVoWthQIj6nJ2Y+Q63W8jb3mQPCxKLMhlDcWvgcx9zjr+ueWIP2OBEtOv8a4AIrNujG74CgRg7SEfxTo3LHicbr1hCX3w0BBDNiEH1pCsHxBNSsBqMmPwb3klj+/huleyhUPa2wv0EZsaZvKpp9omaLfec5myrfMXa5xxXyLAX+yT8GvkEk85U6pQ1L3VGFQQ+ExnjEQgzZDJ1+huOAwIDAQAB
|
||||
alipay.server-url=https://openapi.alipay.com/gateway.do
|
||||
alipay.gateway-url=https://openapi.alipay.com/gateway.do
|
||||
alipay.charset=UTF-8
|
||||
alipay.sign-type=RSA2
|
||||
alipay.domain=${ALIPAY_DOMAIN:https://vionow.com}
|
||||
alipay.notify-url=${ALIPAY_NOTIFY_URL:https://vionow.com/api/payments/alipay/notify}
|
||||
alipay.return-url=${ALIPAY_RETURN_URL:https://vionow.com/payment/success}
|
||||
alipay.domain=https://vionow.com
|
||||
alipay.notify-url=https://vionow.com/api/payments/alipay/notify
|
||||
alipay.return-url=https://vionow.com/payment/success
|
||||
|
||||
# JWT配置 - 使用环境变量
|
||||
jwt.secret=${JWT_SECRET}
|
||||
jwt.expiration=${JWT_EXPIRATION:604800000}
|
||||
# JWT配置
|
||||
jwt.secret=mySecretKey123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
jwt.expiration=604800000
|
||||
|
||||
# 腾讯云SES配置 (生产环境)
|
||||
tencent.ses.secret-id=${TENCENT_SES_SECRET_ID}
|
||||
tencent.ses.secret-key=${TENCENT_SES_SECRET_KEY}
|
||||
tencent.ses.secret-id=AKIDoaEjFbqxxqZAcv8EE6oZCg2IQPG1fCxm
|
||||
tencent.ses.secret-key=nR83I79FOSpGcqNo7JXkqnU8g7SjsxuG
|
||||
tencent.ses.region=ap-hongkong
|
||||
tencent.ses.from-email=${TENCENT_SES_FROM_EMAIL}
|
||||
tencent.ses.from-email=newletter@vionow.com
|
||||
tencent.ses.from-name=AIGC平台
|
||||
# 邮件模板ID(在腾讯云SES控制台创建模板后获取)
|
||||
# 如果未配置或为0,将使用开发模式(仅记录日志)
|
||||
tencent.ses.template-id=${TENCENT_SES_TEMPLATE_ID}
|
||||
tencent.ses.template-id=154360
|
||||
|
||||
# PayPal配置 (生产环境)
|
||||
paypal.client-id=Adpi67TvppjhyyWhrALWwJhLFzv5S_vXoUHzWQchqZe48NaONSryg7QHKBubf0PRmkeJoaxGEKV5v9lT
|
||||
paypal.client-secret=EDzZl-hddwtt2pNt5RpBIICdlrUS8QtcmAttU_kuANL8Vd937SC4xel_K2hArTovVqEtyL2ZS5IcQcQV
|
||||
paypal.mode=sandbox
|
||||
paypal.success-url=https://vionow.com/api/payment/paypal/success
|
||||
paypal.cancel-url=https://vionow.com/api/payment/paypal/cancel
|
||||
|
||||
# Tomcat线程池配置 (生产环境 - 支持50人并发)
|
||||
server.port=8080
|
||||
server.tomcat.threads.max=150
|
||||
server.tomcat.threads.min-spare=20
|
||||
server.tomcat.max-connections=500
|
||||
server.tomcat.accept-count=100
|
||||
server.tomcat.connection-timeout=20000
|
||||
server.tomcat.max-http-post-size=600MB
|
||||
|
||||
# 文件上传配置
|
||||
spring.servlet.multipart.enabled=true
|
||||
spring.servlet.multipart.max-file-size=500MB
|
||||
spring.servlet.multipart.max-request-size=600MB
|
||||
|
||||
# 生产环境日志配置
|
||||
logging.level.root=INFO
|
||||
logging.level.com.example.demo=INFO
|
||||
logging.level.com.example.demo.scheduler=WARN
|
||||
logging.level.com.example.demo.scheduler.OrderScheduler=WARN
|
||||
logging.level.org.springframework.security=WARN
|
||||
logging.level.org.springframework.scheduling=WARN
|
||||
# 关闭 Hibernate SQL 日志
|
||||
logging.level.org.hibernate.SQL=WARN
|
||||
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=WARN
|
||||
@@ -114,40 +132,42 @@ app.ffmpeg.path=${FFMPEG_PATH:ffmpeg}
|
||||
app.upload.path=${UPLOAD_PATH:./uploads}
|
||||
|
||||
# SpringDoc OpenAPI (Swagger) 配置
|
||||
# 生产环境建议禁用或限制访问
|
||||
springdoc.api-docs.path=/v3/api-docs
|
||||
springdoc.swagger-ui.path=/swagger-ui.html
|
||||
springdoc.swagger-ui.enabled=true
|
||||
springdoc.swagger-ui.operationsSorter=method
|
||||
springdoc.swagger-ui.tagsSorter=alpha
|
||||
springdoc.swagger-ui.tryItOutEnabled=true
|
||||
springdoc.swagger-ui.filter=true
|
||||
springdoc.swagger-ui.display-request-duration=true
|
||||
springdoc.swagger-ui.doc-expansion=none
|
||||
# 生产环境禁用以提高安全性和性能
|
||||
springdoc.api-docs.enabled=false
|
||||
springdoc.swagger-ui.enabled=false
|
||||
|
||||
# ============================================
|
||||
# 腾讯云 Redis 配置(生产环境)
|
||||
# Redis 配置(生产环境 - 已禁用)
|
||||
# ============================================
|
||||
# 腾讯云 Redis 内网地址(在云数据库 Redis 控制台查看)
|
||||
spring.data.redis.host=crs-xxxxxxxx.sql.tencentcdb.com
|
||||
spring.data.redis.port=6379
|
||||
# Redis 密码(格式可能是:账号:密码 或 仅密码,取决于是否开启免密)
|
||||
spring.data.redis.password=你的Redis密码
|
||||
# 不使用 Redis,Token 存储依赖 JWT 本身的验证
|
||||
# 如需启用 Redis,设置 redis.enabled=true 并配置连接信息
|
||||
redis.enabled=false
|
||||
|
||||
spring.data.redis.database=0
|
||||
# spring.data.redis.host=your-redis-host
|
||||
# spring.data.redis.port=6379
|
||||
# spring.data.redis.password=your-redis-password
|
||||
# spring.data.redis.database=0
|
||||
|
||||
# 连接池配置
|
||||
spring.data.redis.lettuce.pool.max-active=16
|
||||
spring.data.redis.lettuce.pool.max-idle=8
|
||||
spring.data.redis.lettuce.pool.min-idle=2
|
||||
spring.data.redis.lettuce.pool.max-wait=3000ms
|
||||
|
||||
# 连接超时
|
||||
spring.data.redis.timeout=5000ms
|
||||
spring.data.redis.connect-timeout=5000ms
|
||||
# 禁用 Redis 自动配置
|
||||
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
|
||||
|
||||
# Token过期时间(秒)
|
||||
redis.token.expire-seconds=86400
|
||||
|
||||
# ============================================
|
||||
# 腾讯云COS对象存储配置 (生产环境)
|
||||
# ============================================
|
||||
tencent.cos.enabled=true
|
||||
# 腾讯云SecretId
|
||||
tencent.cos.secret-id=AKIDeLqCNODrKafXSAqPrRtCSp9NRwU0Ok5G
|
||||
# 腾讯云SecretKey
|
||||
tencent.cos.secret-key=4uZ1Hcu0xiHiy1ucAYnsoZ8WhqqlW5RZ
|
||||
# COS区域
|
||||
tencent.cos.region=ap-hongkong
|
||||
# COS存储桶名称
|
||||
tencent.cos.bucket-name=aigc-1393834230
|
||||
# COS文件夹前缀
|
||||
tencent.cos.prefix=aigc
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
spring.application.name=demo
|
||||
spring.messages.basename=messages
|
||||
spring.thymeleaf.cache=false
|
||||
spring.profiles.active=dev
|
||||
spring.profiles.active=prod
|
||||
|
||||
# 服务器配置
|
||||
server.address=0.0.0.0
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
UPDATE users
|
||||
SET role = 'ROLE_SUPER_ADMIN',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE email = '984523799@qq.com';
|
||||
WHERE email = 'shanghairuiyi2026@163.com';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- V14: 为user_works表添加uploaded_images字段
|
||||
-- 用于存储用户上传的参考图片,支持"做同款"功能恢复原始参考图
|
||||
|
||||
ALTER TABLE user_works
|
||||
ADD COLUMN IF NOT EXISTS uploaded_images LONGTEXT COMMENT '用户上传的参考图片(JSON数组),用于做同款功能恢复';
|
||||
@@ -1,15 +0,0 @@
|
||||
# 支付配置
|
||||
# 支付宝沙箱配置
|
||||
alipay.app-id=9021000157616562
|
||||
alipay.private-key=MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCH7wPeptkJlJuoKwDqxvfJJLTOAWVkHa/TLh+wiy1tEtmwcrOwEU3GuqfkUlhij71WJIZi8KBytCwbax1QGZA/oLXvTCGJJrYrsEL624X5gGCCPKWwHRDhewsQ5W8jFxaaMXxth8GKlSW61PZD2cOQClRVEm2xnWFZ+6/7WBI7082g7ayzGCD2eowXsJyWyuEBCUSbHXkSgxVhqj5wUGIXhr8ly+pdUlJmDX5K8UG2rjJYx+0AU5UZJbOAND7d3iyDsOulHDvth50t8MOWDnDCVJ2aAgUB5FZKtOFxOmzNTsMjvzYldFztF0khbypeeMVL2cxgioIgTvjBkUwd55hZAgMBAAECggEAUjk3pARUoEDt7skkYt87nsW/QCUECY0Tf7AUpxtovON8Hgkju8qbuyvIxokwwV2k72hkiZB33Soyy9r8/iiYYoR5yGfKmUV7R+30df03ivYmamD48BCE138v8GZ31Ufv+hEY7MADSCpzihGrbNtaOdSlslfVVmyWKHHfvy9EyD6yHJGYswLpHXC/QX1TuLRRxk6Uup8qENOG/6zjGWMfxoRZFwTt80ml1mKy32YZGyJqDaQpJcdYwAHOPcnJl1emw4E+oVjiLyksl643npuTkgnZXs1iWcWSS8ojF1w/0kVDzcNh9toLg+HDuQlIHOis01VQ7lYcG4oiMOnhX1QHIQKBgQC9fgBuILjBhuCI9fHvLRdzoNC9heD54YK7xGvEV/mv90k8xcmNx+Yg5C57ASaMRtOq3b7muPiCv5wOtMT4tUCcMIwSrTNlcBM6EoTagnaGfpzOMaHGMXO4vbaw+MIynHnvXFj1rZjG1lzkV/9K36LAaHD9ZKVJaBQ9mK+0CIq/3QKBgQC3pL5GbvXj6/4ahTraXzNDQQpPGVgbHxcOioEXL4ibaOPC58puTW8HDbRvVuhl/4EEOBRVX81BSgkN8XHwTSiZdih2iOqByg+o9kixs7nlFn3Iw9BBP2/g+Wqiyi2N+9g17kfWXXVOKYz/eMXLBeOo4KhQE9wqNGyZldYzX2ywrQKBgApJmvBfqmgnUG1fHOFlS06lvm9ro0ktqxFSmp8wP4gEHt/DxSuDXMUQXk2jRFp9ReSS4VhZVnSSvoA15DO0c2uHXzNsX8v0B7cxZjEOwCyRFyZCn4vJB4VSF2cIOlLRF/Wcx9+eqxqwbJ6hAGUqOwXDJc879ZVEp0So03EsvYupAoGAAnI+Wp/VxLB7FQ1bSFdmTmoKYh1bUBks7HOp3o4yiqduCUWfK7L6XKSxF56Xv+wUYuMAWlbJXCpJTpc9xk6w0MKDLXkLbqkrZjvJohxbyJJxIICDQKtAqUWJRxvcWXzWV3mSGWfrTRw+lZSdReQRMUm01EQ/dYx3OeCGFu8Zeo0CgYAlH5YSYdJxZSoDCJeoTrkxUlFoOg8UQ7SrsaLYLwpwcwpuiWJaTrg6jwFocj+XhjQ9RtRbSBHz2wKSLdl+pXbTbqECKk85zMFl6zG3etXtTJU/dD750Ty4i8zt3+JGhvglPrQBY1CfItgml2oXa/VUVMnLCUS0WSZuPRmPYZD8dg==
|
||||
alipay.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAksEwzuR3ASrKtTzaANqdQYKOoD44itA1TWG/6onvQr8PHNEMgcguLuJNrdeuT2PDg23byzZ9qKfEM2D5U4zbpt0/uCYLfZQyAnAWWyMvnKPoSIgrtBjnxYK6HE6fuQV3geJTcZxvP/z8dGZB0V0s6a53rzbKSLh0p4w0hWfVXlQihq3Xh4vSKB+ojdhEkIblhpWPT42NPbjVNdwPzIhUGpRy3/nsgNqVBu+ZacQ5/rCvzXU1RE0allBbjcvjymKQTS7bAE0i1Mgo1eX8njvElsfQUv5P7xQdrvZagqtIuTdP19cmsSNGdIC9Z5Po3j0z3KWPR7MrKgDuJfzkWtJR4wIDAQAB
|
||||
alipay.server-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do
|
||||
alipay.gateway-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do
|
||||
alipay.domain=https://vionow.com
|
||||
alipay.charset=UTF-8
|
||||
alipay.sign-type=RSA2
|
||||
alipay.notify-url=https://vionow.com/api/payments/alipay/notify
|
||||
alipay.return-url=https://vionow.com/api/payments/alipay/return
|
||||
alipay.app-cert-path=classpath:cert/alipay/appCertPublicKey.crt
|
||||
alipay.ali-pay-cert-path=classpath:cert/alipay/alipayCertPublicKey_RSA2.crt
|
||||
alipay.ali-pay-root-cert-path=classpath:cert/alipay/alipayRootCert.crt
|
||||
Reference in New Issue
Block a user