Browse Source

feat: 增加课程附件功能

ChenSiTong 6 years ago
parent
commit
9373b7aa0a

+ 56 - 0
src/main/java/nju/seec/helper/controller/CourseFileController.java

@@ -0,0 +1,56 @@
+package nju.seec.helper.controller;
+
+import nju.seec.helper.aspect.auth.Auth;
+import nju.seec.helper.controller.response.PageResponse;
+import nju.seec.helper.dto.CourseFileDTO;
+import nju.seec.helper.dto.LoginUser;
+import nju.seec.helper.service.CourseFileService;
+import nju.seec.helper.util.enums.UserType;
+import nju.seec.helper.vo.CourseFileVO;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author cst
+ */
+@RestController
+@RequestMapping("/api/courseFile")
+public class CourseFileController {
+    private final CourseFileService courseFileService;
+
+    public CourseFileController(CourseFileService courseFileService) {
+        this.courseFileService = courseFileService;
+    }
+
+    @Auth(roles = UserType.TEACHER, message = "上传课程附件")
+    @PostMapping
+    public void uploadCourseFile(LoginUser user,
+                                 @Validated CourseFileDTO courseFileDTO) {
+        courseFileService.uploadCourseFile(user, courseFileDTO);
+    }
+
+    @Auth(roles = UserType.TEACHER, message = "删除课程附件")
+    @DeleteMapping("/{courseFileId}")
+    public void deleteCourseFile(LoginUser user,
+                                 @PathVariable Long courseFileId) {
+        courseFileService.deleteCourseFile(user, courseFileId);
+    }
+
+    @Auth(roles = {UserType.TEACHER, UserType.STUDENT}, message = "获取课程附件")
+    @GetMapping("/course/{courseId}")
+    public PageResponse<CourseFileVO> getCourseFiles(LoginUser user,
+                                                     @PathVariable Long courseId,
+                                                     @RequestParam(required = false, defaultValue = "") String key,
+                                                     @PageableDefault(Integer.MAX_VALUE) Pageable pageable) {
+        return PageResponse.of(courseFileService.getCourseFiles(user, courseId, key, pageable));
+    }
+
+    @Auth(roles = {UserType.TEACHER, UserType.STUDENT}, message = "获取课程附件链接")
+    @GetMapping("/{courseFileId}/url")
+    public String getOneCourseFile(LoginUser user,
+                                   @PathVariable Long courseFileId) {
+        return courseFileService.getCourseFileUrl(user, courseFileId);
+    }
+}

+ 46 - 0
src/main/java/nju/seec/helper/dao/CourseFileDAO.java

@@ -0,0 +1,46 @@
+package nju.seec.helper.dao;
+
+import nju.seec.helper.entity.CourseFile;
+import nju.seec.helper.util.enums.ExceptionType;
+import nju.seec.helper.util.exception.HelperException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author cst
+ */
+@Repository
+public interface CourseFileDAO extends JpaRepository<CourseFile, Long>, JpaSpecificationExecutor<CourseFile> {
+    /**
+     * 封装findById
+     *
+     * @param id
+     * @return
+     */
+    default CourseFile findCourseFileById(Long id) {
+        return this.findById(id)
+                .orElseThrow(() -> HelperException.of(ExceptionType.NOT_FOUND, "找不到附件"));
+    }
+
+    /**
+     * 重名检查
+     *
+     * @param courseId
+     * @param filename
+     * @return
+     */
+    boolean existsByCourseIdAndFileName(Long courseId, String filename);
+
+    /**
+     * 根据课程ID和关键字查询
+     *
+     * @param courseId
+     * @param filename
+     * @param pageable
+     * @return
+     */
+    Page<CourseFile> findByCourseIdAndFileNameContains(Long courseId, String filename, Pageable pageable);
+}

+ 17 - 0
src/main/java/nju/seec/helper/dto/CourseFileDTO.java

@@ -0,0 +1,17 @@
+package nju.seec.helper.dto;
+
+import lombok.Data;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author cst
+ */
+@Data
+public class CourseFileDTO {
+    @NotNull(message = "缺少课程ID")
+    private Long courseId;
+    @NotNull(message = "缺少文件")
+    private MultipartFile file;
+}

+ 2 - 0
src/main/java/nju/seec/helper/entity/CourseFile.java

@@ -1,6 +1,7 @@
 package nju.seec.helper.entity;
 
 import lombok.Data;
+import lombok.experimental.Accessors;
 import org.hibernate.annotations.CreationTimestamp;
 
 import javax.persistence.*;
@@ -10,6 +11,7 @@ import java.time.LocalDateTime;
  * @author cst
  */
 @Data
+@Accessors(chain = true)
 @Entity
 @Table(name = "course_file", uniqueConstraints = {@UniqueConstraint(columnNames = {"course_id", "file_name"})})
 public class CourseFile {

+ 48 - 0
src/main/java/nju/seec/helper/service/CourseFileService.java

@@ -0,0 +1,48 @@
+package nju.seec.helper.service;
+
+import nju.seec.helper.dto.CourseFileDTO;
+import nju.seec.helper.dto.LoginUser;
+import nju.seec.helper.vo.CourseFileVO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+/**
+ * @author cst
+ */
+public interface CourseFileService {
+    /**
+     * 上传课程附件
+     *
+     * @param user
+     * @param courseFileDTO
+     */
+    void uploadCourseFile(LoginUser user, CourseFileDTO courseFileDTO);
+
+    /**
+     * 删除课程附件
+     *
+     * @param user
+     * @param courseFileId
+     */
+    void deleteCourseFile(LoginUser user, Long courseFileId);
+
+    /**
+     * 取得课程附件
+     *
+     * @param user
+     * @param courseId
+     * @param key
+     * @param pageable
+     * @return
+     */
+    Page<CourseFileVO> getCourseFiles(LoginUser user, Long courseId, String key, Pageable pageable);
+
+    /**
+     * 取得课程附件链接
+     *
+     * @param user
+     * @param courseFileId
+     * @return
+     */
+    String getCourseFileUrl(LoginUser user, Long courseFileId);
+}

+ 134 - 0
src/main/java/nju/seec/helper/service/impl/CourseFileServiceImpl.java

@@ -0,0 +1,134 @@
+package nju.seec.helper.service.impl;
+
+import lombok.SneakyThrows;
+import nju.seec.helper.dao.ChooseDAO;
+import nju.seec.helper.dao.CourseDAO;
+import nju.seec.helper.dao.CourseFileDAO;
+import nju.seec.helper.dto.CourseFileDTO;
+import nju.seec.helper.dto.LoginUser;
+import nju.seec.helper.entity.Course;
+import nju.seec.helper.entity.CourseFile;
+import nju.seec.helper.service.CourseFileService;
+import nju.seec.helper.service.util.AuthUtils;
+import nju.seec.helper.util.CacheUtils;
+import nju.seec.helper.util.Consts;
+import nju.seec.helper.util.FileUtils;
+import nju.seec.helper.util.enums.ExceptionType;
+import nju.seec.helper.util.exception.HelperException;
+import nju.seec.helper.vo.CourseFileVO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author cst
+ */
+@Service
+public class CourseFileServiceImpl implements CourseFileService {
+    private static final String COURSE_FILE_STORE_DIR = "course_file";
+    private static final long COURSE_FILE_URL_LIVING_SECONDS = 20 * 60;
+
+    private final CourseDAO courseDAO;
+    private final CourseFileDAO courseFileDAO;
+    private final ChooseDAO chooseDAO;
+
+
+    private final FileUtils fileUtils;
+    private final CacheUtils cacheUtils;
+
+    public CourseFileServiceImpl(CourseDAO courseDAO, CourseFileDAO courseFileDAO, ChooseDAO chooseDAO, FileUtils fileUtils, CacheUtils cacheUtils) {
+        this.courseDAO = courseDAO;
+        this.courseFileDAO = courseFileDAO;
+        this.chooseDAO = chooseDAO;
+        this.fileUtils = fileUtils;
+        this.cacheUtils = cacheUtils;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @SneakyThrows
+    @Override
+    public void uploadCourseFile(LoginUser user, CourseFileDTO courseFileDTO) {
+        Course course = courseDAO.findCourseById(courseFileDTO.getCourseId());
+        AuthUtils.checkDataAuth(user.getId(), course.getTeacher().getId(), "您无权上传该课程的附件");
+
+        MultipartFile file = courseFileDTO.getFile();
+        String fileName = file.getOriginalFilename();
+
+        if (courseFileDAO.existsByCourseIdAndFileName(courseFileDTO.getCourseId(), fileName)) {
+            throw HelperException.of(ExceptionType.CONFLICT, "已有同名附件");
+        }
+
+        String objectName = getCourseFileObjectName(course.getId(), fileName);
+
+        CourseFile courseFile = new CourseFile()
+                .setCourseId(course.getId())
+                .setTeacherId(user.getId())
+                .setFileName(file.getOriginalFilename())
+                .setObjectName(objectName);
+
+        courseFileDAO.save(courseFile);
+
+        fileUtils.upload(objectName, file.getInputStream());
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void deleteCourseFile(LoginUser user, Long courseFileId) {
+        CourseFile courseFile = courseFileDAO.findCourseFileById(courseFileId);
+        AuthUtils.checkDataAuth(user.getId(), courseFile.getTeacherId(), "您无权删除该附件");
+        courseFileDAO.deleteById(courseFileId);
+        fileUtils.delete(courseFile.getObjectName());
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public Page<CourseFileVO> getCourseFiles(LoginUser user, Long courseId, String key, Pageable pageable) {
+        if (!chooseDAO.existsByStudentIdAndCourseId(user.getId(), courseId)) {
+            throw HelperException.of(ExceptionType.FORBIDDEN, "您无权查看该课程的附件");
+        }
+        return courseFileDAO.findByCourseIdAndFileNameContains(courseId, key, pageable).map(CourseFileVO::new);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public String getCourseFileUrl(LoginUser user, Long courseFileId) {
+        CourseFile courseFile = courseFileDAO.findCourseFileById(courseFileId);
+        checkDataAccessAuth(user, courseFile);
+
+        String url = cacheUtils.get(Consts.COURSE_FILE_URL_CACHE_NAME, courseFile.getObjectName());
+        if (url != null) {
+            return url;
+        }
+        url = fileUtils.getUrl(courseFile.getObjectName(), COURSE_FILE_URL_LIVING_SECONDS);
+        cacheUtils.set(Consts.COURSE_FILE_URL_CACHE_NAME, courseFile.getObjectName(), url, COURSE_FILE_URL_LIVING_SECONDS, TimeUnit.SECONDS);
+        return url;
+    }
+
+    private String getCourseFileObjectName(Long courseId, String fileName) {
+        return COURSE_FILE_STORE_DIR + File.separator + courseId + File.separator + fileName;
+    }
+
+    private void checkDataAccessAuth(LoginUser user, CourseFile courseFile) {
+        HelperException forbiddenEx = HelperException.of(ExceptionType.FORBIDDEN, "您无权访问该附件");
+
+        switch (user.getType()) {
+            case TEACHER:
+                if (!courseFile.getTeacherId().equals(user.getId())) {
+                    throw forbiddenEx;
+                }
+                break;
+            case STUDENT:
+                if (!chooseDAO.existsByStudentIdAndCourseId(user.getId(), courseFile.getCourseId())) {
+                    throw forbiddenEx;
+                }
+                break;
+            default:
+                throw forbiddenEx;
+        }
+    }
+}

+ 1 - 0
src/main/java/nju/seec/helper/util/Consts.java

@@ -9,4 +9,5 @@ public class Consts {
     public static final String EMAIL_CACHE_NAME = "email";
     public static final String PHONE_CACHE_NAME = "phone";
     public static final String SLIDE_URL_CACHE_NAME = "slide_url";
+    public static final String COURSE_FILE_URL_CACHE_NAME = "course_file_url";
 }

+ 27 - 0
src/main/java/nju/seec/helper/vo/CourseFileVO.java

@@ -0,0 +1,27 @@
+package nju.seec.helper.vo;
+
+import lombok.Data;
+import lombok.NonNull;
+import nju.seec.helper.entity.CourseFile;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author cst
+ */
+@Data
+public class CourseFileVO {
+    private Long id;
+    private Long courseId;
+    private Long teacherId;
+    private String fileName;
+    private LocalDateTime creatAt;
+
+    public CourseFileVO(@NonNull CourseFile courseFile) {
+        this.id = courseFile.getId();
+        this.courseId = courseFile.getCourseId();
+        this.teacherId = courseFile.getTeacherId();
+        this.fileName = courseFile.getFileName();
+        this.creatAt = courseFile.getCreatAt();
+    }
+}