提交 d4674ff8 作者: 方治民

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

上级 174f735f
......@@ -10,6 +10,7 @@ import io.minio.ObjectWriteResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
......@@ -69,13 +70,23 @@ public class UploadProcessServiceImpl implements UploadProcessService {
// Video/Audio: 在文件名上追加时长,视频生成封面图
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;
}
@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) {
@Cleanup
InputStream is = file.getInputStream();
......@@ -111,7 +122,7 @@ public class UploadProcessServiceImpl implements UploadProcessService {
}
@SneakyThrows
public String handleMedia(String object, String suffix, MultipartFile file) {
public String handleMedia(String object, String suffix, File file) {
// 判断是否配置 ffmpeg 环境
FFmpeg ffmpeg = new FFmpeg();
FFprobe ffprobe = new FFprobe();
......@@ -124,13 +135,11 @@ public class UploadProcessServiceImpl implements UploadProcessService {
return object;
}
// 将上传的文件转存一份到本地临时目录
Path tempFile = Paths.get(FileUtil.getTmpDirPath(), "T_" + Commons.uuid(), file.getOriginalFilename());
FileUtil.mkParentDirs(tempFile);
file.transferTo(tempFile);
// 获取文件路径
Path path = file.toPath();
// 解析媒体文件
FFmpegProbeResult probeResult = ffprobe.probe(tempFile.toString());
FFmpegProbeResult probeResult = ffprobe.probe(path.toString());
FFmpegFormat format = probeResult.getFormat();
// 构建具有时长(秒)标记的存储地址
......@@ -141,16 +150,16 @@ public class UploadProcessServiceImpl implements UploadProcessService {
if (isSupportiveVideo(suffix)) {
// 大视频文件切片上传(> 5s)
if (sec > 5) {
filepath = handleVideoToHls(filepath, tempFile, ffmpeg, ffprobe);
filepath = handleVideoToHls(filepath, path, ffmpeg, ffprobe);
}
// 使用 ffmpeg 截取视频首帧图片
Path path = Paths.get(tempFile.getParent().toString(), FileUtil.getName(filepath) + ".jpg");
handleVideoScreenshot(tempFile.toString(), path.toString(), filepath, ffmpeg, ffprobe);
Path imagePath = Paths.get(path.getParent().toString(), FileUtil.getName(filepath) + ".jpg");
handleVideoScreenshot(path.toString(), imagePath.toString(), filepath, ffmpeg, ffprobe);
}
// 删除为本次上传进行本地处理所创建的整个临时文件夹目录
FileUtil.del(tempFile.getParent());
FileUtil.del(path.getParent());
return filepath;
}
......
......@@ -24,8 +24,8 @@ import org.springframework.web.bind.annotation.RestController;
*/
@Slf4j
@Validated
@ApiSupport(order = 0)
@Tag(name = "健康指标", description = "Health")
@ApiSupport(order = -99999)
@Tag(name = "Health", description = "Health")
@RequestMapping("/")
@RestController
@RequiredArgsConstructor
......
......@@ -50,7 +50,7 @@ import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Validated
@ApiSupport(order = 0)
@Tag(name = "示例", description = "Example")
@Tag(name = "【Examples】 示例", description = "Example")
@RequestMapping("/example/")
@RestController
@RequiredArgsConstructor
......
......@@ -5,6 +5,7 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.StpUtil;
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.UserRepository;
import com.yiring.auth.param.auth.LoginParam;
......@@ -17,8 +18,6 @@ import com.yiring.common.core.Result;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.util.Commons;
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 jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
......@@ -42,11 +41,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@Tag(
name = "Auth",
description = "身份认证",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9999") }) }
)
@ApiSupport(order = -9999)
@Tag(name = "Auth", description = "身份认证")
@RestController
@RequestMapping("/auth/")
@RequiredArgsConstructor
......
......@@ -2,6 +2,7 @@
package com.yiring.auth.web.sys.permission;
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.PermissionRepository;
import com.yiring.auth.param.permission.PermissionParam;
......@@ -16,8 +17,6 @@ import com.yiring.common.utils.RepositoryUtil;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo;
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 java.util.ArrayList;
import java.util.List;
......@@ -41,11 +40,8 @@ import org.springframework.web.bind.annotation.*;
@Slf4j
@Validated
@Tag(
name = "权限管理",
description = "Permission",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-99") }) }
)
@ApiSupport(order = -99)
@Tag(name = "权限管理", description = "Permission")
@RestController
@RequestMapping("/sys/permission/")
@RequiredArgsConstructor
......
/* (C) 2022 YiRing, Inc. */
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.PermissionRepository;
import com.yiring.auth.domain.role.Role;
......@@ -17,8 +18,6 @@ import com.yiring.common.utils.RepositoryUtil;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo;
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 java.io.Serializable;
import java.util.*;
......@@ -44,11 +43,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@Tag(
name = "角色管理",
description = "Role",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-98") }) }
)
@ApiSupport(order = -98)
@Tag(name = "角色管理", description = "Role")
@RestController
@RequestMapping("/sys/role/")
@RequiredArgsConstructor
......
/* (C) 2022 YiRing, Inc. */
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.RoleRepository;
import com.yiring.auth.domain.user.User;
......@@ -15,11 +16,12 @@ import com.yiring.common.utils.RepositoryUtil;
import com.yiring.common.utils.Specifications;
import com.yiring.common.vo.PageVo;
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 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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
......@@ -41,11 +43,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@Tag(
name = "用户管理",
description = "User",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-97") }) }
)
@ApiSupport(order = -97)
@Tag(name = "用户管理", description = "User")
@RestController
@RequestMapping("/sys/user/")
@RequiredArgsConstructor
......
/* (C) 2022 YiRing, Inc. */
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.PermissionRepository;
import com.yiring.auth.domain.user.User;
......@@ -10,8 +11,6 @@ import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.user.UserInfoVo;
import com.yiring.common.core.Result;
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 java.util.ArrayList;
import java.util.List;
......@@ -33,11 +32,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@Tag(
name = "用户信息",
description = "UserView",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9998") }) }
)
@ApiSupport(order = -9998)
@Tag(name = "用户信息", description = "UserView")
@RestController
@RequestMapping("/user/")
@RequiredArgsConstructor
......
......@@ -8,6 +8,7 @@ import java.lang.annotation.Target;
/**
* 流控注解
* eg: time = 3, count = 1 表示 3 秒内只能请求一次
*
* @author Jim
* @version 0.1
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.aspect;
import cn.hutool.core.util.StrUtil;
import com.yiring.common.annotation.RateLimiter;
import com.yiring.common.core.Redis;
import com.yiring.common.core.Status;
import com.yiring.common.util.Commons;
import com.yiring.common.utils.Contexts;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
......@@ -15,10 +19,9 @@ import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.ZSetOperations;
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;
public class RateLimiterAspect {
final Redis redis;
final Environment environment;
/**
* 带有注解的方法之前执行
......@@ -73,21 +77,41 @@ public class RateLimiterAspect {
*/
private String getRateLimiterKey(String prefixKey, JoinPoint point) {
StringBuilder sb = new StringBuilder(prefixKey);
HttpServletRequest request = getRequest();
HttpServletRequest request = Contexts.getRequest();
// 获取请求的 IP 地址
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();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
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;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result;
import com.yiring.common.util.Commons;
import com.yiring.common.utils.Contexts;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.List;
......@@ -17,8 +18,6 @@ import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Value;
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 {
@Around("apiPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = getRequest();
HttpServletRequest request = Contexts.getRequest();
// 放行白名单
for (String path : IGNORE_LIST) {
if (request.getServletPath().startsWith(path)) {
......@@ -108,10 +107,4 @@ public class RequestAspect {
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 {
// hutool
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. */
package com.yiring.common.service;
import java.io.File;
import org.springframework.web.multipart.MultipartFile;
/**
......@@ -25,4 +26,15 @@ public interface UploadProcessService {
default String handle(String object, MultipartFile file) {
return object;
}
/**
* 对媒体文件进行预处理,例如: 音视频
*
* @param object 文件存储对象
* @param file 文件
* @return 预处理后的文件地址
*/
default String handleMedia(String object, File file) {
return object;
}
}
......@@ -2,13 +2,21 @@
package com.yiring.common.web;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
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.config.MinioConfig;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Minio;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.param.DownloadParam;
import com.yiring.common.param.UploadChunkParam;
import com.yiring.common.service.FileManageService;
import com.yiring.common.service.UploadProcessService;
import com.yiring.common.util.FileUtils;
import com.yiring.common.vo.ImageInfo;
import io.minio.GetObjectResponse;
......@@ -16,16 +24,29 @@ import io.minio.StatObjectResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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 jakarta.servlet.http.HttpServletResponse;
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.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
......@@ -38,17 +59,16 @@ import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Validated
@Tag(
name = "文件管理",
description = "file",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9997") }) }
)
@ApiSupport(order = -9997)
@Tag(name = "文件管理", description = "file")
@RequiredArgsConstructor
@RestController
@RequestMapping("/common/file/")
public class MinioController {
final Minio minio;
final MinioConfig config;
final UploadProcessService service;
final FileManageService fileManageService;
/**
......@@ -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 图片上传")
@Parameter(
name = "base64Image",
......
......@@ -12,9 +12,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import javax.imageio.ImageIO;
import lombok.Cleanup;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.springframework.http.HttpHeaders;
import org.springframework.util.DigestUtils;
import org.springframework.util.FileCopyUtils;
/**
......@@ -116,4 +118,17 @@ public class FileUtils {
.contentType(contentType)
.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. */
package com.yiring.dict.web;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.common.core.Result;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam;
......@@ -17,8 +18,6 @@ import com.yiring.dict.domain.DictRepository;
import com.yiring.dict.param.DictParam;
import com.yiring.dict.vo.DictVo;
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 java.util.ArrayList;
import java.util.List;
......@@ -45,11 +44,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@Tag(
name = "字典管理",
description = "Dict",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-96") }) }
)
@ApiSupport(order = -96)
@Tag(name = "字典管理", description = "Dict")
@RestController
@RequestMapping("/sys/dict/")
@RequiredArgsConstructor
......
......@@ -2,6 +2,7 @@
package com.yiring.dict.web;
import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.common.core.Result;
import com.yiring.common.domain.BasicEntity;
import com.yiring.common.exception.BusinessException;
......@@ -21,8 +22,6 @@ import com.yiring.dict.param.DictItemParam;
import com.yiring.dict.param.SelectorDictItemParam;
import com.yiring.dict.vo.DictItemVo;
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 java.util.ArrayList;
import java.util.List;
......@@ -50,11 +49,8 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@Tag(
name = "字典选项管理",
description = "DictItem",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-95") }) }
)
@ApiSupport(order = -95)
@Tag(name = "字典选项管理", description = "DictItem")
@RestController
@RequestMapping("/sys/dict/item/")
@RequiredArgsConstructor
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.web;
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.websocket.domain.StompPrincipal;
import com.yiring.websocket.registry.StompUserRegistry;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
......@@ -43,7 +45,7 @@ public class StompReceiver {
*
* @param accessor StompHeaderAccessor
*/
@MessageMapping("/login")
@MessageMapping("login")
public void login(StompHeaderAccessor accessor, String token) {
try {
User user = auths.getUserByToken(token);
......@@ -68,7 +70,7 @@ public class StompReceiver {
*
* @param accessor 访问器
*/
@MessageMapping("/state")
@MessageMapping("state")
public void state(StompHeaderAccessor accessor, String message) {
log.info("收到来自 STOMP Client `/app/state` 消息:{}", message);
......@@ -81,17 +83,21 @@ public class StompReceiver {
stompUserRegistry.updateUser(accessor.getSessionId(), principal);
}
@MessageMapping("/test")
@MessageMapping("ping")
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();
log.info("{}", users);
JSONObject body = new JSONObject();
body.put("message", "pong");
body.put("time", DateFormatter.DATE_TIME.format(LocalDateTime.now()));
simpMessagingTemplate.convertAndSendToUser(
Objects.requireNonNull(accessor.getSessionId()),
"/topic/reply",
Result.ok(UUID.fastUUID().toString(true))
Result.ok(body)
);
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论