commit d44ffceaabd83659b80923126c877c34406b0494 Author: zyx <1029606625@qq.com> Date: Fri Apr 26 18:54:15 2024 +0800 项目完成 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7c53eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +files + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..32cd998 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# 多模态文件管理系统后端工程 filemanage-backend + +## 创建数据库表 +运行sql/fileManage.sql(基于MySQL,版本不低于5.5,推荐8) + +## 自定义项目配置 +修改application.yml下的配置信息(或部署后创建生产环境配置文件) + +## 开发环境下启动 +启动src/main/java/cn/czyx007/filemanage/Application.java + +## 打包工程到生产环境部署 +1.mvn clean + +2.mvn package \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1e3498e --- /dev/null +++ b/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + cn.czyx007 + fileManage + 0.0.1-SNAPSHOT + fileManage-backend + fileManage-backend + + 17 + UTF-8 + UTF-8 + 2.7.6 + + + + org.springframework.boot + spring-boot-starter-web + + + com.baomidou + mybatis-plus-boot-starter + 3.5.5 + + + com.alibaba + druid-spring-boot-starter + 1.2.21 + + + org.springframework.boot + spring-boot-starter-data-redis + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.47 + + + com.mysql + mysql-connector-j + + + com.github.whvcse + easy-captcha + 1.6.2 + + + org.apache.commons + commons-email + 1.5 + + + com.qcloud + cos_api + 5.6.205 + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + cn.czyx007.filemanage.Application + false + + + + repackage + + repackage + + + + + + + + diff --git a/sql/fileManage.sql b/sql/fileManage.sql new file mode 100644 index 0000000..3b70c03 --- /dev/null +++ b/sql/fileManage.sql @@ -0,0 +1,53 @@ +create database if not exists fileManage charset=utf8mb4; + +use fileManage; + +/** + * 文件表 + */ +create table if not exists files( + id bigint primary key auto_increment comment '唯一ID', + name varchar(255) comment '文件名', + store_name varchar(64) comment '文件存储名-uuid', + path varchar(255) comment '文件存储路径', + size bigint comment '文件大小', + type tinyint unsigned comment '文件类型-0:图片 1:视频 2:音频 3:文档 4:其他', + create_time datetime comment '文件创建时间', + update_time datetime comment '文件更新时间', + is_delete boolean default false comment '是否逻辑删除', + user_id bigint comment '所属用户ID', + oss_id bigint comment '存储策略ID' +) DEFAULT CHARSET=utf8mb4; + +/** + * 用户表 + */ +create table if not exists user( + id bigint primary key auto_increment comment '唯一ID', + email varchar(255) comment '邮箱', + username varchar(255) comment '用户名', + password char(40) comment '密码', + create_time datetime comment '注册时间', + update_time datetime comment '个人信息更新时间', + is_admin boolean default false comment '是否为管理员', + storage_used bigint default 0 comment '已使用存储空间(B)', + # 默认1024GB + storage_total bigint default (1024*1024*1024*1024) comment '总存储空间(B)' +)DEFAULT CHARSET=utf8mb4; + +/** + * 存储策略表 + */ +create table if not exists oss( + id bigint primary key auto_increment comment '唯一ID', + name varchar(64) comment '存储策略名', + type tinyint unsigned comment '存储策略类型-0:阿里云 1:腾讯云 2:七牛云 3:本地', + config json comment '存储策略配置信息', + create_time datetime comment '存储策略创建时间', + update_time datetime comment '存储策略更新时间', + creator bigint comment '存储策略创建者ID', + updater bigint comment '存储策略更新者ID', + is_active boolean default false comment '是否启用' +)DEFAULT CHARSET=utf8mb4; + +insert into oss values (1, 'default', 3, null, now(), now(), -1, -1, true); \ No newline at end of file diff --git a/src/main/java/cn/czyx007/filemanage/Application.java b/src/main/java/cn/czyx007/filemanage/Application.java new file mode 100644 index 0000000..746985a --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/Application.java @@ -0,0 +1,17 @@ +package cn.czyx007.filemanage; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication +@EnableTransactionManagement +@EnableCaching +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/src/main/java/cn/czyx007/filemanage/bean/Files.java b/src/main/java/cn/czyx007/filemanage/bean/Files.java new file mode 100644 index 0000000..b69171c --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/bean/Files.java @@ -0,0 +1,74 @@ +package cn.czyx007.filemanage.bean; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class Files implements Serializable { + /** + * 唯一ID + */ + private String id; + + /** + * 文件名 + */ + private String name; + + /* + * 文件存储名(UUID形式) + */ + private String storeName; + + /** + * 文件存储路径 + */ + private String path; + + /** + * 文件大小 + */ + private Long size; + + /** + * 文件类型-0:图片 1:视频 2:音频 3:文档 4:其他 + */ + private Integer type; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + + /** + * 是否删除 + */ + private Integer isDelete; + + /** + * 所属用户ID + */ + private String userId; + + /** + * 存储策略ID + */ + private String ossId; +} diff --git a/src/main/java/cn/czyx007/filemanage/bean/OSS.java b/src/main/java/cn/czyx007/filemanage/bean/OSS.java new file mode 100644 index 0000000..0170170 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/bean/OSS.java @@ -0,0 +1,66 @@ +package cn.czyx007.filemanage.bean; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@NoArgsConstructor +@TableName("oss") +public class OSS implements Serializable { + /** + * 唯一ID + */ + private String id; + + /** + * 存储策略名 + */ + private String name; + + /** + * 存储策略类型-0:阿里云 1:腾讯云 2:七牛云 3:本地 + */ + private Integer type; + + /** + * 存储策略配置信息 + */ + private String config; + + /** + * 存储策略创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 存储策略更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + + /** + * 存储策略创建者ID + */ + private String creator; + + /** + * 存储策略更新者ID + */ + private String updater; + + /** + * 是否启用 + */ + private Integer isActive; +} diff --git a/src/main/java/cn/czyx007/filemanage/bean/User.java b/src/main/java/cn/czyx007/filemanage/bean/User.java new file mode 100644 index 0000000..c25c741 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/bean/User.java @@ -0,0 +1,65 @@ +package cn.czyx007.filemanage.bean; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class User implements Serializable { + /** + * 唯一ID + */ + private String id; + + /** + * 邮箱 + */ + private String email; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 注册时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 个人信息更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + + /** + * 是否为管理员 + */ + private Integer isAdmin; + + /** + * 已使用存储空间(B) + */ + private Long storageUsed; + + /** + * 总存储空间(B) + * 默认1024GB + */ + private Long storageTotal; +} diff --git a/src/main/java/cn/czyx007/filemanage/common/CustomException.java b/src/main/java/cn/czyx007/filemanage/common/CustomException.java new file mode 100644 index 0000000..a6d9ba3 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/common/CustomException.java @@ -0,0 +1,12 @@ +package cn.czyx007.filemanage.common; + + +public class CustomException extends RuntimeException{ + public CustomException() { + super(); + } + + public CustomException(String message) { + super(message); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/common/GlobalExceptionHandler.java b/src/main/java/cn/czyx007/filemanage/common/GlobalExceptionHandler.java new file mode 100644 index 0000000..2c11a64 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/common/GlobalExceptionHandler.java @@ -0,0 +1,18 @@ +package cn.czyx007.filemanage.common; + +import cn.czyx007.filemanage.utils.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +//全局异常处理 +@RestControllerAdvice(annotations = RestController.class) +@Slf4j +public class GlobalExceptionHandler { + @ExceptionHandler(Exception.class) + public Result exceptionHandler(CustomException e){ + log.error(e.toString()); + return Result.error(e.getMessage()); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/common/MyMetaObjectHandler.java b/src/main/java/cn/czyx007/filemanage/common/MyMetaObjectHandler.java new file mode 100644 index 0000000..559b152 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/common/MyMetaObjectHandler.java @@ -0,0 +1,26 @@ +package cn.czyx007.filemanage.common; + +import cn.czyx007.filemanage.utils.BaseContext; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +@Slf4j +public class MyMetaObjectHandler implements MetaObjectHandler { + @Override + public void insertFill(MetaObject metaObject) { + log.info("insertFil: {}", metaObject.toString()); + metaObject.setValue("createTime", LocalDateTime.now()); + metaObject.setValue("updateTime", LocalDateTime.now()); + } + + @Override + public void updateFill(MetaObject metaObject) { + log.info("updateFill: {}", metaObject.toString()); + metaObject.setValue("updateTime", LocalDateTime.now()); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/config/CorsConfig.java b/src/main/java/cn/czyx007/filemanage/config/CorsConfig.java new file mode 100644 index 0000000..7832095 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/config/CorsConfig.java @@ -0,0 +1,26 @@ +package cn.czyx007.filemanage.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + + config.addAllowedOrigin("http://localhost:8088"); // 允许域名跨域访问 + config.addAllowedMethod("*"); // 允许所有请求方法跨域访问 + config.addAllowedHeader("*"); // 允许所有请求头跨域访问 + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return new CorsFilter(source); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/config/MybatisPlusConfig.java b/src/main/java/cn/czyx007/filemanage/config/MybatisPlusConfig.java new file mode 100644 index 0000000..b7345c4 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/config/MybatisPlusConfig.java @@ -0,0 +1,18 @@ +package cn.czyx007.filemanage.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +//MybatisPlus的配置类 +@Configuration +public class MybatisPlusConfig { + + @Bean //该注解加在方法上,表示该方法返回的实例交给spring管理 + public MybatisPlusInterceptor mybatisPlusInterceptor(){ + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return interceptor; + } +} \ No newline at end of file diff --git a/src/main/java/cn/czyx007/filemanage/config/WebMvcConfig.java b/src/main/java/cn/czyx007/filemanage/config/WebMvcConfig.java new file mode 100644 index 0000000..28d16ab --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/config/WebMvcConfig.java @@ -0,0 +1,18 @@ +package cn.czyx007.filemanage.config; + +import cn.czyx007.filemanage.interceptor.LoginInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; +import java.util.List; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + List list = Arrays.asList("/user/sendVerCode", "/user/captcha", "/user/login", "/user/registry"); + registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns(list); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/controller/FileController.java b/src/main/java/cn/czyx007/filemanage/controller/FileController.java new file mode 100644 index 0000000..f4395e5 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/controller/FileController.java @@ -0,0 +1,376 @@ +package cn.czyx007.filemanage.controller; + +import cn.czyx007.filemanage.bean.Files; +import cn.czyx007.filemanage.bean.OSS; +import cn.czyx007.filemanage.service.FilesService; +import cn.czyx007.filemanage.service.OSSService; +import cn.czyx007.filemanage.service.UserService; +import cn.czyx007.filemanage.utils.BaseContext; +import cn.czyx007.filemanage.utils.COSUtil; +import cn.czyx007.filemanage.utils.Result; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.net.MalformedURLException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +@RestController +@RequestMapping("/file") +@Slf4j +public class FileController { + private static String uploadPath; + @Autowired + private FilesService filesService; + @Autowired + private OSSService ossService; + @Autowired + private UserService userService; + + @Autowired + private StringRedisTemplate redisTemplate; + + @Value("${oss.uploadPath}") + public void setUploadPath(String uploadPath) { + FileController.uploadPath = uploadPath; + } + + @GetMapping("/page/{page}/{pageSize}/{isDelete}") + public Result> list(@PathVariable("page") Integer page, @PathVariable("pageSize") Integer pageSize, + @PathVariable("isDelete") Integer isDelete, @RequestParam("name") String name) { + String key = "fileCache::" + BaseContext.getCurrentId() + "_" + page + "_" + pageSize + "_" + isDelete + "_" + name; + String res = redisTemplate.opsForValue().get(key); + if (res != null){ + IPage resPage = JSON.parseObject(res, IPage.class); + return Result.success(resPage); + } + + IPage iPage = new Page<>(page, pageSize); + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(Files::getUserId, BaseContext.getCurrentId()).eq(Files::getIsDelete, isDelete); + if(name != null && !name.isEmpty()) + lqw.like(Files::getName, name); + filesService.page(iPage, lqw); + + redisTemplate.opsForValue().set(key, JSON.toJSONString(iPage)); + return Result.success(iPage); + } + +// @GetMapping("/{id}") +// public Result getFile(@PathVariable("id") String id){ +// Files file = filesService.getFileById(id); +// if(file == null) +// return Result.error("文件不存在或被删除"); +// return Result.success(file); +// } + + @GetMapping("/preview/{id}") + public Result getPreviewFile(@PathVariable("id") String id){ + String key = "previewFile::"+BaseContext.getCurrentId()+"_"+id; + Files file = filesService.getById(id); + if(file == null) { + redisTemplate.delete(key); + return Result.error("文件不存在或被删除"); + } else { + String res = redisTemplate.opsForValue().get(key); + if (res != null) + return Result.success(res); + } + + //云存储,直接返回url + if(file.getPath().startsWith("http")) { + redisTemplate.opsForValue().set(key, file.getPath()); + return Result.success(file.getPath()); + } + // 本地存储,读取图片文件内容 + Path imagePath = Paths.get(file.getPath()); + byte[] imageBytes; + try { + imageBytes = java.nio.file.Files.readAllBytes(imagePath); + } catch (IOException e) { + log.error("读取图片文件内容失败", e); + return Result.error(e.getMessage()); + } + + // 将图片的内容转换为 base64 格式 + String suffix = file.getName().substring(file.getName().lastIndexOf(".")+1); + String base64Image = "data:image/"+suffix+";base64,"+Base64.getEncoder().encodeToString(imageBytes); + redisTemplate.opsForValue().set(key, base64Image); + return Result.success(base64Image); + } + + @PostMapping("/upload") + public Result upload(@RequestParam("file") MultipartFile multipartFile){ + if (multipartFile == null || multipartFile.isEmpty()) { + return Result.error("未上传文件"); + } + OSS ossConfig = ossService.getActive(); + if (ossConfig == null) { + return Result.error("没有有效的存储策略"); + } + + //更新用户已用空间 + if(!userService.updateStorageUsed(multipartFile.getSize(), true)) + return Result.error("空间不足"); + + try { + String fileUploadPath = System.getProperty("user.dir") + uploadPath; + + // 处理文件上传逻辑,根据存储策略来保存文件 + Files file = new Files(); + + // 设置文件名 + file.setName(multipartFile.getOriginalFilename()); + + // 设置存储名(UUID 形式,保证唯一性) + String uuid = UUID.randomUUID().toString(); + String originName = multipartFile.getOriginalFilename(); + String suffix = originName.substring(originName.lastIndexOf(".")); + String storeName = uuid+suffix; + file.setStoreName(storeName); + log.info(file.toString()); + + // 设置文件存储路径 + switch (ossConfig.getType()){ + case 0://阿里云 + + break; + case 1://腾讯云 + COSUtil.init(ossConfig); + file.setPath(COSUtil.customUrl + BaseContext.getCurrentId() + "/" + storeName); + break; + case 2://七牛云 + + break; + case 3://本地 + String filePath = fileUploadPath + "/" + storeName; + file.setPath(filePath); + break; + } + log.info("上传文件保存路径:" + file.getPath()); + + // 设置文件大小 + file.setSize(multipartFile.getSize()); + + // 设置文件类型 + int fileType; + String mimeType = java.nio.file.Files.probeContentType(Paths.get(storeName)); + if (mimeType != null) { + if (mimeType.startsWith("image/")) { + fileType = 0; // 图片类型 + } else if (mimeType.startsWith("audio/")) { + fileType = 1; // 音频类型 + } else if (mimeType.startsWith("video/")) { + fileType = 2; // 视频类型 + } else if (mimeType.startsWith("text/") || mimeType.endsWith("pdf") || + mimeType.startsWith("application/vnd.openxmlformats-officedocument") || + mimeType.equals("application/pdf") || mimeType.endsWith("json")) { + fileType = 3; // 文档类型 + } else { + fileType = 4; // 其他类型 + } + } else { + fileType = 4; // 无法确定类型,设置为其他类型 + } + file.setType(fileType); + + // 设置所属用户ID + String userId = BaseContext.getCurrentId(); + file.setUserId(userId); + + //设置存储策略id + file.setOssId(ossConfig.getId()); + + // 保存文件到指定目录 + switch (ossConfig.getType()){ + case 0://阿里云 + break; + case 1://腾讯云 + COSUtil.uploadFile(multipartFile, file); + break; + case 2://七牛云 + break; + case 3://本地 + File tmpFile = new File(fileUploadPath, storeName); + // 检查目录是否存在,如果不存在则创建目录 + if (!tmpFile.getParentFile().exists()) { + tmpFile.getParentFile().mkdirs(); // 创建目录及其父目录 + } + multipartFile.transferTo(tmpFile); + break; + default: + break; + } + + // 调用文件服务保存文件信息到数据库 + filesService.save(file); + //清除该用户的文件列表缓存 + Set keys = redisTemplate.keys("fileCache::" + BaseContext.getCurrentId() + "*"); + if (keys != null) { + redisTemplate.delete(keys); + } + + return Result.success("文件上传成功"); + } catch (Exception e) { + log.error("文件上传失败,", e); + return Result.error(e.getMessage()); + } + } + + @GetMapping("/download/{id}") + public ResponseEntity download(@PathVariable("id") String id) { + Files file = filesService.getFileById(id); + if (file == null) { + return ResponseEntity.status(404).body(null); + } + // 设置响应头信息 + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getName()); + + // 创建文件资源对象并返回 + Resource resource = null; + if(file.getPath().startsWith("http")){ + try { + resource = new UrlResource(file.getPath()); + } catch (MalformedURLException e) { + log.error(e.toString()); + } + } else { + resource = new FileSystemResource(new File(file.getPath())); + } + return ResponseEntity.ok().headers(headers).body(resource); + } + + @PutMapping("/{id}/{isDelete}") + public Result updateIsDelete(@PathVariable("id") String id, @PathVariable("isDelete") Integer isDelete){ + //清除该用户的文件列表缓存 + Set keys = redisTemplate.keys("fileCache::" + BaseContext.getCurrentId() + "*"); + if (keys != null) { + redisTemplate.delete(keys); + } + //清除该用户的该文件预览缓存 + redisTemplate.delete("previewFile::"+BaseContext.getCurrentId()+"_"+id); + + Files file = filesService.getById(id); + if(file == null) { + return Result.error("文件不存在或被删除"); + } + if(isDelete == 1){ + file.setIsDelete(1); + filesService.updateById(file); + return Result.success("文件删除成功"); + } else if(isDelete == 0){ + file.setIsDelete(0); + filesService.updateById(file); + return Result.success("文件恢复成功"); + } else return Result.error("参数错误"); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable("id") String id){ + //清除该用户的文件列表缓存 + Set keys = redisTemplate.keys("fileCache::" + BaseContext.getCurrentId() + "*"); + if(keys != null) + redisTemplate.delete(keys); + + Files file = filesService.getById(id); + if(file == null) + return Result.error("文件不存在或被删除"); + if(file.getIsDelete() == 0) { + return Result.error("你不能永久删除未在回收站的文件"); + } + //物理删除 + filesService.removeById(id); + if(file.getPath().startsWith("http")){ + COSUtil.init(ossService.getById(file.getOssId())); + COSUtil.createCOSClient().deleteObject(COSUtil.bucketName, BaseContext.getCurrentId()+"/"+file.getStoreName()); + } else { + new File(file.getPath()).delete(); + } + //更新用户已用存储空间 + userService.updateStorageUsed(file.getSize(), false); + + return Result.success("文件已被彻底删除"); + } + + @PutMapping("/{isDelete}") + public Result batchUpdateIsDelete(@PathVariable("isDelete") Integer isDelete, @RequestBody List ids){ + //清除该用户的文件列表缓存 + Set keys = redisTemplate.keys("fileCache::" + BaseContext.getCurrentId() + "*"); + if (keys != null) { + redisTemplate.delete(keys); + } + //清除该用户的该文件预览缓存 + keys = redisTemplate.keys("previewFile::" + BaseContext.getCurrentId() + "*"); + if (keys != null) { + redisTemplate.delete(keys); + } + + List list = filesService.listByIds(ids); + if(list == null) { + return Result.error("文件不存在或被删除"); + } + + if(isDelete == 1){ + list.forEach(file -> file.setIsDelete(1)); + filesService.updateBatchById(list); + return Result.success("文件批量删除成功"); + } else if(isDelete == 0){ + list.forEach(file -> file.setIsDelete(0)); + filesService.updateBatchById(list); + return Result.success("文件批量恢复成功"); + } else return Result.error("参数错误"); + } + + @DeleteMapping + public Result batchDelete(@RequestBody List ids){ + //清除该用户的文件列表缓存 + Set keys = redisTemplate.keys("fileCache::" + BaseContext.getCurrentId() + "*"); + if(keys != null) + redisTemplate.delete(keys); + + List list = filesService.listByIds(ids); + if(list == null) + return Result.error("文件不存在或被删除"); + + long updateSize = 0; + for(Files file : list) { + if (file.getIsDelete() == 0) { + return Result.error("你不能永久删除未在回收站的文件"); + } + updateSize += file.getSize(); + } + //物理删除 + filesService.removeBatchByIds(ids); + + for (Files file : list) { + if(file.getPath().startsWith("http")){ + COSUtil.init(ossService.getById(file.getOssId())); + COSUtil.createCOSClient().deleteObject(COSUtil.bucketName, BaseContext.getCurrentId()+"/"+file.getStoreName()); + } else { + new File(file.getPath()).delete(); + } + } + //更新用户已用存储空间 + userService.updateStorageUsed(updateSize, false); + + return Result.success("文件已被彻底删除"); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/controller/OSSController.java b/src/main/java/cn/czyx007/filemanage/controller/OSSController.java new file mode 100644 index 0000000..39ef2df --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/controller/OSSController.java @@ -0,0 +1,147 @@ +package cn.czyx007.filemanage.controller; + +import cn.czyx007.filemanage.bean.Files; +import cn.czyx007.filemanage.bean.OSS; +import cn.czyx007.filemanage.bean.User; +import cn.czyx007.filemanage.service.FilesService; +import cn.czyx007.filemanage.service.OSSService; +import cn.czyx007.filemanage.service.UserService; +import cn.czyx007.filemanage.utils.BaseContext; +import cn.czyx007.filemanage.utils.Result; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/oss") +public class OSSController { + @Autowired + private UserService userService; + @Autowired + private OSSService ossService; + @Autowired + private FilesService filesService; + + @GetMapping + public Result> list(){ + if(userService.getById(BaseContext.getCurrentId()).getIsAdmin() == 0) + return Result.error("无权限"); + + List list = ossService.list(); + Map idToName = new HashMap<>(); + + // 获取所有管理员用户并将其ID与用户名存入idToName映射中 + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(User::getIsAdmin, 1); + List adminUsers = userService.list(lqw); + for (User adminUser : adminUsers) { + idToName.put(adminUser.getId(), adminUser.getUsername()); + } + + // 更新OSS对象中的创建者和更新者信息 + for (OSS oss : list) { + String creator = oss.getCreator(); + if (!"-1".equals(creator)) { + String username = idToName.get(creator); + if (username != null) { + oss.setCreator(username); + } + } + + String updater = oss.getUpdater(); + if (!"-1".equals(updater)) { + String username = idToName.get(updater); + if (username != null) { + oss.setUpdater(username); + } + } + } + + return Result.success(list); + } + + @PostMapping + public Result addConfig(@RequestBody Map config){ + if(userService.getById(BaseContext.getCurrentId()).getIsAdmin() == 0) + return Result.error("无权限"); + OSS oss = new OSS(); + oss.setName(config.get("name")); + oss.setType(Integer.valueOf(config.get("type"))); + + config.remove("name"); + config.remove("type"); + oss.setConfig(JSON.toJSONString(config)); + + oss.setCreator(BaseContext.getCurrentId()); + oss.setUpdater(BaseContext.getCurrentId()); + ossService.save(oss); + return Result.success("存储配置保存成功"); + } + + @PutMapping("/{id}") + public Result updateConfig(@RequestBody Map config, @PathVariable("id") String id){ + if(userService.getById(BaseContext.getCurrentId()).getIsAdmin() == 0) + return Result.error("无权限"); + OSS oss = ossService.getById(id); + oss.setName(config.get("name")); + oss.setUpdater(BaseContext.getCurrentId()); + + config.remove("name"); + config.remove("type"); + JSONObject storageConfig = JSON.parseObject(oss.getConfig()); + if(storageConfig != null) { + storageConfig.put("customUrl", config.get("customUrl")); + oss.setConfig(JSON.toJSONString(storageConfig)); + } else { + oss.setConfig(JSON.toJSONString(config)); + } + + ossService.updateById(oss); + return Result.success("存储配置修改成功"); + } + + @PutMapping("/{id}/{isActive}") + public Result updateConfigStatus(@PathVariable("id") String id, @PathVariable("isActive") Integer isActive){ + if(userService.getById(BaseContext.getCurrentId()).getIsAdmin() == 0) + return Result.error("无权限"); + OSS oss = ossService.getById(id); + if(oss == null) + return Result.error("存储配置不存在"); + + //启用指定存储策略,将其他策略禁用 + if(isActive == 0) { + oss.setIsActive(1); + oss.setUpdater(BaseContext.getCurrentId()); + ossService.updateById(oss); + + LambdaUpdateWrapper luw = new LambdaUpdateWrapper<>(); + luw.ne(OSS::getId, id).set(OSS::getIsActive, 0); + ossService.update(luw); + return Result.success("存储策略启用成功"); + } else { + //试图仅仅禁用某一个策略,未知要启用的存储策略 + //避免误操作导致没有有效的存储策略 + return Result.error("不允许直接禁用某项存储策略"); + } + } + + @DeleteMapping("/{id}") + public Result deleteConfig(@PathVariable("id") String id){ + if(userService.getById(BaseContext.getCurrentId()).getIsAdmin() == 0) + return Result.error("无权限"); + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(Files::getOssId, id); + if(filesService.exists(lqw)){ + return Result.error("该存储策略下存在文件,无法删除"); + } + ossService.removeById(id); + return Result.success("存储配置删除成功"); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/controller/UserController.java b/src/main/java/cn/czyx007/filemanage/controller/UserController.java new file mode 100644 index 0000000..30e98d5 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/controller/UserController.java @@ -0,0 +1,198 @@ +package cn.czyx007.filemanage.controller; + +import cn.czyx007.filemanage.bean.User; +import cn.czyx007.filemanage.dto.UserDto; +import cn.czyx007.filemanage.service.UserService; +import cn.czyx007.filemanage.utils.*; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.wf.captcha.SpecCaptcha; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.mail.EmailException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@RestController +@RequestMapping("/user") +@Slf4j +public class UserController { + @Autowired + private UserService userService; + + @Autowired + private StringRedisTemplate redisTemplate; + + //发送注册验证码 + @PostMapping("/sendVerCode") + public Result sendVerCode(@RequestBody Map map){ + //生成6位随机数字验证码 + String code = ValidateCodeUtils.generateValidateCode(6).toString(); + log.info("验证码:{}", code); + //发送短信,让用户接受验证码 + try { + String email = map.get("email"); + SendEmailUtils.sendAuthCodeEmail(email, code); + //把验证码保存到redis,5分钟有效 + redisTemplate.opsForValue().set(email + ":code", code, 5, TimeUnit.MINUTES); + return Result.success("验证码发送成功,请查看邮箱"); + } catch (EmailException e) { + log.error(e.toString()); + return Result.error("验证码发送失败,请稍后重试"); + } + } + + @GetMapping("/captcha") + public Result sendCaptcha() { + SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 5); + String verCode = specCaptcha.text().toLowerCase(); + String key = UUID.randomUUID().toString(); + // 存入redis并设置过期时间为1分钟 + redisTemplate.opsForValue().set(key, verCode, 1, TimeUnit.MINUTES); + // 将key和base64返回给前端 + HashMap map = new HashMap<>(); + map.put("key", key); + map.put("image", specCaptcha.toBase64()); + log.info("验证码:{}, key:{}", specCaptcha.text(), key); + return Result.success(JSON.toJSONString(map)); + } + + @PostMapping("/login") + public Result login(@RequestBody UserDto userDto, HttpServletResponse response){ + log.info("UserDto: {}", userDto); + // 获取redis中的验证码 + String redisCode = redisTemplate.opsForValue().get(userDto.getVerKey()); + if(redisCode==null){ + return Result.error("验证码已过期,请刷新"); + } + // 校验验证码 + String captcha = userDto.getCaptcha(); + if (captcha==null || !captcha.toLowerCase().equalsIgnoreCase(redisCode)) { + return Result.error("验证码错误"); + } + + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(User::getEmail, userDto.getEmail()) + .eq(User::getPassword, EncryptUtils.hashPassword(userDto.getPassword())) + .or(i -> i.eq(User::getUsername, userDto.getEmail()) + .eq(User::getPassword, EncryptUtils.hashPassword(userDto.getPassword()))); + + User user = userService.getOne(lqw); + if (userService.getOne(lqw)==null){ + return Result.error("账号不存在或密码错误"); + } + + Cookie cookie = new Cookie("user", String.valueOf(user.getId())); + cookie.setPath("/"); + cookie.setDomain("localhost"); +// cookie.setHttpOnly(true); + + response.addCookie(cookie); + BaseContext.setCurrentId(user.getId()); + log.info("用户登录成功,用户id:{}", user.getId()); + log.info("cookie:{}",cookie.getValue()); + + //验证码使用之后,从redis中删除 + redisTemplate.delete(userDto.getVerKey()); + return Result.success("登录成功"); + } + + @PostMapping("/registry") + public Result registry(@RequestBody UserDto user){ + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(User::getEmail, user.getEmail()); + if (userService.getOne(lqw)!=null){ + return Result.error("该邮箱已被注册"); + } + // 获取redis中的验证码 + String redisCode = redisTemplate.opsForValue().get(user.getEmail()+":code"); + if(redisCode==null){ + return Result.error("验证码已过期,请重新获取"); + } + // 校验验证码 + String verCode = user.getVerificationCode(); + if (verCode==null || !verCode.equals(redisCode)) { + return Result.error("验证码错误"); + } + + user.setUsername(user.getEmail()); + user.setPassword(EncryptUtils.hashPassword(user.getPassword())); + userService.save(user); + log.info("用户注册成功:{}", user); + + //验证码使用之后,从redis中删除 + redisTemplate.delete(user.getEmail() + ":code"); + return Result.success("注册成功"); + } + + @PostMapping("/logout") + public Result logout(HttpServletResponse response){ + BaseContext.setCurrentId(null); + Cookie cookie = new Cookie("user", null); + cookie.setPath("/"); + cookie.setDomain("localhost"); +// cookie.setHttpOnly(true); + cookie.setMaxAge(0); + + response.addCookie(cookie); + return Result.success("退出成功"); + } + + @GetMapping + public Result getUser(){ + String userId = BaseContext.getCurrentId(); + log.info("当前用户id:{}", userId); + User user = userService.getById(userId); + if(user==null){ + return Result.error("用户不存在"); + } + user.setPassword(null); + return Result.success(user); + } + + @PutMapping("/updatePassword") + public Result updatePassword(@RequestBody Map map, HttpServletResponse response){ + String oldPassword = map.get("oldPassword"); + String newPassword = map.get("newPassword"); + User user = userService.getById(BaseContext.getCurrentId()); + if(user==null){ + return Result.error("用户不存在"); + } + if(!user.getPassword().equals(EncryptUtils.hashPassword(oldPassword))){ + return Result.error("旧密码错误"); + } + user.setPassword(EncryptUtils.hashPassword(newPassword)); + userService.updateById(user); + + //密码修改成功,删除cookie,将用户踢下线 + BaseContext.setCurrentId(null); + Cookie cookie = new Cookie("user", null); + cookie.setPath("/"); + cookie.setDomain("localhost"); + cookie.setMaxAge(0); + response.addCookie(cookie); + + log.info("用户密码修改成功,用户id:{}", user.getId()); + return Result.success("密码修改成功"); + } + + @PutMapping("/updateUsername") + public Result updateUsername(@RequestBody User user){ + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(User::getUsername, user.getUsername()); + if (userService.getOne(lqw)!=null){ + return Result.error("该用户名已被使用"); + } + user.setId(BaseContext.getCurrentId()); + userService.updateById(user); + log.info("用户修改用户名成功,用户id:{}", user.getId()); + return Result.success("用户名修改成功"); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/dto/UserDto.java b/src/main/java/cn/czyx007/filemanage/dto/UserDto.java new file mode 100644 index 0000000..1cf9616 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/dto/UserDto.java @@ -0,0 +1,40 @@ +package cn.czyx007.filemanage.dto; + +import cn.czyx007.filemanage.bean.User; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UserDto extends User { + /** + * 确认密码 + */ + private String confirmPassword; + + /** + * 邮箱验证码 + */ + private String verificationCode; + + /** + * 图片验证码 + */ + private String captcha; + + /** + * 图片验证码对应key + */ + private String verKey; + + @Override + public String toString() { + return "UserDto{" + + "confirmPassword='" + confirmPassword + '\'' + + ", verificationCode='" + verificationCode + '\'' + + ", captcha='" + captcha + '\'' + + ", verKey='" + verKey + '\'' + + ", " + super.toString() + '\'' + + "} "; + } +} diff --git a/src/main/java/cn/czyx007/filemanage/interceptor/LoginInterceptor.java b/src/main/java/cn/czyx007/filemanage/interceptor/LoginInterceptor.java new file mode 100644 index 0000000..6f72f41 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/interceptor/LoginInterceptor.java @@ -0,0 +1,41 @@ +package cn.czyx007.filemanage.interceptor; + +import cn.czyx007.filemanage.utils.BaseContext; +import cn.czyx007.filemanage.utils.Result; +import com.alibaba.fastjson2.JSON; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; + +@Slf4j +public class LoginInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + //获取cookie['user'] + String userId = null; + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("user".equals(cookie.getName())) { + userId = cookie.getValue(); + break; + } + } + } + log.info("LoginInterceptor >> "+request.getRequestURI()+" >> cookie['user'] = " + userId); + //判断是否为空 + if (userId != null) { + log.info("当前用户已经登录,用户id为:{}", userId); + BaseContext.setCurrentId(userId); + return true; + } + //未登录,响应数据 + response.getWriter().write(JSON.toJSONString(Result.error("NOT-LOGIN"))); + return false; + } +} diff --git a/src/main/java/cn/czyx007/filemanage/mapper/FilesMapper.java b/src/main/java/cn/czyx007/filemanage/mapper/FilesMapper.java new file mode 100644 index 0000000..94053b9 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/mapper/FilesMapper.java @@ -0,0 +1,12 @@ +package cn.czyx007.filemanage.mapper; + +import cn.czyx007.filemanage.bean.Files; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 文件信息(File)表数据库访问层 + */ +@Mapper +public interface FilesMapper extends BaseMapper { +} diff --git a/src/main/java/cn/czyx007/filemanage/mapper/OSSMapper.java b/src/main/java/cn/czyx007/filemanage/mapper/OSSMapper.java new file mode 100644 index 0000000..7fa3ffd --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/mapper/OSSMapper.java @@ -0,0 +1,9 @@ +package cn.czyx007.filemanage.mapper; + +import cn.czyx007.filemanage.bean.OSS; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OSSMapper extends BaseMapper { +} diff --git a/src/main/java/cn/czyx007/filemanage/mapper/UserMapper.java b/src/main/java/cn/czyx007/filemanage/mapper/UserMapper.java new file mode 100644 index 0000000..088ea14 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/mapper/UserMapper.java @@ -0,0 +1,15 @@ +package cn.czyx007.filemanage.mapper; + +import cn.czyx007.filemanage.bean.User; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + + +/** + * 用户信息(User)表数据库访问层 + */ +@Mapper +public interface UserMapper extends BaseMapper { +} diff --git a/src/main/java/cn/czyx007/filemanage/service/FilesService.java b/src/main/java/cn/czyx007/filemanage/service/FilesService.java new file mode 100644 index 0000000..1973508 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/service/FilesService.java @@ -0,0 +1,16 @@ +package cn.czyx007.filemanage.service; + +import cn.czyx007.filemanage.bean.Files; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * 文件信息(File)表服务接口 + */ +public interface FilesService extends IService { + /** + * 查询id对应的未被删除的文件 + * @param id 文件唯一id + * @return 文件对象Files + */ + Files getFileById(String id); +} diff --git a/src/main/java/cn/czyx007/filemanage/service/OSSService.java b/src/main/java/cn/czyx007/filemanage/service/OSSService.java new file mode 100644 index 0000000..1ac4c57 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/service/OSSService.java @@ -0,0 +1,8 @@ +package cn.czyx007.filemanage.service; + +import cn.czyx007.filemanage.bean.OSS; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface OSSService extends IService { + OSS getActive(); +} diff --git a/src/main/java/cn/czyx007/filemanage/service/UserService.java b/src/main/java/cn/czyx007/filemanage/service/UserService.java new file mode 100644 index 0000000..e9ec4db --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/service/UserService.java @@ -0,0 +1,17 @@ +package cn.czyx007.filemanage.service; + +import cn.czyx007.filemanage.bean.User; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * 用户信息(User)表服务接口 + */ +public interface UserService extends IService { + /** + * 更新用户已使用存储空间 + * @param updateSize 变化的存储空间大小 + * @param isAdd 是否增加 + * @return true:更新成功;false:更新失败(用户剩余存储空间不足) + */ + boolean updateStorageUsed(long updateSize, boolean isAdd); +} diff --git a/src/main/java/cn/czyx007/filemanage/service/impl/FilesServiceImpl.java b/src/main/java/cn/czyx007/filemanage/service/impl/FilesServiceImpl.java new file mode 100644 index 0000000..a8eb32a --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/service/impl/FilesServiceImpl.java @@ -0,0 +1,21 @@ +package cn.czyx007.filemanage.service.impl; + +import cn.czyx007.filemanage.bean.Files; +import cn.czyx007.filemanage.mapper.FilesMapper; +import cn.czyx007.filemanage.service.FilesService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + * 文件信息(File)表服务实现类 + */ +@Service +public class FilesServiceImpl extends ServiceImpl implements FilesService { + @Override + public Files getFileById(String id){ + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(Files::getId, id).eq(Files::getIsDelete, 0); + return this.getOne(lqw); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/service/impl/OSSServiceImpl.java b/src/main/java/cn/czyx007/filemanage/service/impl/OSSServiceImpl.java new file mode 100644 index 0000000..4915e17 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/service/impl/OSSServiceImpl.java @@ -0,0 +1,16 @@ +package cn.czyx007.filemanage.service.impl; + +import cn.czyx007.filemanage.bean.OSS; +import cn.czyx007.filemanage.mapper.OSSMapper; +import cn.czyx007.filemanage.service.OSSService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +@Service +public class OSSServiceImpl extends ServiceImpl implements OSSService { + @Override + public OSS getActive(){ + return getOne(new LambdaQueryWrapper().eq(OSS::getIsActive, true)); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/service/impl/UserServiceImpl.java b/src/main/java/cn/czyx007/filemanage/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..5ce3bbc --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/service/impl/UserServiceImpl.java @@ -0,0 +1,31 @@ +package cn.czyx007.filemanage.service.impl; + +import cn.czyx007.filemanage.bean.User; +import cn.czyx007.filemanage.mapper.UserMapper; +import cn.czyx007.filemanage.service.UserService; +import cn.czyx007.filemanage.utils.BaseContext; +import cn.czyx007.filemanage.utils.Result; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + * 用户信息(User)表服务实现类 + */ +@Service +public class UserServiceImpl extends ServiceImpl implements UserService { + @Override + public boolean updateStorageUsed(long updateSize, boolean isAdd) { + User user = getById(BaseContext.getCurrentId()); + if(isAdd) { + long newSize = updateSize + user.getStorageUsed(); + //检查是否有足够空间用于存储 + if(newSize > user.getStorageTotal()) + return false; + user.setStorageUsed(newSize); + } + else + user.setStorageUsed(user.getStorageUsed() - updateSize); + updateById(user); + return true; + } +} diff --git a/src/main/java/cn/czyx007/filemanage/utils/BaseContext.java b/src/main/java/cn/czyx007/filemanage/utils/BaseContext.java new file mode 100644 index 0000000..8160a63 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/utils/BaseContext.java @@ -0,0 +1,22 @@ +package cn.czyx007.filemanage.utils; + +/** + * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id + */ +public class BaseContext { + private static ThreadLocal threadLocal = new ThreadLocal<>(); + /** + * 设置值 + * @param id + */ + public static void setCurrentId(String id){ + threadLocal.set(id); + } + /** + * 获取值 + * @return + */ + public static String getCurrentId(){ + return threadLocal.get(); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/utils/COSUtil.java b/src/main/java/cn/czyx007/filemanage/utils/COSUtil.java new file mode 100644 index 0000000..35d9511 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/utils/COSUtil.java @@ -0,0 +1,121 @@ +package cn.czyx007.filemanage.utils; + +import cn.czyx007.filemanage.bean.Files; +import cn.czyx007.filemanage.bean.OSS; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.qcloud.cos.COSClient; +import com.qcloud.cos.ClientConfig; +import com.qcloud.cos.auth.BasicCOSCredentials; +import com.qcloud.cos.auth.COSCredentials; +import com.qcloud.cos.http.HttpProtocol; +import com.qcloud.cos.model.*; +import com.qcloud.cos.region.Region; +import com.qcloud.cos.transfer.TransferManager; +import com.qcloud.cos.transfer.TransferManagerConfiguration; +import com.qcloud.cos.transfer.Upload; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Slf4j +public class COSUtil { +// private static COSClient cosClient = null; +// private static TransferManager transferManager = null; + private static String secretId; + private static String secretKey; + public static String regionName; + public static String bucketName; + public static String customUrl; + + public static void init(OSS oss) { + JSONObject config = JSON.parseObject(oss.getConfig()); + secretId = (String) config.get("secretId"); + secretKey = (String) config.get("secretKey"); + regionName = (String) config.get("regionName"); + bucketName = (String) config.get("bucketName"); + if(config.get("customUrl") == null) + customUrl = "https://"+COSUtil.bucketName + ".cos." + COSUtil.regionName + ".myqcloud.com/"; + else customUrl = "https://"+config.get("customUrl")+"/"; + } + + // 创建 COSClient 实例,这个实例用来后续调用请求 + public static COSClient createCOSClient() { + // 设置用户身份信息。 + COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); + // ClientConfig 中包含了后续请求 COS 的客户端设置: + ClientConfig clientConfig = new ClientConfig(); + + // 设置 bucket 的地域 + // COS_REGION 请参见 https://cloud.tencent.com/document/product/436/6224 + clientConfig.setRegion(new Region(regionName)); + + // 设置请求协议, http 或者 https + // 5.6.53 及更低的版本,建议设置使用 https 协议 + // 5.6.54 及更高版本,默认使用了 https + clientConfig.setHttpProtocol(HttpProtocol.https); + + // 生成 cos 客户端。 + return new COSClient(cred, clientConfig); + } + + // 创建 TransferManager 实例,这个实例用来后续调用高级接口 + public static TransferManager createTransferManager() { + // 创建一个 COSClient 实例,这是访问 COS 服务的基础实例。 + // 详细代码参见本页: 简单操作 -> 创建 COSClient + COSClient cosClient = createCOSClient(); + + // 自定义线程池大小,建议在客户端与 COS 网络充足(例如使用腾讯云的 CVM,同地域上传 COS)的情况下,设置成16或32即可,可较充分的利用网络资源 + // 对于使用公网传输且网络带宽质量不高的情况,建议减小该值,避免因网速过慢,造成请求超时。 + ExecutorService threadPool = Executors.newFixedThreadPool(16); + + // 传入一个 threadpool, 若不传入线程池,默认 TransferManager 中会生成一个单线程的线程池。 + TransferManager transferManager = new TransferManager(cosClient, threadPool); + + // 设置高级接口的配置项 + // 分块上传阈值和分块大小分别为 5MB 和 1MB + TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration(); + transferManagerConfiguration.setMultipartUploadThreshold(5*1024*1024); + transferManagerConfiguration.setMinimumUploadPartSize(1*1024*1024); + transferManager.setConfiguration(transferManagerConfiguration); + + return transferManager; + } + + public static void uploadFile(MultipartFile multipartFile, Files file) throws Exception { + // 使用高级接口必须先保证本进程存在一个 TransferManager 实例,如果没有则创建 + // 详细代码参见本页:高级接口 -> 创建 TransferManager + TransferManager transferManager = createTransferManager(); + + ObjectMetadata objectMetadata = new ObjectMetadata(); + // 上传的流如果能够获取准确的流长度,则推荐一定填写 content-length + // 如果确实没办法获取到,则下面这行可以省略,但同时高级接口也没办法使用分块上传了 + objectMetadata.setContentLength(multipartFile.getSize()); + + // 对象键(Key)是对象在存储桶中的唯一标识。 + String key = BaseContext.getCurrentId()+"/"+file.getStoreName(); + + try(InputStream inputStream = multipartFile.getInputStream()) { + PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream, objectMetadata); + + // 设置存储类型(如有需要,不需要请忽略此行代码), 默认是标准(Standard), 低频(standard_ia) + // 更多存储类型请参见 https://cloud.tencent.com/document/product/436/33417 + putObjectRequest.setStorageClass(StorageClass.Standard); + // 高级接口会返回一个异步结果Upload + // 可同步地调用 waitForUploadResult 方法等待上传完成,成功返回 UploadResult, 失败抛出异常 + Upload upload = transferManager.upload(putObjectRequest); + upload.waitForUploadResult(); + } + shutdownTransferManager(transferManager); + } + + public static void shutdownTransferManager(TransferManager transferManager) { + if(transferManager != null) { + // 指定参数为 false, 则不会关闭 transferManager 内部的 COSClient 实例。 + transferManager.shutdownNow(true); + } + } +} diff --git a/src/main/java/cn/czyx007/filemanage/utils/EncryptUtils.java b/src/main/java/cn/czyx007/filemanage/utils/EncryptUtils.java new file mode 100644 index 0000000..b8014ad --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/utils/EncryptUtils.java @@ -0,0 +1,31 @@ +package cn.czyx007.filemanage.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +@Slf4j +public class EncryptUtils { + public static String hashPassword(String password) { + try { + // 创建 MessageDigest 实例并指定算法为 SHA-1 + MessageDigest md = MessageDigest.getInstance("SHA-1"); + // 将密码转换为字节数组 + byte[] passwordBytes = password.getBytes(); + // 使用 MessageDigest 更新字节数组 + byte[] hashedBytes = md.digest(passwordBytes); + + // 将字节数组转换为十六进制字符串 + StringBuilder sb = new StringBuilder(); + for (byte b : hashedBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + // 处理算法不存在异常 + log.error("密码加密失败:", e); + return null; + } + } +} diff --git a/src/main/java/cn/czyx007/filemanage/utils/Result.java b/src/main/java/cn/czyx007/filemanage/utils/Result.java new file mode 100644 index 0000000..68d80c8 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/utils/Result.java @@ -0,0 +1,36 @@ +package cn.czyx007.filemanage.utils; + +import lombok.Data; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 通用返回结果,服务端响应的数据最终都会封装成此对象 + * @param + */ +@Data +public class Result implements Serializable { + private Integer code; //编码:1成功,0和其它数字为失败 + private String msg; //错误信息 + private T data; //数据 + private Map map = new HashMap(); //动态数据 + + public static Result success(T object) { + Result result = new Result(); + result.data = object; + result.code = 1; + return result; + } + public static Result error(String msg) { + Result result = new Result(); + result.msg = msg; + result.code = 0; + return result; + } + public Result add(String key, Object value) { + this.map.put(key, value); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/cn/czyx007/filemanage/utils/SendEmailUtils.java b/src/main/java/cn/czyx007/filemanage/utils/SendEmailUtils.java new file mode 100644 index 0000000..7c10613 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/utils/SendEmailUtils.java @@ -0,0 +1,67 @@ +package cn.czyx007.filemanage.utils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.HtmlEmail; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @author : 张宇轩 + * @createTime : 2023/1/19 - 15:43 + */ +@Component +@Slf4j +public class SendEmailUtils { + private static String hostName; + private static String userName; + private static String password; + + @Value("${email.hostName}") + public void setHostName(String hostName) { + SendEmailUtils.hostName = hostName; + } + + @Value("${email.userName}") + public void setUserName(String userName) { + SendEmailUtils.userName = userName; + } + + @Value("${email.password}") + public void setPassword(String password) { + SendEmailUtils.password = password; + } + + + /** + * 发送验证码 + * @param email 接收邮箱 + * @param code 验证码 + */ + public static void sendAuthCodeEmail(String email,String code) throws EmailException { + HtmlEmail mail = new HtmlEmail(); + /*发送邮件的服务器 126邮箱为smtp.126.com,163邮箱为163.smtp.com,QQ为smtp.qq.com*/ + mail.setHostName(hostName); + mail.setSmtpPort(465); + + // 使用 SSL 连接 + mail.setSSLOnConnect(true); + + /*不设置发送的消息有可能是乱码*/ + mail.setCharset("UTF-8"); + /*IMAP/SMTP服务的密码 username为你开启发送验证码功能的邮箱号 password为你在qq邮箱获取到的一串字符串*/ + mail.setAuthentication(userName, password); + /*发送邮件的邮箱和发件人*/ + mail.setFrom(userName, "文件管理系统"); + /*使用安全链接*/ + mail.setSSLOnConnect(true); + /*接收的邮箱*/ + mail.addTo(email); + /*设置邮件的主题*/ + mail.setSubject("登录验证码"); + /*设置邮件的内容*/ + mail.setMsg("尊敬的用户:你好! 登录验证码为:" + code + "(有效期为5分钟)"); + mail.send();//发送 + log.info("邮件发送成功"); + } +} diff --git a/src/main/java/cn/czyx007/filemanage/utils/ValidateCodeUtils.java b/src/main/java/cn/czyx007/filemanage/utils/ValidateCodeUtils.java new file mode 100644 index 0000000..1932045 --- /dev/null +++ b/src/main/java/cn/czyx007/filemanage/utils/ValidateCodeUtils.java @@ -0,0 +1,43 @@ +package cn.czyx007.filemanage.utils; + +import java.util.Random; + +/** + * 随机生成验证码工具类 + */ +public class ValidateCodeUtils { + /** + * 随机生成验证码 + * @param length 长度为4位或者6位 + * @return + */ + public static Integer generateValidateCode(int length){ + Integer code =null; + if(length == 4){ + code = new Random().nextInt(9999);//生成随机数,最大为9999 + if(code < 1000){ + code = code + 1000;//保证随机数为4位数字 + } + }else if(length == 6){ + code = new Random().nextInt(999999);//生成随机数,最大为999999 + if(code < 100000){ + code = code + 100000;//保证随机数为6位数字 + } + }else{ + throw new RuntimeException("只能生成4位或6位数字验证码"); + } + return code; + } + + /** + * 随机生成指定长度字符串验证码 + * @param length 长度 + * @return + */ + public static String generateValidateCode4String(int length){ + Random rdm = new Random(); + String hash1 = Integer.toHexString(rdm.nextInt()); + String capstr = hash1.substring(0, length); + return capstr; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..6fe0a7b --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,55 @@ +server: + port: 8080 + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + global-config: + db-config: + id-type: ASSIGN_ID + configuration: + map-underscore-to-camel-case: true +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +oss: + uploadPath: '/files' + +#用于发送邮箱验证码的账户和密码 +email: + #发送邮件的服务器域名 + #126邮箱为smtp.126.com,163邮箱为163.smtp.com,QQ个人邮箱为smtp.qq.com,腾讯企业邮为smtp.exmail.qq.com + hostName: smtp.qq.com + userName: xxx@qq.com + password: xxx + +spring: + jackson: + serialization: + fail-on-empty-beans: false + servlet: + multipart: + max-file-size: -1 + max-request-size: -1 + redis: + host: localhost + port: 6379 + database: 0 + datasource: + druid: + #MySQL + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/fileManage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai + username: xxx + password: xxx + #初始化连接数 + initial-size: 1 + #最小空闲连接 + min-idle: 1 + #最大活动连接 + max-active: 20 + #获取连接时测试是否可用 + test-on-borrow: true + #监控页面启动 + filter: + wall: + config: + start-transaction-allow: true diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..747fa10 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/cn/czyx007/filemanage/ApplicationTests.java b/src/test/java/cn/czyx007/filemanage/ApplicationTests.java new file mode 100644 index 0000000..eb9d623 --- /dev/null +++ b/src/test/java/cn/czyx007/filemanage/ApplicationTests.java @@ -0,0 +1,12 @@ +package cn.czyx007.filemanage; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + @Test + void contextLoads() { + } + +}