Ver Fonte

分块上传,接口加锁

tsurumure há 3 meses atrás
pai
commit
d4fb1dcca1

+ 152 - 139
src/main/java/com/backendsys/modules/upload/service/impl/SysFileMultipartServiceImpl.java

@@ -156,80 +156,87 @@ public class SysFileMultipartServiceImpl implements SysFileMultipartService {
         String filename = multipartFile.getOriginalFilename();
         if (filename.length() > 50) throw new CustException("文件名长度不能超过 50 字符");
 
-        // 获得公共配置
-        List<SysCommon> sysCommonList = sysCommonService.getCommonByCategory("UPLOAD");
-        AtomicReference<Integer> UPLOAD_TARGET = new AtomicReference<>();
-        AtomicReference<Integer> UPLOAD_THUMB_SIZE = new AtomicReference<>();
-        AtomicReference<Boolean> UPLOAD_MD5_DUPLICATE = new AtomicReference<>();
-        sysCommonList.stream().forEach(sysCommon -> {
-            if (sysCommon.getTag().equals("UPLOAD_TARGET")) UPLOAD_TARGET.set(Convert.toInt(sysCommon.getValue()));
-            if (sysCommon.getTag().equals("UPLOAD_THUMB_SIZE")) UPLOAD_THUMB_SIZE.set(Convert.toInt(sysCommon.getValue()));
-            if (sysCommon.getTag().equals("UPLOAD_MD5_DUPLICATE")) UPLOAD_MD5_DUPLICATE.set(Convert.toBool(sysCommon.getValue()));
-        });
-
-        try {
-
-            SysFile sysFileEntity = null;
-
-            String md5 = DigestUtil.md5Hex(multipartFile.getInputStream());
-
-            // 如果开启了 MD5秒传,则判断文件是否上传过,是的话就直接返回
-            // [系统配置] 是否启用文件MD5查重 (仅判断用户自己上传过的文件)
-            // - 存在,则根据文件MD5 判断是否上传过文件 (仅更新时间,不上传已存在的文件,允许更新文件分类)
-            // - 存在,如果存在两个相同 md5 以上的文件,则需要排重后再上传 (如果是不同用户
-            // - 不存在,则走上传流程
-            Boolean is_exist = false;
-            if (UPLOAD_MD5_DUPLICATE.get()) {
-
-                // 排除文件异常的情况(出现两个以上相同MD5的文件)
-                LambdaQueryWrapper<SysFile> wrapperFile = new LambdaQueryWrapper<>();
-                wrapperFile.eq(SysFile::getMd5, md5);
-                wrapperFile.eq(SysFile::getUser_id, SecurityUtil.getUserId());
-
-                // [DB] 查询已存在的文件分块记录 (只有完全上传成功,才会有 MD5)
-                // - 异常情况:如果有两个相同的文件,一个上传50%,一个上传成功,就会出现以下这种情况,需要手动解决
-                List<SysFile> sysFileEntityList = sysFileDao.selectList(wrapperFile);
-                if (sysFileEntityList != null && sysFileEntityList.size() > 1) {
-                    throw new CustException("存在 " + sysFileEntityList.size() + " 个相同MD5 (" + md5 + ") 的文件,请删除后再重新上传");
-                }
+        // 按用户加锁
+        RLock lock = redissonClient.getLock("lock::multipart-upload::user-id::" + SecurityUtil.getUserId());
+        try { lock.tryLock(3, TimeUnit.SECONDS);
+
+            // 获得公共配置
+            List<SysCommon> sysCommonList = sysCommonService.getCommonByCategory("UPLOAD");
+            AtomicReference<Integer> UPLOAD_TARGET = new AtomicReference<>();
+            AtomicReference<Integer> UPLOAD_THUMB_SIZE = new AtomicReference<>();
+            AtomicReference<Boolean> UPLOAD_MD5_DUPLICATE = new AtomicReference<>();
+            sysCommonList.stream().forEach(sysCommon -> {
+                if (sysCommon.getTag().equals("UPLOAD_TARGET")) UPLOAD_TARGET.set(Convert.toInt(sysCommon.getValue()));
+                if (sysCommon.getTag().equals("UPLOAD_THUMB_SIZE")) UPLOAD_THUMB_SIZE.set(Convert.toInt(sysCommon.getValue()));
+                if (sysCommon.getTag().equals("UPLOAD_MD5_DUPLICATE")) UPLOAD_MD5_DUPLICATE.set(Convert.toBool(sysCommon.getValue()));
+            });
+
+            try {
+
+                SysFile sysFileEntity = null;
+
+                String md5 = DigestUtil.md5Hex(multipartFile.getInputStream());
+
+                // 如果开启了 MD5秒传,则判断文件是否上传过,是的话就直接返回
+                // [系统配置] 是否启用文件MD5查重 (仅判断用户自己上传过的文件)
+                // - 存在,则根据文件MD5 判断是否上传过文件 (仅更新时间,不上传已存在的文件,允许更新文件分类)
+                // - 存在,如果存在两个相同 md5 以上的文件,则需要排重后再上传 (如果是不同用户
+                // - 不存在,则走上传流程
+                Boolean is_exist = false;
+                if (UPLOAD_MD5_DUPLICATE.get()) {
+
+                    // 排除文件异常的情况(出现两个以上相同MD5的文件)
+                    LambdaQueryWrapper<SysFile> wrapperFile = new LambdaQueryWrapper<>();
+                    wrapperFile.eq(SysFile::getMd5, md5);
+                    wrapperFile.eq(SysFile::getUser_id, SecurityUtil.getUserId());
+
+                    // [DB] 查询已存在的文件分块记录 (只有完全上传成功,才会有 MD5)
+                    // - 异常情况:如果有两个相同的文件,一个上传50%,一个上传成功,就会出现以下这种情况,需要手动解决
+                    List<SysFile> sysFileEntityList = sysFileDao.selectList(wrapperFile);
+                    if (sysFileEntityList != null && sysFileEntityList.size() > 1) {
+                        throw new CustException("存在 " + sysFileEntityList.size() + " 个相同MD5 (" + md5 + ") 的文件,请删除后再重新上传");
+                    }
+
+                    if (sysFileEntityList != null && sysFileEntityList.size() > 0) {
+                        is_exist = true;
+                        // 将已存在的文件,赋值
+                        sysFileEntity = sysFileEntityList.get(0);
+                        // [DB] 更新文件 (文件分类、上传时间)
+                        sysFileEntity.setCategory_id(category_id);
+                        sysFileEntity.setUpload_time(DateUtil.now());
+                        sysFileDao.updateById(sysFileEntity);
+                    } else {
+                        // [DB] 创建新的文件
+                        sysFileEntity = uploadInitEvent(multipartFile, category_id, UPLOAD_TARGET.get(), upload_chunk_count, md5);
+                        // [格式化] 封面 (图片类型)
+                        sysFileEntity = setThumbUrl(sysFileEntity, UPLOAD_THUMB_SIZE.get(), UPLOAD_THUMB_SIZE.get(), StyleEnums.THUMB_BACKGROUND.getValue());
+                    }
 
-                if (sysFileEntityList != null && sysFileEntityList.size() > 0) {
-                    is_exist = true;
-                    // 将已存在的文件,赋值
-                    sysFileEntity = sysFileEntityList.get(0);
-                    // [DB] 更新文件 (文件分类、上传时间)
-                    sysFileEntity.setCategory_id(category_id);
-                    sysFileEntity.setUpload_time(DateUtil.now());
-                    sysFileDao.updateById(sysFileEntity);
                 } else {
+                    // 不开启 MD5秒 传,则直接初始化分块
                     // [DB] 创建新的文件
                     sysFileEntity = uploadInitEvent(multipartFile, category_id, UPLOAD_TARGET.get(), upload_chunk_count, md5);
                     // [格式化] 封面 (图片类型)
                     sysFileEntity = setThumbUrl(sysFileEntity, UPLOAD_THUMB_SIZE.get(), UPLOAD_THUMB_SIZE.get(), StyleEnums.THUMB_BACKGROUND.getValue());
                 }
 
-            } else {
-                // 不开启 MD5秒 传,则直接初始化分块
-                // [DB] 创建新的文件
-                sysFileEntity = uploadInitEvent(multipartFile, category_id, UPLOAD_TARGET.get(), upload_chunk_count, md5);
-                // [格式化] 封面 (图片类型)
-                sysFileEntity = setThumbUrl(sysFileEntity, UPLOAD_THUMB_SIZE.get(), UPLOAD_THUMB_SIZE.get(), StyleEnums.THUMB_BACKGROUND.getValue());
+                Map<String, Object> resp = new LinkedHashMap<>();
+                resp.put("target", UPLOAD_TARGET.get());
+                resp.put("target_label", StyleEnums.targetToLabel(UPLOAD_TARGET.get()));
+                resp.put("upload_chunk_count", upload_chunk_count);
+                resp.put("upload_id", sysFileEntity.getUpload_id());
+                resp.put("object_key", sysFileEntity.getObject_key());
+                resp.put("md5", md5);
+                resp.put("is_exist", is_exist);
+                resp.put("file_info", is_exist ? sysFileEntity : null);
+                return resp;
+
+            } catch (IOException e) {
+                throw new RuntimeException(e);
             }
 
-            Map<String, Object> resp = new LinkedHashMap<>();
-            resp.put("target", UPLOAD_TARGET.get());
-            resp.put("target_label", StyleEnums.targetToLabel(UPLOAD_TARGET.get()));
-            resp.put("upload_chunk_count", upload_chunk_count);
-            resp.put("upload_id", sysFileEntity.getUpload_id());
-            resp.put("object_key", sysFileEntity.getObject_key());
-            resp.put("md5", md5);
-            resp.put("is_exist", is_exist);
-            resp.put("file_info", is_exist ? sysFileEntity : null);
-            return resp;
-
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
+        } catch (InterruptedException e) { throw new RuntimeException(e);
+        } finally { lock.unlock(); }
     }
 
     // 2.上传分块
@@ -238,14 +245,14 @@ public class SysFileMultipartServiceImpl implements SysFileMultipartService {
     @Transactional(rollbackFor = Exception.class)
     public Map<String, Object> multipartUpload(MultipartFile multipartFile, String upload_id, Integer upload_chunk_index) {
 
+        if (multipartFile.isEmpty()) throw new CustException("file 上传文件不能为空");
+        if (StrUtil.isEmpty(upload_id)) throw new CustException("upload_id 不能为空");
+        if (upload_chunk_index == null) throw new CustException("upload_chunk_index 不能为空");
+
         // 按用户加锁
         RLock lock = redissonClient.getLock("lock::multipart-upload::user-id::" + SecurityUtil.getUserId());
         try { lock.tryLock(3, TimeUnit.SECONDS);
 
-            if (multipartFile.isEmpty()) throw new CustException("file 上传文件不能为空");
-            if (StrUtil.isEmpty(upload_id)) throw new CustException("upload_id 不能为空");
-            if (upload_chunk_index == null) throw new CustException("upload_chunk_index 不能为空");
-
             // [Get] 获得初始化分块信息
             SysFile sysFileEntity = sysFileDao.selectOne(new LambdaQueryWrapper<SysFile>().eq(SysFile::getUpload_id, upload_id));
             if (sysFileEntity == null) throw new CustException("分块记录不存在");
@@ -302,89 +309,95 @@ public class SysFileMultipartServiceImpl implements SysFileMultipartService {
 
         if (StrUtil.isEmpty(upload_id)) throw new CustException("upload_id 不能为空");
 
-        // [Get] 查询文件分块记录
-        SysFile sysFileEntity = sysFileDao.selectOne(new LambdaQueryWrapper<SysFile>().eq(SysFile::getUpload_id, upload_id));
-        if (sysFileEntity == null) throw new CustException("upload_id 不存在");
+        // 按用户加锁
+        RLock lock = redissonClient.getLock("lock::multipart-upload::user-id::" + SecurityUtil.getUserId());
+        try { lock.tryLock(3, TimeUnit.SECONDS);
 
-        // 判断分块索引和分块数量是否一致
-        if (sysFileEntity.getUpload_chunk_index() != sysFileEntity.getUpload_chunk_count()) {
-            throw new CustException("分块索引和分块数量不一致");
-        }
+            // [Get] 查询文件分块记录
+            SysFile sysFileEntity = sysFileDao.selectOne(new LambdaQueryWrapper<SysFile>().eq(SysFile::getUpload_id, upload_id));
+            if (sysFileEntity == null) throw new CustException("upload_id 不存在");
 
+            // 判断分块索引和分块数量是否一致
+            if (sysFileEntity.getUpload_chunk_index() != sysFileEntity.getUpload_chunk_count()) {
+                throw new CustException("分块索引和分块数量不一致");
+            }
 
-        // 获得公共配置
-        List<SysCommon> sysCommonList = sysCommonService.getCommonByCategory("UPLOAD");
-        AtomicReference<Integer> UPLOAD_TARGET = new AtomicReference<>();
-        AtomicReference<Integer> UPLOAD_THUMB_SIZE = new AtomicReference<>();
-        sysCommonList.stream().forEach(sysCommon -> {
-            if (sysCommon.getTag().equals("UPLOAD_TARGET")) UPLOAD_TARGET.set(Convert.toInt(sysCommon.getValue()));
-            if (sysCommon.getTag().equals("UPLOAD_THUMB_SIZE")) UPLOAD_THUMB_SIZE.set(Convert.toInt(sysCommon.getValue()));
-        });
-        System.out.println("[系统配置] 上传目标(-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云): " + UPLOAD_TARGET);
-        System.out.println("[系统配置] 图片的缩略图宽高尺寸 (px): " + UPLOAD_THUMB_SIZE);
 
+            // 获得公共配置
+            List<SysCommon> sysCommonList = sysCommonService.getCommonByCategory("UPLOAD");
+            AtomicReference<Integer> UPLOAD_TARGET = new AtomicReference<>();
+            AtomicReference<Integer> UPLOAD_THUMB_SIZE = new AtomicReference<>();
+            sysCommonList.stream().forEach(sysCommon -> {
+                if (sysCommon.getTag().equals("UPLOAD_TARGET")) UPLOAD_TARGET.set(Convert.toInt(sysCommon.getValue()));
+                if (sysCommon.getTag().equals("UPLOAD_THUMB_SIZE")) UPLOAD_THUMB_SIZE.set(Convert.toInt(sysCommon.getValue()));
+            });
+            System.out.println("[系统配置] 上传目标(-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云): " + UPLOAD_TARGET);
+            System.out.println("[系统配置] 图片的缩略图宽高尺寸 (px): " + UPLOAD_THUMB_SIZE);
 
-        // target: 上传目标 (-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云)
-        if (UPLOAD_TARGET.get() == 1) {
-
-            // 查询分块集合 (etag)
-            Map<String, Object> partList = listParts(upload_id, sysFileEntity.getObject_key());
-            PartListing partListing = (PartListing) partList.get("listParts");
-            List<PartSummary> partSummaryList = partListing.getParts();
-            List<PartETag> etags = partSummaryList.stream()
-                .map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag()))
-                .collect(Collectors.toList());
-
-            // 如果分块数量对不上,最好还是重新上传
-            if (sysFileEntity.getUpload_chunk_count() != etags.size()) throw new CustException("分块索引异常,请重新上传");
-
-            // [腾讯云] 合并分块
-            CompleteMultipartUploadResult completeResult = tencentCosService.completeMultipartUpload(upload_id, sysFileEntity.getObject_key(), etags);
-            if (completeResult == null) throw new CustException("分块合并失败");
-
-            // 拼接图片路径
-            sysFileEntity.setUrl(TENCENT_ACCESSIBLE_DOMAIN + "/" + completeResult.getKey());
-            sysFileEntity.setRequest_id(completeResult.getRequestId());
-        }
 
-        // 3: 抖音云
-        if (UPLOAD_TARGET.get() == 3) {
+            // target: 上传目标 (-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云)
+            if (UPLOAD_TARGET.get() == 1) {
 
-            CompleteMultipartUploadV2Output completeResult = douyinTosService.completeMultipartUpload(upload_id, sysFileEntity.getObject_key());
-            if (completeResult == null) throw new CustException("分块合并失败");
+                // 查询分块集合 (etag)
+                Map<String, Object> partList = listParts(upload_id, sysFileEntity.getObject_key());
+                PartListing partListing = (PartListing) partList.get("listParts");
+                List<PartSummary> partSummaryList = partListing.getParts();
+                List<PartETag> etags = partSummaryList.stream()
+                    .map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag()))
+                    .collect(Collectors.toList());
 
-            // 拼接图片路径
-            sysFileEntity.setUrl(DOUYIN_ACCESSIBLE_DOMAIN + "/" + completeResult.getKey());
-            sysFileEntity.setRequest_id(completeResult.getRequestInfo().getRequestId());
+                // 如果分块数量对不上,最好还是重新上传
+                if (sysFileEntity.getUpload_chunk_count() != etags.size()) throw new CustException("分块索引异常,请重新上传");
 
-        }
+                // [腾讯云] 合并分块
+                CompleteMultipartUploadResult completeResult = tencentCosService.completeMultipartUpload(upload_id, sysFileEntity.getObject_key(), etags);
+                if (completeResult == null) throw new CustException("分块合并失败");
 
-        /*
-            如果是图片类型,就设置封面:
-            图片Icon (png / jpg / jpeg / gif / bmp / webp)
-            视频 Icon (mp4 / avi / mov / flv / 3gp / mpeg / wmv)
-            Word Icon (doc / docx)
-            Excel Icon (xls / xlsx)
-            PDF Icon (pdf)
-            其他 Icon
-         */
-        setThumbUrl(sysFileEntity, UPLOAD_THUMB_SIZE.get(), UPLOAD_THUMB_SIZE.get(), StyleEnums.THUMB_BACKGROUND.getValue());
-
-        // [DB] 更新分块记录
-        sysFileDao.updateCompleteFile(sysFileEntity);
-
-        // -- 合并分块成功时,返回值 -------------------------------------
-        // 移除 upload_id
-        sysFileEntity.setUpload_id(null);
-        // 翻译 储存介质
-        sysFileEntity.setTarget_label(StyleEnums.targetToLabel(sysFileEntity.getTarget()));
-        // 翻译 用户名
-        SysUser userEntity = sysUserDao.selectById(sysFileEntity.getUser_id());
-        sysFileEntity.setUsername(userEntity.getUsername());
-        // ----------------------------------------------------
+                // 拼接图片路径
+                sysFileEntity.setUrl(TENCENT_ACCESSIBLE_DOMAIN + "/" + completeResult.getKey());
+                sysFileEntity.setRequest_id(completeResult.getRequestId());
+            }
 
-        return sysFileEntity;
+            // 3: 抖音云
+            if (UPLOAD_TARGET.get() == 3) {
+
+                CompleteMultipartUploadV2Output completeResult = douyinTosService.completeMultipartUpload(upload_id, sysFileEntity.getObject_key());
+                if (completeResult == null) throw new CustException("分块合并失败");
 
+                // 拼接图片路径
+                sysFileEntity.setUrl(DOUYIN_ACCESSIBLE_DOMAIN + "/" + completeResult.getKey());
+                sysFileEntity.setRequest_id(completeResult.getRequestInfo().getRequestId());
+
+            }
+
+            /*
+                如果是图片类型,就设置封面:
+                图片Icon (png / jpg / jpeg / gif / bmp / webp)
+                视频 Icon (mp4 / avi / mov / flv / 3gp / mpeg / wmv)
+                Word Icon (doc / docx)
+                Excel Icon (xls / xlsx)
+                PDF Icon (pdf)
+                其他 Icon
+             */
+            setThumbUrl(sysFileEntity, UPLOAD_THUMB_SIZE.get(), UPLOAD_THUMB_SIZE.get(), StyleEnums.THUMB_BACKGROUND.getValue());
+
+            // [DB] 更新分块记录
+            sysFileDao.updateCompleteFile(sysFileEntity);
+
+            // -- 合并分块成功时,返回值 -------------------------------------
+            // 移除 upload_id
+            sysFileEntity.setUpload_id(null);
+            // 翻译 储存介质
+            sysFileEntity.setTarget_label(StyleEnums.targetToLabel(sysFileEntity.getTarget()));
+            // 翻译 用户名
+            SysUser userEntity = sysUserDao.selectById(sysFileEntity.getUser_id());
+            sysFileEntity.setUsername(userEntity.getUsername());
+            // ----------------------------------------------------
+
+            return sysFileEntity;
+
+        } catch (InterruptedException e) { throw new RuntimeException(e);
+        } finally { lock.unlock(); }
     }
 
     // 查询分块上传情况