Initial commit: 添加项目代码
This commit is contained in:
24
demo/src/.claude/settings.local.json
Normal file
24
demo/src/.claude/settings.local.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
30
demo/src/main/java/com/example/demo/DemoApplication.java
Normal file
30
demo/src/main/java/com/example/demo/DemoApplication.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
46
demo/src/main/java/com/example/demo/config/AsyncConfig.java
Normal file
46
demo/src/main/java/com/example/demo/config/AsyncConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
49
demo/src/main/java/com/example/demo/config/CacheConfig.java
Normal file
49
demo/src/main/java/com/example/demo/config/CacheConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
69
demo/src/main/java/com/example/demo/config/CosConfig.java
Normal file
69
demo/src/main/java/com/example/demo/config/CosConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
188
demo/src/main/java/com/example/demo/config/DataInitializer.java
Normal file
188
demo/src/main/java/com/example/demo/config/DataInitializer.java
Normal 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(专业版)");
|
||||
}
|
||||
}
|
||||
153
demo/src/main/java/com/example/demo/config/DynamicApiConfig.java
Normal file
153
demo/src/main/java/com/example/demo/config/DynamicApiConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
188
demo/src/main/java/com/example/demo/config/OpenApiConfig.java
Normal file
188
demo/src/main/java/com/example/demo/config/OpenApiConfig.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
117
demo/src/main/java/com/example/demo/config/PayPalConfig.java
Normal file
117
demo/src/main/java/com/example/demo/config/PayPalConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
348
demo/src/main/java/com/example/demo/config/PaymentConfig.java
Normal file
348
demo/src/main/java/com/example/demo/config/PaymentConfig.java
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 "[无法解码内容]";
|
||||
}
|
||||
}
|
||||
}
|
||||
145
demo/src/main/java/com/example/demo/config/SecurityConfig.java
Normal file
145
demo/src/main/java/com/example/demo/config/SecurityConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
98
demo/src/main/java/com/example/demo/config/WebMvcConfig.java
Normal file
98
demo/src/main/java/com/example/demo/config/WebMvcConfig.java
Normal 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,避免冲突
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
// 所有等级统一使用 displayName(free=免费会员, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()); // 视频URL(JSON数组)
|
||||
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", "重试任务失败"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
demo/src/main/java/com/example/demo/dto/MailMessage.java
Normal file
82
demo/src/main/java/com/example/demo/dto/MailMessage.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
// 忽略更新失败
|
||||
}
|
||||
}
|
||||
}
|
||||
299
demo/src/main/java/com/example/demo/model/ImageToVideoTask.java
Normal file
299
demo/src/main/java/com/example/demo/model/ImageToVideoTask.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
145
demo/src/main/java/com/example/demo/model/MembershipLevel.java
Normal file
145
demo/src/main/java/com/example/demo/model/MembershipLevel.java
Normal 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;
|
||||
}
|
||||
}
|
||||
124
demo/src/main/java/com/example/demo/model/NovelComicTask.java
Normal file
124
demo/src/main/java/com/example/demo/model/NovelComicTask.java
Normal 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; }
|
||||
}
|
||||
314
demo/src/main/java/com/example/demo/model/Order.java
Normal file
314
demo/src/main/java/com/example/demo/model/Order.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
134
demo/src/main/java/com/example/demo/model/OrderItem.java
Normal file
134
demo/src/main/java/com/example/demo/model/OrderItem.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
demo/src/main/java/com/example/demo/model/OrderStatus.java
Normal file
26
demo/src/main/java/com/example/demo/model/OrderStatus.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
21
demo/src/main/java/com/example/demo/model/OrderType.java
Normal file
21
demo/src/main/java/com/example/demo/model/OrderType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
272
demo/src/main/java/com/example/demo/model/Payment.java
Normal file
272
demo/src/main/java/com/example/demo/model/Payment.java
Normal 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;
|
||||
}
|
||||
}
|
||||
20
demo/src/main/java/com/example/demo/model/PaymentMethod.java
Normal file
20
demo/src/main/java/com/example/demo/model/PaymentMethod.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
21
demo/src/main/java/com/example/demo/model/PaymentStatus.java
Normal file
21
demo/src/main/java/com/example/demo/model/PaymentStatus.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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; // 分镜图URL(Base64编码的图片,可能非常大)- 网格图
|
||||
|
||||
@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; // 多个视频任务ID(JSON数组,每张图片对应一个视频任务)
|
||||
|
||||
@Column(name = "video_urls", columnDefinition = "LONGTEXT")
|
||||
private String videoUrls; // 多个视频URL(JSON数组,用于拼接)
|
||||
|
||||
@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; }
|
||||
}
|
||||
258
demo/src/main/java/com/example/demo/model/SystemSettings.java
Normal file
258
demo/src/main/java/com/example/demo/model/SystemSettings.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
285
demo/src/main/java/com/example/demo/model/TaskQueue.java
Normal file
285
demo/src/main/java/com/example/demo/model/TaskQueue.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
277
demo/src/main/java/com/example/demo/model/TaskStatus.java
Normal file
277
demo/src/main/java/com/example/demo/model/TaskStatus.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
160
demo/src/main/java/com/example/demo/model/TextToVideoTask.java
Normal file
160
demo/src/main/java/com/example/demo/model/TextToVideoTask.java
Normal 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; }
|
||||
}
|
||||
284
demo/src/main/java/com/example/demo/model/User.java
Normal file
284
demo/src/main/java/com/example/demo/model/User.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
292
demo/src/main/java/com/example/demo/model/UserErrorLog.java
Normal file
292
demo/src/main/java/com/example/demo/model/UserErrorLog.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
118
demo/src/main/java/com/example/demo/model/UserMembership.java
Normal file
118
demo/src/main/java/com/example/demo/model/UserMembership.java
Normal 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;
|
||||
}
|
||||
}
|
||||
414
demo/src/main/java/com/example/demo/model/UserWork.java
Normal file
414
demo/src/main/java/com/example/demo/model/UserWork.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
86
demo/src/main/java/com/example/demo/model/WorkflowVideo.java
Normal file
86
demo/src/main/java/com/example/demo/model/WorkflowVideo.java
Normal 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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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 表管理
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user