perf(backend+frontend): 列表API响应体积优化 3.1MB→145KB (↓95.4%)

- 后端: JPQL构造器投影排除LONGTEXT大字段(uploadedImages/videoReferenceImages)
- 后端: DTO层过滤非分镜图类型的base64内联resultUrl
- 前端: 列表缩略图从video改为img loading=lazy,消除172并发请求
- 前端: download函数增加resultUrl懒加载(详情接口兜底)
- 文档: 新增性能优化报告 docs/performance-optimization-report.md
This commit is contained in:
blandarebiter
2026-04-10 18:46:37 +08:00
commit 90b5118e45
280 changed files with 92468 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(exclude = {RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class})
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
// 在应用启动时立即设置HTTP超时时间支付宝API调用可能需要更长时间
// 连接超时30秒读取超时120秒
// 必须在SpringApplication.run()之前设置确保在所有HTTP客户端创建之前生效
System.setProperty("sun.net.client.defaultConnectTimeout", "30000");
System.setProperty("sun.net.client.defaultReadTimeout", "120000");
// 增加HTTP缓冲区大小以支持大请求体Base64编码的图片可能很大
// 设置Socket缓冲区大小为10MB
System.setProperty("java.net.preferIPv4Stack", "true");
// Apache HttpClient 使用系统属性
System.setProperty("org.apache.http.client.connection.timeout", "30000");
System.setProperty("org.apache.http.socket.timeout", "300000");
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,46 @@
package com.example.demo.config;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* 异步执行器配置
* 支持100-200人并发处理异步任务如视频生成、图片处理等
*/
@Configuration
@EnableAsync
public class AsyncConfig {
/**
* 配置异步任务执行器
* 核心线程数5最大线程数20队列容量50
* 可支持50人并发每个用户最多3个任务共150个任务
* 大部分任务在队列中等待,实际并发执行的任务数量受线程池限制
*/
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:保持活跃的最小线程数
executor.setCorePoolSize(10);
// 最大线程数:最大并发执行的任务数
executor.setMaxPoolSize(40);
// 队列容量:等待执行的任务数
executor.setQueueCapacity(100);
// 线程名前缀
executor.setThreadNamePrefix("async-task-");
// 拒绝策略:当线程池和队列都满时,使用调用者线程执行(保证任务不丢失)
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务完成后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间(秒)
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,49 @@
package com.example.demo.config;
import java.util.concurrent.TimeUnit;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.benmanes.caffeine.cache.Caffeine;
/**
* 缓存配置类
* 使用 Caffeine 作为本地缓存实现,提高系统性能
*/
@Configuration
@EnableCaching
public class CacheConfig {
/**
* 缓存名称常量
*/
public static final String USER_CACHE = "userCache";
public static final String USER_POINTS_CACHE = "userPointsCache";
public static final String USER_WORK_STATS_CACHE = "userWorkStatsCache";
public static final String SYSTEM_CONFIG_CACHE = "systemConfigCache";
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 默认缓存配置最大1000条5分钟过期
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()); // 记录统计信息
// 注册缓存名称
cacheManager.setCacheNames(java.util.Arrays.asList(
USER_CACHE,
USER_POINTS_CACHE,
USER_WORK_STATS_CACHE,
SYSTEM_CONFIG_CACHE
));
return cacheManager;
}
}

View File

@@ -0,0 +1,69 @@
package com.example.demo.config;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.region.Region;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 腾讯云COS配置类
*/
@Configuration
public class CosConfig {
@Value("${tencent.cos.secret-id:}")
private String secretId;
@Value("${tencent.cos.secret-key:}")
private String secretKey;
@Value("${tencent.cos.region:ap-guangzhou}")
private String region;
@Value("${tencent.cos.bucket-name:}")
private String bucketName;
@Value("${tencent.cos.enabled:false}")
private boolean enabled;
@Value("${tencent.cos.prefix:}")
private String prefix;
@Bean
public COSClient cosClient() {
if (!enabled || secretId.isEmpty() || secretKey.isEmpty()) {
// 如果未配置COS返回null服务层需要处理这种情况
return null;
}
// 1 初始化用户身份信息secretId, secretKey
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
// 2 设置bucket的区域
Region regionObj = new Region(region);
ClientConfig clientConfig = new ClientConfig(regionObj);
// 3 生成cos客户端
return new COSClient(cred, clientConfig);
}
public String getBucketName() {
return bucketName;
}
public String getRegion() {
return region;
}
public boolean isEnabled() {
return enabled;
}
public String getPrefix() {
return prefix;
}
}

View File

@@ -0,0 +1,94 @@
package com.example.demo.config;
import com.example.demo.model.MembershipLevel;
import com.example.demo.repository.MembershipLevelRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Optional;
@Component
public class DataInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(DataInitializer.class);
private final MembershipLevelRepository membershipLevelRepository;
public DataInitializer(MembershipLevelRepository membershipLevelRepository) {
this.membershipLevelRepository = membershipLevelRepository;
}
@Override
public void run(String... args) throws Exception {
initMembershipLevels();
}
private void initMembershipLevels() {
// 兼容旧数据:如果 free 的展示名仍是“免费版”,启动时自动迁移到“入门版”
try {
Optional<MembershipLevel> freeOpt = membershipLevelRepository.findByName("free");
if (freeOpt.isPresent()) {
MembershipLevel freeLevel = freeOpt.get();
if (freeLevel.getDisplayName() == null || "免费版".equals(freeLevel.getDisplayName())) {
freeLevel.setDisplayName("入门版");
if (freeLevel.getDescription() == null || "免费用户".equals(freeLevel.getDescription())) {
freeLevel.setDescription("入门版用户");
}
freeLevel.setUpdatedAt(LocalDateTime.now());
membershipLevelRepository.save(freeLevel);
logger.info("✅ 已迁移会员等级 free 的展示名为入门版");
}
}
} catch (Exception e) {
logger.warn("迁移 free 会员等级展示名失败,将继续执行初始化逻辑: {}", e.getMessage());
}
if (membershipLevelRepository.count() == 0) {
logger.info("初始化会员等级数据...");
MembershipLevel free = new MembershipLevel();
free.setName("free");
// 入门级套餐在后端仍沿用标记 free仅调整对外展示名称
free.setDisplayName("入门版");
free.setPrice(0.0);
free.setDurationDays(0);
free.setPointsBonus(0);
free.setDescription("入门版用户");
free.setIsActive(true);
free.setCreatedAt(LocalDateTime.now());
free.setUpdatedAt(LocalDateTime.now());
membershipLevelRepository.save(free);
MembershipLevel standard = new MembershipLevel();
standard.setName("standard");
standard.setDisplayName("标准会员");
standard.setPrice(298.0);
standard.setDurationDays(365);
standard.setPointsBonus(6000);
standard.setDescription("标准会员 - 每年6000积分");
standard.setIsActive(true);
standard.setCreatedAt(LocalDateTime.now());
standard.setUpdatedAt(LocalDateTime.now());
membershipLevelRepository.save(standard);
MembershipLevel professional = new MembershipLevel();
professional.setName("professional");
professional.setDisplayName("专业版");
professional.setPrice(398.0);
professional.setDurationDays(365);
professional.setPointsBonus(12000);
professional.setDescription("专业会员 - 每年12000积分");
professional.setIsActive(true);
professional.setCreatedAt(LocalDateTime.now());
professional.setUpdatedAt(LocalDateTime.now());
membershipLevelRepository.save(professional);
logger.info("✅ 会员等级数据初始化完成,共{}条", membershipLevelRepository.count());
} else {
logger.info("会员等级数据已存在,跳过初始化");
}
}
}

View File

@@ -0,0 +1,153 @@
package com.example.demo.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.example.demo.model.SystemSettings;
import com.example.demo.repository.SystemSettingsRepository;
import jakarta.annotation.PostConstruct;
/**
* 动态API配置管理器
* 优先从数据库加载配置,支持运行时更新,无需重启应用
* 注意:视频生成和图片生成使用同一个 API Key
*/
@Component
public class DynamicApiConfig {
private static final Logger logger = LoggerFactory.getLogger(DynamicApiConfig.class);
@Autowired
private SystemSettingsRepository systemSettingsRepository;
// 从配置文件读取的默认值(作为兜底)
@Value("${ai.api.key:}")
private String defaultApiKey;
@Value("${ai.api.base-url:https://ai.comfly.chat}")
private String defaultApiBaseUrl;
// 运行时配置(优先级最高,从数据库加载或动态更新)
private volatile String runtimeApiKey = null;
private volatile String runtimeApiBaseUrl = null;
/**
* 应用启动时从数据库加载配置
*/
@PostConstruct
public void loadFromDatabase() {
try {
SystemSettings settings = systemSettingsRepository.findById(1L).orElse(null);
if (settings != null) {
if (settings.getAiApiKey() != null && !settings.getAiApiKey().isEmpty()) {
this.runtimeApiKey = settings.getAiApiKey();
logger.info("✅ 从数据库加载 AI API Key: {}****", settings.getAiApiKey().substring(0, Math.min(4, settings.getAiApiKey().length())));
}
if (settings.getAiApiBaseUrl() != null && !settings.getAiApiBaseUrl().isEmpty()) {
this.runtimeApiBaseUrl = settings.getAiApiBaseUrl();
logger.info("✅ 从数据库加载 AI API Base URL: {}", settings.getAiApiBaseUrl());
}
}
logger.info("API配置加载完成: apiKey={}, apiBaseUrl={}", maskApiKey(getApiKey()), getApiBaseUrl());
} catch (Exception e) {
logger.warn("从数据库加载API配置失败使用配置文件默认值: {}", e.getMessage());
}
}
/**
* 获取当前有效的AI API密钥视频和图片生成共用
* 优先级:运行时配置 > 配置文件默认值
*/
public String getApiKey() {
return runtimeApiKey != null ? runtimeApiKey : defaultApiKey;
}
/**
* 获取当前有效的AI API基础URL
*/
public String getApiBaseUrl() {
return runtimeApiBaseUrl != null ? runtimeApiBaseUrl : defaultApiBaseUrl;
}
/**
* 获取图片API密钥与视频API共用同一个Key
*/
public String getImageApiKey() {
return getApiKey();
}
/**
* 获取图片API基础URL与视频API共用同一个URL
*/
public String getImageApiBaseUrl() {
return getApiBaseUrl();
}
/**
* 动态更新API密钥立即生效无需重启
*/
public synchronized void updateApiKey(String newApiKey) {
if (newApiKey != null && !newApiKey.trim().isEmpty()) {
this.runtimeApiKey = newApiKey.trim();
logger.info("✅ API密钥已动态更新立即生效无需重启");
}
}
/**
* 动态更新API基础URL立即生效无需重启
*/
public synchronized void updateApiBaseUrl(String newBaseUrl) {
if (newBaseUrl != null && !newBaseUrl.trim().isEmpty()) {
this.runtimeApiBaseUrl = newBaseUrl.trim();
logger.info("✅ API基础URL已动态更新立即生效无需重启");
}
}
/**
* 动态更新图片API密钥与视频API共用调用 updateApiKey
*/
public synchronized void updateImageApiKey(String newApiKey) {
updateApiKey(newApiKey);
}
/**
* 动态更新图片API基础URL与视频API共用调用 updateApiBaseUrl
*/
public synchronized void updateImageApiBaseUrl(String newBaseUrl) {
updateApiBaseUrl(newBaseUrl);
}
/**
* 重置为配置文件的初始值
*/
public synchronized void reset() {
this.runtimeApiKey = null;
this.runtimeApiBaseUrl = null;
logger.info("⚠️ API配置已重置为配置文件的初始值");
}
/**
* 获取配置状态信息
*/
public java.util.Map<String, Object> getConfigStatus() {
java.util.Map<String, Object> status = new java.util.HashMap<>();
status.put("apiKey", maskApiKey(getApiKey()));
status.put("apiBaseUrl", getApiBaseUrl());
status.put("usingRuntimeConfig", runtimeApiKey != null || runtimeApiBaseUrl != null);
return status;
}
/**
* 掩码API密钥只显示前4位和后4位
*/
private String maskApiKey(String apiKey) {
if (apiKey == null || apiKey.length() <= 8) {
return "****";
}
return apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4);
}
}

View File

@@ -0,0 +1,35 @@
package com.example.demo.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class JacksonConfig {
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 注册Hibernate模块来处理代理对象
mapper.registerModule(new Hibernate5JakartaModule());
// 注册Java时间模块
mapper.registerModule(new JavaTimeModule());
// 禁用将日期写为时间戳
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 配置Hibernate模块
Hibernate5JakartaModule hibernateModule = new Hibernate5JakartaModule();
hibernateModule.disable(Hibernate5JakartaModule.Feature.USE_TRANSIENT_ANNOTATION);
mapper.registerModule(hibernateModule);
return mapper;
}
}

View File

@@ -0,0 +1,188 @@
package com.example.demo.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
/**
* OpenAPI (Swagger) 配置类
* 访问地址: http://localhost:8080/swagger-ui.html
* API文档JSON: http://localhost:8080/v3/api-docs
*/
@Configuration
public class OpenApiConfig {
/**
* OpenAPI 主配置
*/
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("AIGC平台 API 文档")
.version("1.0.0")
.description("AIGC平台后端API接口文档包含用户认证、视频生成、支付、订单管理等模块")
.contact(new Contact()
.name("AIGC平台开发团队")
.email("support@vionow.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")))
.servers(Arrays.asList(
new Server().url("http://localhost:8080").description("本地开发环境"),
new Server().url("http://172.22.0.1:8080").description("内网环境"),
new Server().url("https://vionow.com").description("生产环境")
))
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes("Bearer Authentication", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT认证格式: Bearer {token}")));
}
/**
* 所有API分组
*/
@Bean
public GroupedOpenApi allApis() {
return GroupedOpenApi.builder()
.group("all")
.displayName("所有API")
.pathsToMatch("/api/**")
.build();
}
/**
* 认证相关API
*/
@Bean
public GroupedOpenApi authApis() {
return GroupedOpenApi.builder()
.group("auth")
.displayName("认证授权")
.pathsToMatch("/api/auth/**", "/api/verification/**")
.build();
}
/**
* 用户作品API
*/
@Bean
public GroupedOpenApi userWorkApis() {
return GroupedOpenApi.builder()
.group("user-works")
.displayName("用户作品")
.pathsToMatch("/api/works/**")
.build();
}
/**
* 视频生成API
*/
@Bean
public GroupedOpenApi videoGenerationApis() {
return GroupedOpenApi.builder()
.group("video-generation")
.displayName("视频生成")
.pathsToMatch("/api/text-to-video/**", "/api/image-to-video/**", "/api/storyboard-video/**")
.build();
}
/**
* 支付相关API
*/
@Bean
public GroupedOpenApi paymentApis() {
return GroupedOpenApi.builder()
.group("payment")
.displayName("支付管理")
.pathsToMatch("/api/payments/**", "/api/orders/**")
.build();
}
/**
* 会员管理API
*/
@Bean
public GroupedOpenApi memberApis() {
return GroupedOpenApi.builder()
.group("member")
.displayName("会员管理")
.pathsToMatch("/api/members/**", "/api/subscription/**")
.build();
}
/**
* 积分系统API
*/
@Bean
public GroupedOpenApi pointsApis() {
return GroupedOpenApi.builder()
.group("points")
.displayName("积分系统")
.pathsToMatch("/api/points/**")
.build();
}
/**
* 任务管理API
*/
@Bean
public GroupedOpenApi taskApis() {
return GroupedOpenApi.builder()
.group("tasks")
.displayName("任务管理")
.pathsToMatch("/api/tasks/**", "/api/queue/**", "/api/task-status/**")
.build();
}
/**
* 管理后台API
* 注意这些API需要ADMIN权限但文档生成不需要权限验证
*/
@Bean
public GroupedOpenApi adminApis() {
return GroupedOpenApi.builder()
.group("admin")
.displayName("管理后台")
.pathsToMatch("/api/admin/**", "/api/dashboard/**", "/api/analytics/**", "/api/cleanup/**")
// 在文档生成时,不验证权限
.build();
}
/**
* 系统设置API
*/
@Bean
public GroupedOpenApi settingsApis() {
return GroupedOpenApi.builder()
.group("settings")
.displayName("系统设置")
.pathsToMatch("/api/settings/**", "/api/api-keys/**")
.build();
}
/**
* 公共API无需认证
*/
@Bean
public GroupedOpenApi publicApis() {
return GroupedOpenApi.builder()
.group("public")
.displayName("公共接口")
.pathsToMatch("/api/public/**", "/api/test/**")
.build();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,117 @@
package com.example.demo.config;
import com.paypal.base.rest.APIContext;
import com.paypal.base.rest.OAuthTokenCredential;
import com.paypal.base.rest.PayPalRESTException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* PayPal配置类
* 配置PayPal SDK和API上下文
*/
@Configuration
public class PayPalConfig {
private static final Logger logger = LoggerFactory.getLogger(PayPalConfig.class);
@Value("${paypal.client-id:}")
private String clientId;
@Value("${paypal.client-secret:}")
private String clientSecret;
@Value("${paypal.mode:sandbox}")
private String mode;
@Value("${paypal.success-url:https://vionow.com/api/payment/paypal/success}")
private String successUrl;
@Value("${paypal.cancel-url:https://vionow.com/api/payment/paypal/cancel}")
private String cancelUrl;
// CNY到USD的汇率配置默认7.2即1美元=7.2人民币)
@Value("${paypal.exchange-rate:7.2}")
private double exchangeRate;
/**
* 创建PayPal API上下文
* @return API上下文对象
*/
@Bean
public APIContext apiContext() {
try {
// 验证配置
if (clientId == null || clientId.isEmpty()) {
logger.warn("PayPal Client ID未配置PayPal功能将不可用");
return null;
}
if (clientSecret == null || clientSecret.isEmpty()) {
logger.warn("PayPal Client Secret未配置PayPal功能将不可用");
return null;
}
logger.info("=== 初始化PayPal配置 ===");
logger.info("Client ID: {}...", clientId.substring(0, Math.min(10, clientId.length())));
logger.info("Mode: {}", mode);
logger.info("Success URL: {}", successUrl);
logger.info("Cancel URL: {}", cancelUrl);
// 创建PayPal配置
Map<String, String> configMap = new HashMap<>();
configMap.put("mode", mode);
// 创建OAuth凭证
OAuthTokenCredential credential = new OAuthTokenCredential(clientId, clientSecret, configMap);
// 创建API上下文
APIContext context = new APIContext(credential.getAccessToken());
context.setConfigurationMap(configMap);
logger.info("✅ PayPal配置初始化成功");
return context;
} catch (PayPalRESTException e) {
logger.error("❌ PayPal配置初始化失败", e);
logger.error("错误信息: {}", e.getMessage());
logger.error("详细错误: {}", e.getDetails());
return null;
} catch (Exception e) {
logger.error("❌ PayPal配置初始化失败", e);
return null;
}
}
// Getters
public String getClientId() {
return clientId;
}
public String getClientSecret() {
return clientSecret;
}
public String getMode() {
return mode;
}
public String getSuccessUrl() {
return successUrl;
}
public String getCancelUrl() {
return cancelUrl;
}
public double getExchangeRate() {
return exchangeRate;
}
}

View File

@@ -0,0 +1,348 @@
package com.example.demo.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.lang.NonNull;
import com.ijpay.alipay.AliPayApiConfig;
import com.ijpay.alipay.AliPayApiConfigKit;
/**
* 支付配置类
* 集成IJPay支付模块配置
*
* 初始化IJPay的AliPayApiConfigKit确保AliPayApi可以正常使用
*/
@Configuration
public class PaymentConfig implements ApplicationListener<ContextRefreshedEvent> {
private static final Logger logger = LoggerFactory.getLogger(PaymentConfig.class);
@Bean
@ConfigurationProperties(prefix = "alipay")
public AliPayConfig aliPayConfig() {
return new AliPayConfig();
}
/**
* 初始化IJPay配置
* 在应用上下文完全初始化后执行确保所有bean都已创建完成
*/
@Override
public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
// 确保只在根上下文触发时初始化,避免子上下文重复触发
if (event.getApplicationContext().getParent() != null) {
return;
}
// 从ApplicationContext获取已创建的bean此时所有bean都已创建完成
AliPayConfig aliPayConfig;
try {
aliPayConfig = event.getApplicationContext().getBean(AliPayConfig.class);
} catch (Exception e) {
logger.error("无法从ApplicationContext获取AliPayConfig bean", e);
return;
}
try {
logger.info("=== 初始化IJPay配置 ===");
// 详细的配置验证
boolean configValid = validateAlipayConfig(aliPayConfig);
if (!configValid) {
logger.error("支付宝配置验证失败,请检查配置");
return;
}
logger.info("应用ID: {}", aliPayConfig.getAppId());
logger.info("网关地址: {}", aliPayConfig.getServerUrl());
logger.info("字符集: {}", aliPayConfig.getCharset());
logger.info("签名类型: {}", aliPayConfig.getSignType());
logger.info("通知URL: {}", aliPayConfig.getNotifyUrl());
logger.info("返回URL: {}", aliPayConfig.getReturnUrl());
// HTTP超时时间已在应用启动时设置DemoApplication.main方法中
// 连接超时30秒读取超时120秒
logger.info("HTTP超时设置连接超时30秒读取超时120秒已在应用启动时设置");
// 构建AliPayApiConfig对象
// 注意DefaultAlipayClient会在build()方法中创建,此时会使用上面设置的系统属性
AliPayApiConfig apiConfig = AliPayApiConfig.builder()
.setAppId(aliPayConfig.getAppId())
.setPrivateKey(aliPayConfig.getPrivateKey())
.setAliPayPublicKey(aliPayConfig.getPublicKey())
.setServiceUrl(aliPayConfig.getServerUrl())
.setCharset(aliPayConfig.getCharset() != null ? aliPayConfig.getCharset() : "UTF-8")
.setSignType(aliPayConfig.getSignType() != null ? aliPayConfig.getSignType() : "RSA2")
.build();
// 验证配置中的serviceUrl是否正确
logger.info("=== 验证IJPay配置中的网关地址 ===");
logger.info("配置中的serviceUrl: {}", apiConfig.getServiceUrl());
logger.info("配置中的appId: {}", apiConfig.getAppId());
// 保存配置到静态变量供AlipayService使用备用方案
AliPayApiConfigHolder.setConfig(apiConfig);
// 设置到AliPayApiConfigKit中供AliPayApi使用
// 根据IJPay源码正确的方法是 putApiConfig
try {
AliPayApiConfigKit.putApiConfig(apiConfig);
logger.info("IJPay配置已成功设置到AliPayApiConfigKit");
// 验证从AliPayApiConfigKit获取的配置
AliPayApiConfig retrievedConfig = AliPayApiConfigKit.getAliPayApiConfig();
logger.info("从AliPayApiConfigKit获取的serviceUrl: {}", retrievedConfig.getServiceUrl());
logger.info("从AliPayApiConfigKit获取的appId: {}", retrievedConfig.getAppId());
logger.info("IJPay配置初始化完成");
} catch (Exception e) {
logger.error("设置IJPay配置到AliPayApiConfigKit时发生异常", e);
// 不抛出异常,允许应用启动,配置将在调用时动态设置
logger.warn("IJPay配置设置失败但应用将继续启动配置将在调用时动态设置");
}
} catch (Exception e) {
logger.error("IJPay配置初始化失败", e);
// 不抛出异常,允许应用启动,配置将在调用时动态设置
logger.warn("IJPay配置初始化失败但应用将继续启动配置将在调用时动态设置");
}
}
/**
* 验证支付宝配置
* @param config 支付宝配置
* @return 验证结果
*/
private boolean validateAlipayConfig(AliPayConfig config) {
logger.info("=== 开始验证支付宝配置 ===");
boolean isValid = true;
StringBuilder errors = new StringBuilder();
// 1. 验证应用ID
if (config.getAppId() == null || config.getAppId().isEmpty()) {
logger.error("❌ 应用ID (app-id) 为空");
errors.append("应用ID为空; ");
isValid = false;
} else {
// 验证appId格式应该是16位数字
String appId = config.getAppId().trim();
if (!appId.matches("^\\d{16}$")) {
logger.warn("⚠️ 应用ID格式可能不正确: {} (应该是16位数字)", appId);
} else {
logger.info("✅ 应用ID格式正确: {}", appId);
}
}
// 2. 验证私钥
if (config.getPrivateKey() == null || config.getPrivateKey().isEmpty()) {
logger.error("❌ 应用私钥 (private-key) 为空");
errors.append("应用私钥为空; ");
isValid = false;
} else {
String privateKey = config.getPrivateKey().trim();
// 验证私钥格式RSA私钥通常以BEGIN PRIVATE KEY或BEGIN RSA PRIVATE KEY开头
if (!privateKey.startsWith("MII") && !privateKey.contains("BEGIN")) {
logger.warn("⚠️ 私钥格式可能不正确应该以MII开头或包含BEGIN PRIVATE KEY");
logger.debug("私钥前20个字符: {}", privateKey.substring(0, Math.min(20, privateKey.length())));
}
// 验证私钥长度RSA私钥通常至少1000字符
if (privateKey.length() < 500) {
logger.error("❌ 私钥长度过短: {} 字符RSA私钥通常至少500字符", privateKey.length());
errors.append("私钥长度过短; ");
isValid = false;
} else {
logger.info("✅ 私钥长度: {} 字符", privateKey.length());
// 隐藏敏感信息,只显示前后部分
String maskedKey = privateKey.substring(0, 20) + "..." + privateKey.substring(privateKey.length() - 20);
logger.debug("私钥摘要: {}", maskedKey);
}
}
// 3. 验证公钥
if (config.getPublicKey() == null || config.getPublicKey().isEmpty()) {
logger.error("❌ 支付宝公钥 (public-key) 为空");
errors.append("支付宝公钥为空; ");
isValid = false;
} else {
String publicKey = config.getPublicKey().trim();
// 验证公钥格式RSA公钥通常以MII开头
if (!publicKey.startsWith("MII") && !publicKey.contains("BEGIN")) {
logger.warn("⚠️ 公钥格式可能不正确应该以MII开头或包含BEGIN PUBLIC KEY");
}
// 验证公钥长度RSA公钥通常至少200字符
if (publicKey.length() < 200) {
logger.error("❌ 公钥长度过短: {} 字符RSA公钥通常至少200字符", publicKey.length());
errors.append("公钥长度过短; ");
isValid = false;
} else {
logger.info("✅ 公钥长度: {} 字符", publicKey.length());
// 隐藏敏感信息,只显示前后部分
String maskedKey = publicKey.substring(0, 20) + "..." + publicKey.substring(publicKey.length() - 20);
logger.debug("公钥摘要: {}", maskedKey);
}
}
// 4. 验证网关地址
if (config.getServerUrl() == null || config.getServerUrl().isEmpty()) {
logger.error("❌ 网关地址 (server-url) 为空");
errors.append("网关地址为空; ");
isValid = false;
} else {
String serverUrl = config.getServerUrl().trim();
if (!serverUrl.startsWith("https://")) {
logger.error("❌ 网关地址格式不正确: {} (应该以https://开头)", serverUrl);
errors.append("网关地址格式不正确; ");
isValid = false;
} else if (!serverUrl.contains("alipay")) {
logger.warn("⚠️ 网关地址可能不正确: {} (应该包含alipay)", serverUrl);
} else {
logger.info("✅ 网关地址: {}", serverUrl);
}
}
// 5. 验证字符集
String charset = config.getCharset();
if (charset == null || charset.isEmpty()) {
logger.warn("⚠️ 字符集未设置,将使用默认值: UTF-8");
} else if (!charset.equalsIgnoreCase("UTF-8")) {
logger.warn("⚠️ 字符集不是UTF-8: {} (建议使用UTF-8)", charset);
} else {
logger.info("✅ 字符集: {}", charset);
}
// 6. 验证签名类型
String signType = config.getSignType();
if (signType == null || signType.isEmpty()) {
logger.warn("⚠️ 签名类型未设置,将使用默认值: RSA2");
} else if (!signType.equalsIgnoreCase("RSA2")) {
logger.warn("⚠️ 签名类型不是RSA2: {} (建议使用RSA2)", signType);
} else {
logger.info("✅ 签名类型: {}", signType);
}
// 7. 验证通知URL
if (config.getNotifyUrl() == null || config.getNotifyUrl().isEmpty()) {
logger.error("❌ 通知URL (notify-url) 为空");
errors.append("通知URL为空; ");
isValid = false;
} else {
String notifyUrl = config.getNotifyUrl().trim();
if (!notifyUrl.startsWith("https://")) {
logger.error("❌ 通知URL格式不正确: {} (应该以https://开头)", notifyUrl);
errors.append("通知URL格式不正确; ");
isValid = false;
} else {
logger.info("✅ 通知URL: {}", notifyUrl);
}
}
// 8. 验证返回URL
if (config.getReturnUrl() == null || config.getReturnUrl().isEmpty()) {
logger.error("❌ 返回URL (return-url) 为空");
errors.append("返回URL为空; ");
isValid = false;
} else {
String returnUrl = config.getReturnUrl().trim();
if (!returnUrl.startsWith("https://")) {
logger.error("❌ 返回URL格式不正确: {} (应该以https://开头)", returnUrl);
errors.append("返回URL格式不正确; ");
isValid = false;
} else {
logger.info("✅ 返回URL: {}", returnUrl);
}
}
// 总结验证结果
if (isValid) {
logger.info("=== 支付宝配置验证通过 ===");
} else {
logger.error("=== 支付宝配置验证失败 ===");
logger.error("错误详情: {}", errors.toString());
logger.error("请检查 application-dev.properties 或 payment.properties 文件中的支付宝配置");
}
return isValid;
}
/**
* IJPay配置持有者
* 用于保存配置供AlipayService在调用时动态设置
*/
public static class AliPayApiConfigHolder {
private static AliPayApiConfig config;
public static void setConfig(AliPayApiConfig config) {
AliPayApiConfigHolder.config = config;
}
public static AliPayApiConfig getConfig() {
return config;
}
}
/**
* 支付宝配置
*/
public static class AliPayConfig {
private String appId;
private String privateKey;
private String publicKey;
private String serverUrl;
private String gatewayUrl;
private String domain;
private String charset;
private String signType;
private String notifyUrl;
private String returnUrl;
private String appCertPath;
private String aliPayCertPath;
private String aliPayRootCertPath;
// Getters and Setters
public String getAppId() { return appId; }
public void setAppId(String appId) { this.appId = appId; }
public String getPrivateKey() { return privateKey; }
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
public String getPublicKey() { return publicKey; }
public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
public String getServerUrl() { return serverUrl; }
public void setServerUrl(String serverUrl) { this.serverUrl = serverUrl; }
public String getGatewayUrl() { return gatewayUrl; }
public void setGatewayUrl(String gatewayUrl) { this.gatewayUrl = gatewayUrl; }
public String getDomain() { return domain; }
public void setDomain(String domain) { this.domain = domain; }
public String getCharset() { return charset; }
public void setCharset(String charset) { this.charset = charset; }
public String getSignType() { return signType; }
public void setSignType(String signType) { this.signType = signType; }
public String getNotifyUrl() { return notifyUrl; }
public void setNotifyUrl(String notifyUrl) { this.notifyUrl = notifyUrl; }
public String getReturnUrl() { return returnUrl; }
public void setReturnUrl(String returnUrl) { this.returnUrl = returnUrl; }
public String getAppCertPath() { return appCertPath; }
public void setAppCertPath(String appCertPath) { this.appCertPath = appCertPath; }
public String getAliPayCertPath() { return aliPayCertPath; }
public void setAliPayCertPath(String aliPayCertPath) { this.aliPayCertPath = aliPayCertPath; }
public String getAliPayRootCertPath() { return aliPayRootCertPath; }
public void setAliPayRootCertPath(String aliPayRootCertPath) { this.aliPayRootCertPath = aliPayRootCertPath; }
}
}

View File

@@ -0,0 +1,26 @@
package com.example.demo.config;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
* 轮询查询配置类
* 确保每2分钟精确执行轮询查询任务
*/
@Configuration
@EnableScheduling
public class PollingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(@NonNull ScheduledTaskRegistrar taskRegistrar) {
// 使用自定义线程池执行定时任务支持100-200人并发
ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
taskRegistrar.setScheduler(executor);
}
}

View File

@@ -0,0 +1,102 @@
package com.example.demo.config;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* 请求日志过滤器
* 用于记录请求和响应的详细信息,特别是在出现 JSON 解析错误时帮助调试
*/
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 包装请求和响应以便读取内容
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
try {
// 继续过滤链
filterChain.doFilter(wrappedRequest, wrappedResponse);
} catch (Exception e) {
// 如果出现异常,记录请求详情
logRequestDetails(wrappedRequest, e);
throw e;
} finally {
// 记录响应状态
if (wrappedResponse.getStatus() >= 400) {
// 只有在出现错误时才记录详细的请求信息
logRequestDetails(wrappedRequest, null);
}
// 复制响应内容到原始响应
wrappedResponse.copyBodyToResponse();
}
}
private void logRequestDetails(ContentCachingRequestWrapper request, Exception exception) {
try {
String uri = request.getRequestURI();
String method = request.getMethod();
// 只记录POST/PUT/PATCH请求的详情
if ("POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method)) {
logger.error("=== 请求详情 ({}出现问题) ===", exception != null ? "捕获异常" : "响应错误");
logger.error("URI: {} {}", method, uri);
logger.error("Content-Type: {}", request.getContentType());
logger.error("Content-Length: {}", request.getContentLength());
// 获取请求体内容
byte[] content = request.getContentAsByteArray();
if (content.length > 0) {
String body = getContentAsString(content, request.getCharacterEncoding());
logger.error("请求体内容 (前1000字符):");
logger.error("{}", body.length() > 1000 ? body.substring(0, 1000) + "..." : body);
// 检查是否包含可疑的反斜杠
if (body.contains("\\")) {
logger.error("⚠️ 警告:请求体中包含反斜杠字符,这可能导致 JSON 解析错误");
// 显示反斜杠周围的上下文
int index = body.indexOf("\\");
if (index >= 0) {
int start = Math.max(0, index - 20);
int end = Math.min(body.length(), index + 20);
logger.error("反斜杠位置附近的内容: [{}]", body.substring(start, end));
}
}
}
if (exception != null) {
logger.error("异常: {}", exception.getMessage());
}
}
} catch (Exception e) {
logger.error("记录请求详情时出错", e);
}
}
private String getContentAsString(byte[] content, String charset) {
try {
return new String(content, charset != null ? charset : "UTF-8");
} catch (UnsupportedEncodingException e) {
return "[无法解码内容]";
}
}
}

View File

@@ -0,0 +1,145 @@
package com.example.demo.config;
import java.util.Arrays;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.example.demo.security.JwtAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session
.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/lulupay/notify", "/api/payments/lulupay/return").permitAll()
.requestMatchers("/api/payment/paypal/success", "/api/payment/paypal/cancel").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")
.usernameParameter("email")
.passwordParameter("password")
.defaultSuccessUrl("/", true)
.permitAll()
)
.logout(Customizer.withDefaults());
// 添加JWT过滤器
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();
provider.setPasswordEncoder(passwordEncoder);
provider.setUserDetailsService(userDetailsService);
return provider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList(
"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"
));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD", "TRACE", "CONNECT"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setExposedHeaders(Arrays.asList("*"));
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View File

@@ -0,0 +1,109 @@
package com.example.demo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 腾讯云配置类
*/
@Configuration
@ConfigurationProperties(prefix = "tencent.cloud")
public class TencentCloudConfig {
/**
* 腾讯云SecretId
*/
private String secretId;
/**
* 腾讯云SecretKey
*/
private String secretKey;
/**
* 邮件推送服务配置
*/
private SesConfig ses = new SesConfig();
/**
* 邮件推送服务配置
*/
public static class SesConfig {
/**
* 邮件服务地域
*/
private String region = "ap-beijing";
/**
* 发件人邮箱
*/
private String fromEmail;
/**
* 发件人名称
*/
private String fromName;
/**
* 验证码邮件模板ID
*/
private String templateId;
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public String getFromEmail() {
return fromEmail;
}
public void setFromEmail(String fromEmail) {
this.fromEmail = fromEmail;
}
public String getFromName() {
return fromName;
}
public void setFromName(String fromName) {
this.fromName = fromName;
}
public String getTemplateId() {
return templateId;
}
public void setTemplateId(String templateId) {
this.templateId = templateId;
}
}
public String getSecretId() {
return secretId;
}
public void setSecretId(String secretId) {
this.secretId = secretId;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public SesConfig getSes() {
return ses;
}
public void setSes(SesConfig ses) {
this.ses = ses;
}
}

View File

@@ -0,0 +1,92 @@
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.TransactionTemplate;
import jakarta.persistence.EntityManagerFactory;
/**
* 事务管理器配置
* 确保事务不会长时间占用数据库连接
*/
@Configuration
@EnableTransactionManagement
public class TransactionManagerConfig {
@Autowired
private EntityManagerFactory entityManagerFactory;
/**
* 配置事务管理器
* 使用 JpaTransactionManager 以支持 JPA 操作(包括悲观锁)
* 注意:超时时间在 TransactionTemplate 中设置,而不是在 TransactionManager 中
* 这样可以更精确地控制不同场景下的超时时间
*/
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
// 设置是否允许嵌套事务
transactionManager.setNestedTransactionAllowed(true);
// 设置是否在回滚时验证事务状态
transactionManager.setValidateExistingTransaction(true);
// 注意:超时时间在 TransactionTemplate 中设置,而不是在 TransactionManager 中
// 这样可以更精确地控制不同场景下的超时时间
return transactionManager;
}
/**
* 配置用于异步方法的事务模板
* 使用更短的超时时间3秒确保异步线程中的事务快速完成
*/
@Bean(name = "asyncTransactionTemplate")
public TransactionTemplate asyncTransactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
// 异步方法中的事务超时时间设置为3秒
// 确保异步线程中的数据库操作快速完成,不会长时间占用连接
template.setTimeout(3);
// 设置传播行为为 REQUIRES_NEW确保每个操作都是独立事务
template.setPropagationBehavior(org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW);
// 设置隔离级别为 READ_COMMITTED默认
template.setIsolationLevel(org.springframework.transaction.TransactionDefinition.ISOLATION_READ_COMMITTED);
// 设置只读标志默认false允许写操作
template.setReadOnly(false);
return template;
}
/**
* 配置用于只读操作的事务模板
* 使用更短的超时时间2秒确保只读操作快速完成
*/
@Bean(name = "readOnlyTransactionTemplate")
public TransactionTemplate readOnlyTransactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
// 只读操作超时时间设置为2秒
template.setTimeout(2);
// 设置传播行为为 REQUIRES_NEW
template.setPropagationBehavior(org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW);
// 设置只读标志为 true
template.setReadOnly(true);
return template;
}
}

View File

@@ -0,0 +1,85 @@
package com.example.demo.config;
import kong.unirest.Unirest;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.client.config.RequestConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
/**
* Unirest HTTP客户端统一配置类
* 确保Unirest在应用启动时只配置一次避免重复配置警告
*/
@Configuration
public class UnirestConfig {
private static final Logger logger = LoggerFactory.getLogger(UnirestConfig.class);
// 静态标志,确保只配置一次
private static volatile boolean configured = false;
private static final Object lock = new Object();
@PostConstruct
public void init() {
configureUnirest();
}
/**
* 配置Unirest HTTP客户端
* 使用双重检查锁定确保线程安全且只配置一次
*/
public static void configureUnirest() {
if (!configured) {
synchronized (lock) {
if (!configured) {
try {
logger.info("正在初始化Unirest HTTP客户端配置...");
// 先重置,确保干净的状态
Unirest.config().reset();
// 配置连接池和超时
Unirest.config()
.connectTimeout(30000) // 30秒连接超时
.socketTimeout(300000) // 5分钟读取超时300秒
.retryAfter(false) // 禁用自动重试,使用自定义重试逻辑
.httpClient(HttpClients.custom()
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false)) // 禁用内部重试
.setMaxConnTotal(500) // 最大连接数
.setMaxConnPerRoute(100) // 每路由最大连接数
.setConnectionTimeToLive(30, TimeUnit.SECONDS)
.evictExpiredConnections()
.evictIdleConnections(30, TimeUnit.SECONDS)
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectTimeout(30000)
.setSocketTimeout(300000)
.setConnectionRequestTimeout(30000)
.setContentCompressionEnabled(false) // 禁用压缩,避免额外开销
.build())
.build());
configured = true;
logger.info("Unirest HTTP客户端配置完成: 连接超时30秒, 读取超时300秒");
} catch (Exception e) {
logger.warn("Unirest配置异常: {}", e.getMessage());
// 即使配置失败,也标记为已配置,避免重复尝试
configured = true;
}
}
}
}
}
/**
* 检查是否已配置
*/
public static boolean isConfigured() {
return configured;
}
}

View File

@@ -0,0 +1,98 @@
package com.example.demo.config;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import com.example.demo.interceptor.UserActivityInterceptor;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${app.upload.path:uploads}")
private String uploadPath;
@Autowired
private UserActivityInterceptor userActivityInterceptor;
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return slr;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 语言切换拦截器
registry.addInterceptor(localeChangeInterceptor());
// 用户活跃时间更新拦截器(用于统计在线用户)
registry.addInterceptor(userActivityInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns(
"/css/**",
"/js/**",
"/images/**",
"/uploads/**",
"/favicon.ico",
"/error"
); // 排除静态资源
}
/**
* 配置静态资源服务使上传的文件可以通过URL访问
* 访问路径:/uploads/** -> 映射到 uploads/ 目录
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 将 /uploads/** 映射到 uploads/ 目录
// 处理路径:如果是相对路径,转换为绝对路径;如果是绝对路径,直接使用
java.nio.file.Path uploadDirPath = java.nio.file.Paths.get(uploadPath);
if (!uploadDirPath.isAbsolute()) {
// 相对路径:基于应用运行目录
uploadDirPath = java.nio.file.Paths.get(System.getProperty("user.dir"), uploadPath);
}
// 确保路径使用正斜杠URL格式
String resourceLocation = "file:" + uploadDirPath.toAbsolutePath().toString().replace("\\", "/") + "/";
// 上传文件缓存7天视频/图片等媒体文件)
registry.addResourceHandler("/uploads/**")
.addResourceLocations(resourceLocation)
.setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS).cachePublic());
// 静态资源缓存配置JS/CSS/图片等)
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic());
// 图片资源缓存30天
registry.addResourceHandler("/images/**")
.addResourceLocations("classpath:/static/images/")
.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic());
}
// CORS配置已移至SecurityConfig避免冲突
}

View File

@@ -0,0 +1,687 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.SystemSettings;
import com.example.demo.model.User;
import com.example.demo.repository.MembershipLevelRepository;
import com.example.demo.repository.TaskStatusRepository;
import com.example.demo.service.OnlineStatsService;
import com.example.demo.service.SystemSettingsService;
import com.example.demo.service.UserService;
import com.example.demo.util.JwtUtils;
/**
* 管理员控制器
* 提供管理员功能,包括积分管理
*/
@RestController
@RequestMapping("/api/admin")
public class AdminController {
private static final Logger logger = LoggerFactory.getLogger(AdminController.class);
@Autowired
private UserService userService;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private SystemSettingsService systemSettingsService;
@Autowired
private TaskStatusRepository taskStatusRepository;
@Autowired
private OnlineStatsService onlineStatsService;
@Autowired
private MembershipLevelRepository membershipLevelRepository;
/**
* 给用户增加积分
*/
@PostMapping("/add-points")
public ResponseEntity<Map<String, Object>> addPoints(
@RequestParam String username,
@RequestParam Integer points,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 增加用户积分
userService.addPoints(username, points);
response.put("success", true);
response.put("message", "积分增加成功");
response.put("username", username);
response.put("points", points);
logger.info("管理员 {} 为用户 {} 增加了 {} 积分", adminUsername, username, points);
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);
}
}
/**
* 重置用户积分为默认值
*/
@PostMapping("/reset-points")
public ResponseEntity<Map<String, Object>> resetPoints(
@RequestParam String username,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 重置用户积分为100
userService.setPoints(username, 100);
response.put("success", true);
response.put("message", "积分重置成功");
response.put("username", username);
response.put("points", 100);
logger.info("管理员 {} 重置用户 {} 的积分为 100", adminUsername, username);
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);
}
}
/**
* 从Token中提取用户名
*/
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
return null;
}
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username != null && !jwtUtils.isTokenExpired(actualToken)) {
return username;
}
return null;
} catch (Exception e) {
logger.error("解析token失败", e);
return null;
}
}
/**
* 获取所有用户列表
*/
@GetMapping("/users")
public ResponseEntity<Map<String, Object>> getAllUsers(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 获取所有用户
List<User> users = userService.findAll();
// 转换为DTO格式
List<Map<String, Object>> userList = users.stream().map(user -> {
Map<String, Object> userMap = new HashMap<>();
userMap.put("id", user.getId());
userMap.put("username", user.getUsername());
userMap.put("email", user.getEmail());
userMap.put("role", user.getRole());
userMap.put("points", user.getPoints());
userMap.put("frozenPoints", user.getFrozenPoints());
userMap.put("createdAt", user.getCreatedAt());
userMap.put("lastLoginAt", user.getLastLoginAt());
userMap.put("isActive", user.getIsActive());
return userMap;
}).collect(Collectors.toList());
response.put("success", true);
response.put("data", userList);
logger.info("管理员 {} 获取用户列表,共 {} 个用户", adminUsername, users.size());
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);
}
}
/**
* 创建新用户
*/
@PostMapping("/users")
public ResponseEntity<Map<String, Object>> createUser(
@RequestBody Map<String, Object> userData,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 提取用户数据
String username = (String) userData.get("username");
String email = (String) userData.get("email");
String password = (String) userData.get("password");
String role = (String) userData.getOrDefault("role", "ROLE_USER");
// 验证必填字段
if (username == null || username.isBlank()) {
response.put("success", false);
response.put("message", "用户名不能为空");
return ResponseEntity.badRequest().body(response);
}
if (email == null || email.isBlank()) {
response.put("success", false);
response.put("message", "邮箱不能为空");
return ResponseEntity.badRequest().body(response);
}
if (password == null || password.isBlank()) {
response.put("success", false);
response.put("message", "密码不能为空");
return ResponseEntity.badRequest().body(response);
}
// 创建用户
User user = userService.create(username, email, password);
// 如果指定了角色,更新角色
if (!role.equals("ROLE_USER")) {
userService.update(user.getId(), username, email, null, role);
user = userService.findById(user.getId());
}
// 构建响应
Map<String, Object> userMap = new HashMap<>();
userMap.put("id", user.getId());
userMap.put("username", user.getUsername());
userMap.put("email", user.getEmail());
userMap.put("role", user.getRole());
userMap.put("points", user.getPoints());
userMap.put("createdAt", user.getCreatedAt());
response.put("success", true);
response.put("message", "用户创建成功");
response.put("data", userMap);
logger.info("管理员 {} 创建用户: {}", adminUsername, username);
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
logger.error("创建用户失败: {}", e.getMessage());
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
} catch (Exception e) {
logger.error("创建用户失败", e);
response.put("success", false);
response.put("message", "创建用户失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 更新用户信息
*/
@PutMapping("/users/{id}")
public ResponseEntity<Map<String, Object>> updateUser(
@PathVariable Long id,
@RequestBody Map<String, Object> userData,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 提取用户数据
String username = (String) userData.get("username");
String email = (String) userData.get("email");
String password = (String) userData.get("password");
String role = (String) userData.get("role");
// 验证必填字段
if (username == null || username.isBlank()) {
response.put("success", false);
response.put("message", "用户名不能为空");
return ResponseEntity.badRequest().body(response);
}
if (email == null || email.isBlank()) {
response.put("success", false);
response.put("message", "邮箱不能为空");
return ResponseEntity.badRequest().body(response);
}
// 更新用户(密码可选)
User user = userService.update(id, username, email, password, role);
// 构建响应
Map<String, Object> userMap = new HashMap<>();
userMap.put("id", user.getId());
userMap.put("username", user.getUsername());
userMap.put("email", user.getEmail());
userMap.put("role", user.getRole());
userMap.put("points", user.getPoints());
userMap.put("updatedAt", user.getUpdatedAt());
response.put("success", true);
response.put("message", "用户更新成功");
response.put("data", userMap);
logger.info("管理员 {} 更新用户: {}", adminUsername, username);
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
logger.error("更新用户失败: {}", e.getMessage());
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
} catch (Exception e) {
logger.error("更新用户失败", e);
response.put("success", false);
response.put("message", "更新用户失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 删除用户
*/
@DeleteMapping("/users/{id}")
public ResponseEntity<Map<String, Object>> deleteUser(
@PathVariable Long id,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 删除用户
userService.delete(id);
response.put("success", true);
response.put("message", "用户删除成功");
logger.info("管理员 {} 删除用户ID: {}", adminUsername, id);
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);
}
}
/**
* 获取系统设置
*/
@GetMapping("/settings")
public ResponseEntity<Map<String, Object>> getSettings() {
Map<String, Object> response = new HashMap<>();
try {
SystemSettings settings = systemSettingsService.getOrCreate();
response.put("promptOptimizationModel", settings.getPromptOptimizationModel());
response.put("storyboardSystemPrompt", settings.getStoryboardSystemPrompt());
response.put("siteName", settings.getSiteName());
response.put("siteSubtitle", settings.getSiteSubtitle());
response.put("registrationOpen", settings.getRegistrationOpen());
response.put("maintenanceMode", settings.getMaintenanceMode());
response.put("contactEmail", settings.getContactEmail());
response.put("tokenExpireHours", settings.getTokenExpireHours());
// 套餐价格配置从membership_levels表读取
membershipLevelRepository.findByName("standard").ifPresent(level ->
response.put("standardPriceCny", level.getPrice().intValue()));
membershipLevelRepository.findByName("professional").ifPresent(level ->
response.put("proPriceCny", level.getPrice().intValue()));
response.put("pointsPerGeneration", settings.getPointsPerGeneration());
// 支付渠道开关
response.put("enableAlipay", settings.getEnableAlipay());
response.put("enablePaypal", settings.getEnablePaypal());
response.put("videoModel", settings.getVideoModel() != null ? settings.getVideoModel() : "grok-video-3");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取系统设置失败", e);
response.put("error", "获取系统设置失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 更新系统设置
*/
@PutMapping("/settings")
public ResponseEntity<Map<String, Object>> updateSettings(
@RequestBody Map<String, Object> settingsData) {
Map<String, Object> response = new HashMap<>();
try {
SystemSettings settings = systemSettingsService.getOrCreate();
// 更新优化提示词模型
if (settingsData.containsKey("promptOptimizationModel")) {
String model = (String) settingsData.get("promptOptimizationModel");
settings.setPromptOptimizationModel(model);
logger.info("更新优化提示词模型为: {}", model);
}
// 更新分镜图系统引导词
if (settingsData.containsKey("storyboardSystemPrompt")) {
String prompt = (String) settingsData.get("storyboardSystemPrompt");
settings.setStoryboardSystemPrompt(prompt);
logger.info("更新分镜图系统引导词");
}
// 更新Token过期时间小时
if (settingsData.containsKey("tokenExpireHours")) {
Object value = settingsData.get("tokenExpireHours");
Integer hours = null;
if (value instanceof Number) {
hours = ((Number) value).intValue();
} else if (value instanceof String) {
try {
hours = Integer.parseInt((String) value);
} catch (NumberFormatException e) {
logger.warn("无效的Token过期时间值: {}", value);
}
}
if (hours != null && hours >= 1 && hours <= 720) {
settings.setTokenExpireHours(hours);
logger.info("更新Token过期时间为: {} 小时", hours);
} else {
logger.warn("Token过期时间超出范围(1-720小时): {}", hours);
}
}
// 更新套餐价格只更新membership_levels表
if (settingsData.containsKey("standardPriceCny")) {
Object value = settingsData.get("standardPriceCny");
Integer price = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString());
membershipLevelRepository.findByName("standard").ifPresent(level -> {
level.setPrice(price.doubleValue());
level.setUpdatedAt(java.time.LocalDateTime.now());
membershipLevelRepository.save(level);
logger.info("更新membership_levels表: standard价格={}", price);
});
logger.info("更新标准版价格为: {} 元", price);
}
if (settingsData.containsKey("proPriceCny")) {
Object value = settingsData.get("proPriceCny");
Integer price = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString());
membershipLevelRepository.findByName("professional").ifPresent(level -> {
level.setPrice(price.doubleValue());
level.setUpdatedAt(java.time.LocalDateTime.now());
membershipLevelRepository.save(level);
logger.info("更新membership_levels表: professional价格={}", price);
});
logger.info("更新专业版价格为: {} 元", price);
}
if (settingsData.containsKey("pointsPerGeneration")) {
Object value = settingsData.get("pointsPerGeneration");
Integer points = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString());
settings.setPointsPerGeneration(points);
logger.info("更新每次生成消耗积分为: {}", points);
}
// 更新支付渠道开关
if (settingsData.containsKey("enableAlipay")) {
Boolean enable = (Boolean) settingsData.get("enableAlipay");
settings.setEnableAlipay(enable);
logger.info("更新支付宝开关为: {}", enable);
}
if (settingsData.containsKey("enablePaypal")) {
Boolean enable = (Boolean) settingsData.get("enablePaypal");
settings.setEnablePaypal(enable);
logger.info("更新PayPal开关为: {}", enable);
}
// 更新视频生成模型
if (settingsData.containsKey("videoModel")) {
String model = (String) settingsData.get("videoModel");
settings.setVideoModel(model);
logger.info("更新视频生成模型为: {}", model);
}
systemSettingsService.update(settings);
response.put("success", true);
response.put("message", "系统设置更新成功");
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);
}
}
/**
* 删除单个任务记录
*/
@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);
}
// 查找并删除任务(使用 findFirst 处理可能的重复记录)
var taskOpt = taskStatusRepository.findFirstByTaskIdOrderByIdDesc(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.findFirstByTaskIdOrderByIdDesc(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);
}
}
/**
* 获取系统在线统计
* 返回当天访问的独立IP数通过IP判断在线人数
*/
@GetMapping("/online-stats")
public ResponseEntity<Map<String, Object>> getOnlineStats() {
Map<String, Object> response = new HashMap<>();
try {
int todayVisitors = onlineStatsService.getTodayVisitorCount();
Map<String, Object> stats = onlineStatsService.getStats();
response.put("success", true);
response.put("todayVisitors", todayVisitors);
response.put("date", stats.get("date"));
response.put("uptime", stats.get("uptime"));
return ResponseEntity.ok()
.header("Cache-Control", "no-cache, no-store, must-revalidate")
.header("Pragma", "no-cache")
.header("Expires", "0")
.body(response);
} catch (Exception e) {
logger.error("获取在线统计失败", e);
response.put("success", false);
response.put("message", "获取在线统计失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
}

View File

@@ -0,0 +1,74 @@
package com.example.demo.controller;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
@Controller
@RequestMapping("/users")
@Validated
public class AdminUserController {
private final UserService userService;
public AdminUserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public String list(Model model) {
List<User> users = userService.findAll();
model.addAttribute("users", users);
return "users/list";
}
@GetMapping("/new")
public String createForm(Model model) {
model.addAttribute("user", new User());
return "users/form";
}
@PostMapping
public String create(@RequestParam String username,
@RequestParam String email,
@RequestParam String password,
@RequestParam String role) {
userService.create(username, email, password);
// 创建后更新角色
User user = userService.findByUsername(username);
userService.update(user.getId(), username, email, null, role);
return "redirect:/users";
}
@GetMapping("/{id}/edit")
public String editForm(@PathVariable Long id, Model model) {
User user = userService.findById(id);
model.addAttribute("user", user);
return "users/form";
}
@PostMapping("/{id}")
public String update(@PathVariable Long id,
@RequestParam String username,
@RequestParam String email,
@RequestParam(required = false) String password,
@RequestParam String role) {
userService.update(id, username, email, password, role);
return "redirect:/users";
}
@PostMapping("/{id}/delete")
public String delete(@PathVariable Long id) {
userService.delete(id);
return "redirect:/users";
}
}

View File

@@ -0,0 +1,334 @@
package com.example.demo.controller;
import java.io.BufferedReader;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.alipay.api.internal.util.AlipaySignature;
import com.example.demo.model.Payment;
import com.example.demo.model.PaymentStatus;
import com.example.demo.repository.PaymentRepository;
import jakarta.servlet.http.HttpServletRequest;
/**
* 支付宝回调接口控制器
* 参考 IJPay 实现,处理支付宝异步通知和同步返回
*
* @author Generated
*/
@RestController
@RequestMapping("/api/payments/alipay")
public class AlipayCallbackController {
private static final Logger logger = LoggerFactory.getLogger(AlipayCallbackController.class);
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private com.example.demo.config.PaymentConfig.AliPayConfig aliPayConfig;
@Autowired
private com.example.demo.service.PaymentService paymentService;
/**
* 支付宝异步通知接口
* 支付宝服务器会通过POST方式调用此接口通知支付结果
*
* 参考 IJPay 实现:
* - 使用 AlipaySignature.rsaCheckV1() 验证签名
* - 处理重复通知(去重)
* - 更新订单状态
* - 返回 "success" 或 "failure"
*
* @param request HTTP请求对象
* @return "success" 表示处理成功,"failure" 表示处理失败
*/
@PostMapping(value = "/notify", produces = MediaType.TEXT_PLAIN_VALUE)
public String notifyUrl(HttpServletRequest request) {
logger.info("========== 收到支付宝异步通知 ==========");
logger.info("请求方法: {}", request.getMethod());
logger.info("请求URL: {}", request.getRequestURL());
logger.info("Content-Type: {}", request.getContentType());
try {
// 获取支付宝POST过来的反馈信息
Map<String, String> params = getRequestParams(request);
// 打印所有参数(用于调试)
logger.info("支付宝异步通知参数:");
for (Map.Entry<String, String> entry : params.entrySet()) {
logger.info(" {} = {}", entry.getKey(), entry.getValue());
}
// 验证签名
boolean verifyResult = AlipaySignature.rsaCheckV1(
params,
aliPayConfig.getPublicKey(),
aliPayConfig.getCharset() != null ? aliPayConfig.getCharset() : "UTF-8",
aliPayConfig.getSignType() != null ? aliPayConfig.getSignType() : "RSA2"
);
if (!verifyResult) {
logger.error("========== 支付宝异步通知签名验证失败 ==========");
return "failure";
}
logger.info("========== 支付宝异步通知签名验证成功 ==========");
// 获取关键参数
String tradeStatus = params.get("trade_status");
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String totalAmount = params.get("total_amount");
String appId = params.get("app_id");
logger.info("订单号: {}, 交易号: {}, 交易状态: {}, 金额: {}, 应用ID: {}",
outTradeNo, tradeNo, tradeStatus, totalAmount, appId);
// 查找支付记录
logger.info("正在查找支付记录,订单号: {}", outTradeNo);
Optional<Payment> paymentOpt = paymentRepository.findByOrderId(outTradeNo);
if (!paymentOpt.isPresent()) {
logger.error("❌ 支付记录不存在: orderId={}", outTradeNo);
logger.error("请检查数据库中是否存在该订单号的支付记录");
return "failure";
}
logger.info("✅ 找到支付记录: paymentId={}", paymentOpt.get().getId());
Payment payment = paymentOpt.get();
// 处理重复通知(去重)
// 如果订单已经是成功状态,且本次通知也是成功,则认为是重复通知
if (payment.getStatus() == PaymentStatus.SUCCESS &&
("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus))) {
logger.info("收到重复的成功通知,订单号: {}, 已处理过直接返回success", outTradeNo);
return "success";
}
// 根据交易状态更新支付记录
boolean shouldUpdateOrder = false;
switch (tradeStatus) {
case "TRADE_SUCCESS":
case "TRADE_FINISHED":
// 交易成功
if (payment.getStatus() != PaymentStatus.SUCCESS) {
payment.setStatus(PaymentStatus.SUCCESS);
payment.setExternalTransactionId(tradeNo);
payment.setPaidAt(LocalDateTime.now());
shouldUpdateOrder = true;
logger.info("订单支付成功: {}", outTradeNo);
}
break;
case "TRADE_CLOSED":
// 交易关闭(未付款交易超时关闭,或支付完成后全额退款)
if (payment.getStatus() != PaymentStatus.CANCELLED) {
payment.setStatus(PaymentStatus.CANCELLED);
shouldUpdateOrder = true;
logger.info("订单已关闭: {}", outTradeNo);
}
break;
case "WAIT_BUYER_PAY":
// 交易创建,等待买家付款
if (payment.getStatus() != PaymentStatus.PROCESSING) {
payment.setStatus(PaymentStatus.PROCESSING);
shouldUpdateOrder = true;
logger.info("订单等待付款: {}", outTradeNo);
}
break;
default:
logger.warn("未知的交易状态: {}, 订单号: {}", tradeStatus, outTradeNo);
break;
}
// 保存支付记录并处理业务逻辑
if (shouldUpdateOrder) {
// 如果是支付成功,调用统一的支付确认方法(会自动创建订单、增加积分等)
if (payment.getStatus() == PaymentStatus.SUCCESS) {
try {
// 使用PaymentService的确认支付成功方法确保和PayPal一样的处理逻辑
paymentService.confirmPaymentSuccess(payment.getId(), tradeNo);
logger.info("✅ 支付确认成功,已创建订单并增加积分: 订单号={}", outTradeNo);
} catch (Exception e) {
logger.error("❌ 支付确认失败(但支付状态已更新): {}", e.getMessage());
// 即使确认失败,也要保存支付状态,避免丢失支付记录
paymentRepository.save(payment);
}
} else {
// 非成功状态,只保存支付记录
paymentRepository.save(payment);
logger.info("支付记录更新成功: 订单号={}, 状态={}", outTradeNo, payment.getStatus());
}
}
logger.info("========== 支付宝异步通知处理完成 ==========");
return "success";
} catch (Exception e) {
logger.error("========== 处理支付宝异步通知异常 ==========", e);
return "failure";
}
}
/**
* 支付宝同步返回接口
* 用户支付完成后支付宝会跳转到此页面GET请求
*
* 参考 IJPay 实现:
* - 使用 AlipaySignature.rsaCheckV1() 验证签名
* - 验证成功后可以跳转到支付成功页面
*
* @param request HTTP请求对象
* @return 支付结果信息
*/
@GetMapping("/return")
public Map<String, Object> returnUrl(HttpServletRequest request) {
logger.info("========== 收到支付宝同步返回 ==========");
logger.info("请求方法: {}", request.getMethod());
logger.info("请求URL: {}", request.getRequestURL());
Map<String, Object> result = new HashMap<>();
try {
// 获取支付宝GET过来的反馈信息
Map<String, String> params = getRequestParams(request);
// 打印所有参数(用于调试)
logger.info("支付宝同步返回参数:");
for (Map.Entry<String, String> entry : params.entrySet()) {
logger.info(" {} = {}", entry.getKey(), entry.getValue());
}
// 验证签名
boolean verifyResult = AlipaySignature.rsaCheckV1(
params,
aliPayConfig.getPublicKey(),
aliPayConfig.getCharset() != null ? aliPayConfig.getCharset() : "UTF-8",
aliPayConfig.getSignType() != null ? aliPayConfig.getSignType() : "RSA2"
);
if (!verifyResult) {
logger.error("========== 支付宝同步返回签名验证失败 ==========");
result.put("success", false);
result.put("message", "签名验证失败");
return result;
}
logger.info("========== 支付宝同步返回签名验证成功 ==========");
// 获取关键参数
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String totalAmount = params.get("total_amount");
logger.info("订单号: {}, 交易号: {}, 金额: {}", outTradeNo, tradeNo, totalAmount);
// 查找支付记录
Optional<Payment> paymentOpt = paymentRepository.findByOrderId(outTradeNo);
if (!paymentOpt.isPresent()) {
logger.error("支付记录不存在: {}", outTradeNo);
result.put("success", false);
result.put("message", "支付记录不存在");
return result;
}
Payment payment = paymentOpt.get();
// 如果状态还是待支付,更新为处理中(实际状态应该由异步通知更新)
if (payment.getStatus() == PaymentStatus.PENDING) {
payment.setStatus(PaymentStatus.PROCESSING);
payment.setExternalTransactionId(tradeNo);
paymentRepository.save(payment);
logger.info("支付记录状态已更新为处理中: {}", outTradeNo);
}
result.put("success", true);
result.put("message", "支付成功");
result.put("orderId", outTradeNo);
result.put("tradeNo", tradeNo);
result.put("amount", totalAmount);
result.put("paymentStatus", payment.getStatus().name());
logger.info("========== 支付宝同步返回处理完成 ==========");
return result;
} catch (Exception e) {
logger.error("========== 处理支付宝同步返回异常 ==========", e);
result.put("success", false);
result.put("message", "处理失败: " + e.getMessage());
return result;
}
}
/**
* 从HttpServletRequest中获取所有参数
* 兼容GET和POST请求支持form-urlencoded格式
*
* 注意Spring Boot 3 使用 jakarta.servlet而 IJPay 可能使用 javax.servlet
* 所以这里手动解析参数,而不是直接使用 AliPayApi.toMap(request)
*
* @param request HTTP请求对象
* @return 参数Map
*/
private Map<String, String> getRequestParams(HttpServletRequest request) throws IOException {
Map<String, String> params = new HashMap<>();
// 获取URL参数GET请求
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
params.put(key, values[0]);
}
});
// 如果是POST请求尝试从请求体获取参数
if ("POST".equalsIgnoreCase(request.getMethod())) {
try (BufferedReader reader = request.getReader()) {
StringBuilder body = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
// 解析form-urlencoded格式的请求体
if (body.length() > 0) {
String bodyStr = body.toString();
if (bodyStr.contains("=")) {
String[] pairs = bodyStr.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=", 2);
if (keyValue.length == 2) {
try {
String key = java.net.URLDecoder.decode(keyValue[0], "UTF-8");
String value = java.net.URLDecoder.decode(keyValue[1], "UTF-8");
// POST参数会覆盖GET参数如果存在
params.put(key, value);
} catch (Exception e) {
logger.warn("解析参数失败: {}", pair, e);
}
}
}
}
}
}
}
return params;
}
}

View File

@@ -0,0 +1,223 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.domain.AlipayTradePagePayModel;
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.domain.AlipayTradeQueryModel;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.ijpay.alipay.AliPayApi;
/**
* 支付宝支付控制器
* 基于IJPay实现
*/
@RestController
@RequestMapping("/api/payments/alipay")
public class AlipayController {
private static final Logger logger = LoggerFactory.getLogger(AlipayController.class);
@Autowired
private com.example.demo.config.PaymentConfig.AliPayConfig aliPayConfig;
/**
* PC网页支付
*/
@PostMapping("/pc-pay")
public void pcPay(@RequestParam String outTradeNo,
@RequestParam String totalAmount,
@RequestParam String subject,
@RequestParam String body,
HttpServletResponse response) {
try {
String returnUrl = aliPayConfig.getDomain() + "/api/payments/alipay/return";
String notifyUrl = aliPayConfig.getDomain() + "/api/payments/alipay/notify";
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(outTradeNo);
model.setProductCode("FAST_INSTANT_TRADE_PAY");
model.setTotalAmount(totalAmount);
model.setSubject(subject);
model.setBody(body);
AliPayApi.tradePage(response, model, notifyUrl, returnUrl);
logger.info("PC支付页面跳转成功: {}", outTradeNo);
} catch (Exception e) {
logger.error("PC支付失败", e);
}
}
/**
* 手机网页支付
*/
@PostMapping("/wap-pay")
public void wapPay(@RequestParam String outTradeNo,
@RequestParam String totalAmount,
@RequestParam String subject,
@RequestParam String body,
HttpServletResponse response) {
try {
String returnUrl = aliPayConfig.getDomain() + "/api/payments/alipay/return";
String notifyUrl = aliPayConfig.getDomain() + "/api/payments/alipay/notify";
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(outTradeNo);
model.setProductCode("QUICK_WAP_PAY");
model.setTotalAmount(totalAmount);
model.setSubject(subject);
model.setBody(body);
AliPayApi.wapPay(response, model, returnUrl, notifyUrl);
logger.info("手机支付页面跳转成功: {}", outTradeNo);
} catch (Exception e) {
logger.error("手机支付失败", e);
}
}
/**
* APP支付
*/
@PostMapping("/app-pay")
public ResponseEntity<Map<String, Object>> appPay(@RequestParam String outTradeNo,
@RequestParam String totalAmount,
@RequestParam String subject,
@RequestParam String body) {
Map<String, Object> response = new HashMap<>();
try {
String notifyUrl = aliPayConfig.getDomain() + "/api/payments/alipay/notify";
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo(outTradeNo);
model.setProductCode("QUICK_MSECURITY_PAY");
model.setTotalAmount(totalAmount);
model.setSubject(subject);
model.setBody(body);
model.setTimeoutExpress("30m");
String orderInfo = AliPayApi.appPayToResponse(model, notifyUrl).getBody();
response.put("success", true);
response.put("orderInfo", orderInfo);
logger.info("APP支付订单创建成功: {}", outTradeNo);
} catch (Exception e) {
logger.error("APP支付失败", e);
response.put("success", false);
response.put("message", "支付失败: " + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 扫码支付
*/
@PostMapping("/qr-pay")
public ResponseEntity<Map<String, Object>> qrPay(@RequestParam String outTradeNo,
@RequestParam String totalAmount,
@RequestParam String subject,
@RequestParam String body) {
Map<String, Object> response = new HashMap<>();
try {
String notifyUrl = aliPayConfig.getDomain() + "/api/payments/alipay/notify";
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setOutTradeNo(outTradeNo);
model.setTotalAmount(totalAmount);
model.setSubject(subject);
model.setBody(body);
model.setTimeoutExpress("5m");
String qrCode = AliPayApi.tradePrecreatePayToResponse(model, notifyUrl).getBody();
response.put("success", true);
response.put("qrCode", qrCode);
logger.info("扫码支付订单创建成功: {}", outTradeNo);
} catch (Exception e) {
logger.error("扫码支付失败", e);
response.put("success", false);
response.put("message", "支付失败: " + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 查询订单
*/
@GetMapping("/query")
public ResponseEntity<Map<String, Object>> queryOrder(@RequestParam(required = false) String outTradeNo,
@RequestParam(required = false) String tradeNo) {
Map<String, Object> response = new HashMap<>();
try {
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
if (outTradeNo != null) {
model.setOutTradeNo(outTradeNo);
}
if (tradeNo != null) {
model.setTradeNo(tradeNo);
}
String result = AliPayApi.tradeQueryToResponse(model).getBody();
response.put("success", true);
response.put("data", result);
logger.info("订单查询成功: outTradeNo={}, tradeNo={}", outTradeNo, tradeNo);
} catch (Exception e) {
logger.error("订单查询失败", e);
response.put("success", false);
response.put("message", "查询失败: " + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 退款
*/
@PostMapping("/refund")
public ResponseEntity<Map<String, Object>> refund(@RequestParam(required = false) String outTradeNo,
@RequestParam(required = false) String tradeNo,
@RequestParam String refundAmount,
@RequestParam String refundReason) {
Map<String, Object> response = new HashMap<>();
try {
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
if (outTradeNo != null) {
model.setOutTradeNo(outTradeNo);
}
if (tradeNo != null) {
model.setTradeNo(tradeNo);
}
model.setRefundAmount(refundAmount);
model.setRefundReason(refundReason);
String result = AliPayApi.tradeRefundToResponse(model).getBody();
response.put("success", true);
response.put("data", result);
logger.info("退款申请成功: outTradeNo={}, tradeNo={}, amount={}", outTradeNo, tradeNo, refundAmount);
} catch (Exception e) {
logger.error("退款失败", e);
response.put("success", false);
response.put("message", "退款失败: " + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 支付同步回调和异步回调已移至 AlipayCallbackController
* 请使用 /api/payments/alipay/return 和 /api/payments/alipay/notify
*
* @deprecated 此方法已移除,请使用 AlipayCallbackController
*/
}

View File

@@ -0,0 +1,228 @@
package com.example.demo.controller;
import com.example.demo.repository.UserActivityStatsRepository;
import com.example.demo.service.UserActivityStatsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/analytics")
@CrossOrigin(origins = "*")
public class AnalyticsApiController {
@Autowired
private UserActivityStatsRepository userActivityStatsRepository;
@Autowired
private UserActivityStatsService userActivityStatsService;
/**
* 获取日活用户趋势数据
*/
@GetMapping("/daily-active-users")
public ResponseEntity<Map<String, Object>> getDailyActiveUsersTrend(
@RequestParam(defaultValue = "2024") String year,
@RequestParam(defaultValue = "monthly") String granularity) {
try {
Map<String, Object> response = new HashMap<>();
if ("monthly".equals(granularity)) {
// 按月聚合数据
List<Map<String, Object>> monthlyData = userActivityStatsRepository.findMonthlyActiveUsers(Integer.parseInt(year));
// 确保12个月都有数据
List<Map<String, Object>> completeData = new ArrayList<>();
for (int month = 1; month <= 12; month++) {
final int currentMonth = month;
Optional<Map<String, Object>> monthData = monthlyData.stream()
.filter(data -> {
Object monthObj = data.get("month");
if (monthObj instanceof Number) {
return ((Number) monthObj).intValue() == currentMonth;
}
return false;
})
.findFirst();
if (monthData.isPresent()) {
completeData.add(monthData.get());
} else {
Map<String, Object> emptyMonth = new HashMap<>();
emptyMonth.put("month", currentMonth);
emptyMonth.put("dailyActiveUsers", 0);
emptyMonth.put("avgDailyActive", 0.0);
completeData.add(emptyMonth);
}
}
response.put("monthlyData", completeData);
} else {
// 按日返回数据
List<Map<String, Object>> dailyData = userActivityStatsRepository.findDailyActiveUsersByYear(Integer.parseInt(year));
response.put("dailyData", dailyData);
}
response.put("year", year);
response.put("granularity", granularity);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取日活用户趋势数据失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 手动触发重新计算日活用户统计数据
* @param days 要重新计算的天数默认30天
*/
@PostMapping("/recalculate-daily-active")
public ResponseEntity<Map<String, Object>> recalculateDailyActiveUsers(
@RequestParam(defaultValue = "30") int days) {
try {
Map<String, Object> response = new HashMap<>();
userActivityStatsService.calculateDailyActiveUsersForRecentDays(days);
response.put("success", true);
response.put("message", "已重新计算最近 " + days + " 天的日活用户数据");
response.put("days", days);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("error", "重新计算日活用户数据失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 获取今日实时日活用户数
*/
@GetMapping("/today-active-users")
public ResponseEntity<Map<String, Object>> getTodayActiveUsers() {
try {
Map<String, Object> response = new HashMap<>();
int todayActiveUsers = userActivityStatsService.getTodayActiveUsersCount();
response.put("todayActiveUsers", todayActiveUsers);
response.put("date", LocalDate.now().toString());
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取今日日活用户数失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 获取用户活跃度概览
*/
@GetMapping("/user-activity-overview")
public ResponseEntity<Map<String, Object>> getUserActivityOverview() {
try {
Map<String, Object> response = new HashMap<>();
// 获取最新数据
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate lastYear = today.minusYears(1);
// 今日日活
Integer todayDAU = userActivityStatsRepository.findDailyActiveUsersByDate(today);
response.put("todayDAU", todayDAU != null ? todayDAU : 0);
// 昨日日活
Integer yesterdayDAU = userActivityStatsRepository.findDailyActiveUsersByDate(yesterday);
response.put("yesterdayDAU", yesterdayDAU != null ? yesterdayDAU : 0);
// 本月平均日活
Double monthlyAvgDAU = userActivityStatsRepository.findAverageDailyActiveUsersByMonth(today.getYear(), today.getMonthValue());
response.put("monthlyAvgDAU", monthlyAvgDAU != null ? monthlyAvgDAU : 0.0);
// 上月平均日活
LocalDate lastMonthDate = today.minusMonths(1);
Double lastMonthAvgDAU = userActivityStatsRepository.findAverageDailyActiveUsersByMonth(lastMonthDate.getYear(), lastMonthDate.getMonthValue());
response.put("lastMonthAvgDAU", lastMonthAvgDAU != null ? lastMonthAvgDAU : 0.0);
// 年度平均日活
Double yearlyAvgDAU = userActivityStatsRepository.findAverageDailyActiveUsersByYear(today.getYear());
response.put("yearlyAvgDAU", yearlyAvgDAU != null ? yearlyAvgDAU : 0.0);
// 计算增长率
if (yesterdayDAU != null && yesterdayDAU > 0) {
double dayGrowthRate = ((todayDAU != null ? todayDAU : 0) - yesterdayDAU) / (double) yesterdayDAU * 100;
response.put("dayGrowthRate", Math.round(dayGrowthRate * 100.0) / 100.0);
} else {
response.put("dayGrowthRate", 0.0);
}
if (lastMonthAvgDAU != null && lastMonthAvgDAU > 0) {
double monthGrowthRate = ((monthlyAvgDAU != null ? monthlyAvgDAU : 0) - lastMonthAvgDAU) / lastMonthAvgDAU * 100;
response.put("monthGrowthRate", Math.round(monthGrowthRate * 100.0) / 100.0);
} else {
response.put("monthGrowthRate", 0.0);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取用户活跃度概览失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 获取用户活跃度热力图数据
*/
@GetMapping("/user-activity-heatmap")
public ResponseEntity<Map<String, Object>> getUserActivityHeatmap(
@RequestParam(defaultValue = "2024") String year) {
try {
Map<String, Object> response = new HashMap<>();
// 获取全年每日数据
List<Map<String, Object>> dailyData = userActivityStatsRepository.findDailyActiveUsersByYear(Integer.parseInt(year));
// 转换为热力图格式
List<List<Object>> heatmapData = new ArrayList<>();
for (Map<String, Object> data : dailyData) {
List<Object> point = new ArrayList<>();
point.add(data.get("dayOfYear")); // 一年中的第几天
point.add(data.get("weekOfYear")); // 一年中的第几周
point.add(data.get("dailyActiveUsers")); // 日活用户数
heatmapData.add(point);
}
response.put("heatmapData", heatmapData);
response.put("year", year);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取用户活跃度热力图数据失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
}

View File

@@ -0,0 +1,177 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.config.DynamicApiConfig;
import com.example.demo.model.SystemSettings;
import com.example.demo.service.SystemSettingsService;
/**
* API密钥管理控制器
* 配置保存到数据库,支持运行时更新,重启后自动加载
*/
@RestController
@RequestMapping("/api/api-key")
@CrossOrigin(origins = "*")
public class ApiKeyController {
private static final Logger logger = LoggerFactory.getLogger(ApiKeyController.class);
@Autowired
private DynamicApiConfig dynamicApiConfig;
@Autowired
private SystemSettingsService systemSettingsService;
/**
* 获取当前API密钥配置仅显示部分用于验证
*/
@GetMapping
public ResponseEntity<Map<String, Object>> getApiKey() {
try {
Map<String, Object> response = new HashMap<>();
// 从动态配置获取当前使用的 API Key
String currentApiKey = dynamicApiConfig.getApiKey();
String currentApiBaseUrl = dynamicApiConfig.getApiBaseUrl();
// 只返回密钥的前4位和后4位中间用*代替
if (currentApiKey != null && currentApiKey.length() > 8) {
String masked = currentApiKey.substring(0, 4) + "****" + currentApiKey.substring(currentApiKey.length() - 4);
response.put("maskedKey", masked);
} else {
response.put("maskedKey", "****");
}
response.put("apiBaseUrl", currentApiBaseUrl);
// 从数据库获取 Token 过期时间
SystemSettings settings = systemSettingsService.getOrCreate();
response.put("tokenExpireHours", settings.getTokenExpireHours());
response.put("success", true);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取API密钥失败", e);
Map<String, Object> error = new HashMap<>();
error.put("error", "获取API密钥失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 更新API密钥配置到数据库立即生效重启后自动加载
*/
@PutMapping
public ResponseEntity<Map<String, Object>> updateApiKey(@RequestBody Map<String, Object> request) {
try {
String newApiKey = (String) request.get("apiKey");
String newApiBaseUrl = (String) request.get("apiBaseUrl");
Object tokenExpireObj = request.get("tokenExpireHours");
// 验证API密钥
if (newApiKey != null && newApiKey.trim().isEmpty()) {
newApiKey = null;
}
// 验证API基础URL
if (newApiBaseUrl != null && newApiBaseUrl.trim().isEmpty()) {
newApiBaseUrl = null;
}
// 验证Token过期时间
Integer tokenExpireHours = null;
if (tokenExpireObj != null) {
if (tokenExpireObj instanceof Number) {
tokenExpireHours = ((Number) tokenExpireObj).intValue();
} else if (tokenExpireObj instanceof String) {
try {
tokenExpireHours = Integer.parseInt((String) tokenExpireObj);
} catch (NumberFormatException e) {
// 忽略无效值
}
}
// 验证范围
if (tokenExpireHours != null && (tokenExpireHours < 1 || tokenExpireHours > 720)) {
tokenExpireHours = null;
}
}
// 如果都没有提供,返回错误
if (newApiKey == null && newApiBaseUrl == null && tokenExpireHours == null) {
Map<String, Object> error = new HashMap<>();
error.put("error", "至少需要提供一个配置项");
error.put("message", "请提供API密钥、API基础URL或Token过期时间");
return ResponseEntity.badRequest().body(error);
}
// 获取系统设置
SystemSettings settings = systemSettingsService.getOrCreate();
StringBuilder message = new StringBuilder();
// 更新API密钥
if (newApiKey != null) {
newApiKey = newApiKey.trim();
settings.setAiApiKey(newApiKey);
// 动态更新运行时配置,立即生效
dynamicApiConfig.updateApiKey(newApiKey);
logger.info("✅ API密钥已保存到数据库并立即生效");
message.append("API密钥已更新。");
}
// 更新API基础URL
if (newApiBaseUrl != null) {
newApiBaseUrl = newApiBaseUrl.trim();
settings.setAiApiBaseUrl(newApiBaseUrl);
// 动态更新运行时配置,立即生效
dynamicApiConfig.updateApiBaseUrl(newApiBaseUrl);
logger.info("✅ API基础URL已保存到数据库并立即生效: {}", newApiBaseUrl);
message.append("API基础URL已更新。");
}
// 更新Token过期时间
if (tokenExpireHours != null) {
settings.setTokenExpireHours(tokenExpireHours);
logger.info("✅ Token过期时间已保存到数据库: {} 小时", tokenExpireHours);
message.append("Token过期时间已更新为" + tokenExpireHours + "小时。");
}
// 保存到数据库
systemSettingsService.update(settings);
message.append("配置已保存到数据库,立即生效且重启后自动加载。");
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", message.toString());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("更新配置失败", e);
Map<String, Object> error = new HashMap<>();
error.put("error", "更新配置失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
}

View File

@@ -0,0 +1,222 @@
package com.example.demo.controller;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.TaskQueue;
import com.example.demo.repository.ImageToVideoTaskRepository;
import com.example.demo.repository.TaskQueueRepository;
import com.example.demo.repository.TextToVideoTaskRepository;
/**
* API监测控制器
* 用于监测API调用状态和系统健康状态
*/
@RestController
@RequestMapping("/api/monitor")
public class ApiMonitorController {
private static final Logger logger = LoggerFactory.getLogger(ApiMonitorController.class);
@Autowired
private TaskQueueRepository taskQueueRepository;
@Autowired
private TextToVideoTaskRepository textToVideoTaskRepository;
@Autowired
private ImageToVideoTaskRepository imageToVideoTaskRepository;
/**
* 获取系统整体状态
*/
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getSystemStatus() {
Map<String, Object> response = new HashMap<>();
try {
// 统计任务队列状态
long pendingCount = taskQueueRepository.countByStatus(TaskQueue.QueueStatus.PENDING);
long processingCount = taskQueueRepository.countByStatus(TaskQueue.QueueStatus.PROCESSING);
long completedCount = taskQueueRepository.countByStatus(TaskQueue.QueueStatus.COMPLETED);
long failedCount = taskQueueRepository.countByStatus(TaskQueue.QueueStatus.FAILED);
long timeoutCount = taskQueueRepository.countByStatus(TaskQueue.QueueStatus.TIMEOUT);
// 统计原始任务状态
long textToVideoTotal = textToVideoTaskRepository.count();
long imageToVideoTotal = imageToVideoTaskRepository.count();
response.put("success", true);
response.put("timestamp", LocalDateTime.now());
response.put("system", Map.of(
"status", "running",
"uptime", System.currentTimeMillis()
));
response.put("taskQueue", Map.of(
"pending", pendingCount,
"processing", processingCount,
"completed", completedCount,
"failed", failedCount,
"timeout", timeoutCount,
"total", pendingCount + processingCount + completedCount + failedCount + timeoutCount
));
response.put("originalTasks", Map.of(
"textToVideo", textToVideoTotal,
"imageToVideo", imageToVideoTotal,
"total", textToVideoTotal + imageToVideoTotal
));
logger.info("系统状态检查完成: 队列任务={}, 原始任务={}",
pendingCount + processingCount + completedCount + failedCount + timeoutCount,
textToVideoTotal + imageToVideoTotal);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取系统状态失败", e);
response.put("success", false);
response.put("error", e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 获取正在处理的任务详情
*/
@GetMapping("/processing-tasks")
public ResponseEntity<Map<String, Object>> getProcessingTasks() {
Map<String, Object> response = new HashMap<>();
try {
List<TaskQueue> processingTasks = taskQueueRepository.findByStatus(TaskQueue.QueueStatus.PROCESSING);
response.put("success", true);
response.put("count", processingTasks.size());
response.put("tasks", processingTasks.stream().map(task -> {
Map<String, Object> taskInfo = new HashMap<>();
taskInfo.put("taskId", task.getTaskId());
taskInfo.put("taskType", task.getTaskType());
taskInfo.put("realTaskId", task.getRealTaskId());
taskInfo.put("status", task.getStatus());
taskInfo.put("createdAt", task.getCreatedAt());
taskInfo.put("checkCount", task.getCheckCount());
taskInfo.put("lastCheckTime", task.getLastCheckTime());
return taskInfo;
}).toList());
logger.info("获取正在处理的任务: {} 个", processingTasks.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取正在处理的任务失败", e);
response.put("success", false);
response.put("error", e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 获取最近的任务活动
*/
@GetMapping("/recent-activities")
public ResponseEntity<Map<String, Object>> getRecentActivities() {
Map<String, Object> response = new HashMap<>();
try {
// 获取最近1小时的任务
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
List<TaskQueue> recentTasks = taskQueueRepository.findByCreatedAtAfter(oneHourAgo);
response.put("success", true);
response.put("timeRange", "最近1小时");
response.put("count", recentTasks.size());
response.put("activities", recentTasks.stream().map(task -> {
Map<String, Object> activity = new HashMap<>();
activity.put("taskId", task.getTaskId());
activity.put("taskType", task.getTaskType());
activity.put("status", task.getStatus());
activity.put("createdAt", task.getCreatedAt());
activity.put("realTaskId", task.getRealTaskId());
return activity;
}).toList());
logger.info("获取最近活动: {} 个任务", recentTasks.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取最近活动失败", e);
response.put("success", false);
response.put("error", e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 测试外部API连接
*/
@GetMapping("/test-external-api")
public ResponseEntity<Map<String, Object>> testExternalApi() {
Map<String, Object> response = new HashMap<>();
try {
logger.info("开始测试外部API连接");
// 这里可以调用一个简单的API来测试连接
// 由于我们没有具体的测试端点,我们返回配置信息
response.put("success", true);
response.put("message", "外部API配置正常");
response.put("apiBaseUrl", "http://116.62.4.26:8081");
response.put("apiKey", "sk-5wOaLydIpNwJXcObtfzSCRWycZgUz90miXfMPOt9KAhLo1T0".substring(0, 10) + "...");
response.put("timestamp", LocalDateTime.now());
logger.info("外部API连接测试完成");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("测试外部API连接失败", e);
response.put("success", false);
response.put("error", e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 获取错误统计
*/
@GetMapping("/error-stats")
public ResponseEntity<Map<String, Object>> getErrorStats() {
Map<String, Object> response = new HashMap<>();
try {
long failedCount = taskQueueRepository.countByStatus(TaskQueue.QueueStatus.FAILED);
long timeoutCount = taskQueueRepository.countByStatus(TaskQueue.QueueStatus.TIMEOUT);
response.put("success", true);
response.put("failedTasks", failedCount);
response.put("timeoutTasks", timeoutCount);
response.put("totalErrors", failedCount + timeoutCount);
response.put("timestamp", LocalDateTime.now());
logger.info("错误统计: 失败={}, 超时={}", failedCount, timeoutCount);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取错误统计失败", e);
response.put("success", false);
response.put("error", e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
}

View File

@@ -0,0 +1,648 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.User;
import com.example.demo.model.SystemSettings;
import com.example.demo.model.UserMembership;
import com.example.demo.model.MembershipLevel;
import com.example.demo.service.RedisTokenService;
import com.example.demo.service.SystemSettingsService;
import com.example.demo.service.UserService;
import com.example.demo.service.VerificationCodeService;
import com.example.demo.util.JwtUtils;
import com.example.demo.util.UserIdGenerator;
import com.example.demo.repository.UserMembershipRepository;
import com.example.demo.repository.MembershipLevelRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.Optional;
@RestController
@RequestMapping("/api/auth")
public class AuthApiController {
private static final Logger logger = LoggerFactory.getLogger(AuthApiController.class);
@Autowired
private UserService userService;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private VerificationCodeService verificationCodeService;
@Autowired
private RedisTokenService redisTokenService;
@Autowired
private SystemSettingsService systemSettingsService;
@Autowired
private UserMembershipRepository userMembershipRepository;
@Autowired
private MembershipLevelRepository membershipLevelRepository;
/**
* 用户登录(已禁用,仅支持邮箱验证码登录)
* 为了向后兼容,保留此接口但返回提示信息
*/
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> credentials,
HttpServletRequest request,
HttpServletResponse response) {
try {
// 支持使用邮箱+密码登录(账号就是邮箱)或用户名+密码
String emailOrUsername = credentials.get("email");
if (emailOrUsername == null || emailOrUsername.trim().isEmpty()) {
// 兼容旧客户端使用 username 字段
emailOrUsername = credentials.get("username");
}
String password = credentials.get("password");
if (emailOrUsername == null || emailOrUsername.trim().isEmpty() || password == null) {
return ResponseEntity.badRequest().body(createErrorResponse("邮箱/用户名或密码不能为空"));
}
// 先尝试按邮箱查找用户
com.example.demo.model.User user = userService.findByEmailOrNull(emailOrUsername);
if (user == null) {
// 再按用户名查找
user = userService.findByUsernameOrNull(emailOrUsername);
}
if (user == null) {
return ResponseEntity.badRequest().body(createErrorResponse("用户不存在"));
}
// 检查密码
if (!userService.checkPassword(password, user.getPasswordHash())) {
return ResponseEntity.badRequest().body(createErrorResponse("邮箱/用户名或密码不正确"));
}
// 检查用户是否被封禁
if (user.getIsActive() == null || !user.getIsActive()) {
return ResponseEntity.badRequest().body(createErrorResponse("您的账号已被封禁,请联系管理员"));
}
// 获取动态配置的过期时间确保不为0或null默认24小时
SystemSettings settings = systemSettingsService.getOrCreate();
int expireHours = (settings.getTokenExpireHours() != null && settings.getTokenExpireHours() > 0) ? settings.getTokenExpireHours() : 24;
long expireMs = expireHours * 60L * 60L * 1000L; // 转换为毫秒
long expireSeconds = expireHours * 60L * 60L; // 转换为秒
// 生成JWT Token为了兼容系统中其他逻辑使用 user.getUsername() 作为 token subject
logger.info("生成JWT: username={}, expireMs={}, expireHours={}", user.getUsername(), expireMs, expireHours);
String token = jwtUtils.generateToken(user.getUsername(), user.getRole(), user.getId(), expireMs);
// 验证刚生成的token是否有效
try {
java.util.Date expDate = jwtUtils.getExpirationDateFromToken(token);
boolean isExpired = jwtUtils.isTokenExpired(token);
logger.info("JWT验证: 过期时间={}, 当前时间={}, 是否过期={}", expDate, new java.util.Date(), isExpired);
} catch (Exception e) {
logger.error("JWT验证失败: {}", e.getMessage());
}
// 将 token 保存到 Redis
redisTokenService.saveToken(user.getUsername(), token, expireSeconds);
Map<String, Object> body = new HashMap<>();
body.put("success", true);
body.put("message", "登录成功");
Map<String, Object> data = new HashMap<>();
// 不返回密码哈希
user.setPasswordHash(null);
data.put("user", user);
data.put("token", token);
body.put("data", data);
return ResponseEntity.ok(body);
} catch (Exception e) {
logger.error("用户名密码登录出错:", e);
return ResponseEntity.badRequest().body(createErrorResponse("登录失败:" + e.getMessage()));
}
}
/**
* 验证码登录(邮箱)
*/
@PostMapping("/login/email")
public ResponseEntity<Map<String, Object>> loginWithEmail(@RequestBody Map<String, String> credentials) {
try {
String email = credentials.get("email");
String code = credentials.get("code");
if (email == null || email.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(createErrorResponse("邮箱不能为空"));
}
if (code == null || code.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(createErrorResponse("验证码不能为空"));
}
// 验证邮箱验证码
if (!verificationCodeService.verifyEmailCode(email, code)) {
return ResponseEntity.badRequest()
.body(createErrorResponse("验证码错误或已过期"));
}
// 查找用户,如果不存在则自动注册
User user = userService.findByEmail(email);
if (user != null) {
// 检查用户是否被封禁
if (user.getIsActive() == null || !user.getIsActive()) {
return ResponseEntity.badRequest().body(createErrorResponse("您的账号已被封禁,请联系管理员"));
}
}
if (user == null) {
// 自动注册新用户
try {
// 从邮箱生成用户名(去掉@符号及后面的部分)
String emailPrefix = email.split("@")[0];
String emailDomain = email.contains("@") ? email.split("@")[1] : "";
// 清理邮箱前缀(移除特殊字符,只保留字母、数字、下划线)
String cleanPrefix = emailPrefix.replaceAll("[^a-zA-Z0-9_]", "_");
// 生成基础用户名:优先使用邮箱前缀
String baseUsername = cleanPrefix;
// 确保用户名不为空且至少3个字符
if (baseUsername.length() < 3) {
baseUsername = baseUsername + "_" + System.currentTimeMillis() % 1000;
}
// 确保用户名长度不超过50个字符
if (baseUsername.length() > 50) {
baseUsername = baseUsername.substring(0, 50);
}
// 确保用户名唯一
// 策略:如果基础用户名已存在,添加邮箱域名的简短标识或数字后缀
String username = baseUsername;
int counter = 1;
int maxAttempts = 1000;
while (userService.findByUsernameOrNull(username) != null && counter <= maxAttempts) {
// 尝试策略1添加邮箱域名的简短标识如 qq, 163
if (counter == 1 && emailDomain.length() > 0) {
String domainShort = emailDomain.split("\\.")[0]; // 获取域名第一部分
if (domainShort.length() > 0 && domainShort.length() <= 5) {
String candidateUsername = baseUsername.length() > 45 ?
baseUsername.substring(0, 45) : baseUsername;
candidateUsername = candidateUsername + "_" + domainShort;
if (candidateUsername.length() <= 50 &&
userService.findByUsernameOrNull(candidateUsername) == null) {
username = candidateUsername;
break;
}
}
}
// 策略2添加数字后缀
String baseForSuffix = baseUsername.length() > 45 ?
baseUsername.substring(0, 45) : baseUsername;
String newUsername = baseForSuffix + counter;
if (newUsername.length() > 50) {
newUsername = newUsername.substring(0, 50);
}
username = newUsername;
counter++;
}
// 如果还是冲突,使用时间戳确保唯一性
if (counter > maxAttempts || userService.findByUsernameOrNull(username) != null) {
username = "user_" + System.currentTimeMillis();
logger.warn("邮箱验证码登录 - 用户名冲突过多,使用时间戳生成: '{}'", username);
}
// 直接创建用户对象并设置所有必要字段
user = new User();
// 生成唯一用户ID重试最多10次确保唯一性
String generatedUserId = null;
for (int i = 0; i < 10; i++) {
generatedUserId = UserIdGenerator.generate();
if (!userService.existsByUserId(generatedUserId)) {
break;
}
logger.warn("邮箱验证码登录 - 用户ID冲突重新生成");
}
user.setUserId(generatedUserId);
logger.info("邮箱验证码登录 - 生成用户ID: {}", generatedUserId);
user.setUsername(username);
user.setEmail(email);
user.setPasswordHash(""); // 邮箱登录不需要密码
user.setRole("ROLE_USER"); // 默认为普通用户
user.setPoints(50); // 默认50积分
user.setFrozenPoints(0); // 默认冻结积分为0
user.setNickname(username); // 默认昵称为用户名
user.setIsActive(true);
// 保存用户(@PrePersist 会自动设置 createdAt 等字段)
user = userService.save(user);
// 为新用户创建默认会员记录标准会员到期时间为1年后
createDefaultMembershipForUser(user);
} catch (IllegalArgumentException e) {
logger.error("自动注册用户失败: {}", email, e);
return ResponseEntity.badRequest()
.body(createErrorResponse("用户注册失败:" + e.getMessage()));
} catch (Exception e) {
logger.error("自动注册用户失败: {}", email, e);
return ResponseEntity.badRequest()
.body(createErrorResponse("用户注册失败:" + e.getMessage()));
}
}
// 获取动态配置的过期时间确保不为0或null默认24小时
SystemSettings settings = systemSettingsService.getOrCreate();
int expireHours = (settings.getTokenExpireHours() != null && settings.getTokenExpireHours() > 0) ? settings.getTokenExpireHours() : 24;
long expireMs = expireHours * 60L * 60L * 1000L; // 转换为毫秒
long expireSeconds = expireHours * 60L * 60L; // 转换为秒
// 生成JWT Token
String token = jwtUtils.generateToken(user.getUsername(), user.getRole(), user.getId(), expireMs);
// 将 token 保存到 Redis
redisTokenService.saveToken(user.getUsername(), token, expireSeconds);
// 检查是否需要设置密码(首次登录的用户密码为空)
boolean needsPasswordChange = user.getPasswordHash() == null || user.getPasswordHash().isEmpty();
Map<String, Object> body = new HashMap<>();
body.put("success", true);
body.put("message", "登录成功");
Map<String, Object> data = new HashMap<>();
data.put("user", user);
data.put("token", token);
data.put("needsPasswordChange", needsPasswordChange); // 告诉前端是否需要修改密码
body.put("data", data);
return ResponseEntity.ok(body);
} catch (Exception e) {
logger.error("邮箱验证码登录失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("登录失败:" + e.getMessage()));
}
}
/**
* 用户注册
*/
@PostMapping("/register")
public ResponseEntity<Map<String, Object>> register(@RequestBody Map<String, String> requestData) {
try {
String username = requestData.get("username");
String email = requestData.get("email");
String password = requestData.get("password");
// 验证必填字段
if (username == null || username.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(createErrorResponse("用户名不能为空"));
}
if (email == null || email.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(createErrorResponse("邮箱不能为空"));
}
if (password == null || password.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(createErrorResponse("密码不能为空"));
}
// 检查用户名是否已存在
User existingUser = userService.findByUsernameOrNull(username);
if (existingUser != null) {
return ResponseEntity.badRequest()
.body(createErrorResponse("用户名已存在"));
}
// 检查邮箱是否已存在
User existingEmail = userService.findByEmailOrNull(email);
if (existingEmail != null) {
return ResponseEntity.badRequest()
.body(createErrorResponse("邮箱已存在"));
}
// 使用 register 方法创建用户(会自动编码密码)
User savedUser = userService.register(username, email, password);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "注册成功");
// 不返回密码哈希
savedUser.setPasswordHash(null);
response.put("data", savedUser);
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
logger.warn("注册失败:{}", e.getMessage());
return ResponseEntity.badRequest()
.body(createErrorResponse(e.getMessage()));
} catch (Exception e) {
logger.error("注册失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("注册失败:" + e.getMessage()));
}
}
/**
* 用户登出
*/
@PostMapping("/logout")
public ResponseEntity<Map<String, Object>> logout(Authentication authentication, HttpServletRequest request) {
try {
String username = null;
String token = null;
// 从请求头获取 token
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
// 从 token 中获取用户名
try {
username = jwtUtils.getUsernameFromToken(token);
} catch (Exception e) {
logger.warn("从 token 获取用户名失败: {}", e.getMessage());
}
}
// 如果从 token 获取失败,尝试从 Authentication 获取
if (username == null && authentication != null && authentication.isAuthenticated()) {
username = authentication.getName();
}
// 从 Redis 删除 token
if (username != null && token != null) {
redisTokenService.removeToken(username, token);
logger.info("用户登出成功token 已从 Redis 删除: username={}", username);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "登出成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("登出失败:", e);
Map<String, Object> response = new HashMap<>();
response.put("success", true); // 即使出错也返回成功,因为客户端无论如何都会清除本地 token
response.put("message", "登出成功");
return ResponseEntity.ok(response);
}
}
/**
* 强制登出所有设备
* 删除用户在 Redis 中的所有 token
*/
@PostMapping("/logout/all")
public ResponseEntity<Map<String, Object>> logoutAll(Authentication authentication) {
try {
if (authentication != null && authentication.isAuthenticated()) {
String username = authentication.getName();
redisTokenService.removeAllTokens(username);
logger.info("用户所有设备登出成功: username={}", username);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "所有设备已登出");
return ResponseEntity.ok(response);
}
return ResponseEntity.badRequest().body(createErrorResponse("用户未登录"));
} catch (Exception e) {
logger.error("强制登出所有设备失败:", e);
return ResponseEntity.badRequest().body(createErrorResponse("登出失败:" + e.getMessage()));
}
}
/**
* 获取当前用户信息
*/
@GetMapping("/me")
public ResponseEntity<Map<String, Object>> getCurrentUser(Authentication authentication,
jakarta.servlet.http.HttpServletRequest request) {
try {
if (authentication != null && authentication.isAuthenticated()) {
User user = null;
String username = authentication.getName();
try {
// 首先尝试通过用户名查找
user = userService.findByUsernameOrNull(username);
// 如果通过用户名找不到尝试从JWT token中获取用户ID
if (user == null) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Long userId = jwtUtils.getUserIdFromToken(token);
if (userId != null) {
logger.warn("通过用户名未找到用户尝试通过用户ID查找: {}", userId);
user = userService.findById(userId);
}
} catch (Exception e) {
logger.warn("从token获取用户ID失败: {}", e.getMessage());
}
}
}
// 如果通过用户名找不到,尝试通过邮箱查找(兼容邮箱登录的情况)
if (user == null && username.contains("@")) {
logger.warn("通过用户名未找到用户,尝试通过邮箱查找: {}", username);
user = userService.findByEmailOrNull(username);
}
if (user == null) {
logger.error("无法找到用户,用户名/邮箱: {}", username);
return ResponseEntity.badRequest()
.body(createErrorResponse("用户不存在: " + username));
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", user);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("查找用户失败: {}", username, e);
return ResponseEntity.badRequest()
.body(createErrorResponse("查找用户失败: " + e.getMessage()));
}
}
return ResponseEntity.badRequest()
.body(createErrorResponse("用户未登录"));
} catch (Exception e) {
logger.error("获取用户信息失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("获取用户信息失败: " + e.getMessage()));
}
}
/**
* 修改当前登录用户密码
* 支持首次设置密码isFirstTimeSetup=true时不验证旧密码
*/
@PostMapping("/change-password")
public ResponseEntity<Map<String, Object>> changePassword(@RequestBody Map<String, Object> requestBody,
Authentication authentication,
HttpServletRequest request) {
try {
String oldPassword = (String) requestBody.get("oldPassword");
String newPassword = (String) requestBody.get("newPassword");
Boolean isFirstTimeSetup = Boolean.TRUE.equals(requestBody.get("isFirstTimeSetup"));
// 尝试从 Spring Security 上下文中获取当前用户名
User user = null;
if (authentication != null && authentication.isAuthenticated()) {
String username = authentication.getName();
user = userService.findByUsernameOrNull(username);
}
// 如果通过用户名未找到再从JWT中解析用户ID
if (user == null) {
String authHeader = request.getHeader("Authorization");
String token = jwtUtils.extractTokenFromHeader(authHeader);
if (token != null) {
Long userId = jwtUtils.getUserIdFromToken(token);
if (userId != null) {
user = userService.findById(userId);
}
}
}
if (user == null) {
return ResponseEntity.badRequest()
.body(createErrorResponse("用户未登录或会话已失效"));
}
// changePassword 方法内部已处理首次设置密码场景
// 如果用户没有密码,可以直接设置新密码,不需要验证旧密码
userService.changePassword(user.getId(), isFirstTimeSetup ? null : oldPassword, newPassword);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "密码修改成功");
return ResponseEntity.ok(response);
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest()
.body(createErrorResponse(ex.getMessage()));
} catch (Exception e) {
logger.error("修改密码失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("修改密码失败:" + e.getMessage()));
}
}
/**
* 检查用户名是否存在
*/
@GetMapping("/public/users/exists/username")
public ResponseEntity<Map<String, Object>> checkUsernameExists(@RequestParam String value) {
try {
boolean exists = userService.findByUsername(value) != null;
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", Map.of("exists", exists));
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("检查用户名失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("检查用户名失败"));
}
}
/**
* 检查邮箱是否存在
*/
@GetMapping("/public/users/exists/email")
public ResponseEntity<Map<String, Object>> checkEmailExists(@RequestParam String value) {
try {
boolean exists = userService.findByEmail(value) != null;
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", Map.of("exists", exists));
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("检查邮箱失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("检查邮箱失败"));
}
}
/**
* 为新用户创建默认会员记录(入门版会员,永久有效)
*/
private void createDefaultMembershipForUser(User user) {
try {
// 查找入门版会员等级(后端标记仍为 free
Optional<MembershipLevel> freeLevel = membershipLevelRepository.findByName("free");
if (freeLevel.isEmpty()) {
logger.warn("未找到入门版会员等级(free),跳过创建会员记录");
return;
}
UserMembership membership = new UserMembership();
membership.setUserId(user.getId());
membership.setMembershipLevelId(freeLevel.get().getId());
membership.setStartDate(LocalDateTime.now());
membership.setEndDate(LocalDateTime.of(2099, 12, 31, 23, 59, 59)); // 入门版会员永久有效
membership.setStatus("ACTIVE");
membership.setAutoRenew(false);
membership.setCreatedAt(LocalDateTime.now());
userMembershipRepository.save(membership);
logger.info("✅ 为新用户创建默认会员记录: userId={}, level=入门版会员(永久有效)", user.getId());
} catch (Exception e) {
logger.error("创建默认会员记录失败: userId={}", user.getId(), e);
// 不抛出异常,允许用户注册成功
}
}
private Map<String, Object> createErrorResponse(String message) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", message);
return response;
}
}

View File

@@ -0,0 +1,76 @@
package com.example.demo.controller;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.demo.service.UserService;
@Controller
@Validated
public class AuthController {
private final UserService userService;
public AuthController(UserService userService) {
this.userService = userService;
}
public static class RegisterForm {
@NotBlank
@Size(min = 3, max = 50)
public String username;
@NotBlank
@Email
public String email;
@NotBlank
@Size(min = 6, max = 100)
public String password;
@NotBlank
@Size(min = 6, max = 100)
public String confirmPassword;
}
@GetMapping("/login")
public String loginPage() {
return "login";
}
@GetMapping("/register")
public String registerPage(Model model) {
model.addAttribute("form", new RegisterForm());
return "register";
}
@PostMapping("/register")
public String doRegister(@Valid @ModelAttribute("form") RegisterForm form, BindingResult bindingResult, Model model) {
if (!bindingResult.hasFieldErrors("password") && !bindingResult.hasFieldErrors("confirmPassword")) {
if (!form.password.equals(form.confirmPassword)) {
bindingResult.rejectValue("confirmPassword", "register.password.mismatch", "两次输入的密码不一致");
}
}
if (bindingResult.hasErrors()) {
return "register";
}
try {
userService.register(form.username, form.email, form.password);
} catch (IllegalArgumentException ex) {
model.addAttribute("error", ex.getMessage());
return "register";
}
return "redirect:/login?registered";
}
}

View File

@@ -0,0 +1,107 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.repository.ImageToVideoTaskRepository;
import com.example.demo.repository.TaskQueueRepository;
import com.example.demo.repository.TextToVideoTaskRepository;
import com.example.demo.service.TaskCleanupService;
/**
* 清理控制器
* 用于清理失败的任务和相关数据
*/
@RestController
@RequestMapping("/api/cleanup")
public class CleanupController {
@Autowired
private TaskQueueRepository taskQueueRepository;
@Autowired
private ImageToVideoTaskRepository imageToVideoTaskRepository;
@Autowired
private TextToVideoTaskRepository textToVideoTaskRepository;
@Autowired
private TaskCleanupService taskCleanupService;
/**
* 清理所有失败的任务
*/
@PostMapping("/failed-tasks")
public ResponseEntity<Map<String, Object>> cleanupFailedTasks() {
Map<String, Object> response = new HashMap<>();
try {
// 统计清理前的数量
long failedQueueCount = taskQueueRepository.findByStatus(com.example.demo.model.TaskQueue.QueueStatus.FAILED).size();
long failedImageCount = imageToVideoTaskRepository.findByStatus(com.example.demo.model.ImageToVideoTask.TaskStatus.FAILED).size();
long failedTextCount = textToVideoTaskRepository.findByStatus(com.example.demo.model.TextToVideoTask.TaskStatus.FAILED).size();
// 删除失败的任务队列记录
taskQueueRepository.deleteByStatus(com.example.demo.model.TaskQueue.QueueStatus.FAILED);
// 删除失败的图生视频任务
imageToVideoTaskRepository.deleteByStatus(com.example.demo.model.ImageToVideoTask.TaskStatus.FAILED.toString());
// 删除失败的文生视频任务
textToVideoTaskRepository.deleteByStatus(com.example.demo.model.TextToVideoTask.TaskStatus.FAILED.toString());
// 注意:积分冻结记录的清理需要根据实际业务需求实现
// 这里暂时注释掉避免引用不存在的Repository
// pointsFreezeRecordRepository.deleteByStatusIn(...)
response.put("success", true);
response.put("message", "失败任务清理完成");
response.put("cleanedQueueTasks", failedQueueCount);
response.put("cleanedImageTasks", failedImageCount);
response.put("cleanedTextTasks", failedTextCount);
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "清理失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 执行完整的任务清理
* 将成功任务导出到归档表,删除失败任务
*/
@PostMapping("/full-cleanup")
public ResponseEntity<Map<String, Object>> performFullCleanup() {
Map<String, Object> result = taskCleanupService.performFullCleanup();
return ResponseEntity.ok(result);
}
/**
* 清理指定用户的任务
*/
@PostMapping("/user-tasks/{username}")
public ResponseEntity<Map<String, Object>> cleanupUserTasks(@PathVariable String username) {
Map<String, Object> result = taskCleanupService.cleanupUserTasks(username);
return ResponseEntity.ok(result);
}
/**
* 获取清理统计信息
*/
@GetMapping("/cleanup-stats")
public ResponseEntity<Map<String, Object>> getCleanupStats() {
Map<String, Object> stats = taskCleanupService.getCleanupStats();
return ResponseEntity.ok(stats);
}
}

View File

@@ -0,0 +1,281 @@
package com.example.demo.controller;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.Order;
import com.example.demo.repository.MembershipLevelRepository;
import com.example.demo.repository.OrderRepository;
import com.example.demo.repository.PaymentRepository;
import com.example.demo.repository.UserMembershipRepository;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.DashboardService;
@RestController
@RequestMapping("/api/dashboard")
@CrossOrigin(origins = "*")
public class DashboardApiController {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private UserMembershipRepository userMembershipRepository;
@Autowired
private MembershipLevelRepository membershipLevelRepository;
@Autowired
private com.example.demo.service.UserService userService;
@Autowired
private DashboardService dashboardService;
// 获取仪表盘概览数据
@GetMapping("/overview")
public ResponseEntity<Map<String, Object>> getDashboardOverview() {
try {
// 使用 DashboardService 获取包含同比变化的完整数据
Map<String, Object> overview = dashboardService.getDashboardOverview();
return ResponseEntity.ok(overview);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取仪表盘数据失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 获取月度收入趋势数据
@GetMapping("/monthly-revenue")
public ResponseEntity<Map<String, Object>> getMonthlyRevenue(@RequestParam(defaultValue = "2024") String year) {
try {
Map<String, Object> response = new HashMap<>();
// 获取指定年份的月度收入数据
List<Map<String, Object>> monthlyData = paymentRepository.findMonthlyRevenueByYear(Integer.parseInt(year));
// 确保12个月都有数据
List<Map<String, Object>> completeData = new ArrayList<>();
for (int month = 1; month <= 12; month++) {
final int currentMonth = month;
Optional<Map<String, Object>> monthData = monthlyData.stream()
.filter(data -> {
Object monthObj = data.get("month");
if (monthObj instanceof Number) {
return ((Number) monthObj).intValue() == currentMonth;
}
return false;
})
.findFirst();
if (monthData.isPresent()) {
completeData.add(monthData.get());
} else {
Map<String, Object> emptyMonth = new HashMap<>();
emptyMonth.put("month", currentMonth);
emptyMonth.put("revenue", 0.0);
emptyMonth.put("orderCount", 0);
completeData.add(emptyMonth);
}
}
response.put("monthlyData", completeData);
response.put("year", year);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取月度收入数据失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 获取用户转化率数据
@GetMapping("/conversion-rate")
public ResponseEntity<Map<String, Object>> getConversionRate(@RequestParam(required = false) String year) {
try {
Map<String, Object> response = new HashMap<>();
// 总用户数
long totalUsers = userRepository.count();
// 付费用户数(使用 Payment 表统计,与卡片统计方式一致)
long paidUsers = paymentRepository.countDistinctUsersByStatus(com.example.demo.model.PaymentStatus.SUCCESS);
// 计算转化率
double conversionRate = totalUsers > 0 ? (double) paidUsers / totalUsers * 100 : 0.0;
response.put("totalUsers", totalUsers);
response.put("paidUsers", paidUsers);
response.put("conversionRate", Math.round(conversionRate * 100.0) / 100.0);
// 如果指定了年份,返回按月转化率数据
if (year != null && !year.isEmpty()) {
List<Map<String, Object>> monthlyConversion = getMonthlyConversionRate(Integer.parseInt(year));
response.put("monthlyData", monthlyConversion);
}
// 按会员等级统计
List<Map<String, Object>> membershipStats = membershipLevelRepository.findMembershipStats();
response.put("membershipStats", membershipStats);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取转化率数据失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 获取按月转化率数据(累计转化率:截止到该月底的累计付费用户/累计总用户)
// 只计算到当前月份,未来月份不显示
private List<Map<String, Object>> getMonthlyConversionRate(int year) {
List<Map<String, Object>> monthlyData = new ArrayList<>();
// 获取当前年月
LocalDate now = LocalDate.now();
int currentYear = now.getYear();
int currentMonth = now.getMonthValue();
// 确定要计算到哪个月份
int maxMonth;
if (year < currentYear) {
// 过去的年份计算全年12个月
maxMonth = 12;
} else if (year == currentYear) {
// 当前年份,只计算到当前月份
maxMonth = currentMonth;
} else {
// 未来年份,不返回数据
return monthlyData;
}
for (int month = 1; month <= maxMonth; month++) {
Map<String, Object> monthData = new HashMap<>();
monthData.put("month", month);
// 计算截止到该月底的累计总用户数
LocalDateTime monthEnd = LocalDateTime.of(year, month, 1, 0, 0, 0).plusMonths(1).minusSeconds(1);
long cumulativeTotalUsers = userRepository.countByCreatedAtBefore(monthEnd);
// 计算截止到该月底的累计付费用户数(使用 Payment 表统计,与卡片统计方式一致)
long cumulativePaidUsers = paymentRepository.countDistinctUsersByStatusAndPaidAtBefore(
com.example.demo.model.PaymentStatus.SUCCESS, monthEnd);
// 计算累计转化率
double monthConversionRate = cumulativeTotalUsers > 0 ? (double) cumulativePaidUsers / cumulativeTotalUsers * 100 : 0.0;
monthData.put("totalUsers", cumulativeTotalUsers);
monthData.put("paidUsers", cumulativePaidUsers);
monthData.put("conversionRate", Math.round(monthConversionRate * 100.0) / 100.0);
monthlyData.add(monthData);
}
return monthlyData;
}
// 获取最近订单数据
@GetMapping("/recent-orders")
public ResponseEntity<Map<String, Object>> getRecentOrders(@RequestParam(defaultValue = "10") int limit) {
try {
Map<String, Object> response = new HashMap<>();
// 获取最近的订单
List<Order> recentOrdersList = orderRepository.findRecentOrders(PageRequest.of(0, limit));
List<Map<String, Object>> recentOrders = recentOrdersList.stream()
.map(order -> {
Map<String, Object> orderMap = new HashMap<>();
orderMap.put("id", order.getId());
orderMap.put("orderNumber", order.getOrderNumber());
orderMap.put("totalAmount", order.getTotalAmount());
orderMap.put("status", order.getStatus());
orderMap.put("orderType", order.getOrderType());
orderMap.put("description", order.getDescription());
orderMap.put("createdAt", order.getCreatedAt());
orderMap.put("username", order.getUser() != null ? order.getUser().getUsername() : "未知用户");
return orderMap;
})
.collect(Collectors.toList());
response.put("recentOrders", recentOrders);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取最近订单失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 获取系统状态
@GetMapping("/system-status")
public ResponseEntity<Map<String, Object>> getSystemStatus() {
try {
Map<String, Object> response = new HashMap<>();
Map<String, Object> data = new HashMap<>();
// 当前在线用户基于最近10分钟内有活动的用户
long onlineUsers = userService.countOnlineUsers();
data.put("onlineUsers", onlineUsers);
data.put("maxUsers", 500);
// 系统运行时间从JVM启动时间计算
long uptimeMillis = java.lang.management.ManagementFactory.getRuntimeMXBean().getUptime();
long uptimeSeconds = uptimeMillis / 1000;
data.put("uptime", uptimeSeconds);
// 格式化的运行时间
long hours = uptimeSeconds / 3600;
long minutes = (uptimeSeconds % 3600) / 60;
data.put("uptimeFormatted", hours + "小时" + minutes + "");
// 数据库连接状态
data.put("databaseStatus", "正常");
// 服务状态
data.put("serviceStatus", "运行中");
response.put("success", true);
response.put("data", data);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("error", "获取系统状态失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
}

View File

@@ -0,0 +1,138 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.fasterxml.jackson.core.JsonParseException;
import jakarta.servlet.http.HttpServletRequest;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理 JSON 解析错误
* 这个异常通常是因为请求体中的 JSON 格式不正确
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleJsonParseException(
HttpMessageNotReadableException ex,
HttpServletRequest request) {
logger.error("=== JSON 解析错误 ===");
logger.error("请求URL: {}", request.getRequestURI());
logger.error("请求方法: {}", request.getMethod());
logger.error("Content-Type: {}", request.getContentType());
logger.error("错误消息: {}", ex.getMessage());
// 记录所有请求头
logger.error("请求头:");
request.getHeaderNames().asIterator().forEachRemaining(headerName -> {
logger.error(" {}: {}", headerName, request.getHeader(headerName));
});
// 获取根本原因
Throwable rootCause = ex.getRootCause();
if (rootCause != null) {
logger.error("根本原因: {}", rootCause.getMessage());
if (rootCause instanceof JsonParseException) {
JsonParseException jsonEx = (JsonParseException) rootCause;
logger.error("JSON解析位置: line {}, column {}",
jsonEx.getLocation().getLineNr(),
jsonEx.getLocation().getColumnNr());
}
}
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "请求数据格式错误JSON 格式不正确");
errorResponse.put("error", "InvalidJsonFormat");
errorResponse.put("details", "请检查发送的数据格式,确保是有效的 JSON");
errorResponse.put("path", request.getRequestURI());
if (rootCause != null && rootCause.getMessage() != null) {
// 提取有用的错误信息,但不暴露太多内部细节
String errorMsg = rootCause.getMessage();
if (errorMsg.length() > 200) {
errorMsg = errorMsg.substring(0, 200) + "...";
}
errorResponse.put("parseError", errorMsg);
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
/**
* 处理资源未找到异常 (404)
* 通常是扫描器或错误的URL
*/
@ExceptionHandler(org.springframework.web.servlet.resource.NoResourceFoundException.class)
public ResponseEntity<Map<String, Object>> handleNoResourceFoundException(
org.springframework.web.servlet.resource.NoResourceFoundException e,
HttpServletRequest request) {
// 这种错误通常不需要 ERROR 级别WARN 即可,且不需要打印堆栈
logger.warn("资源未找到 (404): {} {}", request.getMethod(), request.getRequestURI());
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "未找到资源");
errorResponse.put("path", request.getRequestURI());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
/**
* 处理不支持的请求方法异常(如 POST / 等)
* 这类请求通常是扫描器或误操作,静默返回 405 即可
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Map<String, Object>> handleMethodNotSupported(
HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
// 仅记录简单日志,不输出详细堆栈
logger.warn("不支持的请求方法: {} {}", request.getMethod(), request.getRequestURI());
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "Method Not Allowed");
errorResponse.put("path", request.getRequestURI());
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(errorResponse);
}
/**
* 处理其他所有异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception e, HttpServletRequest request) {
logger.error("全局异常处理器捕获到异常");
logger.error("请求URL: {}", request.getRequestURI());
logger.error("请求方法: {}", request.getMethod());
logger.error("异常类型: {}", e.getClass().getName());
logger.error("异常消息: {}", e.getMessage());
if (e.getCause() != null) {
logger.error("异常原因: {}", e.getCause().getMessage());
}
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "服务器内部错误: " + e.getMessage());
errorResponse.put("error", e.getClass().getSimpleName());
errorResponse.put("path", request.getRequestURI());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
}

View File

@@ -0,0 +1,162 @@
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.sql.DataSource;
import java.sql.Connection;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
/**
* 健康检查控制器
* 用于监控服务状态,无需认证
*/
@RestController
@RequestMapping("/api/health")
@Tag(name = "健康检查", description = "服务健康状态监控接口")
public class HealthCheckController {
@Autowired
private DataSource dataSource;
/**
* 健康检查接口
* @return 服务健康状态
*/
@GetMapping
@Operation(summary = "健康检查", description = "检查服务运行状态、数据库连接等")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "服务正常"),
@ApiResponse(responseCode = "503", description = "服务异常")
})
public ResponseEntity<Map<String, Object>> healthCheck() {
Map<String, Object> health = new HashMap<>();
boolean isHealthy = true;
try {
// 应用状态
health.put("status", "UP");
health.put("timestamp", LocalDateTime.now().toString());
health.put("service", "AIGC Platform API");
// 数据库连接检查
Map<String, Object> database = new HashMap<>();
try (Connection connection = dataSource.getConnection()) {
boolean isValid = connection.isValid(5); // 5秒超时
database.put("status", isValid ? "UP" : "DOWN");
database.put("type", "MySQL");
if (!isValid) {
isHealthy = false;
}
} catch (Exception e) {
database.put("status", "DOWN");
database.put("error", e.getMessage());
isHealthy = false;
}
health.put("database", database);
// 系统信息
Map<String, Object> system = new HashMap<>();
Runtime runtime = Runtime.getRuntime();
system.put("availableProcessors", runtime.availableProcessors());
system.put("totalMemory", runtime.totalMemory() / (1024 * 1024) + " MB");
system.put("freeMemory", runtime.freeMemory() / (1024 * 1024) + " MB");
system.put("maxMemory", runtime.maxMemory() / (1024 * 1024) + " MB");
health.put("system", system);
// 返回状态
if (isHealthy) {
return ResponseEntity.ok(health);
} else {
health.put("status", "DOWN");
return ResponseEntity.status(503).body(health);
}
} catch (Exception e) {
health.put("status", "DOWN");
health.put("error", e.getMessage());
return ResponseEntity.status(503).body(health);
}
}
/**
* 简单的健康检查(轻量级)
* @return 简单的健康状态
*/
@GetMapping("/ping")
@Operation(summary = "简单健康检查", description = "快速检查服务是否在线")
@ApiResponse(responseCode = "200", description = "服务在线")
public ResponseEntity<Map<String, String>> ping() {
Map<String, String> response = new HashMap<>();
response.put("status", "OK");
response.put("message", "pong");
response.put("timestamp", LocalDateTime.now().toString());
return ResponseEntity.ok(response);
}
/**
* 就绪检查Readiness Probe
* @return 服务是否就绪
*/
@GetMapping("/ready")
@Operation(summary = "就绪检查", description = "检查服务是否准备好处理请求")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "服务就绪"),
@ApiResponse(responseCode = "503", description = "服务未就绪")
})
public ResponseEntity<Map<String, Object>> readiness() {
Map<String, Object> readiness = new HashMap<>();
boolean isReady = true;
try {
// 检查数据库连接
try (Connection connection = dataSource.getConnection()) {
if (!connection.isValid(5)) {
isReady = false;
}
} catch (Exception e) {
isReady = false;
readiness.put("error", "Database connection failed: " + e.getMessage());
}
readiness.put("ready", isReady);
readiness.put("timestamp", LocalDateTime.now().toString());
if (isReady) {
return ResponseEntity.ok(readiness);
} else {
return ResponseEntity.status(503).body(readiness);
}
} catch (Exception e) {
readiness.put("ready", false);
readiness.put("error", e.getMessage());
return ResponseEntity.status(503).body(readiness);
}
}
/**
* 存活检查Liveness Probe
* @return 服务是否存活
*/
@GetMapping("/live")
@Operation(summary = "存活检查", description = "检查服务是否存活")
@ApiResponse(responseCode = "200", description = "服务存活")
public ResponseEntity<Map<String, Object>> liveness() {
Map<String, Object> liveness = new HashMap<>();
liveness.put("alive", true);
liveness.put("timestamp", LocalDateTime.now().toString());
return ResponseEntity.ok(liveness);
}
}

View File

@@ -0,0 +1,27 @@
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.example.demo.service.UserService;
@Controller
public class HomeController {
@Autowired
private UserService userService;
@GetMapping("/")
public String home(Model model) {
// 统计在线用户数最近10分钟内活跃的用户
long onlineUserCount = userService.countOnlineUsers();
model.addAttribute("onlineUserCount", onlineUserCount);
return "home";
}
}

View File

@@ -0,0 +1,188 @@
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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
/**
* 图片拼接API控制器
* 用于将多张图片拼接成网格图
*/
@RestController
@RequestMapping("/api/image-grid")
@CrossOrigin(origins = "*", maxAge = 3600)
public class ImageGridApiController {
private static final Logger logger = LoggerFactory.getLogger(ImageGridApiController.class);
@Autowired
private ImageGridService imageGridService;
@Autowired
private CosService cosService;
@Autowired
private JwtUtils jwtUtils;
/**
* 拼接多张图片为六宫格2×3
* 接收多张图片文件或Base64字符串返回拼接后的Base64图片
*/
@PostMapping("/merge")
public ResponseEntity<Map<String, Object>> mergeImages(
@RequestParam(value = "images", required = false) MultipartFile[] imageFiles,
@RequestBody(required = false) Map<String, Object> requestBody,
@RequestHeader(value = "Authorization", required = false) String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证用户身份
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录或token无效");
logger.warn("图片拼接API调用失败: token无效");
return ResponseEntity.status(401).body(response);
}
logger.info("图片拼接API调用: username={}", username);
List<String> imageBase64List = new ArrayList<>();
// 处理文件上传形式
if (imageFiles != null && imageFiles.length > 0) {
logger.info("收到 {} 张图片文件进行拼接", imageFiles.length);
for (MultipartFile file : imageFiles) {
if (file.isEmpty()) {
continue;
}
// 转换为Base64
byte[] bytes = file.getBytes();
String base64 = Base64.getEncoder().encodeToString(bytes);
String contentType = file.getContentType();
if (contentType == null) {
contentType = "image/jpeg";
}
imageBase64List.add("data:" + contentType + ";base64," + base64);
}
}
// 处理JSON Body形式Base64数组
else if (requestBody != null && requestBody.containsKey("images")) {
@SuppressWarnings("unchecked")
List<String> images = (List<String>) requestBody.get("images");
logger.info("收到 {} 张Base64图片进行拼接", images.size());
imageBase64List.addAll(images);
}
// 验证图片数量
if (imageBase64List.isEmpty()) {
response.put("success", false);
response.put("message", "未提供图片");
return ResponseEntity.badRequest().body(response);
}
if (imageBase64List.size() > 6) {
logger.warn("图片数量超过6张只取前6张: {}", imageBase64List.size());
imageBase64List = imageBase64List.subList(0, 6);
}
// 确定网格列数
int cols = 3; // 默认3列2×3六宫格
if (requestBody != null && requestBody.containsKey("cols")) {
cols = (int) requestBody.get("cols");
}
logger.info("开始拼接图片: 数量={}, 列数={}", imageBase64List.size(), cols);
// 调用拼接服务
String mergedImage = imageGridService.mergeImagesToGrid(imageBase64List, cols);
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", "图片拼接成功");
// 构建返回数据
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);
} catch (Exception e) {
logger.error("图片拼接失败", e);
response.put("success", false);
response.put("message", "图片拼接失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 从Token中提取用户名
*/
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
// 提取实际的token
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
return null;
}
// 验证token并获取用户名
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username != null && !jwtUtils.isTokenExpired(actualToken)) {
return username;
}
return null;
} catch (Exception e) {
logger.error("解析token失败", e);
return null;
}
}
}

View File

@@ -0,0 +1,492 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.model.ImageToVideoTask;
import com.example.demo.service.ImageToVideoService;
import com.example.demo.util.JwtUtils;
/**
* 图生视频API控制器
*/
@RestController
@RequestMapping("/api/image-to-video")
public class ImageToVideoApiController {
private static final Logger logger = LoggerFactory.getLogger(ImageToVideoApiController.class);
@Autowired
private ImageToVideoService imageToVideoService;
@Autowired
private JwtUtils jwtUtils;
/**
* 创建图生视频任务
*/
@PostMapping("/create")
public ResponseEntity<Map<String, Object>> createTask(
@RequestParam("firstFrame") MultipartFile firstFrame,
@RequestParam(value = "lastFrame", required = false) MultipartFile lastFrame,
@RequestParam("prompt") String prompt,
@RequestParam(value = "aspectRatio", defaultValue = "3:2") String aspectRatio,
@RequestParam(value = "duration", defaultValue = "10") int duration,
@RequestParam(value = "hdMode", defaultValue = "false") boolean hdMode,
@RequestParam(value = "videoModel", defaultValue = "grok-video-3") String videoModel,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证用户身份
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录或token无效");
logger.warn("图生视频API调用失败: token无效, token={}", token);
return ResponseEntity.status(401).body(response);
}
logger.info("图生视频API调用: username={}, prompt={}", username, prompt);
// 验证文件
if (firstFrame.isEmpty()) {
response.put("success", false);
response.put("message", "请上传首帧图片");
return ResponseEntity.badRequest().body(response);
}
// 验证文件大小最大100MB与文件上传配置保持一致
long maxFileSize = 100 * 1024 * 1024; // 100MB
if (firstFrame.getSize() > maxFileSize) {
response.put("success", false);
response.put("message", "首帧图片大小不能超过100MB");
return ResponseEntity.badRequest().body(response);
}
if (lastFrame != null && !lastFrame.isEmpty() && lastFrame.getSize() > maxFileSize) {
response.put("success", false);
response.put("message", "尾帧图片大小不能超过100MB");
return ResponseEntity.badRequest().body(response);
}
// 验证文件类型
if (!isValidImageFile(firstFrame) || (lastFrame != null && !isValidImageFile(lastFrame))) {
response.put("success", false);
response.put("message", "请上传有效的图片文件JPG、PNG、WEBP");
return ResponseEntity.badRequest().body(response);
}
// 验证参数范围
if (duration != 5 && duration != 10 && duration != 15) {
response.put("success", false);
response.put("message", "视频时长必须为5秒、10秒或15秒");
return ResponseEntity.badRequest().body(response);
}
if (!isValidAspectRatio(aspectRatio)) {
response.put("success", false);
response.put("message", "不支持的视频比例");
return ResponseEntity.badRequest().body(response);
}
// 创建任务
logger.info("开始创建图生视频任务: username={}, prompt={}, aspectRatio={}, duration={}",
username, prompt, aspectRatio, duration);
ImageToVideoTask task = imageToVideoService.createTask(
username, firstFrame, lastFrame, prompt, aspectRatio, duration, hdMode, videoModel
);
response.put("success", true);
response.put("message", "任务创建成功");
response.put("data", task);
logger.info("用户 {} 创建图生视频任务成功: {}", username, task.getId());
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);
}
}
/**
* 通过图片URL创建图生视频任务用于"做同款"功能)
*/
@PostMapping("/create-by-url")
public ResponseEntity<Map<String, Object>> createTaskByUrl(
@RequestBody Map<String, Object> request,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证用户身份
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录或token无效");
return ResponseEntity.status(401).body(response);
}
// 提取参数
String imageUrl = (String) request.get("imageUrl");
String prompt = (String) request.get("prompt");
String aspectRatio = (String) request.getOrDefault("aspectRatio", "3:2");
int duration = request.get("duration") instanceof Number
? ((Number) request.get("duration")).intValue()
: Integer.parseInt(request.getOrDefault("duration", "10").toString());
boolean hdMode = Boolean.parseBoolean(request.getOrDefault("hdMode", "false").toString());
String videoModel = (String) request.getOrDefault("videoModel", "grok-video-3");
logger.info("通过URL创建图生视频任务: username={}, imageUrl={}, prompt={}",
username, imageUrl != null ? imageUrl.substring(0, Math.min(50, imageUrl.length())) : "null", prompt);
// 验证参数
if (imageUrl == null || imageUrl.trim().isEmpty()) {
response.put("success", false);
response.put("message", "图片URL不能为空");
return ResponseEntity.badRequest().body(response);
}
if (prompt == null || prompt.trim().isEmpty()) {
response.put("success", false);
response.put("message", "描述文字不能为空");
return ResponseEntity.badRequest().body(response);
}
if (duration != 5 && duration != 10 && duration != 15) {
response.put("success", false);
response.put("message", "视频时长必须为5秒、10秒或15秒");
return ResponseEntity.badRequest().body(response);
}
if (!isValidAspectRatio(aspectRatio)) {
response.put("success", false);
response.put("message", "不支持的视频比例");
return ResponseEntity.badRequest().body(response);
}
// 创建任务
ImageToVideoTask task = imageToVideoService.createTaskByUrl(
username, imageUrl.trim(), prompt.trim(), aspectRatio, duration, hdMode, videoModel
);
response.put("success", true);
response.put("message", "任务创建成功");
response.put("data", task);
logger.info("用户 {} 通过URL创建图生视频任务成功: {}", username, task.getId());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("通过URL创建图生视频任务失败", e);
response.put("success", false);
response.put("message", "创建任务失败:" + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 获取用户的任务列表
*/
@GetMapping("/tasks")
public ResponseEntity<Map<String, Object>> getUserTasks(
@RequestHeader("Authorization") String token,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "10") int size) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
List<ImageToVideoTask> tasks = imageToVideoService.getUserTasks(username, page, size);
long totalCount = imageToVideoService.getUserTaskCount(username);
response.put("success", true);
response.put("data", tasks);
response.put("total", totalCount);
response.put("page", page);
response.put("size", size);
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);
}
}
/**
* 获取任务详情
*/
@GetMapping("/tasks/{taskId}")
public ResponseEntity<Map<String, Object>> getTaskDetail(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
ImageToVideoTask task = imageToVideoService.getTaskById(taskId);
if (task == null) {
response.put("success", false);
response.put("message", "任务不存在");
return ResponseEntity.notFound().build();
}
// 检查权限
if (task.getUsername() == null || !task.getUsername().equals(username)) {
response.put("success", false);
response.put("message", "无权限访问此任务");
return ResponseEntity.status(403).body(response);
}
response.put("success", true);
response.put("data", task);
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);
}
}
/**
* 获取任务状态
*/
@GetMapping("/tasks/{taskId}/status")
public ResponseEntity<Map<String, Object>> getTaskStatus(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
ImageToVideoTask task = imageToVideoService.getTaskById(taskId);
if (task == null) {
response.put("success", false);
response.put("message", "任务不存在");
return ResponseEntity.notFound().build();
}
// 检查权限
if (task.getUsername() == null || !task.getUsername().equals(username)) {
response.put("success", false);
response.put("message", "无权限访问此任务");
return ResponseEntity.status(403).body(response);
}
// 状态同步已通过数据库触发器实现,无需代码检查
response.put("success", true);
Map<String, Object> taskData = new HashMap<>();
taskData.put("id", task.getId());
taskData.put("status", task.getStatus());
taskData.put("progress", task.getProgress());
taskData.put("resultUrl", task.getResultUrl());
taskData.put("errorMessage", task.getErrorMessage());
response.put("data", taskData);
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);
}
}
/**
* 从Token中提取用户名
*/
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
// 提取实际的token
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
return null;
}
// 验证token并获取用户名
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username != null && !jwtUtils.isTokenExpired(actualToken)) {
return username;
}
return null;
} catch (Exception e) {
logger.error("解析token失败", e);
return null;
}
}
/**
* 验证图片文件
*/
private boolean isValidImageFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
return false;
}
String contentType = file.getContentType();
return contentType != null && (
contentType.equals("image/jpeg") ||
contentType.equals("image/png") ||
contentType.equals("image/webp") ||
contentType.equals("image/jpg")
);
}
/**
* 验证视频比例
*/
private boolean isValidAspectRatio(String aspectRatio) {
if (aspectRatio == null || aspectRatio.trim().isEmpty()) {
return false;
}
String[] validRatios = {"16:9", "4:3", "1:1", "3:4", "9:16", "3:2", "2:3"};
for (String ratio : validRatios) {
if (ratio.equals(aspectRatio.trim())) {
return true;
}
}
return false;
}
/**
* 重试失败的图生视频任务
* 复用原task_id和已上传的图片重新提交至外部API
*/
@PostMapping("/tasks/{taskId}/retry")
public ResponseEntity<Map<String, Object>> retryTask(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证用户身份
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录或token无效");
return ResponseEntity.status(401).body(response);
}
logger.info("收到重试任务请求: taskId={}, username={}", taskId, username);
// 调用重试服务
ImageToVideoTask task = imageToVideoService.retryTask(taskId, username);
response.put("success", true);
response.put("message", "重试任务已提交");
response.put("data", task);
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
logger.error("重试任务失败: taskId={}, error={}", taskId, e.getMessage());
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
} catch (Exception e) {
logger.error("重试任务异常: taskId={}", taskId, e);
response.put("success", false);
response.put("message", "重试任务失败");
return ResponseEntity.internalServerError().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 username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录或token无效");
return ResponseEntity.status(401).body(response);
}
logger.info("删除图生视频任务: taskId={}, username={}", taskId, username);
// 删除任务
boolean deleted = imageToVideoService.deleteTask(taskId, username);
if (deleted) {
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={}", taskId, e);
response.put("success", false);
response.put("message", "删除失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
}

View File

@@ -0,0 +1,151 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.service.LuluPayService;
import com.example.demo.service.PaymentService;
/**
* 噜噜支付(彩虹易支付)回调控制器
*/
@RestController
@RequestMapping("/api/payments/lulupay")
public class LuluPayCallbackController {
private static final Logger logger = LoggerFactory.getLogger(LuluPayCallbackController.class);
@Autowired
private LuluPayService luluPayService;
@Autowired
private PaymentService paymentService;
@Value("${app.frontend-url:}")
private String frontendUrl;
/**
* 异步通知接口
* 支付平台会通过GET方式调用此接口通知支付结果
*/
@GetMapping(value = "/notify", produces = MediaType.TEXT_PLAIN_VALUE)
public String notifyGet(@RequestParam Map<String, String> params) {
return handleNotify(params);
}
/**
* 异步通知接口POST方式
*/
@PostMapping(value = "/notify", produces = MediaType.TEXT_PLAIN_VALUE)
public String notifyPost(@RequestParam Map<String, String> params) {
return handleNotify(params);
}
/**
* 处理异步通知
*/
private String handleNotify(Map<String, String> params) {
logger.info("========== 收到噜噜支付异步通知 ==========");
logger.info("参数: {}", params);
try {
boolean success = luluPayService.handleNotify(params);
if (success) {
String tradeStatus = params.get("trade_status");
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
logger.info("噜噜支付验签成功: tradeStatus={}, outTradeNo={}, tradeNo={}", tradeStatus, outTradeNo, tradeNo);
// 如果支付成功,调用统一的支付确认方法
if ("TRADE_SUCCESS".equals(tradeStatus)) {
try {
// 查找支付记录并确认
logger.info("开始查找支付记录: outTradeNo={}", outTradeNo);
var paymentOpt = paymentService.findByOrderId(outTradeNo);
if (paymentOpt.isPresent()) {
var payment = paymentOpt.get();
logger.info("找到支付记录: paymentId={}, status={}, orderId={}",
payment.getId(), payment.getStatus(), payment.getOrderId());
// 调用确认方法(内部会检查是否已处理,避免重复增加积分)
// 即使状态已经是SUCCESS也要确保会员信息已更新
logger.info("开始调用confirmPaymentSuccess: paymentId={}", payment.getId());
paymentService.confirmPaymentSuccess(payment.getId(), tradeNo);
logger.info("✅ 支付确认处理完成: 订单号={}", outTradeNo);
} else {
logger.error("❌ 未找到支付记录: outTradeNo={}", outTradeNo);
}
} catch (Exception e) {
logger.error("❌ 支付确认失败: outTradeNo={}, error={}", outTradeNo, e.getMessage(), e);
}
} else {
logger.info("交易状态不是TRADE_SUCCESS不处理积分: tradeStatus={}", tradeStatus);
}
logger.info("========== 噜噜支付异步通知处理成功 ==========");
return "success";
} else {
logger.warn("========== 噜噜支付异步通知处理失败(验签失败) ==========");
return "fail";
}
} catch (Exception e) {
logger.error("处理噜噜支付异步通知异常: ", e);
return "fail";
}
}
/**
* 同步返回接口
* 用户支付完成后跳转到会员订阅页面
*/
@GetMapping("/return")
public ResponseEntity<Void> returnUrl(@RequestParam Map<String, String> params) {
logger.info("========== 收到噜噜支付同步返回 ==========");
logger.info("参数: {}", params);
try {
boolean success = luluPayService.handleReturn(params);
String tradeStatus = params.get("trade_status");
String orderId = params.get("out_trade_no");
logger.info("支付同步返回处理结果: success={}, tradeStatus={}, orderId={}", success, tradeStatus, orderId);
// 构建重定向URL跳转到会员订阅页面
String redirectUrl = (frontendUrl != null && !frontendUrl.isEmpty() ? frontendUrl : "")
+ "/subscription?paymentStatus=" + (success && "TRADE_SUCCESS".equals(tradeStatus) ? "success" : "pending")
+ "&orderId=" + (orderId != null ? orderId : "");
logger.info("重定向到: {}", redirectUrl);
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", redirectUrl)
.build();
} catch (Exception e) {
logger.error("处理噜噜支付同步返回异常: ", e);
// 出错也跳转到订阅页面
String redirectUrl = (frontendUrl != null && !frontendUrl.isEmpty() ? frontendUrl : "")
+ "/subscription?paymentStatus=error";
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", redirectUrl)
.build();
}
}
}

View File

@@ -0,0 +1,851 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.MembershipLevel;
import com.example.demo.model.User;
import com.example.demo.model.UserMembership;
import com.example.demo.repository.MembershipLevelRepository;
import com.example.demo.repository.PaymentRepository;
import com.example.demo.repository.UserMembershipRepository;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;
import com.example.demo.util.JwtUtils;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/api/members")
@CrossOrigin(origins = "*")
public class MemberApiController {
private static final Logger logger = LoggerFactory.getLogger(MemberApiController.class);
@Autowired
private UserRepository userRepository;
@Autowired
private UserMembershipRepository userMembershipRepository;
@Autowired
private MembershipLevelRepository membershipLevelRepository;
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserService userService;
// 获取会员列表
@GetMapping
public ResponseEntity<Map<String, Object>> getMembers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(required = false) String level,
@RequestParam(required = false) String status) {
try {
Pageable pageable = PageRequest.of(page - 1, pageSize, Sort.by("createdAt").descending());
// 第一步:根据会员等级筛选(如果指定了等级)
List<Long> filteredUserIds = null;
if (level != null && !level.isEmpty() && !"all".equals(level)) {
// 根据等级名称查找会员等级ID
Optional<MembershipLevel> levelOpt = membershipLevelRepository.findByName(level);
if (levelOpt.isPresent()) {
Long levelId = levelOpt.get().getId();
// 查找具有该等级的所有活跃会员记录
List<UserMembership> memberships = userMembershipRepository.findAll().stream()
.filter(m -> "ACTIVE".equals(m.getStatus()) && levelId.equals(m.getMembershipLevelId()))
.toList();
// 提取用户ID列表
filteredUserIds = memberships.stream()
.map(UserMembership::getUserId)
.distinct()
.toList();
logger.info("会员等级筛选: level={}, levelId={}, 找到 {} 个用户", level, levelId, filteredUserIds.size());
} else {
logger.warn("未找到会员等级: level={}", level);
// 如果找不到等级,返回空列表
filteredUserIds = List.of();
}
}
// 第二步:根据 status 参数和等级筛选结果查询用户
Page<User> userPage;
if (filteredUserIds != null) {
// 如果有等级筛选,需要同时满足等级和状态条件
if (filteredUserIds.isEmpty()) {
// 如果等级筛选结果为空,直接返回空列表
userPage = Page.empty(pageable);
} else {
// 根据用户ID列表和状态筛选
if ("all".equals(status)) {
userPage = userRepository.findByIdIn(filteredUserIds, pageable);
} else if ("banned".equals(status)) {
userPage = userRepository.findByIdInAndIsActive(filteredUserIds, false, pageable);
} else {
userPage = userRepository.findByIdInAndIsActive(filteredUserIds, true, pageable);
}
}
} else {
// 如果没有等级筛选,只根据状态筛选
if ("all".equals(status)) {
userPage = userRepository.findAll(pageable);
} else if ("banned".equals(status)) {
userPage = userRepository.findByIsActive(false, pageable);
} else {
userPage = userRepository.findByIsActive(true, pageable);
}
}
List<Map<String, Object>> members = userPage.getContent().stream()
.map(user -> {
Map<String, Object> member = new HashMap<>();
member.put("id", user.getId());
member.put("username", user.getUsername());
member.put("email", user.getEmail());
member.put("phone", user.getPhone());
member.put("nickname", user.getNickname());
member.put("points", user.getPoints());
member.put("role", user.getRole());
member.put("isActive", user.getIsActive());
member.put("createdAt", user.getCreatedAt());
member.put("lastLoginAt", user.getLastLoginAt());
// 获取会员信息(按到期时间降序,返回最新的)
Optional<UserMembership> membership = userMembershipRepository
.findFirstByUserIdAndStatusOrderByEndDateDesc(user.getId(), "ACTIVE");
if (membership.isPresent()) {
UserMembership userMembership = membership.get();
Optional<MembershipLevel> membershipLevel = membershipLevelRepository
.findById(userMembership.getMembershipLevelId());
if (membershipLevel.isPresent()) {
Map<String, Object> membershipInfo = new HashMap<>();
MembershipLevel memberLevel = membershipLevel.get();
String displayName = memberLevel.getDisplayName();
// 🔥 仅对 free 等级进行特殊判定:根据充值金额区分"免费会员"和"入门会员"
// 其他等级standard/professional直接使用数据库中的 displayName
if ("free".equalsIgnoreCase(memberLevel.getName())) {
// 计算用户的总充值金额(而非充值次数)
java.math.BigDecimal totalPaid = paymentRepository.sumAmountByUserIdAndStatus(
user.getId(),
com.example.demo.model.PaymentStatus.SUCCESS
);
// 获取入门版的价格阈值
double freePrice = memberLevel.getPrice() != null ? memberLevel.getPrice() : 0.0;
double totalAmount = totalPaid != null ? totalPaid.doubleValue() : 0.0;
// 充值金额 >= 入门版价格 → 入门会员,否则 → 免费会员
if (totalAmount >= freePrice && freePrice > 0) {
displayName = "入门会员"; // 充值金额达到入门版价格
} else {
displayName = "免费会员"; // 未充值或充值金额不足
}
}
// 注:如果出现标准会员被判定为入门会员,请检查数据库 user_memberships 表中
// 该用户的 membership_level_id 是否正确指向 standard 等级(通常 id=2
membershipInfo.put("display_name", displayName);
membershipInfo.put("end_date", userMembership.getEndDate());
membershipInfo.put("status", userMembership.getStatus());
member.put("membership", membershipInfo);
}
}
return member;
})
.toList();
Map<String, Object> response = new HashMap<>();
response.put("list", members);
response.put("total", userPage.getTotalElements());
response.put("page", page);
response.put("pageSize", pageSize);
response.put("totalPages", userPage.getTotalPages());
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取会员列表失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 获取会员详情
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getMemberDetail(@PathVariable Long id) {
try {
Optional<User> userOpt = userRepository.findById(id);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
Map<String, Object> member = new HashMap<>();
member.put("id", user.getId());
member.put("username", user.getUsername());
member.put("email", user.getEmail());
member.put("phone", user.getPhone());
member.put("nickname", user.getNickname());
member.put("points", user.getPoints());
member.put("role", user.getRole());
member.put("isActive", user.getIsActive());
member.put("createdAt", user.getCreatedAt());
member.put("lastLoginAt", user.getLastLoginAt());
// 获取会员信息(按到期时间降序,返回最新的)
Optional<UserMembership> membership = userMembershipRepository
.findFirstByUserIdAndStatusOrderByEndDateDesc(user.getId(), "ACTIVE");
if (membership.isPresent()) {
UserMembership userMembership = membership.get();
Optional<MembershipLevel> membershipLevel = membershipLevelRepository
.findById(userMembership.getMembershipLevelId());
if (membershipLevel.isPresent()) {
Map<String, Object> membershipInfo = new HashMap<>();
MembershipLevel memberLevel = membershipLevel.get();
String displayName = memberLevel.getDisplayName();
// 🔥 仅对 free 等级进行特殊判定:根据充值金额区分"免费会员"和"入门会员"
// 其他等级standard/professional直接使用数据库中的 displayName
if ("free".equalsIgnoreCase(memberLevel.getName())) {
// 计算用户的总充值金额(而非充值次数)
java.math.BigDecimal totalPaid = paymentRepository.sumAmountByUserIdAndStatus(
user.getId(),
com.example.demo.model.PaymentStatus.SUCCESS
);
// 获取入门版的价格阈值
double freePrice = memberLevel.getPrice() != null ? memberLevel.getPrice() : 0.0;
double totalAmount = totalPaid != null ? totalPaid.doubleValue() : 0.0;
// 充值金额 >= 入门版价格 → 入门会员,否则 → 免费会员
if (totalAmount >= freePrice && freePrice > 0) {
displayName = "入门会员"; // 充值金额达到入门版价格
} else {
displayName = "免费会员"; // 未充值或充值金额不足
}
}
// 注:如果出现标准会员被判定为入门会员,请检查数据库 user_memberships 表中
// 该用户的 membership_level_id 是否正确指向 standard 等级(通常 id=2
membershipInfo.put("display_name", displayName);
membershipInfo.put("end_date", userMembership.getEndDate());
membershipInfo.put("status", userMembership.getStatus());
member.put("membership", membershipInfo);
}
}
return ResponseEntity.ok(member);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取会员详情失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 更新会员信息
@PutMapping("/{id}")
public ResponseEntity<Map<String, Object>> updateMember(
@PathVariable Long id,
@RequestBody Map<String, Object> updateData,
@RequestHeader("Authorization") String token) {
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
return ResponseEntity.status(401).body(Map.of("success", false, "message", "用户未登录"));
}
User admin = userRepository.findByUsername(adminUsername).orElse(null);
if (admin == null) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要管理员权限"));
}
boolean isSuperAdmin = "ROLE_SUPER_ADMIN".equals(admin.getRole());
boolean isAdmin = "ROLE_ADMIN".equals(admin.getRole());
if (!isSuperAdmin && !isAdmin) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要管理员权限"));
}
Optional<User> userOpt = userRepository.findById(id);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
// 普通管理员不能修改超级管理员的信息
if ("ROLE_SUPER_ADMIN".equals(user.getRole()) && !isSuperAdmin) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "只有超级管理员才能修改超级管理员的信息"));
}
// 更新用户基本信息
if (updateData.containsKey("username")) {
user.setUsername((String) updateData.get("username"));
}
if (updateData.containsKey("points")) {
Object pointsObj = updateData.get("points");
if (pointsObj instanceof Number) {
user.setPoints(((Number) pointsObj).intValue());
}
}
// 只有超级管理员可以修改角色,且不能修改超级管理员的角色
if (updateData.containsKey("role") && isSuperAdmin) {
String newRole = (String) updateData.get("role");
// 如果被编辑的用户是超级管理员,跳过角色修改
if ("ROLE_SUPER_ADMIN".equals(user.getRole())) {
// 不做任何操作,保持超级管理员角色
} else if ("ROLE_USER".equals(newRole) || "ROLE_ADMIN".equals(newRole)) {
// 只允许设置为普通用户或管理员
user.setRole(newRole);
}
// 如果 newRole 是 ROLE_SUPER_ADMIN忽略不允许通过此接口设置超级管理员
}
userService.save(user);
// 更新会员等级和到期时间
String levelName = (String) updateData.get("level");
String expiryDateStr = (String) updateData.get("expiryDate");
logger.info("更新会员等级: userId={}, levelName={}, expiryDate={}", id, levelName, expiryDateStr);
// 只要有会员等级或到期时间参数,就需要更新会员信息
if (levelName != null || (expiryDateStr != null && !expiryDateStr.isEmpty())) {
// 查找或创建会员信息(按到期时间降序,返回最新的)
Optional<UserMembership> membershipOpt = userMembershipRepository
.findFirstByUserIdAndStatusOrderByEndDateDesc(user.getId(), "ACTIVE");
UserMembership membership;
MembershipLevel level = null;
// 如果传入了会员等级,查找对应的等级
if (levelName != null) {
// 先尝试精确匹配 displayName
Optional<MembershipLevel> levelOpt = membershipLevelRepository.findByDisplayName(levelName);
// 如果找不到,尝试模糊匹配
if (!levelOpt.isPresent()) {
List<MembershipLevel> allLevels = membershipLevelRepository.findAll();
for (MembershipLevel lvl : allLevels) {
String name = lvl.getName();
String displayName = lvl.getDisplayName();
// 匹配 "专业会员" -> "professional" 或 "专业版"
if (levelName.contains("专业") && "professional".equalsIgnoreCase(name)) {
levelOpt = Optional.of(lvl);
break;
}
// 匹配 "标准会员" -> "standard" 或 "标准会员"
if (levelName.contains("标准") && "standard".equalsIgnoreCase(name)) {
levelOpt = Optional.of(lvl);
break;
}
// 匹配 "入门"/"免费" -> "free"(后端标记仍为 free
if ((levelName.contains("入门") || levelName.contains("免费")) && "free".equalsIgnoreCase(name)) {
levelOpt = Optional.of(lvl);
break;
}
}
}
logger.info("查找会员等级结果: levelName={}, found={}", levelName, levelOpt.isPresent());
if (levelOpt.isPresent()) {
level = levelOpt.get();
} else {
logger.warn("❌ 未找到会员等级: levelName={}", levelName);
}
}
if (membershipOpt.isPresent()) {
membership = membershipOpt.get();
logger.info("找到现有会员记录: membershipId={}, currentEndDate={}", membership.getId(), membership.getEndDate());
} else {
// 创建新的会员记录
membership = new UserMembership();
membership.setUserId(user.getId());
membership.setStatus("ACTIVE");
membership.setStartDate(java.time.LocalDateTime.now());
// 默认到期时间为1年后
membership.setEndDate(java.time.LocalDateTime.now().plusDays(365));
logger.info("创建新会员记录");
// 如果没有指定等级,默认使用标准会员
if (level == null) {
Optional<MembershipLevel> defaultLevel = membershipLevelRepository.findByName("standard");
if (defaultLevel.isPresent()) {
level = defaultLevel.get();
}
}
}
// 更新会员等级
if (level != null) {
membership.setMembershipLevelId(level.getId());
}
// 更新到期时间
if (expiryDateStr != null && !expiryDateStr.isEmpty()) {
try {
// 尝试解析带时间的格式 (如 2025-12-11T17:03:16)
java.time.LocalDateTime expiryDateTime = java.time.LocalDateTime.parse(expiryDateStr);
membership.setEndDate(expiryDateTime);
logger.info("设置到期时间(带时间格式): {}", expiryDateTime);
} catch (Exception e1) {
try {
// 尝试解析仅日期格式 (如 2025-12-11)
java.time.LocalDate expiryDate = java.time.LocalDate.parse(expiryDateStr);
membership.setEndDate(expiryDate.atTime(23, 59, 59));
logger.info("设置到期时间(日期格式): {}", expiryDate.atTime(23, 59, 59));
} catch (Exception e2) {
logger.warn("日期格式错误: {}", expiryDateStr);
}
}
}
membership.setUpdatedAt(java.time.LocalDateTime.now());
UserMembership saved = userMembershipRepository.save(membership);
logger.info("✅ 会员信息已保存: userId={}, membershipId={}, levelId={}, endDate={}",
user.getId(), saved.getId(), saved.getMembershipLevelId(), saved.getEndDate());
} else {
logger.info("未传入会员等级和到期时间参数,跳过会员信息更新");
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "会员信息更新成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "更新会员信息失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 删除会员
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity<Map<String, Object>> deleteMember(
@PathVariable Long id,
@RequestHeader("Authorization") String token) {
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
return ResponseEntity.status(401).body(Map.of("success", false, "message", "用户未登录"));
}
User admin = userRepository.findByUsername(adminUsername).orElse(null);
if (admin == null) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要管理员权限"));
}
boolean isSuperAdmin = "ROLE_SUPER_ADMIN".equals(admin.getRole());
boolean isAdmin = "ROLE_ADMIN".equals(admin.getRole());
if (!isSuperAdmin && !isAdmin) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要管理员权限"));
}
Optional<User> userOpt = userRepository.findById(id);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
// 不能删除自己
if (user.getUsername().equals(adminUsername)) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "不能删除自己的账号"));
}
// 不能删除超级管理员
if ("ROLE_SUPER_ADMIN".equals(user.getRole())) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "不能删除超级管理员账号"));
}
// 普通管理员不能删除其他管理员,只有超级管理员可以
if ("ROLE_ADMIN".equals(user.getRole()) && !isSuperAdmin) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "只有超级管理员才能删除管理员账号"));
}
// 先删除关联的会员信息
userMembershipRepository.deleteByUserId(user.getId());
// 清除用户缓存
userService.evictUserCache(user.getUsername());
// 物理删除用户
userRepository.delete(user);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "会员删除成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "删除会员失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 批量删除会员
@DeleteMapping("/batch")
@Transactional
public ResponseEntity<Map<String, Object>> deleteMembers(
@RequestBody Map<String, List<Long>> request,
@RequestHeader("Authorization") String token) {
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
return ResponseEntity.status(401).body(Map.of("success", false, "message", "用户未登录"));
}
User admin = userRepository.findByUsername(adminUsername).orElse(null);
if (admin == null) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要管理员权限"));
}
boolean isSuperAdmin = "ROLE_SUPER_ADMIN".equals(admin.getRole());
boolean isAdmin = "ROLE_ADMIN".equals(admin.getRole());
if (!isSuperAdmin && !isAdmin) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要管理员权限"));
}
List<Long> ids = request.get("ids");
if (ids == null || ids.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "请提供要删除的会员ID列表"));
}
List<User> users = userRepository.findAllById(ids);
// 过滤掉自己、超级管理员,普通管理员还需要过滤掉其他管理员
final boolean finalIsSuperAdmin = isSuperAdmin;
List<User> toDelete = users.stream()
.filter(user -> !user.getUsername().equals(adminUsername))
.filter(user -> !"ROLE_SUPER_ADMIN".equals(user.getRole()))
.filter(user -> finalIsSuperAdmin || !"ROLE_ADMIN".equals(user.getRole()))
.toList();
int skipped = users.size() - toDelete.size();
// 物理删除:先删除关联的会员信息,清除缓存,再删除用户
for (User user : toDelete) {
userMembershipRepository.deleteByUserId(user.getId());
userService.evictUserCache(user.getUsername()); // 清除缓存
}
userRepository.deleteAll(toDelete);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", skipped > 0
? "批量删除成功,已跳过 " + skipped + " 个管理员账号"
: "批量删除成功");
response.put("deletedCount", toDelete.size());
response.put("skippedCount", skipped);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "批量删除失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 封禁/解封会员
@PutMapping("/{id}/ban")
public ResponseEntity<Map<String, Object>> toggleBanMember(
@PathVariable Long id,
@RequestBody Map<String, Boolean> request,
@RequestHeader("Authorization") String token) {
try {
// 验证管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
return ResponseEntity.status(401).body(Map.of("success", false, "message", "用户未登录"));
}
User admin = userRepository.findByUsername(adminUsername).orElse(null);
if (admin == null) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要管理员权限"));
}
boolean isSuperAdmin = "ROLE_SUPER_ADMIN".equals(admin.getRole());
boolean isAdmin = "ROLE_ADMIN".equals(admin.getRole());
if (!isSuperAdmin && !isAdmin) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要管理员权限"));
}
Optional<User> userOpt = userRepository.findById(id);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
// 不能封禁自己
if (user.getUsername().equals(adminUsername)) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "不能封禁自己的账号"));
}
// 不能封禁超级管理员
if ("ROLE_SUPER_ADMIN".equals(user.getRole())) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "不能封禁超级管理员账号"));
}
// 普通管理员不能封禁其他管理员,只有超级管理员可以
if ("ROLE_ADMIN".equals(user.getRole()) && !isSuperAdmin) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "只有超级管理员才能封禁管理员账号"));
}
// 获取要设置的状态true=解封false=封禁)
Boolean isActive = request.get("isActive");
if (isActive == null) {
isActive = !user.getIsActive(); // 如果没传,则切换状态
}
user.setIsActive(isActive);
userService.save(user);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", isActive ? "解封成功" : "封禁成功");
response.put("isActive", isActive);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "操作失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 设置用户角色(仅超级管理员可操作)
@PutMapping("/{id}/role")
public ResponseEntity<Map<String, Object>> setUserRole(
@PathVariable Long id,
@RequestBody Map<String, String> request,
@RequestHeader("Authorization") String token) {
try {
// 验证超级管理员权限
String adminUsername = extractUsernameFromToken(token);
if (adminUsername == null) {
return ResponseEntity.status(401).body(Map.of("success", false, "message", "用户未登录"));
}
User admin = userRepository.findByUsername(adminUsername).orElse(null);
if (admin == null || !"ROLE_SUPER_ADMIN".equals(admin.getRole())) {
return ResponseEntity.status(403).body(Map.of("success", false, "message", "需要超级管理员权限"));
}
Optional<User> userOpt = userRepository.findById(id);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
// 不能修改自己的角色
if (user.getUsername().equals(adminUsername)) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "不能修改自己的角色"));
}
// 不能修改其他超级管理员的角色
if ("ROLE_SUPER_ADMIN".equals(user.getRole())) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "不能修改超级管理员的角色"));
}
String newRole = request.get("role");
if (newRole == null || newRole.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "请指定角色"));
}
// 验证角色有效性
if (!"ROLE_USER".equals(newRole) && !"ROLE_ADMIN".equals(newRole)) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "无效的角色"));
}
String oldRole = user.getRole();
user.setRole(newRole);
userService.save(user);
String action = "ROLE_ADMIN".equals(newRole) ? "设置为管理员" : "取消管理员权限";
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "用户 " + user.getUsername() + "" + action);
response.put("oldRole", oldRole);
response.put("newRole", newRole);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "操作失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 从Token中提取用户名
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
String actualToken = token.substring(7);
if (jwtUtils.isTokenExpired(actualToken)) {
return null;
}
return jwtUtils.getUsernameFromToken(actualToken);
} catch (Exception e) {
return null;
}
}
// 获取所有会员等级配置(用于系统设置和订阅页面)
@GetMapping("/levels")
public ResponseEntity<Map<String, Object>> getMembershipLevels() {
try {
List<MembershipLevel> levels = membershipLevelRepository.findAll();
List<Map<String, Object>> levelList = levels.stream()
.map(level -> {
Map<String, Object> levelMap = new HashMap<>();
levelMap.put("id", level.getId());
levelMap.put("name", level.getName());
levelMap.put("displayName", level.getDisplayName());
levelMap.put("description", level.getDescription());
levelMap.put("price", level.getPrice());
levelMap.put("durationDays", level.getDurationDays());
levelMap.put("pointsBonus", level.getPointsBonus());
levelMap.put("features", level.getFeatures());
levelMap.put("isActive", level.getIsActive());
return levelMap;
})
.toList();
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", levelList);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "获取会员等级配置失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
// 更新会员等级价格和配置
@PutMapping("/levels/{id}")
public ResponseEntity<Map<String, Object>> updateMembershipLevel(
@PathVariable Long id,
@RequestBody Map<String, Object> updateData) {
try {
Optional<MembershipLevel> levelOpt = membershipLevelRepository.findById(id);
if (levelOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
MembershipLevel level = levelOpt.get();
// 更新价格
if (updateData.containsKey("price")) {
Object priceObj = updateData.get("price");
if (priceObj instanceof Number) {
level.setPrice(((Number) priceObj).doubleValue());
} else if (priceObj instanceof String) {
level.setPrice(Double.parseDouble((String) priceObj));
}
}
// 更新资源点数量
if (updateData.containsKey("pointsBonus") || updateData.containsKey("resourcePoints")) {
Object pointsObj = updateData.get("pointsBonus") != null
? updateData.get("pointsBonus")
: updateData.get("resourcePoints");
if (pointsObj instanceof Number) {
level.setPointsBonus(((Number) pointsObj).intValue());
} else if (pointsObj instanceof String) {
level.setPointsBonus(Integer.parseInt((String) pointsObj));
}
}
// 更新描述
if (updateData.containsKey("description")) {
level.setDescription((String) updateData.get("description"));
}
level.setUpdatedAt(java.time.LocalDateTime.now());
membershipLevelRepository.save(level);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "会员等级配置更新成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("error", "更新会员等级配置失败");
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
}

View File

@@ -0,0 +1,650 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
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;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.Order;
import com.example.demo.model.OrderStatus;
import com.example.demo.model.PaymentMethod;
import com.example.demo.model.User;
import com.example.demo.service.OrderService;
import com.example.demo.service.UserService;
import com.example.demo.service.PaymentService;
import com.example.demo.service.AlipayService;
import com.example.demo.service.LuluPayService;
import com.example.demo.service.PayPalService;
import jakarta.validation.Valid;
import org.springframework.transaction.annotation.Transactional;
import jakarta.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/api/orders")
public class OrderApiController {
private static final Logger logger = LoggerFactory.getLogger(OrderApiController.class);
@Autowired
private OrderService orderService;
@Autowired
private UserService userService;
@Autowired
private PaymentService paymentService;
@Autowired
private AlipayService alipayService;
@Autowired
private LuluPayService luluPayService;
@Autowired(required = false)
private PayPalService payPalService;
/**
* 获取订单列表
*/
@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 paymentMethod,
@RequestParam(required = false) String type,
@RequestParam(required = false) String search,
Authentication authentication) {
try {
// 检查认证信息
if (authentication == null || !authentication.isAuthenticated()) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "用户未认证,请重新登录");
return ResponseEntity.status(401).body(response);
}
String username = authentication.getName();
User user = userService.findByUsername(username);
if (user == null) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "用户信息获取失败,请重新登录");
return ResponseEntity.status(401).body(response);
}
// 创建排序
Sort sort = Sort.by(Sort.Direction.fromString(sortDir), sortBy);
// 如果有支付方式筛选,需要获取所有数据后在内存中筛选和分页
final String filterPaymentMethod = paymentMethod;
boolean hasPaymentMethodFilter = filterPaymentMethod != null && !filterPaymentMethod.isEmpty();
// 获取订单列表
Page<Order> orderPage;
List<Order> allOrders = null;
if (hasPaymentMethodFilter) {
// 有支付方式筛选时,获取所有订单(不分页)
Pageable unpaged = PageRequest.of(0, Integer.MAX_VALUE, sort);
if ((user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
orderPage = orderService.findAllOrders(unpaged, status, type, search);
} else {
orderPage = orderService.findOrdersByUser(user, unpaged, status, type, search);
}
allOrders = orderPage.getContent();
} else {
// 无支付方式筛选时,正常分页
Pageable pageable = PageRequest.of(page, size, sort);
if ((user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
orderPage = orderService.findAllOrders(pageable, status, type, search);
} else {
orderPage = orderService.findOrdersByUser(user, pageable, status, type, search);
}
}
// 转换订单数据,添加支付方式信息
java.util.function.Function<Order, Map<String, Object>> orderMapper = order -> {
Map<String, Object> orderData = new HashMap<>();
orderData.put("id", order.getId());
orderData.put("orderNumber", order.getOrderNumber());
orderData.put("totalAmount", order.getTotalAmount());
orderData.put("currency", order.getCurrency());
orderData.put("status", order.getStatus());
orderData.put("orderType", order.getOrderType());
orderData.put("description", order.getDescription());
orderData.put("createdAt", order.getCreatedAt());
orderData.put("updatedAt", order.getUpdatedAt());
orderData.put("paidAt", order.getPaidAt());
// 添加用户信息
if (order.getUser() != null) {
Map<String, Object> userData = new HashMap<>();
userData.put("id", order.getUser().getId());
userData.put("username", order.getUser().getUsername());
orderData.put("user", userData);
}
// 从订单关联的Payment中获取支付方式
String orderPaymentMethod = null;
if (order.getPayments() != null && !order.getPayments().isEmpty()) {
// 获取最新的支付记录
var latestPayment = order.getPayments().stream()
.filter(p -> p.getPaymentMethod() != null)
.findFirst();
if (latestPayment.isPresent()) {
orderPaymentMethod = latestPayment.get().getPaymentMethod().name();
}
}
orderData.put("paymentMethod", orderPaymentMethod);
return orderData;
};
List<Map<String, Object>> resultContent;
long totalElements;
int totalPages;
if (hasPaymentMethodFilter && allOrders != null) {
// 有支付方式筛选:先转换,再筛选,最后手动分页
List<Map<String, Object>> allMapped = allOrders.stream()
.map(orderMapper)
.filter(orderData -> filterPaymentMethod.equals(orderData.get("paymentMethod")))
.collect(java.util.stream.Collectors.toList());
totalElements = allMapped.size();
totalPages = (int) Math.ceil((double) totalElements / size);
// 手动分页
int fromIndex = page * size;
int toIndex = Math.min(fromIndex + size, allMapped.size());
if (fromIndex < allMapped.size()) {
resultContent = allMapped.subList(fromIndex, toIndex);
} else {
resultContent = new java.util.ArrayList<>();
}
} else {
// 无支付方式筛选:直接使用分页结果
resultContent = orderPage.getContent().stream()
.map(orderMapper)
.collect(java.util.stream.Collectors.toList());
totalElements = orderPage.getTotalElements();
totalPages = orderPage.getTotalPages();
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
// 构建返回的分页数据
Map<String, Object> pageData = new HashMap<>();
pageData.put("content", resultContent);
pageData.put("totalElements", totalElements);
pageData.put("totalPages", totalPages);
pageData.put("number", page);
pageData.put("size", size);
response.put("data", pageData);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取订单列表失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("获取订单列表失败:" + e.getMessage()));
}
}
/**
* 获取订单详情
*/
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getOrderById(@PathVariable Long id,
Authentication authentication) {
try {
// 检查认证信息
if (authentication == null || !authentication.isAuthenticated()) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "用户未认证,请重新登录");
return ResponseEntity.status(401).body(response);
}
String username = authentication.getName();
User user = userService.findByUsername(username);
if (user == null) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "用户信息获取失败,请重新登录");
return ResponseEntity.status(401).body(response);
}
Order order = orderService.findById(id)
.orElse(null);
if (order == null) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "订单不存在");
return ResponseEntity.status(404).body(response);
}
// 检查权限
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN")) && !order.getUser().getId().equals(user.getId())) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "无权限访问此订单");
return ResponseEntity.status(403).body(response);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", order);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取订单详情失败:", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "获取订单详情失败:" + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 创建订单
*/
@PostMapping("/create")
public ResponseEntity<Map<String, Object>> createOrder(@Valid @RequestBody Order order,
Authentication authentication) {
try {
// 检查认证信息
if (authentication == null || !authentication.isAuthenticated()) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "用户未认证,请重新登录");
return ResponseEntity.status(401).body(response);
}
String username = authentication.getName();
User user = userService.findByUsername(username);
if (user == null) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "用户信息获取失败,请重新登录");
return ResponseEntity.status(401).body(response);
}
order.setUser(user);
Order createdOrder = orderService.createOrder(order);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "订单创建成功");
response.put("data", createdOrder);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("创建订单失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("创建订单失败:" + e.getMessage()));
}
}
/**
* 更新订单状态
*/
@PostMapping("/{id}/status")
public ResponseEntity<Map<String, Object>> updateOrderStatus(@PathVariable Long id,
@RequestBody Map<String, String> request,
Authentication authentication) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Order order = orderService.findById(id)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 检查权限
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN")) && !order.getUser().getId().equals(user.getId())) {
return ResponseEntity.badRequest()
.body(createErrorResponse("无权限操作此订单"));
}
OrderStatus status = OrderStatus.valueOf(request.get("status"));
String notes = request.get("notes");
Order updatedOrder = orderService.updateOrderStatus(id, status);
if (notes != null && !notes.trim().isEmpty()) {
updatedOrder.setNotes((updatedOrder.getNotes() != null ? updatedOrder.getNotes() + "\n" : "") + notes);
orderService.createOrder(updatedOrder);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "订单状态更新成功");
response.put("data", updatedOrder);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("更新订单状态失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("更新订单状态失败:" + e.getMessage()));
}
}
/**
* 取消订单
*/
@PostMapping("/{id}/cancel")
public ResponseEntity<Map<String, Object>> cancelOrder(@PathVariable Long id,
@RequestBody Map<String, String> request,
Authentication authentication) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Order order = orderService.findById(id)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 检查权限
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN")) && !order.getUser().getId().equals(user.getId())) {
return ResponseEntity.badRequest()
.body(createErrorResponse("无权限操作此订单"));
}
String reason = request.get("reason");
Order cancelledOrder = orderService.cancelOrder(id, reason);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "订单取消成功");
response.put("data", cancelledOrder);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("取消订单失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("取消订单失败:" + e.getMessage()));
}
}
/**
* 发货
*/
@PostMapping("/{id}/ship")
public ResponseEntity<Map<String, Object>> shipOrder(@PathVariable Long id,
@RequestBody Map<String, String> request,
Authentication authentication) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
// 只有管理员可以发货
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
return ResponseEntity.badRequest()
.body(createErrorResponse("无权限操作此订单"));
}
String trackingNumber = request.get("trackingNumber");
Order shippedOrder = orderService.shipOrder(id, trackingNumber);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "订单发货成功");
response.put("data", shippedOrder);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("订单发货失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("订单发货失败:" + e.getMessage()));
}
}
/**
* 完成订单
*/
@PostMapping("/{id}/complete")
public ResponseEntity<Map<String, Object>> completeOrder(@PathVariable Long id,
Authentication authentication) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
// 只有管理员可以完成订单
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
return ResponseEntity.badRequest()
.body(createErrorResponse("无权限操作此订单"));
}
Order completedOrder = orderService.completeOrder(id);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "订单完成成功");
response.put("data", completedOrder);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("完成订单失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("完成订单失败:" + e.getMessage()));
}
}
/**
* 创建订单支付
*/
@PostMapping("/{id}/pay")
public ResponseEntity<Map<String, Object>> createOrderPayment(@PathVariable Long id,
@RequestBody Map<String, String> request,
Authentication authentication,
HttpServletRequest httpRequest) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Order order = orderService.findById(id)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 检查权限
if (!order.getUser().getId().equals(user.getId())) {
return ResponseEntity.badRequest()
.body(createErrorResponse("无权限操作此订单"));
}
// 检查订单状态
if (!order.canPay()) {
return ResponseEntity.badRequest()
.body(createErrorResponse("订单当前状态不允许支付"));
}
String pm = request.get("paymentMethod");
if (pm == null || pm.isBlank()) {
return ResponseEntity.badRequest().body(createErrorResponse("paymentMethod不能为空"));
}
PaymentMethod paymentMethod = PaymentMethod.valueOf(pm);
// 1) 创建支付记录并绑定订单
var payment = paymentService.createOrderPayment(order, paymentMethod);
// 2) 根据支付方式创建第三方支付
String clientIp = httpRequest != null ? httpRequest.getRemoteAddr() : null;
Map<String, Object> providerData = new HashMap<>();
if (paymentMethod == PaymentMethod.PAYPAL) {
if (payPalService == null) {
return ResponseEntity.badRequest().body(createErrorResponse("PayPal服务未配置"));
}
providerData = payPalService.createPayment(payment);
} else if (paymentMethod == PaymentMethod.ALIPAY) {
providerData = alipayService.createPayment(payment, clientIp);
} else {
// 其他方式走噜噜支付wxpay/qqpay/bank等
String payType = request.getOrDefault("payType", "alipay");
providerData = luluPayService.createPayment(payment, payType, clientIp);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付创建成功");
Map<String, Object> data = new HashMap<>();
data.put("paymentId", payment.getId());
data.put("orderId", order.getId());
data.put("orderNumber", order.getOrderNumber());
data.put("provider", providerData);
response.put("data", data);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("创建订单支付失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("创建订单支付失败:" + e.getMessage()));
}
}
/**
* 获取订单统计
*/
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> getOrderStats(Authentication authentication) {
try {
// 检查认证信息
if (authentication == null || !authentication.isAuthenticated()) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "用户未认证,请重新登录");
return ResponseEntity.status(401).body(response);
}
String username = authentication.getName();
User user = userService.findByUsername(username);
if (user == null) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "用户信息获取失败,请重新登录");
return ResponseEntity.status(401).body(response);
}
// 获取统计数据
Map<String, Object> stats;
if ((user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
// 管理员查看所有订单统计
stats = orderService.getOrderStats();
} else {
// 普通用户查看自己的订单统计
stats = orderService.getOrderStatsByUser(user);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", stats);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取订单统计失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("获取订单统计失败:" + e.getMessage()));
}
}
/**
* 删除订单
*/
@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Object>> deleteOrder(@PathVariable Long id,
Authentication authentication) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Order order = orderService.findById(id)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 检查权限
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN")) && !order.getUser().getId().equals(user.getId())) {
return ResponseEntity.badRequest()
.body(createErrorResponse("无权限删除此订单"));
}
orderService.deleteOrder(id);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "订单删除成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("删除订单失败:", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("删除订单失败:" + e.getMessage()));
}
}
/**
* 批量删除订单
*/
@DeleteMapping("/batch")
public ResponseEntity<Map<String, Object>> deleteOrders(@RequestBody List<Long> orderIds,
Authentication authentication) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
// 只有管理员可以批量删除
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
return ResponseEntity.badRequest()
.body(createErrorResponse("无权限批量删除订单"));
}
int deletedCount = orderService.deleteOrdersByIds(orderIds);
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);
response.put("message", message);
return response;
}
}

View File

@@ -0,0 +1,454 @@
package com.example.demo.controller;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.demo.model.Order;
import com.example.demo.model.OrderItem;
import com.example.demo.model.OrderStatus;
import com.example.demo.model.OrderType;
import com.example.demo.model.Payment;
import com.example.demo.model.PaymentMethod;
import com.example.demo.model.User;
import com.example.demo.service.OrderService;
import com.example.demo.service.PaymentService;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
@Controller
@RequestMapping("/orders")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private UserService userService;
/**
* 显示订单列表
*/
@GetMapping
public String orderList(@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 search,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Sort sort = sortDir.equalsIgnoreCase("desc") ?
Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<Order> orders;
if (status != null) {
orders = orderService.findByStatus(status, pageable);
} else {
orders = orderService.findByUserId(user.getId(), pageable);
}
model.addAttribute("orders", orders);
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", orders.getTotalPages());
model.addAttribute("totalElements", orders.getTotalElements());
model.addAttribute("status", status);
model.addAttribute("search", search);
model.addAttribute("sortBy", sortBy);
model.addAttribute("sortDir", sortDir);
model.addAttribute("orderStatuses", OrderStatus.values());
return "orders/list";
} catch (Exception e) {
logger.error("获取订单列表失败:", e);
model.addAttribute("error", "获取订单列表失败:" + e.getMessage());
return "orders/list";
}
}
/**
* 显示订单详情
*/
@GetMapping("/{id}")
public String orderDetail(@PathVariable Long id,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Optional<Order> orderOpt = orderService.findById(id);
if (!orderOpt.isPresent()) {
model.addAttribute("error", "订单不存在");
return "orders/detail";
}
Order order = orderOpt.get();
// 检查权限:用户只能查看自己的订单,管理员可以查看所有订单
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN") && !order.getUser().getId().equals(user.getId())) {
model.addAttribute("error", "无权限访问此订单");
return "orders/detail";
}
model.addAttribute("order", order);
model.addAttribute("orderStatuses", OrderStatus.values());
return "orders/detail";
} catch (Exception e) {
logger.error("获取订单详情失败:", e);
model.addAttribute("error", "获取订单详情失败:" + e.getMessage());
return "orders/detail";
}
}
/**
* 显示创建订单表单
*/
@GetMapping("/create")
public String showCreateOrderForm(Model model) {
Order order = new Order();
order.setOrderItems(new ArrayList<>());
order.setOrderItems(new ArrayList<>());
// 添加一个空的订单项
OrderItem item = new OrderItem();
item.setQuantity(1);
item.setUnitPrice(BigDecimal.ZERO);
order.getOrderItems().add(item);
model.addAttribute("order", order);
model.addAttribute("orderTypes", OrderType.values());
model.addAttribute("orderStatuses", OrderStatus.values());
return "orders/form";
}
/**
* 处理创建订单
*/
@PostMapping("/create")
public String createOrder(@Valid @ModelAttribute Order order,
BindingResult result,
Authentication authentication,
Model model) {
try {
if (result.hasErrors()) {
model.addAttribute("orderTypes", OrderType.values());
model.addAttribute("orderStatuses", OrderStatus.values());
return "orders/form";
}
String username = authentication.getName();
User user = userService.findByUsername(username);
order.setUser(user);
// 验证订单项
if (order.getOrderItems() == null || order.getOrderItems().isEmpty()) {
model.addAttribute("error", "订单必须包含至少一个商品");
model.addAttribute("orderTypes", OrderType.values());
model.addAttribute("orderStatuses", OrderStatus.values());
return "orders/form";
}
// 过滤掉空的订单项
order.getOrderItems().removeIf(item ->
item.getProductName() == null || item.getProductName().trim().isEmpty() ||
item.getQuantity() == null || item.getQuantity() <= 0 ||
item.getUnitPrice() == null || item.getUnitPrice().compareTo(BigDecimal.ZERO) <= 0);
if (order.getOrderItems().isEmpty()) {
model.addAttribute("error", "订单必须包含至少一个有效商品");
model.addAttribute("orderTypes", OrderType.values());
model.addAttribute("orderStatuses", OrderStatus.values());
return "orders/form";
}
Order createdOrder = orderService.createOrder(order);
model.addAttribute("success", "订单创建成功,订单号:" + createdOrder.getOrderNumber());
return "redirect:/orders/" + createdOrder.getId();
} catch (Exception e) {
logger.error("创建订单失败:", e);
model.addAttribute("error", "创建订单失败:" + e.getMessage());
model.addAttribute("orderTypes", OrderType.values());
model.addAttribute("orderStatuses", OrderStatus.values());
return "orders/form";
}
}
/**
* 更新订单状态
*/
@PostMapping("/{id}/status")
public String updateOrderStatus(@PathVariable Long id,
@RequestParam OrderStatus status,
@RequestParam(required = false) String notes,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Optional<Order> orderOpt = orderService.findById(id);
if (!orderOpt.isPresent()) {
model.addAttribute("error", "订单不存在");
return "redirect:/orders";
}
Order order = orderOpt.get();
// 检查权限
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN") && !order.getUser().getId().equals(user.getId())) {
model.addAttribute("error", "无权限操作此订单");
return "redirect:/orders";
}
Order updatedOrder = orderService.updateOrderStatus(id, status);
if (notes != null && !notes.trim().isEmpty()) {
updatedOrder.setNotes((updatedOrder.getNotes() != null ? updatedOrder.getNotes() + "\n" : "") + notes);
orderService.createOrder(updatedOrder); // 保存备注
}
model.addAttribute("success", "订单状态更新成功");
return "redirect:/orders/" + id;
} catch (Exception e) {
logger.error("更新订单状态失败:", e);
model.addAttribute("error", "更新订单状态失败:" + e.getMessage());
return "redirect:/orders/" + id;
}
}
/**
* 取消订单
*/
@PostMapping("/{id}/cancel")
public String cancelOrder(@PathVariable Long id,
@RequestParam(required = false) String reason,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Optional<Order> orderOpt = orderService.findById(id);
if (!orderOpt.isPresent()) {
model.addAttribute("error", "订单不存在");
return "redirect:/orders";
}
Order order = orderOpt.get();
// 检查权限
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN") && !order.getUser().getId().equals(user.getId())) {
model.addAttribute("error", "无权限操作此订单");
return "redirect:/orders";
}
orderService.cancelOrder(id, reason);
model.addAttribute("success", "订单取消成功");
return "redirect:/orders/" + id;
} catch (Exception e) {
logger.error("取消订单失败:", e);
model.addAttribute("error", "取消订单失败:" + e.getMessage());
return "redirect:/orders/" + id;
}
}
/**
* 发货
*/
@PostMapping("/{id}/ship")
public String shipOrder(@PathVariable Long id,
@RequestParam(required = false) String trackingNumber,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
// 只有管理员可以发货
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN")) {
model.addAttribute("error", "无权限操作此订单");
return "redirect:/orders/" + id;
}
orderService.shipOrder(id, trackingNumber);
model.addAttribute("success", "订单发货成功");
return "redirect:/orders/" + id;
} catch (Exception e) {
logger.error("订单发货失败:", e);
model.addAttribute("error", "订单发货失败:" + e.getMessage());
return "redirect:/orders/" + id;
}
}
/**
* 完成订单
*/
@PostMapping("/{id}/complete")
public String completeOrder(@PathVariable Long id,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
// 只有管理员可以完成订单
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN")) {
model.addAttribute("error", "无权限操作此订单");
return "redirect:/orders/" + id;
}
orderService.completeOrder(id);
model.addAttribute("success", "订单完成成功");
return "redirect:/orders/" + id;
} catch (Exception e) {
logger.error("完成订单失败:", e);
model.addAttribute("error", "完成订单失败:" + e.getMessage());
return "redirect:/orders/" + id;
}
}
/**
* 为订单创建支付
*/
@PostMapping("/{id}/pay")
public String createPaymentForOrder(@PathVariable Long id,
@RequestParam PaymentMethod paymentMethod,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Optional<Order> orderOpt = orderService.findById(id);
if (!orderOpt.isPresent()) {
model.addAttribute("error", "订单不存在");
return "redirect:/orders";
}
Order order = orderOpt.get();
// 检查权限
if (!order.getUser().getId().equals(user.getId())) {
model.addAttribute("error", "无权限操作此订单");
return "redirect:/orders";
}
// 检查订单状态
if (!order.canPay()) {
model.addAttribute("error", "订单当前状态不允许支付");
return "redirect:/orders/" + id;
}
// 创建支付记录
Payment savedPayment = paymentService.createOrderPayment(order, paymentMethod);
// 根据支付方式跳转到相应的支付页面
if (paymentMethod == PaymentMethod.ALIPAY) {
return "redirect:/payment/alipay/create?paymentId=" + savedPayment.getId();
} else {
model.addAttribute("error", "不支持的支付方式");
return "redirect:/orders/" + id;
}
} catch (Exception e) {
logger.error("创建订单支付失败:", e);
model.addAttribute("error", "创建订单支付失败:" + e.getMessage());
return "redirect:/orders/" + id;
}
}
/**
* 管理员订单管理页面
*/
@GetMapping("/admin")
public String adminOrderList(@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 search,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
// 只有管理员可以访问
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN")) {
model.addAttribute("error", "无权限访问");
return "redirect:/orders";
}
Sort sort = sortDir.equalsIgnoreCase("desc") ?
Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<Order> orders;
if (status != null) {
orders = orderService.findByStatus(status, pageable);
} else {
orders = orderService.findAll(pageable);
}
model.addAttribute("orders", orders);
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", orders.getTotalPages());
model.addAttribute("totalElements", orders.getTotalElements());
model.addAttribute("status", status);
model.addAttribute("search", search);
model.addAttribute("sortBy", sortBy);
model.addAttribute("sortDir", sortDir);
model.addAttribute("orderStatuses", OrderStatus.values());
model.addAttribute("isAdmin", true);
return "orders/admin";
} catch (Exception e) {
logger.error("获取管理员订单列表失败:", e);
model.addAttribute("error", "获取订单列表失败:" + e.getMessage());
return "orders/admin";
}
}
}

View File

@@ -0,0 +1,349 @@
package com.example.demo.controller;
import com.example.demo.model.Payment;
import com.example.demo.model.PaymentStatus;
import com.example.demo.service.PayPalService;
import com.example.demo.service.PaymentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* PayPal支付控制器
* 处理PayPal支付相关的HTTP请求
*/
@RestController
@RequestMapping("/api/payment/paypal")
@Tag(name = "PayPal支付", description = "PayPal支付相关接口")
@CrossOrigin(origins = "*")
public class PayPalController {
private static final Logger logger = LoggerFactory.getLogger(PayPalController.class);
@Autowired(required = false)
private PayPalService payPalService;
@Autowired
private PaymentService paymentService;
@Value("${app.frontend-url:https://www.vionow.com}")
private String frontendUrl;
/**
* 创建PayPal支付
* 支持两种模式:
* 1. 传入 paymentId使用已有的支付记录
* 2. 不传入 paymentId创建新的支付记录
*/
@PostMapping("/create")
@Operation(summary = "创建PayPal支付", description = "创建PayPal支付订单并返回支付URL")
public ResponseEntity<Map<String, Object>> createPayment(@RequestBody Map<String, Object> request) {
try {
logger.info("=== 创建PayPal支付请求 ===");
logger.info("请求参数: {}", request);
if (payPalService == null) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "PayPal服务未配置");
return ResponseEntity.badRequest().body(errorResponse);
}
Payment payment;
// 检查是否传入了已有的 paymentId
Object paymentIdObj = request.get("paymentId");
if (paymentIdObj != null) {
Long paymentId = Long.valueOf(paymentIdObj.toString());
logger.info("使用已有的支付记录paymentId: {}", paymentId);
Optional<Payment> existingPayment = paymentService.findById(paymentId);
if (existingPayment.isEmpty()) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "支付记录不存在");
return ResponseEntity.badRequest().body(errorResponse);
}
payment = existingPayment.get();
// 检查支付状态,如果已成功则不允许重复创建
if (payment.getStatus() == PaymentStatus.SUCCESS) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "该支付已完成,请勿重复支付");
return ResponseEntity.badRequest().body(errorResponse);
}
} else {
// 旧模式:创建新的支付记录
String username = (String) request.get("username");
String orderId = (String) request.get("orderId");
String amount = request.get("amount") != null ? request.get("amount").toString() : null;
String method = (String) request.get("method");
String description = (String) request.get("description");
payment = paymentService.createPayment(username, orderId, amount, method, description);
}
// 调用 PayPal API 创建支付
String paypalUrl = null;
String paypalPaymentId = null;
if (payPalService != null) {
try {
Map<String, Object> paypalResult = payPalService.createPayment(payment);
if (paypalResult.containsKey("paymentUrl")) {
paypalUrl = paypalResult.get("paymentUrl").toString();
}
if (paypalResult.containsKey("paypalPaymentId")) {
paypalPaymentId = paypalResult.get("paypalPaymentId").toString();
}
logger.info("PayPal API 返回: url={}, paypalId={}", paypalUrl, paypalPaymentId);
} catch (Exception e) {
logger.error("调用 PayPal API 失败: {}", e.getMessage());
}
}
// 如果 PayPal API 未配置或调用失败,返回错误
if (paypalUrl == null || paypalUrl.isEmpty()) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "PayPal服务暂不可用请使用支付宝支付");
return ResponseEntity.badRequest().body(errorResponse);
}
// 保存 PayPal 的 paymentId 到数据库
if (paypalPaymentId != null) {
payment.setExternalTransactionId(paypalPaymentId);
payment.setPaymentUrl(paypalUrl);
paymentService.save(payment);
logger.info("已保存 PayPal paymentId: {} 到支付记录: {}", paypalPaymentId, payment.getId());
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("paymentId", payment.getId());
response.put("paymentUrl", paypalUrl);
response.put("externalTransactionId", paypalPaymentId);
logger.info("✅ PayPal支付创建成功: {}", response);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("❌ 创建PayPal支付失败", e);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", e.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
}
}
/**
* PayPal支付成功回调用户同意支付后
*/
@GetMapping("/success")
@Operation(summary = "PayPal支付成功回调", description = "用户在PayPal完成支付后的回调")
public RedirectView paymentSuccess(
@RequestParam("paymentId") Long paymentId,
@RequestParam("PayerID") String payerId,
@RequestParam("token") String token) {
try {
logger.info("=== PayPal支付成功回调 ===");
logger.info("Payment ID: {}", paymentId);
logger.info("Payer ID: {}", payerId);
logger.info("Token: {}", token);
// 检查PayPal服务是否可用
if (payPalService == null) {
logger.error("PayPal服务未配置");
return new RedirectView(frontendUrl + "/subscription?paymentStatus=error&message=PayPal service not configured");
}
// 获取支付记录
Optional<Payment> paymentOpt = paymentService.findById(paymentId);
if (!paymentOpt.isPresent()) {
logger.error("支付记录不存在: {}", paymentId);
return new RedirectView(frontendUrl + "/subscription?paymentStatus=error&message=Payment not found");
}
Payment payment = paymentOpt.get();
String paypalPaymentId = payment.getExternalTransactionId();
// 执行PayPal支付
Map<String, Object> result = payPalService.executePayment(paypalPaymentId, payerId);
if (Boolean.TRUE.equals(result.get("success"))) {
// 更新支付状态为成功
String transactionId = (String) result.get("transactionId");
paymentService.confirmPaymentSuccess(paymentId, transactionId);
logger.info("✅ PayPal支付确认成功");
// 重定向到前端会员订阅页面(支付成功)
return new RedirectView(frontendUrl + "/subscription?paymentStatus=success&paymentId=" + paymentId);
} else {
logger.error("PayPal支付执行失败");
return new RedirectView(frontendUrl + "/subscription?paymentStatus=error&message=Payment execution failed");
}
} catch (Exception e) {
logger.error("❌ PayPal支付成功回调处理失败", e);
return new RedirectView(frontendUrl + "/subscription?paymentStatus=error&message=" + e.getMessage());
}
}
/**
* PayPal支付取消回调用户取消支付
*/
@GetMapping("/cancel")
@Operation(summary = "PayPal支付取消回调", description = "用户取消PayPal支付后的回调")
public RedirectView paymentCancel(@RequestParam("paymentId") Long paymentId) {
try {
logger.info("=== PayPal支付取消回调 ===");
logger.info("Payment ID: {}", paymentId);
// 更新支付状态为取消
paymentService.updatePaymentStatus(paymentId, PaymentStatus.CANCELLED);
logger.info("✅ PayPal支付已取消");
// 重定向到前端会员订阅页面(支付已取消)
return new RedirectView(frontendUrl + "/subscription?paymentStatus=cancelled&paymentId=" + paymentId);
} catch (Exception e) {
logger.error("❌ PayPal支付取消回调处理失败", e);
return new RedirectView(frontendUrl + "/subscription?paymentStatus=error&message=" + e.getMessage());
}
}
/**
* 查询PayPal支付状态
*/
@GetMapping("/status/{paymentId}")
@Operation(summary = "查询PayPal支付状态", description = "查询指定支付的当前状态")
public ResponseEntity<Map<String, Object>> getPaymentStatus(@PathVariable Long paymentId) {
try {
logger.info("=== 查询PayPal支付状态 ===");
logger.info("Payment ID: {}", paymentId);
Optional<Payment> paymentOpt = paymentService.findById(paymentId);
if (!paymentOpt.isPresent()) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "支付记录不存在");
return ResponseEntity.badRequest().body(errorResponse);
}
Payment payment = paymentOpt.get();
String paypalPaymentId = payment.getExternalTransactionId();
if (paypalPaymentId == null || paypalPaymentId.isEmpty()) {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("status", payment.getStatus().name());
response.put("localStatus", true);
return ResponseEntity.ok(response);
}
// 从PayPal查询状态
Map<String, Object> paypalStatus = payPalService.getPaymentStatus(paypalPaymentId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("localStatus", payment.getStatus().name());
response.put("paypalStatus", paypalStatus.get("state"));
response.put("paypalDetails", paypalStatus);
logger.info("✅ 查询成功: {}", response);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("❌ 查询PayPal支付状态失败", e);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", e.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
}
}
/**
* PayPal退款
*/
@PostMapping("/refund")
@Operation(summary = "PayPal退款", description = "对指定支付进行退款")
public ResponseEntity<Map<String, Object>> refundPayment(@RequestBody Map<String, String> request) {
try {
logger.info("=== PayPal退款请求 ===");
logger.info("请求参数: {}", request);
if (payPalService == null) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "PayPal服务未配置");
return ResponseEntity.badRequest().body(errorResponse);
}
Long paymentId = Long.parseLong(request.get("paymentId"));
// 获取支付记录
Optional<Payment> paymentOpt = paymentService.findById(paymentId);
if (!paymentOpt.isPresent()) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "支付记录不存在");
return ResponseEntity.badRequest().body(errorResponse);
}
Payment payment = paymentOpt.get();
String saleId = payment.getExternalTransactionId();
if (saleId == null || saleId.isEmpty()) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "无法获取交易ID");
return ResponseEntity.badRequest().body(errorResponse);
}
// 执行退款
Map<String, Object> result = payPalService.refundPayment(saleId);
if (Boolean.TRUE.equals(result.get("success"))) {
// 更新支付状态
paymentService.updatePaymentStatus(paymentId, PaymentStatus.REFUNDED);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("refundId", result.get("refundId"));
response.put("message", "退款成功");
logger.info("✅ PayPal退款成功");
return ResponseEntity.ok(response);
} else {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "退款失败");
return ResponseEntity.badRequest().body(errorResponse);
}
} catch (Exception e) {
logger.error("❌ PayPal退款失败", e);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", e.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
}
}
}

View File

@@ -0,0 +1,856 @@
package com.example.demo.controller;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
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;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import com.example.demo.model.Payment;
import com.example.demo.model.PaymentMethod;
import com.example.demo.model.PaymentStatus;
import com.example.demo.model.User;
import com.example.demo.model.UserMembership;
import com.example.demo.model.MembershipLevel;
import com.example.demo.repository.PaymentRepository;
import com.example.demo.repository.UserMembershipRepository;
import com.example.demo.repository.MembershipLevelRepository;
import com.example.demo.service.AlipayService;
import com.example.demo.service.LuluPayService;
import com.example.demo.service.OrderService;
import com.example.demo.service.PaymentService;
import com.example.demo.service.UserService;
import com.example.demo.util.JwtUtils;
@RestController
@RequestMapping("/api/payments")
public class PaymentApiController {
private static final Logger logger = LoggerFactory.getLogger(PaymentApiController.class);
@Autowired
private PaymentService paymentService;
@Autowired
private AlipayService alipayService;
@Autowired
private LuluPayService luluPayService;
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserMembershipRepository userMembershipRepository;
@Autowired
private MembershipLevelRepository membershipLevelRepository;
/**
* 获取用户的支付记录
*/
@GetMapping
public ResponseEntity<Map<String, Object>> getUserPayments(
Authentication authentication,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) String search) {
try {
List<Payment> payments;
// 检查用户是否已登录
if (authentication != null && authentication.isAuthenticated()) {
String username = authentication.getName();
payments = paymentService.findByUsername(username);
} else {
// 未登录用户返回空列表
payments = new ArrayList<>();
}
// 简单的筛选逻辑
if (status != null && !status.isEmpty()) {
payments = payments.stream()
.filter(p -> p.getStatus().name().equals(status))
.toList();
}
if (search != null && !search.isEmpty()) {
payments = payments.stream()
.filter(p -> p.getOrderId().contains(search))
.toList();
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "获取支付记录成功");
response.put("data", payments);
response.put("total", payments.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取支付记录失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("获取支付记录失败: " + e.getMessage()));
}
}
/**
* 根据ID获取支付详情
*/
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getPaymentById(
@PathVariable Long id,
Authentication authentication) {
try {
Payment payment = paymentService.findById(id)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
// 检查权限
if (!payment.getUser().getUsername().equals(authentication.getName())) {
return ResponseEntity.status(403)
.body(createErrorResponse("无权限访问此支付记录"));
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", payment);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取支付详情失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("获取支付详情失败: " + e.getMessage()));
}
}
/**
* 创建支付
*/
@PostMapping("/create")
public ResponseEntity<Map<String, Object>> createPayment(
@RequestBody Map<String, Object> paymentData,
Authentication authentication) {
try {
String username;
if (authentication != null && authentication.isAuthenticated()) {
username = authentication.getName();
} else {
// 未登录用户使用匿名用户名
username = "anonymous_" + System.currentTimeMillis();
}
String orderId = (String) paymentData.get("orderId");
String amountStr = paymentData.get("amount") != null ? paymentData.get("amount").toString() : null;
String method = (String) paymentData.get("method");
String description = (String) paymentData.get("description");
if (orderId == null || amountStr == null || method == null) {
return ResponseEntity.badRequest()
.body(createErrorResponse("订单号、金额和支付方式不能为空"));
}
Payment payment = paymentService.createPayment(username, orderId, amountStr, method, description);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付创建成功");
response.put("data", payment);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("创建支付失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("创建支付失败: " + e.getMessage()));
}
}
/**
* 更新支付状态
*/
@PutMapping("/{id}/status")
public ResponseEntity<Map<String, Object>> updatePaymentStatus(
@PathVariable Long id,
@RequestBody Map<String, String> statusData,
Authentication authentication) {
try {
String status = statusData.get("status");
if (status == null) {
return ResponseEntity.badRequest()
.body(createErrorResponse("状态不能为空"));
}
Payment payment = paymentService.findById(id)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
// 检查权限
if (!payment.getUser().getUsername().equals(authentication.getName())) {
return ResponseEntity.status(403)
.body(createErrorResponse("无权限修改此支付记录"));
}
payment.setStatus(PaymentStatus.valueOf(status));
paymentService.save(payment);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "状态更新成功");
response.put("data", payment);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("更新支付状态失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("更新支付状态失败: " + e.getMessage()));
}
}
/**
* 更新支付方式和描述(用于切换支付方式时)
*/
@PutMapping("/{id}/method")
public ResponseEntity<Map<String, Object>> updatePaymentMethod(
@PathVariable Long id,
@RequestBody Map<String, String> methodData,
Authentication authentication) {
try {
String method = methodData.get("method");
String description = methodData.get("description");
if (method == null) {
return ResponseEntity.badRequest()
.body(createErrorResponse("支付方式不能为空"));
}
// 调用Service层方法带事务
Payment payment = paymentService.updatePaymentMethod(id, method, description, authentication.getName());
logger.info("支付方式更新成功: paymentId={}, method={}, description={}", id, method, description);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付方式更新成功");
response.put("data", payment);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("更新支付方式失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("更新支付方式失败: " + e.getMessage()));
}
}
/**
* 确认支付成功
*/
@PostMapping("/{id}/success")
public ResponseEntity<Map<String, Object>> confirmPaymentSuccess(
@PathVariable Long id,
@RequestBody Map<String, String> successData,
Authentication authentication) {
try {
String externalTransactionId = successData.get("externalTransactionId");
Payment payment = paymentService.findById(id)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
// 检查权限
if (!payment.getUser().getUsername().equals(authentication.getName())) {
return ResponseEntity.status(403)
.body(createErrorResponse("无权限操作此支付记录"));
}
paymentService.confirmPaymentSuccess(id, externalTransactionId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付确认成功");
response.put("data", payment);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("确认支付成功失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("确认支付成功失败: " + e.getMessage()));
}
}
/**
* 确认支付失败
*/
@PostMapping("/{id}/failure")
public ResponseEntity<Map<String, Object>> confirmPaymentFailure(
@PathVariable Long id,
@RequestBody Map<String, String> failureData,
Authentication authentication) {
try {
String failureReason = failureData.get("failureReason");
Payment payment = paymentService.findById(id)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
// 检查权限
if (!payment.getUser().getUsername().equals(authentication.getName())) {
return ResponseEntity.status(403)
.body(createErrorResponse("无权限操作此支付记录"));
}
paymentService.confirmPaymentFailure(id, failureReason);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付失败确认成功");
response.put("data", payment);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("确认支付失败失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("确认支付失败失败: " + e.getMessage()));
}
}
/**
* 获取支付统计
*/
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> getPaymentStats(Authentication authentication) {
try {
String username = authentication.getName();
Map<String, Object> stats = paymentService.getUserPaymentStats(username);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", stats);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取支付统计失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("获取支付统计失败: " + e.getMessage()));
}
}
/**
* 创建测试支付记录
*/
@PostMapping("/create-test")
public ResponseEntity<Map<String, Object>> createTestPayment(
@RequestBody Map<String, Object> paymentData,
Authentication authentication) {
try {
String username;
if (authentication != null && authentication.isAuthenticated()) {
username = authentication.getName();
} else {
return ResponseEntity.badRequest()
.body(createErrorResponse("请先登录后再创建支付记录"));
}
String amountStr = paymentData.get("amount") != null ? paymentData.get("amount").toString() : null;
String method = (String) paymentData.get("method");
if (amountStr == null || method == null) {
return ResponseEntity.badRequest()
.body(createErrorResponse("金额和支付方式不能为空"));
}
// 生成测试订单号
String testOrderId = "TEST_" + System.currentTimeMillis();
Payment payment = paymentService.createPayment(username, testOrderId, amountStr, method, "测试支付");
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "测试支付记录创建成功");
response.put("data", payment);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("创建测试支付记录失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("创建测试支付记录失败: " + e.getMessage()));
}
}
/**
* 测试支付完成(用于测试自动创建订单功能)
*/
@PostMapping("/{id}/test-complete")
public ResponseEntity<Map<String, Object>> testPaymentComplete(
@PathVariable Long id,
Authentication authentication) {
try {
Payment payment = paymentService.findById(id)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
// 检查权限
if (!payment.getUser().getUsername().equals(authentication.getName())) {
return ResponseEntity.status(403)
.body(createErrorResponse("无权限操作此支付记录"));
}
// 调用真实支付服务确认支付
String transactionId = "TXN_" + System.currentTimeMillis();
paymentService.confirmPaymentSuccess(id, transactionId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付完成测试成功,订单已自动创建");
response.put("data", payment);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("测试支付完成失败", e);
return ResponseEntity.badRequest()
.body(createErrorResponse("测试支付完成失败: " + e.getMessage()));
}
}
/**
* 获取用户订阅信息(当前套餐、到期时间等)
*/
@GetMapping("/subscription/info")
public ResponseEntity<Map<String, Object>> getUserSubscriptionInfo(
Authentication authentication,
jakarta.servlet.http.HttpServletRequest request) {
try {
if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.badRequest()
.body(createErrorResponse("请先登录"));
}
User user = null;
String username = authentication.getName();
try {
// Principal 现在是用户名字符串,直接通过用户名查找
user = userService.findByUsernameOrNull(username);
// 如果通过用户名找不到尝试从JWT token中获取用户ID
if (user == null) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Long userId = jwtUtils.getUserIdFromToken(token);
if (userId != null) {
logger.warn("通过用户名未找到用户尝试通过用户ID查找: {}", userId);
user = userService.findById(userId);
}
} catch (Exception e) {
logger.warn("从token获取用户ID失败: {}", e.getMessage());
}
}
}
// 如果通过用户名找不到,尝试通过邮箱查找(兼容邮箱登录的情况)
if (user == null && username.contains("@")) {
logger.warn("通过用户名未找到用户,尝试通过邮箱查找: {}", username);
user = userService.findByEmailOrNull(username);
}
} catch (Exception e) {
logger.error("查找用户失败: {}", e.getMessage(), e);
}
if (user == null) {
logger.error("用户不存在: {}", username);
return ResponseEntity.badRequest()
.body(createErrorResponse("用户不存在"));
}
// 获取用户最近一次成功的支付记录(包括所有充值记录,不仅仅是订阅)
List<Payment> allPayments;
try {
// 获取所有成功支付的记录,按支付时间倒序
allPayments = paymentRepository.findByUserIdOrderByCreatedAtDesc(user.getId())
.stream()
.filter(p -> p.getStatus() == PaymentStatus.SUCCESS)
.sorted((p1, p2) -> {
LocalDateTime time1 = p1.getPaidAt() != null ? p1.getPaidAt() : p1.getCreatedAt();
LocalDateTime time2 = p2.getPaidAt() != null ? p2.getPaidAt() : p2.getCreatedAt();
return time2.compareTo(time1); // 倒序
})
.collect(java.util.stream.Collectors.toList());
} catch (Exception e) {
logger.error("查询支付记录失败用户ID: {}", user.getId(), e);
// 如果查询失败,使用空列表
allPayments = new ArrayList<>();
}
Map<String, Object> subscriptionInfo = new HashMap<>();
// 默认值:先判断是否有支付记录来确定是免费会员还是入门会员
String currentPlan = null;
String expiryTime = "永久";
LocalDateTime paidAt = null;
// 优先从UserMembership表获取用户的实际会员等级按到期时间降序返回最新的
try {
java.util.Optional<UserMembership> membershipOpt = userMembershipRepository.findFirstByUserIdAndStatusOrderByEndDateDesc(user.getId(), "ACTIVE");
if (membershipOpt.isPresent()) {
UserMembership membership = membershipOpt.get();
LocalDateTime endDate = membership.getEndDate();
LocalDateTime now = LocalDateTime.now();
if (endDate != null && endDate.isAfter(now)) {
// 会员未过期,获取会员等级名称
java.util.Optional<MembershipLevel> levelOpt = membershipLevelRepository.findById(membership.getMembershipLevelId());
if (levelOpt.isPresent()) {
MembershipLevel level = levelOpt.get();
String levelName = level.getName();
// 🔥 只有 free 等级才需要根据充值金额判定,其他等级直接使用 displayName
if ("free".equalsIgnoreCase(levelName)) {
// 计算用户的总充值金额(而非充值次数)
java.math.BigDecimal totalPaid = paymentRepository.sumAmountByUserIdAndStatus(
user.getId(),
PaymentStatus.SUCCESS
);
// 获取入门版的价格阈值
double freePrice = level.getPrice() != null ? level.getPrice() : 0.0;
double totalAmount = totalPaid != null ? totalPaid.doubleValue() : 0.0;
// 充值金额 >= 入门版价格 → 入门会员,否则 → 免费会员
if (totalAmount >= freePrice && freePrice > 0) {
currentPlan = "入门会员"; // 充值金额达到入门版价格
} else {
currentPlan = "免费会员"; // 未充值或充值金额不足
}
} else {
// 标准版、专业版直接使用 displayName
currentPlan = level.getDisplayName() != null ? level.getDisplayName() : level.getName();
}
}
expiryTime = endDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
paidAt = membership.getStartDate();
} else if (endDate != null) {
// 会员已过期,会员等级保留,但显示已过期
expiryTime = "已过期";
java.util.Optional<MembershipLevel> levelOpt = membershipLevelRepository.findById(membership.getMembershipLevelId());
if (levelOpt.isPresent()) {
MembershipLevel level = levelOpt.get();
String levelName = level.getName();
// 🔥 过期后仍然按照原等级显示
if ("free".equalsIgnoreCase(levelName)) {
// free等级过期根据充值金额判定
java.math.BigDecimal totalPaid = paymentRepository.sumAmountByUserIdAndStatus(
user.getId(),
PaymentStatus.SUCCESS
);
double freePrice = level.getPrice() != null ? level.getPrice() : 0.0;
double totalAmount = totalPaid != null ? totalPaid.doubleValue() : 0.0;
currentPlan = (totalAmount >= freePrice && freePrice > 0) ? "入门会员" : "免费会员";
} else {
// 标准版、专业版过期仍然显示原等级名称
currentPlan = level.getDisplayName() != null ? level.getDisplayName() : level.getName();
}
}
}
}
} catch (Exception e) {
logger.warn("从UserMembership获取会员信息失败将使用支付记录判断: {}", e.getMessage());
}
// 如果currentPlan为null说明是free等级或没有会员记录需要判断是免费会员还是入门会员
if (currentPlan == null) {
// 从数据库获取 free 等级配置
try {
java.util.Optional<MembershipLevel> freeLevelOpt = membershipLevelRepository.findByName("free");
if (freeLevelOpt.isPresent()) {
MembershipLevel freeLevel = freeLevelOpt.get();
double freePrice = freeLevel.getPrice() != null ? freeLevel.getPrice() : 0.0;
// 计算用户总充值金额
java.math.BigDecimal totalPaid = paymentRepository.sumAmountByUserIdAndStatus(
user.getId(),
PaymentStatus.SUCCESS
);
double totalAmount = totalPaid != null ? totalPaid.doubleValue() : 0.0;
// 根据充值金额判定
if (totalAmount >= freePrice && freePrice > 0) {
currentPlan = "入门会员"; // 充值金额达到入门版价格
} else {
currentPlan = "免费会员"; // 未充值或充值金额不足
}
} else {
// 找不到 free 等级配置,默认为免费会员
currentPlan = "免费会员";
}
} catch (Exception e) {
logger.warn("获取 free 等级配置失败: {}", e.getMessage());
// 发生异常,默认为免费会员
currentPlan = "免费会员";
}
}
subscriptionInfo.put("currentPlan", currentPlan);
subscriptionInfo.put("expiryTime", expiryTime);
subscriptionInfo.put("paidAt", paidAt != null ? paidAt.toString() : null);
subscriptionInfo.put("points", user.getPoints());
subscriptionInfo.put("frozenPoints", user.getFrozenPoints() != null ? user.getFrozenPoints() : 0);
subscriptionInfo.put("availablePoints", user.getAvailablePoints());
subscriptionInfo.put("username", user.getUsername());
subscriptionInfo.put("userId", user.getId());
subscriptionInfo.put("email", user.getEmail());
subscriptionInfo.put("nickname", user.getNickname());
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", subscriptionInfo);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取用户订阅信息失败", e);
return ResponseEntity.status(500)
.body(createErrorResponse("获取用户订阅信息失败: " + e.getMessage()));
}
}
// 注意:支付宝异步通知接口已移至 AlipayCallbackController避免路由冲突
/**
* 创建支付宝支付
*/
@PostMapping("/alipay/create")
public ResponseEntity<Map<String, Object>> createAlipayPayment(
@RequestBody Map<String, Object> paymentData,
Authentication authentication,
HttpServletRequest request) {
try {
String username;
if (authentication != null && authentication.isAuthenticated()) {
username = authentication.getName();
} else {
logger.warn("用户未认证,拒绝支付请求");
return ResponseEntity.badRequest()
.body(createErrorResponse("请先登录后再创建支付"));
}
Long paymentId = Long.valueOf(paymentData.get("paymentId").toString());
Payment payment = paymentService.findById(paymentId)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
// 检查权限
if (!payment.getUser().getUsername().equals(username)) {
logger.warn("用户{}无权限操作支付记录{}", username, paymentId);
return ResponseEntity.status(403)
.body(createErrorResponse("无权限操作此支付记录"));
}
// 获取客户端IP
String clientIp = getClientIp(request);
logger.info("创建支付宝支付客户端IP: {}", clientIp);
// 调用支付宝接口创建支付
Map<String, Object> paymentResult = alipayService.createPayment(payment, clientIp);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付宝二维码生成成功");
response.put("data", paymentResult);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("创建支付宝支付失败", e);
// 简化错误消息避免显示过长的URL
String errorMsg = e.getMessage();
if (errorMsg != null && errorMsg.length() > 100) {
errorMsg = errorMsg.substring(0, 100) + "...";
}
return ResponseEntity.badRequest()
.body(createErrorResponse("创建支付宝支付失败: " + errorMsg));
}
}
/**
* 创建噜噜支付(彩虹易支付)
* 支持多种支付方式alipay/wxpay/qqpay/bank
*/
@PostMapping("/lulupay/create")
public ResponseEntity<Map<String, Object>> createLuluPayment(
@RequestBody Map<String, Object> paymentData,
Authentication authentication,
HttpServletRequest request) {
try {
String username;
if (authentication != null && authentication.isAuthenticated()) {
username = authentication.getName();
} else {
logger.warn("用户未认证,拒绝支付请求");
return ResponseEntity.badRequest()
.body(createErrorResponse("请先登录后再创建支付"));
}
Long paymentId = Long.valueOf(paymentData.get("paymentId").toString());
String payType = (String) paymentData.getOrDefault("payType", "alipay");
Payment payment = paymentService.findById(paymentId)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
// 检查权限
if (!payment.getUser().getUsername().equals(username)) {
logger.warn("用户{}无权限操作支付记录{}", username, paymentId);
return ResponseEntity.status(403)
.body(createErrorResponse("无权限操作此支付记录"));
}
// 获取客户端IP
String clientIp = getClientIp(request);
logger.info("创建噜噜支付客户端IP: {}", clientIp);
// 调用噜噜支付接口创建支付
Map<String, Object> paymentResult = luluPayService.createPayment(payment, payType, clientIp);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "支付创建成功");
response.put("data", paymentResult);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("创建噜噜支付失败", e);
String errorMsg = e.getMessage();
if (errorMsg != null && errorMsg.length() > 100) {
errorMsg = errorMsg.substring(0, 100) + "...";
}
return ResponseEntity.badRequest()
.body(createErrorResponse("创建噜噜支付失败: " + errorMsg));
}
}
/**
* 删除支付记录(仅管理员)
*/
@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()) && !"ROLE_SUPER_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()) && !"ROLE_SUPER_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);
response.put("message", message);
return response;
}
/**
* 获取客户端真实IP地址
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 如果是多个代理取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}

View File

@@ -0,0 +1,284 @@
package com.example.demo.controller;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.example.demo.model.Payment;
import com.example.demo.model.PaymentMethod;
import com.example.demo.model.User;
import com.example.demo.service.AlipayService;
import com.example.demo.service.PaymentService;
import com.example.demo.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
@Controller
@RequestMapping("/payment")
public class PaymentController {
private static final Logger logger = LoggerFactory.getLogger(PaymentController.class);
@Autowired
private PaymentService paymentService;
@Autowired
private AlipayService alipayService;
@Autowired
private UserService userService;
/**
* 显示支付页面
*/
@GetMapping("/create")
public String showPaymentForm(Model model) {
model.addAttribute("payment", new Payment());
model.addAttribute("paymentMethods", PaymentMethod.values());
return "payment/form";
}
/**
* 处理支付请求
*/
@PostMapping("/create")
public String createPayment(@Valid @ModelAttribute Payment payment,
BindingResult result,
Authentication authentication,
HttpServletRequest request,
Model model) {
if (result.hasErrors()) {
model.addAttribute("paymentMethods", PaymentMethod.values());
return "payment/form";
}
try {
// 设置当前用户
String username = authentication.getName();
User user = userService.findByUsername(username);
payment.setUser(user);
// 设置默认货币
if (payment.getCurrency() == null || payment.getCurrency().isEmpty()) {
payment.setCurrency("CNY");
}
// 获取客户端IP
String clientIp = getClientIp(request);
// 根据支付方式创建支付
if (payment.getPaymentMethod() == PaymentMethod.ALIPAY) {
Map<String, Object> paymentResult = alipayService.createPayment(payment, clientIp);
if (paymentResult.containsKey("qrCode")) {
// 对于二维码支付,重定向到支付页面显示二维码
return "redirect:/payment/qr?qrCode=" + paymentResult.get("qrCode");
}
return "redirect:/payment/error";
} else {
model.addAttribute("error", "不支持的支付方式");
model.addAttribute("paymentMethods", PaymentMethod.values());
return "payment/form";
}
} catch (Exception e) {
logger.error("创建支付订单失败:", e);
model.addAttribute("error", "创建支付订单失败:" + e.getMessage());
model.addAttribute("paymentMethods", PaymentMethod.values());
return "payment/form";
}
}
/**
* 支付宝异步通知
*/
@PostMapping("/alipay/notify")
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
logger.info("========== 收到支付宝回调请求 ==========");
logger.info("请求方法: {}", request.getMethod());
logger.info("请求URL: {}", request.getRequestURL());
logger.info("Content-Type: {}", request.getContentType());
try {
// 支付宝异步通知参数获取
// 注意IJPay的AliPayApi.toMap()使用javax.servlet但Spring Boot 3使用jakarta.servlet
// 所以手动获取参数
Map<String, String> params = new java.util.HashMap<>();
// 获取URL参数
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
params.put(key, values[0]);
}
});
// 如果是POST请求尝试从请求体获取参数
if ("POST".equalsIgnoreCase(request.getMethod())) {
try {
// 读取请求体
StringBuilder body = new StringBuilder();
try (java.io.BufferedReader reader = request.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
}
// 解析请求体参数(如果存在)
if (body.length() > 0) {
String bodyStr = body.toString();
// 支付宝可能使用form-urlencoded格式
if (bodyStr.contains("=")) {
String[] pairs = bodyStr.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=", 2);
if (keyValue.length == 2) {
try {
params.put(
java.net.URLDecoder.decode(keyValue[0], "UTF-8"),
java.net.URLDecoder.decode(keyValue[1], "UTF-8")
);
} catch (Exception e) {
logger.warn("解析参数失败: {}", pair, e);
}
}
}
}
}
} catch (Exception e) {
logger.warn("读取请求体失败", e);
}
}
logger.info("解析到的参数: {}", params);
boolean success = alipayService.handleNotify(params);
logger.info("处理结果: {}", success ? "success" : "fail");
logger.info("========== 支付宝回调处理完成 ==========");
return success ? "success" : "fail";
} catch (Exception e) {
logger.error("========== 处理支付宝异步通知失败 ==========", e);
return "fail";
}
}
/**
* 支付宝同步返回
*/
@GetMapping("/alipay/return")
public String alipayReturn(HttpServletRequest request, Model model) {
try {
// 支付宝同步返回参数获取GET请求从URL参数获取
Map<String, String> params = new java.util.HashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
params.put(key, values[0]);
}
});
boolean success = alipayService.handleReturn(params);
if (success) {
String outTradeNo = params.get("out_trade_no");
Payment payment = paymentService.findByOrderId(outTradeNo)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
model.addAttribute("payment", payment);
model.addAttribute("success", true);
return "payment/result";
} else {
model.addAttribute("error", "支付验证失败");
return "payment/result";
}
} catch (Exception e) {
logger.error("处理支付宝同步返回失败:", e);
model.addAttribute("error", "支付处理失败:" + e.getMessage());
return "payment/result";
}
}
/**
* 支付记录列表
*/
@GetMapping("/history")
public String paymentHistory(Authentication authentication, Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
List<Payment> payments = paymentService.findByUserId(user.getId());
model.addAttribute("payments", payments);
return "payment/history";
} catch (Exception e) {
logger.error("获取支付记录失败:", e);
model.addAttribute("error", "获取支付记录失败");
return "payment/history";
}
}
/**
* 支付详情
*/
@GetMapping("/detail/{id}")
public String paymentDetail(@PathVariable Long id,
Authentication authentication,
Model model) {
try {
String username = authentication.getName();
User user = userService.findByUsername(username);
Payment payment = paymentService.findById(id)
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
// 检查权限
if (!payment.getUser().getId().equals(user.getId())) {
model.addAttribute("error", "无权限访问此支付记录");
return "payment/detail";
}
model.addAttribute("payment", payment);
return "payment/detail";
} catch (Exception e) {
logger.error("获取支付详情失败:", e);
model.addAttribute("error", "获取支付详情失败");
return "payment/detail";
}
}
/**
* 获取客户端真实IP地址
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 如果是多个代理取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}

View File

@@ -0,0 +1,210 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.PointsFreezeRecord;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import com.example.demo.util.JwtUtils;
/**
* 积分冻结API控制器
*/
@RestController
@RequestMapping("/api/points")
public class PointsApiController {
private static final Logger logger = LoggerFactory.getLogger(PointsApiController.class);
@Autowired
private UserService userService;
@Autowired
private JwtUtils jwtUtils;
/**
* 获取用户积分信息
*/
@GetMapping("/info")
public ResponseEntity<Map<String, Object>> getPointsInfo(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
User user = userService.findByUsername(username);
Integer totalPoints = user.getPoints();
Integer frozenPoints = user.getFrozenPoints();
Integer availablePoints = user.getAvailablePoints();
Map<String, Object> pointsInfo = new HashMap<>();
pointsInfo.put("totalPoints", totalPoints);
pointsInfo.put("frozenPoints", frozenPoints);
pointsInfo.put("availablePoints", availablePoints);
response.put("success", true);
response.put("data", pointsInfo);
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);
}
}
/**
* 获取用户积分冻结记录
*/
@GetMapping("/freeze-records")
public ResponseEntity<Map<String, Object>> getFreezeRecords(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
List<PointsFreezeRecord> records = userService.getPointsFreezeRecords(username);
response.put("success", true);
response.put("data", records);
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);
}
}
/**
* 获取积分使用历史(充值和使用记录)
*/
@GetMapping("/history")
public ResponseEntity<Map<String, Object>> getPointsHistory(
@RequestHeader("Authorization") String token,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") int size) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 获取积分使用历史
List<Map<String, Object>> history = userService.getPointsHistory(username, page, size);
response.put("success", true);
response.put("data", history);
response.put("page", page);
response.put("size", size);
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);
}
}
/**
* 手动处理过期冻结记录(管理员功能)
*/
@PostMapping("/process-expired")
public ResponseEntity<Map<String, Object>> processExpiredRecords(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 这里可以添加管理员权限检查
// 暂时允许所有用户触发
int processedCount = userService.processExpiredFrozenRecords();
response.put("success", true);
response.put("message", "处理过期记录完成");
response.put("processedCount", processedCount);
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);
}
}
/**
* 从Token中提取用户名
*/
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
// 提取实际的token
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
return null;
}
// 验证token并获取用户名
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username != null && !jwtUtils.isTokenExpired(actualToken)) {
return username;
}
return null;
} catch (Exception e) {
logger.error("解析token失败", e);
return null;
}
}
}

View File

@@ -0,0 +1,225 @@
package com.example.demo.controller;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.ImageToVideoTask;
import com.example.demo.model.TaskQueue;
import com.example.demo.repository.ImageToVideoTaskRepository;
import com.example.demo.repository.TaskQueueRepository;
/**
* 轮询诊断控制器
* 专门用于诊断第三次轮询查询时的错误
*/
@RestController
@RequestMapping("/api/polling-diagnostic")
public class PollingDiagnosticController {
@Autowired
private TaskQueueRepository taskQueueRepository;
@Autowired
private ImageToVideoTaskRepository imageToVideoTaskRepository;
/**
* 检查特定任务的轮询状态
*/
@GetMapping("/task-status/{taskId}")
public ResponseEntity<Map<String, Object>> checkTaskPollingStatus(@PathVariable String taskId) {
Map<String, Object> response = new HashMap<>();
try {
// 检查任务队列状态
Optional<TaskQueue> taskQueueOpt = taskQueueRepository.findByTaskId(taskId);
if (!taskQueueOpt.isPresent()) {
response.put("success", false);
response.put("message", "找不到任务队列: " + taskId);
return ResponseEntity.notFound().build();
}
TaskQueue taskQueue = taskQueueOpt.get();
// 检查原始任务状态
Optional<ImageToVideoTask> imageTaskOpt = imageToVideoTaskRepository.findByTaskId(taskId);
Map<String, Object> taskInfo = new HashMap<>();
taskInfo.put("taskId", taskId);
taskInfo.put("queueStatus", taskQueue.getStatus());
taskInfo.put("queueErrorMessage", taskQueue.getErrorMessage());
taskInfo.put("queueCreatedAt", taskQueue.getCreatedAt());
taskInfo.put("queueUpdatedAt", taskQueue.getUpdatedAt());
taskInfo.put("checkCount", taskQueue.getCheckCount());
taskInfo.put("realTaskId", taskQueue.getRealTaskId());
if (imageTaskOpt.isPresent()) {
ImageToVideoTask imageTask = imageTaskOpt.get();
taskInfo.put("originalStatus", imageTask.getStatus());
taskInfo.put("originalProgress", imageTask.getProgress());
taskInfo.put("originalErrorMessage", imageTask.getErrorMessage());
taskInfo.put("originalCreatedAt", imageTask.getCreatedAt());
taskInfo.put("originalUpdatedAt", imageTask.getUpdatedAt());
taskInfo.put("firstFrameUrl", imageTask.getFirstFrameUrl());
taskInfo.put("lastFrameUrl", imageTask.getLastFrameUrl());
} else {
taskInfo.put("originalStatus", "NOT_FOUND");
}
// 分析问题
String analysis = analyzePollingIssue(taskQueue, imageTaskOpt.orElse(null));
taskInfo.put("analysis", analysis);
response.put("success", true);
response.put("taskInfo", taskInfo);
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "检查任务轮询状态失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 分析轮询问题
*/
private String analyzePollingIssue(TaskQueue taskQueue, ImageToVideoTask imageTask) {
StringBuilder analysis = new StringBuilder();
// 检查队列状态
switch (taskQueue.getStatus()) {
case FAILED:
analysis.append("❌ 队列状态: FAILED - ").append(taskQueue.getErrorMessage()).append("\n");
break;
case TIMEOUT:
analysis.append("❌ 队列状态: TIMEOUT - 任务处理超时\n");
break;
case PROCESSING:
analysis.append("⏳ 队列状态: PROCESSING - 任务正在处理中\n");
break;
case COMPLETED:
analysis.append("✅ 队列状态: COMPLETED - 任务已完成\n");
break;
default:
analysis.append("❓ 队列状态: ").append(taskQueue.getStatus()).append("\n");
break;
}
// 检查原始任务状态
if (imageTask != null) {
switch (imageTask.getStatus()) {
case FAILED:
analysis.append("❌ 原始任务状态: FAILED - ").append(imageTask.getErrorMessage()).append("\n");
break;
case COMPLETED:
analysis.append("✅ 原始任务状态: COMPLETED\n");
break;
case PROCESSING:
analysis.append("⏳ 原始任务状态: PROCESSING - 进度: ").append(imageTask.getProgress()).append("%\n");
break;
default:
analysis.append("❓ 原始任务状态: ").append(imageTask.getStatus()).append("\n");
break;
}
} else {
analysis.append("❌ 原始任务: 未找到\n");
}
// 检查轮询次数
int checkCount = taskQueue.getCheckCount();
analysis.append("📊 轮询次数: ").append(checkCount).append("\n");
if (checkCount >= 3) {
analysis.append("⚠️ 已进行多次轮询,可能存在问题\n");
}
// 检查时间
LocalDateTime now = LocalDateTime.now();
if (taskQueue.getCreatedAt() != null) {
long minutesSinceCreated = java.time.Duration.between(taskQueue.getCreatedAt(), now).toMinutes();
analysis.append("⏰ 任务创建时间: ").append(minutesSinceCreated).append(" 分钟前\n");
if (minutesSinceCreated > 10) {
analysis.append("⚠️ 任务创建时间过长,可能已超时\n");
}
}
// 检查图片文件
if (imageTask != null && imageTask.getFirstFrameUrl() != null) {
analysis.append("🖼️ 首帧图片: ").append(imageTask.getFirstFrameUrl()).append("\n");
}
return analysis.toString();
}
/**
* 获取所有失败的任务
*/
@GetMapping("/failed-tasks")
public ResponseEntity<Map<String, Object>> getFailedTasks() {
Map<String, Object> response = new HashMap<>();
try {
List<TaskQueue> allTasks = taskQueueRepository.findAll();
List<TaskQueue> failedTasks = allTasks.stream()
.filter(t -> t.getStatus() == TaskQueue.QueueStatus.FAILED)
.collect(java.util.stream.Collectors.toList());
response.put("success", true);
response.put("failedTasks", failedTasks);
response.put("count", failedTasks.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "获取失败任务失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 重置任务状态(用于测试)
*/
@PostMapping("/reset-task/{taskId}")
public ResponseEntity<Map<String, Object>> resetTask(@PathVariable String taskId) {
Map<String, Object> response = new HashMap<>();
try {
Optional<TaskQueue> taskQueueOpt = taskQueueRepository.findByTaskId(taskId);
if (!taskQueueOpt.isPresent()) {
response.put("success", false);
response.put("message", "找不到任务: " + taskId);
return ResponseEntity.notFound().build();
}
TaskQueue taskQueue = taskQueueOpt.get();
taskQueue.updateStatus(TaskQueue.QueueStatus.PENDING);
taskQueue.setErrorMessage(null);
taskQueue.setCheckCount(0);
taskQueueRepository.save(taskQueue);
response.put("success", true);
response.put("message", "任务已重置为待处理状态");
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "重置任务失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
}

View File

@@ -0,0 +1,107 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.service.RealAIService;
@RestController
@RequestMapping("/api/prompt")
public class PromptOptimizerApiController {
private static final Logger logger = LoggerFactory.getLogger(PromptOptimizerApiController.class);
@Autowired
private RealAIService realAIService;
/**
* 优化提示词
*
* @param request 包含prompt和type的请求体
* @param authentication 用户认证信息
* @return 优化后的提示词
*/
@PostMapping("/optimize")
public ResponseEntity<?> optimizePrompt(
@RequestBody Map<String, Object> request,
Authentication authentication) {
try {
String username = (authentication != null) ? authentication.getName() : "anonymous";
logger.info("收到优化提示词请求,用户: {}", username);
// 从请求中提取参数
String prompt = (String) request.get("prompt");
String type = (String) request.getOrDefault("type", "text-to-video");
// 参数验证
if (prompt == null || prompt.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "提示词不能为空"));
}
// 长度验证(防止过长)
if (prompt.length() > 2000) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "提示词过长请控制在2000字符以内"));
}
// 验证type是否有效
if (!isValidType(type)) {
logger.warn("无效的优化类型: {}, 使用默认类型: text-to-video", type);
type = "text-to-video"; // 默认类型
}
// 调用优化服务
String optimizedPrompt = realAIService.optimizePrompt(prompt.trim(), type);
// 检查优化是否成功(如果返回原始提示词可能是失败)
boolean optimized = !optimizedPrompt.equals(prompt.trim());
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", optimized ? "提示词优化成功" : "提示词优化完成(可能使用了原始提示词)");
response.put("data", Map.of(
"originalPrompt", prompt,
"optimizedPrompt", optimizedPrompt,
"type", type,
"optimized", optimized
));
logger.info("提示词优化完成,用户: {}, 类型: {}, 原始长度: {}, 优化后长度: {}",
username, type, prompt.length(), optimizedPrompt.length());
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
logger.error("参数错误: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "参数错误: " + e.getMessage()));
} catch (Exception e) {
logger.error("优化提示词失败", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "优化提示词失败,请稍后重试"));
}
}
/**
* 验证类型是否有效
*/
private boolean isValidType(String type) {
return type != null && (
type.equals("text-to-video") ||
type.equals("image-to-video") ||
type.equals("storyboard")
);
}
}

View File

@@ -0,0 +1,97 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.MembershipLevel;
import com.example.demo.model.SystemSettings;
import com.example.demo.repository.MembershipLevelRepository;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.SystemSettingsService;
@RestController
@RequestMapping("/api/public")
public class PublicApiController {
private final UserRepository userRepository;
private final SystemSettingsService systemSettingsService;
private final MembershipLevelRepository membershipLevelRepository;
public PublicApiController(UserRepository userRepository, SystemSettingsService systemSettingsService,
MembershipLevelRepository membershipLevelRepository) {
this.userRepository = userRepository;
this.systemSettingsService = systemSettingsService;
this.membershipLevelRepository = membershipLevelRepository;
}
/**
* 获取公开的系统配置(套餐价格等)
* 无需登录即可访问
*/
@GetMapping("/config")
public Map<String, Object> getPublicConfig() {
Map<String, Object> config = new HashMap<>();
SystemSettings settings = systemSettingsService.getOrCreate();
// 从membership_levels表读取价格必须从数据库获取禁止硬编码
MembershipLevel standardLevel = membershipLevelRepository.findByName("standard")
.orElseThrow(() -> new IllegalStateException("数据库中缺少standard会员等级配置"));
MembershipLevel proLevel = membershipLevelRepository.findByName("professional")
.orElseThrow(() -> new IllegalStateException("数据库中缺少professional会员等级配置"));
Double standardPrice = standardLevel.getPrice();
int standardPoints = standardLevel.getPointsBonus();
Double proPrice = proLevel.getPrice();
int proPoints = proLevel.getPointsBonus();
// 套餐价格配置从membership_levels表读取保留两位小数
config.put("standardPriceCny", standardPrice);
config.put("proPriceCny", proPrice);
config.put("standardPoints", standardPoints);
config.put("proPoints", proPoints);
config.put("pointsPerGeneration", settings.getPointsPerGeneration());
// 支付渠道开关
config.put("enableAlipay", settings.getEnableAlipay());
config.put("enablePaypal", settings.getEnablePaypal());
// 视频生成模型
config.put("videoModel", settings.getVideoModel() != null ? settings.getVideoModel() : "grok-video-3");
// 返回所有会员等级列表
List<MembershipLevel> levels = membershipLevelRepository.findAll();
config.put("membershipLevels", levels.stream().map(level -> {
Map<String, Object> levelMap = new HashMap<>();
levelMap.put("id", level.getId());
levelMap.put("name", level.getName());
levelMap.put("displayName", level.getDisplayName());
levelMap.put("price", level.getPrice());
levelMap.put("pointsBonus", level.getPointsBonus());
levelMap.put("durationDays", level.getDurationDays());
levelMap.put("description", level.getDescription());
return levelMap;
}).collect(Collectors.toList()));
return config;
}
@GetMapping("/users/exists/username")
public Map<String, Boolean> existsUsername(@RequestParam("value") String username) {
return Map.of("exists", userRepository.existsByUsername(username));
}
@GetMapping("/users/exists/email")
public Map<String, Boolean> existsEmail(@RequestParam("value") String email) {
return Map.of("exists", userRepository.existsByEmail(email));
}
}

View File

@@ -0,0 +1,244 @@
package com.example.demo.controller;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.ImageToVideoTask;
import com.example.demo.model.TaskQueue;
import com.example.demo.repository.ImageToVideoTaskRepository;
import com.example.demo.repository.TaskQueueRepository;
/**
* 队列诊断控制器
* 用于检查任务队列状态和图片传输问题
*/
@RestController
@RequestMapping("/api/diagnostic")
public class QueueDiagnosticController {
@Autowired
private TaskQueueRepository taskQueueRepository;
@Autowired
private ImageToVideoTaskRepository imageToVideoTaskRepository;
/**
* 检查队列状态
*/
@GetMapping("/queue-status")
public ResponseEntity<Map<String, Object>> checkQueueStatus() {
Map<String, Object> response = new HashMap<>();
try {
List<TaskQueue> allTasks = taskQueueRepository.findAll();
long pendingCount = allTasks.stream().filter(t -> t.getStatus() == TaskQueue.QueueStatus.PENDING).count();
long processingCount = allTasks.stream().filter(t -> t.getStatus() == TaskQueue.QueueStatus.PROCESSING).count();
long completedCount = allTasks.stream().filter(t -> t.getStatus() == TaskQueue.QueueStatus.COMPLETED).count();
long failedCount = allTasks.stream().filter(t -> t.getStatus() == TaskQueue.QueueStatus.FAILED).count();
long timeoutCount = allTasks.stream().filter(t -> t.getStatus() == TaskQueue.QueueStatus.TIMEOUT).count();
response.put("success", true);
response.put("totalTasks", allTasks.size());
response.put("pending", pendingCount);
response.put("processing", processingCount);
response.put("completed", completedCount);
response.put("failed", failedCount);
response.put("timeout", timeoutCount);
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "检查队列状态失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 检查图片文件是否存在
*/
@GetMapping("/check-image/{taskId}")
public ResponseEntity<Map<String, Object>> checkImageFile(@PathVariable String taskId) {
Map<String, Object> response = new HashMap<>();
try {
Optional<ImageToVideoTask> taskOpt = imageToVideoTaskRepository.findByTaskId(taskId);
if (!taskOpt.isPresent()) {
response.put("success", false);
response.put("message", "找不到任务: " + taskId);
return ResponseEntity.notFound().build();
}
ImageToVideoTask task = taskOpt.get();
String firstFrameUrl = task.getFirstFrameUrl();
String lastFrameUrl = task.getLastFrameUrl();
Map<String, Object> imageInfo = new HashMap<>();
// 检查首帧图片
if (firstFrameUrl != null) {
Map<String, Object> firstFrameInfo = checkImageFileExists(firstFrameUrl);
imageInfo.put("firstFrame", firstFrameInfo);
}
// 检查尾帧图片
if (lastFrameUrl != null) {
Map<String, Object> lastFrameInfo = checkImageFileExists(lastFrameUrl);
imageInfo.put("lastFrame", lastFrameInfo);
}
response.put("success", true);
response.put("taskId", taskId);
response.put("imageInfo", imageInfo);
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "检查图片文件失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 检查单个图片文件
*/
private Map<String, Object> checkImageFileExists(String imageUrl) {
Map<String, Object> result = new HashMap<>();
result.put("url", imageUrl);
try {
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
result.put("type", "URL");
result.put("exists", "需要网络访问");
return result;
}
// 检查相对路径
Path imagePath = Paths.get(imageUrl);
if (Files.exists(imagePath)) {
result.put("type", "相对路径");
result.put("exists", true);
result.put("size", Files.size(imagePath));
result.put("readable", Files.isReadable(imagePath));
return result;
}
// 检查绝对路径
String currentDir = System.getProperty("user.dir");
Path absolutePath = Paths.get(currentDir, imageUrl);
if (Files.exists(absolutePath)) {
result.put("type", "绝对路径");
result.put("exists", true);
result.put("size", Files.size(absolutePath));
result.put("readable", Files.isReadable(absolutePath));
result.put("fullPath", absolutePath.toString());
return result;
}
// 检查备用路径
Path altPath = Paths.get("C:\\Users\\UI\\Desktop\\AIGC\\demo", imageUrl);
if (Files.exists(altPath)) {
result.put("type", "备用路径");
result.put("exists", true);
result.put("size", Files.size(altPath));
result.put("readable", Files.isReadable(altPath));
result.put("fullPath", altPath.toString());
return result;
}
result.put("exists", false);
result.put("error", "文件不存在于任何路径");
result.put("checkedPaths", new String[]{
imageUrl,
absolutePath.toString(),
altPath.toString()
});
} catch (Exception e) {
result.put("exists", false);
result.put("error", e.getMessage());
}
return result;
}
/**
* 获取失败任务的详细信息
*/
@GetMapping("/failed-tasks")
public ResponseEntity<Map<String, Object>> getFailedTasks() {
Map<String, Object> response = new HashMap<>();
try {
List<TaskQueue> allTasks = taskQueueRepository.findAll();
List<TaskQueue> failedTasks = allTasks.stream()
.filter(t -> t.getStatus() == TaskQueue.QueueStatus.FAILED)
.collect(java.util.stream.Collectors.toList());
response.put("success", true);
response.put("failedTasks", failedTasks);
response.put("count", failedTasks.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "获取失败任务失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 手动重试失败的任务
*/
@PostMapping("/retry-task/{taskId}")
public ResponseEntity<Map<String, Object>> retryTask(@PathVariable String taskId) {
Map<String, Object> response = new HashMap<>();
try {
Optional<TaskQueue> taskOpt = taskQueueRepository.findByTaskId(taskId);
if (!taskOpt.isPresent()) {
response.put("success", false);
response.put("message", "找不到任务: " + taskId);
return ResponseEntity.notFound().build();
}
TaskQueue task = taskOpt.get();
if (task.getStatus() != TaskQueue.QueueStatus.FAILED) {
response.put("success", false);
response.put("message", "任务状态不是失败状态: " + task.getStatus());
return ResponseEntity.badRequest().body(response);
}
// 重置任务状态
task.updateStatus(TaskQueue.QueueStatus.PENDING);
task.setErrorMessage(null);
taskQueueRepository.save(task);
response.put("success", true);
response.put("message", "任务已重置为待处理状态");
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "重试任务失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
}

View File

@@ -0,0 +1,296 @@
package com.example.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 腾讯云SES Webhook控制器
* 用于接收SES服务的推送数据
*/
@RestController
@RequestMapping("/api/email")
public class SesWebhookController {
private static final Logger logger = LoggerFactory.getLogger(SesWebhookController.class);
/**
* 处理邮件发送状态回调
* 当邮件发送成功或失败时SES会推送状态信息
*/
@PostMapping("/send-status")
public ResponseEntity<Map<String, Object>> handleSendStatus(@RequestBody Map<String, Object> data) {
logger.info("收到邮件发送状态回调: {}", data);
try {
// 解析SES推送的数据
String messageId = (String) data.get("MessageId");
String status = (String) data.get("Status");
String email = (String) data.get("Email");
String timestamp = (String) data.get("Timestamp");
logger.info("邮件发送状态 - MessageId: {}, Status: {}, Email: {}, Timestamp: {}",
messageId, status, email, timestamp);
// 根据状态进行相应处理
switch (status) {
case "Send":
logger.info("邮件发送成功: {}", email);
// 可以更新数据库中的发送状态
break;
case "Reject":
logger.warn("邮件发送被拒绝: {}", email);
// 处理发送被拒绝的情况
break;
case "Bounce":
logger.warn("邮件发送失败(退信): {}", email);
// 处理退信情况
break;
default:
logger.info("邮件发送状态: {} - {}", status, email);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "状态回调处理成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("处理邮件发送状态回调失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 处理邮件投递状态回调
* 当邮件被投递到收件人邮箱时SES会推送投递状态
*/
@PostMapping("/delivery-status")
public ResponseEntity<Map<String, Object>> handleDeliveryStatus(@RequestBody Map<String, Object> data) {
logger.info("收到邮件投递状态回调: {}", data);
try {
String messageId = (String) data.get("MessageId");
String status = (String) data.get("Status");
String email = (String) data.get("Email");
String timestamp = (String) data.get("Timestamp");
logger.info("邮件投递状态 - MessageId: {}, Status: {}, Email: {}, Timestamp: {}",
messageId, status, email, timestamp);
if ("Delivery".equals(status)) {
logger.info("邮件投递成功: {}", email);
// 可以更新数据库中的投递状态
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "投递状态回调处理成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("处理邮件投递状态回调失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 处理邮件退信回调
* 当邮件无法投递时SES会推送退信信息
*/
@PostMapping("/bounce")
public ResponseEntity<Map<String, Object>> handleBounce(@RequestBody Map<String, Object> data) {
logger.info("收到邮件退信回调: {}", data);
try {
String messageId = (String) data.get("MessageId");
String email = (String) data.get("Email");
String bounceType = (String) data.get("BounceType");
String bounceSubType = (String) data.get("BounceSubType");
String timestamp = (String) data.get("Timestamp");
logger.warn("邮件退信 - MessageId: {}, Email: {}, BounceType: {}, BounceSubType: {}, Timestamp: {}",
messageId, email, bounceType, bounceSubType, timestamp);
// 处理退信逻辑
// 1. 记录退信信息到数据库
// 2. 如果是硬退信,可以考虑从邮件列表中移除该邮箱
// 3. 如果是软退信,可以稍后重试
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "退信回调处理成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("处理邮件退信回调失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 处理邮件投诉回调
* 当收件人投诉邮件为垃圾邮件时SES会推送投诉信息
*/
@PostMapping("/complaint")
public ResponseEntity<Map<String, Object>> handleComplaint(@RequestBody Map<String, Object> data) {
logger.info("收到邮件投诉回调: {}", data);
try {
String messageId = (String) data.get("MessageId");
String email = (String) data.get("Email");
String complaintType = (String) data.get("ComplaintType");
String timestamp = (String) data.get("Timestamp");
logger.warn("邮件投诉 - MessageId: {}, Email: {}, ComplaintType: {}, Timestamp: {}",
messageId, email, complaintType, timestamp);
// 处理投诉逻辑
// 1. 记录投诉信息到数据库
// 2. 考虑从邮件列表中移除该邮箱
// 3. 检查邮件内容是否合规
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "投诉回调处理成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("处理邮件投诉回调失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 处理邮件打开事件回调
* 当收件人打开邮件时SES会推送打开事件
*/
@PostMapping("/open")
public ResponseEntity<Map<String, Object>> handleOpen(@RequestBody Map<String, Object> data) {
logger.info("收到邮件打开事件回调: {}", data);
try {
String messageId = (String) data.get("MessageId");
String email = (String) data.get("Email");
String timestamp = (String) data.get("Timestamp");
String userAgent = (String) data.get("UserAgent");
String ipAddress = (String) data.get("IpAddress");
logger.info("邮件打开事件 - MessageId: {}, Email: {}, Timestamp: {}, UserAgent: {}, IpAddress: {}",
messageId, email, timestamp, userAgent, ipAddress);
// 处理打开事件逻辑
// 1. 记录邮件打开统计
// 2. 更新用户活跃度
// 3. 分析用户行为
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "打开事件回调处理成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("处理邮件打开事件回调失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 处理邮件点击事件回调
* 当收件人点击邮件中的链接时SES会推送点击事件
*/
@PostMapping("/click")
public ResponseEntity<Map<String, Object>> handleClick(@RequestBody Map<String, Object> data) {
logger.info("收到邮件点击事件回调: {}", data);
try {
String messageId = (String) data.get("MessageId");
String email = (String) data.get("Email");
String timestamp = (String) data.get("Timestamp");
String link = (String) data.get("Link");
String userAgent = (String) data.get("UserAgent");
String ipAddress = (String) data.get("IpAddress");
logger.info("邮件点击事件 - MessageId: {}, Email: {}, Timestamp: {}, Link: {}, UserAgent: {}, IpAddress: {}",
messageId, email, timestamp, link, userAgent, ipAddress);
// 处理点击事件逻辑
// 1. 记录链接点击统计
// 2. 分析用户兴趣
// 3. 更新用户行为数据
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "点击事件回调处理成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("处理邮件点击事件回调失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 处理SES配置集事件回调
* 当配置集状态发生变化时SES会推送配置集事件
*/
@PostMapping("/configuration-set")
public ResponseEntity<Map<String, Object>> handleConfigurationSet(@RequestBody Map<String, Object> data) {
logger.info("收到SES配置集事件回调: {}", data);
try {
String eventType = (String) data.get("EventType");
String configurationSet = (String) data.get("ConfigurationSet");
String timestamp = (String) data.get("Timestamp");
logger.info("SES配置集事件 - EventType: {}, ConfigurationSet: {}, Timestamp: {}",
eventType, configurationSet, timestamp);
// 处理配置集事件逻辑
// 1. 更新配置集状态
// 2. 记录配置变更历史
// 3. 发送通知给管理员
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "配置集事件回调处理成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("处理SES配置集事件回调失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
}

View File

@@ -0,0 +1,46 @@
package com.example.demo.controller;
import com.example.demo.model.SystemSettings;
import com.example.demo.service.SystemSettingsService;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/settings")
public class SettingsController {
private final SystemSettingsService settingsService;
public SettingsController(SystemSettingsService settingsService) {
this.settingsService = settingsService;
}
@GetMapping
public String showForm(Model model) {
SystemSettings settings = settingsService.getOrCreate();
model.addAttribute("pageTitle", "系统设置");
model.addAttribute("settings", settings);
return "settings/form";
}
@PostMapping
public String save(@Valid @ModelAttribute("settings") SystemSettings form,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("pageTitle", "系统设置");
return "settings/form";
}
settingsService.update(form);
model.addAttribute("success", "保存成功");
return "redirect:/settings";
}
}

View File

@@ -0,0 +1,19 @@
package com.example.demo.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用于兼容前端直接请求 /api-management 的占位接口,
* 避免被 GlobalExceptionHandler 记录 404 日志。
*/
@RestController
public class SpaForwardController {
@GetMapping("/api-management")
public ResponseEntity<Void> apiManagementPlaceholder() {
// 不返回任何内容,仅表示后端该路径存在
return ResponseEntity.noContent().build(); // HTTP 204
}
}

View File

@@ -0,0 +1,409 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.StoryboardVideoTask;
import com.example.demo.service.StoryboardVideoService;
@RestController
@RequestMapping("/api/storyboard-video")
public class StoryboardVideoApiController {
private static final Logger logger = LoggerFactory.getLogger(StoryboardVideoApiController.class);
@Autowired
private StoryboardVideoService storyboardVideoService;
/**
* 创建分镜视频任务
*/
@PostMapping("/create")
public ResponseEntity<?> createTask(
@RequestBody Map<String, Object> request,
Authentication authentication) {
try {
// 检查用户是否已认证
if (authentication == null) {
logger.warn("创建分镜视频任务失败: 用户未登录");
return ResponseEntity.status(401)
.body(Map.of("success", false, "message", "用户未登录,请先登录"));
}
String username = authentication.getName();
logger.info("收到创建分镜视频任务请求,用户: {}", username);
// 从请求中提取参数
String prompt = (String) request.get("prompt");
String aspectRatio = (String) request.getOrDefault("aspectRatio", "3:2");
Boolean hdMode = (Boolean) request.getOrDefault("hdMode", false);
String imageUrl = (String) request.get("imageUrl");
String imageModel = (String) request.getOrDefault("imageModel", "nano-banana-2");
// 提取用户上传的多张图片(新增)
@SuppressWarnings("unchecked")
List<String> uploadedImages = (List<String>) request.get("uploadedImages");
// 提取duration参数支持多种类型
Integer duration = 10; // 默认10秒
Object durationObj = request.get("duration");
if (durationObj instanceof Number) {
duration = ((Number) durationObj).intValue();
} else if (durationObj instanceof String) {
try {
duration = Integer.parseInt((String) durationObj);
} catch (NumberFormatException e) {
logger.warn("无效的duration参数: {}, 使用默认值10", durationObj);
}
}
String videoModel = (String) request.getOrDefault("videoModel", "grok-video-3");
logger.info("任务参数 - duration: {}, aspectRatio: {}, hdMode: {}, imageModel: {}, videoModel: {}, uploadedImages: {}",
duration, aspectRatio, hdMode, imageModel, videoModel, uploadedImages != null ? uploadedImages.size() : 0);
if (prompt == null || prompt.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "提示词不能为空"));
}
// 创建任务(传递上传的图片列表)
StoryboardVideoTask task = storyboardVideoService.createTask(
username, prompt, aspectRatio, hdMode != null && hdMode, imageUrl, duration, imageModel, uploadedImages, videoModel
);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "任务创建成功");
response.put("data", Map.of(
"taskId", task.getTaskId(),
"status", task.getStatus(),
"progress", task.getProgress()
));
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
logger.error("参数错误: {}", e.getMessage(), e);
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
} catch (Exception e) {
logger.error("创建分镜视频任务失败", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "创建任务失败: " + e.getMessage()));
}
}
/**
* 直接使用上传的分镜图创建视频任务(跳过分镜图生成)
* 用户在STEP2上传分镜图后直接生成视频时调用
*/
@PostMapping("/create-video-direct")
public ResponseEntity<?> createVideoDirectly(
@RequestBody Map<String, Object> request,
Authentication authentication) {
try {
// 检查用户是否已认证
if (authentication == null) {
logger.warn("直接创建视频任务失败: 用户未登录");
return ResponseEntity.status(401)
.body(Map.of("success", false, "message", "用户未登录,请先登录"));
}
String username = authentication.getName();
logger.info("收到直接创建视频任务请求,用户: {}", username);
// 从请求中提取参数
String storyboardImage = (String) request.get("storyboardImage"); // 用户上传的分镜图
String prompt = (String) request.getOrDefault("prompt", "根据分镜图生成视频");
String aspectRatio = (String) request.getOrDefault("aspectRatio", "3:2");
Boolean hdMode = (Boolean) request.getOrDefault("hdMode", false);
// 提取视频参考图
@SuppressWarnings("unchecked")
List<String> referenceImages = (List<String>) request.get("referenceImages");
// 提取duration参数
Integer duration = 10;
Object durationObj = request.get("duration");
if (durationObj instanceof Number) {
duration = ((Number) durationObj).intValue();
} else if (durationObj instanceof String) {
try {
duration = Integer.parseInt((String) durationObj);
} catch (NumberFormatException e) {
logger.warn("无效的duration参数: {}, 使用默认值10", durationObj);
}
}
String videoModel = (String) request.getOrDefault("videoModel", "grok-video-3");
if (storyboardImage == null || storyboardImage.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "分镜图不能为空"));
}
logger.info("直接创建视频任务参数 - duration: {}, aspectRatio: {}, hdMode: {}, videoModel: {}, referenceImages: {}",
duration, aspectRatio, hdMode, videoModel, referenceImages != null ? referenceImages.size() : 0);
// 调用服务层方法直接创建视频任务
StoryboardVideoTask task = storyboardVideoService.createVideoDirectTask(
username, prompt, storyboardImage, aspectRatio, hdMode != null && hdMode, duration, referenceImages, videoModel
);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "视频任务创建成功");
response.put("data", Map.of(
"taskId", task.getTaskId(),
"status", task.getStatus(),
"progress", task.getProgress()
));
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
logger.error("参数错误: {}", e.getMessage(), e);
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
} catch (Exception e) {
logger.error("直接创建视频任务失败", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "创建任务失败: " + e.getMessage()));
}
}
/**
* 获取任务详情
*/
@GetMapping("/task/{taskId}")
public ResponseEntity<?> getTask(@PathVariable String taskId, Authentication authentication) {
try {
String username = authentication.getName();
logger.info("收到获取分镜视频任务详情请求任务ID: {}, 用户: {}", taskId, username);
StoryboardVideoTask task = storyboardVideoService.getTask(taskId);
// 验证用户权限
if (!task.getUsername().equals(username)) {
logger.warn("用户 {} 尝试访问任务 {},但任务属于用户 {}", username, taskId, task.getUsername());
return ResponseEntity.status(403)
.body(Map.of("success", false, "message", "无权访问此任务"));
}
// 状态同步已通过数据库触发器实现,无需代码检查
Map<String, Object> taskData = new HashMap<>();
taskData.put("taskId", task.getTaskId());
taskData.put("status", task.getStatus());
taskData.put("progress", task.getProgress());
taskData.put("resultUrl", task.getResultUrl());
taskData.put("videoUrls", task.getVideoUrls()); // 视频URLJSON数组
taskData.put("imageUrl", task.getImageUrl()); // 参考图片(旧字段)
taskData.put("uploadedImages", task.getUploadedImages()); // 用户上传的参考图JSON数组
taskData.put("prompt", task.getPrompt());
taskData.put("aspectRatio", task.getAspectRatio());
taskData.put("hdMode", task.isHdMode());
taskData.put("duration", task.getDuration());
taskData.put("errorMessage", task.getErrorMessage());
taskData.put("createdAt", task.getCreatedAt());
taskData.put("updatedAt", task.getUpdatedAt());
taskData.put("completedAt", task.getCompletedAt());
// 大模型优化后的提示词字段
taskData.put("shotList", task.getShotList());
taskData.put("imagePrompt", task.getImagePrompt());
taskData.put("videoPrompt", task.getVideoPrompt());
return ResponseEntity.ok(Map.of(
"success", true,
"data", taskData
));
} catch (RuntimeException e) {
logger.error("获取任务详情失败: {}", e.getMessage());
return ResponseEntity.status(404)
.body(Map.of("success", false, "message", "任务不存在"));
} catch (Exception e) {
logger.error("获取任务详情异常", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "查询失败"));
}
}
/**
* 获取用户任务列表
*/
@GetMapping("/tasks")
public ResponseEntity<?> getUserTasks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Authentication authentication) {
try {
String username = authentication.getName();
List<StoryboardVideoTask> tasks = storyboardVideoService.getUserTasks(username, page, size);
return ResponseEntity.ok(Map.of(
"success", true,
"data", tasks,
"page", page,
"size", size
));
} catch (Exception e) {
logger.error("获取用户任务列表失败", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "查询失败"));
}
}
/**
* 开始生成视频(从分镜图生成视频)
* 用户点击"开始生成"按钮后调用
*/
@PostMapping("/task/{taskId}/start-video")
public ResponseEntity<?> startVideoGeneration(
@PathVariable String taskId,
@RequestBody(required = false) Map<String, Object> requestBody,
Authentication authentication) {
try {
String username = authentication.getName();
logger.info("收到开始生成视频请求任务ID: {}, 用户: {}", taskId, username);
// 验证任务是否存在且属于该用户
StoryboardVideoTask task = storyboardVideoService.getTask(taskId);
if (!task.getUsername().equals(username)) {
logger.warn("用户 {} 尝试访问任务 {},但任务属于用户 {}", username, taskId, task.getUsername());
return ResponseEntity.status(403)
.body(Map.of("success", false, "message", "无权访问此任务"));
}
// 从请求体中提取参数(如果有)
Integer duration = null;
String aspectRatio = null;
Boolean hdMode = null;
java.util.List<String> referenceImages = null;
String storyboardImage = null; // 前端传递的分镜图URL
if (requestBody != null) {
if (requestBody.containsKey("duration")) {
Object durationObj = requestBody.get("duration");
if (durationObj instanceof Number) {
duration = ((Number) durationObj).intValue();
} else if (durationObj instanceof String) {
duration = Integer.parseInt((String) durationObj);
}
logger.info("视频时长参数: {}", duration);
}
if (requestBody.containsKey("aspectRatio")) {
aspectRatio = (String) requestBody.get("aspectRatio");
logger.info("视频宽高比参数: {}", aspectRatio);
}
if (requestBody.containsKey("hdMode")) {
hdMode = (Boolean) requestBody.get("hdMode");
logger.info("高清模式参数: {}", hdMode);
}
if (requestBody.containsKey("referenceImages")) {
Object refImagesObj = requestBody.get("referenceImages");
if (refImagesObj instanceof java.util.List) {
referenceImages = (java.util.List<String>) refImagesObj;
logger.info("参考图数量: {}", referenceImages.size());
}
}
if (requestBody.containsKey("storyboardImage")) {
storyboardImage = (String) requestBody.get("storyboardImage");
logger.info("前端传递的分镜图URL: {}", storyboardImage != null ? storyboardImage.substring(0, Math.min(80, storyboardImage.length())) + "..." : "null");
} else {
logger.warn("请求体中没有storyboardImage参数requestBody keys: {}", requestBody.keySet());
}
} else {
logger.warn("请求体为空");
}
String videoModel = requestBody != null ? (String) requestBody.getOrDefault("videoModel", "grok-video-3") : "grok-video-3";
logger.info("调用startVideoGeneration: taskId={}, videoModel={}, storyboardImage={}", taskId, videoModel,
storyboardImage != null ? "有值(长度:" + storyboardImage.length() + ")" : "null");
// 开始生成视频传递参数包括参考图和分镜图URL
String finalTaskId = storyboardVideoService.startVideoGeneration(taskId, duration, aspectRatio, hdMode, referenceImages, storyboardImage, videoModel);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "视频生成任务已启动",
"data", Map.of("taskId", finalTaskId)
));
} catch (RuntimeException e) {
logger.error("开始生成视频失败: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
} catch (Exception e) {
logger.error("开始生成视频异常", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "启动视频生成失败"));
}
}
/**
* 重试失败的分镜视频任务
*/
@PostMapping("/task/{taskId}/retry")
public ResponseEntity<?> retryTask(
@PathVariable String taskId,
Authentication authentication) {
logger.info("收到重试任务请求任务ID: {}", taskId);
try {
// 检查用户是否已认证
if (authentication == null) {
return ResponseEntity.status(401)
.body(Map.of("success", false, "message", "请先登录"));
}
String username = authentication.getName();
// 验证任务是否存在且属于该用户
StoryboardVideoTask task = storyboardVideoService.getTask(taskId);
if (!task.getUsername().equals(username)) {
logger.warn("用户 {} 尝试重试任务 {},但任务属于用户 {}", username, taskId, task.getUsername());
return ResponseEntity.status(403)
.body(Map.of("success", false, "message", "无权操作此任务"));
}
// 验证任务状态必须是 FAILED
if (task.getStatus() != StoryboardVideoTask.TaskStatus.FAILED) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "只能重试失败的任务"));
}
// 调用重试服务
storyboardVideoService.retryTask(taskId);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "重试任务已提交",
"data", Map.of("taskId", taskId)
));
} catch (RuntimeException e) {
logger.error("重试任务失败: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
} catch (Exception e) {
logger.error("重试任务异常", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "重试任务失败"));
}
}
}

View File

@@ -0,0 +1,340 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.TaskQueue;
import com.example.demo.service.TaskQueueService;
import com.example.demo.util.JwtUtils;
/**
* 任务队列API控制器
*/
@RestController
@RequestMapping("/api/task-queue")
public class TaskQueueApiController {
private static final Logger logger = LoggerFactory.getLogger(TaskQueueApiController.class);
@Autowired
private TaskQueueService taskQueueService;
@Autowired
private JwtUtils jwtUtils;
/**
* 获取用户的任务队列(仅待处理任务)
*/
@GetMapping("/user-tasks")
public ResponseEntity<Map<String, Object>> getUserTaskQueue(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
List<TaskQueue> taskQueue = taskQueueService.getUserTaskQueue(username);
long totalCount = taskQueueService.getUserTaskCount(username);
response.put("success", true);
response.put("data", taskQueue);
response.put("total", totalCount);
response.put("maxTasks", 3); // 每个用户最多3个任务
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);
}
}
/**
* 分页获取用户的所有任务队列(包括所有状态)
*/
@GetMapping("/all-tasks")
public ResponseEntity<Map<String, Object>> getUserAllTasks(
@RequestHeader("Authorization") String token,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
Page<TaskQueue> taskPage = taskQueueService.getUserAllTasks(username, page, size);
response.put("success", true);
response.put("data", taskPage.getContent());
response.put("total", taskPage.getTotalElements());
response.put("totalPages", taskPage.getTotalPages());
response.put("currentPage", page);
response.put("pageSize", size);
response.put("hasNext", taskPage.hasNext());
response.put("hasPrevious", taskPage.hasPrevious());
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);
}
}
/**
* 取消队列中的任务
*/
@PostMapping("/cancel/{taskId}")
public ResponseEntity<Map<String, Object>> cancelTask(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
boolean cancelled = taskQueueService.cancelTask(taskId, username);
if (cancelled) {
response.put("success", true);
response.put("message", "任务已取消");
} else {
response.put("success", false);
response.put("message", "任务取消失败或任务不存在/无权限");
}
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);
}
}
/**
* 获取任务队列统计信息
*/
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> getQueueStats(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
List<TaskQueue> taskQueue = taskQueueService.getUserTaskQueue(username);
long totalCount = taskQueueService.getUserTaskCount(username);
// 统计各状态的任务数量
long pendingCount = taskQueue.stream()
.filter(tq -> tq.getStatus() == TaskQueue.QueueStatus.PENDING)
.count();
long processingCount = taskQueue.stream()
.filter(tq -> tq.getStatus() == TaskQueue.QueueStatus.PROCESSING)
.count();
long completedCount = taskQueue.stream()
.filter(tq -> tq.getStatus() == TaskQueue.QueueStatus.COMPLETED)
.count();
long failedCount = taskQueue.stream()
.filter(tq -> tq.getStatus() == TaskQueue.QueueStatus.FAILED)
.count();
Map<String, Object> stats = new HashMap<>();
stats.put("total", totalCount);
stats.put("pending", pendingCount);
stats.put("processing", processingCount);
stats.put("completed", completedCount);
stats.put("failed", failedCount);
stats.put("maxTasks", 3);
response.put("success", true);
response.put("data", stats);
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);
}
}
/**
* 手动触发任务处理(管理员功能)
*/
@PostMapping("/process-pending")
public ResponseEntity<Map<String, Object>> processPendingTasks(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 这里可以添加管理员权限检查
// 暂时允许所有用户触发
taskQueueService.processPendingTasks();
response.put("success", true);
response.put("message", "待处理任务处理完成");
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);
}
}
/**
* 手动触发状态检查(管理员功能)
*/
@PostMapping("/check-statuses")
public ResponseEntity<Map<String, Object>> checkTaskStatuses(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 这里可以添加管理员权限检查
// 暂时允许所有用户触发
taskQueueService.checkTaskStatuses();
response.put("success", true);
response.put("message", "任务状态检查完成");
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);
}
}
/**
* 从Token中提取用户名
*/
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
// 提取实际的token
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
return null;
}
// 验证token并获取用户名
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username != null && !jwtUtils.isTokenExpired(actualToken)) {
return username;
}
return null;
} catch (Exception e) {
logger.error("解析token失败", e);
return null;
}
}
/**
* 检查任务是否在队列中(用于前端决定是否需要轮询)
*/
@GetMapping("/check/{taskId}")
public ResponseEntity<Map<String, Object>> checkTaskInQueue(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 检查任务是否在队列中
boolean inQueue = taskQueueService.isTaskInQueue(taskId);
response.put("success", true);
response.put("inQueue", inQueue);
response.put("taskId", taskId);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("检查任务队列状态失败", e);
response.put("success", false);
response.put("message", "检查失败:" + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
}

View File

@@ -0,0 +1,360 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
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;
@RestController
@RequestMapping("/api/task-status")
@CrossOrigin(origins = "http://localhost:5173")
public class TaskStatusApiController {
private static final Logger logger = LoggerFactory.getLogger(TaskStatusApiController.class);
@Autowired
private TaskStatusPollingService taskStatusPollingService;
@Autowired
private TaskStatusRepository taskStatusRepository;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserRepository userRepository;
@Autowired
private com.example.demo.repository.StoryboardVideoTaskRepository storyboardVideoTaskRepository;
/**
* 获取任务状态
*/
@GetMapping("/{taskId}")
public ResponseEntity<Map<String, Object>> getTaskStatus(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
try {
// 从token中提取用户名这里简化处理实际应该解析JWT
String username = extractUsernameFromToken(token);
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
if (taskStatus == null) {
return ResponseEntity.notFound().build();
}
// 检查权限
if (!taskStatus.getUsername().equals(username)) {
return ResponseEntity.status(403).body(Map.of("error", "无权访问此任务"));
}
Map<String, Object> response = new HashMap<>();
response.put("taskId", taskStatus.getTaskId());
response.put("status", taskStatus.getStatus().name());
response.put("statusDescription", taskStatus.getStatus().getDescription());
response.put("progress", taskStatus.getProgress());
response.put("resultUrl", taskStatus.getResultUrl());
response.put("errorMessage", taskStatus.getErrorMessage());
response.put("createdAt", taskStatus.getCreatedAt());
response.put("updatedAt", taskStatus.getUpdatedAt());
response.put("completedAt", taskStatus.getCompletedAt());
response.put("pollCount", taskStatus.getPollCount());
response.put("maxPolls", taskStatus.getMaxPolls());
// 如果是分镜视频任务,额外返回 videoPrompt、imagePrompt、shotList
if (taskId.startsWith("sb_")) {
try {
storyboardVideoTaskRepository.findByTaskId(taskId).ifPresent(storyboardTask -> {
response.put("videoPrompt", storyboardTask.getVideoPrompt());
response.put("imagePrompt", storyboardTask.getImagePrompt());
response.put("shotList", storyboardTask.getShotList());
});
} catch (Exception e) {
logger.warn("获取分镜视频任务额外信息失败: taskId={}, error={}", taskId, e.getMessage());
}
}
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", "获取任务状态失败: " + e.getMessage());
return ResponseEntity.status(500).body(errorResponse);
}
}
/**
* 获取用户的所有任务状态
*/
@GetMapping("/user/{username}")
public ResponseEntity<List<TaskStatus>> getUserTaskStatuses(
@PathVariable String username,
@RequestHeader("Authorization") String token) {
try {
// 验证token中的用户名
String tokenUsername = extractUsernameFromToken(token);
if (!tokenUsername.equals(username)) {
return ResponseEntity.status(403).build();
}
List<TaskStatus> taskStatuses = taskStatusPollingService.getUserTaskStatuses(username);
return ResponseEntity.ok(taskStatuses);
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
/**
* 取消任务
*/
@PostMapping("/{taskId}/cancel")
public ResponseEntity<Map<String, Object>> cancelTask(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
try {
String username = extractUsernameFromToken(token);
boolean cancelled = taskStatusPollingService.cancelTask(taskId, username);
Map<String, Object> response = new HashMap<>();
if (cancelled) {
response.put("success", true);
response.put("message", "任务已取消");
} else {
response.put("success", false);
response.put("message", "任务取消失败,可能任务已完成或不存在");
}
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", "取消任务失败: " + e.getMessage());
return ResponseEntity.status(500).body(errorResponse);
}
}
/**
* 手动触发轮询(管理员功能)
*/
@PostMapping("/poll")
public ResponseEntity<Map<String, Object>> triggerPolling(
@RequestHeader("Authorization") String token) {
try {
// 验证token但不使用用户名管理员接口
extractUsernameFromToken(token);
// 这里可以添加管理员权限检查
// if (!isAdmin(username)) {
// return ResponseEntity.status(403).body(Map.of("error", "权限不足"));
// }
taskStatusPollingService.pollTaskStatuses();
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "轮询已触发");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", "触发轮询失败: " + e.getMessage());
return ResponseEntity.status(500).body(errorResponse);
}
}
/**
* 获取所有任务记录(管理员功能)
*/
@GetMapping("/admin/all")
public ResponseEntity<Map<String, Object>> getAllTaskRecords(
@RequestHeader("Authorization") String token,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) String taskType,
@RequestParam(required = false) String search) {
Map<String, Object> response = new HashMap<>();
try {
// 验证token
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "未授权访问");
return ResponseEntity.status(401).body(response);
}
// TODO: 添加管理员权限检查
// if (!isAdmin(username)) {
// response.put("success", false);
// response.put("message", "权限不足");
// return ResponseEntity.status(403).body(response);
// }
// 输入验证
if (page < 0) page = 0;
if (size <= 0 || size > 100) size = 10;
// 创建分页对象
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<TaskStatus> taskPage;
// 根据条件查询
if (status != null && !status.isEmpty() && !"all".equals(status)) {
try {
TaskStatus.Status statusEnum = TaskStatus.Status.valueOf(status.toUpperCase());
taskPage = taskStatusRepository.findByStatus(statusEnum, pageable);
} catch (IllegalArgumentException e) {
logger.warn("无效的任务状态: {}", status);
taskPage = taskStatusRepository.findAllWithPagination(pageable);
}
} else if (taskType != null && !taskType.isEmpty()) {
try {
TaskStatus.TaskType taskTypeEnum = TaskStatus.TaskType.valueOf(taskType.toUpperCase());
taskPage = taskStatusRepository.findByTaskType(taskTypeEnum, pageable);
} catch (IllegalArgumentException e) {
logger.warn("无效的任务类型: {}", taskType);
taskPage = taskStatusRepository.findAllWithPagination(pageable);
}
} else {
taskPage = taskStatusRepository.findAllWithPagination(pageable);
}
// 构建响应数据
List<Map<String, Object>> taskRecords = taskPage.getContent().stream()
.map(task -> {
Map<String, Object> record = new HashMap<>();
record.put("id", task.getId());
record.put("taskId", task.getTaskId());
// 通过存储的用户名查询真实的系统用户名
// 先尝试按 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());
record.put("statusText", task.getStatus().getDescription());
record.put("progress", task.getProgress());
record.put("resultUrl", task.getResultUrl());
record.put("errorMessage", task.getErrorMessage());
record.put("createdAt", task.getCreatedAt());
record.put("updatedAt", task.getUpdatedAt());
record.put("completedAt", task.getCompletedAt());
// 计算消耗资源(根据任务类型)- 统一30积分
String resources = "0积分";
if (task.getTaskType() != null) {
switch (task.getTaskType()) {
case TEXT_TO_VIDEO:
resources = "30积分";
break;
case IMAGE_TO_VIDEO:
resources = "30积分";
break;
case STORYBOARD_VIDEO:
resources = "30积分";
break;
case STORYBOARD_IMAGE:
resources = "30积分";
break;
}
}
record.put("resources", resources);
return record;
})
.toList();
response.put("success", true);
response.put("data", taskRecords);
response.put("totalElements", taskPage.getTotalElements());
response.put("totalPages", taskPage.getTotalPages());
response.put("currentPage", page);
response.put("size", size);
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);
}
}
/**
* 从token中提取用户名
*/
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
// 提取实际的token
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
return null;
}
// 验证token并获取用户名
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username != null && !jwtUtils.isTokenExpired(actualToken)) {
return username;
}
return null;
} catch (Exception e) {
logger.error("解析token失败", e);
return null;
}
}
}

View File

@@ -0,0 +1,302 @@
package com.example.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 腾讯云SES邮件推送回调控制器
* 用于接收腾讯云SES服务的邮件事件推送
*
* 支持的事件类型:
* - 递送成功 (delivery)
* - 腾讯云拒信 (reject)
* - ESP退信 (bounce)
* - 用户打开邮件 (open)
* - 点击链接 (click)
* - 退订 (unsubscribe)
*/
@RestController
@RequestMapping("/api/tencent/ses")
public class TencentSesWebhookController {
private static final Logger logger = LoggerFactory.getLogger(TencentSesWebhookController.class);
/**
* 腾讯云SES邮件事件回调接口
*
* 回调地址配置:
* - 账户级回调https://your-domain.com/api/tencent/ses/webhook
* - 发信地址级回调https://your-domain.com/api/tencent/ses/webhook
*
* 支持端口8080, 8081, 8082
*/
@PostMapping("/webhook")
public ResponseEntity<Map<String, Object>> handleSesWebhook(
@RequestBody Map<String, Object> payload,
@RequestHeader Map<String, String> headers) {
logger.info("收到腾讯云SES回调: {}", payload);
logger.info("请求头: {}", headers);
try {
// 验证签名(可选,建议在生产环境中启用)
if (!verifySignature(payload, headers)) {
logger.warn("签名验证失败");
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "签名验证失败");
return ResponseEntity.badRequest().body(response);
}
// 解析回调数据
String eventType = (String) payload.get("eventType");
String messageId = (String) payload.get("messageId");
String email = (String) payload.get("email");
String timestamp = (String) payload.get("timestamp");
logger.info("SES事件 - Type: {}, MessageId: {}, Email: {}, Timestamp: {}",
eventType, messageId, email, timestamp);
// 根据事件类型处理
switch (eventType) {
case "delivery":
handleDeliveryEvent(payload);
break;
case "reject":
handleRejectEvent(payload);
break;
case "bounce":
handleBounceEvent(payload);
break;
case "open":
handleOpenEvent(payload);
break;
case "click":
handleClickEvent(payload);
break;
case "unsubscribe":
handleUnsubscribeEvent(payload);
break;
default:
logger.warn("未知事件类型: {}", eventType);
handleUnknownEvent(payload);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "回调处理成功");
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("处理SES回调失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 处理邮件递送成功事件
*/
private void handleDeliveryEvent(Map<String, Object> payload) {
String messageId = (String) payload.get("messageId");
String email = (String) payload.get("email");
String timestamp = (String) payload.get("timestamp");
logger.info("邮件递送成功 - MessageId: {}, Email: {}, Timestamp: {}",
messageId, email, timestamp);
// 业务处理逻辑
// 1. 更新数据库中的邮件状态
// 2. 记录递送统计
// 3. 更新用户活跃度
// 4. 发送递送成功通知(如需要)
// TODO: 实现具体的业务逻辑
updateEmailDeliveryStatus(messageId, email, "delivered");
}
/**
* 处理腾讯云拒信事件
*/
private void handleRejectEvent(Map<String, Object> payload) {
String messageId = (String) payload.get("messageId");
String email = (String) payload.get("email");
String reason = (String) payload.get("reason");
String timestamp = (String) payload.get("timestamp");
logger.warn("腾讯云拒信 - MessageId: {}, Email: {}, Reason: {}, Timestamp: {}",
messageId, email, reason, timestamp);
// 业务处理逻辑
// 1. 记录拒信原因
// 2. 检查邮件内容合规性
// 3. 更新发送策略
// 4. 通知管理员
updateEmailDeliveryStatus(messageId, email, "rejected");
}
/**
* 处理ESP退信事件
*/
private void handleBounceEvent(Map<String, Object> payload) {
String messageId = (String) payload.get("messageId");
String email = (String) payload.get("email");
String bounceType = (String) payload.get("bounceType");
String bounceSubType = (String) payload.get("bounceSubType");
String timestamp = (String) payload.get("timestamp");
logger.warn("邮件退信 - MessageId: {}, Email: {}, BounceType: {}, BounceSubType: {}, Timestamp: {}",
messageId, email, bounceType, bounceSubType, timestamp);
// 业务处理逻辑
// 1. 区分硬退信和软退信
// 2. 硬退信:从邮件列表移除
// 3. 软退信:稍后重试
// 4. 更新邮箱有效性状态
if ("Permanent".equals(bounceType)) {
// 硬退信,移除邮箱
removeInvalidEmail(email);
} else {
// 软退信,标记重试
markEmailForRetry(messageId, email);
}
updateEmailDeliveryStatus(messageId, email, "bounced");
}
/**
* 处理邮件打开事件
*/
private void handleOpenEvent(Map<String, Object> payload) {
String messageId = (String) payload.get("messageId");
String email = (String) payload.get("email");
String timestamp = (String) payload.get("timestamp");
String userAgent = (String) payload.get("userAgent");
String ipAddress = (String) payload.get("ipAddress");
logger.info("邮件打开事件 - MessageId: {}, Email: {}, Timestamp: {}, UserAgent: {}, IpAddress: {}",
messageId, email, timestamp, userAgent, ipAddress);
// 业务处理逻辑
// 1. 记录邮件打开统计
// 2. 更新用户活跃度
// 3. 分析用户行为
// 4. 触发后续营销活动
recordEmailOpen(messageId, email, timestamp, userAgent, ipAddress);
}
/**
* 处理链接点击事件
*/
private void handleClickEvent(Map<String, Object> payload) {
String messageId = (String) payload.get("messageId");
String email = (String) payload.get("email");
String timestamp = (String) payload.get("timestamp");
String link = (String) payload.get("link");
String userAgent = (String) payload.get("userAgent");
String ipAddress = (String) payload.get("ipAddress");
logger.info("链接点击事件 - MessageId: {}, Email: {}, Timestamp: {}, Link: {}, UserAgent: {}, IpAddress: {}",
messageId, email, timestamp, link, userAgent, ipAddress);
// 业务处理逻辑
// 1. 记录链接点击统计
// 2. 分析用户兴趣
// 3. 更新用户行为数据
// 4. 触发转化跟踪
recordLinkClick(messageId, email, timestamp, link, userAgent, ipAddress);
}
/**
* 处理退订事件
*/
private void handleUnsubscribeEvent(Map<String, Object> payload) {
String messageId = (String) payload.get("messageId");
String email = (String) payload.get("email");
String timestamp = (String) payload.get("timestamp");
String unsubscribeType = (String) payload.get("unsubscribeType");
logger.info("用户退订 - MessageId: {}, Email: {}, Timestamp: {}, UnsubscribeType: {}",
messageId, email, timestamp, unsubscribeType);
// 业务处理逻辑
// 1. 立即停止向该邮箱发送邮件
// 2. 更新用户订阅状态
// 3. 记录退订原因
// 4. 发送退订确认邮件
unsubscribeUser(email, unsubscribeType);
}
/**
* 处理未知事件类型
*/
private void handleUnknownEvent(Map<String, Object> payload) {
logger.warn("收到未知事件类型: {}", payload);
// 记录到数据库或日志文件,供后续分析
}
/**
* 验证签名(简化版本)
* 生产环境中应实现完整的签名验证逻辑
*/
private boolean verifySignature(Map<String, Object> payload, Map<String, String> headers) {
// TODO: 实现腾讯云SES签名验证
// 1. 获取签名相关头部
// 2. 验证时间戳
// 3. 验证签名算法
// 4. 验证Token
String token = headers.get("X-Tencent-Token");
String timestamp = headers.get("X-Tencent-Timestamp");
String signature = headers.get("X-Tencent-Signature");
// 简化验证:检查必要头部是否存在
return token != null && timestamp != null && signature != null;
}
// ========== 业务方法实现 ==========
private void updateEmailDeliveryStatus(String messageId, String email, String status) {
// TODO: 更新数据库中的邮件状态
logger.info("更新邮件状态 - MessageId: {}, Email: {}, Status: {}", messageId, email, status);
}
private void removeInvalidEmail(String email) {
// TODO: 从邮件列表中移除无效邮箱
logger.info("移除无效邮箱: {}", email);
}
private void markEmailForRetry(String messageId, String email) {
// TODO: 标记邮件重试
logger.info("标记邮件重试 - MessageId: {}, Email: {}", messageId, email);
}
private void recordEmailOpen(String messageId, String email, String timestamp, String userAgent, String ipAddress) {
// TODO: 记录邮件打开统计
logger.info("记录邮件打开 - MessageId: {}, Email: {}, Timestamp: {}", messageId, email, timestamp);
}
private void recordLinkClick(String messageId, String email, String timestamp, String link, String userAgent, String ipAddress) {
// TODO: 记录链接点击统计
logger.info("记录链接点击 - MessageId: {}, Email: {}, Link: {}", messageId, email, link);
}
private void unsubscribeUser(String email, String unsubscribeType) {
// TODO: 处理用户退订
logger.info("处理用户退订 - Email: {}, Type: {}", email, unsubscribeType);
}
}

View File

@@ -0,0 +1,329 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.TextToVideoTask;
import com.example.demo.service.TextToVideoService;
import com.example.demo.util.JwtUtils;
/**
* 文生视频API控制器
*/
@RestController
@RequestMapping("/api/text-to-video")
public class TextToVideoApiController {
private static final Logger logger = LoggerFactory.getLogger(TextToVideoApiController.class);
@Autowired
private TextToVideoService textToVideoService;
@Autowired
private JwtUtils jwtUtils;
/**
* 创建文生视频任务
*/
@PostMapping("/create")
public ResponseEntity<Map<String, Object>> createTask(
@RequestBody Map<String, Object> request,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证用户身份
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 获取请求参数
String prompt = (String) request.get("prompt");
String aspectRatio = (String) request.getOrDefault("aspectRatio", "3:2");
// 安全的类型转换
Integer duration = 10; // 默认值Grok支持5/10
try {
Object durationObj = request.getOrDefault("duration", 10);
if (durationObj instanceof Integer) {
duration = (Integer) durationObj;
} else if (durationObj instanceof String) {
duration = Integer.parseInt((String) durationObj);
}
} catch (NumberFormatException e) {
duration = 5; // 使用默认值
}
Boolean hdMode = false; // 默认值
try {
Object hdModeObj = request.getOrDefault("hdMode", false);
if (hdModeObj instanceof Boolean) {
hdMode = (Boolean) hdModeObj;
} else if (hdModeObj instanceof String) {
hdMode = Boolean.parseBoolean((String) hdModeObj);
}
} catch (Exception e) {
hdMode = false; // 使用默认值
}
String videoModel = (String) request.getOrDefault("videoModel", "grok-video-3");
// 验证参数
if (prompt == null || prompt.trim().isEmpty()) {
response.put("success", false);
response.put("message", "文本描述不能为空");
return ResponseEntity.badRequest().body(response);
}
if (duration != 5 && duration != 10 && duration != 15) {
response.put("success", false);
response.put("message", "视频时长必须为5秒、10秒或15秒");
return ResponseEntity.badRequest().body(response);
}
if (!isValidAspectRatio(aspectRatio)) {
response.put("success", false);
response.put("message", "不支持的视频比例");
return ResponseEntity.badRequest().body(response);
}
// 创建任务
TextToVideoTask task = textToVideoService.createTask(
username, prompt.trim(), aspectRatio, duration, hdMode, videoModel
);
response.put("success", true);
response.put("message", "文生视频任务创建成功");
response.put("data", task);
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);
}
}
/**
* 获取用户的所有文生视频任务
*/
@GetMapping("/tasks")
public ResponseEntity<Map<String, Object>> getTasks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
List<TextToVideoTask> tasks = textToVideoService.getUserTasks(username, page, size);
long totalCount = textToVideoService.getUserTaskCount(username);
response.put("success", true);
response.put("data", tasks);
response.put("total", totalCount);
response.put("page", page);
response.put("size", size);
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);
}
}
/**
* 获取单个文生视频任务详情
*/
@GetMapping("/tasks/{taskId}")
public ResponseEntity<Map<String, Object>> getTaskDetail(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
TextToVideoTask task = textToVideoService.getTaskByIdAndUsername(taskId, username);
if (task == null) {
response.put("success", false);
response.put("message", "任务不存在或无权限访问");
return ResponseEntity.status(404).body(response);
}
response.put("success", true);
response.put("data", task);
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);
}
}
/**
* 获取文生视频任务状态
*/
@GetMapping("/tasks/{taskId}/status")
public ResponseEntity<Map<String, Object>> getTaskStatus(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
TextToVideoTask task = textToVideoService.getTaskByIdAndUsername(taskId, username);
if (task == null) {
response.put("success", false);
response.put("message", "任务不存在或无权限访问");
return ResponseEntity.status(404).body(response);
}
// 状态同步已通过数据库触发器实现,无需代码检查
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", task.getTaskId());
statusData.put("status", task.getStatus());
statusData.put("progress", task.getProgress());
statusData.put("resultUrl", task.getResultUrl());
statusData.put("errorMessage", task.getErrorMessage());
response.put("success", true);
response.put("data", statusData);
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);
}
}
/**
* 重试失败的文生视频任务
* 复用原task_id重新提交至外部API
*/
@PostMapping("/tasks/{taskId}/retry")
public ResponseEntity<Map<String, Object>> retryTask(
@PathVariable String taskId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
// 验证用户身份
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录或token无效");
return ResponseEntity.status(401).body(response);
}
logger.info("收到重试任务请求: taskId={}, username={}", taskId, username);
// 调用重试服务
TextToVideoTask task = textToVideoService.retryTask(taskId, username);
response.put("success", true);
response.put("message", "重试任务已提交");
response.put("data", task);
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
logger.error("重试任务失败: taskId={}, error={}", taskId, e.getMessage());
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
} catch (Exception e) {
logger.error("重试任务异常: taskId={}", taskId, e);
response.put("success", false);
response.put("message", "重试任务失败");
return ResponseEntity.internalServerError().body(response);
}
}
/**
* 从Token中提取用户名
*/
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
// 提取实际的token
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
return null;
}
// 验证token并获取用户名
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username != null && !jwtUtils.isTokenExpired(actualToken)) {
return username;
}
return null;
} catch (Exception e) {
logger.error("解析token失败", e);
return null;
}
}
/**
* 验证视频比例
*/
private boolean isValidAspectRatio(String aspectRatio) {
if (aspectRatio == null || aspectRatio.trim().isEmpty()) {
return false;
}
String[] validRatios = {"16:9", "4:3", "1:1", "3:4", "9:16", "3:2", "2:3"};
for (String ratio : validRatios) {
if (ratio.equals(aspectRatio.trim())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,252 @@
package com.example.demo.controller;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.UserErrorLog;
import com.example.demo.model.UserErrorLog.ErrorType;
import com.example.demo.service.UserErrorLogService;
/**
* 用户错误日志API控制器
* 提供错误日志的查询和管理接口
*/
@RestController
@RequestMapping("/api/admin/error-logs")
public class UserErrorLogController {
private static final Logger logger = LoggerFactory.getLogger(UserErrorLogController.class);
@Autowired
private UserErrorLogService userErrorLogService;
/**
* 获取错误日志列表(分页)
*/
@GetMapping
public ResponseEntity<Map<String, Object>> getErrorLogs(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
try {
Page<UserErrorLog> errors = userErrorLogService.getAllErrors(page, size);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", errors.getContent());
response.put("totalElements", errors.getTotalElements());
response.put("totalPages", errors.getTotalPages());
response.put("currentPage", page);
response.put("pageSize", size);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取错误日志列表失败: {}", e.getMessage());
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "获取错误日志失败: " + e.getMessage()
));
}
}
/**
* 获取用户的错误日志
*/
@GetMapping("/user/{username}")
public ResponseEntity<Map<String, Object>> getUserErrorLogs(
@PathVariable String username,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
try {
Page<UserErrorLog> errors = userErrorLogService.getUserErrors(username, page, size);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", errors.getContent());
response.put("totalElements", errors.getTotalElements());
response.put("totalPages", errors.getTotalPages());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取用户错误日志失败: {}", e.getMessage());
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "获取错误日志失败: " + e.getMessage()
));
}
}
/**
* 按错误类型查询
*/
@GetMapping("/type/{errorType}")
public ResponseEntity<Map<String, Object>> getErrorsByType(
@PathVariable String errorType,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
try {
ErrorType type = ErrorType.valueOf(errorType.toUpperCase());
Page<UserErrorLog> errors = userErrorLogService.getErrorsByType(type, page, size);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", errors.getContent());
response.put("totalElements", errors.getTotalElements());
response.put("totalPages", errors.getTotalPages());
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "无效的错误类型: " + errorType
));
} catch (Exception e) {
logger.error("按类型查询错误日志失败: {}", e.getMessage());
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "查询失败: " + e.getMessage()
));
}
}
/**
* 获取最近的错误
*/
@GetMapping("/recent")
public ResponseEntity<Map<String, Object>> getRecentErrors(
@RequestParam(defaultValue = "10") int limit) {
try {
List<UserErrorLog> errors = userErrorLogService.getRecentErrors(limit);
return ResponseEntity.ok(Map.of(
"success", true,
"data", errors
));
} catch (Exception e) {
logger.error("获取最近错误失败: {}", e.getMessage());
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "获取失败: " + e.getMessage()
));
}
}
/**
* 按任务ID查询错误
*/
@GetMapping("/task/{taskId}")
public ResponseEntity<Map<String, Object>> getErrorsByTaskId(@PathVariable String taskId) {
try {
List<UserErrorLog> errors = userErrorLogService.getErrorsByTaskId(taskId);
return ResponseEntity.ok(Map.of(
"success", true,
"data", errors
));
} catch (Exception e) {
logger.error("按任务ID查询错误失败: {}", e.getMessage());
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "查询失败: " + e.getMessage()
));
}
}
/**
* 按日期范围查询
*/
@GetMapping("/date-range")
public ResponseEntity<Map<String, Object>> getErrorsByDateRange(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
try {
List<UserErrorLog> errors = userErrorLogService.getErrorsByDateRange(startDate, endDate);
return ResponseEntity.ok(Map.of(
"success", true,
"data", errors,
"count", errors.size()
));
} catch (Exception e) {
logger.error("按日期范围查询错误失败: {}", e.getMessage());
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "查询失败: " + e.getMessage()
));
}
}
/**
* 获取错误统计
*/
@GetMapping("/statistics")
public ResponseEntity<Map<String, Object>> getErrorStatistics(
@RequestParam(defaultValue = "7") int days) {
try {
Map<String, Object> stats = userErrorLogService.getErrorStatistics(days);
return ResponseEntity.ok(Map.of(
"success", true,
"data", stats,
"period", days + " days"
));
} catch (Exception e) {
logger.error("获取错误统计失败: {}", e.getMessage());
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "统计失败: " + e.getMessage()
));
}
}
/**
* 获取用户错误统计
*/
@GetMapping("/statistics/user/{username}")
public ResponseEntity<Map<String, Object>> getUserErrorStatistics(@PathVariable String username) {
try {
Map<String, Object> stats = userErrorLogService.getUserErrorStatistics(username);
return ResponseEntity.ok(Map.of(
"success", true,
"data", stats
));
} catch (Exception e) {
logger.error("获取用户错误统计失败: {}", e.getMessage());
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "统计失败: " + e.getMessage()
));
}
}
/**
* 获取所有错误类型
*/
@GetMapping("/types")
public ResponseEntity<Map<String, Object>> getErrorTypes() {
Map<String, String> types = new HashMap<>();
for (ErrorType type : ErrorType.values()) {
types.put(type.name(), type.getDescription());
}
return ResponseEntity.ok(Map.of(
"success", true,
"data", types
));
}
}

View File

@@ -0,0 +1,825 @@
package com.example.demo.controller;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.dto.UserWorkListDTO;
import com.example.demo.model.UserWork;
import com.example.demo.service.UserWorkService;
import com.example.demo.util.JwtUtils;
import io.jsonwebtoken.ExpiredJwtException;
/**
* 用户作品API控制器
*/
@RestController
@RequestMapping("/api/works")
public class UserWorkApiController {
private static final Logger logger = LoggerFactory.getLogger(UserWorkApiController.class);
@Autowired
private UserWorkService userWorkService;
@Autowired
private JwtUtils jwtUtils;
/**
* 获取正在进行中的作品
*/
@GetMapping("/processing")
public ResponseEntity<Map<String, Object>> getProcessingWorks(
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
List<UserWorkListDTO> processingWorks = userWorkService.getProcessingWorks(username);
response.put("success", true);
response.put("data", processingWorks);
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);
}
}
/**
* 获取我的作品列表
* @param includeProcessing 是否包含正在排队和生成中的作品默认为true
* @param workType 作品类型筛选可选TEXT_TO_VIDEO, IMAGE_TO_VIDEO, STORYBOARD_VIDEO, STORYBOARD_IMAGE
*/
@GetMapping("/my-works")
public ResponseEntity<Map<String, Object>> getMyWorks(
@RequestHeader(value = "Authorization", required = false) String token,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "1000") int size,
@RequestParam(defaultValue = "true") boolean includeProcessing,
@RequestParam(required = false) String workType) {
Map<String, Object> response = new HashMap<>();
long startTime = System.currentTimeMillis();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 输入验证 - 移除size上限限制
if (page < 0) page = 0;
if (size <= 0) size = 1000; // 不设上限默认1000条
// 解析作品类型
UserWork.WorkType filterType = null;
if (workType != null && !workType.isEmpty()) {
try {
filterType = UserWork.WorkType.valueOf(workType.toUpperCase());
} catch (IllegalArgumentException e) {
logger.warn("无效的作品类型: {}", workType);
}
}
// 根据参数决定是否包含正在进行中的作品
long queryStart = System.currentTimeMillis();
Page<UserWorkListDTO> works;
if (filterType != null) {
// 按类型筛选
works = userWorkService.getUserWorksByType(username, filterType, includeProcessing, page, size);
logger.info("获取作品列表(按类型): username={}, type={}, total={}",
username, filterType, works.getTotalElements());
} else if (includeProcessing) {
works = userWorkService.getAllUserWorks(username, page, size);
// 调试日志:检查是否有 PROCESSING 状态的作品
long processingCount = works.getContent().stream()
.filter(w -> "PROCESSING".equals(w.getStatus()) || "PENDING".equals(w.getStatus()))
.count();
logger.info("获取作品列表: username={}, total={}, processing/pending={}",
username, works.getTotalElements(), processingCount);
} else {
works = userWorkService.getUserWorks(username, page, size);
}
long queryEnd = System.currentTimeMillis();
Map<String, Object> workStats = userWorkService.getUserWorkStats(username);
long statsEnd = System.currentTimeMillis();
response.put("success", true);
response.put("data", works.getContent());
response.put("totalElements", works.getTotalElements());
response.put("totalPages", works.getTotalPages());
response.put("currentPage", page);
response.put("size", size);
response.put("stats", workStats);
long totalEnd = System.currentTimeMillis();
logger.info("[PERF] /my-works 耗时: 查询={}ms, 统计={}ms, 组装响应={}ms, 总计={}ms, 记录数={}",
queryEnd - queryStart, statsEnd - queryEnd, totalEnd - statsEnd, totalEnd - startTime, works.getTotalElements());
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);
}
}
/**
* 获取作品详情
*/
@GetMapping("/{workId:\\d+}")
public ResponseEntity<Map<String, Object>> getWorkDetail(
@PathVariable Long workId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
UserWork work = userWorkService.getUserWorkDetail(workId, username);
// 增加浏览次数
userWorkService.incrementViewCount(workId);
response.put("success", true);
response.put("data", work);
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);
}
}
/**
* 更新作品信息
*/
@PutMapping("/{workId:\\d+}")
public ResponseEntity<Map<String, Object>> updateWork(
@PathVariable Long workId,
@RequestHeader("Authorization") String token,
@RequestBody Map<String, Object> updateData) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
String title = (String) updateData.get("title");
String description = (String) updateData.get("description");
String tags = (String) updateData.get("tags");
Boolean isPublic = null;
Object isPublicObj = updateData.get("isPublic");
if (isPublicObj instanceof Boolean) {
isPublic = (Boolean) isPublicObj;
} else if (isPublicObj instanceof String) {
isPublic = Boolean.parseBoolean((String) isPublicObj);
}
UserWork work = userWorkService.updateWork(workId, username, title, description, tags, isPublic);
response.put("success", true);
response.put("data", work);
response.put("message", "作品更新成功");
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);
}
}
/**
* 删除作品
*/
@DeleteMapping("/{workId:\\d+}")
public ResponseEntity<Map<String, Object>> deleteWork(
@PathVariable Long workId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
boolean deleted = userWorkService.deleteWork(workId, username);
if (deleted) {
response.put("success", true);
response.put("message", "作品删除成功");
} else {
response.put("success", false);
response.put("message", "作品不存在");
}
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);
}
}
/**
* 批量删除作品
*/
@PostMapping("/batch-delete")
public ResponseEntity<Map<String, Object>> batchDeleteWorks(
@RequestBody Map<String, Object> requestBody,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
@SuppressWarnings("unchecked")
java.util.List<?> rawWorkIds = (java.util.List<?>) requestBody.get("workIds");
if (rawWorkIds == null || rawWorkIds.isEmpty()) {
response.put("success", false);
response.put("message", "未选择要删除的作品");
return ResponseEntity.badRequest().body(response);
}
int successCount = 0;
int failCount = 0;
for (Object rawId : rawWorkIds) {
try {
Long workId;
if (rawId instanceof Number) {
workId = ((Number) rawId).longValue();
} else {
workId = Long.parseLong(String.valueOf(rawId));
}
boolean deleted = userWorkService.deleteWork(workId, username);
if (deleted) {
successCount++;
} else {
failCount++;
}
} catch (Exception e) {
logger.warn("批量删除中单个作品删除失败: id={}, error={}", rawId, e.getMessage());
failCount++;
}
}
response.put("success", true);
response.put("message", String.format("成功删除 %d 个作品", successCount));
response.put("successCount", successCount);
response.put("failCount", failCount);
logger.info("批量删除作品: 用户={}, 成功={}, 失败={}", username, successCount, failCount);
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);
}
}
/**
* 点赞作品
*/
@PostMapping("/{workId:\\d+}/like")
public ResponseEntity<Map<String, Object>> likeWork(
@PathVariable Long workId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 检查作品是否存在
try {
userWorkService.getUserWorkDetail(workId, username);
userWorkService.incrementLikeCount(workId);
response.put("success", true);
response.put("message", "点赞成功");
} catch (RuntimeException e) {
response.put("success", false);
response.put("message", "作品不存在或无权限");
return ResponseEntity.status(404).body(response);
}
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);
}
}
/**
* 下载作品(记录下载次数)
*/
@PostMapping("/{workId:\\d+}/download")
public ResponseEntity<Map<String, Object>> downloadWork(
@PathVariable Long workId,
@RequestHeader("Authorization") String token) {
Map<String, Object> response = new HashMap<>();
try {
String username = extractUsernameFromToken(token);
if (username == null) {
response.put("success", false);
response.put("message", "用户未登录");
return ResponseEntity.status(401).body(response);
}
// 检查作品是否存在
try {
userWorkService.getUserWorkDetail(workId, username);
userWorkService.incrementDownloadCount(workId);
response.put("success", true);
response.put("message", "下载记录成功");
} catch (RuntimeException e) {
response.put("success", false);
response.put("message", "作品不存在或无权限");
return ResponseEntity.status(404).body(response);
}
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. 直接下载download=true返回文件流供用户下载
* 2. 重定向download=false重定向到COS URL默认
*/
@GetMapping("/{workId:\\d+}/file")
public ResponseEntity<?> downloadWorkFile(
@PathVariable Long workId,
@RequestParam(defaultValue = "false") boolean download,
@RequestHeader("Authorization") String token) {
try {
// 提取用户名(必须登录)
String username = extractUsernameFromToken(token);
if (username == null) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "用户未登录");
return ResponseEntity.status(401).body(errorResponse);
}
// 获取作品并验证权限(只允许作品所有者下载)
UserWork work = userWorkService.getWorkForDownload(workId, username);
String resultUrl = work.getResultUrl();
// 记录下载次数
userWorkService.incrementDownloadCount(workId);
// 判断resultUrl类型
if (resultUrl.startsWith("data:")) {
// Base64编码的数据
return handleBase64Download(work, resultUrl);
} else if (resultUrl.startsWith("http://") || resultUrl.startsWith("https://")) {
// 外部URL如COS
if (download) {
// 直接下载模式:代理下载
return handleUrlDownload(work, resultUrl);
} else {
// 重定向模式默认重定向到COS URL
return ResponseEntity.status(HttpStatus.FOUND)
.header(HttpHeaders.LOCATION, resultUrl)
.build();
}
} else {
logger.error("不支持的resultUrl格式: {}", resultUrl);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "不支持的资源格式");
return ResponseEntity.status(500).body(errorResponse);
}
} catch (RuntimeException e) {
logger.error("下载作品文件失败: workId={}", workId, e);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", e.getMessage());
return ResponseEntity.status(400).body(errorResponse);
} catch (Exception e) {
logger.error("下载作品文件失败: workId={}", workId, e);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "下载失败:" + e.getMessage());
return ResponseEntity.status(500).body(errorResponse);
}
}
/**
* 处理Base64数据下载
*/
private ResponseEntity<InputStreamResource> handleBase64Download(UserWork work, String dataUrl) {
try {
// 解析data URL格式data:image/png;base64,xxxxx
String[] parts = dataUrl.split(",", 2);
if (parts.length != 2) {
throw new RuntimeException("无效的Base64数据格式");
}
// 提取MIME类型
String mimeType = "application/octet-stream";
if (parts[0].contains(":") && parts[0].contains(";")) {
mimeType = parts[0].substring(parts[0].indexOf(":") + 1, parts[0].indexOf(";"));
}
// 解码Base64数据
byte[] fileBytes = Base64.getDecoder().decode(parts[1]);
InputStream inputStream = new ByteArrayInputStream(fileBytes);
// 生成文件名
String filename = generateFilename(work, mimeType);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(mimeType));
headers.setContentLength(fileBytes.length);
headers.setContentDispositionFormData("attachment",
URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20"));
logger.info("下载Base64作品: workId={}, filename={}, size={}KB",
work.getId(), filename, fileBytes.length / 1024);
return ResponseEntity.ok()
.headers(headers)
.body(new InputStreamResource(inputStream));
} catch (Exception e) {
logger.error("处理Base64下载失败: workId={}", work.getId(), e);
throw new RuntimeException("处理下载数据失败:" + e.getMessage());
}
}
/**
* 处理URL下载代理下载
*/
private ResponseEntity<InputStreamResource> handleUrlDownload(UserWork work, String fileUrl) {
try {
logger.info("开始代理下载: workId={}, url={}", work.getId(), fileUrl);
// 打开连接
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(30000); // 30秒连接超时
connection.setReadTimeout(300000); // 5分钟读取超时
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new RuntimeException("下载失败HTTP状态码" + responseCode);
}
// 获取内容信息
String contentType = connection.getContentType();
if (contentType == null) {
contentType = "application/octet-stream";
}
long contentLength = connection.getContentLengthLong();
// 生成文件名
String filename = generateFilename(work, contentType);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(contentType));
if (contentLength > 0) {
headers.setContentLength(contentLength);
}
headers.setContentDispositionFormData("attachment",
URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20"));
logger.info("下载URL作品: workId={}, filename={}, contentType={}, size={}MB",
work.getId(), filename, contentType, contentLength / (1024 * 1024));
// 返回文件流
InputStream inputStream = connection.getInputStream();
return ResponseEntity.ok()
.headers(headers)
.body(new InputStreamResource(inputStream));
} catch (IOException e) {
logger.error("处理URL下载失败: workId={}, url={}", work.getId(), fileUrl, e);
throw new RuntimeException("下载文件失败:" + e.getMessage());
}
}
/**
* 生成下载文件名
*/
private String generateFilename(UserWork work, String mimeType) {
// 使用作品标题作为文件名基础
String basename = work.getTitle();
if (basename == null || basename.isEmpty()) {
basename = "work_" + work.getId();
}
// 清理文件名中的非法字符
basename = basename.replaceAll("[\\\\/:*?\"<>|]", "_");
// 根据MIME类型确定扩展名
String extension = getExtensionFromMimeType(mimeType);
// 添加工作类型信息
String typePrefix = "";
if (work.getWorkType() != null) {
switch (work.getWorkType()) {
case TEXT_TO_VIDEO:
typePrefix = "文生视频_";
break;
case IMAGE_TO_VIDEO:
typePrefix = "图生视频_";
break;
case STORYBOARD_VIDEO:
typePrefix = "分镜视频_";
break;
case STORYBOARD_IMAGE:
typePrefix = "分镜图_";
break;
}
}
return typePrefix + basename + extension;
}
/**
* 根据MIME类型获取文件扩展名
*/
private String getExtensionFromMimeType(String mimeType) {
if (mimeType == null) {
return ".bin";
}
// 视频类型
if (mimeType.contains("video/mp4")) return ".mp4";
if (mimeType.contains("video/webm")) return ".webm";
if (mimeType.contains("video/avi")) return ".avi";
if (mimeType.contains("video/quicktime")) return ".mov";
// 图片类型
if (mimeType.contains("image/png")) return ".png";
if (mimeType.contains("image/jpeg") || mimeType.contains("image/jpg")) return ".jpg";
if (mimeType.contains("image/gif")) return ".gif";
if (mimeType.contains("image/webp")) return ".webp";
// 默认
if (mimeType.contains("video")) return ".mp4";
if (mimeType.contains("image")) return ".png";
return ".bin";
}
/**
* 获取公开作品列表
*/
@GetMapping("/public")
public ResponseEntity<Map<String, Object>> getPublicWorks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String type,
@RequestParam(required = false) String sort) {
Map<String, Object> response = new HashMap<>();
try {
// 输入验证
if (page < 0) page = 0;
if (size <= 0 || size > 100) size = 10;
Page<UserWorkListDTO> works;
if (type != null && !type.isEmpty()) {
try {
UserWork.WorkType workType = UserWork.WorkType.valueOf(type.toUpperCase());
works = userWorkService.getPublicWorksByType(workType, page, size);
} catch (IllegalArgumentException e) {
logger.warn("无效的作品类型: {}", type);
works = userWorkService.getPublicWorks(page, size);
}
} else {
works = userWorkService.getPublicWorks(page, size);
}
response.put("success", true);
response.put("data", works.getContent());
response.put("totalElements", works.getTotalElements());
response.put("totalPages", works.getTotalPages());
response.put("currentPage", page);
response.put("size", size);
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);
}
}
/**
* 搜索公开作品
*/
@GetMapping("/search")
public ResponseEntity<Map<String, Object>> searchPublicWorks(
@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Map<String, Object> response = new HashMap<>();
try {
// 输入验证
if (keyword == null || keyword.trim().isEmpty()) {
response.put("success", false);
response.put("message", "搜索关键词不能为空");
return ResponseEntity.status(400).body(response);
}
if (page < 0) page = 0;
if (size <= 0 || size > 100) size = 10;
Page<UserWorkListDTO> works = userWorkService.searchPublicWorks(keyword.trim(), page, size);
response.put("success", true);
response.put("data", works.getContent());
response.put("totalElements", works.getTotalElements());
response.put("totalPages", works.getTotalPages());
response.put("currentPage", page);
response.put("size", size);
response.put("keyword", keyword);
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);
}
}
/**
* 根据标签搜索作品
*/
@GetMapping("/tag/{tag}")
public ResponseEntity<Map<String, Object>> searchWorksByTag(
@PathVariable String tag,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Map<String, Object> response = new HashMap<>();
try {
// 输入验证
if (tag == null || tag.trim().isEmpty()) {
response.put("success", false);
response.put("message", "标签不能为空");
return ResponseEntity.status(400).body(response);
}
if (page < 0) page = 0;
if (size <= 0 || size > 100) size = 10;
Page<UserWorkListDTO> works = userWorkService.searchPublicWorksByTag(tag.trim(), page, size);
response.put("success", true);
response.put("data", works.getContent());
response.put("totalElements", works.getTotalElements());
response.put("totalPages", works.getTotalPages());
response.put("currentPage", page);
response.put("size", size);
response.put("tag", tag);
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);
}
}
/**
* 从Token中提取用户名
*/
private String extractUsernameFromToken(String token) {
try {
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
// 提取实际的token
String actualToken = jwtUtils.extractTokenFromHeader(token);
if (actualToken == null) {
return null;
}
// 验证token并获取用户名
String username = jwtUtils.getUsernameFromToken(actualToken);
if (username != null && !jwtUtils.isTokenExpired(actualToken)) {
return username;
}
return null;
} catch (ExpiredJwtException e) {
// Token过期是常见情况不打印堆栈
logger.debug("Token已过期");
return null;
} catch (Exception e) {
logger.warn("解析token失败: {}", e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,160 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.service.VerificationCodeService;
/**
* 验证码控制器
*/
@RestController
@RequestMapping("/api/verification")
public class VerificationCodeController {
private static final Logger logger = LoggerFactory.getLogger(VerificationCodeController.class);
@Autowired
private VerificationCodeService verificationCodeService;
/**
* 发送邮件验证码
*/
@PostMapping("/email/send")
public ResponseEntity<Map<String, Object>> sendEmailCode(@RequestBody Map<String, String> request) {
logger.info("收到发送验证码请求,参数: {}", request);
Map<String, Object> response = new HashMap<>();
String email = request.get("email");
if (email == null || email.trim().isEmpty()) {
logger.warn("验证码发送请求失败:邮箱为空");
response.put("success", false);
response.put("message", "邮箱不能为空");
return ResponseEntity.badRequest().body(response);
}
// 简单的邮箱格式验证
if (!email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
logger.warn("验证码发送请求失败:邮箱格式不正确,邮箱: {}", email);
response.put("success", false);
response.put("message", "邮箱格式不正确");
return ResponseEntity.badRequest().body(response);
}
logger.info("开始发送验证码到邮箱: {}", email);
try {
boolean success = verificationCodeService.sendEmailVerificationCode(email);
if (success) {
logger.info("验证码发送成功,邮箱: {}", email);
response.put("success", true);
response.put("message", "验证码发送成功");
} else {
logger.warn("验证码发送失败,邮箱: {}", email);
response.put("success", false);
response.put("message", "验证码发送失败,请稍后重试");
}
} catch (Exception e) {
logger.error("验证码发送异常,邮箱: {}", email, e);
response.put("success", false);
response.put("message", "验证码发送失败:" + e.getMessage());
}
logger.info("验证码发送请求处理完成,邮箱: {}, 结果: {}", email, response.get("success"));
return ResponseEntity.ok(response);
}
/**
* 验证邮件验证码
*/
@PostMapping("/email/verify")
public ResponseEntity<Map<String, Object>> verifyEmailCode(@RequestBody Map<String, String> request) {
Map<String, Object> response = new HashMap<>();
String email = request.get("email");
String code = request.get("code");
if (email == null || email.trim().isEmpty()) {
response.put("success", false);
response.put("message", "邮箱不能为空");
return ResponseEntity.badRequest().body(response);
}
if (code == null || code.trim().isEmpty()) {
response.put("success", false);
response.put("message", "验证码不能为空");
return ResponseEntity.badRequest().body(response);
}
try {
boolean success = verificationCodeService.verifyEmailCode(email, code);
if (success) {
response.put("success", true);
response.put("message", "验证码验证成功");
} else {
response.put("success", false);
response.put("message", "验证码错误或已过期");
}
} catch (Exception e) {
response.put("success", false);
response.put("message", "验证码验证失败:" + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 开发模式:设置验证码(仅开发环境使用)
*/
@PostMapping("/email/dev-set")
public ResponseEntity<Map<String, Object>> setDevCode(@RequestBody Map<String, String> request) {
Map<String, Object> response = new HashMap<>();
// 仅开发环境允许
if (!"dev".equals(System.getProperty("spring.profiles.active")) &&
!"development".equals(System.getProperty("spring.profiles.active"))) {
response.put("success", false);
response.put("message", "此接口仅开发环境可用");
return ResponseEntity.badRequest().body(response);
}
String email = request.get("email");
String code = request.get("code");
if (email == null || email.trim().isEmpty()) {
response.put("success", false);
response.put("message", "邮箱不能为空");
return ResponseEntity.badRequest().body(response);
}
if (code == null || code.trim().isEmpty()) {
response.put("success", false);
response.put("message", "验证码不能为空");
return ResponseEntity.badRequest().body(response);
}
try {
// 直接设置验证码到内存存储
verificationCodeService.setVerificationCode(email, code);
response.put("success", true);
response.put("message", "开发模式验证码设置成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "设置验证码失败:" + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
}

View File

@@ -0,0 +1,82 @@
package com.example.demo.dto;
import java.util.HashMap;
import java.util.Map;
/**
* 邮件消息DTO
* 用于封装邮件发送请求
*/
public class MailMessage {
private String toEmail;
private String subject;
private Long templateId;
private Map<String, Object> templateData;
public MailMessage() {
this.templateData = new HashMap<>();
}
public MailMessage(String toEmail, String subject, Long templateId) {
this();
this.toEmail = toEmail;
this.subject = subject;
this.templateId = templateId;
}
public String getToEmail() {
return toEmail;
}
public void setToEmail(String toEmail) {
this.toEmail = toEmail;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public Long getTemplateId() {
return templateId;
}
public void setTemplateId(Long templateId) {
this.templateId = templateId;
}
public Map<String, Object> getTemplateData() {
return templateData;
}
public void setTemplateData(Map<String, Object> templateData) {
this.templateData = templateData;
}
/**
* 添加模板参数
*/
public MailMessage addParam(String key, Object value) {
this.templateData.put(key, value);
return this;
}
}

View File

@@ -0,0 +1,214 @@
package com.example.demo.dto;
import java.time.LocalDateTime;
import com.example.demo.model.UserWork;
/**
* 用户作品列表轻量DTO
* 排除 uploadedImages / videoReferenceImages 等LONGTEXT大字段
* 避免列表接口序列化数十MB数据导致响应缓慢。
*
* "做同款"等需要大字段的场景应通过作品详情接口GET /works/{id})懒加载。
*/
public class UserWorkListDTO {
private Long id;
private Long userId;
private String username;
private String taskId;
private String workType;
private String title;
private String description;
private String prompt;
private String imagePrompt;
private String videoPrompt;
private String resultUrl;
private String thumbnailUrl;
private String duration;
private String aspectRatio;
private String quality;
private String fileSize;
private Integer pointsCost;
private String status;
private Boolean isPublic;
private Integer viewCount;
private Integer likeCount;
private Integer downloadCount;
private String tags;
private String videoModel;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime completedAt;
// ========== 排除的大字段 ==========
// uploadedImages (LONGTEXT, base64图片数组, 单条可达数MB)
// videoReferenceImages (LONGTEXT, base64图片数组, 单条可达数MB)
public UserWorkListDTO() {}
/**
* JPQL 构造器投影用全参数构造函数
* Hibernate 生成 SQL 时只 SELECT 这些列,不会加载 uploadedImages/videoReferenceImages
*/
public UserWorkListDTO(Long id, Long userId, String username, String taskId,
UserWork.WorkType workType, String title, String description,
String prompt, String imagePrompt, String videoPrompt,
String resultUrl, String thumbnailUrl, String duration,
String aspectRatio, String quality, String fileSize,
Integer pointsCost, UserWork.WorkStatus status, Boolean isPublic,
Integer viewCount, Integer likeCount, Integer downloadCount,
String tags, String videoModel,
LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime completedAt) {
this.id = id;
this.userId = userId;
this.username = username;
this.taskId = taskId;
this.workType = workType != null ? workType.name() : null;
this.title = title;
this.description = description;
this.prompt = prompt;
this.imagePrompt = imagePrompt;
this.videoPrompt = videoPrompt;
// 列表接口过滤非分镜图类型的 base64 内联数据data:image/...单条可达数百KB
// 分镜图(STORYBOARD_IMAGE)的 resultUrl 就是图片本身且无 thumbnailUrl必须保留
boolean isStoryboardImage = workType == com.example.demo.model.UserWork.WorkType.STORYBOARD_IMAGE;
this.resultUrl = (!isStoryboardImage && resultUrl != null && resultUrl.startsWith("data:")) ? null : resultUrl;
this.thumbnailUrl = thumbnailUrl;
this.duration = duration;
this.aspectRatio = aspectRatio;
this.quality = quality;
this.fileSize = fileSize;
this.pointsCost = pointsCost;
this.status = status != null ? status.name() : null;
this.isPublic = isPublic;
this.viewCount = viewCount;
this.likeCount = likeCount;
this.downloadCount = downloadCount;
this.tags = tags;
this.videoModel = videoModel;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.completedAt = completedAt;
}
/**
* 从 UserWork 实体转换为列表 DTO
*/
public static UserWorkListDTO fromEntity(UserWork entity) {
UserWorkListDTO dto = new UserWorkListDTO();
dto.id = entity.getId();
dto.userId = entity.getUserId();
dto.username = entity.getUsername();
dto.taskId = entity.getTaskId();
dto.workType = entity.getWorkType() != null ? entity.getWorkType().name() : null;
dto.title = entity.getTitle();
dto.description = entity.getDescription();
dto.prompt = entity.getPrompt();
dto.imagePrompt = entity.getImagePrompt();
dto.videoPrompt = entity.getVideoPrompt();
String rawResultUrl = entity.getResultUrl();
boolean isStoryboardImg = entity.getWorkType() == com.example.demo.model.UserWork.WorkType.STORYBOARD_IMAGE;
dto.resultUrl = (!isStoryboardImg && rawResultUrl != null && rawResultUrl.startsWith("data:")) ? null : rawResultUrl;
dto.thumbnailUrl = entity.getThumbnailUrl();
dto.duration = entity.getDuration();
dto.aspectRatio = entity.getAspectRatio();
dto.quality = entity.getQuality();
dto.fileSize = entity.getFileSize();
dto.pointsCost = entity.getPointsCost();
dto.status = entity.getStatus() != null ? entity.getStatus().name() : null;
dto.isPublic = entity.getIsPublic();
dto.viewCount = entity.getViewCount();
dto.likeCount = entity.getLikeCount();
dto.downloadCount = entity.getDownloadCount();
dto.tags = entity.getTags();
dto.videoModel = entity.getVideoModel();
dto.createdAt = entity.getCreatedAt();
dto.updatedAt = entity.getUpdatedAt();
dto.completedAt = entity.getCompletedAt();
return dto;
}
// ========== Getters & Setters ==========
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getWorkType() { return workType; }
public void setWorkType(String workType) { this.workType = workType; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getPrompt() { return prompt; }
public void setPrompt(String prompt) { this.prompt = prompt; }
public String getImagePrompt() { return imagePrompt; }
public void setImagePrompt(String imagePrompt) { this.imagePrompt = imagePrompt; }
public String getVideoPrompt() { return videoPrompt; }
public void setVideoPrompt(String videoPrompt) { this.videoPrompt = videoPrompt; }
public String getResultUrl() { return resultUrl; }
public void setResultUrl(String resultUrl) { this.resultUrl = resultUrl; }
public String getThumbnailUrl() { return thumbnailUrl; }
public void setThumbnailUrl(String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; }
public String getDuration() { return duration; }
public void setDuration(String duration) { this.duration = duration; }
public String getAspectRatio() { return aspectRatio; }
public void setAspectRatio(String aspectRatio) { this.aspectRatio = aspectRatio; }
public String getQuality() { return quality; }
public void setQuality(String quality) { this.quality = quality; }
public String getFileSize() { return fileSize; }
public void setFileSize(String fileSize) { this.fileSize = fileSize; }
public Integer getPointsCost() { return pointsCost; }
public void setPointsCost(Integer pointsCost) { this.pointsCost = pointsCost; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Boolean getIsPublic() { return isPublic; }
public void setIsPublic(Boolean isPublic) { this.isPublic = isPublic; }
public Integer getViewCount() { return viewCount; }
public void setViewCount(Integer viewCount) { this.viewCount = viewCount; }
public Integer getLikeCount() { return likeCount; }
public void setLikeCount(Integer likeCount) { this.likeCount = likeCount; }
public Integer getDownloadCount() { return downloadCount; }
public void setDownloadCount(Integer downloadCount) { this.downloadCount = downloadCount; }
public String getTags() { return tags; }
public void setTags(String tags) { this.tags = tags; }
public String getVideoModel() { return videoModel; }
public void setVideoModel(String videoModel) { this.videoModel = videoModel; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getCompletedAt() { return completedAt; }
public void setCompletedAt(LocalDateTime completedAt) { this.completedAt = completedAt; }
}

View File

@@ -0,0 +1,73 @@
package com.example.demo.filter;
import com.example.demo.service.OnlineStatsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 访客追踪过滤器
* 记录每个请求的IP地址用于统计在线人数
*/
@Component
public class VisitorTrackingFilter extends OncePerRequestFilter {
@Autowired
private OnlineStatsService onlineStatsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 获取真实IP地址考虑代理情况
String ip = getClientIP(request);
// 记录访问
if (ip != null && !ip.isEmpty()) {
onlineStatsService.recordVisit(ip);
}
// 继续过滤链
filterChain.doFilter(request, response);
}
/**
* 获取客户端真实IP地址
* 考虑代理服务器的情况
*/
private String getClientIP(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 如果是多个代理取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}

View File

@@ -0,0 +1,60 @@
package com.example.demo.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import com.example.demo.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* 用户活跃时间拦截器
* 每次用户请求时更新其最后活跃时间,用于统计在线用户数
*/
@Component
public class UserActivityInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(UserActivityInterceptor.class);
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
try {
// 获取当前认证用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()
&& !"anonymousUser".equals(authentication.getPrincipal())) {
String username = authentication.getName();
// 异步更新用户活跃时间,避免阻塞请求
updateUserActiveTimeAsync(username);
}
} catch (Exception e) {
// 不因活跃时间更新失败而影响正常请求
}
return true;
}
/**
* 异步更新用户活跃时间
* 使用专门的方法,不触发缓存清除
*/
private void updateUserActiveTimeAsync(String username) {
try {
userService.updateLastActiveTime(username);
} catch (Exception e) {
// 忽略更新失败
}
}
}

View File

@@ -0,0 +1,242 @@
package com.example.demo.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
/**
* 成功任务归档实体
* 用于存储已完成的任务信息
*/
@Entity
@Table(name = "completed_tasks_archive")
public class CompletedTaskArchive {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "task_id", nullable = false, length = 255)
private String taskId;
@Column(name = "username", nullable = false, length = 255)
private String username;
@Column(name = "task_type", nullable = false, length = 50)
private String taskType;
@Column(name = "prompt", columnDefinition = "TEXT")
private String prompt;
@Column(name = "aspect_ratio", length = 20)
private String aspectRatio;
@Column(name = "duration")
private Integer duration;
@Column(name = "hd_mode")
private Boolean hdMode = false;
@Column(name = "result_url", columnDefinition = "TEXT")
private String resultUrl;
@Column(name = "real_task_id", length = 255)
private String realTaskId;
@Column(name = "progress")
private Integer progress = 100;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "completed_at", nullable = false)
private LocalDateTime completedAt;
@Column(name = "archived_at")
private LocalDateTime archivedAt;
@Column(name = "points_cost")
private Integer pointsCost = 0;
// 构造函数
public CompletedTaskArchive() {
this.archivedAt = LocalDateTime.now();
}
public CompletedTaskArchive(String taskId, String username, String taskType,
String prompt, String aspectRatio, Integer duration,
Boolean hdMode, String resultUrl, String realTaskId,
LocalDateTime createdAt, LocalDateTime completedAt,
Integer pointsCost) {
this.taskId = taskId;
this.username = username;
this.taskType = taskType;
this.prompt = prompt;
this.aspectRatio = aspectRatio;
this.duration = duration;
this.hdMode = hdMode;
this.resultUrl = resultUrl;
this.realTaskId = realTaskId;
this.createdAt = createdAt;
this.completedAt = completedAt;
this.archivedAt = LocalDateTime.now();
this.pointsCost = pointsCost;
this.progress = 100;
}
// 从TextToVideoTask创建
public static CompletedTaskArchive fromTextToVideoTask(TextToVideoTask task) {
return new CompletedTaskArchive(
task.getTaskId(),
task.getUsername(),
"TEXT_TO_VIDEO",
task.getPrompt(),
task.getAspectRatio(),
task.getDuration(),
task.isHdMode(),
task.getResultUrl(),
task.getRealTaskId(),
task.getCreatedAt(),
task.getUpdatedAt(),
10 // 默认积分消耗
);
}
// 从ImageToVideoTask创建
public static CompletedTaskArchive fromImageToVideoTask(ImageToVideoTask task) {
return new CompletedTaskArchive(
task.getTaskId(),
task.getUsername(),
"IMAGE_TO_VIDEO",
task.getPrompt(),
task.getAspectRatio(),
task.getDuration(),
task.getHdMode(),
task.getResultUrl(),
task.getRealTaskId(),
task.getCreatedAt(),
task.getUpdatedAt(),
15 // 默认积分消耗
);
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public String getPrompt() {
return prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public String getAspectRatio() {
return aspectRatio;
}
public void setAspectRatio(String aspectRatio) {
this.aspectRatio = aspectRatio;
}
public Integer getDuration() {
return duration;
}
public void setDuration(Integer duration) {
this.duration = duration;
}
public Boolean getHdMode() {
return hdMode;
}
public void setHdMode(Boolean hdMode) {
this.hdMode = hdMode;
}
public String getResultUrl() {
return resultUrl;
}
public void setResultUrl(String resultUrl) {
this.resultUrl = resultUrl;
}
public String getRealTaskId() {
return realTaskId;
}
public void setRealTaskId(String realTaskId) {
this.realTaskId = realTaskId;
}
public Integer getProgress() {
return progress;
}
public void setProgress(Integer progress) {
this.progress = progress;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getCompletedAt() {
return completedAt;
}
public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}
public LocalDateTime getArchivedAt() {
return archivedAt;
}
public void setArchivedAt(LocalDateTime archivedAt) {
this.archivedAt = archivedAt;
}
public Integer getPointsCost() {
return pointsCost;
}
public void setPointsCost(Integer pointsCost) {
this.pointsCost = pointsCost;
}
}

View File

@@ -0,0 +1,162 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 失败任务清理日志实体
* 用于记录被清理的失败任务信息
*/
@Entity
@Table(name = "failed_tasks_cleanup_log")
public class FailedTaskCleanupLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "task_id", nullable = false, length = 255)
private String taskId;
@Column(name = "username", nullable = false, length = 255)
private String username;
@Column(name = "task_type", nullable = false, length = 50)
private String taskType;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "failed_at", nullable = false)
private LocalDateTime failedAt;
@Column(name = "cleaned_at")
private LocalDateTime cleanedAt;
// 构造函数
public FailedTaskCleanupLog() {
this.cleanedAt = LocalDateTime.now();
}
public FailedTaskCleanupLog(String taskId, String username, String taskType,
String errorMessage, LocalDateTime createdAt,
LocalDateTime failedAt) {
this.taskId = taskId;
this.username = username;
this.taskType = taskType;
this.errorMessage = errorMessage;
this.createdAt = createdAt;
this.failedAt = failedAt;
this.cleanedAt = LocalDateTime.now();
}
// 从TextToVideoTask创建
public static FailedTaskCleanupLog fromTextToVideoTask(TextToVideoTask task) {
return new FailedTaskCleanupLog(
task.getTaskId(),
task.getUsername(),
"TEXT_TO_VIDEO",
task.getErrorMessage(),
task.getCreatedAt(),
task.getUpdatedAt()
);
}
// 从ImageToVideoTask创建
public static FailedTaskCleanupLog fromImageToVideoTask(ImageToVideoTask task) {
return new FailedTaskCleanupLog(
task.getTaskId(),
task.getUsername(),
"IMAGE_TO_VIDEO",
task.getErrorMessage(),
task.getCreatedAt(),
task.getUpdatedAt()
);
}
// 从StoryboardVideoTask创建
public static FailedTaskCleanupLog fromStoryboardVideoTask(StoryboardVideoTask task) {
return new FailedTaskCleanupLog(
task.getTaskId(),
task.getUsername(),
"STORYBOARD_VIDEO",
task.getErrorMessage(),
task.getCreatedAt(),
task.getUpdatedAt()
);
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getFailedAt() {
return failedAt;
}
public void setFailedAt(LocalDateTime failedAt) {
this.failedAt = failedAt;
}
public LocalDateTime getCleanedAt() {
return cleanedAt;
}
public void setCleanedAt(LocalDateTime cleanedAt) {
this.cleanedAt = cleanedAt;
}
}

View File

@@ -0,0 +1,310 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 图生视频任务模型
*/
@Entity
@Table(name = "image_to_video_tasks")
public class ImageToVideoTask {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "task_id", unique = true, nullable = false)
private String taskId;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "first_frame_url", nullable = false)
private String firstFrameUrl;
@Column(name = "last_frame_url")
private String lastFrameUrl;
@Column(name = "prompt", columnDefinition = "TEXT")
private String prompt;
@Column(name = "aspect_ratio", nullable = false)
private String aspectRatio;
@Column(name = "duration", nullable = false)
private Integer duration;
@Column(name = "hd_mode", nullable = false)
private Boolean hdMode = false;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private TaskStatus status = TaskStatus.PROCESSING; // 默认生成中
@Column(name = "progress")
private Integer progress = 0;
@Column(name = "result_url", columnDefinition = "TEXT")
private String resultUrl;
@Column(name = "real_task_id")
private String realTaskId;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
@Column(name = "cost_points")
private Integer costPoints = 0;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "completed_at")
private LocalDateTime completedAt;
@Column(name = "video_model", length = 50)
private String videoModel = "grok-video-3";
// 构造函数
public ImageToVideoTask() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public ImageToVideoTask(String taskId, String username, String firstFrameUrl, String prompt,
String aspectRatio, Integer duration, Boolean hdMode) {
this();
this.taskId = taskId;
this.username = username;
this.firstFrameUrl = firstFrameUrl;
this.prompt = prompt;
this.aspectRatio = aspectRatio;
this.duration = duration;
this.hdMode = hdMode;
// 计算消耗积分
this.costPoints = calculateCost();
}
/**
* 计算任务消耗积分
*/
private Integer calculateCost() {
// 安全处理duration可能为null的情况
Integer safeDuration = duration;
int actualDuration = (safeDuration == null || safeDuration <= 0) ? 5 : safeDuration; // 使用默认时长但不修改字段
int baseCost = 10; // 基础消耗
int durationCost = actualDuration * 2; // 时长消耗
int hdCost = (hdMode != null && hdMode) ? 20 : 0; // 高清模式消耗
return baseCost + durationCost + hdCost;
}
/**
* 更新任务状态
*/
public void updateStatus(TaskStatus newStatus) {
this.status = newStatus;
this.updatedAt = LocalDateTime.now();
// 任务结束状态都应该设置完成时间
if (newStatus == TaskStatus.COMPLETED || newStatus == TaskStatus.FAILED || newStatus == TaskStatus.CANCELLED) {
this.completedAt = LocalDateTime.now();
}
}
/**
* 更新进度
*/
public void updateProgress(Integer progress) {
this.progress = Math.min(100, Math.max(0, progress));
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFirstFrameUrl() {
return firstFrameUrl;
}
public void setFirstFrameUrl(String firstFrameUrl) {
this.firstFrameUrl = firstFrameUrl;
}
public String getLastFrameUrl() {
return lastFrameUrl;
}
public void setLastFrameUrl(String lastFrameUrl) {
this.lastFrameUrl = lastFrameUrl;
}
public String getPrompt() {
return prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public String getAspectRatio() {
return aspectRatio;
}
public void setAspectRatio(String aspectRatio) {
this.aspectRatio = aspectRatio;
}
public Integer getDuration() {
return duration;
}
public void setDuration(Integer duration) {
this.duration = duration;
}
public Boolean getHdMode() {
return hdMode;
}
public void setHdMode(Boolean hdMode) {
this.hdMode = hdMode;
}
public TaskStatus getStatus() {
return status;
}
public void setStatus(TaskStatus status) {
this.status = status;
}
public Integer getProgress() {
return progress;
}
public void setProgress(Integer progress) {
this.progress = progress;
}
public String getResultUrl() {
return resultUrl;
}
public void setResultUrl(String resultUrl) {
this.resultUrl = resultUrl;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public Integer getCostPoints() {
return costPoints;
}
public void setCostPoints(Integer costPoints) {
this.costPoints = costPoints;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getCompletedAt() {
return completedAt;
}
public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}
public String getRealTaskId() {
return realTaskId;
}
public void setRealTaskId(String realTaskId) {
this.realTaskId = realTaskId;
}
public String getVideoModel() {
return videoModel;
}
public void setVideoModel(String videoModel) {
this.videoModel = videoModel;
}
/**
* 任务状态枚举
*/
public enum TaskStatus {
PENDING("等待中"),
PROCESSING("处理中"),
COMPLETED("已完成"),
FAILED("失败"),
CANCELLED("已取消");
private final String description;
TaskStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
}

View File

@@ -0,0 +1,145 @@
package com.example.demo.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "membership_levels")
public class MembershipLevel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
@Column(name = "display_name", nullable = false)
private String displayName;
@Column(name = "description")
private String description;
@Column(name = "price", nullable = false)
private Double price;
@Column(name = "duration_days", nullable = false)
private Integer durationDays;
@Column(name = "points_bonus", nullable = false)
private Integer pointsBonus;
@Column(name = "features", columnDefinition = "JSON")
private String features;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// 构造函数
public MembershipLevel() {}
public MembershipLevel(String name, String displayName, String description, Double price,
Integer durationDays, Integer pointsBonus, String features) {
this.name = name;
this.displayName = displayName;
this.description = description;
this.price = price;
this.durationDays = durationDays;
this.pointsBonus = pointsBonus;
this.features = features;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getDurationDays() {
return durationDays;
}
public void setDurationDays(Integer durationDays) {
this.durationDays = durationDays;
}
public Integer getPointsBonus() {
return pointsBonus;
}
public void setPointsBonus(Integer pointsBonus) {
this.pointsBonus = pointsBonus;
}
public String getFeatures() {
return features;
}
public void setFeatures(String features) {
this.features = features;
}
public Boolean getIsActive() {
return isActive;
}
public void setIsActive(Boolean isActive) {
this.isActive = isActive;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,314 @@
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50, unique = true)
private String orderNumber;
@NotNull
@DecimalMin(value = "0.01", message = "订单金额必须大于0")
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount;
@NotBlank
@Column(nullable = false, length = 3)
private String currency;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderType orderType;
@Column(length = 500)
private String description;
@Column(length = 1000)
private String notes;
@Column(name = "shipping_address", length = 1000)
private String shippingAddress;
@Column(name = "billing_address", length = 1000)
private String billingAddress;
@Column(name = "contact_phone", length = 20)
private String contactPhone;
@Column(name = "contact_email", length = 100)
private String contactEmail;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "paid_at")
private LocalDateTime paidAt;
@Column(name = "shipped_at")
private LocalDateTime shippedAt;
@Column(name = "delivered_at")
private LocalDateTime deliveredAt;
@Column(name = "cancelled_at")
private LocalDateTime cancelledAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Payment> payments = new ArrayList<>();
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (orderNumber == null) {
orderNumber = generateOrderNumber();
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
/**
* 生成订单号
*/
private String generateOrderNumber() {
return "ORD" + System.currentTimeMillis() + String.format("%03d", (int)(Math.random() * 1000));
}
/**
* 计算订单总金额
*/
public BigDecimal calculateTotalAmount() {
return orderItems.stream()
.map(OrderItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* 检查是否可以支付
*/
public boolean canPay() {
return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED;
}
/**
* 检查是否可以取消
*/
public boolean canCancel() {
return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED;
}
/**
* 检查是否可以发货
*/
public boolean canShip() {
return status == OrderStatus.PAID || status == OrderStatus.CONFIRMED;
}
/**
* 检查是否可以完成
*/
public boolean canComplete() {
return status == OrderStatus.SHIPPED;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
public OrderType getOrderType() {
return orderType;
}
public void setOrderType(OrderType orderType) {
this.orderType = orderType;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public String getShippingAddress() {
return shippingAddress;
}
public void setShippingAddress(String shippingAddress) {
this.shippingAddress = shippingAddress;
}
public String getBillingAddress() {
return billingAddress;
}
public void setBillingAddress(String billingAddress) {
this.billingAddress = billingAddress;
}
public String getContactPhone() {
return contactPhone;
}
public void setContactPhone(String contactPhone) {
this.contactPhone = contactPhone;
}
public String getContactEmail() {
return contactEmail;
}
public void setContactEmail(String contactEmail) {
this.contactEmail = contactEmail;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getPaidAt() {
return paidAt;
}
public void setPaidAt(LocalDateTime paidAt) {
this.paidAt = paidAt;
}
public LocalDateTime getShippedAt() {
return shippedAt;
}
public void setShippedAt(LocalDateTime shippedAt) {
this.shippedAt = shippedAt;
}
public LocalDateTime getDeliveredAt() {
return deliveredAt;
}
public void setDeliveredAt(LocalDateTime deliveredAt) {
this.deliveredAt = deliveredAt;
}
public LocalDateTime getCancelledAt() {
return cancelledAt;
}
public void setCancelledAt(LocalDateTime cancelledAt) {
this.cancelledAt = cancelledAt;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public List<OrderItem> getOrderItems() {
return orderItems;
}
public void setOrderItems(List<OrderItem> orderItems) {
this.orderItems = orderItems;
}
public List<Payment> getPayments() {
return payments;
}
public void setPayments(List<Payment> payments) {
this.payments = payments;
}
}

View File

@@ -0,0 +1,134 @@
package com.example.demo.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Column(nullable = false, length = 100)
private String productName;
@Column(length = 500)
private String productDescription;
@Column(length = 200)
private String productSku;
@NotNull
@DecimalMin(value = "0.01", message = "单价必须大于0")
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
@NotNull
@Min(value = 1, message = "数量必须大于0")
@Column(nullable = false)
private Integer quantity;
@NotNull
@DecimalMin(value = "0.00", message = "小计不能为负数")
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal subtotal;
@Column(length = 100)
private String productImage;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@PrePersist
@PreUpdate
protected void calculateSubtotal() {
if (unitPrice != null && quantity != null) {
subtotal = unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getProductDescription() {
return productDescription;
}
public void setProductDescription(String productDescription) {
this.productDescription = productDescription;
}
public String getProductSku() {
return productSku;
}
public void setProductSku(String productSku) {
this.productSku = productSku;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public BigDecimal getSubtotal() {
return subtotal;
}
public void setSubtotal(BigDecimal subtotal) {
this.subtotal = subtotal;
}
public String getProductImage() {
return productImage;
}
public void setProductImage(String productImage) {
this.productImage = productImage;
}
@JsonIgnore
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
}

View File

@@ -0,0 +1,26 @@
package com.example.demo.model;
public enum OrderStatus {
PENDING("待支付"),
CONFIRMED("已确认"),
PAID("已支付"),
PROCESSING("处理中"),
SHIPPED("已发货"),
DELIVERED("已送达"),
COMPLETED("已完成"),
CANCELLED("已取消"),
REFUNDED("已退款");
private final String displayName;
OrderStatus(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -0,0 +1,21 @@
package com.example.demo.model;
public enum OrderType {
PRODUCT("商品订单"),
SERVICE("服务订单"),
SUBSCRIPTION("订阅订单"),
DIGITAL("数字商品"),
PHYSICAL("实体商品"),
PAYMENT("支付订单"),
MEMBERSHIP("会员订单");
private final String displayName;
OrderType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -0,0 +1,272 @@
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "payments")
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Column(nullable = false, length = 50)
private String orderId;
@NotNull
@DecimalMin(value = "0.01", message = "金额必须大于0")
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal amount;
@NotBlank
@Column(nullable = false, length = 3)
private String currency;
// PayPal USD转换相关字段
@Column(precision = 10, scale = 2)
private BigDecimal originalAmount; // 原始人民币金额
@Column(length = 3)
private String originalCurrency; // 原始货币(CNY)
@Column(precision = 10, scale = 6)
private BigDecimal exchangeRate; // 汇率
@Column(precision = 10, scale = 2)
private BigDecimal convertedAmount; // 转换后的USD金额
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentMethod paymentMethod;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentStatus status;
@Column(length = 500)
private String description;
@Column(length = 100)
private String externalTransactionId;
@Column(length = 1000)
private String callbackUrl;
@Column(length = 1000)
private String returnUrl;
@Column(length = 2000)
private String paymentUrl;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "paid_at")
private LocalDateTime paidAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id_ref")
private Order order;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (status == null) {
status = PaymentStatus.PENDING;
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
/**
* 检查是否可以支付
*/
public boolean canPay() {
return status == PaymentStatus.PENDING;
}
/**
* 检查是否可以退款
*/
public boolean canRefund() {
return status == PaymentStatus.SUCCESS;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
public BigDecimal getOriginalAmount() {
return originalAmount;
}
public void setOriginalAmount(BigDecimal originalAmount) {
this.originalAmount = originalAmount;
}
public String getOriginalCurrency() {
return originalCurrency;
}
public void setOriginalCurrency(String originalCurrency) {
this.originalCurrency = originalCurrency;
}
public BigDecimal getExchangeRate() {
return exchangeRate;
}
public void setExchangeRate(BigDecimal exchangeRate) {
this.exchangeRate = exchangeRate;
}
public BigDecimal getConvertedAmount() {
return convertedAmount;
}
public void setConvertedAmount(BigDecimal convertedAmount) {
this.convertedAmount = convertedAmount;
}
public PaymentMethod getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
public PaymentStatus getStatus() {
return status;
}
public void setStatus(PaymentStatus status) {
this.status = status;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getExternalTransactionId() {
return externalTransactionId;
}
public void setExternalTransactionId(String externalTransactionId) {
this.externalTransactionId = externalTransactionId;
}
public String getCallbackUrl() {
return callbackUrl;
}
public void setCallbackUrl(String callbackUrl) {
this.callbackUrl = callbackUrl;
}
public String getReturnUrl() {
return returnUrl;
}
public void setReturnUrl(String returnUrl) {
this.returnUrl = returnUrl;
}
public String getPaymentUrl() {
return paymentUrl;
}
public void setPaymentUrl(String paymentUrl) {
this.paymentUrl = paymentUrl;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getPaidAt() {
return paidAt;
}
public void setPaidAt(LocalDateTime paidAt) {
this.paidAt = paidAt;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
}

View File

@@ -0,0 +1,20 @@
package com.example.demo.model;
public enum PaymentMethod {
ALIPAY("支付宝"),
WECHAT("微信支付"),
QQPAY("QQ钱包"),
BANK("云闪付"),
PAYPAL("PayPal");
private final String displayName;
PaymentMethod(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -0,0 +1,21 @@
package com.example.demo.model;
public enum PaymentStatus {
PENDING("待支付"),
PROCESSING("处理中"),
SUCCESS("支付成功"),
FAILED("支付失败"),
CANCELLED("已取消"),
REFUNDED("已退款");
private final String displayName;
PaymentStatus(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -0,0 +1,220 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 积分冻结记录实体
* 记录每次积分冻结的详细信息
*/
@Entity
@Table(name = "points_freeze_records")
public class PointsFreezeRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, length = 100)
private String username;
@Column(name = "task_id", nullable = false, length = 50)
private String taskId;
@Enumerated(EnumType.STRING)
@Column(name = "task_type", nullable = false, length = 20)
private TaskType taskType;
@Column(name = "freeze_points", nullable = false)
private Integer freezePoints; // 冻结的积分数量
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private FreezeStatus status; // 冻结状态
@Column(name = "freeze_reason", length = 200)
private String freezeReason; // 冻结原因
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Column(name = "completed_at")
private LocalDateTime completedAt;
/**
* 任务类型枚举
*/
public enum TaskType {
TEXT_TO_VIDEO("文生视频"),
IMAGE_TO_VIDEO("图生视频"),
STORYBOARD_VIDEO("分镜视频"),
STORYBOARD_IMAGE("分镜图");
private final String description;
TaskType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 冻结状态枚举
*/
public enum FreezeStatus {
FROZEN("已冻结"),
DEDUCTED("已扣除"),
RETURNED("已返还"),
EXPIRED("已过期");
private final String description;
FreezeStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
// 构造函数
public PointsFreezeRecord() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public PointsFreezeRecord(String username, String taskId, TaskType taskType, Integer freezePoints, String freezeReason) {
this();
this.username = username;
this.taskId = taskId;
this.taskType = taskType;
this.freezePoints = freezePoints;
this.freezeReason = freezeReason;
this.status = FreezeStatus.FROZEN;
}
/**
* 更新状态
*/
public void updateStatus(FreezeStatus newStatus) {
this.status = newStatus;
this.updatedAt = LocalDateTime.now();
if (newStatus == FreezeStatus.DEDUCTED ||
newStatus == FreezeStatus.RETURNED ||
newStatus == FreezeStatus.EXPIRED) {
this.completedAt = LocalDateTime.now();
}
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public TaskType getTaskType() {
return taskType;
}
public void setTaskType(TaskType taskType) {
this.taskType = taskType;
}
public Integer getFreezePoints() {
return freezePoints;
}
public void setFreezePoints(Integer freezePoints) {
this.freezePoints = freezePoints;
}
public FreezeStatus getStatus() {
return status;
}
public void setStatus(FreezeStatus status) {
this.status = status;
}
public String getFreezeReason() {
return freezeReason;
}
public void setFreezeReason(String freezeReason) {
this.freezeReason = freezeReason;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getCompletedAt() {
return completedAt;
}
public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}
}

View File

@@ -0,0 +1,233 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 分镜视频任务实体
*/
@Entity
@Table(name = "storyboard_video_tasks")
public class StoryboardVideoTask {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String taskId;
@Column(nullable = false, length = 100)
private String username;
@Column(columnDefinition = "TEXT")
private String prompt; // 文本描述
@Column(name = "image_url", columnDefinition = "LONGTEXT")
private String imageUrl; // 上传的参考图片URL可选支持Base64格式
@Column(nullable = false, length = 10)
private String aspectRatio; // 3:2, 16:9, 4:3, 1:1, 3:4, 9:16, 2:3
@Column(nullable = false)
private boolean hdMode; // 是否高清模式
@Column(nullable = false)
private Integer duration; // 视频时长5, 10, 15
@Column(name = "image_model", length = 50)
private String imageModel = "nano-banana-2"; // 图像生成模型nano-banana, nano-banana-2
@Column(name = "video_model", length = 50)
private String videoModel = "grok-video-3";
@Column(name = "uploaded_images", columnDefinition = "LONGTEXT")
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)
private TaskStatus status;
@Column(nullable = false)
private int progress; // 0-100
@Column(name = "result_url", columnDefinition = "LONGTEXT")
private String resultUrl; // 分镜图URLBase64编码的图片可能非常大- 网格图
@Column(name = "storyboard_images", columnDefinition = "LONGTEXT")
private String storyboardImages; // 单独的分镜图片JSON数组每张图片为Base64格式带data URI前缀
@Column(name = "real_task_id")
private String realTaskId; // 主任务ID已废弃保留用于兼容
@Column(name = "video_task_ids", columnDefinition = "TEXT")
private String videoTaskIds; // 多个视频任务IDJSON数组每张图片对应一个视频任务
@Column(name = "video_urls", columnDefinition = "LONGTEXT")
private String videoUrls; // 多个视频URLJSON数组用于拼接
@Column(columnDefinition = "TEXT")
private String errorMessage;
// 大模型优化后的提示词字段
@Column(name = "shot_list", columnDefinition = "TEXT")
private String shotList; // 镜头列表描述
@Column(name = "image_prompt", columnDefinition = "TEXT")
private String imagePrompt; // 生成分镜图的提示词
@Column(name = "video_prompt", columnDefinition = "TEXT")
private String videoPrompt; // 生成视频的提示词
@Column(nullable = false)
private int costPoints; // 消耗积分
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime updatedAt;
@Column
private LocalDateTime completedAt;
/**
* 任务来源类型AI_GENERATED = AI生成分镜图UPLOADED = 直接上传图片
*/
@Enumerated(EnumType.STRING)
@Column(name = "source_type", length = 20)
private SourceType sourceType = SourceType.AI_GENERATED;
public enum TaskStatus {
PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED
}
public enum SourceType {
AI_GENERATED, // AI生成分镜图
UPLOADED // 直接上传图片
}
// 构造函数
public StoryboardVideoTask() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public StoryboardVideoTask(String username, String prompt, String aspectRatio, boolean hdMode, Integer duration) {
this();
this.username = username;
this.prompt = prompt;
this.aspectRatio = aspectRatio;
this.hdMode = hdMode;
this.duration = duration != null ? duration : 10; // 默认10秒
// 计算消耗积分
this.costPoints = calculateCost();
}
// 保留旧的构造函数以保持向后兼容
public StoryboardVideoTask(String username, String prompt, String aspectRatio, boolean hdMode) {
this(username, prompt, aspectRatio, hdMode, 10);
}
/**
* 计算任务消耗积分
*/
private Integer calculateCost() {
int baseCost = 10; // 基础消耗
int hdCost = hdMode ? 20 : 0; // 高清模式消耗
return baseCost + hdCost;
}
/**
* 更新任务状态
*/
public void updateStatus(TaskStatus newStatus) {
this.status = newStatus;
this.updatedAt = LocalDateTime.now();
// 任务结束状态都应该设置完成时间
if (newStatus == TaskStatus.COMPLETED || newStatus == TaskStatus.FAILED || newStatus == TaskStatus.CANCELLED) {
this.completedAt = LocalDateTime.now();
}
}
/**
* 更新进度
*/
public void updateProgress(Integer progress) {
this.progress = Math.min(100, Math.max(0, progress));
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPrompt() { return prompt; }
public void setPrompt(String prompt) { this.prompt = prompt; }
public String getImageUrl() { return imageUrl; }
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
public String getAspectRatio() { return aspectRatio; }
public void setAspectRatio(String aspectRatio) { this.aspectRatio = aspectRatio; }
public boolean isHdMode() { return hdMode; }
public void setHdMode(boolean hdMode) { this.hdMode = hdMode; }
public boolean getHdMode() { return hdMode; } // 添加getHdMode方法以支持Boolean类型调用
public Integer getDuration() { return duration; }
public void setDuration(Integer duration) { this.duration = duration; }
public String getImageModel() { return imageModel; }
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; }
public void setProgress(int progress) { this.progress = progress; }
public String getResultUrl() { return resultUrl; }
public void setResultUrl(String resultUrl) { this.resultUrl = resultUrl; }
public String getStoryboardImages() { return storyboardImages; }
public void setStoryboardImages(String storyboardImages) { this.storyboardImages = storyboardImages; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public String getRealTaskId() { return realTaskId; }
public void setRealTaskId(String realTaskId) { this.realTaskId = realTaskId; }
public String getVideoTaskIds() { return videoTaskIds; }
public void setVideoTaskIds(String videoTaskIds) { this.videoTaskIds = videoTaskIds; }
public String getVideoUrls() { return videoUrls; }
public void setVideoUrls(String videoUrls) { this.videoUrls = videoUrls; }
public int getCostPoints() { return costPoints; }
public void setCostPoints(int costPoints) { this.costPoints = costPoints; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getCompletedAt() { return completedAt; }
public void setCompletedAt(LocalDateTime completedAt) { this.completedAt = completedAt; }
public SourceType getSourceType() { return sourceType; }
public void setSourceType(SourceType sourceType) { this.sourceType = sourceType; }
public String getShotList() { return shotList; }
public void setShotList(String shotList) { this.shotList = shotList; }
public String getImagePrompt() { return imagePrompt; }
public void setImagePrompt(String imagePrompt) { this.imagePrompt = imagePrompt; }
public String getVideoPrompt() { return videoPrompt; }
public void setVideoPrompt(String videoPrompt) { this.videoPrompt = videoPrompt; }
public String getVideoModel() { return videoModel; }
public void setVideoModel(String videoModel) { this.videoModel = videoModel; }
}

View File

@@ -0,0 +1,214 @@
package com.example.demo.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
@Entity
@Table(name = "system_settings")
public class SystemSettings {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 每次生成消耗的资源点数量 */
@NotNull
@Min(0)
@Column(nullable = false)
private Integer pointsPerGeneration = 1;
/** 站点名称 */
@Column(nullable = false, length = 100)
private String siteName = "AIGC Demo";
/** 站点副标题 */
@Column(nullable = false, length = 150)
private String siteSubtitle = "现代化的Spring Boot应用演示";
/** 是否开放注册 */
@NotNull
@Column(nullable = false)
private Boolean registrationOpen = true;
/** 维护模式开关 */
@NotNull
@Column(nullable = false)
private Boolean maintenanceMode = false;
/** 支付渠道开关 */
@NotNull
@Column(nullable = false)
private Boolean enableAlipay = true;
@NotNull
@Column(nullable = false)
private Boolean enablePaypal = true;
/** 联系邮箱 */
@Column(length = 120)
private String contactEmail = "support@example.com";
/** 优化提示词使用的模型 */
@Column(length = 50)
private String promptOptimizationModel = "gpt-5.1-thinking";
/** 分镜图生成系统引导词 */
@Column(length = 2000)
private String storyboardSystemPrompt = "";
/** Token过期时间小时范围1-720小时默认720小时(30天) */
@NotNull
@Min(1)
@Column(nullable = false)
private Integer tokenExpireHours = 720;
/** AI API密钥视频和图片生成共用 */
@Column(length = 200)
private String aiApiKey;
/** AI API基础URL */
@Column(length = 200)
private String aiApiBaseUrl;
/** 视频生成模型 */
@Column(name = "video_model", length = 50)
private String videoModel = "grok-video-3";
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getPointsPerGeneration() {
return pointsPerGeneration;
}
public void setPointsPerGeneration(Integer pointsPerGeneration) {
this.pointsPerGeneration = pointsPerGeneration;
}
public String getSiteName() {
return siteName;
}
public void setSiteName(String siteName) {
this.siteName = siteName;
}
public String getSiteSubtitle() {
return siteSubtitle;
}
public void setSiteSubtitle(String siteSubtitle) {
this.siteSubtitle = siteSubtitle;
}
public Boolean getRegistrationOpen() {
return registrationOpen;
}
public void setRegistrationOpen(Boolean registrationOpen) {
this.registrationOpen = registrationOpen;
}
public Boolean getMaintenanceMode() {
return maintenanceMode;
}
public void setMaintenanceMode(Boolean maintenanceMode) {
this.maintenanceMode = maintenanceMode;
}
public Boolean getEnableAlipay() {
return enableAlipay;
}
public void setEnableAlipay(Boolean enableAlipay) {
this.enableAlipay = enableAlipay;
}
public Boolean getEnablePaypal() {
return enablePaypal;
}
public void setEnablePaypal(Boolean enablePaypal) {
this.enablePaypal = enablePaypal;
}
public String getContactEmail() {
return contactEmail;
}
public void setContactEmail(String contactEmail) {
this.contactEmail = contactEmail;
}
public String getPromptOptimizationModel() {
return promptOptimizationModel;
}
public void setPromptOptimizationModel(String promptOptimizationModel) {
this.promptOptimizationModel = promptOptimizationModel;
}
public String getStoryboardSystemPrompt() {
return storyboardSystemPrompt;
}
public void setStoryboardSystemPrompt(String storyboardSystemPrompt) {
this.storyboardSystemPrompt = storyboardSystemPrompt;
}
public Integer getTokenExpireHours() {
return tokenExpireHours;
}
public void setTokenExpireHours(Integer tokenExpireHours) {
// 限制范围在1-720小时1小时到30天
if (tokenExpireHours != null && tokenExpireHours >= 1 && tokenExpireHours <= 720) {
this.tokenExpireHours = tokenExpireHours;
} else if (tokenExpireHours != null) {
// 值超出范围时,强制设置为边界值
if (tokenExpireHours < 1) {
this.tokenExpireHours = 1;
} else {
this.tokenExpireHours = 720;
}
}
}
public String getAiApiKey() {
return aiApiKey;
}
public void setAiApiKey(String aiApiKey) {
this.aiApiKey = aiApiKey;
}
public String getAiApiBaseUrl() {
return aiApiBaseUrl;
}
public void setAiApiBaseUrl(String aiApiBaseUrl) {
this.aiApiBaseUrl = aiApiBaseUrl;
}
public String getVideoModel() {
return videoModel;
}
public void setVideoModel(String videoModel) {
this.videoModel = videoModel;
}
}

View File

@@ -0,0 +1,288 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 任务队列实体
* 用于管理用户的视频生成任务队列
*/
@Entity
@Table(name = "task_queue")
public class TaskQueue {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, length = 100)
private String username;
@Column(name = "task_id", nullable = false, length = 50)
private String taskId;
@Enumerated(EnumType.STRING)
@Column(name = "task_type", nullable = false, length = 20)
private TaskType taskType;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private QueueStatus status;
@Column(name = "priority", nullable = false)
private Integer priority = 0; // 优先级,数字越小优先级越高
@Column(name = "real_task_id", length = 100)
private String realTaskId; // 外部API返回的真实任务ID
@Column(name = "last_check_time")
private LocalDateTime lastCheckTime; // 最后一次检查时间
@Column(name = "check_count", nullable = false)
private Integer checkCount = 0; // 检查次数
@Column(name = "max_check_count", nullable = false)
private Integer maxCheckCount = 5; // 最大检查次数5次 * 2分钟 = 10分钟
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Column(name = "completed_at")
private LocalDateTime completedAt;
/**
* 任务类型枚举
*/
public enum TaskType {
TEXT_TO_VIDEO("文生视频"),
IMAGE_TO_VIDEO("图生视频"),
STORYBOARD_VIDEO("分镜视频"),
STORYBOARD_IMAGE("分镜图");
private final String description;
TaskType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 队列状态枚举
*/
public enum QueueStatus {
PENDING("等待中"),
PROCESSING("处理中"),
COMPLETED("已完成"),
FAILED("失败"),
CANCELLED("已取消"),
TIMEOUT("超时");
private final String description;
QueueStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
// 构造函数
public TaskQueue() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public TaskQueue(String username, String taskId, TaskType taskType) {
this();
this.username = username;
this.taskId = taskId;
this.taskType = taskType;
this.status = QueueStatus.PENDING;
}
/**
* 更新状态
*/
public void updateStatus(QueueStatus newStatus) {
this.status = newStatus;
this.updatedAt = LocalDateTime.now();
if (newStatus == QueueStatus.COMPLETED ||
newStatus == QueueStatus.FAILED ||
newStatus == QueueStatus.CANCELLED ||
newStatus == QueueStatus.TIMEOUT) {
this.completedAt = LocalDateTime.now();
}
}
/**
* 增加检查次数
*/
public void incrementCheckCount() {
this.checkCount++;
this.lastCheckTime = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
/**
* 检查是否超时
*/
public boolean isTimeout() {
return this.checkCount >= this.maxCheckCount;
}
/**
* 检查是否可以处理
*/
public boolean canProcess() {
return this.status == QueueStatus.PENDING || this.status == QueueStatus.PROCESSING;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public TaskType getTaskType() {
return taskType;
}
public void setTaskType(TaskType taskType) {
this.taskType = taskType;
}
public QueueStatus getStatus() {
return status;
}
public void setStatus(QueueStatus status) {
this.status = status;
}
public Integer getPriority() {
return priority;
}
public void setPriority(Integer priority) {
this.priority = priority;
}
public String getRealTaskId() {
return realTaskId;
}
public void setRealTaskId(String realTaskId) {
this.realTaskId = realTaskId;
}
public LocalDateTime getLastCheckTime() {
return lastCheckTime;
}
public void setLastCheckTime(LocalDateTime lastCheckTime) {
this.lastCheckTime = lastCheckTime;
}
public Integer getCheckCount() {
return checkCount;
}
public void setCheckCount(Integer checkCount) {
this.checkCount = checkCount;
}
public Integer getMaxCheckCount() {
return maxCheckCount;
}
public void setMaxCheckCount(Integer maxCheckCount) {
this.maxCheckCount = maxCheckCount;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getCompletedAt() {
return completedAt;
}
public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}
}

View File

@@ -0,0 +1,277 @@
package com.example.demo.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "task_status")
public class TaskStatus {
public enum TaskType {
TEXT_TO_VIDEO("文生视频"),
IMAGE_TO_VIDEO("图生视频"),
STORYBOARD_VIDEO("分镜视频"),
STORYBOARD_IMAGE("分镜图");
private final String description;
TaskType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
public enum Status {
PENDING("待处理"),
PROCESSING("处理中"),
COMPLETED("已完成"),
FAILED("失败"),
CANCELLED("已取消"),
TIMEOUT("超时");
private final String description;
Status(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "task_id", nullable = false)
private String taskId;
@Column(name = "username", nullable = false)
private String username;
@Enumerated(EnumType.STRING)
@Column(name = "task_type", nullable = false)
private TaskType taskType;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private Status status = Status.PENDING;
@Column(name = "progress")
private Integer progress = 0;
@Column(name = "result_url", columnDefinition = "LONGTEXT")
private String resultUrl;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
@Column(name = "external_task_id")
private String externalTaskId;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "completed_at")
private LocalDateTime completedAt;
@Column(name = "last_polled_at")
private LocalDateTime lastPolledAt;
@Column(name = "poll_count")
private Integer pollCount = 0;
@Column(name = "max_polls")
private Integer maxPolls = 60; // 2小时每2分钟一次
// 构造函数
public TaskStatus() {}
public TaskStatus(String taskId, String username, TaskType taskType) {
this.taskId = taskId;
this.username = username;
this.taskType = taskType;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public TaskType getTaskType() {
return taskType;
}
public void setTaskType(TaskType taskType) {
this.taskType = taskType;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public Integer getProgress() {
return progress;
}
public void setProgress(Integer progress) {
this.progress = progress;
}
public String getResultUrl() {
return resultUrl;
}
public void setResultUrl(String resultUrl) {
this.resultUrl = resultUrl;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public String getExternalTaskId() {
return externalTaskId;
}
public void setExternalTaskId(String externalTaskId) {
this.externalTaskId = externalTaskId;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getCompletedAt() {
return completedAt;
}
public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}
public LocalDateTime getLastPolledAt() {
return lastPolledAt;
}
public void setLastPolledAt(LocalDateTime lastPolledAt) {
this.lastPolledAt = lastPolledAt;
}
public Integer getPollCount() {
return pollCount;
}
public void setPollCount(Integer pollCount) {
this.pollCount = pollCount;
}
public Integer getMaxPolls() {
return maxPolls;
}
public void setMaxPolls(Integer maxPolls) {
this.maxPolls = maxPolls;
}
// 业务方法
public void incrementPollCount() {
this.pollCount++;
this.lastPolledAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public boolean isPollingExpired() {
return pollCount >= maxPolls;
}
public void markAsCompleted(String resultUrl) {
this.status = Status.COMPLETED;
this.resultUrl = resultUrl;
this.progress = 100;
this.completedAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public void markAsFailed(String errorMessage) {
this.status = Status.FAILED;
this.errorMessage = errorMessage;
this.updatedAt = LocalDateTime.now();
}
public void markAsTimeout() {
this.status = Status.FAILED; // 超时也标记为 FAILED便于前端统一处理
this.errorMessage = "任务超时,超过最大轮询次数";
this.updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,165 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 文生视频任务实体
*/
@Entity
@Table(name = "text_to_video_tasks")
public class TextToVideoTask {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String taskId;
@Column(nullable = false, length = 100)
private String username; // 关联用户
@Column(columnDefinition = "TEXT")
private String prompt; // 文本描述
@Column(nullable = false, length = 10)
private String aspectRatio; // 3:2, 16:9, 4:3, 1:1, 3:4, 9:16, 2:3
@Column(nullable = false)
private int duration; // in seconds, e.g., 5, 10, 15, 30
@Column(nullable = false)
private boolean hdMode; // 是否高清模式
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private TaskStatus status;
@Column(nullable = false)
private int progress; // 0-100
@Column(name = "result_url", columnDefinition = "TEXT")
private String resultUrl;
@Column(name = "real_task_id")
private String realTaskId;
@Column(columnDefinition = "TEXT")
private String errorMessage;
@Column(nullable = false)
private int costPoints; // 消耗积分
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime updatedAt;
@Column
private LocalDateTime completedAt;
@Column(name = "video_model", length = 50)
private String videoModel = "grok-video-3";
public enum TaskStatus {
PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED
}
// 构造函数
public TextToVideoTask() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public TextToVideoTask(String username, String prompt, String aspectRatio, int duration, boolean hdMode) {
this();
this.username = username;
this.prompt = prompt;
this.aspectRatio = aspectRatio;
this.duration = duration;
this.hdMode = hdMode;
// 计算消耗积分
this.costPoints = calculateCost();
}
/**
* 计算任务消耗积分
*/
private Integer calculateCost() {
int actualDuration = duration <= 0 ? 5 : duration; // 使用默认时长但不修改字段
int baseCost = 15; // 文生视频基础消耗比图生视频高
int durationCost = actualDuration * 3; // 时长消耗
int hdCost = hdMode ? 25 : 0; // 高清模式消耗
return baseCost + durationCost + hdCost;
}
/**
* 更新任务状态
*/
public void updateStatus(TaskStatus newStatus) {
this.status = newStatus;
this.updatedAt = LocalDateTime.now();
// 任务结束状态都应该设置完成时间
if (newStatus == TaskStatus.COMPLETED || newStatus == TaskStatus.FAILED || newStatus == TaskStatus.CANCELLED) {
this.completedAt = LocalDateTime.now();
}
}
/**
* 更新进度
*/
public void updateProgress(Integer progress) {
this.progress = Math.min(100, Math.max(0, progress));
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPrompt() { return prompt; }
public void setPrompt(String prompt) { this.prompt = prompt; }
public String getAspectRatio() { return aspectRatio; }
public void setAspectRatio(String aspectRatio) { this.aspectRatio = aspectRatio; }
public int getDuration() { return duration; }
public void setDuration(int duration) { this.duration = duration; }
public boolean isHdMode() { return hdMode; }
public void setHdMode(boolean hdMode) { this.hdMode = hdMode; }
public TaskStatus getStatus() { return status; }
public void setStatus(TaskStatus status) { this.status = status; }
public int getProgress() { return progress; }
public void setProgress(int progress) { this.progress = progress; }
public String getResultUrl() { return resultUrl; }
public void setResultUrl(String resultUrl) { this.resultUrl = resultUrl; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public String getRealTaskId() { return realTaskId; }
public void setRealTaskId(String realTaskId) { this.realTaskId = realTaskId; }
public int getCostPoints() { return costPoints; }
public void setCostPoints(int costPoints) { this.costPoints = costPoints; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getCompletedAt() { return completedAt; }
public void setCompletedAt(LocalDateTime completedAt) { this.completedAt = completedAt; }
public String getVideoModel() { return videoModel; }
public void setVideoModel(String videoModel) { this.videoModel = videoModel; }
}

View File

@@ -0,0 +1,284 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", unique = true, length = 20)
private String userId; // 业务用户ID格式: UID + yyMMdd + 4位随机字符
@NotBlank
@Size(min = 3, max = 50)
@Column(nullable = false, unique = true, length = 50)
private String username;
@NotBlank
@Email
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = true)
private String passwordHash;
@NotBlank
@Column(nullable = false, length = 30)
private String role = "ROLE_USER";
@Min(0)
@Column(nullable = false)
private Integer points = 50; // 默认50积分
@Min(0)
@Column(nullable = false)
private Integer frozenPoints = 0; // 冻结积分
@Column(name = "phone", length = 20)
private String phone;
@Column(name = "avatar", columnDefinition = "TEXT")
private String avatar;
@Column(name = "nickname", length = 100)
private String nickname;
@Column(name = "gender", length = 10)
private String gender;
@Column(name = "birthday")
private java.time.LocalDate birthday;
@Column(name = "address", columnDefinition = "TEXT")
private String address;
@Column(name = "bio", columnDefinition = "TEXT")
private String bio; // 个人简介
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
@Column(name = "last_active_time")
private LocalDateTime lastActiveTime;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
// 自动生成业务用户ID如果未设置
if (userId == null || userId.isEmpty()) {
userId = com.example.demo.util.UserIdGenerator.generate();
}
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
if (updatedAt == null) {
updatedAt = LocalDateTime.now();
}
if (points == null) {
points = 50; // 默认50积分
}
if (frozenPoints == null) {
frozenPoints = 0; // 默认冻结积分为0
}
if (role == null || role.isEmpty()) {
role = "ROLE_USER"; // 默认角色
}
if (isActive == null) {
isActive = true; // 默认激活
}
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public Integer getPoints() {
return points;
}
public void setPoints(Integer points) {
this.points = points;
}
public Integer getFrozenPoints() {
return frozenPoints;
}
public void setFrozenPoints(Integer frozenPoints) {
this.frozenPoints = frozenPoints;
}
/**
* 获取可用积分(总积分 - 冻结积分)
*/
public Integer getAvailablePoints() {
return Math.max(0, points - frozenPoints);
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public java.time.LocalDate getBirthday() {
return birthday;
}
public void setBirthday(java.time.LocalDate birthday) {
this.birthday = birthday;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getBio() {
return bio;
}
public void setBio(String bio) {
this.bio = bio;
}
public Boolean getIsActive() {
return isActive;
}
public void setIsActive(Boolean isActive) {
this.isActive = isActive;
}
public LocalDateTime getLastLoginAt() {
return lastLoginAt;
}
public void setLastLoginAt(LocalDateTime lastLoginAt) {
this.lastLoginAt = lastLoginAt;
}
public LocalDateTime getLastActiveTime() {
return lastActiveTime;
}
public void setLastActiveTime(LocalDateTime lastActiveTime) {
this.lastActiveTime = lastActiveTime;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,84 @@
package com.example.demo.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_activity_stats")
public class UserActivityStats {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "activity_date", nullable = false, unique = true)
private LocalDate activityDate;
@Column(name = "daily_active_users", nullable = false)
private Integer dailyActiveUsers = 0;
@Column(name = "monthly_active_users", nullable = false)
private Integer monthlyActiveUsers = 0;
@Column(name = "new_users", nullable = false)
private Integer newUsers = 0;
@Column(name = "returning_users", nullable = false)
private Integer returningUsers = 0;
@Column(name = "session_count", nullable = false)
private Integer sessionCount = 0;
@Column(name = "avg_session_duration", precision = 10, scale = 2)
private BigDecimal avgSessionDuration = BigDecimal.ZERO;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public LocalDate getActivityDate() { return activityDate; }
public void setActivityDate(LocalDate activityDate) { this.activityDate = activityDate; }
public Integer getDailyActiveUsers() { return dailyActiveUsers; }
public void setDailyActiveUsers(Integer dailyActiveUsers) { this.dailyActiveUsers = dailyActiveUsers; }
public Integer getMonthlyActiveUsers() { return monthlyActiveUsers; }
public void setMonthlyActiveUsers(Integer monthlyActiveUsers) { this.monthlyActiveUsers = monthlyActiveUsers; }
public Integer getNewUsers() { return newUsers; }
public void setNewUsers(Integer newUsers) { this.newUsers = newUsers; }
public Integer getReturningUsers() { return returningUsers; }
public void setReturningUsers(Integer returningUsers) { this.returningUsers = returningUsers; }
public Integer getSessionCount() { return sessionCount; }
public void setSessionCount(Integer sessionCount) { this.sessionCount = sessionCount; }
public BigDecimal getAvgSessionDuration() { return avgSessionDuration; }
public void setAvgSessionDuration(BigDecimal avgSessionDuration) { this.avgSessionDuration = avgSessionDuration; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,292 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
/**
* 用户错误日志实体
* 用于记录和统计用户操作过程中产生的错误
*/
@Entity
@Table(name = "user_error_log", indexes = {
@Index(name = "idx_username", columnList = "username"),
@Index(name = "idx_error_type", columnList = "error_type"),
@Index(name = "idx_created_at", columnList = "created_at"),
@Index(name = "idx_error_source", columnList = "error_source")
})
public class UserErrorLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", length = 100)
private String username; // 可能为空(未登录用户)
@Enumerated(EnumType.STRING)
@Column(name = "error_type", nullable = false, length = 50)
private ErrorType errorType;
@Column(name = "error_code", length = 50)
private String errorCode;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
@Column(name = "error_source", nullable = false, length = 100)
private String errorSource; // 错误来源(服务类名或接口路径)
@Column(name = "task_id", length = 100)
private String taskId; // 关联的任务ID如果有
@Column(name = "task_type", length = 50)
private String taskType; // 任务类型
@Column(name = "request_path", length = 500)
private String requestPath; // 请求路径
@Column(name = "request_method", length = 10)
private String requestMethod; // 请求方法 GET/POST等
@Column(name = "request_params", columnDefinition = "TEXT")
private String requestParams; // 请求参数JSON格式敏感信息需脱敏
@Column(name = "stack_trace", columnDefinition = "TEXT")
private String stackTrace; // 堆栈跟踪(可选,用于调试)
@Column(name = "ip_address", length = 50)
private String ipAddress;
@Column(name = "user_agent", length = 500)
private String userAgent;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
/**
* 错误类型枚举
*/
public enum ErrorType {
// 任务相关错误
TASK_SUBMIT_ERROR("任务提交失败"),
TASK_PROCESSING_ERROR("任务处理失败"),
TASK_TIMEOUT("任务超时"),
TASK_CANCELLED("任务取消"),
// API相关错误
API_CALL_ERROR("API调用失败"),
API_RESPONSE_ERROR("API响应异常"),
API_TIMEOUT("API超时"),
// 支付相关错误
PAYMENT_ERROR("支付失败"),
PAYMENT_CALLBACK_ERROR("支付回调异常"),
REFUND_ERROR("退款失败"),
// 认证相关错误
AUTH_ERROR("认证失败"),
TOKEN_EXPIRED("Token过期"),
PERMISSION_DENIED("权限不足"),
// 数据相关错误
DATA_VALIDATION_ERROR("数据验证失败"),
DATA_NOT_FOUND("数据未找到"),
DATA_CONFLICT("数据冲突"),
// 文件相关错误
FILE_UPLOAD_ERROR("文件上传失败"),
FILE_DOWNLOAD_ERROR("文件下载失败"),
FILE_PROCESS_ERROR("文件处理失败"),
// 系统错误
SYSTEM_ERROR("系统错误"),
DATABASE_ERROR("数据库错误"),
NETWORK_ERROR("网络错误"),
// 其他
UNKNOWN("未知错误");
private final String description;
ErrorType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
// 构造函数
public UserErrorLog() {
this.createdAt = LocalDateTime.now();
}
public UserErrorLog(String username, ErrorType errorType, String errorMessage, String errorSource) {
this();
this.username = username;
this.errorType = errorType;
this.errorMessage = errorMessage;
this.errorSource = errorSource;
}
// 静态工厂方法 - 快速创建任务错误日志
public static UserErrorLog createTaskError(String username, String taskId, String taskType,
ErrorType errorType, String errorMessage) {
UserErrorLog log = new UserErrorLog(username, errorType, errorMessage, "TaskService");
log.setTaskId(taskId);
log.setTaskType(taskType);
return log;
}
// 静态工厂方法 - 快速创建API错误日志
public static UserErrorLog createApiError(String username, String requestPath,
String errorMessage, String errorCode) {
UserErrorLog log = new UserErrorLog(username, ErrorType.API_CALL_ERROR, errorMessage, "ApiService");
log.setRequestPath(requestPath);
log.setErrorCode(errorCode);
return log;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public ErrorType getErrorType() {
return errorType;
}
public void setErrorType(ErrorType errorType) {
this.errorType = errorType;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public String getErrorSource() {
return errorSource;
}
public void setErrorSource(String errorSource) {
this.errorSource = errorSource;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public String getRequestPath() {
return requestPath;
}
public void setRequestPath(String requestPath) {
this.requestPath = requestPath;
}
public String getRequestMethod() {
return requestMethod;
}
public void setRequestMethod(String requestMethod) {
this.requestMethod = requestMethod;
}
public String getRequestParams() {
return requestParams;
}
public void setRequestParams(String requestParams) {
this.requestParams = requestParams;
}
public String getStackTrace() {
return stackTrace;
}
public void setStackTrace(String stackTrace) {
this.stackTrace = stackTrace;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
@Override
public String toString() {
return "UserErrorLog{" +
"id=" + id +
", username='" + username + '\'' +
", errorType=" + errorType +
", errorCode='" + errorCode + '\'' +
", errorSource='" + errorSource + '\'' +
", createdAt=" + createdAt +
'}';
}
}

View File

@@ -0,0 +1,118 @@
package com.example.demo.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_memberships")
public class UserMembership {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "membership_level_id", nullable = false)
private Long membershipLevelId;
@Column(name = "start_date", nullable = false)
private LocalDateTime startDate = LocalDateTime.now();
@Column(name = "end_date", nullable = false)
private LocalDateTime endDate;
@Column(name = "status", nullable = false)
private String status = "ACTIVE";
@Column(name = "auto_renew", nullable = false)
private Boolean autoRenew = false;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// 构造函数
public UserMembership() {}
public UserMembership(Long userId, Long membershipLevelId, LocalDateTime endDate) {
this.userId = userId;
this.membershipLevelId = membershipLevelId;
this.endDate = endDate;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getMembershipLevelId() {
return membershipLevelId;
}
public void setMembershipLevelId(Long membershipLevelId) {
this.membershipLevelId = membershipLevelId;
}
public LocalDateTime getStartDate() {
return startDate;
}
public void setStartDate(LocalDateTime startDate) {
this.startDate = startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
public void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Boolean getAutoRenew() {
return autoRenew;
}
public void setAutoRenew(Boolean autoRenew) {
this.autoRenew = autoRenew;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,443 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Basic;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 用户作品实体
* 记录用户生成的视频作品
*/
@Entity
@Table(name = "user_works")
public class UserWork {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "username", nullable = false, length = 100)
private String username;
@Column(name = "task_id", nullable = false, length = 50, unique = true)
private String taskId;
@Enumerated(EnumType.STRING)
@Column(name = "work_type", nullable = false, length = 20)
private WorkType workType;
@Column(name = "title", length = 200)
private String title; // 作品标题
@Column(name = "description", columnDefinition = "TEXT")
private String description; // 作品描述
@Column(name = "prompt", columnDefinition = "TEXT")
private String prompt; // 生成提示词
@Column(name = "image_prompt", columnDefinition = "TEXT")
private String imagePrompt; // 优化后的分镜图提示词
@Column(name = "video_prompt", columnDefinition = "TEXT")
private String videoPrompt; // 优化后的视频提示词
@Column(name = "result_url", columnDefinition = "LONGTEXT")
private String resultUrl; // 结果视频URL
@Column(name = "thumbnail_url", columnDefinition = "LONGTEXT")
private String thumbnailUrl; // 缩略图URL
@Column(name = "duration", length = 10)
private String duration; // 视频时长
@Column(name = "aspect_ratio", length = 10)
private String aspectRatio; // 宽高比
@Column(name = "quality", length = 20)
private String quality; // 画质 (HD/SD)
@Column(name = "file_size", length = 20)
private String fileSize; // 文件大小
@Column(name = "points_cost", nullable = false)
private Integer pointsCost; // 消耗积分
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private WorkStatus status; // 作品状态
@Column(name = "is_public", nullable = false)
private Boolean isPublic = false; // 是否公开
@Column(name = "view_count", nullable = false)
private Integer viewCount = 0; // 浏览次数
@Column(name = "like_count", nullable = false)
private Integer likeCount = 0; // 点赞次数
@Column(name = "download_count", nullable = false)
private Integer downloadCount = 0; // 下载次数
@Column(name = "tags", length = 500)
private String tags; // 标签,用逗号分隔
@Basic(fetch = FetchType.LAZY)
@Column(name = "uploaded_images", columnDefinition = "LONGTEXT")
private String uploadedImages; // 用户上传的参考图片JSON数组用于"做同款"功能恢复(分镜图阶段)
@Basic(fetch = FetchType.LAZY)
@Column(name = "video_reference_images", columnDefinition = "LONGTEXT")
private String videoReferenceImages; // 视频阶段用户上传的参考图片JSON数组用于分镜视频"做同款"功能恢复
@Column(name = "video_model", length = 50)
private String videoModel = "grok-video-3";
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Column(name = "completed_at")
private LocalDateTime completedAt;
/**
* 作品类型枚举
*/
public enum WorkType {
TEXT_TO_VIDEO("文生视频"),
IMAGE_TO_VIDEO("图生视频"),
STORYBOARD_VIDEO("分镜视频"),
STORYBOARD_IMAGE("分镜图");
private final String description;
WorkType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 作品状态枚举
*/
public enum WorkStatus {
PENDING("排队中"),
PROCESSING("处理中"),
COMPLETED("已完成"),
FAILED("失败"),
DELETED("已删除");
private final String description;
WorkStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
// 构造函数
public UserWork() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public UserWork(String username, String taskId, WorkType workType, String prompt, String resultUrl) {
this();
this.username = username;
this.taskId = taskId;
this.workType = workType;
this.prompt = prompt;
this.resultUrl = resultUrl;
this.status = WorkStatus.COMPLETED;
this.completedAt = LocalDateTime.now();
}
/**
* 更新状态
*/
public void updateStatus(WorkStatus newStatus) {
this.status = newStatus;
this.updatedAt = LocalDateTime.now();
if (newStatus == WorkStatus.COMPLETED) {
this.completedAt = LocalDateTime.now();
}
}
/**
* 增加浏览次数
*/
public void incrementViewCount() {
this.viewCount++;
this.updatedAt = LocalDateTime.now();
}
/**
* 增加点赞次数
*/
public void incrementLikeCount() {
this.likeCount++;
this.updatedAt = LocalDateTime.now();
}
/**
* 增加下载次数
*/
public void incrementDownloadCount() {
this.downloadCount++;
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public WorkType getWorkType() {
return workType;
}
public void setWorkType(WorkType workType) {
this.workType = workType;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getPrompt() {
return prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public String getImagePrompt() {
return imagePrompt;
}
public void setImagePrompt(String imagePrompt) {
this.imagePrompt = imagePrompt;
}
public String getVideoPrompt() {
return videoPrompt;
}
public void setVideoPrompt(String videoPrompt) {
this.videoPrompt = videoPrompt;
}
public String getResultUrl() {
return resultUrl;
}
public void setResultUrl(String resultUrl) {
this.resultUrl = resultUrl;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
public String getDuration() {
return duration;
}
public void setDuration(String duration) {
this.duration = duration;
}
public String getAspectRatio() {
return aspectRatio;
}
public void setAspectRatio(String aspectRatio) {
this.aspectRatio = aspectRatio;
}
public String getQuality() {
return quality;
}
public void setQuality(String quality) {
this.quality = quality;
}
public String getFileSize() {
return fileSize;
}
public void setFileSize(String fileSize) {
this.fileSize = fileSize;
}
public Integer getPointsCost() {
return pointsCost;
}
public void setPointsCost(Integer pointsCost) {
this.pointsCost = pointsCost;
}
public WorkStatus getStatus() {
return status;
}
public void setStatus(WorkStatus status) {
this.status = status;
}
public Boolean getIsPublic() {
return isPublic;
}
public void setIsPublic(Boolean isPublic) {
this.isPublic = isPublic;
}
public Integer getViewCount() {
return viewCount;
}
public void setViewCount(Integer viewCount) {
this.viewCount = viewCount;
}
public Integer getLikeCount() {
return likeCount;
}
public void setLikeCount(Integer likeCount) {
this.likeCount = likeCount;
}
public Integer getDownloadCount() {
return downloadCount;
}
public void setDownloadCount(Integer downloadCount) {
this.downloadCount = downloadCount;
}
public String getTags() {
return tags;
}
public void setTags(String tags) {
this.tags = tags;
}
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 LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getCompletedAt() {
return completedAt;
}
public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}
public String getVideoModel() {
return videoModel;
}
public void setVideoModel(String videoModel) {
this.videoModel = videoModel;
}
}

View File

@@ -0,0 +1,86 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
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;
import com.example.demo.model.CompletedTaskArchive;
/**
* 成功任务归档Repository
*/
@Repository
public interface CompletedTaskArchiveRepository extends JpaRepository<CompletedTaskArchive, Long> {
/**
* 根据用户名查找归档任务
*/
List<CompletedTaskArchive> findByUsernameOrderByArchivedAtDesc(String username);
/**
* 根据用户名分页查找归档任务
*/
Page<CompletedTaskArchive> findByUsernameOrderByArchivedAtDesc(String username, Pageable pageable);
/**
* 根据任务类型查找归档任务
*/
List<CompletedTaskArchive> findByTaskTypeOrderByArchivedAtDesc(String taskType);
/**
* 根据用户名和任务类型查找归档任务
*/
List<CompletedTaskArchive> findByUsernameAndTaskTypeOrderByArchivedAtDesc(String username, String taskType);
/**
* 统计用户归档任务数量
*/
long countByUsername(String username);
/**
* 统计任务类型归档数量
*/
long countByTaskType(String taskType);
/**
* 查找指定时间范围内的归档任务
*/
@Query("SELECT c FROM CompletedTaskArchive c WHERE c.archivedAt BETWEEN :startDate AND :endDate ORDER BY c.archivedAt DESC")
List<CompletedTaskArchive> findByArchivedAtBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
/**
* 查找指定时间范围内的归档任务(分页)
*/
@Query("SELECT c FROM CompletedTaskArchive c WHERE c.archivedAt BETWEEN :startDate AND :endDate ORDER BY c.archivedAt DESC")
Page<CompletedTaskArchive> findByArchivedAtBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
Pageable pageable);
/**
* 统计指定时间范围内的归档任务数量
*/
@Query("SELECT COUNT(c) FROM CompletedTaskArchive c WHERE c.archivedAt BETWEEN :startDate AND :endDate")
long countByArchivedAtBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
/**
* 查找超过指定天数的归档任务
*/
@Query("SELECT c FROM CompletedTaskArchive c WHERE c.archivedAt < :cutoffDate")
List<CompletedTaskArchive> findOlderThan(@Param("cutoffDate") LocalDateTime cutoffDate);
/**
* 删除超过指定天数的归档任务
*/
@Modifying
@Query("DELETE FROM CompletedTaskArchive c WHERE c.archivedAt < :cutoffDate")
int deleteOlderThan(@Param("cutoffDate") LocalDateTime cutoffDate);
}

View File

@@ -0,0 +1,86 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
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;
import com.example.demo.model.FailedTaskCleanupLog;
/**
* 失败任务清理日志Repository
*/
@Repository
public interface FailedTaskCleanupLogRepository extends JpaRepository<FailedTaskCleanupLog, Long> {
/**
* 根据用户名查找清理日志
*/
List<FailedTaskCleanupLog> findByUsernameOrderByCleanedAtDesc(String username);
/**
* 根据用户名分页查找清理日志
*/
Page<FailedTaskCleanupLog> findByUsernameOrderByCleanedAtDesc(String username, Pageable pageable);
/**
* 根据任务类型查找清理日志
*/
List<FailedTaskCleanupLog> findByTaskTypeOrderByCleanedAtDesc(String taskType);
/**
* 根据用户名和任务类型查找清理日志
*/
List<FailedTaskCleanupLog> findByUsernameAndTaskTypeOrderByCleanedAtDesc(String username, String taskType);
/**
* 统计用户清理日志数量
*/
long countByUsername(String username);
/**
* 统计任务类型清理日志数量
*/
long countByTaskType(String taskType);
/**
* 查找指定时间范围内的清理日志
*/
@Query("SELECT f FROM FailedTaskCleanupLog f WHERE f.cleanedAt BETWEEN :startDate AND :endDate ORDER BY f.cleanedAt DESC")
List<FailedTaskCleanupLog> findByCleanedAtBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
/**
* 查找指定时间范围内的清理日志(分页)
*/
@Query("SELECT f FROM FailedTaskCleanupLog f WHERE f.cleanedAt BETWEEN :startDate AND :endDate ORDER BY f.cleanedAt DESC")
Page<FailedTaskCleanupLog> findByCleanedAtBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
Pageable pageable);
/**
* 统计指定时间范围内的清理日志数量
*/
@Query("SELECT COUNT(f) FROM FailedTaskCleanupLog f WHERE f.cleanedAt BETWEEN :startDate AND :endDate")
long countByCleanedAtBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
/**
* 查找超过指定天数的清理日志
*/
@Query("SELECT f FROM FailedTaskCleanupLog f WHERE f.cleanedAt < :cutoffDate")
List<FailedTaskCleanupLog> findOlderThan(@Param("cutoffDate") LocalDateTime cutoffDate);
/**
* 删除超过指定天数的清理日志
*/
@Modifying
@Query("DELETE FROM FailedTaskCleanupLog f WHERE f.cleanedAt < :cutoffDate")
int deleteOlderThan(@Param("cutoffDate") LocalDateTime cutoffDate);
}

View File

@@ -0,0 +1,107 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
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;
import com.example.demo.model.ImageToVideoTask;
/**
* 图生视频任务数据访问层
*/
@Repository
public interface ImageToVideoTaskRepository extends JpaRepository<ImageToVideoTask, Long> {
/**
* 根据任务ID查找任务
*/
Optional<ImageToVideoTask> findByTaskId(String taskId);
/**
* 根据用户名查找任务列表(分页)
*/
Page<ImageToVideoTask> findByUsernameOrderByCreatedAtDesc(String username, Pageable pageable);
/**
* 根据用户名查找任务列表
*/
List<ImageToVideoTask> findByUsernameOrderByCreatedAtDesc(String username);
/**
* 统计用户任务数量
*/
long countByUsername(String username);
/**
* 统计用户进行中的任务数量PENDING 或 PROCESSING
*/
@Query("SELECT COUNT(t) FROM ImageToVideoTask t WHERE t.username = :username AND t.status IN ('PENDING', 'PROCESSING')")
long countProcessingTasksByUsername(@Param("username") String username);
/**
* 根据状态查找任务列表
*/
List<ImageToVideoTask> findByStatus(ImageToVideoTask.TaskStatus status);
/**
* 根据用户名和状态查找任务列表
*/
List<ImageToVideoTask> findByUsernameAndStatus(String username, ImageToVideoTask.TaskStatus status);
/**
* 查找需要处理的任务状态为PENDING或PROCESSING
*/
@Query("SELECT t FROM ImageToVideoTask t WHERE t.status IN ('PENDING', 'PROCESSING') ORDER BY t.createdAt ASC")
List<ImageToVideoTask> findPendingTasks();
/**
* 查找指定状态的任务列表
*/
@Query("SELECT t FROM ImageToVideoTask t WHERE t.status = :status ORDER BY t.createdAt DESC")
List<ImageToVideoTask> findByStatusOrderByCreatedAtDesc(@Param("status") ImageToVideoTask.TaskStatus status);
/**
* 统计用户各状态任务数量
*/
@Query("SELECT t.status, COUNT(t) FROM ImageToVideoTask t WHERE t.username = :username GROUP BY t.status")
List<Object[]> countTasksByStatus(@Param("username") String username);
/**
* 查找用户最近的任务
*/
@Query("SELECT t FROM ImageToVideoTask t WHERE t.username = :username ORDER BY t.createdAt DESC")
List<ImageToVideoTask> findRecentTasksByUsername(@Param("username") String username, Pageable pageable);
/**
* 删除过期的任务超过30天且已完成或失败
*/
@Modifying
@Query("DELETE FROM ImageToVideoTask t WHERE t.createdAt < :expiredDate AND t.status IN ('COMPLETED', 'FAILED', 'CANCELLED')")
int deleteExpiredTasks(@Param("expiredDate") java.time.LocalDateTime expiredDate);
/**
* 根据状态删除任务
*/
@Modifying
@Query("DELETE FROM ImageToVideoTask t WHERE t.status = :status")
int deleteByStatus(@Param("status") String status);
/**
* 查找超时的图生视频任务
* 条件状态为PROCESSING且创建时间超过10分钟
*/
@Query("SELECT t FROM ImageToVideoTask t WHERE t.status = :status " +
"AND t.createdAt < :timeoutTime")
List<ImageToVideoTask> findTimeoutTasks(
@Param("status") ImageToVideoTask.TaskStatus status,
@Param("timeoutTime") LocalDateTime timeoutTime
);
}

View File

@@ -0,0 +1,20 @@
package com.example.demo.repository;
import com.example.demo.model.MembershipLevel;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface MembershipLevelRepository extends JpaRepository<MembershipLevel, Long> {
Optional<MembershipLevel> findByDisplayName(String displayName);
Optional<MembershipLevel> findByName(String name);
@Query("SELECT ml.displayName as levelName, COUNT(um) as userCount " +
"FROM MembershipLevel ml LEFT JOIN UserMembership um ON ml.id = um.membershipLevelId AND um.status = 'ACTIVE' " +
"GROUP BY ml.id, ml.displayName")
List<java.util.Map<String, Object>> findMembershipStats();
}

View File

@@ -0,0 +1,44 @@
package com.example.demo.repository;
import com.example.demo.model.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
/**
* 根据订单ID查找订单项
*/
List<OrderItem> findByOrderId(Long orderId);
/**
* 根据产品SKU查找订单项
*/
List<OrderItem> findByProductSku(String productSku);
/**
* 根据产品名称模糊查询订单项
*/
@Query("SELECT oi FROM OrderItem oi WHERE oi.productName LIKE %:productName%")
List<OrderItem> findByProductNameContaining(@Param("productName") String productName);
/**
* 统计指定产品的销售数量
*/
@Query("SELECT SUM(oi.quantity) FROM OrderItem oi WHERE oi.productSku = :productSku")
Long sumQuantityByProductSku(@Param("productSku") String productSku);
/**
* 统计指定产品的销售金额
*/
@Query("SELECT SUM(oi.subtotal) FROM OrderItem oi WHERE oi.productSku = :productSku")
java.math.BigDecimal sumSubtotalByProductSku(@Param("productSku") String productSku);
}

View File

@@ -0,0 +1,268 @@
package com.example.demo.repository;
import com.example.demo.model.Order;
import com.example.demo.model.OrderStatus;
import com.example.demo.model.OrderType;
import com.example.demo.model.User;
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.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
/**
* 根据ID查找订单并加载用户信息
*/
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.id = :id")
Optional<Order> findByIdWithUser(@Param("id") Long id);
/**
* 根据订单号查找订单
*/
Optional<Order> findByOrderNumber(String orderNumber);
/**
* 根据用户ID查找订单
*/
List<Order> findByUserId(Long userId);
/**
* 根据用户ID分页查找订单
*/
Page<Order> findByUserId(Long userId, Pageable pageable);
/**
* 根据订单状态查找订单
*/
List<Order> findByStatus(OrderStatus status);
/**
* 根据订单状态分页查找订单
*/
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
/**
* 根据订单类型查找订单
*/
List<Order> findByOrderType(OrderType orderType);
/**
* 根据用户ID和订单状态查找订单
*/
List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);
/**
* 根据创建时间范围查找订单
*/
List<Order> findByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
/**
* 根据用户ID和创建时间范围查找订单
*/
List<Order> findByUserIdAndCreatedAtBetween(Long userId, LocalDateTime startDate, LocalDateTime endDate);
/**
* 统计用户订单数量
*/
long countByUserId(Long userId);
/**
* 统计指定状态的订单数量
*/
long countByStatus(OrderStatus status);
/**
* 统计用户指定状态的订单数量
*/
long countByUserIdAndStatus(Long userId, OrderStatus status);
/**
* 查找用户的最近订单
*/
@Query("SELECT o FROM Order o WHERE o.user.id = :userId ORDER BY o.createdAt DESC")
List<Order> findRecentOrdersByUserId(@Param("userId") Long userId, Pageable pageable);
/**
* 查找待支付的订单
*/
@Query("SELECT o FROM Order o WHERE o.status IN ('PENDING', 'CONFIRMED') AND o.createdAt < :expireTime")
List<Order> findExpiredPendingOrders(@Param("expireTime") LocalDateTime expireTime);
/**
* 根据订单号模糊查询
*/
@Query("SELECT o FROM Order o WHERE o.orderNumber LIKE %:orderNumber%")
List<Order> findByOrderNumberContaining(@Param("orderNumber") String orderNumber);
/**
* 根据用户邮箱查找订单
*/
@Query("SELECT o FROM Order o WHERE o.contactEmail = :email")
List<Order> findByContactEmail(@Param("email") String email);
/**
* 根据用户手机号查找订单
*/
@Query("SELECT o FROM Order o WHERE o.contactPhone = :phone")
List<Order> findByContactPhone(@Param("phone") String phone);
/**
* 统计指定时间范围内的订单数量
*/
@Query("SELECT COUNT(o) FROM Order o WHERE o.createdAt BETWEEN :startDate AND :endDate")
long countByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
/**
* 统计指定时间范围内的订单总金额
*/
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.createdAt BETWEEN :startDate AND :endDate AND o.status = 'COMPLETED'")
BigDecimal sumTotalAmountByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
/**
* 查找需要自动取消的订单超过指定时间未支付的PENDING订单
*/
List<Order> findByStatusAndCreatedAtBefore(OrderStatus status, LocalDateTime cancelTime);
// 新增的查询方法
/**
* 根据状态和订单号模糊查询(分页)
*/
Page<Order> findByStatusAndOrderNumberContainingIgnoreCase(OrderStatus status, String orderNumber, Pageable pageable);
/**
* 根据订单号模糊查询(分页)
*/
Page<Order> findByOrderNumberContainingIgnoreCase(String orderNumber, Pageable pageable);
/**
* 根据用户查找订单(分页)
*/
Page<Order> findByUser(User user, Pageable pageable);
/**
* 根据用户和状态查找订单(分页)
*/
Page<Order> findByUserAndStatus(User user, OrderStatus status, Pageable pageable);
/**
* 根据用户和订单号模糊查询(分页)
*/
Page<Order> findByUserAndOrderNumberContainingIgnoreCase(User user, String orderNumber, Pageable pageable);
/**
* 根据用户、状态和订单号模糊查询(分页)
*/
Page<Order> findByUserAndStatusAndOrderNumberContainingIgnoreCase(User user, OrderStatus status, String orderNumber, Pageable pageable);
/**
* 统计指定时间之后的订单数量
*/
long countByCreatedAtAfter(LocalDateTime dateTime);
/**
* 统计用户指定时间之后的订单数量
*/
long countByUserAndCreatedAtAfter(User user, LocalDateTime dateTime);
/**
* 统计用户的订单数量
*/
long countByUser(User user);
/**
* 统计用户指定状态的订单数量
*/
long countByUserAndStatus(User user, OrderStatus status);
/**
* 统计指定状态的订单总金额
*/
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.status = :status")
BigDecimal sumTotalAmountByStatus(@Param("status") OrderStatus status);
/**
* 统计用户指定状态的订单总金额
*/
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.user = :user AND o.status = :status")
BigDecimal sumTotalAmountByUserAndStatus(@Param("user") User user, @Param("status") OrderStatus status);
/**
* 获取最近的订单(用于仪表盘)
*/
@Query("SELECT o FROM Order o ORDER BY o.createdAt DESC")
List<Order> findRecentOrders(org.springframework.data.domain.Pageable pageable);
/**
* 统计指定时间范围内有订单的不同用户数(日活用户)
*/
@Query("SELECT COUNT(DISTINCT o.user.id) FROM Order o WHERE o.createdAt BETWEEN :startTime AND :endTime")
long countDistinctUsersByCreatedAtBetween(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
// ============ 使用 JOIN FETCH 预加载 User 的查询方法 ============
/**
* 分页查找所有订单预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user",
countQuery = "SELECT COUNT(o) FROM Order o")
Page<Order> findAllWithUser(Pageable pageable);
/**
* 根据状态分页查找订单预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.status = :status",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.status = :status")
Page<Order> findByStatusWithUser(@Param("status") OrderStatus status, Pageable pageable);
/**
* 根据订单号模糊查询分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
countQuery = "SELECT COUNT(o) FROM Order o WHERE LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
Page<Order> findByOrderNumberContainingIgnoreCaseWithUser(@Param("orderNumber") String orderNumber, Pageable pageable);
/**
* 根据状态和订单号模糊查询分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
Page<Order> findByStatusAndOrderNumberContainingIgnoreCaseWithUser(@Param("status") OrderStatus status, @Param("orderNumber") String orderNumber, Pageable pageable);
/**
* 根据用户查找订单分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user")
Page<Order> findByUserWithUser(@Param("user") User user, Pageable pageable);
/**
* 根据用户和状态查找订单分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND o.status = :status",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND o.status = :status")
Page<Order> findByUserAndStatusWithUser(@Param("user") User user, @Param("status") OrderStatus status, Pageable pageable);
/**
* 根据用户和订单号模糊查询分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
Page<Order> findByUserAndOrderNumberContainingIgnoreCaseWithUser(@Param("user") User user, @Param("orderNumber") String orderNumber, Pageable pageable);
/**
* 根据用户、状态和订单号模糊查询分页预加载User
*/
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
Page<Order> findByUserAndStatusAndOrderNumberContainingIgnoreCaseWithUser(@Param("user") User user, @Param("status") OrderStatus status, @Param("orderNumber") String orderNumber, Pageable pageable);
}

View File

@@ -0,0 +1,118 @@
package com.example.demo.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.example.demo.model.Payment;
import com.example.demo.model.PaymentStatus;
@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByOrderId(String orderId);
/**
* 根据ID查询Payment并立即加载User避免LazyInitializationException
*/
@Query("SELECT p FROM Payment p LEFT JOIN FETCH p.user WHERE p.id = :id")
Optional<Payment> findByIdWithUser(@Param("id") Long id);
/**
* 根据ID查询Payment并立即加载User和Order避免LazyInitializationException
*/
@Query("SELECT p FROM Payment p LEFT JOIN FETCH p.user LEFT JOIN FETCH p.order WHERE p.id = :id")
Optional<Payment> findByIdWithUserAndOrder(@Param("id") Long id);
Optional<Payment> findByExternalTransactionId(String externalTransactionId);
List<Payment> findByUserId(Long userId);
List<Payment> findByStatus(PaymentStatus status);
@Query("SELECT p FROM Payment p WHERE p.user.id = :userId ORDER BY p.createdAt DESC")
List<Payment> findByUserIdOrderByCreatedAtDesc(@Param("userId") Long userId);
@Query("SELECT COUNT(p) FROM Payment p WHERE p.status = :status")
long countByStatus(@Param("status") PaymentStatus status);
/**
* 统计用户支付记录数量
*/
long countByUserId(Long userId);
/**
* 统计用户指定状态的支付记录数量
*/
long countByUserIdAndStatus(Long userId, PaymentStatus status);
/**
* 获取今日收入
*/
@Query("SELECT SUM(p.amount) FROM Payment p WHERE p.status = 'COMPLETED' AND p.paidAt BETWEEN :startTime AND :endTime")
Double findTodayRevenue(@Param("startTime") java.time.LocalDateTime startTime, @Param("endTime") java.time.LocalDateTime endTime);
/**
* 获取总收入
*/
@Query("SELECT SUM(p.amount) FROM Payment p WHERE p.status = 'COMPLETED'")
Double findTotalRevenue();
/**
* 获取指定日期范围的收入
*/
@Query("SELECT SUM(p.amount) FROM Payment p WHERE p.status = 'COMPLETED' AND p.paidAt BETWEEN :startTime AND :endTime")
Double findRevenueByDateRange(@Param("startTime") java.time.LocalDateTime startTime, @Param("endTime") java.time.LocalDateTime endTime);
/**
* 获取指定年份的月度收入数据
*/
@Query("SELECT MONTH(p.paidAt) as month, SUM(p.amount) as revenue, COUNT(p) as orderCount " +
"FROM Payment p WHERE p.status = 'COMPLETED' AND YEAR(p.paidAt) = :year " +
"GROUP BY MONTH(p.paidAt) ORDER BY MONTH(p.paidAt)")
List<java.util.Map<String, Object>> findMonthlyRevenueByYear(@Param("year") int year);
/**
* 获取用户最近一次成功的订阅支付(按支付时间倒序)
*/
@Query("SELECT p FROM Payment p WHERE p.user.id = :userId " +
"AND p.status = :status " +
"AND (p.description LIKE '%标准版%' OR p.description LIKE '%专业版%' OR p.description LIKE '%会员%') " +
"ORDER BY p.paidAt DESC, p.createdAt DESC")
List<Payment> findLatestSuccessfulSubscriptionByUserId(@Param("userId") Long userId, @Param("status") PaymentStatus status);
/**
* 统计成功支付的不同用户数(付费用户数)
*/
@Query("SELECT COUNT(DISTINCT p.user.id) FROM Payment p WHERE p.status = :status")
long countDistinctUsersByStatus(@Param("status") PaymentStatus status);
/**
* 获取今日成功支付的总金额
*/
@Query("SELECT COALESCE(SUM(p.amount), 0) FROM Payment p WHERE p.status = :status AND p.paidAt BETWEEN :startTime AND :endTime")
java.math.BigDecimal sumAmountByStatusAndPaidAtBetween(@Param("status") PaymentStatus status, @Param("startTime") java.time.LocalDateTime startTime, @Param("endTime") java.time.LocalDateTime endTime);
/**
* 统计指定时间范围内成功支付的不同用户数
*/
@Query("SELECT COUNT(DISTINCT p.user.id) FROM Payment p WHERE p.status = :status AND p.paidAt BETWEEN :startTime AND :endTime")
long countDistinctUsersByStatusAndPaidAtBetween(@Param("status") PaymentStatus status, @Param("startTime") java.time.LocalDateTime startTime, @Param("endTime") java.time.LocalDateTime endTime);
/**
* 统计指定时间之前成功支付的不同用户数(用于累计统计)
*/
@Query("SELECT COUNT(DISTINCT p.user.id) FROM Payment p WHERE p.status = :status AND p.paidAt < :beforeTime")
long countDistinctUsersByStatusAndPaidAtBefore(@Param("status") PaymentStatus status, @Param("beforeTime") java.time.LocalDateTime beforeTime);
/**
* 计算用户指定状态的支付总金额
* 用于判定会员等级:根据总充值金额区分免费会员和入门会员
*/
@Query("SELECT COALESCE(SUM(p.amount), 0) FROM Payment p WHERE p.user.id = :userId AND p.status = :status")
java.math.BigDecimal sumAmountByUserIdAndStatus(@Param("userId") Long userId, @Param("status") PaymentStatus status);
}

View File

@@ -0,0 +1,100 @@
package com.example.demo.repository;
import com.example.demo.model.PointsFreezeRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
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;
import jakarta.persistence.LockModeType;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 积分冻结记录仓库接口
*/
@Repository
public interface PointsFreezeRecordRepository extends JpaRepository<PointsFreezeRecord, Long> {
/**
* 根据任务ID查找冻结记录返回最新的一条
* 注意同一个taskId可能有多条记录重试任务返回最新的
*/
@Query("SELECT pfr FROM PointsFreezeRecord pfr WHERE pfr.taskId = :taskId ORDER BY pfr.createdAt DESC LIMIT 1")
Optional<PointsFreezeRecord> findByTaskId(@Param("taskId") String taskId);
/**
* 根据任务ID查找冻结记录带悲观写锁用于防止并发重复扣除
* 使用悲观锁确保在高并发场景下不会重复扣除积分
* 只返回状态为 FROZEN 的第一条记录,避免重复记录导致的问题
*/
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT pfr FROM PointsFreezeRecord pfr WHERE pfr.taskId = :taskId AND pfr.status = 'FROZEN' ORDER BY pfr.createdAt ASC LIMIT 1")
Optional<PointsFreezeRecord> findByTaskIdWithLock(@Param("taskId") String taskId);
/**
* 根据用户名查找冻结记录
*/
List<PointsFreezeRecord> findByUsernameOrderByCreatedAtDesc(String username);
/**
* 根据用户名和状态查找冻结记录(按完成时间倒序)
* 用于获取已扣除的消耗记录
*/
List<PointsFreezeRecord> findByUsernameAndStatusOrderByCompletedAtDesc(String username, PointsFreezeRecord.FreezeStatus status);
/**
* 查找用户的冻结中记录
*/
@Query("SELECT pfr FROM PointsFreezeRecord pfr WHERE pfr.username = :username AND pfr.status = 'FROZEN' ORDER BY pfr.createdAt DESC")
List<PointsFreezeRecord> findFrozenRecordsByUsername(@Param("username") String username);
/**
* 统计用户冻结中的积分总数
*/
@Query("SELECT COALESCE(SUM(pfr.freezePoints), 0) FROM PointsFreezeRecord pfr WHERE pfr.username = :username AND pfr.status = 'FROZEN'")
Integer sumFrozenPointsByUsername(@Param("username") String username);
/**
* 查找过期的冻结记录超过24小时未处理
*/
@Query("SELECT pfr FROM PointsFreezeRecord pfr WHERE pfr.status = 'FROZEN' AND pfr.createdAt < :expiredTime")
List<PointsFreezeRecord> findExpiredFrozenRecords(@Param("expiredTime") LocalDateTime expiredTime);
/**
* 更新过期记录状态
*/
@Modifying
@Query("UPDATE PointsFreezeRecord pfr SET pfr.status = 'EXPIRED', pfr.updatedAt = :updatedAt, pfr.completedAt = :completedAt WHERE pfr.id = :id")
int updateExpiredRecord(@Param("id") Long id,
@Param("updatedAt") LocalDateTime updatedAt,
@Param("completedAt") LocalDateTime completedAt);
/**
* 根据任务ID更新状态
*/
@Modifying
@Query("UPDATE PointsFreezeRecord pfr SET pfr.status = :status, pfr.updatedAt = :updatedAt, pfr.completedAt = :completedAt WHERE pfr.taskId = :taskId")
int updateStatusByTaskId(@Param("taskId") String taskId,
@Param("status") PointsFreezeRecord.FreezeStatus status,
@Param("updatedAt") LocalDateTime updatedAt,
@Param("completedAt") LocalDateTime completedAt);
/**
* 删除过期记录超过7天
*/
@Modifying
@Query("DELETE FROM PointsFreezeRecord pfr WHERE pfr.createdAt < :expiredDate")
int deleteExpiredRecords(@Param("expiredDate") LocalDateTime expiredDate);
/**
* 根据状态列表删除记录
*/
@Modifying
@Query("DELETE FROM PointsFreezeRecord pfr WHERE pfr.status IN :statuses")
int deleteByStatusIn(@Param("statuses") List<PointsFreezeRecord.FreezeStatus> statuses);
}

View File

@@ -0,0 +1,38 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
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.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.example.demo.model.StoryboardVideoTask;
@Repository
public interface StoryboardVideoTaskRepository extends JpaRepository<StoryboardVideoTask, Long> {
Optional<StoryboardVideoTask> findByTaskId(String taskId);
List<StoryboardVideoTask> findByUsernameOrderByCreatedAtDesc(String username);
Page<StoryboardVideoTask> findByUsernameOrderByCreatedAtDesc(String username, Pageable pageable);
List<StoryboardVideoTask> findByStatus(StoryboardVideoTask.TaskStatus status);
/**
* 查找超时的分镜视频任务
* 条件状态为PROCESSING且更新时间超过指定时间
* 使用updatedAt而非createdAt因为视频生成可能在分镜图生成后很久才开始
*/
@Query("SELECT t FROM StoryboardVideoTask t WHERE t.status = :status " +
"AND t.updatedAt < :timeoutTime")
List<StoryboardVideoTask> findTimeoutTasks(
@Param("status") StoryboardVideoTask.TaskStatus status,
@Param("timeoutTime") LocalDateTime timeoutTime
);
}

View File

@@ -0,0 +1,10 @@
package com.example.demo.repository;
import com.example.demo.model.SystemSettings;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SystemSettingsRepository extends JpaRepository<SystemSettings, Long> {
// 套餐价格已移至 membership_levels 表管理
}

View File

@@ -0,0 +1,165 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
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.Lock;
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;
import com.example.demo.model.TaskQueue;
import jakarta.persistence.LockModeType;
/**
* 任务队列仓库接口
*/
@Repository
public interface TaskQueueRepository extends JpaRepository<TaskQueue, Long> {
/**
* 根据用户名查找待处理的任务
*/
@Query("SELECT tq FROM TaskQueue tq WHERE tq.username = :username AND tq.status IN ('PENDING', 'PROCESSING') ORDER BY tq.priority ASC, tq.createdAt ASC")
List<TaskQueue> findPendingTasksByUsername(@Param("username") String username);
/**
* 统计用户待处理任务数量
*/
@Query("SELECT COUNT(tq) FROM TaskQueue tq WHERE tq.username = :username AND tq.status IN ('PENDING', 'PROCESSING')")
long countPendingTasksByUsername(@Param("username") String username);
/**
* 根据任务ID查找队列任务
*/
Optional<TaskQueue> findByTaskId(String taskId);
/**
* 使用悲观锁查找任务SELECT FOR UPDATE
* 用于防止并发处理同一任务
*/
@Query("SELECT tq FROM TaskQueue tq WHERE tq.taskId = :taskId")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<TaskQueue> findByTaskIdWithLock(@Param("taskId") String taskId);
/**
* 根据用户名和任务ID查找队列任务
*/
Optional<TaskQueue> findByUsernameAndTaskId(String username, String taskId);
/**
* 查找所有需要检查的任务状态为PROCESSING且未超时
*/
@Query("SELECT tq FROM TaskQueue tq WHERE tq.status = 'PROCESSING' AND tq.checkCount < tq.maxCheckCount ORDER BY tq.lastCheckTime ASC NULLS FIRST, tq.createdAt ASC")
List<TaskQueue> findTasksToCheck();
/**
* 统计需要检查的任务数量状态为PROCESSING且未超时
* 用于快速检查是否有待处理任务
*/
@Query("SELECT COUNT(tq) FROM TaskQueue tq WHERE tq.status = 'PROCESSING' AND tq.checkCount < tq.maxCheckCount")
long countTasksToCheck();
/**
* 查找超时的任务
*/
@Query("SELECT tq FROM TaskQueue tq WHERE tq.status = 'PROCESSING' AND tq.checkCount >= tq.maxCheckCount")
List<TaskQueue> findTimeoutTasks();
/**
* 查找所有待处理的任务(按优先级排序)
*/
@Query("SELECT tq FROM TaskQueue tq WHERE tq.status = 'PENDING' ORDER BY tq.priority ASC, tq.createdAt ASC")
List<TaskQueue> findAllPendingTasks();
/**
* 根据用户名分页查询任务
*/
@Query("SELECT tq FROM TaskQueue tq WHERE tq.username = :username ORDER BY tq.createdAt DESC")
Page<TaskQueue> findByUsernameOrderByCreatedAtDesc(@Param("username") String username, Pageable pageable);
/**
* 统计用户总任务数
*/
long countByUsername(String username);
/**
* 删除过期任务超过7天
*/
@Modifying
@Query("DELETE FROM TaskQueue tq WHERE tq.createdAt < :expiredDate")
int deleteExpiredTasks(@Param("expiredDate") LocalDateTime expiredDate);
/**
* 更新任务状态
*/
@Modifying
@Query("UPDATE TaskQueue tq SET tq.status = :status, tq.updatedAt = :updatedAt, tq.completedAt = :completedAt WHERE tq.taskId = :taskId")
int updateTaskStatus(@Param("taskId") String taskId,
@Param("status") TaskQueue.QueueStatus status,
@Param("updatedAt") LocalDateTime updatedAt,
@Param("completedAt") LocalDateTime completedAt);
/**
* 更新检查信息
*/
@Modifying
@Query("UPDATE TaskQueue tq SET tq.checkCount = tq.checkCount + 1, tq.lastCheckTime = :lastCheckTime, tq.updatedAt = :updatedAt WHERE tq.taskId = :taskId")
int updateCheckInfo(@Param("taskId") String taskId,
@Param("lastCheckTime") LocalDateTime lastCheckTime,
@Param("updatedAt") LocalDateTime updatedAt);
/**
* 更新真实任务ID
*/
@Modifying
@Query("UPDATE TaskQueue tq SET tq.realTaskId = :realTaskId, tq.updatedAt = :updatedAt WHERE tq.taskId = :taskId")
int updateRealTaskId(@Param("taskId") String taskId,
@Param("realTaskId") String realTaskId,
@Param("updatedAt") LocalDateTime updatedAt);
/**
* 更新错误信息
*/
@Modifying
@Query("UPDATE TaskQueue tq SET tq.errorMessage = :errorMessage, tq.updatedAt = :updatedAt WHERE tq.taskId = :taskId")
int updateErrorMessage(@Param("taskId") String taskId,
@Param("errorMessage") String errorMessage,
@Param("updatedAt") LocalDateTime updatedAt);
/**
* 根据状态查找任务
*/
List<TaskQueue> findByStatus(TaskQueue.QueueStatus status);
/**
* 根据状态删除任务
*/
@Modifying
@Query("DELETE FROM TaskQueue tq WHERE tq.status = :status")
int deleteByStatus(@Param("status") TaskQueue.QueueStatus status);
/**
* 根据状态统计任务数量
*/
long countByStatus(TaskQueue.QueueStatus status);
/**
* 查找创建时间在指定时间之后的任务
*/
List<TaskQueue> findByCreatedAtAfter(LocalDateTime dateTime);
/**
* 查找超时的任务(基于创建时间)
* 状态为PENDING或PROCESSING且创建时间早于指定时间
*/
@Query("SELECT tq FROM TaskQueue tq WHERE tq.status IN ('PENDING', 'PROCESSING') AND tq.createdAt < :timeoutTime")
List<TaskQueue> findTimeoutTasksByCreatedTime(@Param("timeoutTime") LocalDateTime timeoutTime);
}

View File

@@ -0,0 +1,118 @@
package com.example.demo.repository;
import com.example.demo.model.TaskStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface TaskStatusRepository extends JpaRepository<TaskStatus, Long> {
/**
* 根据任务ID查找状态
*/
Optional<TaskStatus> findByTaskId(String taskId);
/**
* 根据任务ID查找状态取第一条处理重复记录情况
*/
Optional<TaskStatus> findFirstByTaskIdOrderByIdDesc(String taskId);
/**
* 根据用户名查找所有任务状态
*/
List<TaskStatus> findByUsernameOrderByCreatedAtDesc(String username);
/**
* 根据用户名和状态查找任务
*/
List<TaskStatus> findByUsernameAndStatus(String username, TaskStatus.Status status);
/**
* 查找需要轮询的任务(处理中且未超时)
*/
@Query("SELECT t FROM TaskStatus t WHERE t.status = 'PROCESSING' AND t.pollCount < t.maxPolls AND (t.lastPolledAt IS NULL OR t.lastPolledAt < :cutoffTime)")
List<TaskStatus> findTasksNeedingPolling(@Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 计数:查找需要轮询的任务数量(用于快速短路判断,避免加载实体列表)
*/
@Query("SELECT COUNT(t) FROM TaskStatus t WHERE t.status = 'PROCESSING' AND t.pollCount < t.maxPolls AND (t.lastPolledAt IS NULL OR t.lastPolledAt < :cutoffTime)")
long countTasksNeedingPolling(@Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 查找超时的任务
*/
@Query("SELECT t FROM TaskStatus t WHERE t.status = 'PROCESSING' AND t.pollCount >= t.maxPolls")
List<TaskStatus> findTimeoutTasks();
/**
* 根据外部任务ID查找状态
*/
Optional<TaskStatus> findByExternalTaskId(String externalTaskId);
/**
* 统计用户的任务数量
*/
long countByUsername(String username);
/**
* 根据状态统计任务数量(用于快速短路判断)
*/
long countByStatus(TaskStatus.Status status);
/**
* 根据状态查找所有任务(不分页)
*/
List<TaskStatus> findAllByStatus(TaskStatus.Status status);
/**
* 统计用户指定状态的任务数量
*/
long countByUsernameAndStatus(String username, TaskStatus.Status status);
/**
* 查找最近创建的任务
*/
@Query("SELECT t FROM TaskStatus t WHERE t.username = :username ORDER BY t.createdAt DESC")
List<TaskStatus> findRecentTasksByUsername(@Param("username") String username, org.springframework.data.domain.Pageable pageable);
/**
* 查找所有任务(管理员功能,支持分页)
*/
@Query("SELECT t FROM TaskStatus t ORDER BY t.createdAt DESC")
org.springframework.data.domain.Page<TaskStatus> findAllWithPagination(org.springframework.data.domain.Pageable pageable);
/**
* 根据状态查找任务(管理员功能,支持分页)
*/
org.springframework.data.domain.Page<TaskStatus> findByStatus(TaskStatus.Status status, org.springframework.data.domain.Pageable pageable);
/**
* 根据任务类型查找任务(管理员功能,支持分页)
*/
org.springframework.data.domain.Page<TaskStatus> findByTaskType(TaskStatus.TaskType taskType, org.springframework.data.domain.Pageable pageable);
}

View File

@@ -0,0 +1,112 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
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;
import com.example.demo.model.TextToVideoTask;
/**
* 文生视频任务Repository
*/
@Repository
public interface TextToVideoTaskRepository extends JpaRepository<TextToVideoTask, Long> {
/**
* 根据任务ID查找任务
*/
Optional<TextToVideoTask> findByTaskId(String taskId);
/**
* 根据用户名查找任务列表(按创建时间倒序)
*/
List<TextToVideoTask> findByUsernameOrderByCreatedAtDesc(String username);
/**
* 根据用户名分页查找任务列表
*/
Page<TextToVideoTask> findByUsernameOrderByCreatedAtDesc(String username, Pageable pageable);
/**
* 根据任务ID和用户名查找任务
*/
Optional<TextToVideoTask> findByTaskIdAndUsername(String taskId, String username);
/**
* 统计用户任务数量
*/
long countByUsername(String username);
/**
* 统计用户进行中的任务数量PENDING 或 PROCESSING
*/
@Query("SELECT COUNT(t) FROM TextToVideoTask t WHERE t.username = :username AND t.status IN ('PENDING', 'PROCESSING')")
long countProcessingTasksByUsername(@Param("username") String username);
/**
* 根据状态查找任务列表
*/
List<TextToVideoTask> findByStatus(TextToVideoTask.TaskStatus status);
/**
* 根据用户名和状态查找任务列表
*/
List<TextToVideoTask> findByUsernameAndStatus(String username, TextToVideoTask.TaskStatus status);
/**
* 查找需要处理的任务状态为PENDING或PROCESSING
*/
@Query("SELECT t FROM TextToVideoTask t WHERE t.status IN ('PENDING', 'PROCESSING') ORDER BY t.createdAt ASC")
List<TextToVideoTask> findPendingTasks();
/**
* 查找指定状态的任务列表
*/
@Query("SELECT t FROM TextToVideoTask t WHERE t.status = :status ORDER BY t.createdAt DESC")
List<TextToVideoTask> findByStatusOrderByCreatedAtDesc(@Param("status") TextToVideoTask.TaskStatus status);
/**
* 统计用户各状态任务数量
*/
@Query("SELECT t.status, COUNT(t) FROM TextToVideoTask t WHERE t.username = :username GROUP BY t.status")
List<Object[]> countTasksByStatus(@Param("username") String username);
/**
* 查找用户最近的任务
*/
@Query("SELECT t FROM TextToVideoTask t WHERE t.username = :username ORDER BY t.createdAt DESC")
List<TextToVideoTask> findRecentTasksByUsername(@Param("username") String username, Pageable pageable);
/**
* 删除过期的任务超过30天且已完成或失败
*/
@Modifying
@Query("DELETE FROM TextToVideoTask t WHERE t.createdAt < :expiredDate AND t.status IN ('COMPLETED', 'FAILED', 'CANCELLED')")
int deleteExpiredTasks(@Param("expiredDate") java.time.LocalDateTime expiredDate);
/**
* 根据状态删除任务
*/
@Modifying
@Query("DELETE FROM TextToVideoTask t WHERE t.status = :status")
int deleteByStatus(@Param("status") String status);
/**
* 查找超时的文生视频任务
* 条件状态为PROCESSING且创建时间超过10分钟
*/
@Query("SELECT t FROM TextToVideoTask t WHERE t.status = :status " +
"AND t.createdAt < :timeoutTime")
List<TextToVideoTask> findTimeoutTasks(
@Param("status") TextToVideoTask.TaskStatus status,
@Param("timeoutTime") LocalDateTime timeoutTime
);
}

View File

@@ -0,0 +1,76 @@
package com.example.demo.repository;
import com.example.demo.model.UserActivityStats;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserActivityStatsRepository extends JpaRepository<UserActivityStats, Long> {
/**
* 根据日期查找日活用户数
*/
@Query("SELECT uas.dailyActiveUsers FROM UserActivityStats uas WHERE uas.activityDate = :date")
Integer findDailyActiveUsersByDate(@Param("date") LocalDate date);
/**
* 获取指定年份的月度日活用户数据
*/
@Query("SELECT MONTH(uas.activityDate) as month, " +
"AVG(uas.dailyActiveUsers) as avgDailyActive, " +
"MAX(uas.dailyActiveUsers) as maxDailyActive, " +
"MIN(uas.dailyActiveUsers) as minDailyActive " +
"FROM UserActivityStats uas " +
"WHERE YEAR(uas.activityDate) = :year " +
"GROUP BY MONTH(uas.activityDate) " +
"ORDER BY MONTH(uas.activityDate)")
List<java.util.Map<String, Object>> findMonthlyActiveUsers(@Param("year") int year);
/**
* 获取指定年份的每日日活用户数据
*/
@Query("SELECT DAYOFYEAR(uas.activityDate) as dayOfYear, " +
"WEEK(uas.activityDate) as weekOfYear, " +
"uas.dailyActiveUsers as dailyActiveUsers, " +
"uas.activityDate as activityDate " +
"FROM UserActivityStats uas " +
"WHERE YEAR(uas.activityDate) = :year " +
"ORDER BY uas.activityDate")
List<java.util.Map<String, Object>> findDailyActiveUsersByYear(@Param("year") int year);
/**
* 获取指定月份的平均日活用户数
*/
@Query("SELECT AVG(uas.dailyActiveUsers) FROM UserActivityStats uas " +
"WHERE YEAR(uas.activityDate) = :year AND MONTH(uas.activityDate) = :month")
Double findAverageDailyActiveUsersByMonth(@Param("year") int year, @Param("month") int month);
/**
* 获取指定年份的平均日活用户数
*/
@Query("SELECT AVG(uas.dailyActiveUsers) FROM UserActivityStats uas " +
"WHERE YEAR(uas.activityDate) = :year")
Double findAverageDailyActiveUsersByYear(@Param("year") int year);
/**
* 获取最新的统计数据
*/
Optional<UserActivityStats> findTopByOrderByActivityDateDesc();
/**
* 获取指定日期范围的统计数据
*/
List<UserActivityStats> findByActivityDateBetween(LocalDate startDate, LocalDate endDate);
/**
* 获取指定年份的所有统计数据
*/
@Query("SELECT uas FROM UserActivityStats uas WHERE YEAR(uas.activityDate) = :year ORDER BY uas.activityDate")
List<UserActivityStats> findByActivityDateYear(@Param("year") int year);
}

View File

@@ -0,0 +1,77 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
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.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.example.demo.model.UserErrorLog;
import com.example.demo.model.UserErrorLog.ErrorType;
@Repository
public interface UserErrorLogRepository extends JpaRepository<UserErrorLog, Long> {
// 按用户名查询
List<UserErrorLog> findByUsernameOrderByCreatedAtDesc(String username);
Page<UserErrorLog> findByUsername(String username, Pageable pageable);
// 按错误类型查询
List<UserErrorLog> findByErrorTypeOrderByCreatedAtDesc(ErrorType errorType);
Page<UserErrorLog> findByErrorType(ErrorType errorType, Pageable pageable);
// 按时间范围查询
List<UserErrorLog> findByCreatedAtBetweenOrderByCreatedAtDesc(
LocalDateTime startTime, LocalDateTime endTime);
// 按用户和时间范围查询
List<UserErrorLog> findByUsernameAndCreatedAtBetweenOrderByCreatedAtDesc(
String username, LocalDateTime startTime, LocalDateTime endTime);
// 按错误来源查询
List<UserErrorLog> findByErrorSourceOrderByCreatedAtDesc(String errorSource);
// 按任务ID查询
List<UserErrorLog> findByTaskIdOrderByCreatedAtDesc(String taskId);
// 统计:按错误类型分组统计数量
@Query("SELECT e.errorType, COUNT(e) FROM UserErrorLog e " +
"WHERE e.createdAt >= :startTime " +
"GROUP BY e.errorType ORDER BY COUNT(e) DESC")
List<Object[]> countByErrorType(@Param("startTime") LocalDateTime startTime);
// 统计:按错误来源分组统计
@Query("SELECT e.errorSource, COUNT(e) FROM UserErrorLog e " +
"WHERE e.createdAt >= :startTime " +
"GROUP BY e.errorSource ORDER BY COUNT(e) DESC")
List<Object[]> countByErrorSource(@Param("startTime") LocalDateTime startTime);
// 统计:按日期分组统计错误数量
@Query("SELECT FUNCTION('DATE', e.createdAt), COUNT(e) FROM UserErrorLog e " +
"WHERE e.createdAt >= :startTime " +
"GROUP BY FUNCTION('DATE', e.createdAt) ORDER BY FUNCTION('DATE', e.createdAt) DESC")
List<Object[]> countByDate(@Param("startTime") LocalDateTime startTime);
// 统计:指定时间范围内的错误总数
long countByCreatedAtBetween(LocalDateTime startTime, LocalDateTime endTime);
// 统计:指定用户的错误总数
long countByUsername(String username);
// 删除:清理指定时间之前的错误(用于日志清理)
void deleteByCreatedAtBefore(LocalDateTime beforeTime);
// 最近N条错误
List<UserErrorLog> findTop10ByOrderByCreatedAtDesc();
List<UserErrorLog> findTop50ByOrderByCreatedAtDesc();
// 用户最近的错误
List<UserErrorLog> findTop10ByUsernameOrderByCreatedAtDesc(String username);
}

View File

@@ -0,0 +1,36 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.model.UserMembership;
@Repository
public interface UserMembershipRepository extends JpaRepository<UserMembership, Long> {
Optional<UserMembership> findByUserIdAndStatus(Long userId, String status);
/**
* 按到期时间降序查找用户的有效会员记录(返回到期时间最晚的)
*/
Optional<UserMembership> findFirstByUserIdAndStatusOrderByEndDateDesc(Long userId, String status);
long countByStatus(String status);
long countByStartDateBetween(LocalDateTime startDate, LocalDateTime endDate);
/**
* 统计指定时间之前开始的会员数(用于累计统计)
*/
long countByStartDateBefore(LocalDateTime beforeTime);
// 根据用户ID删除会员信息
void deleteByUserId(Long userId);
/**
* 查找已过期但状态仍为ACTIVE的会员记录
*/
java.util.List<UserMembership> findByStatusAndEndDateBefore(String status, LocalDateTime endDate);
}

View File

@@ -0,0 +1,52 @@
package com.example.demo.repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.model.User;
public interface UserRepository extends JpaRepository<User, Long> {
// 分页查询活跃用户isActive=true
Page<User> findByIsActive(Boolean isActive, Pageable pageable);
// 根据用户ID列表分页查询
Page<User> findByIdIn(List<Long> ids, Pageable pageable);
// 根据用户ID列表和活跃状态分页查询
Page<User> findByIdInAndIsActive(List<Long> ids, Boolean isActive, Pageable pageable);
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);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
boolean existsByPhone(String phone);
boolean existsByUserId(String userId);
long countByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
/**
* 统计指定时间之后活跃的用户数量(用于在线用户统计)
*/
long countByLastActiveTimeAfter(LocalDateTime afterTime);
/**
* 统计今日新增用户数
*/
long countByCreatedAtAfter(LocalDateTime startTime);
/**
* 统计指定时间之前创建的用户数(用于累计统计)
*/
long countByCreatedAtBefore(LocalDateTime beforeTime);
}

Some files were not shown because too many files have changed in this diff Show More