diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..4ff5223 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,113 @@ + + + + + mysql.8 + true + true + $PROJECT_DIR$/video-comment/src/main/resources/application.yml + com.mysql.cj.jdbc.Driver + jdbc:mysql://5.180.253.115:3306/video_comment?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai + $ProjectFileDir$ + + + redis + true + true + $PROJECT_DIR$/video-content/src/main/resources/application.yml + jdbc.RedisDriver + jdbc:redis://5.180.253.115:6379/2 + $ProjectFileDir$ + + + mysql.8 + true + true + $PROJECT_DIR$/video-admin/src/main/resources/application.yml + com.mysql.cj.jdbc.Driver + jdbc:mysql://5.180.253.115:3306/video_admin?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai + $ProjectFileDir$ + + + mysql.8 + true + true + $PROJECT_DIR$/video-auth/src/main/resources/application.yml + com.mysql.cj.jdbc.Driver + jdbc:mysql://5.180.253.115:3306/video_auth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai + $ProjectFileDir$ + + + redis + true + true + $PROJECT_DIR$/video-user/src/main/resources/application.yml + jdbc.RedisDriver + jdbc:redis://5.180.253.115:6379/1 + $ProjectFileDir$ + + + mysql.8 + true + true + $PROJECT_DIR$/video-recommend/src/main/resources/application.yml + com.mysql.cj.jdbc.Driver + jdbc:mysql://5.180.253.115:3306/video_recommend?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai + $ProjectFileDir$ + + + redis + true + true + $PROJECT_DIR$/video-recommend/src/main/resources/application.yml + jdbc.RedisDriver + jdbc:redis://5.180.253.115:6379/4 + $ProjectFileDir$ + + + redis + true + true + $PROJECT_DIR$/video-comment/src/main/resources/application.yml + jdbc.RedisDriver + jdbc:redis://5.180.253.115:6379/3 + $ProjectFileDir$ + + + redis + true + true + $PROJECT_DIR$/video-auth/src/main/resources/application.yml + jdbc.RedisDriver + jdbc:redis://5.180.253.115:6379/0 + $ProjectFileDir$ + + + mysql.8 + true + true + $PROJECT_DIR$/video-content/src/main/resources/application.yml + com.mysql.cj.jdbc.Driver + jdbc:mysql://5.180.253.115:3306/video_content?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai + $ProjectFileDir$ + + + mysql.8 + true + true + $PROJECT_DIR$/video-user/src/main/resources/application.yml + com.mysql.cj.jdbc.Driver + jdbc:mysql://5.180.253.115:3306/video_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai + $ProjectFileDir$ + + + redis + true + true + $PROJECT_DIR$/video-admin/src/main/resources/application.yml + jdbc.RedisDriver + jdbc:redis://5.180.253.115:6379/5 + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..f6d2542 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/docs/video-common模块实现步骤和代码.md b/docs/video-common模块实现步骤和代码.md new file mode 100644 index 0000000..3170741 --- /dev/null +++ b/docs/video-common模块实现步骤和代码.md @@ -0,0 +1,1788 @@ +# 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.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配置类 + */ +@Configuration +@EnableCaching +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties(RedisProperties.class) +public class RedisConfig { + + @Bean + @ConditionalOnMissingBean(name = "redisTemplate") + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + // 创建配置好的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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * MyBatis Plus配置类 + */ +@Configuration +@EnableTransactionManagement +@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 +``` diff --git a/video-common/pom.xml b/video-common/pom.xml index 0d167bc..601aaa8 100644 --- a/video-common/pom.xml +++ b/video-common/pom.xml @@ -87,18 +87,18 @@ io.jsonwebtoken jjwt-api - 0.12.3 + 0.12.6 io.jsonwebtoken jjwt-impl - 0.12.3 + 0.12.6 runtime io.jsonwebtoken jjwt-jackson - 0.12.3 + 0.12.6 runtime diff --git a/video-common/src/main/java/com/nuaa/video/common/App.java b/video-common/src/main/java/com/nuaa/video/common/App.java deleted file mode 100644 index e2d2187..0000000 --- a/video-common/src/main/java/com/nuaa/video/common/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.nuaa.video.common; - -/** - * Hello world! - * - */ -public class App -{ - public static void main( String[] args ) - { - System.out.println( "Hello World!" ); - } -} diff --git a/video-common/src/main/java/com/nuaa/video/common/config/FeignConfig.java b/video-common/src/main/java/com/nuaa/video/common/config/FeignConfig.java new file mode 100644 index 0000000..dc0186f --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/config/FeignConfig.java @@ -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 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); + } + } + } + } + }; + } +} diff --git a/video-common/src/main/java/com/nuaa/video/common/config/MinioConfig.java b/video-common/src/main/java/com/nuaa/video/common/config/MinioConfig.java new file mode 100644 index 0000000..7158dba --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/config/MinioConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusConfig.java b/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusConfig.java new file mode 100644 index 0000000..1a34016 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusConfig.java @@ -0,0 +1,36 @@ +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * MyBatis Plus配置类 + */ +@Configuration +@EnableTransactionManagement +@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; + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusMetaObjectHandler.java b/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusMetaObjectHandler.java new file mode 100644 index 0000000..b33e074 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusMetaObjectHandler.java @@ -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); + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/config/RedisConfig.java b/video-common/src/main/java/com/nuaa/video/common/config/RedisConfig.java new file mode 100644 index 0000000..027b47e --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/config/RedisConfig.java @@ -0,0 +1,55 @@ +package com.nuaa.video.common.config; + +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配置类 + */ +@Configuration +@EnableCaching +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties(RedisProperties.class) +public class RedisConfig { + + @Bean + @ConditionalOnMissingBean(name = "redisTemplate") + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + // 创建配置好的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; + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/config/WebConfig.java b/video-common/src/main/java/com/nuaa/video/common/config/WebConfig.java new file mode 100644 index 0000000..14daaae --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/config/WebConfig.java @@ -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); + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/constants/CommonConstants.java b/video-common/src/main/java/com/nuaa/video/common/constants/CommonConstants.java new file mode 100644 index 0000000..2323f9b --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/constants/CommonConstants.java @@ -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"; +} diff --git a/video-common/src/main/java/com/nuaa/video/common/constants/SecurityConstants.java b/video-common/src/main/java/com/nuaa/video/common/constants/SecurityConstants.java new file mode 100644 index 0000000..23b3942 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/constants/SecurityConstants.java @@ -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; +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/exception/BusinessException.java b/video-common/src/main/java/com/nuaa/video/common/exception/BusinessException.java new file mode 100644 index 0000000..527c092 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/exception/BusinessException.java @@ -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; + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/exception/GlobalExceptionHandler.java b/video-common/src/main/java/com/nuaa/video/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..71ee6a3 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/exception/GlobalExceptionHandler.java @@ -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 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("系统繁忙,请稍后再试"); + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/model/dto/PageRequestDTO.java b/video-common/src/main/java/com/nuaa/video/common/model/dto/PageRequestDTO.java new file mode 100644 index 0000000..4ef8a0a --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/model/dto/PageRequestDTO.java @@ -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; +} diff --git a/video-common/src/main/java/com/nuaa/video/common/model/entity/BaseEntity.java b/video-common/src/main/java/com/nuaa/video/common/model/entity/BaseEntity.java new file mode 100644 index 0000000..6da7326 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/model/entity/BaseEntity.java @@ -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; +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/model/enums/ResultCode.java b/video-common/src/main/java/com/nuaa/video/common/model/enums/ResultCode.java new file mode 100644 index 0000000..73dea8c --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/model/enums/ResultCode.java @@ -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; + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/model/vo/Result.java b/video-common/src/main/java/com/nuaa/video/common/model/vo/Result.java new file mode 100644 index 0000000..a491a31 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/model/vo/Result.java @@ -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 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); + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/utils/DateUtils.java b/video-common/src/main/java/com/nuaa/video/common/utils/DateUtils.java new file mode 100644 index 0000000..8874625 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/utils/DateUtils.java @@ -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); + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/utils/JwtUtils.java b/video-common/src/main/java/com/nuaa/video/common/utils/JwtUtils.java new file mode 100644 index 0000000..e5f3785 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/utils/JwtUtils.java @@ -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 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); + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/utils/MinioUtils.java b/video-common/src/main/java/com/nuaa/video/common/utils/MinioUtils.java new file mode 100644 index 0000000..dd0c11d --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/utils/MinioUtils.java @@ -0,0 +1,227 @@ +// 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); + } + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/utils/PageUtils.java b/video-common/src/main/java/com/nuaa/video/common/utils/PageUtils.java new file mode 100644 index 0000000..3bbc96c --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/utils/PageUtils.java @@ -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 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; + } +} diff --git a/video-common/src/main/java/com/nuaa/video/common/utils/RedisUtils.java b/video-common/src/main/java/com/nuaa/video/common/utils/RedisUtils.java new file mode 100644 index 0000000..4979046 --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/utils/RedisUtils.java @@ -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 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; + } + } +} \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/utils/StringUtils.java b/video-common/src/main/java/com/nuaa/video/common/utils/StringUtils.java new file mode 100644 index 0000000..7c1845a --- /dev/null +++ b/video-common/src/main/java/com/nuaa/video/common/utils/StringUtils.java @@ -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(); + } +} diff --git a/video-common/src/main/resources/META-INF/spring.factories b/video-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..4f0ad8c --- /dev/null +++ b/video-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,9 @@ +# 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 \ No newline at end of file