mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 00:57:13 +08:00 
			
		
		
		
	feat: 存储管理适配后端 API
This commit is contained in:
		| @@ -22,6 +22,7 @@ import lombok.Data; | ||||
|  | ||||
| import com.baomidou.mybatisplus.annotation.TableName; | ||||
|  | ||||
| import top.charles7c.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.charles7c.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.charles7c.continew.starter.extension.crud.base.BaseDO; | ||||
|  | ||||
| @@ -96,5 +97,5 @@ public class StorageDO extends BaseDO { | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     private Integer status; | ||||
|     private DisEnableStatusEnum status; | ||||
| } | ||||
| @@ -24,6 +24,9 @@ import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import org.hibernate.validator.constraints.Length; | ||||
|  | ||||
| import top.charles7c.continew.admin.common.constant.RegexConstants; | ||||
| import top.charles7c.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.charles7c.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.charles7c.continew.starter.extension.crud.base.BaseReq; | ||||
| @@ -44,71 +47,79 @@ public class StorageReq extends BaseReq { | ||||
|     /** | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "名称") | ||||
|     @Schema(description = "名称", example = "存储库1") | ||||
|     @NotBlank(message = "名称不能为空") | ||||
|     @Length(max = 100, message = "名称长度不能超过 {max} 个字符") | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * 编码 | ||||
|      */ | ||||
|     @Schema(description = "编码") | ||||
|     @Schema(description = "编码", example = "local") | ||||
|     @NotBlank(message = "编码不能为空") | ||||
|     @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头") | ||||
|     private String code; | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     @Schema(description = "类型") | ||||
|     @Schema(description = "类型", type = "Integer", allowableValues = {"1", "2"}, example = "2") | ||||
|     @NotNull(message = "类型非法") | ||||
|     private StorageTypeEnum type; | ||||
|  | ||||
|     /** | ||||
|      * Access Key | ||||
|      */ | ||||
|     @Schema(description = "Access Key") | ||||
|     @Schema(description = "Access Key", example = "") | ||||
|     @Length(max = 255, message = "Access Key长度不能超过 {max} 个字符") | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * Secret Key | ||||
|      */ | ||||
|     @Schema(description = "Secret Key") | ||||
|     @Schema(description = "Secret Key", example = "") | ||||
|     @Length(max = 255, message = "Secret Key长度不能超过 {max} 个字符") | ||||
|     private String secretKey; | ||||
|  | ||||
|     /** | ||||
|      * Endpoint | ||||
|      */ | ||||
|     @Schema(description = "Endpoint") | ||||
|     @Schema(description = "Endpoint", example = "") | ||||
|     @Length(max = 255, message = "Endpoint长度不能超过 {max} 个字符") | ||||
|     private String endpoint; | ||||
|  | ||||
|     /** | ||||
|      * 桶名称 | ||||
|      */ | ||||
|     @Schema(description = "桶名称") | ||||
|     @Schema(description = "桶名称", example = "C:/continew-admin/data/file/") | ||||
|     @Length(max = 255, message = "桶名称长度不能超过 {max} 个字符") | ||||
|     private String bucketName; | ||||
|  | ||||
|     /** | ||||
|      * 自定义域名 | ||||
|      */ | ||||
|     @Schema(description = "自定义域名") | ||||
|     @Schema(description = "自定义域名", example = "http://localhost:8000/file") | ||||
|     @Length(max = 255, message = "自定义域名长度不能超过 {max} 个字符") | ||||
|     private String domain; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     @Schema(description = "描述") | ||||
|     @Schema(description = "描述", example = "存储库描述") | ||||
|     @Length(max = 200, message = "描述长度不能超过 {max} 个字符") | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 是否为默认存储 | ||||
|      */ | ||||
|     @Schema(description = "是否为默认存储") | ||||
|     @Schema(description = "是否为默认存储", example = "true") | ||||
|     @NotNull(message = "是否为默认存储不能为空") | ||||
|     private Boolean isDefault; | ||||
|  | ||||
|     /** | ||||
|      * 排序 | ||||
|      */ | ||||
|     @Schema(description = "排序") | ||||
|     @Schema(description = "排序", example = "1") | ||||
|     private Integer sort; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -23,7 +23,6 @@ import lombok.Data; | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; | ||||
| import com.alibaba.excel.annotation.ExcelProperty; | ||||
|  | ||||
| import top.charles7c.continew.admin.system.enums.FileTypeEnum; | ||||
| import top.charles7c.continew.starter.extension.crud.base.BaseDetailResp; | ||||
| @@ -46,48 +45,35 @@ public class FileResp extends BaseDetailResp { | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "名称", example = "example") | ||||
|     @ExcelProperty(value = "名称") | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * 大小(字节) | ||||
|      */ | ||||
|     @Schema(description = "大小(字节)", example = "4096") | ||||
|     @ExcelProperty(value = "大小(字节)") | ||||
|     private Long size; | ||||
|  | ||||
|     /** | ||||
|      * URL | ||||
|      */ | ||||
|     @Schema(description = "URL", example = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/example/example.jpg") | ||||
|     @ExcelProperty(value = "URL") | ||||
|     private String url; | ||||
|  | ||||
|     /** | ||||
|      * 扩展名 | ||||
|      */ | ||||
|     @Schema(description = "扩展名", example = "jpg") | ||||
|     @ExcelProperty(value = "扩展名") | ||||
|     private String extension; | ||||
|  | ||||
|     /** | ||||
|      * MIME类型 | ||||
|      */ | ||||
|     @Schema(description = "MIME类型") | ||||
|     @ExcelProperty(value = "MIME类型") | ||||
|     private String mimeType; | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     @Schema(description = "类型", type = "Integer", allowableValues = {"1", "2", "3", "4", "5"}, example = "2") | ||||
|     @ExcelProperty(value = "类型") | ||||
|     private FileTypeEnum type; | ||||
|  | ||||
|     /** | ||||
|      * 存储库 ID | ||||
|      */ | ||||
|     @Schema(description = "存储库ID", example = "1") | ||||
|     @ExcelProperty(value = "存储库ID") | ||||
|     private Long storageId; | ||||
| } | ||||
| @@ -25,6 +25,8 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; | ||||
| import com.alibaba.excel.annotation.ExcelProperty; | ||||
|  | ||||
| import top.charles7c.continew.admin.common.config.easyexcel.ExcelBaseEnumConverter; | ||||
| import top.charles7c.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.charles7c.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.charles7c.continew.starter.extension.crud.base.BaseDetailResp; | ||||
|  | ||||
| @@ -45,83 +47,89 @@ public class StorageDetailResp extends BaseDetailResp { | ||||
|     /** | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "名称") | ||||
|     @Schema(description = "名称", example = "存储库1") | ||||
|     @ExcelProperty(value = "名称") | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * 编码 | ||||
|      */ | ||||
|     @Schema(description = "编码") | ||||
|     @Schema(description = "编码", example = "local") | ||||
|     @ExcelProperty(value = "编码") | ||||
|     private String code; | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     @Schema(description = "类型") | ||||
|     @Schema(description = "类型", type = "Integer", allowableValues = {"1", "2"}, example = "2") | ||||
|     @ExcelProperty(value = "类型", converter = ExcelBaseEnumConverter.class) | ||||
|     private StorageTypeEnum type; | ||||
|  | ||||
|     /** | ||||
|      * Access Key | ||||
|      */ | ||||
|     @Schema(description = "Access Key") | ||||
|     @Schema(description = "Access Key", example = "") | ||||
|     @ExcelProperty(value = "Access Key") | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * Secret Key | ||||
|      */ | ||||
|     @Schema(description = "Secret Key") | ||||
|     @Schema(description = "Secret Key", example = "") | ||||
|     @ExcelProperty(value = "Secret Key") | ||||
|     private String secretKey; | ||||
|  | ||||
|     /** | ||||
|      * Endpoint | ||||
|      */ | ||||
|     @Schema(description = "Endpoint") | ||||
|     @Schema(description = "Endpoint", example = "") | ||||
|     @ExcelProperty(value = "Endpoint") | ||||
|     private String endpoint; | ||||
|  | ||||
|     /** | ||||
|      * 桶名称 | ||||
|      */ | ||||
|     @Schema(description = "桶名称") | ||||
|     @Schema(description = "桶名称", example = "C:/continew-admin/data/file/") | ||||
|     @ExcelProperty(value = "桶名称") | ||||
|     private String bucketName; | ||||
|  | ||||
|     /** | ||||
|      * 自定义域名 | ||||
|      */ | ||||
|     @Schema(description = "自定义域名") | ||||
|     @Schema(description = "自定义域名", example = "http://localhost:8000/file") | ||||
|     @ExcelProperty(value = "自定义域名") | ||||
|     private String domain; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     @Schema(description = "描述") | ||||
|     @Schema(description = "描述", example = "存储库描述") | ||||
|     @ExcelProperty(value = "描述") | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 是否为默认存储 | ||||
|      */ | ||||
|     @Schema(description = "是否为默认存储") | ||||
|     @Schema(description = "是否为默认存储", example = "true") | ||||
|     @ExcelProperty(value = "是否为默认存储") | ||||
|     private Boolean isDefault; | ||||
|  | ||||
|     /** | ||||
|      * 排序 | ||||
|      */ | ||||
|     @Schema(description = "排序") | ||||
|     @Schema(description = "排序", example = "1") | ||||
|     @ExcelProperty(value = "排序") | ||||
|     private Integer sort; | ||||
|  | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态") | ||||
|     @ExcelProperty(value = "状态") | ||||
|     private Integer status; | ||||
|     @Schema(description = "状态(1:启用;2:禁用)", type = "Integer", allowableValues = {"1", "2"}, example = "1") | ||||
|     @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class) | ||||
|     private DisEnableStatusEnum status; | ||||
|  | ||||
|     @Override | ||||
|     public Boolean getDisabled() { | ||||
|         return this.getIsDefault(); | ||||
|     } | ||||
| } | ||||
| @@ -22,6 +22,9 @@ import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
|  | ||||
| import top.charles7c.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.charles7c.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.charles7c.continew.starter.extension.crud.base.BaseResp; | ||||
|  | ||||
| @@ -41,72 +44,78 @@ public class StorageResp extends BaseResp { | ||||
|     /** | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "名称") | ||||
|     @Schema(description = "名称", example = "存储库1") | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * 编码 | ||||
|      */ | ||||
|     @Schema(description = "编码") | ||||
|     @Schema(description = "编码", example = "local") | ||||
|     private String code; | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     @Schema(description = "类型") | ||||
|     @Schema(description = "类型", type = "Integer", allowableValues = {"1", "2"}, example = "2") | ||||
|     private StorageTypeEnum type; | ||||
|  | ||||
|     /** | ||||
|      * Access Key | ||||
|      */ | ||||
|     @Schema(description = "Access Key") | ||||
|     @Schema(description = "Access Key", example = "") | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * Secret Key | ||||
|      */ | ||||
|     @Schema(description = "Secret Key") | ||||
|     @Schema(description = "Secret Key", example = "") | ||||
|     @JsonIgnore | ||||
|     private String secretKey; | ||||
|  | ||||
|     /** | ||||
|      * Endpoint | ||||
|      */ | ||||
|     @Schema(description = "Endpoint") | ||||
|     @Schema(description = "Endpoint", example = "") | ||||
|     private String endpoint; | ||||
|  | ||||
|     /** | ||||
|      * 桶名称 | ||||
|      */ | ||||
|     @Schema(description = "桶名称") | ||||
|     @Schema(description = "桶名称", example = "C:/continew-admin/data/file/") | ||||
|     private String bucketName; | ||||
|  | ||||
|     /** | ||||
|      * 自定义域名 | ||||
|      */ | ||||
|     @Schema(description = "自定义域名") | ||||
|     @Schema(description = "自定义域名", example = "http://localhost:8000/file") | ||||
|     private String domain; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     @Schema(description = "描述") | ||||
|     @Schema(description = "描述", example = "存储库描述") | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 是否为默认存储 | ||||
|      */ | ||||
|     @Schema(description = "是否为默认存储") | ||||
|     @Schema(description = "是否为默认存储", example = "true") | ||||
|     private Boolean isDefault; | ||||
|  | ||||
|     /** | ||||
|      * 排序 | ||||
|      */ | ||||
|     @Schema(description = "排序") | ||||
|     @Schema(description = "排序", example = "1") | ||||
|     private Integer sort; | ||||
|  | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态") | ||||
|     private Integer status; | ||||
|     @Schema(description = "状态(1:启用;2:禁用)", type = "Integer", allowableValues = {"1", "2"}, example = "1") | ||||
|     private DisEnableStatusEnum status; | ||||
|  | ||||
|     @Override | ||||
|     public Boolean getDisabled() { | ||||
|         return this.getIsDefault(); | ||||
|     } | ||||
| } | ||||
| @@ -78,6 +78,7 @@ public class StorageServiceImpl | ||||
|  | ||||
|     @Override | ||||
|     public Long add(StorageReq req) { | ||||
|         CheckUtils.throwIf(Boolean.TRUE.equals(req.getIsDefault()) && this.isDefaultExists(null), "请先取消原有默认存储库"); | ||||
|         String code = req.getCode(); | ||||
|         CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code); | ||||
|         req.setStatus(DisEnableStatusEnum.ENABLE); | ||||
| @@ -87,14 +88,20 @@ public class StorageServiceImpl | ||||
|  | ||||
|     @Override | ||||
|     public void update(StorageReq req, Long id) { | ||||
|         CheckUtils.throwIf(Boolean.TRUE.equals(req.getIsDefault()) && this.isDefaultExists(id), "请先取消原有默认存储库"); | ||||
|         String code = req.getCode(); | ||||
|         CheckUtils.throwIf(this.isCodeExists(code, id), "修改失败,[{}] 已存在", code); | ||||
|         StorageDO oldStorage = super.getById(id); | ||||
|         CheckUtils.throwIf( | ||||
|             Boolean.TRUE.equals(oldStorage.getIsDefault()) && DisEnableStatusEnum.DISABLE.equals(req.getStatus()), | ||||
|             "[{}] 是默认存储库,不允许禁用", oldStorage.getName()); | ||||
|         if (DisEnableStatusEnum.ENABLE.equals(oldStorage.getStatus()) | ||||
|             || DisEnableStatusEnum.DISABLE.equals(req.getStatus())) { | ||||
|             this.unload(BeanUtil.copyProperties(oldStorage, StorageReq.class)); | ||||
|         } | ||||
|         if (DisEnableStatusEnum.ENABLE.equals(req.getStatus())) { | ||||
|             this.load(req); | ||||
|         } | ||||
|         super.update(req, id); | ||||
|     } | ||||
|  | ||||
| @@ -102,7 +109,10 @@ public class StorageServiceImpl | ||||
|     public void delete(List<Long> ids) { | ||||
|         CheckUtils.throwIf(fileService.countByStorageIds(ids) > 0, "所选存储库存在文件关联,请删除文件后重试"); | ||||
|         List<StorageDO> storageList = baseMapper.lambdaQuery().in(StorageDO::getId, ids).list(); | ||||
|         storageList.forEach(s -> this.unload(BeanUtil.copyProperties(s, StorageReq.class))); | ||||
|         storageList.forEach(s -> { | ||||
|             CheckUtils.throwIfEqual(Boolean.TRUE, s.getIsDefault(), "[{}] 是默认存储库,不允许禁用", s.getName()); | ||||
|             this.unload(BeanUtil.copyProperties(s, StorageReq.class)); | ||||
|         }); | ||||
|         super.delete(ids); | ||||
|     } | ||||
|  | ||||
| @@ -161,6 +171,18 @@ public class StorageServiceImpl | ||||
|         this.registerResource(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), req.getBucketName()), true); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 默认存储库是否存在 | ||||
|      * | ||||
|      * @param id | ||||
|      *            ID | ||||
|      * @return 是否存在 | ||||
|      */ | ||||
|     private boolean isDefaultExists(Long id) { | ||||
|         return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, Boolean.TRUE).ne(null != id, StorageDO::getId, id) | ||||
|             .exists(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 编码是否存在 | ||||
|      * | ||||
|   | ||||
| @@ -9,7 +9,6 @@ export interface FileItem { | ||||
|   size: number; | ||||
|   url: string; | ||||
|   extension: string; | ||||
|   mimeType?: string; | ||||
|   type?: string; | ||||
|   storageId?: string; | ||||
|   createUser?: string; | ||||
|   | ||||
							
								
								
									
										64
									
								
								continew-admin-ui/src/api/system/storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								continew-admin-ui/src/api/system/storage.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import axios from 'axios'; | ||||
| import qs from 'query-string'; | ||||
|  | ||||
| const BASE_URL = '/system/storage'; | ||||
|  | ||||
| export interface DataRecord { | ||||
|   id?: number; | ||||
|   name?: string; | ||||
|   code?: string; | ||||
|   type?: number; | ||||
|   accessKey?: string; | ||||
|   secretKey?: string; | ||||
|   endpoint?: string; | ||||
|   bucketName?: string; | ||||
|   domain?: string; | ||||
|   description?: string; | ||||
|   isDefault?: boolean; | ||||
|   sort?: number; | ||||
|   status?: number; | ||||
|   createUser?: string; | ||||
|   createTime?: string; | ||||
|   updateUser?: string; | ||||
|   updateTime?: string; | ||||
|   createUserString?: string; | ||||
|   updateUserString?: string; | ||||
| } | ||||
|  | ||||
| export interface ListParam { | ||||
|   name?: string; | ||||
|   status?: string; | ||||
|   page?: number; | ||||
|   size?: number; | ||||
|   sort?: Array<string>; | ||||
| } | ||||
|  | ||||
| export interface PageRes<T> { | ||||
|   total: number; | ||||
|   list: T; | ||||
| } | ||||
|  | ||||
| export function list(params: ListParam) { | ||||
|   return axios.get<PageRes<DataRecord[]>>(`${BASE_URL}`, { | ||||
|     params, | ||||
|     paramsSerializer: (obj) => { | ||||
|       return qs.stringify(obj); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function get(id: number) { | ||||
|   return axios.get<DataRecord>(`${BASE_URL}/${id}`); | ||||
| } | ||||
|  | ||||
| export function add(req: DataRecord) { | ||||
|   return axios.post(BASE_URL, req); | ||||
| } | ||||
|  | ||||
| export function update(req: DataRecord, id: number) { | ||||
|   return axios.put(`${BASE_URL}/${id}`, req); | ||||
| } | ||||
|  | ||||
| export function del(ids: number | Array<number>) { | ||||
|   return axios.delete(`${BASE_URL}/${ids}`); | ||||
| } | ||||
| @@ -1,3 +1,3 @@ | ||||
| export default { | ||||
|   'menu.system.file.list': '文件管理(尚在开发)', | ||||
|   'menu.system.file.list': '文件管理', | ||||
| }; | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| <template> | ||||
|   <GiOption :class="{ option: showClassStyle }"> | ||||
|     <GiOptionItem @click="onClickItem('rename')"> | ||||
|     <GiOptionItem v-permission="['system:file:update']" @click="onClickItem('rename')"> | ||||
|       <template #icon><svg-icon icon-class="menu-edit" /></template> | ||||
|       <span>重命名</span> | ||||
|     </GiOptionItem> | ||||
|     <GiOptionItem @click="onClickItem('download')"> | ||||
|     <GiOptionItem v-permission="['system:file:download']" @click="onClickItem('download')"> | ||||
|       <template #icon><svg-icon icon-class="menu-download" /></template> | ||||
|       <span>下载</span> | ||||
|     </GiOptionItem> | ||||
|     <GiOptionItem @click="onClickItem('detail')"> | ||||
|     <GiOptionItem v-permission="['system:file:list']" @click="onClickItem('detail')"> | ||||
|       <template #icon><svg-icon icon-class="menu-detail" /></template> | ||||
|       <span>详情</span> | ||||
|     </GiOptionItem> | ||||
|     <GiOptionItem @click="onClickItem('delete')"> | ||||
|     <GiOptionItem v-permission="['system:file:delete']" @click="onClickItem('delete')"> | ||||
|       <template #icon><svg-icon icon-class="menu-delete" /></template> | ||||
|       <span>删除</span> | ||||
|     </GiOptionItem> | ||||
|   | ||||
| @@ -5,7 +5,11 @@ | ||||
|       <a-space wrap> | ||||
|         <a-form ref="queryRef" :model="queryParams" layout="inline"> | ||||
|           <a-form-item hide-label> | ||||
|             <a-upload :show-file-list="false" :custom-request="handleUpload"> | ||||
|             <a-upload | ||||
|               v-permission="['system:file:upload']" | ||||
|               :show-file-list="false" | ||||
|               :custom-request="handleUpload" | ||||
|             > | ||||
|               <template #upload-button> | ||||
|                 <a-button type="primary" shape="round"> | ||||
|                   <template #icon><icon-upload /></template> | ||||
| @@ -45,7 +49,11 @@ | ||||
|           @click="handleMulDelete" | ||||
|           ><template #icon><icon-delete /></template | ||||
|         ></a-button> | ||||
|         <a-button type="primary" @click="isBatchMode = !isBatchMode"> | ||||
|         <a-button | ||||
|           v-permission="['system:file:delete']" | ||||
|           type="primary" | ||||
|           @click="isBatchMode = !isBatchMode" | ||||
|         > | ||||
|           <template #icon><icon-select-all /></template> | ||||
|           <template #default>{{ | ||||
|             isBatchMode ? '取消批量' : '批量操作' | ||||
| @@ -60,6 +68,16 @@ | ||||
|               </template> | ||||
|             </a-button> | ||||
|           </a-tooltip> | ||||
|           <a-tooltip content="配置存储库" position="bottom"> | ||||
|             <a-button | ||||
|               v-permission="['system:storage:list']" | ||||
|               @click="handleConfig" | ||||
|             > | ||||
|               <template #icon> | ||||
|                 <icon-settings /> | ||||
|               </template> | ||||
|             </a-button> | ||||
|           </a-tooltip> | ||||
|         </a-button-group> | ||||
|       </a-space> | ||||
|     </a-row> | ||||
| @@ -87,16 +105,303 @@ | ||||
|  | ||||
|       <a-empty v-show="!fileList.length"></a-empty> | ||||
|     </a-spin> | ||||
|  | ||||
|     <!-- 配置存储库 --> | ||||
|     <a-drawer | ||||
|       title="配置存储库" | ||||
|       :visible="storageVisible" | ||||
|       :width="1070" | ||||
|       :mask-closable="false" | ||||
|       :esc-to-close="false" | ||||
|       unmount-on-close | ||||
|       render-to-body | ||||
|       :footer="false" | ||||
|       @cancel="handleCancelConfig" | ||||
|     > | ||||
|       <!-- 操作栏 --> | ||||
|       <div class="header-operation" style="margin-bottom: 16px"> | ||||
|         <a-row> | ||||
|           <a-col :span="12"> | ||||
|             <a-space> | ||||
|               <a-button | ||||
|                 v-permission="['system:storage:add']" | ||||
|                 type="primary" | ||||
|                 @click="toAddStorage" | ||||
|               > | ||||
|                 <template #icon><icon-plus /></template>新增 | ||||
|               </a-button> | ||||
|               <a-button | ||||
|                 v-permission="['system:storage:export']" | ||||
|                 :loading="exportStorageLoading" | ||||
|                 type="primary" | ||||
|                 status="warning" | ||||
|                 @click="handleStorageExport" | ||||
|               > | ||||
|                 <template #icon><icon-download /></template>导出 | ||||
|               </a-button> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </div> | ||||
|       <a-table | ||||
|         row-key="id" | ||||
|         :data="storageList" | ||||
|         :loading="storageLoading" | ||||
|         :bordered="false" | ||||
|         :pagination="{ | ||||
|           showTotal: true, | ||||
|           showPageSize: true, | ||||
|           total: totalStorage, | ||||
|           current: storageQueryParams.page, | ||||
|         }" | ||||
|         size="large" | ||||
|         column-resizable | ||||
|         stripe | ||||
|         @page-change="handleStoragePageChange" | ||||
|         @page-size-change="handleStoragePageSizeChange" | ||||
|       > | ||||
|         <template #columns> | ||||
|           <a-table-column title="名称" :width="135"> | ||||
|             <template #cell="{ record }"> | ||||
|               {{ record.name }} | ||||
|               <a-tag v-if="record.isDefault" color="arcoblue">默认</a-tag> | ||||
|             </template> | ||||
|           </a-table-column> | ||||
|           <a-table-column title="编码" data-index="code" /> | ||||
|           <a-table-column title="类型" align="center"> | ||||
|             <template #cell="{ record }"> | ||||
|               <dict-tag :value="record.type" :dict="storage_type_enum" /> | ||||
|             </template> | ||||
|           </a-table-column> | ||||
|           <a-table-column | ||||
|             title="访问密钥" | ||||
|             data-index="accessKey" | ||||
|             ellipsis | ||||
|             tooltip | ||||
|           /> | ||||
|           <a-table-column | ||||
|             title="终端节点" | ||||
|             data-index="endpoint" | ||||
|             ellipsis | ||||
|             tooltip | ||||
|           /> | ||||
|           <a-table-column | ||||
|             title="桶名称" | ||||
|             data-index="bucketName" | ||||
|             ellipsis | ||||
|             tooltip | ||||
|           /> | ||||
|           <a-table-column title="域名" data-index="domain" ellipsis tooltip /> | ||||
|           <a-table-column title="状态" align="center"> | ||||
|             <template #cell="{ record }"> | ||||
|               <a-switch | ||||
|                 v-model="record.status" | ||||
|                 :checked-value="1" | ||||
|                 :unchecked-value="2" | ||||
|                 :disabled="!checkPermission(['system:storage:update'])" | ||||
|                 @change="handleStorageChangeStatus(record)" | ||||
|               /> | ||||
|             </template> | ||||
|           </a-table-column> | ||||
|           <a-table-column | ||||
|             title="描述" | ||||
|             data-index="description" | ||||
|             ellipsis | ||||
|             tooltip | ||||
|           /> | ||||
|           <a-table-column | ||||
|             v-if=" | ||||
|               checkPermission([ | ||||
|                 'system:storage:update', | ||||
|                 'system:storage:delete', | ||||
|               ]) | ||||
|             " | ||||
|             title="操作" | ||||
|             align="center" | ||||
|             fixed="right" | ||||
|             :width="90" | ||||
|           > | ||||
|             <template #cell="{ record }"> | ||||
|               <a-button | ||||
|                 v-permission="['system:storage:update']" | ||||
|                 type="text" | ||||
|                 size="small" | ||||
|                 title="修改" | ||||
|                 @click="toUpdateStorage(record.id)" | ||||
|               > | ||||
|                 <template #icon><icon-edit /></template> | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 content="确定要删除当前选中的数据吗?" | ||||
|                 type="warning" | ||||
|                 @ok="handleDeleteStorage([record.id])" | ||||
|               > | ||||
|                 <a-button | ||||
|                   v-permission="['system:storage:delete']" | ||||
|                   type="text" | ||||
|                   size="small" | ||||
|                   :title="record.isDefault ? '默认存储库不能删除' : '删除'" | ||||
|                   :disabled="record.disabled" | ||||
|                 > | ||||
|                   <template #icon><icon-delete /></template> | ||||
|                 </a-button> | ||||
|               </a-popconfirm> | ||||
|             </template> | ||||
|           </a-table-column> | ||||
|         </template> | ||||
|       </a-table> | ||||
|     </a-drawer> | ||||
|     <!-- 表单区域 --> | ||||
|     <a-modal | ||||
|       :title="storageFormTitle" | ||||
|       :visible="storageFormVisible" | ||||
|       :width="580" | ||||
|       :mask-closable="false" | ||||
|       :esc-to-close="false" | ||||
|       unmount-on-close | ||||
|       render-to-body | ||||
|       @ok="handleStorageFormOk" | ||||
|       @cancel="handleStorageFormCancel" | ||||
|     > | ||||
|       <a-form | ||||
|         ref="storageFormRef" | ||||
|         :model="storageForm" | ||||
|         :rules="storageRules" | ||||
|         size="large" | ||||
|         :label-col-style="{ width: '84px' }" | ||||
|         layout="inline" | ||||
|       > | ||||
|         <a-form-item label="名称" field="name"> | ||||
|           <a-input | ||||
|             v-model="storageForm.name" | ||||
|             placeholder="请输入名称" | ||||
|             style="width: 162px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item label="编码" field="code"> | ||||
|           <a-input | ||||
|             v-model="storageForm.code" | ||||
|             placeholder="请输入编码" | ||||
|             style="width: 162px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item label="类型" field="type"> | ||||
|           <a-select | ||||
|             v-model="storageForm.type" | ||||
|             :options="storage_type_enum" | ||||
|             placeholder="请选择存储类型" | ||||
|             style="width: 416px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item | ||||
|           v-if="storageForm.type === 1" | ||||
|           label="访问密钥" | ||||
|           field="accessKey" | ||||
|         > | ||||
|           <a-input | ||||
|             v-model="storageForm.accessKey" | ||||
|             placeholder="请输入访问密钥" | ||||
|             style="width: 416px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item | ||||
|           v-if="storageForm.type === 1" | ||||
|           label="私有密钥" | ||||
|           field="secretKey" | ||||
|         > | ||||
|           <a-input | ||||
|             v-model="storageForm.secretKey" | ||||
|             placeholder="请输入私有密钥" | ||||
|             style="width: 416px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item | ||||
|           v-if="storageForm.type === 1" | ||||
|           label="终端节点" | ||||
|           field="endpoint" | ||||
|         > | ||||
|           <a-input | ||||
|             v-model="storageForm.endpoint" | ||||
|             placeholder="请输入终端节点" | ||||
|             style="width: 416px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item label="桶名称" field="bucketName"> | ||||
|           <a-input | ||||
|             v-model="storageForm.bucketName" | ||||
|             placeholder="请输入桶名称" | ||||
|             style="width: 416px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item v-if="storageForm.type === 1" label="域名" field="domain"> | ||||
|           <a-input | ||||
|             v-model="storageForm.domain" | ||||
|             placeholder="请输入域名" | ||||
|             style="width: 416px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item | ||||
|           v-if="storageForm.type === 2" | ||||
|           label="域名" | ||||
|           field="domain" | ||||
|           :rules="[ | ||||
|             { | ||||
|               required: true, | ||||
|               message: '请输入域名', | ||||
|             }, | ||||
|           ]" | ||||
|         > | ||||
|           <a-input | ||||
|             v-model="storageForm.domain" | ||||
|             placeholder="请输入域名" | ||||
|             style="width: 416px" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item label="排序" field="sort"> | ||||
|           <a-input-number | ||||
|             v-model="storageForm.sort" | ||||
|             placeholder="请输入排序" | ||||
|             style="width: 416px" | ||||
|             :min="1" | ||||
|             mode="button" | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item label="描述" field="description"> | ||||
|           <a-textarea | ||||
|             v-model="storageForm.description" | ||||
|             :max-length="200" | ||||
|             placeholder="请输入描述" | ||||
|             style="width: 416px" | ||||
|             :auto-size="{ | ||||
|               minRows: 3, | ||||
|             }" | ||||
|             show-word-limit | ||||
|           /> | ||||
|         </a-form-item> | ||||
|         <a-form-item label="默认存储" field="isDefault"> | ||||
|           <a-switch v-model="storageForm.isDefault" type="round" /> | ||||
|         </a-form-item> | ||||
|       </a-form> | ||||
|     </a-modal> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
|   import { Modal, RequestOption } from '@arco-design/web-vue'; | ||||
|   import checkPermission from '@/utils/permission'; | ||||
|   import { api as viewerApi } from 'v-viewer'; | ||||
|   import { imageTypeList } from '@/constant/file'; | ||||
|   import { useFileStore } from '@/store/modules/file'; | ||||
|   import type { ListParam, FileItem } from '@/api/system/file'; | ||||
|   import { list, del } from '@/api/system/file'; | ||||
|   import type { DataRecord as StorageDataRecord } from '@/api/system/storage'; | ||||
|   import { | ||||
|     list as listStorage, | ||||
|     get as getStorage, | ||||
|     add as addStorage, | ||||
|     update as updateStorage, | ||||
|     del as delStorage, | ||||
|   } from '@/api/system/storage'; | ||||
|   import { upload } from '@/api/common'; | ||||
|   import { onBeforeRouteUpdate, useRoute } from 'vue-router'; | ||||
|   import { getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue'; | ||||
| @@ -118,6 +423,14 @@ | ||||
|   const fileList = ref<FileItem[]>([]); | ||||
|   // 批量操作 | ||||
|   const isBatchMode = ref(false); | ||||
|   const storageVisible = ref(false); | ||||
|   const storageLoading = ref(false); | ||||
|   const exportStorageLoading = ref(false); | ||||
|   const storageList = ref<StorageDataRecord[]>([]); | ||||
|   const totalStorage = ref(0); | ||||
|   const storageFormTitle = ref(); | ||||
|   const storageFormVisible = ref(false); | ||||
|   const { storage_type_enum } = proxy.useDict('storage_type_enum'); | ||||
|  | ||||
|   const data = reactive({ | ||||
|     // 查询参数 | ||||
| @@ -126,8 +439,24 @@ | ||||
|       type: route.query.type?.toString() || undefined, | ||||
|       sort: ['updateTime,desc'], | ||||
|     }, | ||||
|     storageQueryParams: { | ||||
|       page: 1, | ||||
|       size: 10, | ||||
|       sort: ['updateTime,desc'], | ||||
|     }, | ||||
|     storageForm: {} as StorageDataRecord, | ||||
|     storageRules: { | ||||
|       name: [{ required: true, message: '请输入名称' }], | ||||
|       code: [{ required: true, message: '请输入编码' }], | ||||
|       type: [{ required: true, message: '请选择类型' }], | ||||
|       accessKey: [{ required: true, message: '请输入访问密钥' }], | ||||
|       secretKey: [{ required: true, message: '请输入私有密钥' }], | ||||
|       endpoint: [{ required: true, message: '请输入终端节点' }], | ||||
|       bucketName: [{ required: true, message: '请输入桶名称' }], | ||||
|     }, | ||||
|   }); | ||||
|   const { queryParams } = toRefs(data); | ||||
|   const { queryParams, storageQueryParams, storageForm, storageRules } = | ||||
|     toRefs(data); | ||||
|  | ||||
|   const getList = async (params: ListParam = { ...queryParams.value }) => { | ||||
|     try { | ||||
| @@ -276,6 +605,177 @@ | ||||
|     proxy.$refs.queryRef.resetFields(); | ||||
|     handleQuery(); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 查询存储库列表 | ||||
|    * | ||||
|    * @param params 参数 | ||||
|    */ | ||||
|   const getStorageList = async ( | ||||
|     params: ListParam = { ...storageQueryParams.value }, | ||||
|   ) => { | ||||
|     try { | ||||
|       storageLoading.value = true; | ||||
|       const res = await listStorage(params); | ||||
|       storageList.value = res.data.list; | ||||
|       totalStorage.value = res.data.total; | ||||
|     } finally { | ||||
|       storageLoading.value = false; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 重置表单 | ||||
|    */ | ||||
|   const resetStorage = () => { | ||||
|     storageForm.value = { | ||||
|       type: 1, | ||||
|       sort: 999, | ||||
|       isDefault: false, | ||||
|     }; | ||||
|     proxy.$refs.storageFormRef?.resetFields(); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 打开修改对话框 | ||||
|    * | ||||
|    * @param id ID | ||||
|    */ | ||||
|   const toUpdateStorage = (id: number) => { | ||||
|     resetStorage(); | ||||
|     getStorage(id).then((res) => { | ||||
|       storageForm.value = res.data; | ||||
|       storageFormTitle.value = '修改存储库'; | ||||
|       storageFormVisible.value = true; | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 打开新增对话框 | ||||
|    */ | ||||
|   const toAddStorage = () => { | ||||
|     resetStorage(); | ||||
|     storageFormTitle.value = '新增存储库'; | ||||
|     storageFormVisible.value = true; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 删除 | ||||
|    * | ||||
|    * @param ids ID 列表 | ||||
|    */ | ||||
|   const handleDeleteStorage = (ids: Array<number>) => { | ||||
|     delStorage(ids).then((res) => { | ||||
|       proxy.$message.success(res.msg); | ||||
|       getStorageList(); | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 配置 | ||||
|    */ | ||||
|   const handleConfig = () => { | ||||
|     getStorageList(); | ||||
|     storageVisible.value = true; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 取消配置 | ||||
|    */ | ||||
|   const handleCancelConfig = () => { | ||||
|     storageVisible.value = false; | ||||
|     storageList.value = []; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 切换页码 | ||||
|    * | ||||
|    * @param current 页码 | ||||
|    */ | ||||
|   const handleStoragePageChange = (current: number) => { | ||||
|     storageQueryParams.value.page = current; | ||||
|     getStorageList(); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 切换每页条数 | ||||
|    * | ||||
|    * @param pageSize 每页条数 | ||||
|    */ | ||||
|   const handleStoragePageSizeChange = (pageSize: number) => { | ||||
|     storageQueryParams.value.size = pageSize; | ||||
|     getStorageList(); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 修改状态 | ||||
|    * | ||||
|    * @param record 记录信息 | ||||
|    */ | ||||
|   const handleStorageChangeStatus = (record: StorageDataRecord) => { | ||||
|     const { id } = record; | ||||
|     if (id) { | ||||
|       const tip = record.status === 1 ? '启用' : '禁用'; | ||||
|       getStorage(id) | ||||
|         .then((res) => { | ||||
|           res.data.status = record.status; | ||||
|           updateStorage(res.data, id) | ||||
|             .then(() => { | ||||
|               proxy.$message.success(`${tip}成功`); | ||||
|             }) | ||||
|             .catch(() => { | ||||
|               record.status = record.status === 1 ? 2 : 1; | ||||
|             }); | ||||
|         }) | ||||
|         .catch(() => { | ||||
|           record.status = record.status === 1 ? 2 : 1; | ||||
|         }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 取消 | ||||
|    */ | ||||
|   const handleStorageFormCancel = () => { | ||||
|     storageFormVisible.value = false; | ||||
|     proxy.$refs.storageFormRef?.resetFields(); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 确定 | ||||
|    */ | ||||
|   const handleStorageFormOk = () => { | ||||
|     proxy.$refs.storageFormRef.validate((valid: any) => { | ||||
|       if (!valid) { | ||||
|         if (storageForm.value.id !== undefined) { | ||||
|           updateStorage(storageForm.value, storageForm.value.id).then((res) => { | ||||
|             handleStorageFormCancel(); | ||||
|             getStorageList(); | ||||
|             proxy.$message.success(res.msg); | ||||
|           }); | ||||
|         } else { | ||||
|           addStorage(storageForm.value).then((res) => { | ||||
|             handleStorageFormCancel(); | ||||
|             getStorageList(); | ||||
|             proxy.$message.success(res.msg); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 导出 | ||||
|    */ | ||||
|   const handleStorageExport = () => { | ||||
|     if (exportStorageLoading.value) return; | ||||
|     exportStorageLoading.value = true; | ||||
|     proxy | ||||
|       .download('/system/storage/export', { ...storageQueryParams.value }, '存储库数据') | ||||
|       .finally(() => { | ||||
|         exportStorageLoading.value = false; | ||||
|       }); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
|   | ||||
| @@ -82,6 +82,7 @@ public class CommonController { | ||||
|     @Operation(summary = "上传文件", description = "上传文件") | ||||
|     @PostMapping("/file") | ||||
|     public R<String> upload(@NotNull(message = "文件不能为空") MultipartFile file) { | ||||
|         ValidationUtils.throwIf(projectProperties.isProduction(), "演示环境不支持上传文件"); | ||||
|         ValidationUtils.throwIf(file::isEmpty, "文件不能为空"); | ||||
|         FileInfo fileInfo = fileService.upload(file); | ||||
|         return R.ok("上传成功", fileInfo.getUrl()); | ||||
|   | ||||
| @@ -20,5 +20,5 @@ VALUES | ||||
| INSERT IGNORE INTO `sys_storage` | ||||
| (`id`, `name`, `code`, `type`, `access_key`, `secret_key`, `endpoint`, `bucket_name`, `domain`, `description`, `is_default`, `sort`, `status`, `create_user`, `create_time`, `update_user`, `update_time`) | ||||
| VALUES | ||||
| (1, '本地存储-开发环境', 'local-dev', 2, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file', '本地存储-开发环境', b'1', 1, 1, 1, NOW(), NULL, NULL), | ||||
| (2, '本地存储-生产环境', 'local-prod', 2, NULL, NULL, NULL, '../data/file/', 'http://api.charles7c.top/file', '本地存储-生产环境', b'0', 2, 2, 1, NOW(), NULL, NULL); | ||||
| (1, '开发环境', 'local-dev', 2, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file', '本地存储', b'1', 1, 1, 1, NOW(), NULL, NULL), | ||||
| (2, '生产环境', 'local-prod', 2, NULL, NULL, NULL, '../data/file/', 'http://api.charles7c.top/file', '本地存储', b'0', 2, 2, 1, NOW(), NULL, NULL); | ||||
| @@ -7,7 +7,6 @@ CREATE TABLE IF NOT EXISTS `sys_file` ( | ||||
|     `size`          bigint(20)   NOT NULL                    COMMENT '大小(字节)', | ||||
|     `url`           varchar(512) NOT NULL                    COMMENT 'URL', | ||||
|     `extension`     varchar(100) DEFAULT NULL                COMMENT '扩展名', | ||||
|     `mime_type`     varchar(100) DEFAULT NULL                COMMENT 'MIME类型', | ||||
|     `type`          tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:其他;2:图片;3:文档;4:视频;5:音频)', | ||||
|     `storage_id`    bigint(20)   NOT NULL                    COMMENT '存储库ID', | ||||
|     `create_user`   bigint(20)   NOT NULL                    COMMENT '创建人', | ||||
| @@ -15,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `sys_file` ( | ||||
|     `update_user`   bigint(20)   NOT NULL                    COMMENT '修改人', | ||||
|     `update_time`   datetime     NOT NULL                    COMMENT '修改时间', | ||||
|     PRIMARY KEY (`id`) USING BTREE, | ||||
|     INDEX `idx_url`(`url`) USING BTREE, | ||||
|     INDEX `idx_type`(`type`) USING BTREE, | ||||
|     INDEX `idx_create_user`(`create_user`) USING BTREE, | ||||
|     INDEX `idx_update_user`(`update_user`) USING BTREE | ||||
|   | ||||
		Reference in New Issue
	
	Block a user