# video-common模块实现步骤和代码 基于Spring Boot 3.2.4、Spring Cloud 2023.0.1和Spring Cloud Alibaba 2023.0.1.0技术栈,完整实现video-common模块。 ## 1. 目录结构设计 首先,创建以下目录结构: ``` video-common/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── nuaa/ │ │ │ └── video/ │ │ │ └── common/ │ │ │ ├── annotation/ # 自定义注解 │ │ │ ├── config/ # 配置类 │ │ │ ├── constants/ # 常量定义 │ │ │ ├── exception/ # 异常处理 │ │ │ ├── model/ # 通用数据模型 │ │ │ │ ├── dto/ # 数据传输对象 │ │ │ │ ├── entity/ # 实体类 │ │ │ │ ├── enums/ # 枚举类 │ │ │ │ └── vo/ # 视图对象 │ │ │ ├── utils/ # 工具类 │ │ │ └── feign/ # Feign接口 │ │ └── resources/ │ │ └── META-INF/ │ │ └── spring.factories # 自动配置 │ └── test/ └── pom.xml ``` ## 2. 实现通用响应模型 创建统一的响应对象: ```java // com.nuaa.video.common.model.vo.Result.java 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 implements Serializable { private static final long serialVersionUID = 1L; /** * 状态码 */ private Integer code; /** * 返回消息 */ private String message; /** * 返回数据 */ private T data; /** * 成功结果 */ public static Result success() { return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null); } /** * 成功结果(带数据) */ public static Result success(T data) { return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } /** * 成功结果(自定义消息和数据) */ public static Result success(String message, T data) { return new Result<>(ResultCode.SUCCESS.getCode(), message, data); } /** * 失败结果 */ public static Result error() { return new Result<>(ResultCode.ERROR.getCode(), ResultCode.ERROR.getMessage(), null); } /** * 失败结果(自定义消息) */ public static Result error(String message) { return new Result<>(ResultCode.ERROR.getCode(), message, null); } /** * 失败结果(自定义状态码和消息) */ public static Result error(Integer code, String message) { return new Result<>(code, message, null); } /** * 自定义结果 */ public static Result result(ResultCode resultCode, T data) { return new Result<>(resultCode.getCode(), resultCode.getMessage(), data); } } ``` ## 3. 创建响应状态码枚举 ```java // com.nuaa.video.common.model.enums.ResultCode.java 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; } } ``` ## 4. 实现全局异常处理 ```java // com.nuaa.video.common.exception.BusinessException.java 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; } } ``` ```java // com.nuaa.video.common.exception.GlobalExceptionHandler.java 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 handleBusinessException(BusinessException e) { log.error("业务异常:{}", e.getMessage()); return Result.error(e.getCode(), e.getMessage()); } /** * 处理参数校验异常 (JSR303) */ @ExceptionHandler(ConstraintViolationException.class) public Result handleConstraintViolationException(ConstraintViolationException e) { Set> 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 handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { List 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 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 handleException(Exception e) { log.error("系统异常:", e); return Result.error("系统繁忙,请稍后再试"); } } ``` ## 5. 实现JWT工具类 ```java // com.nuaa.video.common.utils.JwtUtils.java 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 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 getClaimFromToken(String token, Function 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 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); } } ``` ## 6. 实现Redis工具类 ```java // com.nuaa.video.common.utils.RedisUtils.java package com.nuaa.video.common.utils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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 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) 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 hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * 设置Hash结构数据 */ public boolean hmset(String key, Map 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 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 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 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; } } } ``` ## 7. 实现MinIO工具类 ```java // 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 listBuckets() { try { return minioClient.listBuckets(); } catch (Exception e) { log.error("列出所有存储桶失败:", e); throw new RuntimeException("列出所有存储桶失败", e); } } /** * 列出存储桶中的所有对象 */ public Iterable> 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); } } } ``` ## 8. 创建Redis配置类 ```java // 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 redisTemplate(RedisConnectionFactory factory) { RedisTemplate 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 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; } } ``` ## 9. 创建MinIO配置类 ```java // com.nuaa.video.common.config.MinioConfig.java 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(); } } ``` ## 10. 创建MyBatis Plus配置类 ```java // com.nuaa.video.common.config.MybatisPlusConfig.java 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; } } ``` ## 11. 创建Web配置类 ```java // com.nuaa.video.common.config.WebConfig.java 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); } } ``` ## 12. 创建分页工具类 ```java // com.nuaa.video.common.utils.PageUtils.java 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 implements Serializable { private static final long serialVersionUID = 1L; /** * 总记录数 */ private long total; /** * 每页记录数 */ private long size; /** * 当前页数 */ private long current; /** * 总页数 */ private long pages; /** * 列表数据 */ private List records; /** * 是否有前一页 */ private boolean hasPrevious; /** * 是否有下一页 */ private boolean hasNext; /** * 构造方法 */ public PageUtils(IPage 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; } } ``` ## 13. 创建字符串工具类 ```java // com.nuaa.video.common.utils.StringUtils.java 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(); } } ``` ## 14. 创建基础实体类 ```java // com.nuaa.video.common.model.entity.BaseEntity.java 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; } ``` ## 15. 创建常量类 ```java // com.nuaa.video.common.constants.CommonConstants.java 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"; } ``` ```java // com.nuaa.video.common.constants.SecurityConstants.java 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; } ``` ## 16. 创建元数据处理器 ```java // com.nuaa.video.common.config.MybatisPlusMetaObjectHandler.java 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); } } ``` ## 17. 创建分页请求DTO ```java // com.nuaa.video.common.model.dto.PageRequestDTO.java 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; } ``` ## 18. 创建Feign配置类 ```java // com.nuaa.video.common.config.FeignConfig.java 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 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); } } } } }; } } ``` ## 19. 创建日期工具类 ```java // com.nuaa.video.common.utils.DateUtils.java 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); } } ``` ## 20. 创建spring.factories文件 用于自动配置加载Bean。 ``` # src/main/resources/META-INF/spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.nuaa.video.common.config.RedisConfig,\ 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 ```