mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-09 20:57:21 +08:00
Revert "feat(system/config): 移除系统管理,新增存储配置"
This reverts commit 6d64f47d3e
.
This commit is contained in:
@@ -110,8 +110,7 @@ public class AccountLoginHandler extends AbstractLoginHandler<AccountLoginReq> {
|
||||
.getClientIP(request));
|
||||
int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name());
|
||||
Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0);
|
||||
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.getMsg()
|
||||
.formatted(lockMinutes));
|
||||
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes);
|
||||
// 登录成功清除计数
|
||||
if (!isError) {
|
||||
RedisUtils.delete(key);
|
||||
@@ -120,7 +119,6 @@ public class AccountLoginHandler extends AbstractLoginHandler<AccountLoginReq> {
|
||||
// 登录失败递增计数
|
||||
currentErrorCount++;
|
||||
RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes));
|
||||
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.getMsg()
|
||||
.formatted(maxErrorCount, lockMinutes));
|
||||
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", maxErrorCount, lockMinutes);
|
||||
}
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.config.file;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.EscapeUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.x.file.storage.core.FileInfo;
|
||||
import org.dromara.x.file.storage.core.recorder.FileRecorder;
|
||||
import org.dromara.x.file.storage.core.upload.FilePartInfo;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.continew.admin.common.context.UserContextHolder;
|
||||
import top.continew.admin.system.enums.FileTypeEnum;
|
||||
import top.continew.admin.system.mapper.FileMapper;
|
||||
import top.continew.admin.system.mapper.StorageMapper;
|
||||
import top.continew.admin.system.model.entity.FileDO;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 文件记录实现类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/24 22:31
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FileRecorderImpl implements FileRecorder {
|
||||
|
||||
private final FileMapper fileMapper;
|
||||
private final StorageMapper storageMapper;
|
||||
|
||||
@Override
|
||||
public boolean save(FileInfo fileInfo) {
|
||||
FileDO file = new FileDO();
|
||||
String originalFilename = EscapeUtil.unescape(fileInfo.getOriginalFilename());
|
||||
file.setName(StrUtil.contains(originalFilename, StringConstants.DOT)
|
||||
? StrUtil.subBefore(originalFilename, StringConstants.DOT, true)
|
||||
: originalFilename);
|
||||
file.setUrl(fileInfo.getUrl());
|
||||
file.setSize(fileInfo.getSize());
|
||||
file.setExtension(fileInfo.getExt());
|
||||
file.setType(FileTypeEnum.getByExtension(file.getExtension()));
|
||||
file.setThumbnailUrl(fileInfo.getThUrl());
|
||||
file.setThumbnailSize(fileInfo.getThSize());
|
||||
StorageDO storage = (StorageDO)fileInfo.getAttr().get(ClassUtil.getClassName(StorageDO.class, false));
|
||||
file.setStorageId(storage.getId());
|
||||
file.setCreateTime(DateUtil.toLocalDateTime(fileInfo.getCreateTime()));
|
||||
file.setUpdateUser(UserContextHolder.getUserId());
|
||||
file.setUpdateTime(file.getCreateTime());
|
||||
fileMapper.insert(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileInfo getByUrl(String url) {
|
||||
FileDO file = this.getFileByUrl(url);
|
||||
if (null == file) {
|
||||
return null;
|
||||
}
|
||||
StorageDO storageDO = storageMapper.lambdaQuery().eq(StorageDO::getId, file.getStorageId()).one();
|
||||
return file.toFileInfo(storageDO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String url) {
|
||||
FileDO file = this.getFileByUrl(url);
|
||||
return fileMapper.lambdaUpdate().eq(FileDO::getUrl, file.getUrl()).remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(FileInfo fileInfo) {
|
||||
/* 不使用分片功能则无需重写 */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveFilePart(FilePartInfo filePartInfo) {
|
||||
/* 不使用分片功能则无需重写 */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFilePartByUploadId(String s) {
|
||||
/* 不使用分片功能则无需重写 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 URL 查询文件
|
||||
*
|
||||
* @param url URL
|
||||
* @return 文件信息
|
||||
*/
|
||||
private FileDO getFileByUrl(String url) {
|
||||
Optional<FileDO> fileOptional = fileMapper.lambdaQuery().eq(FileDO::getUrl, url).oneOpt();
|
||||
return fileOptional.orElseGet(() -> fileMapper.lambdaQuery()
|
||||
.likeLeft(FileDO::getUrl, StrUtil.subAfter(url, StringConstants.SLASH, true))
|
||||
.oneOpt()
|
||||
.orElse(null));
|
||||
}
|
||||
}
|
@@ -16,24 +16,25 @@
|
||||
|
||||
package top.continew.admin.system.config.file;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.continew.admin.system.enums.OptionCategoryEnum;
|
||||
import top.continew.admin.system.mapper.FileMapper;
|
||||
import top.continew.admin.system.service.OptionService;
|
||||
import top.continew.starter.storage.dao.StorageDao;
|
||||
import top.continew.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.continew.admin.system.model.query.StorageQuery;
|
||||
import top.continew.admin.system.model.req.StorageReq;
|
||||
import top.continew.admin.system.model.resp.StorageResp;
|
||||
import top.continew.admin.system.service.StorageService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件存储配置加载器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author echo
|
||||
* @since 2023/12/24 22:31
|
||||
*/
|
||||
@Slf4j
|
||||
@@ -41,22 +42,16 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class FileStorageConfigLoader implements ApplicationRunner {
|
||||
|
||||
private final OptionService optionService;
|
||||
private final FileStorageInit fileStorageInit;
|
||||
private final StorageService storageService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
// 查询存储配置
|
||||
Map<String, String> map = optionService.getByCategory(OptionCategoryEnum.STORAGE);
|
||||
// 加载存储配置
|
||||
fileStorageInit.load(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储持久层接口本地实现类
|
||||
*/
|
||||
@Bean
|
||||
public StorageDao storageDao(FileMapper fileMapper) {
|
||||
return new StorageDaoImpl(fileMapper);
|
||||
StorageQuery query = new StorageQuery();
|
||||
query.setStatus(DisEnableStatusEnum.ENABLE);
|
||||
List<StorageResp> storageList = storageService.list(query, null);
|
||||
if (CollUtil.isEmpty(storageList)) {
|
||||
return;
|
||||
}
|
||||
storageList.forEach(s -> storageService.load(BeanUtil.copyProperties(s, StorageReq.class)));
|
||||
}
|
||||
}
|
||||
|
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.config.file;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.starter.cache.redisson.util.RedisUtils;
|
||||
import top.continew.starter.storage.client.LocalClient;
|
||||
import top.continew.starter.storage.client.OssClient;
|
||||
import top.continew.starter.storage.constant.StorageConstant;
|
||||
import top.continew.starter.storage.dao.StorageDao;
|
||||
import top.continew.starter.storage.manger.StorageManager;
|
||||
import top.continew.starter.storage.model.req.StorageProperties;
|
||||
import top.continew.starter.storage.strategy.LocalStorageStrategy;
|
||||
import top.continew.starter.storage.strategy.OssStorageStrategy;
|
||||
import top.continew.starter.storage.util.StorageUtils;
|
||||
import top.continew.starter.web.util.SpringWebUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 文件存储初始化
|
||||
*
|
||||
* @author echo
|
||||
* @author Charles7c
|
||||
* @since 2024/12/20 11:10
|
||||
*/
|
||||
@Component
|
||||
public class FileStorageInit {
|
||||
|
||||
/**
|
||||
* 加载文件存储
|
||||
*
|
||||
* @param map 存储配置
|
||||
*/
|
||||
public void load(Map<String, String> map) {
|
||||
StorageManager.unload(StorageTypeEnum.OSS.name());
|
||||
StorageManager.unload(StorageTypeEnum.LOCAL.name());
|
||||
// 获取默认存储值并缓存
|
||||
String storageDefault = cacheDefaultStorage(map);
|
||||
if (StorageTypeEnum.LOCAL.name().equals(storageDefault)) {
|
||||
// 获取本地终端地址 和桶地址
|
||||
String localEndpoint = map.get("STORAGE_LOCAL_ENDPOINT");
|
||||
String localBucket = map.get("STORAGE_LOCAL_BUCKET");
|
||||
// 构建并加载本地存储配置
|
||||
StorageProperties localProperties = buildStorageProperties(StorageTypeEnum.LOCAL
|
||||
.name(), localBucket, storageDefault, localEndpoint);
|
||||
// 本地静态资源映射
|
||||
SpringWebUtils.registerResourceHandler(MapUtil.of(StorageUtils.createUriWithProtocol(localEndpoint)
|
||||
.getPath(), localBucket));
|
||||
StorageManager.load(localProperties
|
||||
.getCode(), new LocalStorageStrategy(new LocalClient(localProperties), SpringUtil
|
||||
.getBean(StorageDao.class)));
|
||||
} else if (StorageTypeEnum.OSS.name().equals(storageDefault)) {
|
||||
// 构建并加载对象存储配置
|
||||
StorageProperties ossProperties = buildStorageProperties(StorageTypeEnum.OSS.name(), map
|
||||
.get("STORAGE_OSS_BUCKET"), storageDefault, map.get("STORAGE_OSS_ACCESS_KEY"), map
|
||||
.get("STORAGE_OSS_SECRET_KEY"), map.get("STORAGE_OSS_ENDPOINT"), map.get("STORAGE_OSS_REGION"));
|
||||
StorageManager.load(ossProperties.getCode(), new OssStorageStrategy(new OssClient(ossProperties), SpringUtil
|
||||
.getBean(StorageDao.class)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载文件存储
|
||||
*
|
||||
* @param code 存储编码
|
||||
*/
|
||||
public void unLoad(String code) {
|
||||
StorageManager.unload(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认存储值放入缓存
|
||||
*
|
||||
* @param map 存储配置
|
||||
* @return {@link String }
|
||||
*/
|
||||
private String cacheDefaultStorage(Map<String, String> map) {
|
||||
String storageDefault = MapUtil.getStr(map, "STORAGE_DEFAULT");
|
||||
RedisUtils.set(StorageConstant.DEFAULT_KEY, storageDefault);
|
||||
return storageDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建本地存储配置属性
|
||||
*
|
||||
* @param code 存储码
|
||||
* @param bucketName 桶名称
|
||||
* @param defaultCode 默认存储码
|
||||
* @return {@link StorageProperties }
|
||||
*/
|
||||
private StorageProperties buildStorageProperties(String code,
|
||||
String bucketName,
|
||||
String defaultCode,
|
||||
String endpoint) {
|
||||
StorageProperties properties = new StorageProperties();
|
||||
properties.setCode(code);
|
||||
properties.setBucketName(bucketName);
|
||||
properties.setEndpoint(endpoint);
|
||||
properties.setIsDefault(code.equals(defaultCode));
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建对象存储配置属性
|
||||
*
|
||||
* @param code 存储码
|
||||
* @param bucketName 桶名称
|
||||
* @param defaultCode 默认存储码
|
||||
* @param accessKey 访问密钥
|
||||
* @param secretKey 秘密密钥
|
||||
* @param endpoint 端点
|
||||
* @param region 区域
|
||||
* @return {@link StorageProperties }
|
||||
*/
|
||||
private StorageProperties buildStorageProperties(String code,
|
||||
String bucketName,
|
||||
String defaultCode,
|
||||
String accessKey,
|
||||
String secretKey,
|
||||
String endpoint,
|
||||
String region) {
|
||||
StorageProperties properties = buildStorageProperties(code, bucketName, defaultCode, endpoint);
|
||||
properties.setAccessKey(accessKey);
|
||||
properties.setSecretKey(secretKey);
|
||||
properties.setRegion(region);
|
||||
return properties;
|
||||
}
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.config.file;
|
||||
|
||||
import cn.hutool.core.util.EscapeUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.continew.admin.common.context.UserContextHolder;
|
||||
import top.continew.admin.system.enums.FileTypeEnum;
|
||||
import top.continew.admin.system.mapper.FileMapper;
|
||||
import top.continew.admin.system.model.entity.FileDO;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.storage.dao.StorageDao;
|
||||
import top.continew.starter.storage.model.resp.UploadResp;
|
||||
|
||||
/**
|
||||
* 存储持久层接口本地实现类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author echo
|
||||
* @since 2023/12/24 22:31
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class StorageDaoImpl implements StorageDao {
|
||||
|
||||
private final FileMapper fileMapper;
|
||||
|
||||
@Override
|
||||
public void add(UploadResp uploadResp) {
|
||||
FileDO file = new FileDO();
|
||||
file.setStorageCode(uploadResp.getCode());
|
||||
String originalFilename = EscapeUtil.unescape(uploadResp.getOriginalFilename());
|
||||
file.setName(StrUtil.contains(originalFilename, StringConstants.DOT)
|
||||
? StrUtil.subBefore(originalFilename, StringConstants.DOT, true)
|
||||
: originalFilename);
|
||||
file.setUrl(uploadResp.getUrl());
|
||||
file.setPath(uploadResp.getBasePath());
|
||||
file.setSize(uploadResp.getSize());
|
||||
file.setThumbnailUrl(uploadResp.getThumbnailUrl());
|
||||
file.setThumbnailSize(uploadResp.getThumbnailSize());
|
||||
file.setExtension(uploadResp.getExt());
|
||||
file.setType(FileTypeEnum.getByExtension(file.getExtension()));
|
||||
file.setETag(uploadResp.geteTag());
|
||||
file.setBucketName(uploadResp.getBucketName());
|
||||
file.setCreateTime(uploadResp.getCreateTime());
|
||||
file.setUpdateUser(UserContextHolder.getUserId());
|
||||
file.setUpdateTime(file.getCreateTime());
|
||||
fileMapper.insert(file);
|
||||
}
|
||||
}
|
@@ -43,9 +43,4 @@ public enum OptionCategoryEnum {
|
||||
* 登录配置
|
||||
*/
|
||||
LOGIN,
|
||||
|
||||
/**
|
||||
* 存储配置
|
||||
*/
|
||||
STORAGE,;
|
||||
}
|
||||
|
@@ -45,14 +45,14 @@ import java.util.Map;
|
||||
public enum PasswordPolicyEnum {
|
||||
|
||||
/**
|
||||
* 密码错误锁定阈值
|
||||
* 登录密码错误锁定账号的次数
|
||||
*/
|
||||
PASSWORD_ERROR_LOCK_COUNT("密码错误锁定阈值取值范围为 %d-%d", SysConstants.NO, 10, "密码错误已达 %d 次,账号锁定 %d 分钟"),
|
||||
PASSWORD_ERROR_LOCK_COUNT("登录密码错误锁定账号的次数取值范围为 %d-%d", SysConstants.NO, 10, null),
|
||||
|
||||
/**
|
||||
* 账号锁定时长(分钟)
|
||||
* 登录密码错误锁定账号的时间(min)
|
||||
*/
|
||||
PASSWORD_ERROR_LOCK_MINUTES("账号锁定时长取值范围为 %d-%d 分钟", 1, 1440, "账号锁定 %d 分钟,请稍后再试"),
|
||||
PASSWORD_ERROR_LOCK_MINUTES("登录密码错误锁定账号的时间取值范围为 %d-%d 分钟", 1, 1440, null),
|
||||
|
||||
/**
|
||||
* 密码有效期(天)
|
||||
@@ -60,9 +60,9 @@ public enum PasswordPolicyEnum {
|
||||
PASSWORD_EXPIRATION_DAYS("密码有效期取值范围为 %d-%d 天", SysConstants.NO, 999, null),
|
||||
|
||||
/**
|
||||
* 密码到期提醒(天)
|
||||
* 密码到期提前提示(天)
|
||||
*/
|
||||
PASSWORD_EXPIRATION_WARNING_DAYS("密码到期提醒取值范围为 %d-%d 天", SysConstants.NO, 998, null) {
|
||||
PASSWORD_EXPIRATION_WARNING_DAYS("密码到期提前提示取值范围为 %d-%d 天", SysConstants.NO, 998, null) {
|
||||
@Override
|
||||
public void validateRange(int value, Map<String, String> policyMap) {
|
||||
if (CollUtil.isEmpty(policyMap)) {
|
||||
@@ -73,7 +73,7 @@ public enum PasswordPolicyEnum {
|
||||
.get(PASSWORD_EXPIRATION_DAYS.name())), SpringUtil.getBean(OptionService.class)
|
||||
.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name()));
|
||||
if (passwordExpirationDays > SysConstants.NO) {
|
||||
ValidationUtils.throwIf(value >= passwordExpirationDays, "密码到期提醒时间应小于密码有效期");
|
||||
ValidationUtils.throwIf(value >= passwordExpirationDays, "密码到期前的提示时间应小于密码有效期");
|
||||
return;
|
||||
}
|
||||
super.validateRange(value, policyMap);
|
||||
@@ -113,9 +113,9 @@ public enum PasswordPolicyEnum {
|
||||
},
|
||||
|
||||
/**
|
||||
* 密码是否允许包含用户名
|
||||
* 密码是否允许包含正反序账号名
|
||||
*/
|
||||
PASSWORD_ALLOW_CONTAIN_USERNAME("密码是否允许包含用户名取值只能为是(%d)或否(%d)", SysConstants.NO, SysConstants.YES, "密码不允许包含正反序用户名") {
|
||||
PASSWORD_ALLOW_CONTAIN_USERNAME("密码是否允许包含正反序账号名取值只能为是(%d)或否(%d)", SysConstants.NO, SysConstants.YES, "密码不允许包含正反序账号名") {
|
||||
@Override
|
||||
public void validateRange(int value, Map<String, String> policyMap) {
|
||||
ValidationUtils.throwIf(value != SysConstants.YES && value != SysConstants.NO, this.getDescription()
|
||||
@@ -133,9 +133,9 @@ public enum PasswordPolicyEnum {
|
||||
},
|
||||
|
||||
/**
|
||||
* 历史密码重复校验次数
|
||||
* 密码重复使用次数
|
||||
*/
|
||||
PASSWORD_REPETITION_TIMES("历史密码重复校验次数取值范围为 %d-%d", 3, 32, "新密码不得与历史前 %d 次密码重复") {
|
||||
PASSWORD_REPETITION_TIMES("密码重复使用规则取值范围为 %d-%d", 3, 32, "新密码不得与历史前 %d 次密码重复") {
|
||||
@Override
|
||||
public void validate(String password, int value, UserDO user) {
|
||||
UserPasswordHistoryService userPasswordHistoryService = SpringUtil
|
||||
|
@@ -31,9 +31,9 @@ import top.continew.starter.core.enums.BaseEnum;
|
||||
public enum StorageTypeEnum implements BaseEnum<Integer> {
|
||||
|
||||
/**
|
||||
* 对象存储
|
||||
* 兼容S3协议存储
|
||||
*/
|
||||
OSS(1, "对象存储"),
|
||||
S3(1, "兼容S3协议存储"),
|
||||
|
||||
/**
|
||||
* 本地存储
|
||||
|
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.mapper;
|
||||
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.starter.data.mp.base.BaseMapper;
|
||||
|
||||
/**
|
||||
* 存储 Mapper
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/26 22:09
|
||||
*/
|
||||
public interface StorageMapper extends BaseMapper<StorageDO> {
|
||||
}
|
@@ -16,12 +16,19 @@
|
||||
|
||||
package top.continew.admin.system.model.entity;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.SneakyThrows;
|
||||
import org.dromara.x.file.storage.core.FileInfo;
|
||||
import top.continew.admin.system.enums.FileTypeEnum;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.util.StrUtils;
|
||||
import top.continew.admin.common.model.entity.BaseDO;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* 文件实体
|
||||
@@ -72,23 +79,64 @@ public class FileDO extends BaseDO {
|
||||
private String thumbnailUrl;
|
||||
|
||||
/**
|
||||
* 存储code
|
||||
* 存储 ID
|
||||
*/
|
||||
private String storageCode;
|
||||
private Long storageId;
|
||||
|
||||
/**
|
||||
* 基础路径
|
||||
* 转换为 X-File-Storage 文件信息对象
|
||||
*
|
||||
* @param storageDO 存储桶信息
|
||||
* @return X-File-Storage 文件信息对象
|
||||
*/
|
||||
private String path;
|
||||
public FileInfo toFileInfo(StorageDO storageDO) {
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setUrl(this.url);
|
||||
fileInfo.setSize(this.size);
|
||||
fileInfo.setFilename(StrUtil.contains(this.url, StringConstants.SLASH)
|
||||
? StrUtil.subAfter(this.url, StringConstants.SLASH, true)
|
||||
: this.url);
|
||||
fileInfo.setOriginalFilename(StrUtils
|
||||
.blankToDefault(this.extension, this.name, ex -> this.name + StringConstants.DOT + ex));
|
||||
fileInfo.setBasePath(StringConstants.EMPTY);
|
||||
// 优化 path 处理
|
||||
fileInfo.setPath(extractRelativePath(this.url, storageDO));
|
||||
|
||||
fileInfo.setExt(this.extension);
|
||||
fileInfo.setPlatform(storageDO.getCode());
|
||||
fileInfo.setThUrl(this.thumbnailUrl);
|
||||
fileInfo.setThFilename(StrUtil.contains(this.thumbnailUrl, StringConstants.SLASH)
|
||||
? StrUtil.subAfter(this.thumbnailUrl, StringConstants.SLASH, true)
|
||||
: this.thumbnailUrl);
|
||||
fileInfo.setThSize(this.thumbnailSize);
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储桶
|
||||
* 将文件路径处理成资源路径
|
||||
* 例如:
|
||||
* http://domain.cn/bucketName/2024/11/27/6746ec3b2907f0de80afdd70.png => 2024/11/27/
|
||||
* http://bucketName.domain.cn/2024/11/27/6746ec3b2907f0de80afdd70.png => 2024/11/27/
|
||||
*
|
||||
* @param url 文件路径
|
||||
* @param storageDO 存储桶信息
|
||||
* @return
|
||||
*/
|
||||
private String bucketName;
|
||||
|
||||
/**
|
||||
* 文件标识
|
||||
*/
|
||||
private String eTag;
|
||||
@SneakyThrows
|
||||
private static String extractRelativePath(String url, StorageDO storageDO) {
|
||||
url = StrUtil.subBefore(url, StringConstants.SLASH, true) + StringConstants.SLASH;
|
||||
if (storageDO.getType().equals(StorageTypeEnum.LOCAL)) {
|
||||
return url;
|
||||
}
|
||||
// 提取 URL 中的路径部分
|
||||
String fullPath = new URL(url).getPath();
|
||||
// 移除开头的斜杠
|
||||
String relativePath = fullPath.startsWith(StringConstants.SLASH) ? fullPath.substring(1) : fullPath;
|
||||
// 如果路径以 bucketName 开头,则移除 bucketName 例如: bucketName/2024/11/27/ -> 2024/11/27/
|
||||
if (relativePath.startsWith(storageDO.getBucketName())) {
|
||||
return StrUtil.split(relativePath, storageDO.getBucketName()).get(1);
|
||||
}
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import top.continew.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.admin.common.model.entity.BaseDO;
|
||||
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 存储实体
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/26 22:09
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_storage")
|
||||
public class StorageDO extends BaseDO {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private StorageTypeEnum type;
|
||||
|
||||
/**
|
||||
* Access Key(访问密钥)
|
||||
*/
|
||||
@FieldEncrypt
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* Secret Key(私有密钥)
|
||||
*/
|
||||
@FieldEncrypt
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* Endpoint(终端节点)
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* 桶名称
|
||||
*/
|
||||
private String bucketName;
|
||||
|
||||
/**
|
||||
* 域名
|
||||
*/
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 是否为默认存储
|
||||
*/
|
||||
private Boolean isDefault;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private DisEnableStatusEnum status;
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.query;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import top.continew.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.continew.starter.data.core.annotation.Query;
|
||||
import top.continew.starter.data.core.enums.QueryType;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 存储查询条件
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/26 22:09
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "存储查询条件")
|
||||
public class StorageQuery implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 关键词
|
||||
*/
|
||||
@Schema(description = "关键词", example = "本地存储")
|
||||
@Query(columns = {"name", "code", "description"}, type = QueryType.LIKE)
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@Schema(description = "状态", example = "1")
|
||||
private DisEnableStatusEnum status;
|
||||
}
|
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import top.continew.admin.common.constant.RegexConstants;
|
||||
import top.continew.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.admin.system.validation.ValidationGroup;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 存储请求参数
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/26 22:09
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "存储请求参数")
|
||||
public class StorageReq implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
@Schema(description = "名称", example = "存储1")
|
||||
@NotBlank(message = "名称不能为空")
|
||||
@Length(max = 100, message = "名称长度不能超过 {max} 个字符")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
@Schema(description = "编码", example = "local")
|
||||
@NotBlank(message = "编码不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@Schema(description = "类型", example = "2")
|
||||
@NotNull(message = "类型非法")
|
||||
private StorageTypeEnum type;
|
||||
|
||||
/**
|
||||
* 访问密钥
|
||||
*/
|
||||
@Schema(description = "访问密钥", example = "")
|
||||
@Length(max = 255, message = "访问密钥长度不能超过 {max} 个字符")
|
||||
@NotBlank(message = "访问密钥不能为空", groups = ValidationGroup.Storage.S3.class)
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* 私有密钥
|
||||
*/
|
||||
@Schema(description = "私有密钥", example = "")
|
||||
@NotBlank(message = "私有密钥不能为空", groups = ValidationGroup.Storage.S3.class)
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 终端节点
|
||||
*/
|
||||
@Schema(description = "终端节点", example = "")
|
||||
@Length(max = 255, message = "终端节点长度不能超过 {max} 个字符")
|
||||
@NotBlank(message = "终端节点不能为空", groups = ValidationGroup.Storage.S3.class)
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* 桶名称
|
||||
*/
|
||||
@Schema(description = "桶名称", example = "C:/continew-admin/data/file/")
|
||||
@Length(max = 255, message = "桶名称长度不能超过 {max} 个字符")
|
||||
@NotBlank(message = "桶名称不能为空", groups = ValidationGroup.Storage.S3.class)
|
||||
@NotBlank(message = "存储路径不能为空", groups = ValidationGroup.Storage.Local.class)
|
||||
private String bucketName;
|
||||
|
||||
/**
|
||||
* 域名
|
||||
*/
|
||||
@Schema(description = "域名", example = "http://localhost:8000/file")
|
||||
@Length(max = 255, message = "域名长度不能超过 {max} 个字符")
|
||||
@NotBlank(message = "域名不能为空")
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
@Schema(description = "排序", example = "1")
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Schema(description = "描述", example = "存储描述")
|
||||
@Length(max = 200, message = "描述长度不能超过 {max} 个字符")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 是否为默认存储
|
||||
*/
|
||||
@Schema(description = "是否为默认存储", example = "true")
|
||||
@NotNull(message = "是否为默认存储不能为空")
|
||||
private Boolean isDefault;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@Schema(description = "状态", example = "1")
|
||||
private DisEnableStatusEnum status;
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import top.continew.admin.common.model.resp.BaseDetailResp;
|
||||
import top.continew.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.starter.security.mask.annotation.JsonMask;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 存储响应信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/26 22:09
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "存储响应信息")
|
||||
public class StorageResp extends BaseDetailResp {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
@Schema(description = "名称", example = "存储1")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
@Schema(description = "编码", example = "local")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@Schema(description = "状态", example = "1")
|
||||
private DisEnableStatusEnum status;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@Schema(description = "类型", example = "2")
|
||||
private StorageTypeEnum type;
|
||||
|
||||
/**
|
||||
* 访问密钥
|
||||
*/
|
||||
@Schema(description = "访问密钥", example = "")
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* 私有密钥
|
||||
*/
|
||||
@Schema(description = "私有密钥", example = "")
|
||||
@JsonMask(left = 4, right = 3)
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 终端节点
|
||||
*/
|
||||
@Schema(description = "终端节点", example = "")
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* 桶名称
|
||||
*/
|
||||
@Schema(description = "桶名称", example = "C:/continew-admin/data/file/")
|
||||
private String bucketName;
|
||||
|
||||
/**
|
||||
* 域名
|
||||
*/
|
||||
@Schema(description = "域名", example = "http://localhost:8000/file")
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Schema(description = "描述", example = "存储描述")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 是否为默认存储
|
||||
*/
|
||||
@Schema(description = "是否为默认存储", example = "true")
|
||||
private Boolean isDefault;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
@Schema(description = "排序", example = "1")
|
||||
private Integer sort;
|
||||
|
||||
@Override
|
||||
public Boolean getDisabled() {
|
||||
return this.getIsDefault();
|
||||
}
|
||||
|
||||
}
|
@@ -16,13 +16,13 @@
|
||||
|
||||
package top.continew.admin.system.service;
|
||||
|
||||
import org.dromara.x.file.storage.core.FileInfo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.continew.admin.system.model.entity.FileDO;
|
||||
import top.continew.admin.system.model.query.FileQuery;
|
||||
import top.continew.admin.system.model.req.FileReq;
|
||||
import top.continew.admin.system.model.resp.FileResp;
|
||||
import top.continew.admin.system.model.resp.FileStatisticsResp;
|
||||
import top.continew.admin.system.model.resp.FileUploadResp;
|
||||
import top.continew.starter.data.mp.service.IService;
|
||||
import top.continew.starter.extension.crud.service.BaseService;
|
||||
|
||||
@@ -42,7 +42,7 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
|
||||
* @param file 文件信息
|
||||
* @return 文件信息
|
||||
*/
|
||||
default FileUploadResp upload(MultipartFile file) {
|
||||
default FileInfo upload(MultipartFile file) {
|
||||
return upload(file, null);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
|
||||
* @param storageCode 存储编码
|
||||
* @return 文件信息
|
||||
*/
|
||||
FileUploadResp upload(MultipartFile file, String storageCode);
|
||||
FileInfo upload(MultipartFile file, String storageCode);
|
||||
|
||||
/**
|
||||
* 根据存储 ID 列表查询
|
||||
|
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.service;
|
||||
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.admin.system.model.query.StorageQuery;
|
||||
import top.continew.admin.system.model.req.StorageReq;
|
||||
import top.continew.admin.system.model.resp.StorageResp;
|
||||
import top.continew.starter.data.mp.service.IService;
|
||||
import top.continew.starter.extension.crud.service.BaseService;
|
||||
|
||||
/**
|
||||
* 存储业务接口
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/26 22:09
|
||||
*/
|
||||
public interface StorageService extends BaseService<StorageResp, StorageResp, StorageQuery, StorageReq>, IService<StorageDO> {
|
||||
|
||||
/**
|
||||
* 查询默认存储
|
||||
*
|
||||
* @return 存储信息
|
||||
*/
|
||||
StorageDO getDefaultStorage();
|
||||
|
||||
/**
|
||||
* 根据编码查询
|
||||
*
|
||||
* @param code 编码
|
||||
* @return 存储信息
|
||||
*/
|
||||
StorageDO getByCode(String code);
|
||||
|
||||
/**
|
||||
* 加载存储
|
||||
*
|
||||
* @param req 存储信息
|
||||
*/
|
||||
void load(StorageReq req);
|
||||
|
||||
/**
|
||||
* 卸载存储
|
||||
*
|
||||
* @param req 存储信息
|
||||
*/
|
||||
void unload(StorageReq req);
|
||||
}
|
@@ -17,27 +17,39 @@
|
||||
package top.continew.admin.system.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.x.file.storage.core.FileInfo;
|
||||
import org.dromara.x.file.storage.core.FileStorageService;
|
||||
import org.dromara.x.file.storage.core.ProgressListener;
|
||||
import org.dromara.x.file.storage.core.upload.UploadPretreatment;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.continew.admin.system.enums.FileTypeEnum;
|
||||
import top.continew.admin.system.mapper.FileMapper;
|
||||
import top.continew.admin.system.model.entity.FileDO;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.admin.system.model.query.FileQuery;
|
||||
import top.continew.admin.system.model.req.FileReq;
|
||||
import top.continew.admin.system.model.resp.FileResp;
|
||||
import top.continew.admin.system.model.resp.FileStatisticsResp;
|
||||
import top.continew.admin.system.model.resp.FileUploadResp;
|
||||
import top.continew.admin.system.service.FileService;
|
||||
import top.continew.starter.core.exception.BusinessException;
|
||||
import top.continew.admin.system.service.StorageService;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.util.StrUtils;
|
||||
import top.continew.starter.core.util.URLUtils;
|
||||
import top.continew.starter.core.validation.CheckUtils;
|
||||
import top.continew.starter.extension.crud.service.BaseServiceImpl;
|
||||
import top.continew.starter.storage.manger.StorageManager;
|
||||
import top.continew.starter.storage.model.resp.UploadResp;
|
||||
import top.continew.starter.storage.strategy.StorageStrategy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 文件业务实现
|
||||
@@ -50,31 +62,65 @@ import java.util.List;
|
||||
@RequiredArgsConstructor
|
||||
public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileResp, FileResp, FileQuery, FileReq> implements FileService {
|
||||
|
||||
private final FileStorageService fileStorageService;
|
||||
@Resource
|
||||
private StorageService storageService;
|
||||
|
||||
@Override
|
||||
protected void beforeDelete(List<Long> ids) {
|
||||
List<FileDO> fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list();
|
||||
fileList.forEach(file -> {
|
||||
StorageStrategy<?> instance = StorageManager.instance(file.getStorageCode());
|
||||
instance.delete(file.getBucketName(), file.getPath());
|
||||
});
|
||||
Map<Long, List<FileDO>> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId));
|
||||
for (Map.Entry<Long, List<FileDO>> entry : fileListGroup.entrySet()) {
|
||||
StorageDO storage = storageService.getById(entry.getKey());
|
||||
for (FileDO file : entry.getValue()) {
|
||||
FileInfo fileInfo = file.toFileInfo(storage);
|
||||
fileStorageService.delete(fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileUploadResp upload(MultipartFile file, String storageCode) {
|
||||
StorageStrategy<?> instance;
|
||||
public FileInfo upload(MultipartFile file, String storageCode) {
|
||||
StorageDO storage;
|
||||
if (StrUtil.isBlank(storageCode)) {
|
||||
instance = StorageManager.instance();
|
||||
storage = storageService.getDefaultStorage();
|
||||
CheckUtils.throwIfNull(storage, "请先指定默认存储");
|
||||
} else {
|
||||
instance = StorageManager.instance(storageCode);
|
||||
storage = storageService.getByCode(storageCode);
|
||||
CheckUtils.throwIfNotExists(storage, "StorageDO", "Code", storageCode);
|
||||
}
|
||||
UploadResp uploadResp;
|
||||
try {
|
||||
uploadResp = instance.upload(file.getOriginalFilename(), null, file.getInputStream(), file
|
||||
.getContentType(), true);
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("文件上传失败", e);
|
||||
LocalDate today = LocalDate.now();
|
||||
String path = today.getYear() + StringConstants.SLASH + today.getMonthValue() + StringConstants.SLASH + today
|
||||
.getDayOfMonth() + StringConstants.SLASH;
|
||||
UploadPretreatment uploadPretreatment = fileStorageService.of(file)
|
||||
.setPlatform(storage.getCode())
|
||||
.putAttr(ClassUtil.getClassName(StorageDO.class, false), storage)
|
||||
.setPath(path);
|
||||
// 图片文件生成缩略图
|
||||
if (FileTypeEnum.IMAGE.getExtensions().contains(FileNameUtil.extName(file.getOriginalFilename()))) {
|
||||
uploadPretreatment.thumbnail(img -> img.size(100, 100));
|
||||
}
|
||||
return FileUploadResp.builder().url(uploadResp.getUrl()).build();
|
||||
uploadPretreatment.setProgressMonitor(new ProgressListener() {
|
||||
@Override
|
||||
public void start() {
|
||||
log.info("开始上传");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void progress(long progressSize, Long allSize) {
|
||||
log.info("已上传 [{}],总大小 [{}]", progressSize, allSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
log.info("上传结束");
|
||||
}
|
||||
});
|
||||
// 处理本地存储文件 URL
|
||||
FileInfo fileInfo = uploadPretreatment.upload();
|
||||
String domain = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH);
|
||||
fileInfo.setUrl(URLUtil.normalize(domain + fileInfo.getPath() + fileInfo.getFilename()));
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,7 +128,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
|
||||
if (CollUtil.isEmpty(storageIds)) {
|
||||
return 0L;
|
||||
}
|
||||
return baseMapper.lambdaQuery().in(FileDO::getStorageCode, storageIds).count();
|
||||
return baseMapper.lambdaQuery().in(FileDO::getStorageId, storageIds).count();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,4 +143,19 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
|
||||
resp.setNumber(statisticsList.stream().mapToLong(FileStatisticsResp::getNumber).sum());
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fill(Object obj) {
|
||||
super.fill(obj);
|
||||
if (obj instanceof FileResp fileResp && !URLUtils.isHttpUrl(fileResp.getUrl())) {
|
||||
StorageDO storage = storageService.getById(fileResp.getStorageId());
|
||||
String prefix = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH);
|
||||
String url = URLUtil.normalize(prefix + fileResp.getUrl());
|
||||
fileResp.setUrl(url);
|
||||
String thumbnailUrl = StrUtils.blankToDefault(fileResp.getThumbnailUrl(), url, thUrl -> URLUtil
|
||||
.normalize(prefix + thUrl));
|
||||
fileResp.setThumbnailUrl(thumbnailUrl);
|
||||
fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode()));
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,7 +26,6 @@ import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWra
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import top.continew.admin.common.constant.CacheConstants;
|
||||
import top.continew.admin.system.config.file.FileStorageInit;
|
||||
import top.continew.admin.system.enums.OptionCategoryEnum;
|
||||
import top.continew.admin.system.enums.PasswordPolicyEnum;
|
||||
import top.continew.admin.system.mapper.OptionMapper;
|
||||
@@ -58,7 +57,6 @@ import java.util.stream.Collectors;
|
||||
public class OptionServiceImpl implements OptionService {
|
||||
|
||||
private final OptionMapper baseMapper;
|
||||
private final FileStorageInit fileStorageInit;
|
||||
|
||||
@Override
|
||||
public List<OptionResp> list(OptionQuery query) {
|
||||
@@ -100,7 +98,6 @@ public class OptionServiceImpl implements OptionService {
|
||||
PasswordPolicyEnum passwordPolicy = PasswordPolicyEnum.valueOf(code);
|
||||
passwordPolicy.validateRange(Integer.parseInt(value), passwordPolicyOptionMap);
|
||||
}
|
||||
storageReload(options);
|
||||
RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK);
|
||||
baseMapper.updateById(BeanUtil.copyToList(options, OptionDO.class));
|
||||
}
|
||||
@@ -141,18 +138,4 @@ public class OptionServiceImpl implements OptionService {
|
||||
RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code, value);
|
||||
return mapper.apply(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储重新加载
|
||||
*
|
||||
* @param options 选项
|
||||
*/
|
||||
private void storageReload(List<OptionReq> options) {
|
||||
Map<String, String> storage = options.stream()
|
||||
.filter(option -> option.getCode() != null && option.getCode().startsWith("STORAGE_"))
|
||||
.collect(Collectors.toMap(OptionReq::getCode, OptionReq::getValue));
|
||||
if (ObjectUtil.isNotEmpty(storage)) {
|
||||
fileStorageInit.load(storage);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.x.file.storage.core.FileStorageProperties;
|
||||
import org.dromara.x.file.storage.core.FileStorageService;
|
||||
import org.dromara.x.file.storage.core.FileStorageServiceBuilder;
|
||||
import org.dromara.x.file.storage.core.platform.FileStorage;
|
||||
import org.springframework.stereotype.Service;
|
||||
import top.continew.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.continew.admin.common.util.SecureUtils;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.admin.system.mapper.StorageMapper;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.admin.system.model.query.StorageQuery;
|
||||
import top.continew.admin.system.model.req.StorageReq;
|
||||
import top.continew.admin.system.model.resp.StorageResp;
|
||||
import top.continew.admin.system.service.FileService;
|
||||
import top.continew.admin.system.service.StorageService;
|
||||
import top.continew.admin.system.validation.ValidationGroup;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.util.ExceptionUtils;
|
||||
import top.continew.starter.core.util.URLUtils;
|
||||
import top.continew.starter.core.validation.CheckUtils;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
import top.continew.starter.extension.crud.service.BaseServiceImpl;
|
||||
import top.continew.starter.web.util.SpringWebUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* 存储业务实现
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/26 22:09
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO, StorageResp, StorageResp, StorageQuery, StorageReq> implements StorageService {
|
||||
|
||||
private final FileStorageService fileStorageService;
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
|
||||
@Override
|
||||
public void beforeAdd(StorageReq req) {
|
||||
this.decodeSecretKey(req, null);
|
||||
CheckUtils.throwIf(Boolean.TRUE.equals(req.getIsDefault()) && this.isDefaultExists(null), "请先取消原有默认存储");
|
||||
String code = req.getCode();
|
||||
CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code);
|
||||
this.load(req);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeUpdate(StorageReq req, Long id) {
|
||||
StorageDO oldStorage = super.getById(id);
|
||||
CheckUtils.throwIfNotEqual(req.getCode(), oldStorage.getCode(), "不允许修改存储编码");
|
||||
CheckUtils.throwIfNotEqual(req.getType(), oldStorage.getType(), "不允许修改存储类型");
|
||||
DisEnableStatusEnum newStatus = req.getStatus();
|
||||
CheckUtils.throwIf(Boolean.TRUE.equals(oldStorage.getIsDefault()) && DisEnableStatusEnum.DISABLE
|
||||
.equals(newStatus), "[{}] 是默认存储,不允许禁用", oldStorage.getName());
|
||||
this.decodeSecretKey(req, oldStorage);
|
||||
DisEnableStatusEnum oldStatus = oldStorage.getStatus();
|
||||
if (Boolean.TRUE.equals(req.getIsDefault())) {
|
||||
CheckUtils.throwIf(this.isDefaultExists(id), "请先取消原有默认存储");
|
||||
CheckUtils.throwIf(!DisEnableStatusEnum.ENABLE.equals(oldStatus) && !DisEnableStatusEnum.ENABLE
|
||||
.equals(newStatus), "请先启用该存储");
|
||||
}
|
||||
// 先卸载
|
||||
if (DisEnableStatusEnum.ENABLE.equals(oldStatus)) {
|
||||
this.unload(BeanUtil.copyProperties(oldStorage, StorageReq.class));
|
||||
}
|
||||
// 再加载
|
||||
if (DisEnableStatusEnum.ENABLE.equals(newStatus)) {
|
||||
this.load(req);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeDelete(List<Long> ids) {
|
||||
CheckUtils.throwIf(fileService.countByStorageIds(ids) > 0, "所选存储存在文件关联,请删除文件后重试");
|
||||
List<StorageDO> storageList = baseMapper.lambdaQuery().in(StorageDO::getId, ids).list();
|
||||
storageList.forEach(s -> {
|
||||
CheckUtils.throwIfEqual(Boolean.TRUE, s.getIsDefault(), "[{}] 是默认存储,不允许禁用", s.getName());
|
||||
// 卸载启用状态的存储
|
||||
if (DisEnableStatusEnum.ENABLE.equals(s.getStatus())) {
|
||||
this.unload(BeanUtil.copyProperties(s, StorageReq.class));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageDO getDefaultStorage() {
|
||||
return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).one();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageDO getByCode(String code) {
|
||||
return baseMapper.lambdaQuery().eq(StorageDO::getCode, code).one();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(StorageReq req) {
|
||||
CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList();
|
||||
String domain = req.getDomain();
|
||||
ValidationUtils.throwIf(!URLUtils.isHttpUrl(domain), "域名格式错误");
|
||||
String bucketName = req.getBucketName();
|
||||
StorageTypeEnum type = req.getType();
|
||||
if (StorageTypeEnum.LOCAL.equals(type)) {
|
||||
ValidationUtils.validate(req, ValidationGroup.Storage.Local.class);
|
||||
req.setBucketName(StrUtil.appendIfMissing(bucketName
|
||||
.replace(StringConstants.BACKSLASH, StringConstants.SLASH), StringConstants.SLASH));
|
||||
FileStorageProperties.LocalPlusConfig config = new FileStorageProperties.LocalPlusConfig();
|
||||
config.setPlatform(req.getCode());
|
||||
config.setStoragePath(bucketName);
|
||||
fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections
|
||||
.singletonList(config)));
|
||||
SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), bucketName));
|
||||
} else if (StorageTypeEnum.S3.equals(type)) {
|
||||
ValidationUtils.validate(req, ValidationGroup.Storage.S3.class);
|
||||
FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config();
|
||||
config.setPlatform(req.getCode());
|
||||
config.setAccessKey(req.getAccessKey());
|
||||
config.setSecretKey(req.getSecretKey());
|
||||
config.setEndPoint(req.getEndpoint());
|
||||
config.setBucketName(bucketName);
|
||||
config.setDomain(domain);
|
||||
fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections
|
||||
.singletonList(config), null));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload(StorageReq req) {
|
||||
CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList();
|
||||
FileStorage fileStorage = fileStorageService.getFileStorage(req.getCode());
|
||||
fileStorageList.remove(fileStorage);
|
||||
fileStorage.close();
|
||||
SpringWebUtils.deRegisterResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), req
|
||||
.getBucketName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密 SecretKey
|
||||
*
|
||||
* @param req 请求参数
|
||||
* @param storage 存储信息
|
||||
*/
|
||||
private void decodeSecretKey(StorageReq req, StorageDO storage) {
|
||||
if (!StorageTypeEnum.S3.equals(req.getType())) {
|
||||
return;
|
||||
}
|
||||
// 修改时,如果 SecretKey 不修改,需要手动修正
|
||||
String newSecretKey = req.getSecretKey();
|
||||
boolean isSecretKeyNotUpdate = StrUtil.isBlank(newSecretKey) || newSecretKey.contains(StringConstants.ASTERISK);
|
||||
if (null != storage && isSecretKeyNotUpdate) {
|
||||
req.setSecretKey(storage.getSecretKey());
|
||||
return;
|
||||
}
|
||||
// 新增时或修改了 SecretKey
|
||||
String secretKey = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(newSecretKey));
|
||||
ValidationUtils.throwIfNull(secretKey, "私有密钥解密失败");
|
||||
ValidationUtils.throwIf(secretKey.length() > 255, "私有密钥长度不能超过 255 个字符");
|
||||
req.setSecretKey(secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认存储是否存在
|
||||
*
|
||||
* @param id ID
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean isDefaultExists(Long id) {
|
||||
return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).ne(null != id, StorageDO::getId, id).exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码是否存在
|
||||
*
|
||||
* @param code 编码
|
||||
* @param id ID
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean isCodeExists(String code, Long id) {
|
||||
return baseMapper.lambdaQuery().eq(StorageDO::getCode, code).ne(null != id, StorageDO::getId, id).exists();
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.validation;
|
||||
|
||||
import jakarta.validation.groups.Default;
|
||||
|
||||
/**
|
||||
* 分组校验
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2024/7/3 22:01
|
||||
*/
|
||||
public interface ValidationGroup extends Default {
|
||||
|
||||
/**
|
||||
* 分组校验-增删改查
|
||||
*/
|
||||
interface Storage extends ValidationGroup {
|
||||
/**
|
||||
* 本地存储
|
||||
*/
|
||||
interface Local extends Storage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容S3协议存储
|
||||
*/
|
||||
interface S3 extends Storage {
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user