Initial commit: 添加项目代码

This commit is contained in:
2026-02-13 18:24:52 +08:00
commit 05d3cc539d
303 changed files with 97922 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
{
"permissions": {
"allow": [
"Bash(./mvnw clean:*)",
"Bash(java -jar:*)",
"Bash(tasklist:*)",
"Bash(taskkill:*)",
"Bash(cmd.exe /c \"taskkill /F /PID 32336\")",
"Bash(powershell -Command \"Stop-Process -Id 32336 -Force\")",
"Bash(powershell -Command \"Get-Process | Where-Object {$_ProcessName -eq ''java'' -and $_WorkingSet -gt 1GB} | Stop-Process -Force\")",
"Bash(dir /s /b C:UsersUIDesktopAIGCdemosrcfrontendsrcviews*Works*.vue)",
"Bash(dir /s /b C:UsersUIDesktopAIGCdemosrcmainjavacomexampledemocontroller*Dashboard*.java)",
"Bash(mysql:*)",
"Bash(curl:*)",
"Bash(python:*)",
"Bash(powershell -Command \"Get-Process | Where-Object {$_ProcessName -eq ''java''} | Select-Object Id, ProcessName, WorkingSet\")",
"Bash(cmd.exe /c \"tasklist | findstr java\")",
"Bash(dir /s /b C:UsersUIDesktopAIGCdemosrcfrontendsrcviews*Storyboard*.vue)",
"Bash(powershell -Command:*)"
],
"deny": [],
"ask": []
}
}

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,188 @@
package com.example.demo.config;
import com.example.demo.model.MembershipLevel;
import com.example.demo.model.UserMembership;
import com.example.demo.model.PaymentStatus;
import com.example.demo.repository.MembershipLevelRepository;
import com.example.demo.repository.UserMembershipRepository;
import com.example.demo.repository.PaymentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Component
public class DataInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(DataInitializer.class);
private final MembershipLevelRepository membershipLevelRepository;
private final UserMembershipRepository userMembershipRepository;
private final PaymentRepository paymentRepository;
public DataInitializer(MembershipLevelRepository membershipLevelRepository,
UserMembershipRepository userMembershipRepository,
PaymentRepository paymentRepository) {
this.membershipLevelRepository = membershipLevelRepository;
this.userMembershipRepository = userMembershipRepository;
this.paymentRepository = paymentRepository;
}
@Override
public void run(String... args) throws Exception {
initMembershipLevels();
}
@Transactional
private void initMembershipLevels() {
if (membershipLevelRepository.count() == 0) {
// ===== 全新数据库:创建 4 个等级 =====
logger.info("初始化会员等级数据...");
createAllLevels();
logger.info("✅ 会员等级数据初始化完成,共{}条", membershipLevelRepository.count());
} else {
// ===== 已有数据:检查是否需要迁移(添加 starter 等级)=====
Optional<MembershipLevel> starterOpt = membershipLevelRepository.findByName("starter");
if (starterOpt.isEmpty()) {
logger.info("检测到数据库缺少 starter 等级,开始执行迁移...");
migrateToFourLevels();
} else {
logger.info("会员等级数据已存在(含 starter跳过初始化");
}
}
}
/**
* 全新数据库:创建完整的 4 级会员体系
*/
private void createAllLevels() {
// free: 免费会员(新用户注册默认等级,无价格、无积分)
MembershipLevel free = new MembershipLevel();
free.setName("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);
// starter: 入门会员(最低付费等级)
MembershipLevel starter = new MembershipLevel();
starter.setName("starter");
starter.setDisplayName("入门会员");
starter.setPrice(99.0);
starter.setDurationDays(365);
starter.setPointsBonus(1000);
starter.setDescription("入门会员 - 每年1000积分");
starter.setIsActive(true);
starter.setCreatedAt(LocalDateTime.now());
starter.setUpdatedAt(LocalDateTime.now());
membershipLevelRepository.save(starter);
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);
}
/**
* 从旧的 3 级体系迁移到新的 4 级体系:
* 1. 从现有 free 等级复制价格和积分创建 starter
* 2. 将已付费且当前为 free 的用户迁移到 starter
* 3. 将 free 等级归零price=0, points=0
*/
private void migrateToFourLevels() {
Optional<MembershipLevel> freeOpt = membershipLevelRepository.findByName("free");
if (freeOpt.isEmpty()) {
logger.error("❌ 迁移失败:数据库中找不到 free 等级");
return;
}
MembershipLevel freeLevel = freeOpt.get();
// 第一步:创建 starter 等级,复制 free 的价格和积分
Double oldFreePrice = freeLevel.getPrice();
int oldFreePoints = freeLevel.getPointsBonus();
MembershipLevel starter = new MembershipLevel();
starter.setName("starter");
starter.setDisplayName("入门会员");
starter.setPrice(oldFreePrice != null && oldFreePrice > 0 ? oldFreePrice : 99.0);
starter.setDurationDays(freeLevel.getDurationDays() > 0 ? freeLevel.getDurationDays() : 365);
starter.setPointsBonus(oldFreePoints > 0 ? oldFreePoints : 1000);
starter.setDescription("入门会员 - 每年" + starter.getPointsBonus() + "积分");
starter.setIsActive(true);
starter.setCreatedAt(LocalDateTime.now());
starter.setUpdatedAt(LocalDateTime.now());
membershipLevelRepository.save(starter);
logger.info("✅ 已创建 starter 等级: price={}, points={}", starter.getPrice(), starter.getPointsBonus());
// 第二步:将已付费用户从 free 迁移到 starter
int migratedCount = 0;
try {
Long freeLevelId = freeLevel.getId();
Long starterLevelId = starter.getId();
// 查找所有当前为 free 等级的活跃会员
List<UserMembership> freeMemberships = userMembershipRepository.findAll().stream()
.filter(m -> "ACTIVE".equals(m.getStatus()) && freeLevelId.equals(m.getMembershipLevelId()))
.toList();
for (UserMembership membership : freeMemberships) {
// 检查该用户是否有成功的支付记录
BigDecimal totalPaid = paymentRepository.sumAmountByUserIdAndStatus(
membership.getUserId(), PaymentStatus.SUCCESS);
if (totalPaid != null && totalPaid.compareTo(BigDecimal.ZERO) > 0) {
// 有支付记录,迁移到 starter
membership.setMembershipLevelId(starterLevelId);
membership.setUpdatedAt(LocalDateTime.now());
userMembershipRepository.save(membership);
migratedCount++;
}
}
logger.info("✅ 已将 {} 个已付费用户从 free 迁移到 starter", migratedCount);
} catch (Exception e) {
logger.warn("⚠️ 用户迁移过程中出现异常(不影响等级创建): {}", e.getMessage());
}
// 第三步:将 free 等级归零
freeLevel.setPrice(0.0);
freeLevel.setPointsBonus(0);
freeLevel.setDisplayName("免费会员");
freeLevel.setDescription("免费会员(新用户注册默认)");
freeLevel.setUpdatedAt(LocalDateTime.now());
membershipLevelRepository.save(freeLevel);
logger.info("✅ 已将 free 等级归零: price=0, points=0, displayName=免费会员");
logger.info("✅ 迁移完成4 级会员体系: free(免费会员) → starter(入门会员) → standard(标准会员) → professional(专业版)");
}
}

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,744 @@
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.User;
import com.example.demo.model.SystemSettings;
import com.example.demo.model.MembershipLevel;
import com.example.demo.repository.TaskStatusRepository;
import com.example.demo.repository.MembershipLevelRepository;
import com.example.demo.service.UserService;
import com.example.demo.service.SystemSettingsService;
import com.example.demo.service.OnlineStatsService;
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("starter").ifPresent(level ->
response.put("starterPriceCny", level.getPrice().intValue()));
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("textToVideoPoints", settings.getTextToVideoPoints());
response.put("imageToVideoPoints", settings.getImageToVideoPoints());
response.put("storyboardImagePoints", settings.getStoryboardImagePoints());
response.put("storyboardVideoPoints", settings.getStoryboardVideoPoints());
// 支付渠道开关
response.put("enableAlipay", settings.getEnableAlipay());
response.put("enablePaypal", settings.getEnablePaypal());
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("starterPriceCny")) {
Object value = settingsData.get("starterPriceCny");
Integer price = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString());
membershipLevelRepository.findByName("starter").ifPresent(level -> {
level.setPrice(price.doubleValue());
level.setUpdatedAt(java.time.LocalDateTime.now());
membershipLevelRepository.save(level);
logger.info("更新membership_levels表: starter价格={}", price);
});
logger.info("更新入门版价格为: {} 元", price);
}
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("textToVideoPoints")) {
Object value = settingsData.get("textToVideoPoints");
if (value instanceof Number num) {
settings.setTextToVideoPoints(num.intValue());
logger.info("更新文生视频消耗积分为: {}", num.intValue());
} else if (value != null) {
int pts = Integer.parseInt(String.valueOf(value));
settings.setTextToVideoPoints(pts);
logger.info("更新文生视频消耗积分为: {}", pts);
}
}
if (settingsData.containsKey("imageToVideoPoints")) {
Object value = settingsData.get("imageToVideoPoints");
if (value instanceof Number num) {
settings.setImageToVideoPoints(num.intValue());
logger.info("更新图生视频消耗积分为: {}", num.intValue());
} else if (value != null) {
int pts = Integer.parseInt(String.valueOf(value));
settings.setImageToVideoPoints(pts);
logger.info("更新图生视频消耗积分为: {}", pts);
}
}
if (settingsData.containsKey("storyboardImagePoints")) {
Object value = settingsData.get("storyboardImagePoints");
if (value instanceof Number num) {
settings.setStoryboardImagePoints(num.intValue());
logger.info("更新分镜图生成消耗积分为: {}", num.intValue());
} else if (value != null) {
int pts = Integer.parseInt(String.valueOf(value));
settings.setStoryboardImagePoints(pts);
logger.info("更新分镜图生成消耗积分为: {}", pts);
}
}
if (settingsData.containsKey("storyboardVideoPoints")) {
Object value = settingsData.get("storyboardVideoPoints");
if (value instanceof Number num) {
settings.setStoryboardVideoPoints(num.intValue());
logger.info("更新分镜视频生成消耗积分为: {}", num.intValue());
} else if (value != null) {
int pts = Integer.parseInt(String.valueOf(value));
settings.setStoryboardVideoPoints(pts);
logger.info("更新分镜视频生成消耗积分为: {}", pts);
}
}
// 更新支付渠道开关
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);
}
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 {
// 查找免费会员等级(新用户注册默认)
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,284 @@
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<>();
// 当月新增用户数
LocalDate today = LocalDate.now();
LocalDateTime currentMonthStart = LocalDateTime.of(today.getYear(), today.getMonthValue(), 1, 0, 0, 0);
LocalDateTime currentMonthEnd = currentMonthStart.plusMonths(1).minusSeconds(1);
long newUsers = userRepository.countByCreatedAtBetween(currentMonthStart, currentMonthEnd);
// 当月付费用户数(当月内产生成功支付的去重用户)
long paidUsers = paymentRepository.countDistinctUsersByStatusAndPaidAtBetween(
com.example.demo.model.PaymentStatus.SUCCESS, currentMonthStart, currentMonthEnd);
// 当月付费转化率 = 当月付费用户数 / 当月新增用户数
double conversionRate = newUsers > 0 ? (double) paidUsers / newUsers * 100 : 0.0;
response.put("totalUsers", newUsers);
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) {
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 monthStart = LocalDateTime.of(year, month, 1, 0, 0, 0);
LocalDateTime monthEnd = monthStart.plusMonths(1).minusSeconds(1);
// 当月新增用户数
long monthNewUsers = userRepository.countByCreatedAtBetween(monthStart, monthEnd);
// 当月付费用户数(当月内产生成功支付的去重用户)
long monthPaidUsers = paymentRepository.countDistinctUsersByStatusAndPaidAtBetween(
com.example.demo.model.PaymentStatus.SUCCESS, monthStart, monthEnd);
// 当月转化率
double monthConversionRate = monthNewUsers > 0 ? (double) monthPaidUsers / monthNewUsers * 100 : 0.0;
monthData.put("totalUsers", monthNewUsers);
monthData.put("paidUsers", monthPaidUsers);
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,490 @@
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 = "16:9") String aspectRatio,
@RequestParam(value = "duration", defaultValue = "5") int duration,
@RequestParam(value = "hdMode", defaultValue = "false") boolean hdMode,
@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 < 1 || duration > 60) {
response.put("success", false);
response.put("message", "视频时长必须在1-60秒之间");
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
);
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", "16:9");
int duration = request.get("duration") instanceof Number
? ((Number) request.get("duration")).intValue()
: Integer.parseInt(request.getOrDefault("duration", "5").toString());
boolean hdMode = Boolean.parseBoolean(request.getOrDefault("hdMode", "false").toString());
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 < 1 || duration > 60) {
response.put("success", false);
response.put("message", "视频时长必须在1-60秒之间");
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
);
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"};
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,822 @@
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();
// 所有等级统一使用 displayNamefree=免费会员, starter=入门会员, standard=标准会员, professional=专业会员)
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();
// 所有等级统一使用 displayName
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;
}
// 匹配 "入门" -> "starter"
if (levelName.contains("入门") && "starter".equalsIgnoreCase(name)) {
levelOpt = Optional.of(lvl);
break;
}
// 匹配 "免费" -> "free"
if (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("free");
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("durationDays")) {
Object daysObj = updateData.get("durationDays");
if (daysObj instanceof Number) {
level.setDurationDays(((Number) daysObj).intValue());
} else if (daysObj instanceof String) {
level.setDurationDays(Integer.parseInt((String) daysObj));
}
}
// 更新描述
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,312 @@
package com.example.demo.controller;
import com.example.demo.model.NovelComicTask;
import com.example.demo.repository.NovelComicTaskRepository;
import com.example.demo.service.CosService;
import com.example.demo.service.CozeWorkflowService;
import com.example.demo.util.JwtUtils;
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.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
/**
* 小说漫剧生成 API 控制器
* 使用异步模式:提交任务立即返回 taskId前端轮询状态
*/
@RestController
@RequestMapping("/api/novel-comic")
public class NovelComicApiController {
private static final Logger logger = LoggerFactory.getLogger(NovelComicApiController.class);
private static final long MAX_MUSIC_FILE_SIZE = 50 * 1024 * 1024; // 50MB
private static final Set<String> ALLOWED_MUSIC_TYPES = Set.of(
"audio/mpeg", "audio/wav", "audio/aac", "audio/ogg",
"audio/mp4", "audio/x-m4a", "audio/flac", "audio/webm"
);
@Autowired
private CozeWorkflowService cozeWorkflowService;
@Autowired
private CosService cosService;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private NovelComicTaskRepository novelComicTaskRepository;
/**
* 创建小说漫剧生成任务(异步模式)
* 立即返回 taskId (executeId),前端通过 /status 接口轮询结果
*/
@PostMapping("/create")
public ResponseEntity<Map<String, Object>> createNovelComic(
@RequestParam("storyBackground") String storyBackground,
@RequestParam("storyScript") String storyScript,
@RequestParam(value = "theme", required = false, defaultValue = "") String theme,
@RequestParam(value = "backgroundMusic", required = false) MultipartFile backgroundMusic,
@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);
}
// 必填校验
if (storyBackground == null || storyBackground.trim().isEmpty()) {
response.put("success", false);
response.put("message", "故事背景不能为空");
return ResponseEntity.badRequest().body(response);
}
if (storyScript == null || storyScript.trim().isEmpty()) {
response.put("success", false);
response.put("message", "故事文案不能为空");
return ResponseEntity.badRequest().body(response);
}
// 验证音乐文件(如果有),上传到 COS 获取 URL
String musicUrl = "";
if (backgroundMusic != null && !backgroundMusic.isEmpty()) {
if (backgroundMusic.getSize() > MAX_MUSIC_FILE_SIZE) {
response.put("success", false);
response.put("message", "音乐文件不能超过 50MB");
return ResponseEntity.badRequest().body(response);
}
String contentType = backgroundMusic.getContentType();
if (contentType == null || !ALLOWED_MUSIC_TYPES.contains(contentType)) {
response.put("success", false);
response.put("message", "不支持的音频格式,请上传 MP3、WAV、AAC、OGG 等格式");
return ResponseEntity.badRequest().body(response);
}
String musicFileName = backgroundMusic.getOriginalFilename();
logger.info("音乐文件: name={}, size={}, type={}", musicFileName,
backgroundMusic.getSize(), contentType);
String cosFileName = "bgm_" + System.currentTimeMillis() + "_" + musicFileName;
musicUrl = cosService.uploadBytes(
backgroundMusic.getBytes(), cosFileName, contentType);
if (musicUrl == null || musicUrl.isEmpty()) {
response.put("success", false);
response.put("message", "音乐文件上传失败,请稍后重试");
return ResponseEntity.status(500).body(response);
}
logger.info("音乐文件已上传到 COS: {}", musicUrl);
}
logger.info("小说漫剧异步提交: user={}, theme={}, backgroundLen={}, scriptLen={}, hasMusicUrl={}",
username, theme, storyBackground.length(), storyScript.length(), !musicUrl.isEmpty());
// 异步提交 Coze 工作流 — 立即返回 executeId
Map<String, Object> result = cozeWorkflowService.generateNovelComicAsync(
theme, storyBackground.trim(), musicUrl, storyScript.trim());
if (Boolean.TRUE.equals(result.get("success"))) {
String executeId = (String) result.get("executeId");
// 持久化任务记录
try {
NovelComicTask task = new NovelComicTask(
executeId, username, theme,
storyBackground.trim(), storyScript.trim(), musicUrl);
novelComicTaskRepository.save(task);
logger.info("任务记录已保存: taskId={}", executeId);
} catch (Exception dbEx) {
logger.warn("保存任务记录失败(不影响生成): {}", dbEx.getMessage());
}
response.put("success", true);
response.put("taskId", executeId);
response.put("message", "任务已提交,正在后台生成中");
logger.info("小说漫剧任务提交成功: user={}, taskId={}", username, executeId);
return ResponseEntity.ok(response);
} else {
response.put("success", false);
response.put("message", result.getOrDefault("message", "任务提交失败"));
logger.warn("小说漫剧任务提交失败: user={}, message={}", username, result.get("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);
}
}
/**
* 查询任务执行状态(前端轮询)
* 返回: status=RUNNING/SUCCESS/FAIL, data, message
*/
@GetMapping("/status/{executeId}")
public ResponseEntity<Map<String, Object>> getTaskStatus(
@PathVariable String executeId,
@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);
}
// 查询 Coze 工作流执行状态
Map<String, Object> statusResult = cozeWorkflowService.getNovelComicStatus(executeId);
String status = (String) statusResult.getOrDefault("status", "RUNNING");
response.put("success", true);
response.put("status", status);
if ("SUCCESS".equals(status)) {
Object data = statusResult.get("data");
response.put("data", data);
// 更新数据库记录
updateTaskRecord(executeId, status, data, null);
} else if ("FAIL".equals(status)) {
String msg = (String) statusResult.getOrDefault("message", "生成失败");
response.put("message", msg);
updateTaskRecord(executeId, status, null, msg);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("查询任务状态异常: executeId={}", executeId, e);
response.put("success", false);
response.put("status", "FAIL");
response.put("message", "查询状态失败: " + e.getMessage());
return ResponseEntity.status(500).body(response);
}
}
/**
* 获取用户历史记录(分页)
*/
@GetMapping("/history")
public ResponseEntity<Map<String, Object>> getHistory(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") 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);
}
Page<NovelComicTask> tasks = novelComicTaskRepository
.findByUsernameOrderByCreatedAtDesc(username, PageRequest.of(page, size));
// 转换为前端需要的格式
List<Map<String, Object>> items = new ArrayList<>();
for (NovelComicTask task : tasks.getContent()) {
Map<String, Object> item = new HashMap<>();
item.put("id", task.getId());
item.put("taskId", task.getTaskId());
item.put("theme", task.getTheme());
item.put("storyBackground", task.getStoryBackground());
item.put("storyScript", task.getStoryScript());
item.put("status", task.getStatus().name());
item.put("result", task.getResult());
item.put("errorMessage", task.getErrorMessage());
item.put("createdAt", task.getCreatedAt());
items.add(item);
}
Map<String, Object> data = new HashMap<>();
data.put("content", items);
data.put("totalElements", tasks.getTotalElements());
data.put("totalPages", tasks.getTotalPages());
response.put("success", true);
response.put("data", data);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取历史记录失败", e);
response.put("success", false);
response.put("message", "获取历史记录失败");
return ResponseEntity.status(500).body(response);
}
}
/**
* 获取 Coze 配置状态(调试用)
*/
@GetMapping("/config-status")
public ResponseEntity<Map<String, Object>> getConfigStatus() {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", cozeWorkflowService.getConfigStatus());
return ResponseEntity.ok(response);
}
/**
* 更新数据库中的任务记录
*/
private void updateTaskRecord(String executeId, String status, Object data, String errorMsg) {
try {
novelComicTaskRepository.findByTaskId(executeId).ifPresent(task -> {
// 只更新终态,避免重复写入
if (task.getStatus() == NovelComicTask.TaskStatus.COMPLETED
|| task.getStatus() == NovelComicTask.TaskStatus.FAILED) {
return;
}
if ("SUCCESS".equals(status)) {
String resultStr = data instanceof String ? (String) data : String.valueOf(data);
task.markCompleted(resultStr);
novelComicTaskRepository.save(task);
} else if ("FAIL".equals(status)) {
task.markFailed(errorMsg);
novelComicTaskRepository.save(task);
}
});
} catch (Exception e) {
logger.warn("更新任务记录失败: {}", e.getMessage());
}
}
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;
}
}
}

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,806 @@
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();
// 所有等级统一使用 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();
// 过期后仍然按照原等级显示
currentPlan = level.getDisplayName() != null ? level.getDisplayName() : level.getName();
}
}
}
} catch (Exception e) {
logger.warn("从UserMembership获取会员信息失败将使用支付记录判断: {}", e.getMessage());
}
// 如果currentPlan为null没有会员记录默认为免费会员
if (currentPlan == null) {
try {
java.util.Optional<MembershipLevel> freeLevelOpt = membershipLevelRepository.findByName("free");
if (freeLevelOpt.isPresent()) {
currentPlan = freeLevelOpt.get().getDisplayName();
} else {
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,102 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.SystemSettings;
import com.example.demo.model.MembershipLevel;
import com.example.demo.repository.UserRepository;
import com.example.demo.repository.MembershipLevelRepository;
import com.example.demo.service.SystemSettingsService;
@RestController
@RequestMapping("/api/public")
public class PublicApiController {
private static final Logger logger = LoggerFactory.getLogger(PublicApiController.class);
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表读取价格容错缺少某个等级不影响其他等级
Optional<MembershipLevel> starterOpt = membershipLevelRepository.findByName("starter");
Optional<MembershipLevel> standardOpt = membershipLevelRepository.findByName("standard");
Optional<MembershipLevel> proOpt = membershipLevelRepository.findByName("professional");
if (starterOpt.isEmpty()) {
logger.warn("数据库中缺少starter会员等级配置请检查V15迁移是否执行");
}
if (standardOpt.isEmpty()) {
logger.warn("数据库中缺少standard会员等级配置");
}
if (proOpt.isEmpty()) {
logger.warn("数据库中缺少professional会员等级配置");
}
// 套餐价格配置从membership_levels表读取
config.put("starterPriceCny", starterOpt.map(MembershipLevel::getPrice).orElse(0.0));
config.put("starterPoints", starterOpt.map(MembershipLevel::getPointsBonus).orElse(0));
config.put("standardPriceCny", standardOpt.map(MembershipLevel::getPrice).orElse(0.0));
config.put("standardPoints", standardOpt.map(MembershipLevel::getPointsBonus).orElse(0));
config.put("proPriceCny", proOpt.map(MembershipLevel::getPrice).orElse(0.0));
config.put("proPoints", proOpt.map(MembershipLevel::getPointsBonus).orElse(0));
config.put("pointsPerGeneration", settings.getPointsPerGeneration());
// 支付渠道开关
config.put("enableAlipay", settings.getEnableAlipay());
config.put("enablePaypal", settings.getEnablePaypal());
// 返回所有会员等级列表
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,402 @@
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", "16:9");
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);
}
}
logger.info("任务参数 - duration: {}, aspectRatio: {}, hdMode: {}, imageModel: {}, uploadedImages: {}",
duration, aspectRatio, hdMode, imageModel, 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
);
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", "16:9");
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);
}
}
if (storyboardImage == null || storyboardImage.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "分镜图不能为空"));
}
logger.info("直接创建视频任务参数 - duration: {}, aspectRatio: {}, hdMode: {}, referenceImages: {}",
duration, aspectRatio, hdMode, referenceImages != null ? referenceImages.size() : 0);
// 调用服务层方法直接创建视频任务
StoryboardVideoTask task = storyboardVideoService.createVideoDirectTask(
username, prompt, storyboardImage, aspectRatio, hdMode != null && hdMode, duration, referenceImages
);
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("请求体为空");
}
logger.info("调用startVideoGeneration: taskId={}, storyboardImage={}", taskId,
storyboardImage != null ? "有值(长度:" + storyboardImage.length() + ")" : "null");
// 开始生成视频传递参数包括参考图和分镜图URL
storyboardVideoService.startVideoGeneration(taskId, duration, aspectRatio, hdMode, referenceImages, storyboardImage);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "视频生成任务已启动"
));
} 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,361 @@
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.model.enums.CommonTaskType;
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 {
CommonTaskType taskTypeEnum = CommonTaskType.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,327 @@
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", "16:9");
// 安全的类型转换
Integer duration = 5; // 默认值
try {
Object durationObj = request.getOrDefault("duration", 5);
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; // 使用默认值
}
// 验证参数
if (prompt == null || prompt.trim().isEmpty()) {
response.put("success", false);
response.put("message", "文本描述不能为空");
return ResponseEntity.badRequest().body(response);
}
if (duration < 1 || duration > 60) {
response.put("success", false);
response.put("message", "视频时长必须在1-60秒之间");
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
);
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"};
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,851 @@
package com.example.demo.controller;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
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.model.UserWork;
import com.example.demo.model.enums.CommonTaskType;
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<UserWork> 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<>();
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条
// 解析作品类型
CommonTaskType filterType = null;
if (workType != null && !workType.isEmpty()) {
try {
filterType = CommonTaskType.valueOf(workType.toUpperCase());
} catch (IllegalArgumentException e) {
logger.warn("无效的作品类型: {}", workType);
}
}
// 根据参数决定是否包含正在进行中的作品
Page<UserWork> 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 -> w.getStatus() == UserWork.WorkStatus.PROCESSING || w.getStatus() == UserWork.WorkStatus.PENDING)
.count();
logger.info("获取作品列表: username={}, total={}, processing/pending={}",
username, works.getTotalElements(), processingCount);
} else {
works = userWorkService.getUserWorks(username, page, size);
}
Map<String, Object> workStats = userWorkService.getUserWorkStats(username);
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);
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);
}
}
/**
* 删除作品(按数据库 ID
*/
@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);
}
}
/**
* 删除作品(按 taskId支持 sb_xxx、sb_xxx_image 等非数字 ID
*/
@DeleteMapping("/{taskId}")
public ResponseEntity<Map<String, Object>> deleteWorkByTaskId(
@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);
}
// 通过 taskId 查找作品
var workOpt = userWorkService.getWorkByTaskId(taskId);
if (workOpt.isPresent()) {
boolean deleted = userWorkService.deleteWork(workOpt.get().getId(), username);
if (deleted) {
response.put("success", true);
response.put("message", "作品删除成功");
} else {
response.put("success", false);
response.put("message", "作品不存在或无权限删除");
}
} else {
response.put("success", false);
response.put("message", "作品不存在: " + taskId);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("按taskId删除作品失败: {}", taskId, 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, List<Long>> body,
@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<Long> workIds = body.get("workIds");
if (workIds == null || workIds.isEmpty()) {
response.put("success", false);
response.put("message", "请选择要删除的作品");
return ResponseEntity.badRequest().body(response);
}
// 限制单次批量删除数量,防止滥用
if (workIds.size() > 100) {
response.put("success", false);
response.put("message", "单次最多删除100个作品");
return ResponseEntity.badRequest().body(response);
}
Map<String, Object> result = userWorkService.batchDeleteWorks(workIds, username);
int deletedCount = (int) result.get("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);
}
}
/**
* 点赞作品
*/
@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<UserWork> works;
if ("popular".equals(sort)) {
works = userWorkService.getPopularWorks(page, size);
} else if ("latest".equals(sort)) {
works = userWorkService.getLatestWorks(page, size);
} else if (type != null) {
try {
CommonTaskType workType = CommonTaskType.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<UserWork> 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<UserWork> 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,188 @@
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.*;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.model.WorkflowVideo;
import com.example.demo.service.WorkflowVideoService;
/**
* 优质工作流视频 API 控制器
* 公开接口:/api/workflow-videos
* 管理员接口:/api/admin/workflow-videos
*/
@RestController
public class WorkflowVideoApiController {
private static final Logger logger = LoggerFactory.getLogger(WorkflowVideoApiController.class);
@Autowired
private WorkflowVideoService workflowVideoService;
// ==================== 公开接口 ====================
/**
* 获取已启用的工作流视频列表(无需认证)
*/
@GetMapping("/api/workflow-videos")
public ResponseEntity<Map<String, Object>> getActiveVideos() {
Map<String, Object> response = new HashMap<>();
try {
List<WorkflowVideo> videos = workflowVideoService.getActiveVideos();
response.put("success", true);
response.put("data", videos);
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("/api/admin/workflow-videos")
public ResponseEntity<Map<String, Object>> getAllVideos(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Map<String, Object> response = new HashMap<>();
try {
Page<WorkflowVideo> videos = workflowVideoService.getAllVideos(page, size);
response.put("success", true);
response.put("data", videos.getContent());
response.put("totalElements", videos.getTotalElements());
response.put("totalPages", videos.getTotalPages());
response.put("currentPage", page);
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("/api/admin/workflow-videos")
public ResponseEntity<Map<String, Object>> createVideo(@RequestBody WorkflowVideo video) {
Map<String, Object> response = new HashMap<>();
try {
WorkflowVideo created = workflowVideoService.create(video);
response.put("success", true);
response.put("data", created);
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);
}
}
/**
* 管理后台 - 更新工作流视频
*/
@PutMapping("/api/admin/workflow-videos/{id}")
public ResponseEntity<Map<String, Object>> updateVideo(
@PathVariable Long id,
@RequestBody WorkflowVideo video) {
Map<String, Object> response = new HashMap<>();
try {
WorkflowVideo updated = workflowVideoService.update(id, video);
response.put("success", true);
response.put("data", updated);
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("/api/admin/workflow-videos/{id}")
public ResponseEntity<Map<String, Object>> deleteVideo(@PathVariable Long id) {
Map<String, Object> response = new HashMap<>();
try {
workflowVideoService.delete(id);
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("/api/admin/workflow-videos/upload-video")
public ResponseEntity<Map<String, Object>> uploadVideo(@RequestParam("file") MultipartFile file) {
Map<String, Object> response = new HashMap<>();
try {
if (file.isEmpty()) {
response.put("success", false);
response.put("message", "文件不能为空");
return ResponseEntity.badRequest().body(response);
}
String url = workflowVideoService.uploadVideo(file);
response.put("success", true);
response.put("url", url);
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("/api/admin/workflow-videos/upload-thumbnail")
public ResponseEntity<Map<String, Object>> uploadThumbnail(@RequestParam("file") MultipartFile file) {
Map<String, Object> response = new HashMap<>();
try {
if (file.isEmpty()) {
response.put("success", false);
response.put("message", "文件不能为空");
return ResponseEntity.badRequest().body(response);
}
String url = workflowVideoService.uploadThumbnail(file);
response.put("success", true);
response.put("url", url);
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);
}
}
}

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,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,299 @@
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;
// 构造函数
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 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,124 @@
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 = "novel_comic_tasks")
public class NovelComicTask {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 100)
private String taskId; // Coze executeId
@Column(nullable = false, length = 100)
private String username;
@Column(length = 200)
private String theme; // 主题(选填)
@Column(columnDefinition = "TEXT")
private String storyBackground; // 故事背景
@Column(columnDefinition = "TEXT")
private String storyScript; // 故事文案
@Column(length = 500)
private String musicUrl; // 背景音乐 COS URL
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private TaskStatus status;
@Column(columnDefinition = "LONGTEXT")
private String result; // 生成结果
@Column(columnDefinition = "TEXT")
private String errorMessage;
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime updatedAt;
@Column
private LocalDateTime completedAt;
public enum TaskStatus {
PENDING, PROCESSING, COMPLETED, FAILED
}
public NovelComicTask() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
this.status = TaskStatus.PROCESSING;
}
public NovelComicTask(String taskId, String username, String theme,
String storyBackground, String storyScript, String musicUrl) {
this();
this.taskId = taskId;
this.username = username;
this.theme = theme;
this.storyBackground = storyBackground;
this.storyScript = storyScript;
this.musicUrl = musicUrl;
}
public void markCompleted(String result) {
this.status = TaskStatus.COMPLETED;
this.result = result;
this.updatedAt = LocalDateTime.now();
this.completedAt = LocalDateTime.now();
}
public void markFailed(String errorMessage) {
this.status = TaskStatus.FAILED;
this.errorMessage = errorMessage;
this.updatedAt = LocalDateTime.now();
this.completedAt = 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 getTheme() { return theme; }
public void setTheme(String theme) { this.theme = theme; }
public String getStoryBackground() { return storyBackground; }
public void setStoryBackground(String storyBackground) { this.storyBackground = storyBackground; }
public String getStoryScript() { return storyScript; }
public void setStoryScript(String storyScript) { this.storyScript = storyScript; }
public String getMusicUrl() { return musicUrl; }
public void setMusicUrl(String musicUrl) { this.musicUrl = musicUrl; }
public TaskStatus getStatus() { return status; }
public void setStatus(TaskStatus status) { this.status = status; }
public String getResult() { return result; }
public void setResult(String result) { this.result = result; }
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,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(name = "order_id", 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,206 @@
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;
import com.example.demo.model.enums.CommonTaskType;
/**
* 积分冻结记录实体
* 记录每次积分冻结的详细信息
*/
@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 CommonTaskType 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;
/**
* 任务类型枚举
*/
// TaskType 枚举已统一到 com.example.demo.model.enums.CommonTaskType
/**
* 冻结状态枚举
*/
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, CommonTaskType 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 CommonTaskType getTaskType() {
return taskType;
}
public void setTaskType(CommonTaskType 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,228 @@
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; // 16:9, 4:3, 1:1, 3:4, 9:16
@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 = "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; }
}

View File

@@ -0,0 +1,258 @@
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;
/** 文生视频消耗资源点 */
@NotNull
@Min(0)
@Column(nullable = false)
private Integer textToVideoPoints = 30;
/** 图生视频消耗资源点 */
@NotNull
@Min(0)
@Column(nullable = false)
private Integer imageToVideoPoints = 30;
/** 分镜图生成消耗资源点 */
@NotNull
@Min(0)
@Column(nullable = false)
private Integer storyboardImagePoints = 30;
/** 分镜视频生成消耗资源点 */
@NotNull
@Min(0)
@Column(nullable = false)
private Integer storyboardVideoPoints = 30;
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 Integer getTextToVideoPoints() {
return textToVideoPoints;
}
public void setTextToVideoPoints(Integer textToVideoPoints) {
this.textToVideoPoints = textToVideoPoints;
}
public Integer getImageToVideoPoints() {
return imageToVideoPoints;
}
public void setImageToVideoPoints(Integer imageToVideoPoints) {
this.imageToVideoPoints = imageToVideoPoints;
}
public Integer getStoryboardImagePoints() {
return storyboardImagePoints;
}
public void setStoryboardImagePoints(Integer storyboardImagePoints) {
this.storyboardImagePoints = storyboardImagePoints;
}
public Integer getStoryboardVideoPoints() {
return storyboardVideoPoints;
}
public void setStoryboardVideoPoints(Integer storyboardVideoPoints) {
this.storyboardVideoPoints = storyboardVideoPoints;
}
}

View File

@@ -0,0 +1,285 @@
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;
import com.example.demo.model.enums.CommonTaskType;
/**
* 任务队列实体task_queue 表)
*
* 职责:任务调度与执行引擎的持久化层。
* - 用户提交任务后先入此表排队PENDING
* - TaskQueueService 通过内存 BlockingQueue 消费,状态变为 PROCESSING
* - 任务执行完毕后更新为 COMPLETED 或 FAILED
*
* 注意:此表与 task_status 表职责不同,不可合并。
* task_queue 负责"调度执行"task_status 负责"轮询外部 API 结果"。
*
* @see TaskStatus
* @see com.example.demo.service.TaskQueueService
*/
@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 CommonTaskType 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;
/**
* 任务类型枚举
*/
// TaskType 枚举已统一到 com.example.demo.model.enums.CommonTaskType
/**
* 队列状态枚举
*/
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, CommonTaskType 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 CommonTaskType getTaskType() {
return taskType;
}
public void setTaskType(CommonTaskType 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;
import com.example.demo.model.enums.CommonTaskType;
/**
* 任务状态轮询实体task_status 表)
*
* 职责:跟踪已提交到外部 AI API 的任务的轮询状态。
* - 任务提交到外部 API 后,在此表创建记录,存储 externalTaskId
* - TaskStatusPollingService 定期轮询外部 API更新 status / resultUrl
* - 轮询完成后通过级联更新同步到 task_queue、业务任务表和 user_works
*
* 注意:此表与 task_queue 表职责不同,不可合并。
* task_status 负责"轮询外部 API 结果"task_queue 负责"调度执行"。
*
* @see TaskQueue
* @see com.example.demo.service.TaskStatusPollingService
*/
@Entity
@Table(name = "task_status")
public class TaskStatus {
// TaskType 枚举已统一到 com.example.demo.model.enums.CommonTaskType
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 CommonTaskType 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, CommonTaskType 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 CommonTaskType getTaskType() {
return taskType;
}
public void setTaskType(CommonTaskType 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,160 @@
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; // 16:9, 4:3, 1:1, 3:4, 9:16
@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;
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; }
}

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,414 @@
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;
import com.example.demo.model.enums.CommonTaskType;
/**
* 用户作品实体
* 记录用户生成的视频作品
*/
@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 CommonTaskType 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; // 标签,用逗号分隔
@Column(name = "uploaded_images", columnDefinition = "LONGTEXT")
private String uploadedImages; // 用户上传的参考图片JSON数组用于"做同款"功能恢复(分镜图阶段)
@Column(name = "video_reference_images", columnDefinition = "LONGTEXT")
private String videoReferenceImages; // 视频阶段用户上传的参考图片JSON数组用于分镜视频"做同款"功能恢复
@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;
/**
* 作品类型枚举
*/
// WorkType 枚举已统一到 com.example.demo.model.enums.CommonTaskType
/**
* 作品状态枚举
*/
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, CommonTaskType 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 CommonTaskType getWorkType() {
return workType;
}
public void setWorkType(CommonTaskType 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;
}
}

View File

@@ -0,0 +1,86 @@
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 = "workflow_videos")
public class WorkflowVideo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "title", nullable = false, length = 200)
private String title;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "video_url", nullable = false, columnDefinition = "TEXT")
private String videoUrl;
@Column(name = "thumbnail_url", columnDefinition = "TEXT")
private String thumbnailUrl;
@Column(name = "tags", length = 500)
private String tags;
@Column(name = "sort_order", nullable = false)
private Integer sortOrder = 0;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
public WorkflowVideo() {
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 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 getVideoUrl() { return videoUrl; }
public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; }
public String getThumbnailUrl() { return thumbnailUrl; }
public void setThumbnailUrl(String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; }
public String getTags() { return tags; }
public void setTags(String tags) { this.tags = tags; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
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,26 @@
package com.example.demo.model.enums;
/**
* 统一的任务类型枚举
*
* 替代之前在 TaskStatus、TaskQueue、PointsFreezeRecord、UserWork 中各自定义的 TaskType/WorkType。
* 所有任务类型统一在此定义,避免重复和不一致。
*
* 注意枚举值名称必须与数据库中已有的字符串值一致JPA 使用 @Enumerated(EnumType.STRING))。
*/
public enum CommonTaskType {
TEXT_TO_VIDEO("文生视频"),
IMAGE_TO_VIDEO("图生视频"),
STORYBOARD_VIDEO("分镜视频"),
STORYBOARD_IMAGE("分镜图");
private final String description;
CommonTaskType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}

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,18 @@
package com.example.demo.repository;
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.stereotype.Repository;
import com.example.demo.model.NovelComicTask;
@Repository
public interface NovelComicTaskRepository extends JpaRepository<NovelComicTask, Long> {
Optional<NovelComicTask> findByTaskId(String taskId);
Page<NovelComicTask> findByUsernameOrderByCreatedAtDesc(String username, Pageable pageable);
}

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,119 @@
package com.example.demo.repository;
import com.example.demo.model.TaskStatus;
import com.example.demo.model.enums.CommonTaskType;
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(CommonTaskType 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);
}

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