Appearance
文件相关
说明
文件上传直接调用文件服务接口即可, 本地文件下载需要依赖nginx, 文件预览依赖第三方组件 kkFileView
名词解释
热更新:不重启项目,修改nacos中配置的参数后,实时生效
文件服务:本文档中提到的文件服务,你直接把他理解成acuity-base-server中文件相关的逻辑即可、或直接理解为acuity-base-server也行。
上传
文件服务目前支持本地存储、阿里云OSS、FastDFS存储、华为云OBS、MinIO、七牛云OSS,可以通过配置切换。完整的文件存储配置如下:
yaml
# acuity.file 相关的配置会注入到 FileServerProperties 对象,使用时只需要注入即可。
acuity:
file:
storageType: MIN_IO # 存储类型 支持 LOCAL FAST_DFS MIN_IO ALI_OSS HUAWEI_OSS QINIU_OSS
delFile: false # 调用删除接口是否删除文件, 设置为false只删除数据库记录
local: # 文件存储到服务器本机
storage-path: /data/projects/uploadfile/file/ # 文件存储到服务器的绝对路径(需要启动项目前创建好,并且需要有读和写的权限!)
endpoint: http://127.0.0.1/file/ # 外网文件访问前缀 (部署nginx后,配置为nginx的访问地址,并需要在nginx配置文件中静态代理storage-path)
inner-uri-prefix: null # 内网的url访问前缀
ali: # 文件存储到阿里云OSS
# 请填写自己的阿里云存储配置
uriPrefix: "http://acuity-admin-cloud.oss-cn-beijing.aliyuncs.com/"
bucket-name: "acuity-admin-cloud"
endpoint: "oss-cn-beijing.aliyuncs.com"
access-key-id: "填写你的id"
access-key-secret: "填写你的秘钥"
minIo: # 文件存储到MINIO
endpoint: "https://127.0.0.1:9000/"
accessKey: "acuity"
secretKey: "acuity"
bucket: "dev"
huawei: # 文件存储到华为云OBS
uriPrefix: "dev.obs.cn-southwest-2.myhuaweicloud.com"
endpoint: "obs.cn-southwest-2.myhuaweicloud.com"
accessKey: "1"
secretKey: "2"
location: "cn-southwest-2"
bucket: "dev"
qiNiu: # 文件存储到七牛云OSS
zone: "z0"
accessKey: "1"
secretKey: "2"
bucket: "acuity_admin_cloud"
# acuity.file.storageType=FAST_DFS 时,需要配置
fdfs:
soTimeout: 1500
connectTimeout: 600
thumb-image:
width: 150
height: 150
tracker-list:
- 192.168.1.2:22122
pool:
#从池中借出的对象的最大数目
max-total: 153
max-wait-millis: 102
jmx-name-base: 1
jmx-name-prefix: 1
# acuity.file 相关的配置会注入到 FileServerProperties 对象,使用时只需要注入即可。
acuity:
file:
storageType: MIN_IO # 存储类型 支持 LOCAL FAST_DFS MIN_IO ALI_OSS HUAWEI_OSS QINIU_OSS
delFile: false # 调用删除接口是否删除文件, 设置为false只删除数据库记录
local: # 文件存储到服务器本机
storage-path: /data/projects/uploadfile/file/ # 文件存储到服务器的绝对路径(需要启动项目前创建好,并且需要有读和写的权限!)
endpoint: http://127.0.0.1/file/ # 外网文件访问前缀 (部署nginx后,配置为nginx的访问地址,并需要在nginx配置文件中静态代理storage-path)
inner-uri-prefix: null # 内网的url访问前缀
ali: # 文件存储到阿里云OSS
# 请填写自己的阿里云存储配置
uriPrefix: "http://acuity-admin-cloud.oss-cn-beijing.aliyuncs.com/"
bucket-name: "acuity-admin-cloud"
endpoint: "oss-cn-beijing.aliyuncs.com"
access-key-id: "填写你的id"
access-key-secret: "填写你的秘钥"
minIo: # 文件存储到MINIO
endpoint: "https://127.0.0.1:9000/"
accessKey: "acuity"
secretKey: "acuity"
bucket: "dev"
huawei: # 文件存储到华为云OBS
uriPrefix: "dev.obs.cn-southwest-2.myhuaweicloud.com"
endpoint: "obs.cn-southwest-2.myhuaweicloud.com"
accessKey: "1"
secretKey: "2"
location: "cn-southwest-2"
bucket: "dev"
qiNiu: # 文件存储到七牛云OSS
zone: "z0"
accessKey: "1"
secretKey: "2"
bucket: "acuity_admin_cloud"
# acuity.file.storageType=FAST_DFS 时,需要配置
fdfs:
soTimeout: 1500
connectTimeout: 600
thumb-image:
width: 150
height: 150
tracker-list:
- 192.168.1.2:22122
pool:
#从池中借出的对象的最大数目
max-total: 153
max-wait-millis: 102
jmx-name-base: 1
jmx-name-prefix: 1
修改配置服务器存储类型
存储类型支持LOCAL、FAST_DFS、MIN_IO、ALI_OSS、HUAWEI_OSS、QINIU_OSS,详见:FileStorageType.java。 存储类型支持热更新
yaml
acuity:
file:
storageType: MIN_IO
acuity:
file:
storageType: MIN_IO
程序启动时,会将所有存储类型的文件存储都初始化到内存,在执行上传接口时,根据上传接口或yaml中配置的存储类型参数,选择使用那个实现类。
java
public FileContext(Map<String, FileStrategy> map,
Map<String, FileChunkStrategy> chunkMap,
FileServerProperties fileServerProperties,
FileMapper fileMapper) {
// 利用 spring 注入功能,在程序启动时,将6种类型的存储实现注入到map
map.forEach(this.contextStrategyMap::put);
//...
}
private FileStrategy getFileStrategy(FileStorageType storageType) {
// 上传接口若没有传递storageType参数,则使用yml中配置的acuity.file.storageType参数。
storageType = storageType == null ? fileServerProperties.getStorageType() : storageType;
FileStrategy fileStrategy = contextStrategyMap.get(storageType.name());
ArgumentAssert.notNull(fileStrategy, "请配置正确的文件存储类型");
return fileStrategy;
}
public FileContext(Map<String, FileStrategy> map,
Map<String, FileChunkStrategy> chunkMap,
FileServerProperties fileServerProperties,
FileMapper fileMapper) {
// 利用 spring 注入功能,在程序启动时,将6种类型的存储实现注入到map
map.forEach(this.contextStrategyMap::put);
//...
}
private FileStrategy getFileStrategy(FileStorageType storageType) {
// 上传接口若没有传递storageType参数,则使用yml中配置的acuity.file.storageType参数。
storageType = storageType == null ? fileServerProperties.getStorageType() : storageType;
FileStrategy fileStrategy = contextStrategyMap.get(storageType.name());
ArgumentAssert.notNull(fileStrategy, "请配置正确的文件存储类型");
return fileStrategy;
}
控制基础平台 - 系统功能 - 附件管理 - 删除功能 是否真删除存储到服务器或OSS中的文件
yaml
acuity:
file:
delFile: false # true:删除数据库记录的同时,调用oss接口删除文件 ;false: 只删除数据库记录,保留文件
acuity:
file:
delFile: false # true:删除数据库记录的同时,调用oss接口删除文件 ;false: 只删除数据库记录,保留文件
参数介绍
swagger文档切换到 文件服务
- 附件
- 附件上传
bizType : 每个业务定义一个唯一的业务类型字符串,如用户头像=USER__AVATAR, 商品缩略图=product_thumbnail。
推荐的命名规则(参考:AppendixType.java):模块之间用双下划线分割,库名、表名、字段名有多个单词的用单下划线分割,
{库名}__{表名}__{字段名}
{库名}__{表名}__{字段名}
bucket : 上传文件到指定的桶 , 不传此参数,就默认上传到
acuity.file.xxx.bucket
。storageType : 存储类型 , 不传此参数,就默认上传到
acuity.file.storageType
。返回值介绍
json
{
// 业务类型
"bizType": "每个业务定义一个唯一字符串,建议:库名_表名_字段名",
// 文件id
"id": "1418756607709282304",
// 文件的唯一路径
"path": "0000/每个业务定义一个唯一字符串,建议:库名_表名_字段名/2021/07/24/30cfb050e5bb40868cc66ea66006a083.png",
"fileType": {
"code": "IMAGE",
"desc": "图片"
},
"bucket": "dev",
"uniqueFileName": "30cfb050e5bb40868cc66ea66006a083.png",
"fileMd5": null,
"originalFileName": "调用流程.png",
"contentType": "image/png",
"suffix": "png",
"size": "276776",
}
{
// 业务类型
"bizType": "每个业务定义一个唯一字符串,建议:库名_表名_字段名",
// 文件id
"id": "1418756607709282304",
// 文件的唯一路径
"path": "0000/每个业务定义一个唯一字符串,建议:库名_表名_字段名/2021/07/24/30cfb050e5bb40868cc66ea66006a083.png",
"fileType": {
"code": "IMAGE",
"desc": "图片"
},
"bucket": "dev",
"uniqueFileName": "30cfb050e5bb40868cc66ea66006a083.png",
"fileMd5": null,
"originalFileName": "调用流程.png",
"contentType": "image/png",
"suffix": "png",
"size": "276776",
}
热更新
虽然FileServerProperties
类上加了@RefreshScope
注解,但仅以下参数支持热更新,没在下面的参数不支持。
原因是MinIO、FastDFS、七牛云等在启动时就初始化好了oss client,在运行过程中改了参数,只会影响FileServerProperties实例中的参数,并不会影响已经初始化好了的 oss client 内部的参数,所以MinIO、FastDFS、七牛云等无法热更新。
yaml
acuity:
file:
storageType: MIN_IO
delFile: false
local:
storage-path: /data/projects/uploadfile/file/
endpoint: http://127.0.0.1/file/
inner-uri-prefix: null
ali:
uriPrefix: "http://acuity-admin-cloud.oss-cn-beijing.aliyuncs.com/"
bucket-name: "acuity-admin-cloud"
endpoint: "oss-cn-beijing.aliyuncs.com"
access-key-id: "填写你的id"
access-key-secret: "填写你的秘钥"
huawei:
uriPrefix: "dev.obs.cn-southwest-2.myhuaweicloud.com"
endpoint: "obs.cn-southwest-2.myhuaweicloud.com"
accessKey: "1"
secretKey: "2"
location: "cn-southwest-2"
bucket: "dev"
acuity:
file:
storageType: MIN_IO
delFile: false
local:
storage-path: /data/projects/uploadfile/file/
endpoint: http://127.0.0.1/file/
inner-uri-prefix: null
ali:
uriPrefix: "http://acuity-admin-cloud.oss-cn-beijing.aliyuncs.com/"
bucket-name: "acuity-admin-cloud"
endpoint: "oss-cn-beijing.aliyuncs.com"
access-key-id: "填写你的id"
access-key-secret: "填写你的秘钥"
huawei:
uriPrefix: "dev.obs.cn-southwest-2.myhuaweicloud.com"
endpoint: "obs.cn-southwest-2.myhuaweicloud.com"
accessKey: "1"
secretKey: "2"
location: "cn-southwest-2"
bucket: "dev"
当然,你也可以自行修改代码让MinIO、FastDFS、七牛云等实现热更新,以MinIO举例,方法如下:
- 注释FileAutoConfigure中的MinioClient
java
// FileAutoConfigure 注释以下代码
@Bean
public MinioClient minioClient(FileServerProperties properties) {
return new MinioClient.Builder()
.endpoint(properties.getMinIo().getEndpoint())
.credentials(properties.getMinIo().getAccessKey(), properties.getMinIo().getSecretKey())
.build();
}
// FileAutoConfigure 注释以下代码
@Bean
public MinioClient minioClient(FileServerProperties properties) {
return new MinioClient.Builder()
.endpoint(properties.getMinIo().getEndpoint())
.credentials(properties.getMinIo().getAccessKey(), properties.getMinIo().getSecretKey())
.build();
}
- 修改MinIoFileStrategyImpl代码
java
// MinIoFileStrategyImpl
// 1. 构造函数不在注入 MinioClient
// private final MinioClient minioClient;
public MinIoFileStrategyImpl(FileServerProperties fileProperties/*, MinioClient minioClient*/,
FileMapper fileMapper) {
super(fileProperties, fileMapper);
// this.minioClient = minioClient;
}
@Override
protected void uploadFile(File file, MultipartFile multipartFile, String bucket) throws Exception {
// 划重点: 七牛云 和 FastDFS 也类似,在需要 oss client 的地方,取properties中的参数重新构建一个client,就能实现热更新。
// 2. 在需要minioClient的地方,每次都重新构建
MinioClient minioClient = new MinioClient.Builder()
.endpoint(properties.getMinIo().getEndpoint())
.credentials(properties.getMinIo().getAccessKey(), properties.getMinIo().getSecretKey())
.build();
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
// ...
}
// MinIoFileStrategyImpl
// 1. 构造函数不在注入 MinioClient
// private final MinioClient minioClient;
public MinIoFileStrategyImpl(FileServerProperties fileProperties/*, MinioClient minioClient*/,
FileMapper fileMapper) {
super(fileProperties, fileMapper);
// this.minioClient = minioClient;
}
@Override
protected void uploadFile(File file, MultipartFile multipartFile, String bucket) throws Exception {
// 划重点: 七牛云 和 FastDFS 也类似,在需要 oss client 的地方,取properties中的参数重新构建一个client,就能实现热更新。
// 2. 在需要minioClient的地方,每次都重新构建
MinioClient minioClient = new MinioClient.Builder()
.endpoint(properties.getMinIo().getEndpoint())
.credentials(properties.getMinIo().getAccessKey(), properties.getMinIo().getSecretKey())
.build();
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
// ...
}
接口介绍
在本系统中,文件上传的场景有2种。
文件数据存储租户库(acuity_pro_base):明确知道上传请求和回显文件请求是归属于那个租户的。
如:基础平台 - 消息中心 - 消息管理 - 发送消息,富文本编辑器中的上传接口,上传到租户库。
文件数据存储默认库(acuity_pro_defaults):无法确定请求是来自那个租户。
如:开发运营系统 - 应用管理 - 应用维护,上传应用的logo,上传到默认库。
如:开发运营系统 - 租户管理 - 租户维护,上传租户头像,上传到默认库。
如:右上角 - 个人中心,上传用户头像,上传到默认库。
可以简单理解为:你的业务表存在于默认库,则上传、下载、回显附件也需要调用默认库的相关接口。你的业务表存在于租户库,则上传、下载、回显附件也需要调用租户库的相关接口。所以,上传、下载、回显接口有以下几个:
接口描述 | 租户库 | 默认库 | 接口位置 |
---|---|---|---|
上传文件 | /file/anyone/upload | /file/anyone/upload | acuity-base-controller |
根据文件id打包下载文件 | /file/anyone/download | /file/anyone/download | acuity-base-controller |
根据文件id查询文件的临时访问路径 | /file/anyone/findUrlById | /file/anyone/findUrlById | acuity-base-controller |
根据业务id 和 业务类型查询文件信息 | /anyone/appendix/listByBizId | /anyone/appendix/listByBizId | acuity-file-sdk |
表结构介绍
文件表有2张,这2张表的区别在于:
com_file: 存储全量数据
com_appendix: 存储当前某个业务的实时数据
如:新增应用时,上传了3次文件(点击了3次确认并上传),然后在点击确认按钮保存应用数据,com_file
表会存3条数据,com_appendix
存1条数据。
每次点击确认并上传,文件会立即上传到文件服务,所以若你上传了N次文件,但最后不保存应用数据,com_file
表也会存在N条数据,第三方OSS中也还存在N个已经上传的文件,但 com_appendix
表中无数据。
所以,com_file
和com_appendix
的表结构基本一致,有如下异同点:
com_appendix
表比com_file
多了1个字段:biz_id
;少的其他字段都是无关紧要,若你想保持一致,加上去也行。com_appendix
表的id
、biz_id
、biz_type
3个字段时必须的,其他字段都是为了冗余数据,方便业务数据查询和回显时才加上去的。2张表的id是一致的,
com_file
表的数据一定多于或等于com_appendix
表。表中的
url
字段表示文件是公开bucket时的访问链接,若bucket是私有的,该字段的存储的链接无法将无法使用。私有bucket的文件访问连接需要在使用时临时生成,并且每次生成的链接都有有效期。
上传流程:
附件、图片等上传,一般都会跟业务表单进行关联(如:新增应用、编辑应用),为了使业务逻辑和文件存储逻辑尽可能的解耦,文件逻辑被独立封装成文件服务^1和文件sdk^2。
上传流程:
文件实时上传。
/file/anyTenant/upload
或/file/anyone/upload
json{ label: t('devOperation.application.defApplication.logo'), field: 'appendixIcon', component: 'CropperAvatar', componentProps: { uploadParams: { bizType: FileBizTypeEnum.DEF_APPLICATION_LOGO }, circled: false, isDef: true, # true:调用 /file/anyTenant/upload;false:调用/file/anyone/upload }, },
{ label: t('devOperation.application.defApplication.logo'), field: 'appendixIcon', component: 'CropperAvatar', componentProps: { uploadParams: { bizType: FileBizTypeEnum.DEF_APPLICATION_LOGO }, circled: false, isDef: true, # true:调用 /file/anyTenant/upload;false:调用/file/anyone/upload }, },
文件上传后,将返回以下数据。
若上传N次,com_file表会立即新增N条数据,此时不会存入com_appendix表。
若一个表单有多个字段都需要上传附件,则需要为每个字段定义一个业务类型(bizType),bizType的规则建议为:{库}__{表}__
前端将文件实时上传接口调用返回的数据封装到业务接口的参数。
将传成功后,
CropperAvatar
组件会自动将文件数据封装到appendixIcon
字段。java的实体类需要使用对象来接收此参数。javapublic class DefApplicationSaveVO implements Serializable { @ApiModelProperty("图标") @Valid // 表示需要校验此对象 private AppendixSaveVO appendixIcon; }
public class DefApplicationSaveVO implements Serializable { @ApiModelProperty("图标") @Valid // 表示需要校验此对象 private AppendixSaveVO appendixIcon; }
调用业务接口保存业务数据 + 附件数据
java// defaults 库 def_appendix 表操作接口 @Resource private DefAppendixService appendixService; @Override @Transactional(rollbackFor = Exception.class) public DefApplication save(DefApplicationSaveVO saveVO) { // ... // 保存业务数据 superManager.save(defApplication); // 保存附件数据到 defaults 库 appendixService.save(defApplication.getId(), saveVO.getAppendixIcon()); return defApplication; }
// defaults 库 def_appendix 表操作接口 @Resource private DefAppendixService appendixService; @Override @Transactional(rollbackFor = Exception.class) public DefApplication save(DefApplicationSaveVO saveVO) { // ... // 保存业务数据 superManager.save(defApplication); // 保存附件数据到 defaults 库 appendixService.save(defApplication.getId(), saveVO.getAppendixIcon()); return defApplication; }
调用appendixService.save后,会在com_appendix表存入一条数据(com_file的N条数据仍然保留),defApplication.getId() 存入 biz_id 字段,FileBizTypeEnum.DEF_APPLICATION_LOGO 存入 biz_type字段。
文件回显流程:
根据业务id 和 业务类型查询文件信息
typescript# Edit.vue const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => { // ... if (unref(type) !== ActionEnum.ADD) { const record = { ...data?.record }; // 调用 acuity-file-sdk 模块的 /anyone/appendix/listByBizId 接口,根据业务id和业务类型查询附件信息 const appendixIcons = await listByBizId({ prefix: ServicePrefixEnum.TENANT, bizId: record.id, isDef: true, bizType: FileBizTypeEnum.DEF_APPLICATION_LOGO, }); record.appendixIcon = appendixIcons?.[0]; // 赋值到表单 await setFieldsValue(record); } // ... });
# Edit.vue const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => { // ... if (unref(type) !== ActionEnum.ADD) { const record = { ...data?.record }; // 调用 acuity-file-sdk 模块的 /anyone/appendix/listByBizId 接口,根据业务id和业务类型查询附件信息 const appendixIcons = await listByBizId({ prefix: ServicePrefixEnum.TENANT, bizId: record.id, isDef: true, bizType: FileBizTypeEnum.DEF_APPLICATION_LOGO, }); record.appendixIcon = appendixIcons?.[0]; // 赋值到表单 await setFieldsValue(record); } // ... });
根据文件id查询文件的临时访问路径
大多数OSS的文件都分为公开bucket和私有bucket,私有bucket的访问连接需要在使用时实时获取,同一个文件每次获取的链接都不一样,且链接有失效时间。公开bucket的文件则可以使用固定链接访问。
为了保证文件的访问流程一致,acuity系统约定无论是公开和私有bucket的文件,访问链接都需要每次访问时获取!
typescript// Cropper和Upload组件内部都会 监听value值,给value赋值后,会调用/file/anyone/findUrlById 或 /file/anyone/findUrlById 方法获取文件的临时访问路径。 watch( () => props.value, () => { realSrc.value = ''; if (props.value && props.value.id) { loadSrc(); } }, { immediate: true }, ); function loadSrc() { if (!props.value.id) { return; } // 发起请求 获取临时访问路径 const api = props.isDef ? asyncFindDefUrlById : asyncFindUrlById; api(props.value.id).then((res) => { if (res.code === 0) { realSrc.value = res.data as string; } }); }
// Cropper和Upload组件内部都会 监听value值,给value赋值后,会调用/file/anyone/findUrlById 或 /file/anyone/findUrlById 方法获取文件的临时访问路径。 watch( () => props.value, () => { realSrc.value = ''; if (props.value && props.value.id) { loadSrc(); } }, { immediate: true }, ); function loadSrc() { if (!props.value.id) { return; } // 发起请求 获取临时访问路径 const api = props.isDef ? asyncFindDefUrlById : asyncFindUrlById; api(props.value.id).then((res) => { if (res.code === 0) { realSrc.value = res.data as string; } }); }
下载:
- 若
acuity.file.storageType = LOCAL
,下载和预览时需要通过nginx才能访问 文件上传之后, 返回的文件url, 是无法直接在线访问的, 需要通过nginx进行代理才能访问。.
- nginx 配置
nginx
server {
listen 10000;
# 这个路径要配置成 acuity.file.local.storage-path 最后一段路径.
# 如: acuity.file.local.storage-path = /data/projects/uploadfile/file/ , 则需要配置为 location ^~ /file
# 如: acuity.file.local.storage-path = /data/projects/uploadfile/abcd/ , 则需要配置为 location ^~ /abcd
location ^~ /file {
if ($request_uri ~* ^.*\/(.*)\.(apk|java|txt|doc|pdf|rar|gz|zip|docx|exe|xlsx|ppt|pptx|jpg|png)(\?fileName=([^&]+))$) {
add_header Content-Disposition "attachment;filename=$arg_attname";
}
# 这个地址要配置成 acuity.file.local.storage-path 并去除最后一段路径 /file/ 或 /abcd/ 后的地址.
root /data/projects/uploadfile;
index index.html;
}
}
server {
listen 10000;
# 这个路径要配置成 acuity.file.local.storage-path 最后一段路径.
# 如: acuity.file.local.storage-path = /data/projects/uploadfile/file/ , 则需要配置为 location ^~ /file
# 如: acuity.file.local.storage-path = /data/projects/uploadfile/abcd/ , 则需要配置为 location ^~ /abcd
location ^~ /file {
if ($request_uri ~* ^.*\/(.*)\.(apk|java|txt|doc|pdf|rar|gz|zip|docx|exe|xlsx|ppt|pptx|jpg|png)(\?fileName=([^&]+))$) {
add_header Content-Disposition "attachment;filename=$arg_attname";
}
# 这个地址要配置成 acuity.file.local.storage-path 并去除最后一段路径 /file/ 或 /abcd/ 后的地址.
root /data/projects/uploadfile;
index index.html;
}
}
- yml 配置
yaml
acuity:
file:
storageType: LOCAL
local:
storage-path: /data/projects/uploadfile/file/
uriPrefix: http://127.0.0.1:10000/file/ # 配置成nginx的ip + 端口
# 或者
acuity:
file:
storageType: LOCAL
local:
storage-path: /data/projects/uploadfile/abcd/
uriPrefix: http://127.0.0.1:10000/abcd/ # 配置成nginx的ip + 端口
acuity:
file:
storageType: LOCAL
local:
storage-path: /data/projects/uploadfile/file/
uriPrefix: http://127.0.0.1:10000/file/ # 配置成nginx的ip + 端口
# 或者
acuity:
file:
storageType: LOCAL
local:
storage-path: /data/projects/uploadfile/abcd/
uriPrefix: http://127.0.0.1:10000/abcd/ # 配置成nginx的ip + 端口
按上述配置后, 启动nginx 就可以直接访问文件: http://192.168.2.178:10000/file/0000/2020/12/92b19723-3b20-4e73-a089-de31cf02883e.png
预览
保证自己上传的文件能通过url直接访问之后, 安装 kkFileView 实现附件预览。