fix(system/file): 修复创建上级文件夹的并发问题

Co-authored-by: kiki1373639299<zkai0106@163.com>



# message auto-generated for no-merge-commit merge:
merge 2025-09-10 into dev

fix(system): 修复创建上级文件夹的并发问题

Created-by: kiki1373639299
Commit-by: kiki1373639299
Merged-by: Charles_7c
Description: <!--
  非常感谢您的 PR!在提交之前,请务必确保您 PR 的代码经过了完整测试,并且通过了代码规范检查。
-->

<!-- 在 [] 中输入 x 来勾选) -->

## PR 类型

<!-- 您的 PR 引入了哪种类型的变更? -->
<!-- 只支持选择一种类型,如果有多种类型,可以在更新日志中增加 “类型” 列。 -->

- [ ] 新 feature
- [X] Bug 修复
- [ ] 功能增强
- [ ] 文档变更
- [ ] 代码样式变更
- [ ] 重构
- [ ] 性能改进
- [ ] 单元测试
- [ ] CI/CD
- [ ] 其他

## PR 目的

<!-- 描述一下您的 PR 解决了什么问题。如果可以,请链接到相关 issues。 -->
修复创建上级文件夹的并发问题
## 解决方案

<!-- 详细描述您是如何解决的问题 -->
使用redis做锁
## PR 测试

<!-- 如果可以,请为您的 PR 添加或更新单元测试。 -->
<!-- 请描述一下您是如何测试 PR 的。例如:创建/更新单元测试或添加相关的截图。 -->

## Changelog

| 模块  | Changelog | Related issues |
|-----|-----------| -------------- |
|  continew-system   |     为文件服务添加Redis分布式锁防止并发创建目录冲突,使用Lock:storageCode:parentPath作为锁键,使用try-with-resources确保锁的正确释放	      |          [#33](https://gitcode.com/continew/continew-admin/issues/33)       |

<!-- 如果有多种类型的变更,可以在变更日志表中增加 “类型” 列,该列的值与上方 “PR 类型” 相同。 -->
<!-- Related issues 格式为 Closes #<issue号>,或者 Fixes #<issue号>,或者 Resolves #<issue号>。 -->

## 其他信息

<!-- 请描述一下还有哪些注意事项。例如:如果引入了一个不向下兼容的变更,请描述其影响。 -->

## 提交前确认

- [X] PR 代码经过了完整测试,并且通过了代码规范检查
- [X] 已经完整填写 Changelog,并链接到了相关 issues
- [X] PR 代码将要提交到 dev 分支

See merge request: continew/continew-admin!12
This commit is contained in:
kiki1373639299
2025-09-10 17:50:03 +08:00
committed by Charles_7c
parent c733eab8ea
commit 1b065b1755

View File

@@ -43,6 +43,7 @@ import top.continew.admin.system.model.resp.file.FileResp;
import top.continew.admin.system.model.resp.file.FileStatisticsResp;
import top.continew.admin.system.service.FileService;
import top.continew.admin.system.service.StorageService;
import top.continew.starter.cache.redisson.util.RedisLockUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.StrUtils;
import top.continew.starter.core.util.validation.CheckUtils;
@@ -280,38 +281,44 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
*/
@Override
public void createParentDir(String parentPath, StorageDO storage) {
if (StrUtil.isBlank(parentPath) || StringConstants.SLASH.equals(parentPath)) {
return;
}
// user/avatar/ => user、avatar
String[] parentPathParts = StrUtil.split(parentPath, StringConstants.SLASH, false, true).toArray(String[]::new);
String lastPath = StringConstants.SLASH;
StringBuilder currentPathBuilder = new StringBuilder();
for (int i = 0; i < parentPathParts.length; i++) {
String parentPathPart = parentPathParts[i];
if (i > 0) {
lastPath = currentPathBuilder.toString();
String lockKey = StrUtil.format("Lock:{}:{}", storage.getCode(), parentPath);
try (RedisLockUtils lock = RedisLockUtils.tryLock(lockKey)) {
if (!lock.isLocked()) {
return; // 获取锁失败,直接返回
}
// /user、/user/avatar
currentPathBuilder.append(StringConstants.SLASH).append(parentPathPart);
String currentPath = currentPathBuilder.toString();
// 文件夹和文件存储引擎需要一致
FileDO dirFile = baseMapper.lambdaQuery()
.eq(FileDO::getPath, currentPath)
.eq(FileDO::getType, FileTypeEnum.DIR)
.one();
if (dirFile != null) {
CheckUtils.throwIfNotEqual(dirFile.getStorageId(), storage.getId(), "文件夹和上传文件存储引擎不一致");
continue;
if (StrUtil.isBlank(parentPath) || StringConstants.SLASH.equals(parentPath)) {
return;
}
// user/avatar/ => user、avatar
String[] parentPathParts = StrUtil.split(parentPath, StringConstants.SLASH, false, true).toArray(String[]::new);
String lastPath = StringConstants.SLASH;
StringBuilder currentPathBuilder = new StringBuilder();
for (int i = 0; i < parentPathParts.length; i++) {
String parentPathPart = parentPathParts[i];
if (i > 0) {
lastPath = currentPathBuilder.toString();
}
// /user、/user/avatar
currentPathBuilder.append(StringConstants.SLASH).append(parentPathPart);
String currentPath = currentPathBuilder.toString();
// 文件夹和文件存储引擎需要一致
FileDO dirFile = baseMapper.lambdaQuery()
.eq(FileDO::getPath, currentPath)
.eq(FileDO::getType, FileTypeEnum.DIR)
.one();
if (dirFile != null) {
CheckUtils.throwIfNotEqual(dirFile.getStorageId(), storage.getId(), "文件夹和上传文件存储引擎不一致");
continue;
}
FileDO file = new FileDO();
file.setName(parentPathPart);
file.setOriginalName(parentPathPart);
file.setPath(currentPath);
file.setParentPath(lastPath);
file.setType(FileTypeEnum.DIR);
file.setStorageId(storage.getId());
baseMapper.insert(file);
}
FileDO file = new FileDO();
file.setName(parentPathPart);
file.setOriginalName(parentPathPart);
file.setPath(currentPath);
file.setParentPath(lastPath);
file.setType(FileTypeEnum.DIR);
file.setStorageId(storage.getId());
baseMapper.insert(file);
}
}
}