refactor(project): Reorganize frontend and backend directories

This commit is contained in:
2026-03-18 10:45:15 +08:00
parent cc9eaf4cdf
commit 92a139e470
513 changed files with 28251 additions and 287 deletions
@@ -0,0 +1,40 @@
package com.nuaa.video.common.config;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Enumeration;
import jakarta.servlet.http.HttpServletRequest;
/**
* Feign配置类
*/
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
// 传递认证信息
if ("Authorization".equalsIgnoreCase(name) ||
"Content-Type".equalsIgnoreCase(name)) {
requestTemplate.header(name, value);
}
}
}
}
};
}
}
@@ -0,0 +1,44 @@
package com.nuaa.video.common.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MinIO配置类
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
/**
* 服务地址
*/
private String endpoint;
/**
* 访问密钥
*/
private String accessKey;
/**
* 密钥
*/
private String secretKey;
/**
* 存储桶名称
*/
private String bucketName;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
@@ -0,0 +1,43 @@
package com.nuaa.video.common.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
/**
* MyBatis Plus配置类
* 只有在存在DataSource且启用mybatis-plus时才生效
*/
@Configuration
@EnableTransactionManagement
@ConditionalOnClass({DataSource.class})
@ConditionalOnProperty(prefix = "spring.datasource", name = "url")
@MapperScan({"com.nuaa.video.**.mapper"})
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 防止全表更新与删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
@@ -0,0 +1,45 @@
package com.nuaa.video.common.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.nuaa.video.common.constants.CommonConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatis Plus元数据处理器
*/
@Slf4j
@Component
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.debug("自动填充插入字段");
// 创建时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
// 更新时间
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 创建人
this.strictInsertFill(metaObject, "createdBy", String.class, CommonConstants.SYSTEM_USER);
// 更新人
this.strictInsertFill(metaObject, "updatedBy", String.class, CommonConstants.SYSTEM_USER);
}
@Override
public void updateFill(MetaObject metaObject) {
log.debug("自动填充更新字段");
// 更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 更新人
this.strictUpdateFill(metaObject, "updatedBy", String.class, CommonConstants.SYSTEM_USER);
}
}
@@ -0,0 +1,59 @@
// com.nuaa.video.common.config.RedisConfig.java
package com.nuaa.video.common.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
/**
* Redis配置类
* 只有在存在RedisOperations类且没有redisTemplate bean时才会创建
*/
@Configuration
@EnableCaching
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnBean(RedisConnectionFactory.class) // 只有当存在RedisConnectionFactory时才创建
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 创建配置好的ObjectMapper
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
@@ -0,0 +1,25 @@
package com.nuaa.video.common.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 配置跨域
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
@@ -0,0 +1,52 @@
package com.nuaa.video.common.constants;
/**
* 通用常量
*/
public interface CommonConstants {
/**
* 成功标记
*/
Integer SUCCESS = 200;
/**
* 失败标记
*/
Integer FAIL = 500;
/**
* 已删除
*/
Integer DELETED = 1;
/**
* 未删除
*/
Integer NOT_DELETED = 0;
/**
* 正常状态
*/
Integer STATUS_NORMAL = 1;
/**
* 禁用状态
*/
Integer STATUS_DISABLED = 0;
/**
* 锁定状态
*/
Integer STATUS_LOCKED = 2;
/**
* 系统默认用户
*/
String SYSTEM_USER = "system";
/**
* 匿名用户
*/
String ANONYMOUS_USER = "anonymous";
}
@@ -0,0 +1,57 @@
package com.nuaa.video.common.constants;
/**
* 安全相关常量
*/
public interface SecurityConstants {
/**
* 认证请求头
*/
String AUTHORIZATION_HEADER = "Authorization";
/**
* 授权token前缀
*/
String BEARER_PREFIX = "Bearer ";
/**
* Basic认证前缀
*/
String BASIC_PREFIX = "Basic ";
/**
* JWT存储权限前缀
*/
String AUTHORITY_PREFIX = "ROLE_";
/**
* JWT存储权限属性
*/
String JWT_AUTHORITIES_KEY = "authorities";
/**
* 刷新token
*/
String REFRESH_TOKEN = "refresh_token";
/**
* 默认登录URL
*/
String LOGIN_URL = "/auth/login";
/**
* 验证码缓存前缀
*/
String CAPTCHA_PREFIX = "captcha:";
/**
* 验证码有效期(秒)
*/
long CAPTCHA_EXPIRATION = 60;
/**
* Token过期时间(秒)
*/
long TOKEN_EXPIRATION = 86400;
}
@@ -0,0 +1,42 @@
package com.nuaa.video.common.exception;
import com.nuaa.video.common.model.enums.ResultCode;
import lombok.Getter;
/**
* 业务异常类
*/
@Getter
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private final Integer code;
/**
* 错误消息
*/
private final String message;
public BusinessException(String message) {
this.code = ResultCode.ERROR.getCode();
this.message = message;
}
public BusinessException(Integer code, String message) {
this.code = code;
this.message = message;
}
public BusinessException(ResultCode resultCode) {
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
public BusinessException(ResultCode resultCode, String message) {
this.code = resultCode.getCode();
this.message = message;
}
}
@@ -0,0 +1,79 @@
package com.nuaa.video.common.exception;
import com.nuaa.video.common.model.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常 (JSR303)
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
String message = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
log.warn("参数校验异常:{}", message);
return Result.error(400, message);
}
/**
* 处理方法参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String message = fieldErrors.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.warn("方法参数校验异常:{}", message);
return Result.error(400, message);
}
/**
* 处理绑定异常
*/
@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e) {
String message = e.getBindingResult().getAllErrors().stream()
.map(error -> error.getDefaultMessage())
.collect(Collectors.joining(", "));
log.warn("参数绑定异常:{}", message);
return Result.error(400, message);
}
/**
* 处理未知异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常:", e);
return Result.error("系统繁忙,请稍后再试");
}
}
@@ -0,0 +1,43 @@
package com.nuaa.video.common.model.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 分页请求DTO
*/
@Data
@Accessors(chain = true)
public class PageRequestDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 当前页码
*/
@NotNull(message = "页码不能为空")
@Min(value = 1, message = "页码最小值为1")
private Integer current = 1;
/**
* 每页记录数
*/
@NotNull(message = "每页记录数不能为空")
@Min(value = 1, message = "每页记录数最小值为1")
@Max(value = 100, message = "每页记录数最大值为100")
private Integer size = 10;
/**
* 排序字段
*/
private String orderField;
/**
* 排序方式(asc/desc)
*/
private String orderType;
}
@@ -0,0 +1,48 @@
package com.nuaa.video.common.model.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 基础实体类
*/
@Data
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private String createdBy;
/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updatedBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
@@ -0,0 +1,82 @@
package com.nuaa.video.common.model.enums;
import lombok.Getter;
/**
* 响应状态码枚举
*/
@Getter
public enum ResultCode {
/**
* 成功
*/
SUCCESS(200, "操作成功"),
/**
* 参数错误
*/
PARAM_ERROR(400, "参数错误"),
/**
* 未授权
*/
UNAUTHORIZED(401, "未授权"),
/**
* 禁止访问
*/
FORBIDDEN(403, "禁止访问"),
/**
* 资源不存在
*/
NOT_FOUND(404, "资源不存在"),
/**
* 服务器内部错误
*/
ERROR(500, "服务器内部错误"),
/**
* 服务不可用
*/
SERVICE_UNAVAILABLE(503, "服务不可用"),
/**
* 用户不存在
*/
USER_NOT_FOUND(1001, "用户不存在"),
/**
* 用户已存在
*/
USER_ALREADY_EXISTS(1002, "用户已存在"),
/**
* 密码错误
*/
PASSWORD_ERROR(1003, "密码错误"),
/**
* 视频不存在
*/
VIDEO_NOT_FOUND(2001, "视频不存在"),
/**
* 视频上传失败
*/
VIDEO_UPLOAD_FAILED(2002, "视频上传失败"),
/**
* 评论不存在
*/
COMMENT_NOT_FOUND(3001, "评论不存在");
private final Integer code;
private final String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}
@@ -0,0 +1,82 @@
package com.nuaa.video.common.model.vo;
import com.nuaa.video.common.model.enums.ResultCode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.io.Serializable;
/**
* 统一响应结果类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
private Integer code;
/**
* 返回消息
*/
private String message;
/**
* 返回数据
*/
private T data;
/**
* 成功结果
*/
public static <T> Result<T> success() {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
}
/**
* 成功结果(带数据)
*/
public static <T> Result<T> success(T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功结果(自定义消息和数据)
*/
public static <T> Result<T> success(String message, T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败结果
*/
public static <T> Result<T> error() {
return new Result<>(ResultCode.ERROR.getCode(), ResultCode.ERROR.getMessage(), null);
}
/**
* 失败结果(自定义消息)
*/
public static <T> Result<T> error(String message) {
return new Result<>(ResultCode.ERROR.getCode(), message, null);
}
/**
* 失败结果(自定义状态码和消息)
*/
public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message, null);
}
/**
* 自定义结果
*/
public static <T> Result<T> result(ResultCode resultCode, T data) {
return new Result<>(resultCode.getCode(), resultCode.getMessage(), data);
}
}
@@ -0,0 +1,115 @@
package com.nuaa.video.common.utils;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
/**
* 日期工具类
*/
public class DateUtils {
/**
* 日期格式化
*/
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String SIMPLE_DATE_FORMAT = "yyyyMMdd";
public static final String SIMPLE_DATE_TIME_FORMAT = "yyyyMMddHHmmss";
/**
* 日期对象格式化
*/
public static String format(LocalDateTime localDateTime, String pattern) {
if (localDateTime == null) {
return null;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return formatter.format(localDateTime);
}
/**
* 日期对象格式化(默认格式)
*/
public static String format(LocalDateTime localDateTime) {
return format(localDateTime, DEFAULT_DATE_TIME_FORMAT);
}
/**
* 字符串转日期
*/
public static LocalDateTime parse(String dateTimeStr, String pattern) {
if (StringUtils.isEmpty(dateTimeStr)) {
return null;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return LocalDateTime.parse(dateTimeStr, formatter);
}
/**
* 字符串转日期(默认格式)
*/
public static LocalDateTime parse(String dateTimeStr) {
return parse(dateTimeStr, DEFAULT_DATE_TIME_FORMAT);
}
/**
* LocalDateTime转Date
*/
public static Date toDate(LocalDateTime localDateTime) {
if (localDateTime == null) {
return null;
}
ZoneId zoneId = ZoneId.systemDefault();
return Date.from(localDateTime.atZone(zoneId).toInstant());
}
/**
* Date转LocalDateTime
*/
public static LocalDateTime toLocalDateTime(Date date) {
if (date == null) {
return null;
}
ZoneId zoneId = ZoneId.systemDefault();
return date.toInstant().atZone(zoneId).toLocalDateTime();
}
/**
* 获取当前日期时间
*/
public static LocalDateTime now() {
return LocalDateTime.now();
}
/**
* 计算两个日期之间的天数
*/
public static long daysBetween(LocalDateTime start, LocalDateTime end) {
if (start == null || end == null) {
return 0;
}
return ChronoUnit.DAYS.between(start, end);
}
/**
* 计算两个日期之间的小时数
*/
public static long hoursBetween(LocalDateTime start, LocalDateTime end) {
if (start == null || end == null) {
return 0;
}
return ChronoUnit.HOURS.between(start, end);
}
/**
* 计算两个日期之间的分钟数
*/
public static long minutesBetween(LocalDateTime start, LocalDateTime end) {
if (start == null || end == null) {
return 0;
}
return ChronoUnit.MINUTES.between(start, end);
}
}
@@ -0,0 +1,123 @@
package com.nuaa.video.common.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* JWT工具类
*/
@Slf4j
@Component
public class JwtUtils {
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
private String secret;
@Value("${jwt.expiration:86400}")
private long expiration;
@Value("${jwt.token-header:Authorization}")
private String tokenHeader;
/**
* 根据用户ID生成JWT
*/
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return createToken(claims, userId.toString());
}
/**
* 从JWT中获取用户ID
*/
public Long getUserIdFromToken(String token) {
return Long.valueOf(getClaimFromToken(token, Claims::getSubject));
}
/**
* 判断JWT是否过期
*/
public boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 验证JWT
*/
public boolean validateToken(String token, String username) {
try {
final String tokenUsername = getUsernameFromToken(token);
return (tokenUsername.equals(username) && !isTokenExpired(token));
} catch (Exception e) {
log.error("JWT验证失败:", e);
return false;
}
}
/**
* 从JWT中获取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("username", String.class));
}
/**
* 获取JWT过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* 从JWT中获取指定声明
*/
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* 从JWT中获取所有声明
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 创建JWT
*/
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey())
.compact();
}
/**
* 获取签名密钥 - 适配JJWT 0.12.3
*/
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes();
return Keys.hmacShaKeyFor(keyBytes);
}
}
@@ -0,0 +1,247 @@
// com.nuaa.video.common.utils.MinioUtils.java
package com.nuaa.video.common.utils;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* MinIO工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MinioUtils {
private final MinioClient minioClient;
@Value("${minio.bucketName:video-platform}")
private String bucketName;
/**
* 检查存储桶是否存在
*/
public boolean bucketExists(String bucketName) {
try {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
log.error("检查存储桶是否存在失败:", e);
return false;
}
}
/**
* 创建存储桶
*/
public void createBucket(String bucketName) {
try {
boolean exists = bucketExists(bucketName);
if (!exists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
} catch (Exception e) {
log.error("创建存储桶失败:", e);
throw new RuntimeException("创建存储桶失败", e);
}
}
/**
* 列出所有存储桶
*/
public List<Bucket> listBuckets() {
try {
return minioClient.listBuckets();
} catch (Exception e) {
log.error("列出所有存储桶失败:", e);
throw new RuntimeException("列出所有存储桶失败", e);
}
}
/**
* 列出存储桶中的所有对象
*/
public Iterable<Result<Item>> listObjects(String bucketName) {
return minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).recursive(true).build());
}
/**
* 上传文件
*/
public String uploadFile(MultipartFile file, String fileType) {
try {
// 检查存储桶是否存在
createBucket(bucketName);
// 生成文件名
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
String objectName = fileType + "/" + fileName;
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
return objectName;
} catch (Exception e) {
log.error("上传文件失败:", e);
throw new RuntimeException("上传文件失败", e);
}
}
/**
* 上传视频文件
*/
public String uploadVideo(MultipartFile file) {
return uploadFile(file, "videos");
}
/**
* 上传图片文件
*/
public String uploadImage(MultipartFile file) {
return uploadFile(file, "images");
}
/**
* 上传字节数组
*/
public String uploadBytes(byte[] bytes, String fileName, String contentType, String fileType) {
try {
// 检查存储桶是否存在
createBucket(bucketName);
// 生成文件名
String objectName = fileType + "/" + System.currentTimeMillis() + "_" + fileName;
// 上传字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(bais, bytes.length, -1)
.contentType(contentType)
.build()
);
return objectName;
} catch (Exception e) {
log.error("上传字节数组失败:", e);
throw new RuntimeException("上传字节数组失败", e);
}
}
/**
* 获取文件
*/
public InputStream getObject(String objectName) {
try {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
} catch (Exception e) {
log.error("获取文件失败:", e);
throw new RuntimeException("获取文件失败", e);
}
}
/**
* 删除文件
*/
public void removeObject(String objectName) {
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
} catch (Exception e) {
log.error("删除文件失败:", e);
throw new RuntimeException("删除文件失败", e);
}
}
/**
* 获取文件外链
*/
public String getPreSignedUrl(String objectName, int expiry) {
try {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(objectName)
.method(Method.GET)
.expiry(expiry, TimeUnit.SECONDS)
.build()
);
} catch (Exception e) {
log.error("获取文件外链失败:", e);
throw new RuntimeException("获取文件外链失败", e);
}
}
/**
* 获取文件外链(使用默认过期时间7天)
*/
public String getPreSignedUrl(String objectName) {
return getPreSignedUrl(objectName, 7 * 24 * 60 * 60);
}
/**
* 获取文件基础URL
*/
public String getBaseUrl(String objectName) {
try {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(objectName)
.method(Method.GET)
.build()
).split("\\?")[0];
} catch (Exception e) {
log.error("获取文件基础URL失败:", e);
throw new RuntimeException("获取文件基础URL失败", e);
}
}
public String uploadObject(String fileType, String fileName, InputStream inputStream, long size, String contentType) throws Exception {
// 检查存储桶是否存在
createBucket(bucketName);
// 生成对象名称
String objectName = fileType + "/" + fileName;
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, size, -1)
.contentType(contentType)
.build()
);
return objectName;
}
}
@@ -0,0 +1,63 @@
package com.nuaa.video.common.utils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页工具类
*/
@Data
public class PageUtils<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总记录数
*/
private long total;
/**
* 每页记录数
*/
private long size;
/**
* 当前页数
*/
private long current;
/**
* 总页数
*/
private long pages;
/**
* 列表数据
*/
private List<T> records;
/**
* 是否有前一页
*/
private boolean hasPrevious;
/**
* 是否有下一页
*/
private boolean hasNext;
/**
* 构造方法
*/
public PageUtils(IPage<T> page) {
this.total = page.getTotal();
this.size = page.getSize();
this.current = page.getCurrent();
this.pages = page.getPages();
this.records = page.getRecords();
this.hasPrevious = current > 1;
this.hasNext = current < pages;
}
}
@@ -0,0 +1,277 @@
package com.nuaa.video.common.utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnBean(RedisTemplate.class)
public class RedisUtils {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 设置缓存过期时间
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
log.error("Redis设置过期时间失败:", e);
return false;
}
}
/**
* 获取缓存过期时间
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*/
public boolean hasKey(String key) {
try {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
} catch (Exception e) {
log.error("Redis判断key是否存在失败:", e);
return false;
}
}
/**
* 删除缓存
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) List.of(key));
}
}
}
/**
* 获取普通缓存
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 设置普通缓存
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error("Redis设置缓存失败:", e);
return false;
}
}
/**
* 设置普通缓存并设置过期时间
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error("Redis设置缓存并设置过期时间失败:", e);
return false;
}
}
/**
* 递增
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().decrement(key, delta);
}
/**
* 获取Hash结构数据
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取Hash结构中的所有键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 设置Hash结构数据
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
log.error("Redis设置Hash结构数据失败:", e);
return false;
}
}
/**
* 设置Hash结构数据并设置过期时间
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("Redis设置Hash结构数据并设置过期时间失败:", e);
return false;
}
}
/**
* 向Hash结构中放入一个数据项
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
log.error("Redis向Hash结构中放入一个数据项失败:", e);
return false;
}
}
/**
* 向Hash结构中放入一个数据项并设置过期时间
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("Redis向Hash结构中放入一个数据项并设置过期时间失败:", e);
return false;
}
}
/**
* 删除Hash结构中的值
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断Hash结构中是否有该项的值
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* Hash结构中递增
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* Hash结构中递减
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
/**
* 获取Set结构数据
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error("Redis获取Set结构数据失败:", e);
return null;
}
}
/**
* 向Set结构中添加值
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
log.error("Redis向Set结构中添加值失败:", e);
return 0;
}
}
/**
* 获取List结构数据
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
log.error("Redis获取List结构数据失败:", e);
return null;
}
}
/**
* 向List结构中添加值
*/
public long lSet(String key, Object value) {
try {
return redisTemplate.opsForList().rightPush(key, value);
} catch (Exception e) {
log.error("Redis向List结构中添加值失败:", e);
return 0;
}
}
}
@@ -0,0 +1,87 @@
package com.nuaa.video.common.utils;
import java.util.UUID;
/**
* 字符串工具类
*/
public class StringUtils {
/**
* 判断字符串是否为空
*/
public static boolean isEmpty(String str) {
return str == null || str.trim().length() == 0;
}
/**
* 判断字符串是否不为空
*/
public static boolean isNotEmpty(String str) {
return !isEmpty(str);
}
/**
* 生成UUID
*/
public static String generateUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
/**
* 生成固定长度的随机字符串
*/
public static String randomString(int length) {
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = (int) (Math.random() * str.length());
sb.append(str.charAt(number));
}
return sb.toString();
}
/**
* 驼峰转下划线
*/
public static String camelToUnderscore(String str) {
if (isEmpty(str)) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isUpperCase(c)) {
sb.append("_").append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* 下划线转驼峰
*/
public static String underscoreToCamel(String str) {
if (isEmpty(str)) {
return "";
}
StringBuilder sb = new StringBuilder();
boolean underscoreFlag = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == '_') {
underscoreFlag = true;
} else {
if (underscoreFlag) {
sb.append(Character.toUpperCase(c));
underscoreFlag = false;
} else {
sb.append(c);
}
}
}
return sb.toString();
}
}
@@ -0,0 +1,8 @@
# src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.nuaa.video.common.config.MybatisPlusConfig,\
com.nuaa.video.common.config.MinioConfig,\
com.nuaa.video.common.config.WebConfig,\
com.nuaa.video.common.config.FeignConfig,\
com.nuaa.video.common.config.MybatisPlusMetaObjectHandler,\
com.nuaa.video.common.exception.GlobalExceptionHandler
@@ -0,0 +1,32 @@
package com.nuaa.video.common.test;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
/**
* Controller层测试基类
* 提供MockMvc和其他测试通用配置
*/
@ExtendWith(MockitoExtension.class)
@ActiveProfiles("test")
public abstract class BaseControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
/**
* 将对象转换为JSON字符串
*/
protected String toJson(Object obj) throws Exception {
return objectMapper.writeValueAsString(obj);
}
}
@@ -0,0 +1,41 @@
package com.nuaa.video.common.test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
/**
* Service层测试基类
* 提供通用测试工具方法
*/
@ExtendWith(MockitoExtension.class)
@ActiveProfiles("test")
public abstract class BaseServiceTest {
/**
* 断言结果成功
*/
protected <T> void assertSuccess(com.nuaa.video.common.model.vo.Result<T> result) {
assertNotNull(result);
assertEquals(200, result.getCode());
}
/**
* 断言结果失败
*/
protected <T> void assertError(com.nuaa.video.common.model.vo.Result<T> result) {
assertNotNull(result);
assertNotEquals(200, result.getCode());
}
/**
* 断言结果失败并包含特定消息
*/
protected <T> void assertErrorWithMessage(com.nuaa.video.common.model.vo.Result<T> result, String message) {
assertNotNull(result);
assertNotEquals(200, result.getCode());
assertTrue(result.getMessage().contains(message));
}
}