feat: 添加任务状态级联触发器,优化支付和做同款功能
主要更新: - 添加 MySQL 触发器实现 task_status 表到其他表的状态级联 - 移除控制器中的多表状态检查代码 - 完善做同款功能,支持参数传递 - 支付宝 USD 转 CNY 汇率转换 - 修复状态枚举映射问题 注意: 触发器仅在 task_status 更新时触发,部分代码仍直接更新业务表
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,9 @@ public class CosConfig {
|
||||
@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()) {
|
||||
@@ -59,4 +62,8 @@ public class CosConfig {
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ public class PayPalConfig {
|
||||
@Value("${paypal.mode:sandbox}")
|
||||
private String mode;
|
||||
|
||||
@Value("${paypal.success-url:http://localhost:8080/payment/paypal/success}")
|
||||
@Value("${paypal.success-url:https://vionow.com/api/payment/paypal/success}")
|
||||
private String successUrl;
|
||||
|
||||
@Value("${paypal.cancel-url:http://localhost:8080/payment/paypal/cancel}")
|
||||
@Value("${paypal.cancel-url:https://vionow.com/api/payment/paypal/cancel}")
|
||||
private String cancelUrl;
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,10 +81,10 @@ public class SecurityConfig {
|
||||
.requestMatchers("/api/payments/**").authenticated() // 其他支付接口需要认证
|
||||
.requestMatchers("/api/image-to-video/**").authenticated() // 图生视频接口需要认证
|
||||
.requestMatchers("/api/text-to-video/**").authenticated() // 文生视频接口需要认证
|
||||
.requestMatchers("/api/dashboard/**").hasRole("ADMIN") // 仪表盘API需要管理员权限
|
||||
.requestMatchers("/api/admin/**").hasRole("ADMIN") // 管理员API需要管理员权限
|
||||
.requestMatchers("/settings", "/settings/**").hasRole("ADMIN")
|
||||
.requestMatchers("/users/**").hasRole("ADMIN")
|
||||
.requestMatchers("/api/dashboard/**").hasAnyRole("ADMIN", "SUPER_ADMIN") // 仪表盘API需要管理员权限
|
||||
.requestMatchers("/api/admin/**").hasAnyRole("ADMIN", "SUPER_ADMIN") // 管理员API需要管理员权限
|
||||
.requestMatchers("/settings", "/settings/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
|
||||
.requestMatchers("/users/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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;
|
||||
@@ -73,9 +75,20 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
// 确保路径使用正斜杠(URL格式)
|
||||
String resourceLocation = "file:" + uploadDirPath.toAbsolutePath().toString().replace("\\", "/") + "/";
|
||||
|
||||
// 上传文件缓存7天(视频/图片等媒体文件)
|
||||
registry.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations(resourceLocation)
|
||||
.setCachePeriod(3600); // 缓存1小时
|
||||
.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,避免冲突
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.example.demo.model.SystemSettings;
|
||||
import com.example.demo.repository.TaskStatusRepository;
|
||||
import com.example.demo.service.UserService;
|
||||
import com.example.demo.service.SystemSettingsService;
|
||||
import com.example.demo.service.OnlineStatsService;
|
||||
import com.example.demo.util.JwtUtils;
|
||||
|
||||
/**
|
||||
@@ -49,6 +50,9 @@ public class AdminController {
|
||||
@Autowired
|
||||
private TaskStatusRepository taskStatusRepository;
|
||||
|
||||
@Autowired
|
||||
private OnlineStatsService onlineStatsService;
|
||||
|
||||
/**
|
||||
* 给用户增加积分
|
||||
*/
|
||||
@@ -582,5 +586,29 @@ public class AdminController {
|
||||
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(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取在线统计失败", e);
|
||||
response.put("success", false);
|
||||
response.put("message", "获取在线统计失败: " + e.getMessage());
|
||||
return ResponseEntity.status(500).body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,11 +100,14 @@ public class AlipayCallbackController {
|
||||
outTradeNo, tradeNo, tradeStatus, totalAmount, appId);
|
||||
|
||||
// 查找支付记录
|
||||
logger.info("正在查找支付记录,订单号: {}", outTradeNo);
|
||||
Optional<Payment> paymentOpt = paymentRepository.findByOrderId(outTradeNo);
|
||||
if (!paymentOpt.isPresent()) {
|
||||
logger.error("支付记录不存在: {}", outTradeNo);
|
||||
logger.error("❌ 支付记录不存在: orderId={}", outTradeNo);
|
||||
logger.error("请检查数据库中是否存在该订单号的支付记录");
|
||||
return "failure";
|
||||
}
|
||||
logger.info("✅ 找到支付记录: paymentId={}", paymentOpt.get().getId());
|
||||
|
||||
Payment payment = paymentOpt.get();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.example.demo.model.User;
|
||||
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 jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -187,6 +188,12 @@ public class AuthApiController {
|
||||
|
||||
// 直接创建用户对象并设置所有必要字段
|
||||
user = new User();
|
||||
|
||||
// 生成唯一用户ID(UID + yyMMdd + 4位随机字符)
|
||||
String generatedUserId = UserIdGenerator.generate();
|
||||
user.setUserId(generatedUserId);
|
||||
logger.info("邮箱验证码登录 - 生成用户ID: {}", generatedUserId);
|
||||
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
user.setPasswordHash(""); // 邮箱登录不需要密码
|
||||
@@ -377,14 +384,16 @@ public class AuthApiController {
|
||||
|
||||
/**
|
||||
* 修改当前登录用户密码
|
||||
* 支持首次设置密码(isFirstTimeSetup=true时不验证旧密码)
|
||||
*/
|
||||
@PostMapping("/change-password")
|
||||
public ResponseEntity<Map<String, Object>> changePassword(@RequestBody Map<String, String> requestBody,
|
||||
public ResponseEntity<Map<String, Object>> changePassword(@RequestBody Map<String, Object> requestBody,
|
||||
Authentication authentication,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
String oldPassword = requestBody.get("oldPassword");
|
||||
String newPassword = requestBody.get("newPassword");
|
||||
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;
|
||||
@@ -410,7 +419,9 @@ public class AuthApiController {
|
||||
.body(createErrorResponse("用户未登录或会话已失效"));
|
||||
}
|
||||
|
||||
userService.changePassword(user.getId(), oldPassword, newPassword);
|
||||
// changePassword 方法内部已处理首次设置密码场景
|
||||
// 如果用户没有密码,可以直接设置新密码,不需要验证旧密码
|
||||
userService.changePassword(user.getId(), isFirstTimeSetup ? null : oldPassword, newPassword);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
|
||||
@@ -23,6 +23,7 @@ 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")
|
||||
@@ -46,43 +47,16 @@ public class DashboardApiController {
|
||||
|
||||
@Autowired
|
||||
private com.example.demo.service.UserService userService;
|
||||
|
||||
@Autowired
|
||||
private DashboardService dashboardService;
|
||||
|
||||
// 获取仪表盘概览数据
|
||||
@GetMapping("/overview")
|
||||
public ResponseEntity<Map<String, Object>> getDashboardOverview() {
|
||||
try {
|
||||
Map<String, Object> overview = new HashMap<>();
|
||||
|
||||
// 用户总数
|
||||
long totalUsers = userRepository.count();
|
||||
overview.put("totalUsers", totalUsers);
|
||||
|
||||
// 付费用户数(有会员的用户)
|
||||
long paidUsers = userMembershipRepository.countByStatus("ACTIVE");
|
||||
overview.put("paidUsers", paidUsers);
|
||||
|
||||
// 今日收入(今日完成的支付)
|
||||
LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
|
||||
LocalDateTime todayEnd = LocalDateTime.now().withHour(23).withMinute(59).withSecond(59);
|
||||
|
||||
Double todayRevenue = paymentRepository.findTodayRevenue(todayStart, todayEnd);
|
||||
overview.put("todayRevenue", todayRevenue != null ? todayRevenue : 0.0);
|
||||
|
||||
// 总订单数
|
||||
long totalOrders = orderRepository.count();
|
||||
overview.put("totalOrders", totalOrders);
|
||||
|
||||
// 总收入
|
||||
Double totalRevenue = paymentRepository.findTotalRevenue();
|
||||
overview.put("totalRevenue", totalRevenue != null ? totalRevenue : 0.0);
|
||||
|
||||
// 本月收入
|
||||
LocalDateTime monthStart = LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0);
|
||||
LocalDateTime monthEnd = LocalDateTime.now();
|
||||
|
||||
Double monthRevenue = paymentRepository.findRevenueByDateRange(monthStart, monthEnd);
|
||||
overview.put("monthRevenue", monthRevenue != null ? monthRevenue : 0.0);
|
||||
|
||||
// 使用 DashboardService 获取包含同比变化的完整数据
|
||||
Map<String, Object> overview = dashboardService.getDashboardOverview();
|
||||
return ResponseEntity.ok(overview);
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -149,8 +123,8 @@ public class DashboardApiController {
|
||||
// 总用户数
|
||||
long totalUsers = userRepository.count();
|
||||
|
||||
// 付费用户数
|
||||
long paidUsers = userMembershipRepository.countByStatus("ACTIVE");
|
||||
// 付费用户数(使用 Payment 表统计,与卡片统计方式一致)
|
||||
long paidUsers = paymentRepository.countDistinctUsersByStatus(com.example.demo.model.PaymentStatus.SUCCESS);
|
||||
|
||||
// 计算转化率
|
||||
double conversionRate = totalUsers > 0 ? (double) paidUsers / totalUsers * 100 : 0.0;
|
||||
@@ -179,7 +153,7 @@ public class DashboardApiController {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取按月转化率数据
|
||||
// 获取按月转化率数据(累计转化率:截止到该月底的累计付费用户/累计总用户)
|
||||
private List<Map<String, Object>> getMonthlyConversionRate(int year) {
|
||||
List<Map<String, Object>> monthlyData = new ArrayList<>();
|
||||
|
||||
@@ -187,20 +161,20 @@ public class DashboardApiController {
|
||||
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);
|
||||
// 计算截止到该月底的累计总用户数
|
||||
LocalDateTime monthEnd = LocalDateTime.of(year, month, 1, 0, 0, 0).plusMonths(1).minusSeconds(1);
|
||||
|
||||
long monthTotalUsers = userRepository.countByCreatedAtBetween(monthStart, monthEnd);
|
||||
long cumulativeTotalUsers = userRepository.countByCreatedAtBefore(monthEnd);
|
||||
|
||||
// 计算该月新增的付费用户数(会员开始时间在该月)
|
||||
long monthPaidUsers = userMembershipRepository.countByStartDateBetween(monthStart, monthEnd);
|
||||
// 计算截止到该月底的累计付费用户数(使用 Payment 表统计,与卡片统计方式一致)
|
||||
long cumulativePaidUsers = paymentRepository.countDistinctUsersByStatusAndPaidAtBefore(
|
||||
com.example.demo.model.PaymentStatus.SUCCESS, monthEnd);
|
||||
|
||||
// 计算该月转化率
|
||||
double monthConversionRate = monthTotalUsers > 0 ? (double) monthPaidUsers / monthTotalUsers * 100 : 0.0;
|
||||
// 计算累计转化率
|
||||
double monthConversionRate = cumulativeTotalUsers > 0 ? (double) cumulativePaidUsers / cumulativeTotalUsers * 100 : 0.0;
|
||||
|
||||
monthData.put("totalUsers", monthTotalUsers);
|
||||
monthData.put("paidUsers", monthPaidUsers);
|
||||
monthData.put("totalUsers", cumulativeTotalUsers);
|
||||
monthData.put("paidUsers", cumulativePaidUsers);
|
||||
monthData.put("conversionRate", Math.round(monthConversionRate * 100.0) / 100.0);
|
||||
|
||||
monthlyData.add(monthData);
|
||||
@@ -247,25 +221,38 @@ public class DashboardApiController {
|
||||
@GetMapping("/system-status")
|
||||
public ResponseEntity<Map<String, Object>> getSystemStatus() {
|
||||
try {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
// 当前在线用户(基于最近10分钟内有活动的用户)
|
||||
long onlineUsers = userService.countOnlineUsers();
|
||||
status.put("onlineUsers", onlineUsers);
|
||||
data.put("onlineUsers", onlineUsers);
|
||||
data.put("maxUsers", 500);
|
||||
|
||||
// 系统运行时间
|
||||
status.put("systemUptime", "48小时32分");
|
||||
// 系统运行时间(从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 + "分");
|
||||
|
||||
// 数据库连接状态
|
||||
status.put("databaseStatus", "正常");
|
||||
data.put("databaseStatus", "正常");
|
||||
|
||||
// 服务状态
|
||||
status.put("serviceStatus", "运行中");
|
||||
data.put("serviceStatus", "运行中");
|
||||
|
||||
return ResponseEntity.ok(status);
|
||||
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);
|
||||
|
||||
@@ -8,14 +8,15 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
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;
|
||||
|
||||
@ControllerAdvice
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
@@ -92,6 +93,24 @@ public class GlobalExceptionHandler {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理其他所有异常
|
||||
*/
|
||||
|
||||
@@ -242,6 +242,8 @@ public class ImageToVideoApiController {
|
||||
response.put("message", "无权限访问此任务");
|
||||
return ResponseEntity.status(403).body(response);
|
||||
}
|
||||
|
||||
// 状态同步已通过数据库触发器实现,无需代码检查
|
||||
|
||||
response.put("success", true);
|
||||
Map<String, Object> taskData = new HashMap<>();
|
||||
|
||||
@@ -27,6 +27,10 @@ import com.example.demo.model.UserMembership;
|
||||
import com.example.demo.repository.MembershipLevelRepository;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/members")
|
||||
@@ -42,16 +46,35 @@ public class MemberApiController {
|
||||
@Autowired
|
||||
private MembershipLevelRepository membershipLevelRepository;
|
||||
|
||||
@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 level,
|
||||
@RequestParam(required = false) String status) {
|
||||
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, pageSize, Sort.by("createdAt").descending());
|
||||
Page<User> userPage = userRepository.findAll(pageable);
|
||||
|
||||
Page<User> userPage;
|
||||
// 根据 status 参数筛选用户
|
||||
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 -> {
|
||||
@@ -160,8 +183,27 @@ public class MemberApiController {
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> updateMember(
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, Object> updateData) {
|
||||
@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();
|
||||
@@ -174,29 +216,69 @@ public class MemberApiController {
|
||||
user.setUsername((String) updateData.get("username"));
|
||||
}
|
||||
if (updateData.containsKey("points")) {
|
||||
user.setPoints((Integer) updateData.get("points"));
|
||||
Object pointsObj = updateData.get("points");
|
||||
if (pointsObj instanceof Number) {
|
||||
user.setPoints(((Number) pointsObj).intValue());
|
||||
}
|
||||
}
|
||||
|
||||
userRepository.save(user);
|
||||
// 只有超级管理员可以修改角色,且不能修改超级管理员的角色
|
||||
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,忽略(不允许通过此接口设置超级管理员)
|
||||
}
|
||||
|
||||
// 更新会员等级
|
||||
if (updateData.containsKey("level")) {
|
||||
String levelName = (String) updateData.get("level");
|
||||
userService.save(user);
|
||||
|
||||
// 更新会员等级和到期时间
|
||||
String levelName = (String) updateData.get("level");
|
||||
String expiryDateStr = (String) updateData.get("expiryDate");
|
||||
|
||||
if (levelName != null) {
|
||||
Optional<MembershipLevel> levelOpt = membershipLevelRepository
|
||||
.findByDisplayName(levelName);
|
||||
|
||||
if (levelOpt.isPresent()) {
|
||||
MembershipLevel level = levelOpt.get();
|
||||
|
||||
// 更新或创建会员信息
|
||||
// 查找或创建会员信息
|
||||
Optional<UserMembership> membershipOpt = userMembershipRepository
|
||||
.findByUserIdAndStatus(user.getId(), "ACTIVE");
|
||||
|
||||
UserMembership membership;
|
||||
if (membershipOpt.isPresent()) {
|
||||
UserMembership membership = membershipOpt.get();
|
||||
membership.setMembershipLevelId(level.getId());
|
||||
userMembershipRepository.save(membership);
|
||||
membership = membershipOpt.get();
|
||||
} else {
|
||||
// 创建新的会员记录
|
||||
membership = new UserMembership();
|
||||
membership.setUserId(user.getId());
|
||||
membership.setStatus("ACTIVE");
|
||||
membership.setStartDate(java.time.LocalDateTime.now());
|
||||
// 根据会员等级的 durationDays 计算到期时间
|
||||
int durationDays = level.getDurationDays() != null ? level.getDurationDays() : 365;
|
||||
membership.setEndDate(java.time.LocalDateTime.now().plusDays(durationDays));
|
||||
}
|
||||
|
||||
membership.setMembershipLevelId(level.getId());
|
||||
|
||||
// 更新到期时间
|
||||
if (expiryDateStr != null && !expiryDateStr.isEmpty()) {
|
||||
try {
|
||||
java.time.LocalDate expiryDate = java.time.LocalDate.parse(expiryDateStr);
|
||||
membership.setEndDate(expiryDate.atStartOfDay());
|
||||
} catch (Exception e) {
|
||||
// 日期格式错误,忽略
|
||||
}
|
||||
}
|
||||
|
||||
userMembershipRepository.save(membership);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,17 +298,59 @@ public class MemberApiController {
|
||||
|
||||
// 删除会员
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteMember(@PathVariable Long 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();
|
||||
user.setIsActive(false);
|
||||
userRepository.save(user);
|
||||
|
||||
// 不能删除自己
|
||||
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);
|
||||
@@ -244,21 +368,60 @@ public class MemberApiController {
|
||||
|
||||
// 批量删除会员
|
||||
@DeleteMapping("/batch")
|
||||
public ResponseEntity<Map<String, Object>> deleteMembers(@RequestBody Map<String, List<Long>> request) {
|
||||
@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);
|
||||
users.forEach(user -> user.setIsActive(false));
|
||||
userRepository.saveAll(users);
|
||||
|
||||
// 过滤掉自己、超级管理员,普通管理员还需要过滤掉其他管理员
|
||||
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", "批量删除成功");
|
||||
response.put("deletedCount", users.size());
|
||||
response.put("message", skipped > 0
|
||||
? "批量删除成功,已跳过 " + skipped + " 个管理员账号"
|
||||
: "批量删除成功");
|
||||
response.put("deletedCount", toDelete.size());
|
||||
response.put("skippedCount", skipped);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
@@ -270,6 +433,160 @@ public class MemberApiController {
|
||||
}
|
||||
}
|
||||
|
||||
// 封禁/解封会员
|
||||
@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() {
|
||||
|
||||
@@ -82,7 +82,7 @@ public class OrderApiController {
|
||||
|
||||
// 获取订单列表
|
||||
Page<Order> orderPage;
|
||||
if (user.getRole().equals("ROLE_ADMIN")) {
|
||||
if ((user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
|
||||
// 管理员可以查看所有订单
|
||||
orderPage = orderService.findAllOrders(pageable, status, type, search);
|
||||
} else {
|
||||
@@ -176,7 +176,7 @@ public class OrderApiController {
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
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", "无权限访问此订单");
|
||||
@@ -254,7 +254,7 @@ public class OrderApiController {
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN")) && !order.getUser().getId().equals(user.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
@@ -297,7 +297,7 @@ public class OrderApiController {
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN")) && !order.getUser().getId().equals(user.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
@@ -331,7 +331,7 @@ public class OrderApiController {
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 只有管理员可以发货
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
@@ -364,7 +364,7 @@ public class OrderApiController {
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 只有管理员可以完成订单
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
@@ -457,7 +457,7 @@ public class OrderApiController {
|
||||
|
||||
// 获取统计数据
|
||||
Map<String, Object> stats;
|
||||
if (user.getRole().equals("ROLE_ADMIN")) {
|
||||
if ((user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
|
||||
// 管理员查看所有订单统计
|
||||
stats = orderService.getOrderStats();
|
||||
} else {
|
||||
@@ -491,7 +491,7 @@ public class OrderApiController {
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN")) && !order.getUser().getId().equals(user.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限删除此订单"));
|
||||
}
|
||||
@@ -522,7 +522,7 @@ public class OrderApiController {
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 只有管理员可以批量删除
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
if (!(user.getRole().equals("ROLE_ADMIN") || user.getRole().equals("ROLE_SUPER_ADMIN"))) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限批量删除订单"));
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ public class OrderController {
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 检查权限:用户只能查看自己的订单,管理员可以查看所有订单
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
model.addAttribute("error", "无权限访问此订单");
|
||||
return "orders/detail";
|
||||
}
|
||||
@@ -230,7 +230,7 @@ public class OrderController {
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
@@ -273,7 +273,7 @@ public class OrderController {
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
@@ -303,7 +303,7 @@ public class OrderController {
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 只有管理员可以发货
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN")) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
@@ -332,7 +332,7 @@ public class OrderController {
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 只有管理员可以完成订单
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN")) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
@@ -416,7 +416,7 @@ public class OrderController {
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 只有管理员可以访问
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !user.getRole().equals("ROLE_SUPER_ADMIN")) {
|
||||
model.addAttribute("error", "无权限访问");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
|
||||
@@ -37,10 +37,13 @@ public class PayPalController {
|
||||
|
||||
/**
|
||||
* 创建PayPal支付
|
||||
* 支持两种模式:
|
||||
* 1. 传入 paymentId:使用已有的支付记录
|
||||
* 2. 不传入 paymentId:创建新的支付记录
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建PayPal支付", description = "创建PayPal支付订单并返回支付URL")
|
||||
public ResponseEntity<Map<String, Object>> createPayment(@RequestBody Map<String, String> request) {
|
||||
public ResponseEntity<Map<String, Object>> createPayment(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
logger.info("=== 创建PayPal支付请求 ===");
|
||||
logger.info("请求参数: {}", request);
|
||||
@@ -52,19 +55,72 @@ public class PayPalController {
|
||||
return ResponseEntity.badRequest().body(errorResponse);
|
||||
}
|
||||
|
||||
String username = request.get("username");
|
||||
String orderId = request.get("orderId");
|
||||
String amount = request.get("amount");
|
||||
String method = request.get("method");
|
||||
Payment payment;
|
||||
|
||||
// 创建支付记录
|
||||
Payment payment = paymentService.createPayment(username, orderId, amount, method);
|
||||
// 检查是否传入了已有的 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();
|
||||
} 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");
|
||||
|
||||
payment = paymentService.createPayment(username, orderId, amount, method);
|
||||
}
|
||||
|
||||
// 调用 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", payment.getPaymentUrl());
|
||||
response.put("externalTransactionId", payment.getExternalTransactionId());
|
||||
response.put("paymentUrl", paypalUrl);
|
||||
response.put("externalTransactionId", paypalPaymentId);
|
||||
|
||||
logger.info("✅ PayPal支付创建成功: {}", response);
|
||||
|
||||
|
||||
@@ -540,65 +540,7 @@ public class PaymentApiController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/alipay/notify")
|
||||
public String alipayNotify(HttpServletRequest request) {
|
||||
logger.info("========== [API] 收到支付宝回调请求 ==========");
|
||||
logger.info("请求方法: {}", request.getMethod());
|
||||
logger.info("请求URL: {}", request.getRequestURL());
|
||||
logger.info("Content-Type: {}", request.getContentType());
|
||||
try {
|
||||
Map<String, String> params = new java.util.HashMap<>();
|
||||
|
||||
request.getParameterMap().forEach((key, values) -> {
|
||||
if (values != null && values.length > 0) {
|
||||
params.put(key, values[0]);
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
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("========== [API] 支付宝回调处理完成 ==========");
|
||||
return success ? "success" : "fail";
|
||||
} catch (Exception e) {
|
||||
logger.error("========== [API] 处理支付宝异步通知失败 ==========", e);
|
||||
return "fail";
|
||||
}
|
||||
}
|
||||
// 注意:支付宝异步通知接口已移至 AlipayCallbackController,避免路由冲突
|
||||
|
||||
/**
|
||||
* 创建支付宝支付
|
||||
@@ -657,7 +599,7 @@ public class PaymentApiController {
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 只有管理员可以删除
|
||||
if (!"ROLE_ADMIN".equals(user.getRole())) {
|
||||
if (!"ROLE_ADMIN".equals(user.getRole()) && !"ROLE_SUPER_ADMIN".equals(user.getRole())) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限删除支付记录"));
|
||||
}
|
||||
@@ -688,7 +630,7 @@ public class PaymentApiController {
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 只有管理员可以删除
|
||||
if (!"ROLE_ADMIN".equals(user.getRole())) {
|
||||
if (!"ROLE_ADMIN".equals(user.getRole()) && !"ROLE_SUPER_ADMIN".equals(user.getRole())) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限批量删除支付记录"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,8 @@ public class StoryboardVideoApiController {
|
||||
.body(Map.of("success", false, "message", "无权访问此任务"));
|
||||
}
|
||||
|
||||
// 状态同步已通过数据库触发器实现,无需代码检查
|
||||
|
||||
Map<String, Object> taskData = new HashMap<>();
|
||||
taskData.put("taskId", task.getTaskId());
|
||||
taskData.put("status", task.getStatus());
|
||||
|
||||
@@ -216,6 +216,8 @@ public class TextToVideoApiController {
|
||||
return ResponseEntity.status(404).body(response);
|
||||
}
|
||||
|
||||
// 状态同步已通过数据库触发器实现,无需代码检查
|
||||
|
||||
Map<String, Object> statusData = new HashMap<>();
|
||||
statusData.put("taskId", task.getTaskId());
|
||||
statusData.put("status", task.getStatus());
|
||||
|
||||
@@ -85,13 +85,15 @@ public class UserWorkApiController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的作品列表(只返回有resultUrl的作品)
|
||||
* 获取我的作品列表
|
||||
* @param includeProcessing 是否包含正在排队和生成中的作品,默认为true
|
||||
*/
|
||||
@GetMapping("/my-works")
|
||||
public ResponseEntity<Map<String, Object>> getMyWorks(
|
||||
@RequestHeader("Authorization") String token,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "1000") int size) {
|
||||
@RequestParam(defaultValue = "1000") int size,
|
||||
@RequestParam(defaultValue = "true") boolean includeProcessing) {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
@@ -107,7 +109,13 @@ public class UserWorkApiController {
|
||||
if (page < 0) page = 0;
|
||||
if (size <= 0) size = 1000; // 不设上限,默认1000条
|
||||
|
||||
Page<UserWork> works = userWorkService.getUserWorks(username, page, size);
|
||||
// 根据参数决定是否包含正在进行中的作品
|
||||
Page<UserWork> works;
|
||||
if (includeProcessing) {
|
||||
works = userWorkService.getAllUserWorks(username, page, size);
|
||||
} else {
|
||||
works = userWorkService.getUserWorks(username, page, size);
|
||||
}
|
||||
Map<String, Object> workStats = userWorkService.getUserWorkStats(username);
|
||||
|
||||
// 调试日志:检查返回的作品数据
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.example.demo.interceptor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -10,7 +8,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.service.UserService;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -51,14 +48,11 @@ public class UserActivityInterceptor implements HandlerInterceptor {
|
||||
|
||||
/**
|
||||
* 异步更新用户活跃时间
|
||||
* 使用专门的方法,不触发缓存清除
|
||||
*/
|
||||
private void updateUserActiveTimeAsync(String username) {
|
||||
try {
|
||||
User user = userService.findByUsernameOrNull(username);
|
||||
if (user != null) {
|
||||
user.setLastActiveTime(LocalDateTime.now());
|
||||
userService.save(user);
|
||||
}
|
||||
userService.updateLastActiveTime(username);
|
||||
} catch (Exception e) {
|
||||
// 忽略更新失败
|
||||
}
|
||||
|
||||
@@ -39,6 +39,12 @@ public interface ImageToVideoTaskRepository extends JpaRepository<ImageToVideoTa
|
||||
* 统计用户任务数量
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 根据状态查找任务列表
|
||||
|
||||
@@ -90,4 +90,16 @@ public interface PaymentRepository extends JpaRepository<Payment, Long> {
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ public interface TextToVideoTaskRepository extends JpaRepository<TextToVideoTask
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 根据状态查找任务列表
|
||||
*/
|
||||
|
||||
@@ -15,4 +15,12 @@ public interface UserMembershipRepository extends JpaRepository<UserMembership,
|
||||
long countByStatus(String status);
|
||||
|
||||
long countByStartDateBetween(LocalDateTime startDate, LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* 统计指定时间之前开始的会员数(用于累计统计)
|
||||
*/
|
||||
long countByStartDateBefore(LocalDateTime beforeTime);
|
||||
|
||||
// 根据用户ID删除会员信息
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ package com.example.demo.repository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
// 分页查询活跃用户(isActive=true)
|
||||
Page<User> findByIsActive(Boolean isActive, Pageable pageable);
|
||||
Optional<User> findByUsername(String username);
|
||||
Optional<User> findByNickname(String nickname);
|
||||
Optional<User> findByEmail(String email);
|
||||
@@ -28,6 +33,11 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
* 统计今日新增用户数
|
||||
*/
|
||||
long countByCreatedAtAfter(LocalDateTime startTime);
|
||||
|
||||
/**
|
||||
* 统计指定时间之前创建的用户数(用于累计统计)
|
||||
*/
|
||||
long countByCreatedAtBefore(LocalDateTime beforeTime);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ public interface UserWorkRepository extends JpaRepository<UserWork, Long> {
|
||||
/**
|
||||
* 根据用户名查找作品
|
||||
*/
|
||||
@Query("SELECT uw FROM UserWork uw WHERE uw.username = :username AND uw.status != 'DELETED' ORDER BY uw.createdAt DESC")
|
||||
@Query("SELECT uw FROM UserWork uw WHERE uw.username = :username AND uw.status NOT IN ('DELETED', 'FAILED') ORDER BY uw.createdAt DESC")
|
||||
Page<UserWork> findByUsernameOrderByCreatedAtDesc(@Param("username") String username, Pageable pageable);
|
||||
|
||||
/**
|
||||
@@ -41,10 +41,9 @@ public interface UserWorkRepository extends JpaRepository<UserWork, Long> {
|
||||
|
||||
/**
|
||||
* 根据用户名查找正在进行中和排队中的作品
|
||||
* 增加时间限制:只返回最近24小时内的任务,避免返回陈旧的僵尸任务
|
||||
*/
|
||||
@Query("SELECT uw FROM UserWork uw WHERE uw.username = :username AND (uw.status = 'PROCESSING' OR uw.status = 'PENDING') AND uw.createdAt > :afterTime ORDER BY uw.createdAt DESC")
|
||||
List<UserWork> findByUsernameAndProcessingOrPendingOrderByCreatedAtDesc(@Param("username") String username, @Param("afterTime") LocalDateTime afterTime);
|
||||
@Query("SELECT uw FROM UserWork uw WHERE uw.username = :username AND (uw.status = 'PROCESSING' OR uw.status = 'PENDING') ORDER BY uw.createdAt DESC")
|
||||
List<UserWork> findByUsernameAndProcessingOrPendingOrderByCreatedAtDesc(@Param("username") String username);
|
||||
|
||||
/**
|
||||
* 查找所有PROCESSING状态的作品(用于系统重启清理)
|
||||
@@ -97,6 +96,13 @@ public interface UserWorkRepository extends JpaRepository<UserWork, Long> {
|
||||
*/
|
||||
@Query("SELECT COUNT(uw) FROM UserWork uw WHERE uw.username = :username AND uw.status != 'DELETED'")
|
||||
long countByUsername(@Param("username") String username);
|
||||
|
||||
/**
|
||||
* 统计用户进行中的任务数量(所有类型:文生视频、图生视频、分镜视频等)
|
||||
* 包括 PROCESSING 和 PENDING 状态
|
||||
*/
|
||||
@Query("SELECT COUNT(uw) FROM UserWork uw WHERE uw.username = :username AND (uw.status = 'PROCESSING' OR uw.status = 'PENDING')")
|
||||
long countProcessingTasksByUsername(@Param("username") String username);
|
||||
|
||||
/**
|
||||
* 统计用户公开作品数量
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.example.demo.security;
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
/**
|
||||
* 明文密码编码器(仅用于测试,生产环境不要使用)
|
||||
* 注意:已移除 @Component 注解,避免与 BCryptPasswordEncoder 冲突
|
||||
*/
|
||||
public class PlainTextPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -133,7 +133,16 @@ public class AlipayService {
|
||||
// 设置业务参数
|
||||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
||||
model.setOutTradeNo(payment.getOrderId());
|
||||
model.setTotalAmount(payment.getAmount().toString());
|
||||
|
||||
// 如果是美元,按汇率转换为人民币(支付宝只支持CNY)
|
||||
java.math.BigDecimal amount = payment.getAmount();
|
||||
String currency = payment.getCurrency();
|
||||
if ("USD".equalsIgnoreCase(currency)) {
|
||||
java.math.BigDecimal exchangeRate = new java.math.BigDecimal("7.2"); // USD -> CNY 汇率
|
||||
amount = amount.multiply(exchangeRate).setScale(2, java.math.RoundingMode.HALF_UP);
|
||||
logger.info("货币转换: {} USD -> {} CNY (汇率: 7.2)", payment.getAmount(), amount);
|
||||
}
|
||||
model.setTotalAmount(amount.toString());
|
||||
model.setSubject(payment.getDescription() != null ? payment.getDescription() : "AIGC会员订阅");
|
||||
model.setBody(payment.getDescription() != null ? payment.getDescription() : "AIGC平台会员订阅服务");
|
||||
model.setTimeoutExpress("5m");
|
||||
@@ -201,6 +210,21 @@ public class AlipayService {
|
||||
} else {
|
||||
String subCode = (String) precreateResponse.get("sub_code");
|
||||
String subMsg = (String) precreateResponse.get("sub_msg");
|
||||
|
||||
// 如果交易已经成功,不需要再生成二维码
|
||||
if ("ACQ.TRADE_HAS_SUCCESS".equals(subCode)) {
|
||||
logger.info("交易已成功支付,订单号:{},无需再生成二维码", payment.getOrderId());
|
||||
// 更新支付状态为成功
|
||||
payment.setStatus(PaymentStatus.SUCCESS);
|
||||
payment.setPaidAt(LocalDateTime.now());
|
||||
paymentRepository.save(payment);
|
||||
// 返回已支付成功的信息
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("alreadyPaid", true);
|
||||
result.put("message", "该订单已支付成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new RuntimeException("二维码生成失败:" + msg + (subMsg != null ? " - " + subMsg : "") + " (code: " + code + (subCode != null ? ", sub_code: " + subCode : "") + ")");
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -11,12 +11,9 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -156,14 +153,12 @@ public class CosService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片字节数组到COS(存储在images目录下)
|
||||
* 上传图片字节数组到COS(存储在配置的目录下)
|
||||
*/
|
||||
private String uploadImageBytes(byte[] bytes, String filename, String contentType) {
|
||||
try {
|
||||
// 构建对象键(图片使用 images 目录)
|
||||
LocalDate now = LocalDate.now();
|
||||
String datePath = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
String key = "images/" + datePath + "/" + filename;
|
||||
// 添加前缀目录
|
||||
String key = buildObjectKey(filename);
|
||||
|
||||
// 设置元数据
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
@@ -282,13 +277,18 @@ public class CosService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成对象键(带日期目录结构)
|
||||
* 例如: videos/2025/01/14/uuid.mp4
|
||||
* 生成对象键(添加配置的前缀目录)
|
||||
*/
|
||||
private String buildObjectKey(String filename) {
|
||||
LocalDate now = LocalDate.now();
|
||||
String datePath = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
return "videos/" + datePath + "/" + filename;
|
||||
String prefix = cosConfig.getPrefix();
|
||||
if (prefix != null && !prefix.isEmpty()) {
|
||||
// 确保前缀以 / 结尾
|
||||
if (!prefix.endsWith("/")) {
|
||||
prefix = prefix + "/";
|
||||
}
|
||||
return prefix + filename;
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.Order;
|
||||
import com.example.demo.model.OrderStatus;
|
||||
import com.example.demo.model.Payment;
|
||||
import com.example.demo.model.PaymentStatus;
|
||||
import com.example.demo.repository.OrderRepository;
|
||||
import com.example.demo.repository.PaymentRepository;
|
||||
@@ -39,8 +38,8 @@ public class DashboardService {
|
||||
overview.put("totalUsers", totalUsers);
|
||||
|
||||
// 付费用户数(有成功支付记录的不同用户数)- 优化:直接在数据库层面统计
|
||||
long payingUsers = paymentRepository.countDistinctUsersByStatus(PaymentStatus.SUCCESS);
|
||||
overview.put("payingUsers", payingUsers);
|
||||
long paidUsers = paymentRepository.countDistinctUsersByStatus(PaymentStatus.SUCCESS);
|
||||
overview.put("paidUsers", paidUsers);
|
||||
|
||||
// 今日收入(今日成功支付的金额)- 优化:直接在数据库层面求和
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
@@ -48,16 +47,54 @@ public class DashboardService {
|
||||
|
||||
BigDecimal todayRevenue = paymentRepository.sumAmountByStatusAndPaidAtBetween(
|
||||
PaymentStatus.SUCCESS, todayStart, todayEnd);
|
||||
overview.put("todayRevenue", todayRevenue);
|
||||
overview.put("todayRevenue", todayRevenue != null ? todayRevenue : BigDecimal.ZERO);
|
||||
|
||||
// 转化率(付费用户数 / 总用户数)
|
||||
double conversionRate = totalUsers > 0 ? (double) payingUsers / totalUsers * 100 : 0;
|
||||
double conversionRate = totalUsers > 0 ? (double) paidUsers / totalUsers * 100 : 0;
|
||||
overview.put("conversionRate", Math.round(conversionRate * 100.0) / 100.0);
|
||||
|
||||
// 今日新增用户 - 优化:直接在数据库层面统计
|
||||
long todayNewUsers = userRepository.countByCreatedAtBetween(todayStart, todayEnd);
|
||||
overview.put("todayNewUsers", todayNewUsers);
|
||||
|
||||
// ===== 计算环比变化(当前值 vs 上月底值)=====
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
// 上月底时间点
|
||||
LocalDate lastMonthEnd = today.withDayOfMonth(1).minusDays(1);
|
||||
LocalDateTime lastMonthEndTime = lastMonthEnd.atTime(23, 59, 59);
|
||||
|
||||
// 用户总数环比:当前总用户数 vs 上月底总用户数
|
||||
long lastMonthTotalUsers = userRepository.countByCreatedAtBefore(lastMonthEndTime.plusSeconds(1));
|
||||
double totalUsersChange = lastMonthTotalUsers > 0 ?
|
||||
((double) totalUsers - lastMonthTotalUsers) / lastMonthTotalUsers * 100 :
|
||||
(totalUsers > 0 ? 100 : 0);
|
||||
overview.put("totalUsersChange", Math.round(totalUsersChange * 10.0) / 10.0);
|
||||
|
||||
// 付费用户环比:当前付费用户数 vs 上月底付费用户数
|
||||
long lastMonthPaidUsers = paymentRepository.countDistinctUsersByStatusAndPaidAtBefore(
|
||||
PaymentStatus.SUCCESS, lastMonthEndTime.plusSeconds(1));
|
||||
double paidUsersChange = lastMonthPaidUsers > 0 ?
|
||||
((double) paidUsers - lastMonthPaidUsers) / lastMonthPaidUsers * 100 :
|
||||
(paidUsers > 0 ? 100 : 0);
|
||||
overview.put("paidUsersChange", Math.round(paidUsersChange * 10.0) / 10.0);
|
||||
|
||||
// 昨日收入(用于对比今日收入)
|
||||
LocalDateTime yesterdayStart = today.minusDays(1).atStartOfDay();
|
||||
LocalDateTime yesterdayEnd = today.minusDays(1).atTime(23, 59, 59);
|
||||
BigDecimal yesterdayRevenue = paymentRepository.sumAmountByStatusAndPaidAtBetween(
|
||||
PaymentStatus.SUCCESS, yesterdayStart, yesterdayEnd);
|
||||
if (yesterdayRevenue == null) yesterdayRevenue = BigDecimal.ZERO;
|
||||
BigDecimal todayRevenueValue = todayRevenue != null ? todayRevenue : BigDecimal.ZERO;
|
||||
|
||||
// 收入变化率
|
||||
double todayRevenueChange = yesterdayRevenue.compareTo(BigDecimal.ZERO) > 0 ?
|
||||
todayRevenueValue.subtract(yesterdayRevenue)
|
||||
.divide(yesterdayRevenue, 4, java.math.RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100)).doubleValue() :
|
||||
(todayRevenueValue.compareTo(BigDecimal.ZERO) > 0 ? 100 : 0);
|
||||
overview.put("todayRevenueChange", Math.round(todayRevenueChange * 10.0) / 10.0);
|
||||
|
||||
return overview;
|
||||
}
|
||||
|
||||
|
||||
@@ -205,6 +205,151 @@ public class ImageGridService {
|
||||
return compressedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接分镜图:将用户上传的原图和AI生成的分镜图拼接在一起
|
||||
*
|
||||
* 规则:
|
||||
* - 如果原图是16:9(横向),创建9:16的画布,原图在左,生成图在右
|
||||
* - 如果原图是9:16(纵向),创建16:9的画布,原图在上,生成图在下
|
||||
*
|
||||
* @param originalImageUrl 用户上传的原图URL或Base64
|
||||
* @param generatedImageUrl AI生成的分镜图URL或Base64
|
||||
* @return 拼接后的图片Base64
|
||||
*/
|
||||
public String mergeStoryboardImages(String originalImageUrl, String generatedImageUrl) {
|
||||
if (originalImageUrl == null || originalImageUrl.isEmpty()) {
|
||||
throw new IllegalArgumentException("原图不能为空");
|
||||
}
|
||||
if (generatedImageUrl == null || generatedImageUrl.isEmpty()) {
|
||||
throw new IllegalArgumentException("生成图不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 加载原图和生成图
|
||||
BufferedImage originalImage = loadImageFromUrl(originalImageUrl);
|
||||
BufferedImage generatedImage = loadImageFromUrl(generatedImageUrl);
|
||||
|
||||
if (originalImage == null) {
|
||||
throw new RuntimeException("无法加载原图");
|
||||
}
|
||||
if (generatedImage == null) {
|
||||
throw new RuntimeException("无法加载生成图");
|
||||
}
|
||||
|
||||
int origWidth = originalImage.getWidth();
|
||||
int origHeight = originalImage.getHeight();
|
||||
double aspectRatio = (double) origWidth / origHeight;
|
||||
|
||||
logger.info("分镜图拼接: 原图尺寸={}x{}, 宽高比={:.2f}, 生成图尺寸={}x{}",
|
||||
origWidth, origHeight, aspectRatio,
|
||||
generatedImage.getWidth(), generatedImage.getHeight());
|
||||
|
||||
BufferedImage resultImage;
|
||||
Graphics2D g;
|
||||
|
||||
// 判断原图是横向(16:9)还是纵向(9:16)
|
||||
if (aspectRatio >= 1.0) {
|
||||
// 横向图片(16:9或更宽):创建9:16的画布,左右拼接
|
||||
// 画布宽度 = 原图宽度,画布高度 = 画布宽度 * 16 / 9
|
||||
int canvasWidth = origWidth;
|
||||
int canvasHeight = (int) (canvasWidth * 16.0 / 9.0);
|
||||
|
||||
// 每张图片占画布宽度的一半
|
||||
int halfWidth = canvasWidth / 2;
|
||||
int imageHeight = canvasHeight;
|
||||
|
||||
resultImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_RGB);
|
||||
g = resultImage.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
|
||||
// 白色背景
|
||||
g.setColor(java.awt.Color.WHITE);
|
||||
g.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// 左侧放原图(缩放并居中)
|
||||
drawImageCentered(g, originalImage, 0, 0, halfWidth, imageHeight);
|
||||
|
||||
// 右侧放生成图(缩放并居中)
|
||||
drawImageCentered(g, generatedImage, halfWidth, 0, halfWidth, imageHeight);
|
||||
|
||||
logger.info("横向拼接完成: 画布={}x{}, 每张图={}x{}", canvasWidth, canvasHeight, halfWidth, imageHeight);
|
||||
|
||||
} else {
|
||||
// 纵向图片(9:16或更窄):创建16:9的画布,上下拼接
|
||||
// 画布高度 = 原图高度,画布宽度 = 画布高度 * 16 / 9
|
||||
int canvasHeight = origHeight;
|
||||
int canvasWidth = (int) (canvasHeight * 16.0 / 9.0);
|
||||
|
||||
// 每张图片占画布高度的一半
|
||||
int halfHeight = canvasHeight / 2;
|
||||
int imageWidth = canvasWidth;
|
||||
|
||||
resultImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_RGB);
|
||||
g = resultImage.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
|
||||
// 白色背景
|
||||
g.setColor(java.awt.Color.WHITE);
|
||||
g.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// 上侧放原图(缩放并居中)
|
||||
drawImageCentered(g, originalImage, 0, 0, imageWidth, halfHeight);
|
||||
|
||||
// 下侧放生成图(缩放并居中)
|
||||
drawImageCentered(g, generatedImage, 0, halfHeight, imageWidth, halfHeight);
|
||||
|
||||
logger.info("纵向拼接完成: 画布={}x{}, 每张图={}x{}", canvasWidth, canvasHeight, imageWidth, halfHeight);
|
||||
}
|
||||
|
||||
g.dispose();
|
||||
|
||||
// 压缩并转换为Base64
|
||||
BufferedImage compressedImage = compressGridImage(resultImage, 2048);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
javax.imageio.ImageWriter writer = javax.imageio.ImageIO.getImageWritersByFormatName("jpg").next();
|
||||
javax.imageio.ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
if (param.canWriteCompressed()) {
|
||||
param.setCompressionMode(javax.imageio.ImageWriteParam.MODE_EXPLICIT);
|
||||
param.setCompressionQuality(0.85f);
|
||||
}
|
||||
javax.imageio.IIOImage iioImage = new javax.imageio.IIOImage(compressedImage, null, null);
|
||||
writer.setOutput(javax.imageio.ImageIO.createImageOutputStream(baos));
|
||||
writer.write(null, iioImage, param);
|
||||
writer.dispose();
|
||||
|
||||
byte[] imageBytes = baos.toByteArray();
|
||||
String base64 = Base64.getEncoder().encodeToString(imageBytes);
|
||||
|
||||
logger.info("分镜图拼接完成: 最终尺寸={}x{}, 大小={} KB",
|
||||
compressedImage.getWidth(), compressedImage.getHeight(), imageBytes.length / 1024);
|
||||
|
||||
return "data:image/jpeg;base64," + base64;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("拼接分镜图失败", e);
|
||||
throw new RuntimeException("分镜图拼接失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定区域内居中绘制图片(保持比例)
|
||||
*/
|
||||
private void drawImageCentered(Graphics2D g, BufferedImage img, int x, int y, int width, int height) {
|
||||
double scaleX = (double) width / img.getWidth();
|
||||
double scaleY = (double) height / img.getHeight();
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
|
||||
int scaledWidth = (int) (img.getWidth() * scale);
|
||||
int scaledHeight = (int) (img.getHeight() * scale);
|
||||
int imgX = x + (width - scaledWidth) / 2;
|
||||
int imgY = y + (height - scaledHeight) / 2;
|
||||
|
||||
g.drawImage(img, imgX, imgY, scaledWidth, scaledHeight, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL加载图片
|
||||
*/
|
||||
|
||||
@@ -58,7 +58,6 @@ public class ImageToVideoService {
|
||||
@Value("${app.video.output.path:/outputs}")
|
||||
private String outputPath;
|
||||
|
||||
|
||||
/**
|
||||
* 创建图生视频任务
|
||||
*/
|
||||
@@ -68,6 +67,9 @@ public class ImageToVideoService {
|
||||
String aspectRatio, int duration, boolean hdMode) {
|
||||
|
||||
try {
|
||||
// 检查用户所有类型任务的总数(统一检查)
|
||||
userWorkService.checkMaxConcurrentTasks(username);
|
||||
|
||||
// 生成任务ID
|
||||
String taskId = generateTaskId();
|
||||
|
||||
@@ -338,6 +340,16 @@ public class ImageToVideoService {
|
||||
task.updateStatus(ImageToVideoTask.TaskStatus.COMPLETED);
|
||||
task.updateProgress(100);
|
||||
taskRepository.save(task);
|
||||
|
||||
// 同步更新 UserWork 表的状态和结果URL
|
||||
try {
|
||||
userWorkService.updateWorkOnComplete(task.getTaskId(), resultUrl,
|
||||
com.example.demo.model.UserWork.WorkStatus.COMPLETED);
|
||||
logger.info("图生视频任务完成,UserWork已更新: {}", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.warn("更新UserWork状态失败: taskId={}, error={}", task.getTaskId(), e.getMessage());
|
||||
}
|
||||
|
||||
logger.info("图生视频任务完成: {}", task.getTaskId());
|
||||
return;
|
||||
} else if ("failed".equals(status) || "error".equals(status)) {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 在线统计服务
|
||||
* 通过IP地址统计当天访问的独立用户数
|
||||
*/
|
||||
@Service
|
||||
public class OnlineStatsService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OnlineStatsService.class);
|
||||
|
||||
// 存储当天访问过的IP地址(使用Set去重)
|
||||
private final Set<String> todayVisitors = ConcurrentHashMap.newKeySet();
|
||||
|
||||
// 记录当前日期,用于判断是否需要重置
|
||||
private volatile LocalDate currentDate = LocalDate.now();
|
||||
|
||||
// 系统启动时间
|
||||
private final LocalDateTime startTime = LocalDateTime.now();
|
||||
|
||||
/**
|
||||
* 记录访问IP
|
||||
* @param ip 访问者IP地址
|
||||
*/
|
||||
public void recordVisit(String ip) {
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否需要重置(新的一天)
|
||||
checkAndResetIfNewDay();
|
||||
|
||||
// 添加IP到今日访客列表
|
||||
todayVisitors.add(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当天在线人数(独立IP数)
|
||||
* @return 当天访问的独立IP数量
|
||||
*/
|
||||
public int getTodayVisitorCount() {
|
||||
checkAndResetIfNewDay();
|
||||
return todayVisitors.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是新的一天,如果是则重置统计
|
||||
*/
|
||||
private void checkAndResetIfNewDay() {
|
||||
LocalDate today = LocalDate.now();
|
||||
if (!today.equals(currentDate)) {
|
||||
synchronized (this) {
|
||||
if (!today.equals(currentDate)) {
|
||||
logger.info("新的一天开始,重置在线统计。昨日访客数: {}", todayVisitors.size());
|
||||
todayVisitors.clear();
|
||||
currentDate = today;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天凌晨0点重置统计
|
||||
*/
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void resetDailyStats() {
|
||||
logger.info("定时任务:重置每日在线统计。昨日访客数: {}", todayVisitors.size());
|
||||
todayVisitors.clear();
|
||||
currentDate = LocalDate.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统运行时间(格式化为 X小时X分)
|
||||
* @return 运行时间字符串
|
||||
*/
|
||||
public String getUptime() {
|
||||
Duration duration = Duration.between(startTime, LocalDateTime.now());
|
||||
long hours = duration.toHours();
|
||||
long minutes = duration.toMinutes() % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return hours + "小时" + minutes + "分";
|
||||
} else {
|
||||
return minutes + "分钟";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
* @return 包含统计数据的Map
|
||||
*/
|
||||
public Map<String, Object> getStats() {
|
||||
checkAndResetIfNewDay();
|
||||
Map<String, Object> stats = new ConcurrentHashMap<>();
|
||||
stats.put("todayVisitors", todayVisitors.size());
|
||||
stats.put("date", currentDate.toString());
|
||||
stats.put("uptime", getUptime());
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
@@ -394,18 +394,13 @@ public class OrderService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除订单(软删除,仅管理员可操作)
|
||||
* 删除订单(仅管理员可操作)
|
||||
*/
|
||||
public void deleteOrder(Long orderId) {
|
||||
try {
|
||||
Order order = orderRepository.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在:" + orderId));
|
||||
|
||||
// 只有已取消或已退款的订单才能删除
|
||||
if (order.getStatus() != OrderStatus.CANCELLED && order.getStatus() != OrderStatus.REFUNDED) {
|
||||
throw new RuntimeException("只有已取消或已退款的订单才能删除");
|
||||
}
|
||||
|
||||
orderRepository.delete(order);
|
||||
logger.info("订单删除成功,订单号:{}", order.getOrderNumber());
|
||||
|
||||
|
||||
@@ -13,518 +13,242 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
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.PaymentStatus;
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.model.*;
|
||||
import com.example.demo.repository.PaymentRepository;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class PaymentService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
|
||||
@Autowired private PaymentRepository paymentRepository;
|
||||
@Autowired private OrderService orderService;
|
||||
@Autowired private UserService userService;
|
||||
@Autowired private AlipayService alipayService;
|
||||
@Autowired(required = false) private PayPalService payPalService;
|
||||
|
||||
@Autowired
|
||||
private PaymentRepository paymentRepository;
|
||||
public Payment save(Payment payment) { return paymentRepository.save(payment); }
|
||||
@Transactional(readOnly = true) public Optional<Payment> findById(Long id) { return paymentRepository.findByIdWithUser(id); }
|
||||
@Transactional(readOnly = true) public Optional<Payment> findByOrderId(String orderId) { return paymentRepository.findByOrderId(orderId); }
|
||||
@Transactional(readOnly = true) public Optional<Payment> findByExternalTransactionId(String id) { return paymentRepository.findByExternalTransactionId(id); }
|
||||
@Transactional(readOnly = true) public List<Payment> findByUserId(Long userId) { return paymentRepository.findByUserIdOrderByCreatedAtDesc(userId); }
|
||||
@Transactional(readOnly = true) public List<Payment> findAll() { return paymentRepository.findAll(); }
|
||||
@Transactional(readOnly = true) public List<Payment> findByStatus(PaymentStatus status) { return paymentRepository.findByStatus(status); }
|
||||
@Transactional(readOnly = true) public long countByUserId(Long userId) { return paymentRepository.countByUserId(userId); }
|
||||
@Transactional(readOnly = true) public long countByStatus(PaymentStatus status) { return paymentRepository.countByStatus(status); }
|
||||
@Transactional(readOnly = true) public long countByUserIdAndStatus(Long userId, PaymentStatus status) { return paymentRepository.countByUserIdAndStatus(userId, status); }
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private AlipayService alipayService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private PayPalService payPalService;
|
||||
|
||||
/**
|
||||
* 保存支付记录
|
||||
*/
|
||||
public Payment save(Payment payment) {
|
||||
try {
|
||||
Payment savedPayment = paymentRepository.save(payment);
|
||||
logger.info("支付记录保存成功,支付ID:{}", savedPayment.getId());
|
||||
return savedPayment;
|
||||
} catch (Exception e) {
|
||||
logger.error("保存支付记录失败:", e);
|
||||
throw new RuntimeException("保存支付记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找支付记录(包含User信息,避免LazyInitializationException)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Payment> findById(Long id) {
|
||||
return paymentRepository.findByIdWithUser(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据订单ID查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Payment> findByOrderId(String orderId) {
|
||||
return paymentRepository.findByOrderId(orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据外部交易ID查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Payment> findByExternalTransactionId(String externalTransactionId) {
|
||||
return paymentRepository.findByExternalTransactionId(externalTransactionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findByUserId(Long userId) {
|
||||
return paymentRepository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找所有支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findAll() {
|
||||
return paymentRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findByStatus(PaymentStatus status) {
|
||||
return paymentRepository.findByStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付状态
|
||||
*/
|
||||
public Payment updatePaymentStatus(Long paymentId, PaymentStatus newStatus) {
|
||||
try {
|
||||
Payment payment = paymentRepository.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
PaymentStatus oldStatus = payment.getStatus();
|
||||
payment.setStatus(newStatus);
|
||||
|
||||
if (newStatus == PaymentStatus.SUCCESS) {
|
||||
payment.setPaidAt(LocalDateTime.now());
|
||||
|
||||
// 更新关联订单状态
|
||||
if (payment.getOrder() != null) {
|
||||
orderService.confirmPayment(payment.getOrder().getId(), payment.getExternalTransactionId());
|
||||
}
|
||||
}
|
||||
|
||||
Payment updatedPayment = paymentRepository.save(payment);
|
||||
logger.info("支付状态更新成功,支付ID:{},状态:{} -> {}",
|
||||
paymentId, oldStatus, newStatus);
|
||||
|
||||
return updatedPayment;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("更新支付状态失败:", e);
|
||||
throw new RuntimeException("更新支付状态失败:" + e.getMessage());
|
||||
}
|
||||
Payment payment = paymentRepository.findById(paymentId).orElseThrow(() -> new RuntimeException("Not found"));
|
||||
payment.setStatus(newStatus);
|
||||
if (newStatus == PaymentStatus.SUCCESS) payment.setPaidAt(LocalDateTime.now());
|
||||
return paymentRepository.save(payment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认支付成功
|
||||
*/
|
||||
public Payment confirmPaymentSuccess(Long paymentId, String externalTransactionId) {
|
||||
try {
|
||||
Payment payment = paymentRepository.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
payment.setStatus(PaymentStatus.SUCCESS);
|
||||
payment.setPaidAt(LocalDateTime.now());
|
||||
payment.setExternalTransactionId(externalTransactionId);
|
||||
|
||||
Payment confirmedPayment = paymentRepository.save(payment);
|
||||
|
||||
// 更新关联订单状态
|
||||
if (payment.getOrder() != null) {
|
||||
orderService.confirmPayment(payment.getOrder().getId(), externalTransactionId);
|
||||
} else {
|
||||
// 如果没有关联订单,自动创建一个订单
|
||||
createOrderFromPayment(confirmedPayment);
|
||||
}
|
||||
|
||||
// 根据支付金额增加积分
|
||||
addPointsForPayment(confirmedPayment);
|
||||
|
||||
logger.info("支付确认成功,支付ID:{},外部交易ID:{}", paymentId, externalTransactionId);
|
||||
return confirmedPayment;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("确认支付成功失败:", e);
|
||||
throw new RuntimeException("确认支付成功失败:" + e.getMessage());
|
||||
Payment payment = paymentRepository.findById(paymentId).orElseThrow(() -> new RuntimeException("Not found"));
|
||||
|
||||
// 检查是否已经处理过(防止重复增加积分和重复创建订单)
|
||||
if (payment.getStatus() == PaymentStatus.SUCCESS) {
|
||||
logger.info("支付记录已经是成功状态,跳过重复处理: paymentId={}", paymentId);
|
||||
return payment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认支付失败
|
||||
*/
|
||||
public Payment confirmPaymentFailure(Long paymentId, String failureReason) {
|
||||
|
||||
payment.setStatus(PaymentStatus.SUCCESS);
|
||||
payment.setPaidAt(LocalDateTime.now());
|
||||
payment.setExternalTransactionId(externalTransactionId);
|
||||
Payment savedPayment = paymentRepository.save(payment);
|
||||
|
||||
// 支付成功后创建订单
|
||||
try {
|
||||
Payment payment = paymentRepository.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
payment.setStatus(PaymentStatus.FAILED);
|
||||
if (failureReason != null && !failureReason.isEmpty()) {
|
||||
payment.setDescription((payment.getDescription() != null ? payment.getDescription() + "\n" : "") +
|
||||
"失败原因:" + failureReason);
|
||||
}
|
||||
|
||||
Payment failedPayment = paymentRepository.save(payment);
|
||||
logger.info("支付确认失败,支付ID:{},失败原因:{}", paymentId, failureReason);
|
||||
|
||||
return failedPayment;
|
||||
|
||||
createOrderForPayment(savedPayment);
|
||||
} catch (Exception e) {
|
||||
logger.error("确认支付失败失败:", e);
|
||||
throw new RuntimeException("确认支付失败失败:" + e.getMessage());
|
||||
logger.error("支付成功但创建订单失败: paymentId={}, error={}", paymentId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单支付
|
||||
*/
|
||||
public Payment createOrderPayment(Order order, PaymentMethod paymentMethod) {
|
||||
|
||||
// 支付成功后增加用户积分
|
||||
try {
|
||||
Payment payment = new Payment();
|
||||
payment.setOrderId(order.getOrderNumber());
|
||||
payment.setAmount(order.getTotalAmount());
|
||||
payment.setCurrency(order.getCurrency());
|
||||
payment.setPaymentMethod(paymentMethod);
|
||||
payment.setDescription("订单支付 - " + order.getOrderNumber());
|
||||
payment.setUser(order.getUser());
|
||||
payment.setOrder(order);
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
|
||||
Payment savedPayment = paymentRepository.save(payment);
|
||||
logger.info("订单支付创建成功,订单号:{},支付ID:{}", order.getOrderNumber(), savedPayment.getId());
|
||||
|
||||
return savedPayment;
|
||||
|
||||
addPointsForPayment(savedPayment);
|
||||
} catch (Exception e) {
|
||||
logger.error("创建订单支付失败:", e);
|
||||
throw new RuntimeException("创建订单支付失败:" + e.getMessage());
|
||||
logger.error("支付成功但增加积分失败: paymentId={}, error={}", paymentId, e.getMessage(), e);
|
||||
}
|
||||
|
||||
return savedPayment;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计支付记录数量
|
||||
* 为支付成功的记录创建订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByUserId(Long userId) {
|
||||
return paymentRepository.countByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定状态的支付记录数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByStatus(PaymentStatus status) {
|
||||
return paymentRepository.countByStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户指定状态的支付记录数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByUserIdAndStatus(Long userId, PaymentStatus status) {
|
||||
return paymentRepository.countByUserIdAndStatus(userId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支付记录
|
||||
*/
|
||||
public void deletePayment(Long paymentId) {
|
||||
try {
|
||||
Payment payment = paymentRepository.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
// 只有失败的支付记录才能删除
|
||||
if (payment.getStatus() != PaymentStatus.FAILED) {
|
||||
throw new RuntimeException("只有失败的支付记录才能删除");
|
||||
}
|
||||
|
||||
paymentRepository.delete(payment);
|
||||
logger.info("支付记录删除成功,支付ID:{}", paymentId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("删除支付记录失败:", e);
|
||||
throw new RuntimeException("删除支付记录失败:" + e.getMessage());
|
||||
private void createOrderForPayment(Payment payment) {
|
||||
if (payment == null || payment.getUser() == null) {
|
||||
logger.warn("无法创建订单: payment或user为空");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findByUsername(String username) {
|
||||
try {
|
||||
logger.info("PaymentService: 开始查找用户 {} 的支付记录", username);
|
||||
|
||||
// 先查找用户
|
||||
User user = userService.findByUsername(username);
|
||||
if (user == null) {
|
||||
logger.error("PaymentService: 用户 {} 不存在", username);
|
||||
throw new RuntimeException("用户不存在: " + username);
|
||||
}
|
||||
|
||||
logger.info("PaymentService: 找到用户 {}, ID: {}", username, user.getId());
|
||||
|
||||
// 查找支付记录
|
||||
List<Payment> payments = paymentRepository.findByUserIdOrderByCreatedAtDesc(user.getId());
|
||||
logger.info("PaymentService: 用户 {} 的支付记录数量: {}", username, payments.size());
|
||||
|
||||
return payments;
|
||||
} catch (Exception e) {
|
||||
logger.error("PaymentService: 根据用户名查找支付记录失败,用户名: {}, 错误: {}", username, e.getMessage(), e);
|
||||
throw new RuntimeException("查找支付记录失败:" + e.getMessage());
|
||||
|
||||
// 检查是否已经关联了订单
|
||||
if (payment.getOrder() != null) {
|
||||
logger.info("支付记录已关联订单,跳过创建: paymentId={}, orderId={}", payment.getId(), payment.getOrder().getId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付
|
||||
*/
|
||||
public Payment createPayment(String username, String orderId, String amountStr, String method) {
|
||||
|
||||
try {
|
||||
logger.info("创建支付 - 用户名: '{}', 订单ID: {}, 金额: {}, 支付方式: {}", username, orderId, amountStr, method);
|
||||
|
||||
User user;
|
||||
try {
|
||||
logger.info("PaymentService - 尝试查找用户: '{}'", username);
|
||||
user = userService.findByUsername(username);
|
||||
logger.info("PaymentService - 用户查找结果: {}", user != null ? "找到用户 '" + user.getUsername() + "'" : "未找到用户");
|
||||
} catch (Exception e) {
|
||||
logger.error("PaymentService - 用户查找异常: {}", username, e);
|
||||
// 如果是匿名用户,创建一个临时用户记录
|
||||
if (username.startsWith("anonymous_")) {
|
||||
user = createAnonymousUser(username);
|
||||
} else {
|
||||
throw new RuntimeException("用户不存在: " + username);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("找到用户: {}", user.getUsername());
|
||||
|
||||
BigDecimal amount = new BigDecimal(amountStr);
|
||||
PaymentMethod paymentMethod = PaymentMethod.valueOf(method);
|
||||
|
||||
logger.info("金额: {}, 支付方式: {}", amount, paymentMethod);
|
||||
|
||||
Payment payment = new Payment();
|
||||
payment.setUser(user);
|
||||
payment.setOrderId(orderId);
|
||||
payment.setAmount(amount);
|
||||
payment.setCurrency("CNY"); // 设置默认货币为人民币
|
||||
payment.setPaymentMethod(paymentMethod);
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
payment.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
Payment savedPayment = save(payment);
|
||||
logger.info("支付记录创建成功: {}", savedPayment.getId());
|
||||
|
||||
// 根据支付方式调用相应的支付服务
|
||||
if (paymentMethod == PaymentMethod.ALIPAY) {
|
||||
try {
|
||||
Map<String, Object> paymentResult = alipayService.createPayment(savedPayment);
|
||||
if (paymentResult.containsKey("qrCode")) {
|
||||
savedPayment.setPaymentUrl(paymentResult.get("qrCode").toString());
|
||||
}
|
||||
save(savedPayment);
|
||||
logger.info("支付宝二维码生成成功: {}", paymentResult.get("qrCode"));
|
||||
} catch (Exception e) {
|
||||
logger.error("调用支付宝支付接口失败:", e);
|
||||
// 不抛出异常,让前端处理
|
||||
}
|
||||
} else if (paymentMethod == PaymentMethod.PAYPAL) {
|
||||
try {
|
||||
if (payPalService == null) {
|
||||
throw new RuntimeException("PayPal服务未配置");
|
||||
}
|
||||
Map<String, Object> paymentResult = payPalService.createPayment(savedPayment);
|
||||
if (paymentResult.containsKey("paymentUrl")) {
|
||||
savedPayment.setPaymentUrl(paymentResult.get("paymentUrl").toString());
|
||||
}
|
||||
if (paymentResult.containsKey("paypalPaymentId")) {
|
||||
savedPayment.setExternalTransactionId(paymentResult.get("paypalPaymentId").toString());
|
||||
}
|
||||
save(savedPayment);
|
||||
logger.info("PayPal支付链接生成成功: {}", paymentResult.get("paymentUrl"));
|
||||
} catch (Exception e) {
|
||||
logger.error("调用PayPal支付接口失败:", e);
|
||||
throw new RuntimeException("创建PayPal支付失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return savedPayment;
|
||||
} catch (Exception e) {
|
||||
logger.error("创建支付失败:", e);
|
||||
throw new RuntimeException("创建支付失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建匿名用户
|
||||
*/
|
||||
private User createAnonymousUser(String username) {
|
||||
try {
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setEmail(username + "@anonymous.com");
|
||||
user.setPasswordHash("anonymous");
|
||||
user.setRole("ROLE_USER");
|
||||
|
||||
return userService.save(user);
|
||||
} catch (Exception e) {
|
||||
logger.error("创建匿名用户失败:", e);
|
||||
throw new RuntimeException("创建匿名用户失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户支付统计
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getUserPaymentStats(String username) {
|
||||
try {
|
||||
User user = userService.findByUsername(username);
|
||||
if (user == null) {
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
Long userId = user.getId();
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("totalPayments", paymentRepository.countByUserId(userId));
|
||||
stats.put("successfulPayments", paymentRepository.countByUserIdAndStatus(userId, PaymentStatus.SUCCESS));
|
||||
stats.put("pendingPayments", paymentRepository.countByUserIdAndStatus(userId, PaymentStatus.PENDING));
|
||||
stats.put("failedPayments", paymentRepository.countByUserIdAndStatus(userId, PaymentStatus.FAILED));
|
||||
stats.put("cancelledPayments", paymentRepository.countByUserIdAndStatus(userId, PaymentStatus.CANCELLED));
|
||||
|
||||
return stats;
|
||||
} catch (Exception e) {
|
||||
logger.error("获取用户支付统计失败:", e);
|
||||
throw new RuntimeException("获取支付统计失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据支付信息增加积分
|
||||
*/
|
||||
private void addPointsForPayment(Payment payment) {
|
||||
try {
|
||||
BigDecimal amount = payment.getAmount();
|
||||
String description = payment.getDescription() != null ? payment.getDescription() : "";
|
||||
Integer pointsToAdd = 0;
|
||||
|
||||
// 优先从描述中识别套餐类型
|
||||
if (description.contains("标准版") || description.contains("standard")) {
|
||||
// 标准版订阅 - 200积分
|
||||
pointsToAdd = 200;
|
||||
logger.info("识别到标准版订阅,奖励 200 积分");
|
||||
} else if (description.contains("专业版") || description.contains("premium")) {
|
||||
// 专业版订阅 - 1000积分
|
||||
pointsToAdd = 1000;
|
||||
logger.info("识别到专业版订阅,奖励 1000 积分");
|
||||
} else {
|
||||
// 如果描述中没有套餐信息,根据金额判断
|
||||
// 标准版订阅 (59-258元) - 200积分
|
||||
if (amount.compareTo(new BigDecimal("59.00")) >= 0 && amount.compareTo(new BigDecimal("259.00")) < 0) {
|
||||
pointsToAdd = 200;
|
||||
logger.info("根据金额 {} 判断为标准版订阅,奖励 200 积分", amount);
|
||||
}
|
||||
// 专业版订阅 (259元以上) - 1000积分
|
||||
else if (amount.compareTo(new BigDecimal("259.00")) >= 0) {
|
||||
pointsToAdd = 1000;
|
||||
logger.info("根据金额 {} 判断为专业版订阅,奖励 1000 积分", amount);
|
||||
} else {
|
||||
logger.warn("支付金额 {} 不在已知套餐范围内,不增加积分", amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (pointsToAdd > 0) {
|
||||
userService.addPoints(payment.getUser().getId(), pointsToAdd);
|
||||
logger.info("✅ 用户 {} 支付 {} 元,成功获得 {} 积分",
|
||||
payment.getUser().getUsername(), amount, pointsToAdd);
|
||||
} else {
|
||||
// 如果金额不在套餐范围内,给予基础积分(1元=1积分)
|
||||
// 这样可以避免用户支付后没有任何积分的情况
|
||||
int basePoints = amount.intValue(); // 1元=1积分
|
||||
if (basePoints > 0) {
|
||||
userService.addPoints(payment.getUser().getId(), basePoints);
|
||||
logger.info("✅ 用户 {} 支付 {} 元(非套餐金额),获得基础积分 {} 积分(按1元=1积分计算)",
|
||||
payment.getUser().getUsername(), amount, basePoints);
|
||||
} else {
|
||||
logger.warn("⚠️ 用户 {} 支付 {} 元,金额过小未获得积分(描述: {})",
|
||||
payment.getUser().getUsername(), amount, description);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ 增加积分失败:", e);
|
||||
// 不抛出异常,避免影响支付流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从支付记录自动创建订单
|
||||
*/
|
||||
private void createOrderFromPayment(Payment payment) {
|
||||
try {
|
||||
// 生成订单号
|
||||
String orderNumber = "ORD" + System.currentTimeMillis();
|
||||
|
||||
// 创建订单
|
||||
Order order = new Order();
|
||||
order.setUser(payment.getUser());
|
||||
order.setOrderNumber(orderNumber);
|
||||
order.setOrderNumber("ORD" + System.currentTimeMillis() + payment.getId());
|
||||
order.setTotalAmount(payment.getAmount());
|
||||
order.setCurrency("CNY");
|
||||
order.setStatus(OrderStatus.PAID); // 支付成功,订单状态为已支付
|
||||
order.setOrderType(OrderType.PAYMENT); // 订单类型为支付订单
|
||||
order.setCreatedAt(LocalDateTime.now());
|
||||
order.setUpdatedAt(LocalDateTime.now());
|
||||
order.setCurrency(payment.getCurrency() != null ? payment.getCurrency() : "CNY");
|
||||
order.setStatus(OrderStatus.PAID);
|
||||
order.setPaidAt(LocalDateTime.now());
|
||||
order.setOrderType(OrderType.SUBSCRIPTION);
|
||||
order.setNotes(payment.getDescription() != null ? payment.getDescription() : "会员订阅");
|
||||
|
||||
// 保存订单
|
||||
Order savedOrder = orderService.save(order);
|
||||
// 根据金额设置订单描述
|
||||
BigDecimal amount = payment.getAmount();
|
||||
if (amount != null) {
|
||||
if (amount.compareTo(new BigDecimal("259.00")) >= 0) {
|
||||
order.setNotes("专业版会员订阅 - " + amount + "元");
|
||||
} else if (amount.compareTo(new BigDecimal("59.00")) >= 0) {
|
||||
order.setNotes("标准版会员订阅 - " + amount + "元");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建订单项
|
||||
OrderItem orderItem = new OrderItem();
|
||||
orderItem.setOrder(savedOrder);
|
||||
orderItem.setProductName("支付服务 - " + payment.getPaymentMethod().name());
|
||||
orderItem.setProductDescription("通过" + payment.getPaymentMethod().name() + "完成的支付服务");
|
||||
orderItem.setQuantity(1);
|
||||
orderItem.setUnitPrice(payment.getAmount());
|
||||
orderItem.setSubtotal(payment.getAmount());
|
||||
Order savedOrder = orderService.createOrder(order);
|
||||
|
||||
// 保存订单项
|
||||
orderService.saveOrderItem(orderItem);
|
||||
|
||||
// 更新支付记录,关联到新创建的订单
|
||||
// 关联支付记录和订单
|
||||
payment.setOrder(savedOrder);
|
||||
paymentRepository.save(payment);
|
||||
|
||||
logger.info("从支付记录自动创建订单成功,支付ID:{},订单ID:{},订单号:{}",
|
||||
payment.getId(), savedOrder.getId(), orderNumber);
|
||||
|
||||
logger.info("✅ 订单创建成功: orderId={}, orderNumber={}, paymentId={}",
|
||||
savedOrder.getId(), savedOrder.getOrderNumber(), payment.getId());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("从支付记录创建订单失败:", e);
|
||||
throw new RuntimeException("创建订单失败:" + e.getMessage());
|
||||
logger.error("创建订单失败: paymentId={}, error={}", payment.getId(), e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据支付金额增加用户积分
|
||||
* 标准版(59元) -> 200积分
|
||||
* 专业版(259元) -> 1000积分
|
||||
*/
|
||||
private void addPointsForPayment(Payment payment) {
|
||||
if (payment == null || payment.getUser() == null) {
|
||||
logger.warn("无法增加积分: payment或user为空");
|
||||
return;
|
||||
}
|
||||
|
||||
java.math.BigDecimal amount = payment.getAmount();
|
||||
if (amount == null) {
|
||||
logger.warn("无法增加积分: 支付金额为空, paymentId={}", payment.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据金额计算积分
|
||||
int points = 0;
|
||||
String planName = "";
|
||||
|
||||
// 专业版订阅 (259元以上) -> 1000积分
|
||||
if (amount.compareTo(new java.math.BigDecimal("259.00")) >= 0) {
|
||||
points = 1000;
|
||||
planName = "专业版";
|
||||
}
|
||||
// 标准版订阅 (59-258元) -> 200积分
|
||||
else if (amount.compareTo(new java.math.BigDecimal("59.00")) >= 0) {
|
||||
points = 200;
|
||||
planName = "标准版";
|
||||
}
|
||||
// 其他金额不增加积分
|
||||
else {
|
||||
logger.info("支付金额不在套餐范围内,不增加积分: amount={}", amount);
|
||||
return;
|
||||
}
|
||||
|
||||
// 增加积分
|
||||
Long userId = payment.getUser().getId();
|
||||
logger.info("开始为用户增加积分: userId={}, points={}, plan={}, paymentId={}", userId, points, planName, payment.getId());
|
||||
|
||||
userService.addPoints(userId, points);
|
||||
|
||||
logger.info("✅ 积分增加成功: userId={}, addedPoints={}, plan={}", userId, points, planName);
|
||||
}
|
||||
|
||||
public Payment confirmPaymentFailure(Long paymentId, String failureReason) {
|
||||
Payment payment = paymentRepository.findById(paymentId).orElseThrow(() -> new RuntimeException("Not found"));
|
||||
payment.setStatus(PaymentStatus.FAILED);
|
||||
return paymentRepository.save(payment);
|
||||
}
|
||||
|
||||
public Payment createOrderPayment(Order order, PaymentMethod paymentMethod) {
|
||||
Payment payment = new Payment();
|
||||
payment.setOrderId(order.getOrderNumber());
|
||||
payment.setAmount(order.getTotalAmount());
|
||||
payment.setCurrency(order.getCurrency());
|
||||
payment.setPaymentMethod(paymentMethod);
|
||||
payment.setUser(order.getUser());
|
||||
payment.setOrder(order);
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
return paymentRepository.save(payment);
|
||||
}
|
||||
|
||||
public void deletePayment(Long paymentId) {
|
||||
Payment payment = paymentRepository.findById(paymentId).orElseThrow(() -> new RuntimeException("Not found"));
|
||||
paymentRepository.delete(payment);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findByUsername(String username) {
|
||||
User user = userService.findByUsername(username);
|
||||
if (user == null) throw new RuntimeException("User not found");
|
||||
return paymentRepository.findByUserIdOrderByCreatedAtDesc(user.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Payment createPayment(String username, String orderId, String amountStr, String method) {
|
||||
// 检查是否已存在相同 orderId 的支付记录
|
||||
Optional<Payment> existing = paymentRepository.findByOrderId(orderId);
|
||||
if (existing.isPresent()) {
|
||||
Payment existingPayment = existing.get();
|
||||
// 如果已存在且状态是 PENDING,直接返回
|
||||
if (existingPayment.getStatus() == PaymentStatus.PENDING) {
|
||||
logger.info("复用已存在的PENDING支付记录: orderId={}, paymentId={}", orderId, existingPayment.getId());
|
||||
return existingPayment;
|
||||
}
|
||||
// 如果是其他状态,生成新的 orderId
|
||||
orderId = orderId + "_" + System.currentTimeMillis();
|
||||
logger.info("已存在相同orderId但状态为{},生成新orderId: {}", existingPayment.getStatus(), orderId);
|
||||
}
|
||||
|
||||
User user = null;
|
||||
if (username != null) { try { user = userService.findByUsername(username); } catch (Exception e) {} }
|
||||
if (user == null) { user = userService.findByUsernameOrNull(username != null ? username : "anon"); if (user == null) user = createAnonymousUser(username != null ? username : "anon"); }
|
||||
Payment payment = new Payment();
|
||||
payment.setUser(user);
|
||||
payment.setOrderId(orderId);
|
||||
payment.setAmount(new BigDecimal(amountStr));
|
||||
payment.setCurrency("CNY");
|
||||
payment.setPaymentMethod(PaymentMethod.valueOf(method));
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
payment.setCreatedAt(LocalDateTime.now());
|
||||
return paymentRepository.save(payment);
|
||||
}
|
||||
|
||||
private User createAnonymousUser(String username) {
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setEmail(username + "@anon.com");
|
||||
user.setPasswordHash("anon");
|
||||
user.setRole("ROLE_USER");
|
||||
return userService.save(user);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getUserPaymentStats(String username) {
|
||||
User user = userService.findByUsername(username);
|
||||
if (user == null) throw new RuntimeException("User not found");
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("totalPayments", paymentRepository.countByUserId(user.getId()));
|
||||
stats.put("successfulPayments", paymentRepository.countByUserIdAndStatus(user.getId(), PaymentStatus.SUCCESS));
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,11 @@ public class StoryboardVideoService {
|
||||
@Autowired
|
||||
private CosService cosService;
|
||||
|
||||
// 默认生成6张分镜图
|
||||
private static final int DEFAULT_STORYBOARD_IMAGES = 6;
|
||||
@Autowired
|
||||
private SystemSettingsService systemSettingsService;
|
||||
|
||||
// 默认生成1张分镜图
|
||||
private static final int DEFAULT_STORYBOARD_IMAGES = 1;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@@ -99,6 +102,9 @@ public class StoryboardVideoService {
|
||||
throw new IllegalArgumentException("文本描述不能为空");
|
||||
}
|
||||
|
||||
// 检查用户所有类型任务的总数(统一检查)
|
||||
userWorkService.checkMaxConcurrentTasks(username);
|
||||
|
||||
// 生成任务ID
|
||||
String taskId = generateTaskId();
|
||||
|
||||
@@ -113,8 +119,25 @@ public class StoryboardVideoService {
|
||||
task.setImageModel(imageModel);
|
||||
}
|
||||
|
||||
// 上传用户参考图片到COS
|
||||
if (imageUrl != null && !imageUrl.isEmpty()) {
|
||||
task.setImageUrl(imageUrl);
|
||||
String finalImageUrl = imageUrl;
|
||||
// 如果是Base64图片且COS启用,上传到COS
|
||||
if (imageUrl.startsWith("data:image") && cosService.isEnabled()) {
|
||||
try {
|
||||
logger.info("开始上传用户参考图片到COS: taskId={}", taskId);
|
||||
String cosUrl = cosService.uploadBase64Image(imageUrl, "ref_" + taskId + ".png");
|
||||
if (cosUrl != null && !cosUrl.isEmpty()) {
|
||||
finalImageUrl = cosUrl;
|
||||
logger.info("用户参考图片上传COS成功: taskId={}, url={}", taskId, cosUrl);
|
||||
} else {
|
||||
logger.warn("用户参考图片上传COS失败,使用原始URL: taskId={}", taskId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("用户参考图片上传COS异常: taskId={}", taskId, e);
|
||||
}
|
||||
}
|
||||
task.setImageUrl(finalImageUrl);
|
||||
}
|
||||
|
||||
// 保存任务(快速完成,事务立即提交)
|
||||
@@ -162,14 +185,27 @@ public class StoryboardVideoService {
|
||||
|
||||
// 判断是否有参考图片
|
||||
boolean hasReferenceImage = imageUrl != null && !imageUrl.isEmpty();
|
||||
logger.info("任务参数 - prompt: {}, aspectRatio: {}, hdMode: {}, 有参考图片: {}, imageModel: {}",
|
||||
prompt, aspectRatio, hdMode, hasReferenceImage, imageModel);
|
||||
|
||||
// 从系统设置读取分镜图系统引导词,并拼接到用户提示词前面
|
||||
String finalPrompt = prompt;
|
||||
try {
|
||||
com.example.demo.model.SystemSettings settings = systemSettingsService.getOrCreate();
|
||||
String storyboardSystemPrompt = settings.getStoryboardSystemPrompt();
|
||||
if (storyboardSystemPrompt != null && !storyboardSystemPrompt.trim().isEmpty()) {
|
||||
finalPrompt = storyboardSystemPrompt.trim() + ", " + prompt;
|
||||
logger.info("已添加系统引导词,最终提示词长度: {}", finalPrompt.length());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("获取系统引导词失败,使用原始提示词: {}", e.getMessage());
|
||||
}
|
||||
logger.info("任务参数 - 原始提示词: {}, 最终提示词长度: {}, aspectRatio: {}, hdMode: {}, 有参考图片: {}, imageModel: {}",
|
||||
prompt, finalPrompt.length(), aspectRatio, hdMode, hasReferenceImage, imageModel);
|
||||
|
||||
// 更新任务状态为处理中(使用 TransactionTemplate 确保事务正确关闭)
|
||||
updateTaskStatusWithTransactionTemplate(taskId);
|
||||
|
||||
// 调用AI生图API,生成多张分镜图
|
||||
logger.info("开始生成{}张分镜图({}模式)...", DEFAULT_STORYBOARD_IMAGES, hasReferenceImage ? "图生图" : "文生图");
|
||||
// 调用AI生图API,生成分镜图
|
||||
logger.info("开始生成分镜图({}模式)...", hasReferenceImage ? "图生图" : "文生图");
|
||||
|
||||
// 收集所有图片URL
|
||||
List<String> imageUrls = new ArrayList<>();
|
||||
@@ -192,11 +228,12 @@ public class StoryboardVideoService {
|
||||
}
|
||||
|
||||
// 根据是否有参考图片,调用不同的API
|
||||
// 使用拼接后的提示词 finalPrompt
|
||||
Map<String, Object> apiResponse;
|
||||
if (hasReferenceImage) {
|
||||
// 有参考图片,使用图生图API
|
||||
// 有参考图片,使用图生图API(bananaAPI)
|
||||
apiResponse = realAIService.submitImageToImageTask(
|
||||
prompt,
|
||||
finalPrompt,
|
||||
imageUrl,
|
||||
aspectRatio,
|
||||
hdMode
|
||||
@@ -204,7 +241,7 @@ public class StoryboardVideoService {
|
||||
} else {
|
||||
// 无参考图片,使用文生图API
|
||||
apiResponse = realAIService.submitTextToImageTask(
|
||||
prompt,
|
||||
finalPrompt,
|
||||
aspectRatio,
|
||||
1, // 每次生成1张图片
|
||||
hdMode,
|
||||
@@ -284,19 +321,19 @@ public class StoryboardVideoService {
|
||||
}
|
||||
}
|
||||
|
||||
// 严格检查:必须生成6张图片才能拼接
|
||||
// 严格检查:必须生成指定数量的图片
|
||||
if (imageUrls.size() < DEFAULT_STORYBOARD_IMAGES) {
|
||||
String errorMsg = String.format("只生成了%d张图片,需要%d张才能拼接分镜图",
|
||||
String errorMsg = String.format("只生成了%d张图片,需要%d张",
|
||||
imageUrls.size(), DEFAULT_STORYBOARD_IMAGES);
|
||||
logger.error(errorMsg);
|
||||
throw new RuntimeException(errorMsg);
|
||||
}
|
||||
|
||||
logger.info("成功生成{}张分镜图,开始拼接...", imageUrls.size());
|
||||
logger.info("成功生成{}张分镜图", imageUrls.size());
|
||||
|
||||
// 确保不超过6张图片(如果多于6张,只取前6张)
|
||||
// 确保不超过指定数量
|
||||
if (imageUrls.size() > DEFAULT_STORYBOARD_IMAGES) {
|
||||
logger.warn("生成了{}张图片,多于预期的{}张,只取前{}张进行拼接",
|
||||
logger.warn("生成了{}张图片,多于预期的{}张,只取前{}张",
|
||||
imageUrls.size(), DEFAULT_STORYBOARD_IMAGES, DEFAULT_STORYBOARD_IMAGES);
|
||||
imageUrls = imageUrls.subList(0, DEFAULT_STORYBOARD_IMAGES);
|
||||
}
|
||||
@@ -308,22 +345,38 @@ public class StoryboardVideoService {
|
||||
// 参考sora2实现:确保所有图片格式一致
|
||||
List<String> validatedImages = validateAndNormalizeImages(imageUrls);
|
||||
|
||||
// 验证后的图片数量也要满足要求(必须6张)
|
||||
// 验证后的图片数量也要满足要求
|
||||
if (validatedImages.size() < DEFAULT_STORYBOARD_IMAGES) {
|
||||
String errorMsg = String.format("验证后只有%d张图片,需要%d张才能拼接分镜图",
|
||||
String errorMsg = String.format("验证后只有%d张图片,需要%d张",
|
||||
validatedImages.size(), DEFAULT_STORYBOARD_IMAGES);
|
||||
logger.error(errorMsg);
|
||||
throw new RuntimeException(errorMsg);
|
||||
}
|
||||
|
||||
logger.info("开始拼接{}张图片成分镜图网格...", validatedImages.size());
|
||||
|
||||
// 拼接多张图片成网格(支持4-6张图片的灵活拼接)
|
||||
// 使用验证后的图片列表(都是Base64格式)
|
||||
// 处理分镜图
|
||||
String mergedImageUrl;
|
||||
long mergeStartTime = System.currentTimeMillis();
|
||||
String mergedImageUrl = imageGridService.mergeImagesToGrid(validatedImages, 0); // 0表示自动计算列数
|
||||
|
||||
// 如果有参考图片,将原图和生成图拼接在一起
|
||||
if (hasReferenceImage && validatedImages.size() >= 1) {
|
||||
String generatedImage = validatedImages.get(0);
|
||||
logger.info("有参考图片,开始拼接原图和生成图...");
|
||||
// 根据原图比例决定拼接方式:
|
||||
// - 16:9横向图:创建9:16画布,左原图右生成图
|
||||
// - 9:16纵向图:创建16:9画布,上原图下生成图
|
||||
mergedImageUrl = imageGridService.mergeStoryboardImages(imageUrl, generatedImage);
|
||||
logger.info("原图和生成图拼接完成");
|
||||
} else if (validatedImages.size() == 1) {
|
||||
// 无参考图片,只有1张图片,直接使用
|
||||
mergedImageUrl = validatedImages.get(0);
|
||||
logger.info("只有1张分镜图,直接使用");
|
||||
} else {
|
||||
// 多张图片,拼接成网格
|
||||
logger.info("开始拼接{}张图片成分镜图网格...", validatedImages.size());
|
||||
mergedImageUrl = imageGridService.mergeImagesToGrid(validatedImages, 0);
|
||||
}
|
||||
long mergeTime = System.currentTimeMillis() - mergeStartTime;
|
||||
logger.info("图片网格拼接完成,耗时: {}ms", mergeTime);
|
||||
logger.info("分镜图处理完成,耗时: {}ms", mergeTime);
|
||||
|
||||
// 检查拼接后的图片URL是否有效
|
||||
if (mergedImageUrl == null || mergedImageUrl.isEmpty()) {
|
||||
|
||||
@@ -1650,11 +1650,13 @@ public class TaskQueueService {
|
||||
|
||||
/**
|
||||
* 更新任务为完成状态(使用独立事务,快速完成)
|
||||
* 使用 TransactionTemplate 确保事务正确执行(因为私有方法无法使用 @Transactional)
|
||||
* COS上传在事务外执行,避免事务超时
|
||||
*/
|
||||
private void updateTaskAsCompleted(TaskQueue taskQueue, String resultUrl) {
|
||||
String taskId = taskQueue.getTaskId();
|
||||
|
||||
// 第一步:在事务中完成数据库更新(不包含COS上传)
|
||||
try {
|
||||
// 使用 TransactionTemplate 确保在事务中执行
|
||||
transactionTemplate.executeWithoutResult(status -> {
|
||||
try {
|
||||
// 使用悲观锁查询,防止多线程并发重复处理
|
||||
@@ -1678,87 +1680,116 @@ public class TaskQueueService {
|
||||
|
||||
// 扣除冻结的积分(内部已处理重复扣除的情况)
|
||||
try {
|
||||
userService.deductFrozenPoints(taskQueue.getTaskId());
|
||||
userService.deductFrozenPoints(taskId);
|
||||
} catch (Exception e) {
|
||||
// 积分扣除失败不影响任务完成状态
|
||||
}
|
||||
|
||||
// 更新原始任务状态
|
||||
updateOriginalTaskStatus(taskQueue, "COMPLETED", resultUrl, null);
|
||||
// 更新原始任务状态(先用原始URL)
|
||||
updateOriginalTaskStatus(taskQueue, "COMPLETED", resultUrl, null);
|
||||
|
||||
// 上传视频到COS(如果启用)
|
||||
String finalResultUrl = resultUrl;
|
||||
if (resultUrl != null && !resultUrl.isEmpty() && cosService.isEnabled()) {
|
||||
try {
|
||||
logger.info("开始上传视频到COS: taskId={}", taskQueue.getTaskId());
|
||||
String cosUrl = cosService.uploadVideoFromUrl(resultUrl, null);
|
||||
if (cosUrl != null && !cosUrl.isEmpty()) {
|
||||
finalResultUrl = cosUrl;
|
||||
// 更新任务的resultUrl为COS URL
|
||||
updateOriginalTaskStatus(taskQueue, "COMPLETED", cosUrl, null);
|
||||
} else {
|
||||
logger.warn("COS上传失败,使用原始URL: taskId={}", taskQueue.getTaskId());
|
||||
// 创建/更新用户作品
|
||||
if (resultUrl != null && !resultUrl.isEmpty()) {
|
||||
try {
|
||||
userWorkService.createWorkFromTask(taskId, resultUrl);
|
||||
} catch (Exception workException) {
|
||||
if (workException.getMessage() == null ||
|
||||
(!workException.getMessage().contains("已存在") &&
|
||||
!workException.getMessage().contains("Duplicate entry"))) {
|
||||
logger.warn("创建/更新用户作品失败: {}", taskId, workException);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception cosException) {
|
||||
logger.error("上传视频到COS失败,使用原始URL: taskId={}", taskQueue.getTaskId(), cosException);
|
||||
// COS上传失败不影响任务完成,继续使用原始URL
|
||||
}
|
||||
}
|
||||
|
||||
// 创建/更新用户作品 - 在最后执行,避免影响主要流程
|
||||
// 只有在 resultUrl 有效时才更新为 COMPLETED
|
||||
// 如果 resultUrl 为空,不做处理(等待超时机制处理)
|
||||
if (finalResultUrl != null && !finalResultUrl.isEmpty()) {
|
||||
try {
|
||||
userWorkService.createWorkFromTask(taskQueue.getTaskId(), finalResultUrl);
|
||||
} catch (Exception workException) {
|
||||
// 如果是重复创建异常,静默处理
|
||||
if (workException.getMessage() == null ||
|
||||
(!workException.getMessage().contains("已存在") &&
|
||||
!workException.getMessage().contains("Duplicate entry"))) {
|
||||
logger.warn("创建/更新用户作品失败: {}", taskQueue.getTaskId(), workException);
|
||||
}
|
||||
// 作品创建失败不影响任务完成状态
|
||||
}
|
||||
} else {
|
||||
logger.warn("任务返回完成但 resultUrl 为空,保持 user_works 状态不变(等待超时机制处理): {}", taskQueue.getTaskId());
|
||||
}
|
||||
|
||||
// 更新 task_status 表中的状态(保留记录)
|
||||
// 只有在 resultUrl 有效时才更新为 COMPLETED
|
||||
// 如果 resultUrl 为空,保持原状态不变(等待超时机制处理)
|
||||
if (finalResultUrl != null && !finalResultUrl.isEmpty()) {
|
||||
try {
|
||||
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskQueue.getTaskId());
|
||||
if (taskStatus != null) {
|
||||
taskStatus.setStatus(TaskStatus.Status.COMPLETED);
|
||||
taskStatus.setProgress(100);
|
||||
taskStatus.setResultUrl(finalResultUrl);
|
||||
taskStatus.setCompletedAt(java.time.LocalDateTime.now());
|
||||
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||
logger.info("task_status 表状态已更新为 COMPLETED: {}", taskQueue.getTaskId());
|
||||
// 更新 task_status 表中的状态
|
||||
if (resultUrl != null && !resultUrl.isEmpty()) {
|
||||
try {
|
||||
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
|
||||
if (taskStatus != null) {
|
||||
taskStatus.setStatus(TaskStatus.Status.COMPLETED);
|
||||
taskStatus.setProgress(100);
|
||||
taskStatus.setResultUrl(resultUrl);
|
||||
taskStatus.setCompletedAt(java.time.LocalDateTime.now());
|
||||
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||
logger.info("task_status 表状态已更新为 COMPLETED: {}", taskId);
|
||||
}
|
||||
} catch (Exception statusException) {
|
||||
logger.warn("更新 task_status 状态失败: {}", taskId, statusException);
|
||||
}
|
||||
}
|
||||
} catch (Exception statusException) {
|
||||
logger.warn("更新 task_status 状态失败: {}", taskQueue.getTaskId(), statusException);
|
||||
}
|
||||
}
|
||||
|
||||
// 任务完成后从 task_queue 中删除记录(task_status 保留)
|
||||
try {
|
||||
taskQueueRepository.delete(freshTaskQueue);
|
||||
logger.info("任务完成,已从 task_queue 中删除: {}", taskQueue.getTaskId());
|
||||
} catch (Exception deleteException) {
|
||||
logger.warn("删除 task_queue 记录失败: {}", taskQueue.getTaskId(), deleteException);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("更新任务完成状态失败: {}", taskQueue.getTaskId(), e);
|
||||
// 任务完成后从 task_queue 中删除记录
|
||||
try {
|
||||
taskQueueRepository.delete(freshTaskQueue);
|
||||
logger.info("任务完成,已从 task_queue 中删除: {}", taskId);
|
||||
} catch (Exception deleteException) {
|
||||
logger.warn("删除 task_queue 记录失败: {}", taskId, deleteException);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("更新任务完成状态失败: {}", taskId, e);
|
||||
status.setRollbackOnly();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("执行更新任务完成状态事务失败: {}", taskQueue.getTaskId(), e);
|
||||
// 如果原始任务状态更新失败,至少保证队列状态正确
|
||||
logger.error("执行更新任务完成状态事务失败: {}", taskId, e);
|
||||
return; // 事务失败,不继续COS上传
|
||||
}
|
||||
|
||||
// 第二步:在事务外执行COS上传(避免事务超时)
|
||||
if (resultUrl != null && !resultUrl.isEmpty() && cosService.isEnabled()) {
|
||||
try {
|
||||
logger.info("开始上传视频到COS(事务外): taskId={}", taskId);
|
||||
String cosUrl = cosService.uploadVideoFromUrl(resultUrl, null);
|
||||
if (cosUrl != null && !cosUrl.isEmpty()) {
|
||||
logger.info("COS上传成功: taskId={}, cosUrl={}", taskId, cosUrl);
|
||||
// 用新事务更新URL为COS URL
|
||||
updateResultUrlToCos(taskQueue, cosUrl);
|
||||
} else {
|
||||
logger.warn("COS上传返回空URL,使用原始URL: taskId={}", taskId);
|
||||
}
|
||||
} catch (Exception cosException) {
|
||||
logger.error("上传视频到COS失败,使用原始URL: taskId={}", taskId, cosException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新结果URL为COS URL(独立事务)
|
||||
*/
|
||||
private void updateResultUrlToCos(TaskQueue taskQueue, String cosUrl) {
|
||||
String taskId = taskQueue.getTaskId();
|
||||
try {
|
||||
transactionTemplate.executeWithoutResult(status -> {
|
||||
try {
|
||||
// 更新原始任务的resultUrl
|
||||
updateOriginalTaskStatus(taskQueue, "COMPLETED", cosUrl, null);
|
||||
|
||||
// 更新用户作品的resultUrl
|
||||
try {
|
||||
userWorkService.createWorkFromTask(taskId, cosUrl);
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
// 更新task_status的resultUrl
|
||||
try {
|
||||
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
|
||||
if (taskStatus != null) {
|
||||
taskStatus.setResultUrl(cosUrl);
|
||||
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("更新task_status COS URL失败: {}", taskId, e);
|
||||
}
|
||||
|
||||
logger.info("已更新resultUrl为COS URL: taskId={}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.error("更新COS URL失败: {}", taskId, e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("执行更新COS URL事务失败: {}", taskId, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +239,8 @@ public class TaskStatusPollingService {
|
||||
public void pollTaskStatus(TaskStatus task) {
|
||||
logger.info("轮询任务状态: taskId={}, externalTaskId={}", task.getTaskId(), task.getExternalTaskId());
|
||||
|
||||
String[] pendingAction = null;
|
||||
|
||||
try {
|
||||
// 使用正确的 API 端点:GET /v2/videos/generations/{task_id}
|
||||
String url = apiBaseUrl + "/v2/videos/generations/" + task.getExternalTaskId();
|
||||
@@ -249,8 +251,8 @@ public class TaskStatusPollingService {
|
||||
if (response.getStatus() == 200) {
|
||||
JsonNode responseJson = objectMapper.readTree(response.getBody());
|
||||
logger.info("轮询任务状态成功: taskId={}, response={}", task.getTaskId(), response.getBody());
|
||||
// 更新任务状态(使用单独的事务方法)
|
||||
updateTaskStatusWithTransaction(task, responseJson);
|
||||
// 更新任务状态(使用单独的事务方法),返回需要后续处理的信息
|
||||
pendingAction = updateTaskStatusWithTransaction(task, responseJson);
|
||||
} else {
|
||||
logger.warn("查询任务状态失败: taskId={}, status={}, response={}",
|
||||
task.getTaskId(), response.getStatus(), response.getBody());
|
||||
@@ -263,14 +265,32 @@ public class TaskStatusPollingService {
|
||||
// 更新轮询次数(使用单独的事务方法)
|
||||
incrementPollCountWithTransaction(task);
|
||||
}
|
||||
|
||||
// 在事务外处理后续操作(避免事务超时)
|
||||
if (pendingAction != null) {
|
||||
try {
|
||||
String actionType = pendingAction[0];
|
||||
String taskId = pendingAction[1];
|
||||
String data = pendingAction[2];
|
||||
|
||||
if ("COMPLETED".equals(actionType)) {
|
||||
taskQueueService.handleTaskCompletionByTaskId(taskId, data);
|
||||
} else if ("FAILED".equals(actionType)) {
|
||||
taskQueueService.handleTaskFailureByTaskId(taskId, data);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("处理任务后续操作失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态(单独的事务方法)
|
||||
* 更新任务状态(单独的事务方法)- 只更新 TaskStatus 表
|
||||
* 返回值:null=无需后续处理, [0]=完成需处理, [1]=失败需处理
|
||||
*/
|
||||
@Transactional
|
||||
public void updateTaskStatusWithTransaction(TaskStatus task, JsonNode responseJson) {
|
||||
updateTaskStatus(task, responseJson);
|
||||
public String[] updateTaskStatusWithTransaction(TaskStatus task, JsonNode responseJson) {
|
||||
return updateTaskStatusOnly(task, responseJson);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,9 +303,10 @@ public class TaskStatusPollingService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
* 更新任务状态 - 只更新 TaskStatus 表,返回后续需要处理的信息
|
||||
* 返回:[taskId, resultUrl] 表示完成,[taskId, null, errorMessage] 表示失败,null 表示无需后续处理
|
||||
*/
|
||||
private void updateTaskStatus(TaskStatus task, JsonNode responseJson) {
|
||||
private String[] updateTaskStatusOnly(TaskStatus task, JsonNode responseJson) {
|
||||
try {
|
||||
String status = responseJson.path("status").asText();
|
||||
String progressStr = responseJson.path("progress").asText("0%");
|
||||
@@ -304,22 +325,21 @@ public class TaskStatusPollingService {
|
||||
task.markAsCompleted(resultUrl);
|
||||
logger.info("任务完成: taskId={}, resultUrl={}", task.getTaskId(), resultUrl);
|
||||
taskStatusRepository.save(task);
|
||||
// 同步更新业务表、UserWork 等
|
||||
taskQueueService.handleTaskCompletionByTaskId(task.getTaskId(), resultUrl);
|
||||
// 返回任务信息,在事务外处理
|
||||
return new String[]{"COMPLETED", task.getTaskId(), resultUrl};
|
||||
} else {
|
||||
logger.warn("任务状态为成功但 resultUrl 为空,保持 PROCESSING: taskId={}", task.getTaskId());
|
||||
taskStatusRepository.save(task);
|
||||
}
|
||||
return; // 已保存,直接返回
|
||||
return null;
|
||||
|
||||
case "failed":
|
||||
case "error":
|
||||
task.markAsFailed(errorMessage);
|
||||
logger.warn("任务失败: taskId={}, error={}", task.getTaskId(), errorMessage);
|
||||
taskStatusRepository.save(task);
|
||||
// 同步更新业务表、UserWork 等
|
||||
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), errorMessage);
|
||||
return; // 已保存,直接返回
|
||||
// 返回任务信息,在事务外处理
|
||||
return new String[]{"FAILED", task.getTaskId(), errorMessage};
|
||||
|
||||
case "processing":
|
||||
case "in_progress":
|
||||
@@ -334,9 +354,11 @@ public class TaskStatusPollingService {
|
||||
}
|
||||
|
||||
taskStatusRepository.save(task);
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("更新任务状态时发生错误: taskId={}, error={}", task.getTaskId(), e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ public class TextToVideoService {
|
||||
throw new IllegalArgumentException("视频时长必须在1-60秒之间");
|
||||
}
|
||||
|
||||
// 检查用户所有类型任务的总数(统一检查)
|
||||
userWorkService.checkMaxConcurrentTasks(username);
|
||||
|
||||
// 生成任务ID
|
||||
String taskId = generateTaskId();
|
||||
|
||||
@@ -269,6 +272,16 @@ public class TextToVideoService {
|
||||
task.updateStatus(TextToVideoTask.TaskStatus.COMPLETED);
|
||||
task.updateProgress(100);
|
||||
taskRepository.save(task);
|
||||
|
||||
// 同步更新 UserWork 表的状态和结果URL
|
||||
try {
|
||||
userWorkService.updateWorkOnComplete(task.getTaskId(), resultUrl,
|
||||
com.example.demo.model.UserWork.WorkStatus.COMPLETED);
|
||||
logger.info("文生视频任务完成,UserWork已更新: {}", task.getTaskId());
|
||||
} catch (Exception e) {
|
||||
logger.warn("更新UserWork状态失败: taskId={}, error={}", task.getTaskId(), e.getMessage());
|
||||
}
|
||||
|
||||
logger.info("文生视频任务完成: {}", task.getTaskId());
|
||||
return;
|
||||
} else if ("failed".equals(status) || "error".equals(status)) {
|
||||
|
||||
@@ -4,11 +4,15 @@ import java.time.LocalDateTime;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.config.CacheConfig;
|
||||
import com.example.demo.model.PointsFreezeRecord;
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.repository.PointsFreezeRecordRepository;
|
||||
@@ -25,16 +29,19 @@ public class UserService {
|
||||
private final PointsFreezeRecordRepository pointsFreezeRecordRepository;
|
||||
private final com.example.demo.repository.OrderRepository orderRepository;
|
||||
private final com.example.demo.repository.PaymentRepository paymentRepository;
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder,
|
||||
PointsFreezeRecordRepository pointsFreezeRecordRepository,
|
||||
com.example.demo.repository.OrderRepository orderRepository,
|
||||
com.example.demo.repository.PaymentRepository paymentRepository) {
|
||||
com.example.demo.repository.PaymentRepository paymentRepository,
|
||||
CacheManager cacheManager) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.pointsFreezeRecordRepository = pointsFreezeRecordRepository;
|
||||
this.orderRepository = orderRepository;
|
||||
this.paymentRepository = paymentRepository;
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -74,11 +81,13 @@ public class UserService {
|
||||
return userRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("用户不存在"));
|
||||
}
|
||||
|
||||
@Cacheable(value = CacheConfig.USER_CACHE, key = "#username", unless = "#result == null")
|
||||
@Transactional(readOnly = true)
|
||||
public User findByUsername(String username) {
|
||||
return userRepository.findByUsername(username).orElseThrow(() -> new IllegalArgumentException("用户不存在"));
|
||||
}
|
||||
|
||||
@Cacheable(value = CacheConfig.USER_CACHE, key = "#username", unless = "#result == null")
|
||||
@Transactional(readOnly = true)
|
||||
public User findByUsernameOrNull(String username) {
|
||||
return userRepository.findByUsername(username).orElse(null);
|
||||
@@ -97,7 +106,8 @@ public class UserService {
|
||||
@Transactional
|
||||
public User update(Long id, String username, String email, String rawPasswordNullable, String role) {
|
||||
User user = findById(id);
|
||||
if (!user.getUsername().equals(username) && userRepository.existsByUsername(username)) {
|
||||
String oldUsername = user.getUsername();
|
||||
if (!oldUsername.equals(username) && userRepository.existsByUsername(username)) {
|
||||
throw new IllegalArgumentException("用户名已存在");
|
||||
}
|
||||
if (!user.getEmail().equals(email) && userRepository.existsByEmail(email)) {
|
||||
@@ -111,11 +121,35 @@ public class UserService {
|
||||
if (rawPasswordNullable != null && !rawPasswordNullable.isBlank()) {
|
||||
user.setPasswordHash(rawPasswordNullable);
|
||||
}
|
||||
return userRepository.save(user);
|
||||
User savedUser = userRepository.save(user);
|
||||
|
||||
// 手动清除旧用户名和新用户名的缓存
|
||||
evictUserCache(oldUsername);
|
||||
if (!oldUsername.equals(username)) {
|
||||
evictUserCache(username);
|
||||
}
|
||||
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动清除用户缓存
|
||||
* 注意:使用 CacheManager 直接操作,避免同类内部调用 @CacheEvict 不生效的问题
|
||||
*/
|
||||
public void evictUserCache(String username) {
|
||||
Cache cache = cacheManager.getCache(CacheConfig.USER_CACHE);
|
||||
if (cache != null && username != null) {
|
||||
cache.evict(username);
|
||||
logger.debug("清除用户缓存: {}", username);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
// 先获取用户名用于清除缓存
|
||||
userRepository.findById(id).ifPresent(user -> {
|
||||
evictUserCache(user.getUsername());
|
||||
});
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@@ -129,8 +163,9 @@ public class UserService {
|
||||
/**
|
||||
* 修改指定用户的密码
|
||||
*
|
||||
* 如果用户已经有密码,则需要提供正确的原密码;
|
||||
* 如果用户当前没有设置密码(例如仅使用邮箱验证码登录),则可以直接设置新密码。
|
||||
* 原密码为可选:
|
||||
* - 如果提供了原密码,则验证原密码是否正确
|
||||
* - 如果未提供原密码,则直接设置新密码
|
||||
*/
|
||||
@Transactional
|
||||
public void changePassword(Long userId, String oldPassword, String newPassword) {
|
||||
@@ -139,23 +174,31 @@ public class UserService {
|
||||
if (newPassword == null || newPassword.isBlank()) {
|
||||
throw new IllegalArgumentException("新密码不能为空");
|
||||
}
|
||||
if (newPassword.length() < 6) {
|
||||
throw new IllegalArgumentException("新密码长度不能少于6位");
|
||||
if (newPassword.length() < 8) {
|
||||
throw new IllegalArgumentException("新密码长度不能少于8位");
|
||||
}
|
||||
// 验证密码必须包含英文字母和数字
|
||||
if (!newPassword.matches(".*[a-zA-Z].*")) {
|
||||
throw new IllegalArgumentException("新密码必须包含英文字母");
|
||||
}
|
||||
if (!newPassword.matches(".*[0-9].*")) {
|
||||
throw new IllegalArgumentException("新密码必须包含数字");
|
||||
}
|
||||
|
||||
String currentPasswordHash = user.getPasswordHash();
|
||||
// 如果已经设置过密码,则需要校验原密码
|
||||
if (currentPasswordHash != null && !currentPasswordHash.isBlank()) {
|
||||
if (oldPassword == null || oldPassword.isBlank()) {
|
||||
throw new IllegalArgumentException("原密码不能为空");
|
||||
}
|
||||
if (!checkPassword(oldPassword, currentPasswordHash)) {
|
||||
throw new IllegalArgumentException("原密码不正确");
|
||||
// 如果提供了原密码,则需要验证
|
||||
if (oldPassword != null && !oldPassword.isBlank()) {
|
||||
if (currentPasswordHash != null && !currentPasswordHash.isBlank()) {
|
||||
if (!checkPassword(oldPassword, currentPasswordHash)) {
|
||||
throw new IllegalArgumentException("原密码不正确");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果未提供原密码,直接设置新密码
|
||||
|
||||
user.setPasswordHash(passwordEncoder.encode(newPassword));
|
||||
userRepository.save(user);
|
||||
evictUserCache(user.getUsername()); // 清除缓存
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +227,22 @@ public class UserService {
|
||||
*/
|
||||
@Transactional
|
||||
public User save(User user) {
|
||||
return userRepository.save(user);
|
||||
User savedUser = userRepository.save(user);
|
||||
evictUserCache(user.getUsername()); // 清除缓存
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户活跃时间(不清除缓存,避免频繁清除影响性能)
|
||||
* 此方法专门用于 UserActivityInterceptor,仅更新 lastActiveTime 字段
|
||||
*/
|
||||
@Transactional
|
||||
public void updateLastActiveTime(String username) {
|
||||
userRepository.findByUsername(username).ifPresent(user -> {
|
||||
user.setLastActiveTime(java.time.LocalDateTime.now());
|
||||
userRepository.save(user);
|
||||
// 注意:不清除缓存,因为 lastActiveTime 不是缓存的关键数据
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,6 +250,7 @@ public class UserService {
|
||||
*/
|
||||
@Transactional
|
||||
public User addPoints(Long userId, Integer points) {
|
||||
java.util.Objects.requireNonNull(userId, "用户ID不能为空");
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||
|
||||
@@ -201,7 +260,9 @@ public class UserService {
|
||||
}
|
||||
|
||||
user.setPoints(newPoints);
|
||||
return userRepository.save(user);
|
||||
User savedUser = userRepository.save(user);
|
||||
evictUserCache(user.getUsername()); // 清除缓存
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +279,9 @@ public class UserService {
|
||||
}
|
||||
|
||||
user.setPoints(newPoints);
|
||||
return userRepository.save(user);
|
||||
User savedUser = userRepository.save(user);
|
||||
evictUserCache(user.getUsername()); // 清除缓存
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,6 +305,7 @@ public class UserService {
|
||||
// 增加冻结积分
|
||||
user.setFrozenPoints(user.getFrozenPoints() + points);
|
||||
userRepository.save(user);
|
||||
evictUserCache(username); // 清除缓存
|
||||
|
||||
// 创建冻结记录
|
||||
PointsFreezeRecord record = new PointsFreezeRecord(username, taskId, taskType, points, reason);
|
||||
@@ -278,6 +342,7 @@ public class UserService {
|
||||
user.setPoints(user.getPoints() - record.getFreezePoints());
|
||||
user.setFrozenPoints(user.getFrozenPoints() - record.getFreezePoints());
|
||||
userRepository.save(user);
|
||||
evictUserCache(record.getUsername()); // 清除缓存
|
||||
|
||||
// 更新冻结记录状态
|
||||
record.updateStatus(PointsFreezeRecord.FreezeStatus.DEDUCTED);
|
||||
@@ -303,6 +368,7 @@ public class UserService {
|
||||
// 减少冻结积分(总积分不变)
|
||||
user.setFrozenPoints(user.getFrozenPoints() - record.getFreezePoints());
|
||||
userRepository.save(user);
|
||||
evictUserCache(record.getUsername()); // 清除缓存
|
||||
|
||||
// 更新冻结记录状态
|
||||
record.updateStatus(PointsFreezeRecord.FreezeStatus.RETURNED);
|
||||
@@ -319,6 +385,7 @@ public class UserService {
|
||||
|
||||
user.setPoints(user.getPoints() + points);
|
||||
userRepository.save(user);
|
||||
evictUserCache(username); // 清除缓存
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -331,6 +398,7 @@ public class UserService {
|
||||
|
||||
user.setPoints(points);
|
||||
userRepository.save(user);
|
||||
evictUserCache(username); // 清除缓存
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,31 @@ public class UserWorkService {
|
||||
@Autowired
|
||||
private StoryboardVideoTaskRepository storyboardVideoTaskRepository;
|
||||
|
||||
// 每个用户最多同时执行的任务数
|
||||
private static final int MAX_CONCURRENT_TASKS = 3;
|
||||
|
||||
/**
|
||||
* 检查用户是否可以创建新任务
|
||||
* @param username 用户名
|
||||
* @throws IllegalStateException 如果用户已达到最大并发任务数
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public void checkMaxConcurrentTasks(String username) {
|
||||
long processingCount = userWorkRepository.countProcessingTasksByUsername(username);
|
||||
if (processingCount >= MAX_CONCURRENT_TASKS) {
|
||||
throw new IllegalStateException("您当前有 " + processingCount + " 个任务正在执行,最多同时执行 " + MAX_CONCURRENT_TASKS + " 个任务,请等待部分任务完成后再提交");
|
||||
}
|
||||
logger.info("用户 {} 当前进行中任务数: {}/{}", username, processingCount, MAX_CONCURRENT_TASKS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户当前进行中的任务数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long getProcessingTaskCount(String username) {
|
||||
return userWorkRepository.countProcessingTasksByUsername(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从任务创建作品
|
||||
* 使用 REQUIRES_NEW 传播行为,防止内部异常导致外部事务回滚
|
||||
@@ -354,16 +379,23 @@ public class UserWorkService {
|
||||
Pageable pageable = PageRequest.of(page, size);
|
||||
return userWorkRepository.findByUsernameWithResultUrlOrderByCreatedAtDesc(username, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有作品列表(分页),包括正在排队和生成中的作品
|
||||
* 用于"我的作品"页面,显示所有状态的作品
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<UserWork> getAllUserWorks(String username, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size);
|
||||
return userWorkRepository.findByUsernameOrderByCreatedAtDesc(username, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户正在进行中的作品(包括PROCESSING和PENDING状态)
|
||||
* 只返回最近24小时内的任务,避免返回陈旧的僵尸任务
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public java.util.List<UserWork> getProcessingWorks(String username) {
|
||||
// 只查询最近24小时内的任务
|
||||
LocalDateTime afterTime = LocalDateTime.now().minusHours(24);
|
||||
return userWorkRepository.findByUsernameAndProcessingOrPendingOrderByCreatedAtDesc(username, afterTime);
|
||||
return userWorkRepository.findByUsernameAndProcessingOrPendingOrderByCreatedAtDesc(username);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -452,6 +484,26 @@ public class UserWorkService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务完成时更新作品状态和结果URL
|
||||
* 使用 REQUIRES_NEW 传播行为,防止内部异常导致外部事务回滚
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void updateWorkOnComplete(String taskId, String resultUrl, UserWork.WorkStatus status) {
|
||||
Optional<UserWork> workOpt = userWorkRepository.findByTaskId(taskId);
|
||||
if (workOpt.isPresent()) {
|
||||
UserWork work = workOpt.get();
|
||||
work.setStatus(status);
|
||||
work.setResultUrl(resultUrl);
|
||||
work.setUpdatedAt(LocalDateTime.now());
|
||||
userWorkRepository.save(work);
|
||||
logger.info("任务完成,更新作品: taskId={}, status={}, resultUrl={}",
|
||||
taskId, status, resultUrl != null ? resultUrl.substring(0, Math.min(50, resultUrl.length())) + "..." : "null");
|
||||
} else {
|
||||
logger.warn("未找到对应的作品记录: taskId={}", taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公开作品列表
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#Updated by API Key Management
|
||||
#Mon Nov 24 17:13:13 CST 2025
|
||||
#Sat Dec 06 10:23:35 CST 2025
|
||||
ai.api.base-url=https\://ai.comfly.chat
|
||||
ai.api.key=sk-6J0Lpb0NYSwCCEbFUym8SZho1kJZPFN9au19VC78vJckTbCc
|
||||
ai.image.api.base-url=https\://ai.comfly.chat
|
||||
@@ -7,21 +7,26 @@ ai.image.api.key=sk-6J0Lpb0NYSwCCEbFUym8SZho1kJZPFN9au19VC78vJckTbCc
|
||||
alipay.app-id=9021000157616562
|
||||
alipay.charset=UTF-8
|
||||
alipay.gateway-url=https\://openapi-sandbox.dl.alipaydev.com/gateway.do
|
||||
alipay.notify-url=https\://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/notify
|
||||
alipay.notify-url=https\://vionow.com/api/payments/alipay/notify
|
||||
alipay.private-key=MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCH7wPeptkJlJuoKwDqxvfJJLTOAWVkHa/TLh+wiy1tEtmwcrOwEU3GuqfkUlhij71WJIZi8KBytCwbax1QGZA/oLXvTCGJJrYrsEL624X5gGCCPKWwHRDhewsQ5W8jFxaaMXxth8GKlSW61PZD2cOQClRVEm2xnWFZ+6/7WBI7082g7ayzGCD2eowXsJyWyuEBCUSbHXkSgxVhqj5wUGIXhr8ly+pdUlJmDX5K8UG2rjJYx+0AU5UZJbOAND7d3iyDsOulHDvth50t8MOWDnDCVJ2aAgUB5FZKtOFxOmzNTsMjvzYldFztF0khbypeeMVL2cxgioIgTvjBkUwd55hZAgMBAAECggEAUjk3pARUoEDt7skkYt87nsW/QCUECY0Tf7AUpxtovON8Hgkju8qbuyvIxokwwV2k72hkiZB33Soyy9r8/iiYYoR5yGfKmUV7R+30df03ivYmamD48BCE138v8GZ31Ufv+hEY7MADSCpzihGrbNtaOdSlslfVVmyWKHHfvy9EyD6yHJGYswLpHXC/QX1TuLRRxk6Uup8qENOG/6zjGWMfxoRZFwTt80ml1mKy32YZGyJqDaQpJcdYwAHOPcnJl1emw4E+oVjiLyksl643npuTkgnZXs1iWcWSS8ojF1w/0kVDzcNh9toLg+HDuQlIHOis01VQ7lYcG4oiMOnhX1QHIQKBgQC9fgBuILjBhuCI9fHvLRdzoNC9heD54YK7xGvEV/mv90k8xcmNx+Yg5C57ASaMRtOq3b7muPiCv5wOtMT4tUCcMIwSrTNlcBM6EoTagnaGfpzOMaHGMXO4vbaw+MIynHnvXFj1rZjG1lzkV/9K36LAaHD9ZKVJaBQ9mK+0CIq/3QKBgQC3pL5GbvXj6/4ahTraXzNDQQpPGVgbHxcOioEXL4ibaOPC58puTW8HDbRvVuhl/4EEOBRVX81BSgkN8XHwTSiZdih2iOqByg+o9kixs7nlFn3Iw9BBP2/g+Wqiyi2N+9g17kfWXXVOKYz/eMXLBeOo4KhQE9wqNGyZldYzX2ywrQKBgApJmvBfqmgnUG1fHOFlS06lvm9ro0ktqxFSmp8wP4gEHt/DxSuDXMUQXk2jRFp9ReSS4VhZVnSSvoA15DO0c2uHXzNsX8v0B7cxZjEOwCyRFyZCn4vJB4VSF2cIOlLRF/Wcx9+eqxqwbJ6hAGUqOwXDJc879ZVEp0So03EsvYupAoGAAnI+Wp/VxLB7FQ1bSFdmTmoKYh1bUBks7HOp3o4yiqduCUWfK7L6XKSxF56Xv+wUYuMAWlbJXCpJTpc9xk6w0MKDLXkLbqkrZjvJohxbyJJxIICDQKtAqUWJRxvcWXzWV3mSGWfrTRw+lZSdReQRMUm01EQ/dYx3OeCGFu8Zeo0CgYAlH5YSYdJxZSoDCJeoTrkxUlFoOg8UQ7SrsaLYLwpwcwpuiWJaTrg6jwFocj+XhjQ9RtRbSBHz2wKSLdl+pXbTbqECKk85zMFl6zG3etXtTJU/dD750Ty4i8zt3+JGhvglPrQBY1CfItgml2oXa/VUVMnLCUS0WSZuPRmPYZD8dg\=\=
|
||||
alipay.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAksEwzuR3ASrKtTzaANqdQYKOoD44itA1TWG/6onvQr8PHNEMgcguLuJNrdeuT2PDg23byzZ9qKfEM2D5U4zbpt0/uCYLfZQyAnAWWyMvnKPoSIgrtBjnxYK6HE6fuQV3geJTcZxvP/z8dGZB0V0s6a53rzbKSLh0p4w0hWfVXlQihq3Xh4vSKB+ojdhEkIblhpWPT42NPbjVNdwPzIhUGpRy3/nsgNqVBu+ZacQ5/rCvzXU1RE0allBbjcvjymKQTS7bAE0i1Mgo1eX8njvElsfQUv5P7xQdrvZagqtIuTdP19cmsSNGdIC9Z5Po3j0z3KWPR7MrKgDuJfzkWtJR4wIDAQAB
|
||||
alipay.return-url=https\://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/return
|
||||
alipay.return-url=https\://vionow.com/api/payments/alipay/return
|
||||
alipay.server-url=https\://openapi-sandbox.dl.alipaydev.com/gateway.do
|
||||
alipay.sign-type=RSA2
|
||||
app.ffmpeg.path=C\:/Users/UI/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-8.0-full_build/bin/ffmpeg.exe
|
||||
app.temp.dir=./temp
|
||||
jwt.expiration=86400000
|
||||
jwt.expiration=7200000
|
||||
jwt.secret=mySecretKey123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
logging.level.com.example.demo=DEBUG
|
||||
logging.level.org.hibernate.SQL=WARN
|
||||
logging.level.org.hibernate.orm.jdbc.bind=WARN
|
||||
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=WARN
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
logging.level.org.springframework.security=WARN
|
||||
paypal.cancel-url=https\://vionow.com/api/payment/paypal/cancel
|
||||
paypal.client-id=Adpi67TvppjhyyWhrALWwJhLFzv5S_vXoUHzWQchqZe48NaONSryg7QHKBubf0PRmkeJoaxGEKV5v9lT
|
||||
paypal.client-secret=EDzZl-hddwtt2pNt5RpBIICdlrUS8QtcmAttU_kuANL8Vd937SC4xel_K2hArTovVqEtyL2ZS5IcQcQV
|
||||
paypal.mode=sandbox
|
||||
paypal.success-url=https\://vionow.com/api/payment/paypal/success
|
||||
server.port=8080
|
||||
server.tomcat.accept-count=100
|
||||
server.tomcat.connection-timeout=20000
|
||||
@@ -56,11 +61,5 @@ tencent.ses.region=ap-hongkong
|
||||
tencent.ses.secret-id=AKIDoaEjFbqxxqZAcv8EE6oZCg2IQPG1fCxm
|
||||
tencent.ses.secret-key=nR83I79FOSpGcqNo7JXkqnU8g7SjsxuG
|
||||
tencent.ses.template-id=154360
|
||||
# ============================================
|
||||
# PayPal支付配置(沙箱测试环境)
|
||||
# ============================================
|
||||
paypal.client-id=Adpi67TvppjhyyWhrALWwJhLFzv5S_vXoUHzWQchqZe48NaONSryg7QHKBubf0PRmkeJoaxGEKV5v9lT
|
||||
paypal.client-secret=EDzZl-hddwtt2pNt5RpBIICdlrUS8QtcmAttU_kuANL8Vd937SC4xel_K2hArTovVqEtyL2ZS5IcQcQV
|
||||
paypal.mode=sandbox
|
||||
paypal.success-url=http://localhost:8080/api/payment/paypal/success
|
||||
paypal.cancel-url=http://localhost:8080/api/payment/paypal/cancel
|
||||
|
||||
alipay.domain=https\://vionow.com
|
||||
|
||||
@@ -17,6 +17,20 @@ server.tomcat.max-http-post-size=600MB
|
||||
# JPA配置 - 禁用open-in-view避免视图层执行SQL查询
|
||||
spring.jpa.open-in-view=false
|
||||
|
||||
# HikariCP连接池配置
|
||||
# 连接泄漏检测阈值(毫秒),设置为0禁用检测,避免长时间任务触发误报
|
||||
spring.datasource.hikari.leak-detection-threshold=0
|
||||
# 最大连接池大小
|
||||
spring.datasource.hikari.maximum-pool-size=20
|
||||
# 最小空闲连接数
|
||||
spring.datasource.hikari.minimum-idle=5
|
||||
# 连接超时(毫秒)
|
||||
spring.datasource.hikari.connection-timeout=30000
|
||||
# 空闲连接超时(毫秒)
|
||||
spring.datasource.hikari.idle-timeout=600000
|
||||
# 连接最大存活时间(毫秒)
|
||||
spring.datasource.hikari.max-lifetime=1800000
|
||||
|
||||
# 应用配置
|
||||
app.upload.path=uploads
|
||||
app.video.output.path=outputs
|
||||
@@ -52,6 +66,8 @@ tencent.cos.secret-key=Xrxywju0wfAf3QiqlT2ZvGYgeS6WjnjT
|
||||
tencent.cos.region=ap-nanjing
|
||||
# COS存储桶名称(例如:my-bucket-1234567890)
|
||||
tencent.cos.bucket-name=test-1323844400
|
||||
# COS文件夹前缀(所有文件存储在此目录下)
|
||||
tencent.cos.prefix=test-sx
|
||||
|
||||
# ============================================
|
||||
# PayPal支付配置
|
||||
@@ -69,3 +85,18 @@ paypal.success-url=https://vionow.com/api/payment/paypal/success
|
||||
# 支付取消回调URL
|
||||
paypal.cancel-url=https://vionow.com/api/payment/paypal/cancel
|
||||
|
||||
# ============================================
|
||||
# GZIP 压缩配置(提升传输性能)
|
||||
# ============================================
|
||||
server.compression.enabled=true
|
||||
server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml
|
||||
server.compression.min-response-size=1024
|
||||
|
||||
# ============================================
|
||||
# 日志配置
|
||||
# ============================================
|
||||
# 关闭 Spring Security DEBUG 日志
|
||||
logging.level.org.springframework.security=INFO
|
||||
# 减少 Tomcat HTTP 解析错误日志(扫描器/HTTPS误连等导致的)
|
||||
logging.level.org.apache.coyote.http11.Http11Processor=ERROR
|
||||
logging.level.org.apache.coyote.AbstractProtocol=ERROR
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
-- 注意:生产环境部署时,此文件应保持为空或仅包含必要的系统配置数据
|
||||
|
||||
-- ============================================
|
||||
-- 管理员权限自动设置
|
||||
-- 超级管理员权限自动设置
|
||||
-- ============================================
|
||||
-- 应用启动时自动将 984523799@qq.com 设置为管理员
|
||||
-- 如果该用户存在,则更新其角色为管理员
|
||||
-- 应用启动时自动将 984523799@qq.com 设置为超级管理员
|
||||
-- 如果该用户存在,则更新其角色为超级管理员
|
||||
UPDATE users
|
||||
SET role = 'ROLE_ADMIN',
|
||||
SET role = 'ROLE_SUPER_ADMIN',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE email = '984523799@qq.com';
|
||||
|
||||
48
demo/src/main/resources/db/migration/V2__add_indexes.sql
Normal file
48
demo/src/main/resources/db/migration/V2__add_indexes.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
-- 性能优化索引(MySQL 兼容)
|
||||
-- 注意:此文件需要手动在数据库中执行,如果索引已存在会报错(可忽略)
|
||||
|
||||
-- users 表索引
|
||||
ALTER TABLE users ADD INDEX idx_users_phone (phone);
|
||||
ALTER TABLE users ADD INDEX idx_users_is_active (is_active);
|
||||
ALTER TABLE users ADD INDEX idx_users_last_active_time (last_active_time);
|
||||
ALTER TABLE users ADD INDEX idx_users_created_at (created_at);
|
||||
|
||||
-- payments 表索引
|
||||
ALTER TABLE payments ADD INDEX idx_payments_user_id (user_id);
|
||||
ALTER TABLE payments ADD INDEX idx_payments_status (status);
|
||||
ALTER TABLE payments ADD INDEX idx_payments_created_at (created_at);
|
||||
ALTER TABLE payments ADD INDEX idx_payments_user_status (user_id, status);
|
||||
|
||||
-- orders 表索引
|
||||
ALTER TABLE orders ADD INDEX idx_orders_user_id (user_id);
|
||||
ALTER TABLE orders ADD INDEX idx_orders_status (status);
|
||||
ALTER TABLE orders ADD INDEX idx_orders_created_at (created_at);
|
||||
ALTER TABLE orders ADD INDEX idx_orders_user_status (user_id, status);
|
||||
|
||||
-- user_works 表索引(高频查询优化)
|
||||
ALTER TABLE user_works ADD INDEX idx_user_works_username (username);
|
||||
ALTER TABLE user_works ADD INDEX idx_user_works_status (status);
|
||||
ALTER TABLE user_works ADD INDEX idx_user_works_task_id (task_id);
|
||||
ALTER TABLE user_works ADD INDEX idx_user_works_created_at (created_at);
|
||||
ALTER TABLE user_works ADD INDEX idx_user_works_username_status (username, status);
|
||||
ALTER TABLE user_works ADD INDEX idx_user_works_is_public_status (is_public, status);
|
||||
|
||||
-- text_to_video_tasks 表索引
|
||||
ALTER TABLE text_to_video_tasks ADD INDEX idx_text_to_video_tasks_task_id (task_id);
|
||||
ALTER TABLE text_to_video_tasks ADD INDEX idx_text_to_video_tasks_username (username);
|
||||
ALTER TABLE text_to_video_tasks ADD INDEX idx_text_to_video_tasks_status (status);
|
||||
|
||||
-- image_to_video_tasks 表索引
|
||||
ALTER TABLE image_to_video_tasks ADD INDEX idx_image_to_video_tasks_task_id (task_id);
|
||||
ALTER TABLE image_to_video_tasks ADD INDEX idx_image_to_video_tasks_username (username);
|
||||
ALTER TABLE image_to_video_tasks ADD INDEX idx_image_to_video_tasks_status (status);
|
||||
|
||||
-- storyboard_video_tasks 表索引
|
||||
ALTER TABLE storyboard_video_tasks ADD INDEX idx_storyboard_video_tasks_task_id (task_id);
|
||||
ALTER TABLE storyboard_video_tasks ADD INDEX idx_storyboard_video_tasks_username (username);
|
||||
ALTER TABLE storyboard_video_tasks ADD INDEX idx_storyboard_video_tasks_status (status);
|
||||
|
||||
-- points_freeze_records 表索引
|
||||
ALTER TABLE points_freeze_records ADD INDEX idx_points_freeze_records_task_id (task_id);
|
||||
ALTER TABLE points_freeze_records ADD INDEX idx_points_freeze_records_username (username);
|
||||
ALTER TABLE points_freeze_records ADD INDEX idx_points_freeze_records_status (status);
|
||||
@@ -0,0 +1,126 @@
|
||||
-- ============================================
|
||||
-- 任务状态级联更新触发器
|
||||
-- 当 task_status 表状态更新时,自动同步到其他关联表
|
||||
-- ============================================
|
||||
|
||||
DELIMITER //
|
||||
|
||||
-- 删除已存在的触发器(如果存在)
|
||||
DROP TRIGGER IF EXISTS trg_task_status_update//
|
||||
|
||||
-- 创建触发器:当 task_status 更新时,级联更新其他表
|
||||
CREATE TRIGGER trg_task_status_update
|
||||
AFTER UPDATE ON task_status
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- 只在状态发生变化时触发
|
||||
IF NEW.status <> OLD.status THEN
|
||||
|
||||
-- 更新 task_queue 表
|
||||
UPDATE task_queue
|
||||
SET status = NEW.status,
|
||||
updated_at = NOW(),
|
||||
error_message = CASE WHEN NEW.status = 'FAILED' THEN NEW.error_message ELSE error_message END
|
||||
WHERE task_id = NEW.task_id;
|
||||
|
||||
-- 更新 user_works 表
|
||||
UPDATE user_works
|
||||
SET status = CASE
|
||||
WHEN NEW.status = 'COMPLETED' THEN 'COMPLETED'
|
||||
WHEN NEW.status = 'FAILED' THEN 'FAILED'
|
||||
WHEN NEW.status = 'PROCESSING' THEN 'PROCESSING'
|
||||
WHEN NEW.status = 'PENDING' THEN 'PENDING'
|
||||
WHEN NEW.status = 'CANCELLED' THEN 'CANCELLED'
|
||||
ELSE status
|
||||
END,
|
||||
updated_at = NOW(),
|
||||
result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END
|
||||
WHERE task_id = NEW.task_id;
|
||||
|
||||
-- 更新 text_to_video_tasks 表(根据 task_id 前缀判断)
|
||||
IF NEW.task_id LIKE 'txt2vid_%' THEN
|
||||
UPDATE text_to_video_tasks
|
||||
SET status = NEW.status,
|
||||
updated_at = NOW(),
|
||||
error_message = CASE WHEN NEW.status = 'FAILED' THEN NEW.error_message ELSE error_message END,
|
||||
result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END
|
||||
WHERE task_id = NEW.task_id;
|
||||
END IF;
|
||||
|
||||
-- 更新 image_to_video_tasks 表
|
||||
IF NEW.task_id LIKE 'img2vid_%' THEN
|
||||
UPDATE image_to_video_tasks
|
||||
SET status = NEW.status,
|
||||
updated_at = NOW(),
|
||||
error_message = CASE WHEN NEW.status = 'FAILED' THEN NEW.error_message ELSE error_message END,
|
||||
result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END
|
||||
WHERE task_id = NEW.task_id;
|
||||
END IF;
|
||||
|
||||
-- 更新 storyboard_video_tasks 表(分镜视频,排除分镜图)
|
||||
IF NEW.task_id LIKE 'storyboard_%' AND NEW.task_id NOT LIKE '%_image' THEN
|
||||
UPDATE storyboard_video_tasks
|
||||
SET status = NEW.status,
|
||||
updated_at = NOW(),
|
||||
error_message = CASE WHEN NEW.status = 'FAILED' THEN NEW.error_message ELSE error_message END,
|
||||
result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END
|
||||
WHERE task_id = NEW.task_id;
|
||||
END IF;
|
||||
|
||||
-- 分镜图任务(taskId 以 _image 结尾)
|
||||
IF NEW.task_id LIKE '%_image' THEN
|
||||
-- 分镜图关联到主任务,更新主任务的时间戳和错误信息
|
||||
UPDATE storyboard_video_tasks
|
||||
SET updated_at = NOW(),
|
||||
error_message = CASE WHEN NEW.status = 'FAILED' THEN NEW.error_message ELSE error_message END
|
||||
WHERE task_id = REPLACE(NEW.task_id, '_image', '');
|
||||
END IF;
|
||||
|
||||
END IF;
|
||||
END//
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- ============================================
|
||||
-- 可选:反向触发器(业务表更新时同步到 task_status)
|
||||
-- 根据需要启用
|
||||
-- ============================================
|
||||
|
||||
/*
|
||||
DELIMITER //
|
||||
|
||||
-- text_to_video_tasks 状态更新时同步
|
||||
DROP TRIGGER IF EXISTS trg_text_to_video_status_update//
|
||||
CREATE TRIGGER trg_text_to_video_status_update
|
||||
AFTER UPDATE ON text_to_video_tasks
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.status <> OLD.status THEN
|
||||
UPDATE task_status SET status = NEW.status, updated_at = NOW() WHERE task_id = NEW.task_id;
|
||||
END IF;
|
||||
END//
|
||||
|
||||
-- image_to_video_tasks 状态更新时同步
|
||||
DROP TRIGGER IF EXISTS trg_image_to_video_status_update//
|
||||
CREATE TRIGGER trg_image_to_video_status_update
|
||||
AFTER UPDATE ON image_to_video_tasks
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.status <> OLD.status THEN
|
||||
UPDATE task_status SET status = NEW.status, updated_at = NOW() WHERE task_id = NEW.task_id;
|
||||
END IF;
|
||||
END//
|
||||
|
||||
-- storyboard_video_tasks 状态更新时同步
|
||||
DROP TRIGGER IF EXISTS trg_storyboard_status_update//
|
||||
CREATE TRIGGER trg_storyboard_status_update
|
||||
AFTER UPDATE ON storyboard_video_tasks
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.status <> OLD.status THEN
|
||||
UPDATE task_status SET status = NEW.status, updated_at = NOW() WHERE task_id = NEW.task_id;
|
||||
END IF;
|
||||
END//
|
||||
|
||||
DELIMITER ;
|
||||
*/
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 修复外键约束,添加级联删除
|
||||
|
||||
-- 1. 修复 payments 表的外键约束
|
||||
ALTER TABLE payments DROP FOREIGN KEY payments_ibfk_1;
|
||||
ALTER TABLE payments ADD CONSTRAINT payments_user_fk
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
-- 2. 修复 orders 表的外键约束
|
||||
ALTER TABLE orders DROP FOREIGN KEY orders_ibfk_1;
|
||||
ALTER TABLE orders ADD CONSTRAINT orders_user_fk
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
@@ -1,15 +1,15 @@
|
||||
# 支付配置
|
||||
# 支付宝配置 - 请替换为您的实际配置
|
||||
alipay.app-id=您的APPID
|
||||
alipay.private-key=您的应用私钥
|
||||
alipay.public-key=支付宝公钥
|
||||
# 支付宝沙箱配置
|
||||
alipay.app-id=9021000157616562
|
||||
alipay.private-key=MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCH7wPeptkJlJuoKwDqxvfJJLTOAWVkHa/TLh+wiy1tEtmwcrOwEU3GuqfkUlhij71WJIZi8KBytCwbax1QGZA/oLXvTCGJJrYrsEL624X5gGCCPKWwHRDhewsQ5W8jFxaaMXxth8GKlSW61PZD2cOQClRVEm2xnWFZ+6/7WBI7082g7ayzGCD2eowXsJyWyuEBCUSbHXkSgxVhqj5wUGIXhr8ly+pdUlJmDX5K8UG2rjJYx+0AU5UZJbOAND7d3iyDsOulHDvth50t8MOWDnDCVJ2aAgUB5FZKtOFxOmzNTsMjvzYldFztF0khbypeeMVL2cxgioIgTvjBkUwd55hZAgMBAAECggEAUjk3pARUoEDt7skkYt87nsW/QCUECY0Tf7AUpxtovON8Hgkju8qbuyvIxokwwV2k72hkiZB33Soyy9r8/iiYYoR5yGfKmUV7R+30df03ivYmamD48BCE138v8GZ31Ufv+hEY7MADSCpzihGrbNtaOdSlslfVVmyWKHHfvy9EyD6yHJGYswLpHXC/QX1TuLRRxk6Uup8qENOG/6zjGWMfxoRZFwTt80ml1mKy32YZGyJqDaQpJcdYwAHOPcnJl1emw4E+oVjiLyksl643npuTkgnZXs1iWcWSS8ojF1w/0kVDzcNh9toLg+HDuQlIHOis01VQ7lYcG4oiMOnhX1QHIQKBgQC9fgBuILjBhuCI9fHvLRdzoNC9heD54YK7xGvEV/mv90k8xcmNx+Yg5C57ASaMRtOq3b7muPiCv5wOtMT4tUCcMIwSrTNlcBM6EoTagnaGfpzOMaHGMXO4vbaw+MIynHnvXFj1rZjG1lzkV/9K36LAaHD9ZKVJaBQ9mK+0CIq/3QKBgQC3pL5GbvXj6/4ahTraXzNDQQpPGVgbHxcOioEXL4ibaOPC58puTW8HDbRvVuhl/4EEOBRVX81BSgkN8XHwTSiZdih2iOqByg+o9kixs7nlFn3Iw9BBP2/g+Wqiyi2N+9g17kfWXXVOKYz/eMXLBeOo4KhQE9wqNGyZldYzX2ywrQKBgApJmvBfqmgnUG1fHOFlS06lvm9ro0ktqxFSmp8wP4gEHt/DxSuDXMUQXk2jRFp9ReSS4VhZVnSSvoA15DO0c2uHXzNsX8v0B7cxZjEOwCyRFyZCn4vJB4VSF2cIOlLRF/Wcx9+eqxqwbJ6hAGUqOwXDJc879ZVEp0So03EsvYupAoGAAnI+Wp/VxLB7FQ1bSFdmTmoKYh1bUBks7HOp3o4yiqduCUWfK7L6XKSxF56Xv+wUYuMAWlbJXCpJTpc9xk6w0MKDLXkLbqkrZjvJohxbyJJxIICDQKtAqUWJRxvcWXzWV3mSGWfrTRw+lZSdReQRMUm01EQ/dYx3OeCGFu8Zeo0CgYAlH5YSYdJxZSoDCJeoTrkxUlFoOg8UQ7SrsaLYLwpwcwpuiWJaTrg6jwFocj+XhjQ9RtRbSBHz2wKSLdl+pXbTbqECKk85zMFl6zG3etXtTJU/dD750Ty4i8zt3+JGhvglPrQBY1CfItgml2oXa/VUVMnLCUS0WSZuPRmPYZD8dg==
|
||||
alipay.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAksEwzuR3ASrKtTzaANqdQYKOoD44itA1TWG/6onvQr8PHNEMgcguLuJNrdeuT2PDg23byzZ9qKfEM2D5U4zbpt0/uCYLfZQyAnAWWyMvnKPoSIgrtBjnxYK6HE6fuQV3geJTcZxvP/z8dGZB0V0s6a53rzbKSLh0p4w0hWfVXlQihq3Xh4vSKB+ojdhEkIblhpWPT42NPbjVNdwPzIhUGpRy3/nsgNqVBu+ZacQ5/rCvzXU1RE0allBbjcvjymKQTS7bAE0i1Mgo1eX8njvElsfQUv5P7xQdrvZagqtIuTdP19cmsSNGdIC9Z5Po3j0z3KWPR7MrKgDuJfzkWtJR4wIDAQAB
|
||||
alipay.server-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do
|
||||
alipay.gateway-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do
|
||||
alipay.domain=https://curtly-aphorismatic-ginger.ngrok-free.dev
|
||||
alipay.domain=https://vionow.com
|
||||
alipay.charset=UTF-8
|
||||
alipay.sign-type=RSA2
|
||||
alipay.notify-url=https://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/notify
|
||||
alipay.return-url=https://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/return
|
||||
alipay.notify-url=https://vionow.com/api/payments/alipay/notify
|
||||
alipay.return-url=https://vionow.com/api/payments/alipay/return
|
||||
alipay.app-cert-path=classpath:cert/alipay/appCertPublicKey.crt
|
||||
alipay.ali-pay-cert-path=classpath:cert/alipay/alipayCertPublicKey_RSA2.crt
|
||||
alipay.ali-pay-root-cert-path=classpath:cert/alipay/alipayRootCert.crt
|
||||
|
||||
@@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS payments (
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
paid_at TIMESTAMP NULL,
|
||||
user_id BIGINT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
@@ -55,7 +55,7 @@ CREATE TABLE IF NOT EXISTS orders (
|
||||
delivered_at TIMESTAMP NULL,
|
||||
cancelled_at TIMESTAMP NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS order_items (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:fragment="layout(title, content)">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title th:text="${pageTitle} + ' - ' + ${siteName}">AIGC Demo</title>
|
||||
<title th:replace="${title}">AIGC Demo</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
@@ -168,22 +168,22 @@
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/}" th:classappend="${#httpServletRequest.requestURI == '/'} ? 'active' : ''">
|
||||
<a class="nav-link" th:href="@{/}" th:classappend="${#httpServletRequest != null && #httpServletRequest.requestURI == '/'} ? 'active' : ''">
|
||||
<i class="fas fa-home me-1"></i>首页
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="hasRole('ADMIN')">
|
||||
<a class="nav-link" th:href="@{/settings}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/settings')} ? 'active' : ''">
|
||||
<a class="nav-link" th:href="@{/settings}" th:classappend="${#httpServletRequest != null && #strings.startsWith(#httpServletRequest.requestURI, '/settings')} ? 'active' : ''">
|
||||
<i class="fas fa-gear me-1"></i>系统设置
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="hasRole('ADMIN')">
|
||||
<a class="nav-link" th:href="@{/users}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/users')} ? 'active' : ''">
|
||||
<a class="nav-link" th:href="@{/users}" th:classappend="${#httpServletRequest != null && #strings.startsWith(#httpServletRequest.requestURI, '/users')} ? 'active' : ''">
|
||||
<i class="fas fa-users me-1"></i>用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="isAuthenticated()">
|
||||
<a class="nav-link" th:href="@{/payment/create}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/payment')} ? 'active' : ''">
|
||||
<a class="nav-link" th:href="@{/payment/create}" th:classappend="${#httpServletRequest != null && #strings.startsWith(#httpServletRequest.requestURI, '/payment')} ? 'active' : ''">
|
||||
<i class="fas fa-credit-card me-1"></i>支付管理
|
||||
</a>
|
||||
</li>
|
||||
@@ -265,7 +265,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div th:fragment="content">
|
||||
<div th:replace="${content}">
|
||||
<!-- Page specific content will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user