提交 d4674ff8 作者: 方治民

feat: 优化接口排序配置、限流补充 Token 校验、添加分片上传实现

上级 174f735f
...@@ -10,6 +10,7 @@ import io.minio.ObjectWriteResponse; ...@@ -10,6 +10,7 @@ import io.minio.ObjectWriteResponse;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
...@@ -69,13 +70,23 @@ public class UploadProcessServiceImpl implements UploadProcessService { ...@@ -69,13 +70,23 @@ public class UploadProcessServiceImpl implements UploadProcessService {
// Video/Audio: 在文件名上追加时长,视频生成封面图 // Video/Audio: 在文件名上追加时长,视频生成封面图
if (isSupportiveMedia(suffix)) { if (isSupportiveMedia(suffix)) {
object = handleMedia(object, suffix, file); // 将上传的文件转存一份到本地临时目录
Path tempFile = Paths.get(FileUtil.getTmpDirPath(), "T_" + Commons.uuid(), file.getOriginalFilename());
FileUtil.mkParentDirs(tempFile);
file.transferTo(tempFile);
object = handleMedia(object, suffix, tempFile.toFile());
} }
return object; return object;
} }
@SneakyThrows @SneakyThrows
@Override
public String handleMedia(String object, File file) {
return handleMedia(object, FileUtil.getSuffix(file.getName()), file);
}
@SneakyThrows
public String handleImage(String object, MultipartFile file) { public String handleImage(String object, MultipartFile file) {
@Cleanup @Cleanup
InputStream is = file.getInputStream(); InputStream is = file.getInputStream();
...@@ -111,7 +122,7 @@ public class UploadProcessServiceImpl implements UploadProcessService { ...@@ -111,7 +122,7 @@ public class UploadProcessServiceImpl implements UploadProcessService {
} }
@SneakyThrows @SneakyThrows
public String handleMedia(String object, String suffix, MultipartFile file) { public String handleMedia(String object, String suffix, File file) {
// 判断是否配置 ffmpeg 环境 // 判断是否配置 ffmpeg 环境
FFmpeg ffmpeg = new FFmpeg(); FFmpeg ffmpeg = new FFmpeg();
FFprobe ffprobe = new FFprobe(); FFprobe ffprobe = new FFprobe();
...@@ -124,13 +135,11 @@ public class UploadProcessServiceImpl implements UploadProcessService { ...@@ -124,13 +135,11 @@ public class UploadProcessServiceImpl implements UploadProcessService {
return object; return object;
} }
// 将上传的文件转存一份到本地临时目录 // 获取文件路径
Path tempFile = Paths.get(FileUtil.getTmpDirPath(), "T_" + Commons.uuid(), file.getOriginalFilename()); Path path = file.toPath();
FileUtil.mkParentDirs(tempFile);
file.transferTo(tempFile);
// 解析媒体文件 // 解析媒体文件
FFmpegProbeResult probeResult = ffprobe.probe(tempFile.toString()); FFmpegProbeResult probeResult = ffprobe.probe(path.toString());
FFmpegFormat format = probeResult.getFormat(); FFmpegFormat format = probeResult.getFormat();
// 构建具有时长(秒)标记的存储地址 // 构建具有时长(秒)标记的存储地址
...@@ -141,16 +150,16 @@ public class UploadProcessServiceImpl implements UploadProcessService { ...@@ -141,16 +150,16 @@ public class UploadProcessServiceImpl implements UploadProcessService {
if (isSupportiveVideo(suffix)) { if (isSupportiveVideo(suffix)) {
// 大视频文件切片上传(> 5s) // 大视频文件切片上传(> 5s)
if (sec > 5) { if (sec > 5) {
filepath = handleVideoToHls(filepath, tempFile, ffmpeg, ffprobe); filepath = handleVideoToHls(filepath, path, ffmpeg, ffprobe);
} }
// 使用 ffmpeg 截取视频首帧图片 // 使用 ffmpeg 截取视频首帧图片
Path path = Paths.get(tempFile.getParent().toString(), FileUtil.getName(filepath) + ".jpg"); Path imagePath = Paths.get(path.getParent().toString(), FileUtil.getName(filepath) + ".jpg");
handleVideoScreenshot(tempFile.toString(), path.toString(), filepath, ffmpeg, ffprobe); handleVideoScreenshot(path.toString(), imagePath.toString(), filepath, ffmpeg, ffprobe);
} }
// 删除为本次上传进行本地处理所创建的整个临时文件夹目录 // 删除为本次上传进行本地处理所创建的整个临时文件夹目录
FileUtil.del(tempFile.getParent()); FileUtil.del(path.getParent());
return filepath; return filepath;
} }
......
...@@ -24,8 +24,8 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -24,8 +24,8 @@ import org.springframework.web.bind.annotation.RestController;
*/ */
@Slf4j @Slf4j
@Validated @Validated
@ApiSupport(order = 0) @ApiSupport(order = -99999)
@Tag(name = "健康指标", description = "Health") @Tag(name = "Health", description = "Health")
@RequestMapping("/") @RequestMapping("/")
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
......
...@@ -50,7 +50,7 @@ import org.springframework.web.multipart.MultipartFile; ...@@ -50,7 +50,7 @@ import org.springframework.web.multipart.MultipartFile;
@Slf4j @Slf4j
@Validated @Validated
@ApiSupport(order = 0) @ApiSupport(order = 0)
@Tag(name = "示例", description = "Example") @Tag(name = "【Examples】 示例", description = "Example")
@RequestMapping("/example/") @RequestMapping("/example/")
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
......
...@@ -5,6 +5,7 @@ import cn.dev33.satoken.annotation.SaCheckLogin; ...@@ -5,6 +5,7 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.secure.SaSecureUtil; import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository; import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.param.auth.LoginParam; import com.yiring.auth.param.auth.LoginParam;
...@@ -17,8 +18,6 @@ import com.yiring.common.core.Result; ...@@ -17,8 +18,6 @@ import com.yiring.common.core.Result;
import com.yiring.common.exception.BusinessException; import com.yiring.common.exception.BusinessException;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
...@@ -42,11 +41,8 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -42,11 +41,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@Tag( @ApiSupport(order = -9999)
name = "Auth", @Tag(name = "Auth", description = "身份认证")
description = "身份认证",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9999") }) }
)
@RestController @RestController
@RequestMapping("/auth/") @RequestMapping("/auth/")
@RequiredArgsConstructor @RequiredArgsConstructor
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
package com.yiring.auth.web.sys.permission; package com.yiring.auth.web.sys.permission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository; import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.param.permission.PermissionParam; import com.yiring.auth.param.permission.PermissionParam;
...@@ -16,8 +17,6 @@ import com.yiring.common.utils.RepositoryUtil; ...@@ -16,8 +17,6 @@ import com.yiring.common.utils.RepositoryUtil;
import com.yiring.common.validation.group.Group; import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -41,11 +40,8 @@ import org.springframework.web.bind.annotation.*; ...@@ -41,11 +40,8 @@ import org.springframework.web.bind.annotation.*;
@Slf4j @Slf4j
@Validated @Validated
@Tag( @ApiSupport(order = -99)
name = "权限管理", @Tag(name = "权限管理", description = "Permission")
description = "Permission",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-99") }) }
)
@RestController @RestController
@RequestMapping("/sys/permission/") @RequestMapping("/sys/permission/")
@RequiredArgsConstructor @RequiredArgsConstructor
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.sys.role; package com.yiring.auth.web.sys.role;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository; import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
...@@ -17,8 +18,6 @@ import com.yiring.common.utils.RepositoryUtil; ...@@ -17,8 +18,6 @@ import com.yiring.common.utils.RepositoryUtil;
import com.yiring.common.validation.group.Group; import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
...@@ -44,11 +43,8 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -44,11 +43,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@Tag( @ApiSupport(order = -98)
name = "角色管理", @Tag(name = "角色管理", description = "Role")
description = "Role",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-98") }) }
)
@RestController @RestController
@RequestMapping("/sys/role/") @RequestMapping("/sys/role/")
@RequiredArgsConstructor @RequiredArgsConstructor
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.sys.user; package com.yiring.auth.web.sys.user;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.role.RoleRepository; import com.yiring.auth.domain.role.RoleRepository;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
...@@ -15,11 +16,12 @@ import com.yiring.common.utils.RepositoryUtil; ...@@ -15,11 +16,12 @@ import com.yiring.common.utils.RepositoryUtil;
import com.yiring.common.utils.Specifications; import com.yiring.common.utils.Specifications;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
...@@ -41,11 +43,8 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -41,11 +43,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@Tag( @ApiSupport(order = -97)
name = "用户管理", @Tag(name = "用户管理", description = "User")
description = "User",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-97") }) }
)
@RestController @RestController
@RequestMapping("/sys/user/") @RequestMapping("/sys/user/")
@RequiredArgsConstructor @RequiredArgsConstructor
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.user; package com.yiring.auth.web.user;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository; import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
...@@ -10,8 +11,6 @@ import com.yiring.auth.vo.permission.MenuVo; ...@@ -10,8 +11,6 @@ import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.user.UserInfoVo; import com.yiring.auth.vo.user.UserInfoVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -33,11 +32,8 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -33,11 +32,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@Tag( @ApiSupport(order = -9998)
name = "用户信息", @Tag(name = "用户信息", description = "UserView")
description = "UserView",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9998") }) }
)
@RestController @RestController
@RequestMapping("/user/") @RequestMapping("/user/")
@RequiredArgsConstructor @RequiredArgsConstructor
......
...@@ -8,6 +8,7 @@ import java.lang.annotation.Target; ...@@ -8,6 +8,7 @@ import java.lang.annotation.Target;
/** /**
* 流控注解 * 流控注解
* eg: time = 3, count = 1 表示 3 秒内只能请求一次
* *
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
......
/* (C) 2023 YiRing, Inc. */ /* (C) 2023 YiRing, Inc. */
package com.yiring.common.aspect; package com.yiring.common.aspect;
import cn.hutool.core.util.StrUtil;
import com.yiring.common.annotation.RateLimiter; import com.yiring.common.annotation.RateLimiter;
import com.yiring.common.core.Redis; import com.yiring.common.core.Redis;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import com.yiring.common.utils.Contexts;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
...@@ -15,10 +19,9 @@ import org.aspectj.lang.JoinPoint; ...@@ -15,10 +19,9 @@ import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.ZSetOperations; import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
/** /**
* 流控切面 * 流控切面
...@@ -34,6 +37,7 @@ import org.springframework.web.context.request.RequestContextHolder; ...@@ -34,6 +37,7 @@ import org.springframework.web.context.request.RequestContextHolder;
public class RateLimiterAspect { public class RateLimiterAspect {
final Redis redis; final Redis redis;
final Environment environment;
/** /**
* 带有注解的方法之前执行 * 带有注解的方法之前执行
...@@ -73,21 +77,41 @@ public class RateLimiterAspect { ...@@ -73,21 +77,41 @@ public class RateLimiterAspect {
*/ */
private String getRateLimiterKey(String prefixKey, JoinPoint point) { private String getRateLimiterKey(String prefixKey, JoinPoint point) {
StringBuilder sb = new StringBuilder(prefixKey); StringBuilder sb = new StringBuilder(prefixKey);
HttpServletRequest request = getRequest(); HttpServletRequest request = Contexts.getRequest();
// 获取请求的 IP 地址
sb.append(Commons.getClientIpAddress(request)); sb.append(Commons.getClientIpAddress(request));
// 考虑登录用户的 token
String tokenName = environment.getProperty("sa-token.token-name");
if (StrUtil.isNotBlank(tokenName)) {
// 1. 优先从请求头中获取 token
String token = request.getHeader(tokenName);
// 2. 其次从 cookie 中获取 token
if (StrUtil.isBlank(token)) {
token =
Arrays
.stream(request.getCookies())
.filter(Objects::nonNull)
.filter(cookie -> tokenName.equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
if (StrUtil.isBlank(token)) {
// 3. 再其次从请求参数中获取 token
token = request.getParameter(tokenName);
}
// 设置 token 作为 key 的一部分
if (StrUtil.isNotBlank(token)) {
sb.append("_").append(token);
}
}
// 获取类名和方法名
MethodSignature signature = (MethodSignature) point.getSignature(); MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod(); Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass(); Class<?> targetClass = method.getDeclaringClass();
return sb.append("_").append(targetClass.getName()).append("_").append(method.getName()).toString(); return sb.append("_").append(targetClass.getName()).append("_").append(method.getName()).toString();
} }
/**
* 获取 HttpServletRequest
*/
private HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
return (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
}
} }
...@@ -7,6 +7,7 @@ import com.alibaba.fastjson2.JSONWriter; ...@@ -7,6 +7,7 @@ import com.alibaba.fastjson2.JSONWriter;
import com.yiring.common.constant.DateFormatter; import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import com.yiring.common.utils.Contexts;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
...@@ -17,8 +18,6 @@ import org.aspectj.lang.annotation.Aspect; ...@@ -17,8 +18,6 @@ import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
/** /**
* 请求接口切面,记录接口耗时相关 * 请求接口切面,记录接口耗时相关
...@@ -47,7 +46,7 @@ public class RequestAspect { ...@@ -47,7 +46,7 @@ public class RequestAspect {
@Around("apiPointCut()") @Around("apiPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = getRequest(); HttpServletRequest request = Contexts.getRequest();
// 放行白名单 // 放行白名单
for (String path : IGNORE_LIST) { for (String path : IGNORE_LIST) {
if (request.getServletPath().startsWith(path)) { if (request.getServletPath().startsWith(path)) {
...@@ -108,10 +107,4 @@ public class RequestAspect { ...@@ -108,10 +107,4 @@ public class RequestAspect {
return result; return result;
} }
private HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
return (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
}
} }
/* (C) 2024 YiRing, Inc. */
package com.yiring.common.utils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.experimental.UtilityClass;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
/**
* 作用域工具类
*
* @author Jim
* @version 0.1
*/
@UtilityClass
public class Contexts {
/**
* 获取当前请求
*
* @return HttpServletRequest
*/
public HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
return (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
}
}
...@@ -18,4 +18,7 @@ dependencies { ...@@ -18,4 +18,7 @@ dependencies {
// hutool // hutool
implementation "cn.hutool:hutool-core:${hutoolVersion}" implementation "cn.hutool:hutool-core:${hutoolVersion}"
// fastjson
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
} }
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
import org.springframework.validation.annotation.Validated;
/**
* 上传分片参数
*
* @author Jim
* @version 0.1
* 2023/7/27 18:16
*/
@Schema(name = "UploadChunkParam", description = "分片上传参数")
@Validated
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UploadChunkParam implements Serializable {
@Serial
private static final long serialVersionUID = 3118356781129185420L;
@Schema(description = "文件名", defaultValue = "test.png", example = "test.png")
@NotEmpty(message = "文件名不能为空")
String name;
@Schema(description = "文件大小", defaultValue = "1024", example = "1024", type = "long")
@NotEmpty(message = "文件大小不能为空")
@Min(0)
Long size;
@Schema(description = "文件类型", defaultValue = "image/png", example = "image/png")
@NotEmpty(message = "文件类型不能为空")
String type;
@Schema(description = "文件扩展", defaultValue = "png", example = "png")
@NotEmpty(message = "文件扩展不能为空")
String ext;
@Schema(description = "文件 md5", defaultValue = "md5", example = "md5")
@NotEmpty(message = "文件 md5不能为空")
String md5;
@Schema(description = "文件分片总数", defaultValue = "10", example = "10", type = "integer")
@NotEmpty(message = "文件分片总数不能为空")
@Min(1)
Integer chunks;
@Schema(description = "当前分片", defaultValue = "1", example = "1", type = "integer")
@NotEmpty(message = "当前分片不能为空")
@Min(0)
Integer chunkIndex;
@Schema(description = "分片文件大小", defaultValue = "1024", example = "1024", type = "long")
@NotEmpty(message = "分片文件大小不能为空")
@Min(0)
Long chunkSize;
/**
* 可以记录与业务相关的数据标识等描述信息
*/
@Schema(description = "自定义扩展参数")
String extra;
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.common.service; package com.yiring.common.service;
import java.io.File;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
/** /**
...@@ -25,4 +26,15 @@ public interface UploadProcessService { ...@@ -25,4 +26,15 @@ public interface UploadProcessService {
default String handle(String object, MultipartFile file) { default String handle(String object, MultipartFile file) {
return object; return object;
} }
/**
* 对媒体文件进行预处理,例如: 音视频
*
* @param object 文件存储对象
* @param file 文件
* @return 预处理后的文件地址
*/
default String handleMedia(String object, File file) {
return object;
}
} }
...@@ -2,13 +2,21 @@ ...@@ -2,13 +2,21 @@
package com.yiring.common.web; package com.yiring.common.web;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.common.annotation.DownloadResponse; import com.yiring.common.annotation.DownloadResponse;
import com.yiring.common.config.MinioConfig;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Minio; import com.yiring.common.core.Minio;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.param.DownloadParam; import com.yiring.common.param.DownloadParam;
import com.yiring.common.param.UploadChunkParam;
import com.yiring.common.service.FileManageService; import com.yiring.common.service.FileManageService;
import com.yiring.common.service.UploadProcessService;
import com.yiring.common.util.FileUtils; import com.yiring.common.util.FileUtils;
import com.yiring.common.vo.ImageInfo; import com.yiring.common.vo.ImageInfo;
import io.minio.GetObjectResponse; import io.minio.GetObjectResponse;
...@@ -16,16 +24,29 @@ import io.minio.StatObjectResponse; ...@@ -16,16 +24,29 @@ import io.minio.StatObjectResponse;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject; import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
...@@ -38,17 +59,16 @@ import org.springframework.web.multipart.MultipartFile; ...@@ -38,17 +59,16 @@ import org.springframework.web.multipart.MultipartFile;
@Slf4j @Slf4j
@Validated @Validated
@Tag( @ApiSupport(order = -9997)
name = "文件管理", @Tag(name = "文件管理", description = "file")
description = "file",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9997") }) }
)
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@RequestMapping("/common/file/") @RequestMapping("/common/file/")
public class MinioController { public class MinioController {
final Minio minio; final Minio minio;
final MinioConfig config;
final UploadProcessService service;
final FileManageService fileManageService; final FileManageService fileManageService;
/** /**
...@@ -66,6 +86,133 @@ public class MinioController { ...@@ -66,6 +86,133 @@ public class MinioController {
} }
} }
/**
* 文件分片上传
* 说明:此接口需要前端配合切片和处理上传逻辑,后端只负责接收和合并
*/
@SneakyThrows
@Operation(summary = "文件分片上传")
@PostMapping(value = "uploadChunk", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> chunk(
@ParameterObject @Validated UploadChunkParam chunkParam,
@Parameter(name = "分片文件", required = true) @RequestPart("chunk") MultipartFile chunk
) {
// 获取临时目录
String base = "Upload";
String tmpDir = System.getProperty("java.io.tmpdir");
// 存储桶
String bucket = config.getBucket();
// 获取文件临时目录
Path fileTempDir = Paths.get(tmpDir, base, chunkParam.getMd5());
if (!fileTempDir.toFile().exists()) {
// 文件临时目录不存在则创建
Files.createDirectories(fileTempDir);
}
// 获取分片文件路径
String partSuffix = ".part";
String chunkFileName = chunkParam.getName() + "_" + chunkParam.getChunkIndex() + partSuffix;
String chunkFilePath = Paths.get(fileTempDir.toString(), chunkFileName).toString();
boolean isLastChunk = chunkParam.getChunkIndex().equals(chunkParam.getChunks() - 1);
// 校验分片文件是否存在
File chunkFile = new File(chunkFilePath);
if (!isLastChunk && chunkFile.exists() && chunkFile.length() == chunkParam.getChunkSize()) {
// 分片文件已存在,直接返回
return Result.ok();
}
// 保存分片文件
chunk.transferTo(chunkFile);
// 校验分片文件是否为最后一个分片
if (isLastChunk) {
// 获取目录下所有分片文件
List<File> parts = Stream
.of(FileUtil.ls(fileTempDir.toString()))
.filter(temp -> temp.getName().endsWith(partSuffix))
.sorted((a, b) -> {
// 按照分片索引排序
String aIndex = a.getName().replaceAll(".*_(\\d+)\\" + partSuffix, "$1");
String bIndex = b.getName().replaceAll(".*_(\\d+)\\" + partSuffix, "$1");
return Integer.compare(Integer.parseInt(aIndex), Integer.parseInt(bIndex));
})
.toList();
// 校验分片数是否一致
if (parts.size() != chunkParam.getChunks()) {
// 文件分片数不一致,无法合并
throw Status.BAD_REQUEST.exception();
}
// 合并后的文件
String object = StrUtil.join("/", base, chunkParam.getMd5(), chunkParam.getName());
// 获取合并后的文件
File file = Paths.get(fileTempDir.toString(), chunkParam.getName()).toFile();
if (file.exists()) {
// 合并后的文件已存在,校验 MD5 是否一致
if (Objects.equals(chunkParam.getMd5(), FileUtils.getFileMd5(file))) {
// MD5 一致
GetObjectResponse response = minio.getObject(bucket, object + ".info");
JSONObject info = JSONObject.parseObject(IoUtil.read(response, StandardCharsets.UTF_8));
return Result.ok(minio.getURI(info.getString("targetObject"), bucket));
}
}
// 循环以追加的方式合并分片文件
@Cleanup
FileOutputStream outputStream = new FileOutputStream(file);
for (File part : parts) {
byte[] chunkData = FileCopyUtils.copyToByteArray(part);
outputStream.write(chunkData);
}
// 合并完成后上传文件
minio.putObject(file, object);
// 上传文件描述信息
String infoObject = object + ".info";
JSONObject info = new JSONObject();
info.put("name", chunkParam.getName());
info.put("size", chunkParam.getSize());
info.put("type", chunkParam.getType());
info.put("ext", chunkParam.getExt());
info.put("md5", chunkParam.getMd5());
info.put("chunks", chunkParam.getChunks());
info.put("extra", chunkParam.getExtra());
info.put("sourceObject", object);
// Video/Audio: 在文件名上追加时长,视频生成封面图
object = service.handleMedia(object, file);
// 补充转换后的文件信息
info.put("targetObject", object);
info.put("datetime", LocalDateTime.now().format(DateFormatter.DATE_TIME));
// 上传文件描述信息
@Cleanup
InputStream infoInputStream = new ByteArrayInputStream(
info.toJSONString().getBytes(StandardCharsets.UTF_8)
);
minio.putObject(infoInputStream, MediaType.APPLICATION_JSON_VALUE, infoObject);
try {
// 删除临时文件
FileUtil.del(fileTempDir.toFile());
} catch (Exception e) {
// 删除失败,设置为在 JVM 退出时删除
fileTempDir.toFile().deleteOnExit();
}
// 返回文件路径
return Result.ok(minio.getURI(object, bucket));
}
// 非合并操作时,仅返回成功
return Result.ok();
}
@Operation(summary = "Base64 图片上传") @Operation(summary = "Base64 图片上传")
@Parameter( @Parameter(
name = "base64Image", name = "base64Image",
......
...@@ -12,9 +12,11 @@ import java.nio.charset.StandardCharsets; ...@@ -12,9 +12,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import lombok.Cleanup;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.util.DigestUtils;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
/** /**
...@@ -116,4 +118,17 @@ public class FileUtils { ...@@ -116,4 +118,17 @@ public class FileUtils {
.contentType(contentType) .contentType(contentType)
.build(); .build();
} }
/**
* 获取文件 MD5
*
* @param file 文件
* @return MD5 Hash
*/
@SneakyThrows
public String getFileMd5(File file) {
@Cleanup
FileInputStream fis = new FileInputStream(file);
return DigestUtils.md5DigestAsHex(fis);
}
} }
/* (C) 2023 YiRing, Inc. */ /* (C) 2023 YiRing, Inc. */
package com.yiring.dict.web; package com.yiring.dict.web;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.exception.BusinessException; import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam; import com.yiring.common.param.IdParam;
...@@ -17,8 +18,6 @@ import com.yiring.dict.domain.DictRepository; ...@@ -17,8 +18,6 @@ import com.yiring.dict.domain.DictRepository;
import com.yiring.dict.param.DictParam; import com.yiring.dict.param.DictParam;
import com.yiring.dict.vo.DictVo; import com.yiring.dict.vo.DictVo;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -45,11 +44,8 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -45,11 +44,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@Tag( @ApiSupport(order = -96)
name = "字典管理", @Tag(name = "字典管理", description = "Dict")
description = "Dict",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-96") }) }
)
@RestController @RestController
@RequestMapping("/sys/dict/") @RequestMapping("/sys/dict/")
@RequiredArgsConstructor @RequiredArgsConstructor
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
package com.yiring.dict.web; package com.yiring.dict.web;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.domain.BasicEntity; import com.yiring.common.domain.BasicEntity;
import com.yiring.common.exception.BusinessException; import com.yiring.common.exception.BusinessException;
...@@ -21,8 +22,6 @@ import com.yiring.dict.param.DictItemParam; ...@@ -21,8 +22,6 @@ import com.yiring.dict.param.DictItemParam;
import com.yiring.dict.param.SelectorDictItemParam; import com.yiring.dict.param.SelectorDictItemParam;
import com.yiring.dict.vo.DictItemVo; import com.yiring.dict.vo.DictItemVo;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -50,11 +49,8 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -50,11 +49,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@Tag( @ApiSupport(order = -95)
name = "字典选项管理", @Tag(name = "字典选项管理", description = "DictItem")
description = "DictItem",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-95") }) }
)
@RestController @RestController
@RequestMapping("/sys/dict/item/") @RequestMapping("/sys/dict/item/")
@RequiredArgsConstructor @RequiredArgsConstructor
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.web; package com.yiring.websocket.web;
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths; import com.yiring.auth.util.Auths;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.websocket.domain.StompPrincipal; import com.yiring.websocket.domain.StompPrincipal;
import com.yiring.websocket.registry.StompUserRegistry; import com.yiring.websocket.registry.StompUserRegistry;
import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
...@@ -43,7 +45,7 @@ public class StompReceiver { ...@@ -43,7 +45,7 @@ public class StompReceiver {
* *
* @param accessor StompHeaderAccessor * @param accessor StompHeaderAccessor
*/ */
@MessageMapping("/login") @MessageMapping("login")
public void login(StompHeaderAccessor accessor, String token) { public void login(StompHeaderAccessor accessor, String token) {
try { try {
User user = auths.getUserByToken(token); User user = auths.getUserByToken(token);
...@@ -68,7 +70,7 @@ public class StompReceiver { ...@@ -68,7 +70,7 @@ public class StompReceiver {
* *
* @param accessor 访问器 * @param accessor 访问器
*/ */
@MessageMapping("/state") @MessageMapping("state")
public void state(StompHeaderAccessor accessor, String message) { public void state(StompHeaderAccessor accessor, String message) {
log.info("收到来自 STOMP Client `/app/state` 消息:{}", message); log.info("收到来自 STOMP Client `/app/state` 消息:{}", message);
...@@ -81,17 +83,21 @@ public class StompReceiver { ...@@ -81,17 +83,21 @@ public class StompReceiver {
stompUserRegistry.updateUser(accessor.getSessionId(), principal); stompUserRegistry.updateUser(accessor.getSessionId(), principal);
} }
@MessageMapping("/test") @MessageMapping("ping")
public void test(StompHeaderAccessor accessor, String message) { public void test(StompHeaderAccessor accessor, String message) {
log.info("收到来自 STOMP Client `/app/test` 消息:{}", message); log.info("收到来自 STOMP Client `/app/ping` 消息:{}", message);
Set<SimpUser> users = simpUserRegistry.getUsers(); Set<SimpUser> users = simpUserRegistry.getUsers();
log.info("{}", users); log.info("{}", users);
JSONObject body = new JSONObject();
body.put("message", "pong");
body.put("time", DateFormatter.DATE_TIME.format(LocalDateTime.now()));
simpMessagingTemplate.convertAndSendToUser( simpMessagingTemplate.convertAndSendToUser(
Objects.requireNonNull(accessor.getSessionId()), Objects.requireNonNull(accessor.getSessionId()),
"/topic/reply", "/topic/reply",
Result.ok(UUID.fastUUID().toString(true)) Result.ok(body)
); );
} }
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论