Browse Source

Merge branch 'quiz' into iterator2

ChenSiTong 6 years ago
parent
commit
196907d3b8
37 changed files with 1965 additions and 14 deletions
  1. 69 0
      config/application-dev.yml
  2. 2 0
      src/main/java/nju/seec/helper/HelperApplication.java
  3. 22 0
      src/main/java/nju/seec/helper/config/PageableConfig.java
  4. 94 0
      src/main/java/nju/seec/helper/controller/QuestionController.java
  5. 149 0
      src/main/java/nju/seec/helper/controller/QuizController.java
  6. 31 0
      src/main/java/nju/seec/helper/dao/QuizDAO.java
  7. 16 0
      src/main/java/nju/seec/helper/dao/QuizStudentAnswerDAO.java
  8. 7 0
      src/main/java/nju/seec/helper/dao/StudentScoreDAO.java
  9. 20 0
      src/main/java/nju/seec/helper/dto/QuestionStudentAnswerDTO.java
  10. 10 0
      src/main/java/nju/seec/helper/dto/QuizAnswerDTO.java
  11. 25 0
      src/main/java/nju/seec/helper/dto/QuizDTO.java
  12. 22 0
      src/main/java/nju/seec/helper/dto/QuizUpdateDTO.java
  13. 66 0
      src/main/java/nju/seec/helper/entity/Quiz.java
  14. 58 0
      src/main/java/nju/seec/helper/entity/QuizStudentAnswer.java
  15. 26 0
      src/main/java/nju/seec/helper/entity/StudentScore.java
  16. 34 0
      src/main/java/nju/seec/helper/service/QuestionService.java
  17. 111 0
      src/main/java/nju/seec/helper/service/QuizService.java
  18. 3 3
      src/main/java/nju/seec/helper/service/impl/CodeServiceImpl.java
  19. 68 0
      src/main/java/nju/seec/helper/service/impl/QuestionServiceImpl.java
  20. 347 0
      src/main/java/nju/seec/helper/service/impl/QuizServiceImpl.java
  21. 8 3
      src/main/java/nju/seec/helper/service/impl/SlideServiceImpl.java
  22. 3 3
      src/main/java/nju/seec/helper/service/impl/UserServiceImpl.java
  23. 262 0
      src/main/java/nju/seec/helper/util/BokUtil.java
  24. 64 0
      src/main/java/nju/seec/helper/util/GuavaCacheUtil.java
  25. 13 3
      src/main/java/nju/seec/helper/util/RedisCacheUtils.java
  26. 38 0
      src/main/java/nju/seec/helper/util/RestRequestUtil.java
  27. 9 0
      src/main/java/nju/seec/helper/util/enums/ExceptionType.java
  28. 20 0
      src/main/java/nju/seec/helper/util/enums/QuestionKindState.java
  29. 20 0
      src/main/java/nju/seec/helper/util/enums/QuizState.java
  30. 9 1
      src/main/java/nju/seec/helper/util/enums/SlideState.java
  31. 96 0
      src/main/java/nju/seec/helper/vo/quiz/BaseQuestionVO.java
  32. 26 0
      src/main/java/nju/seec/helper/vo/quiz/BokQuestion.java
  33. 52 0
      src/main/java/nju/seec/helper/vo/quiz/ChoiceQuestionVO.java
  34. 29 0
      src/main/java/nju/seec/helper/vo/quiz/QuizResultVO.java
  35. 94 0
      src/main/java/nju/seec/helper/vo/quiz/QuizVO.java
  36. 40 0
      src/main/java/nju/seec/helper/vo/quiz/TrueOrFalseQuestionVO.java
  37. 2 1
      src/main/resources/application-dev.yml

+ 69 - 0
config/application-dev.yml

@@ -0,0 +1,69 @@
+spring:
+  datasource:
+    url: jdbc:mysql://120.78.159.171:19999/helper?setUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: root
+    password: password
+  jpa:
+    open-in-view: true
+    hibernate:
+      ddl-auto: update
+    database: mysql
+    database-platform: org.hibernate.dialect.MySQL8Dialect
+    show-sql: true
+  http:
+    encoding:
+      force: true
+  redis:
+    host: 120.78.159.171
+    port: 6379
+    jedis:
+      pool:
+        max-active: 32
+        max-wait: 300ms
+        max-idle: 16
+        min-idle: 8
+  data:
+    redis:
+      repositories:
+        enabled: false
+  mail:
+    host: smtp.163.com
+    username: seecoder@163.com
+    password: seec67NJU
+    properties:
+      mail:
+        smtp:
+          auth: true
+          socketFactory:
+            class: javax.net.ssl.SSLSocketFactory
+            port: 465
+          starttls:
+            enable: true
+            required: true
+  servlet:
+    multipart:
+      max-file-size: 50MB
+      max-request-size: 100MB
+aliyun:
+  oss:
+    endpoint: oss-cn-hangzhou.aliyuncs.com
+    accessKeyId: LTAI4Fi1qmL4iuhH8t7G7r2H
+    accessKeySecret: 7tJWyQqa0cMkQGaPMMvtH6c0VLiOO2
+    bucketName: seec-helper-pdf
+  sms:
+    accessKeyId: LTAI4FiAXXyNfYJLweBXpKPx
+    accessSecret: IBoC7KGkZHOvxZQIfQgp98ZY48AVjW
+    signName: seeccoder
+    templateCode: SMS_181555598
+bok:
+  url: http://bok.seecoder.cn
+guavaCache:
+  maximumSize: 1000
+  expireAfterAccessInSeconds: 1800
+  expireAfterWriteInSeconds: 1800
+# 自定义数据
+helper:
+  mail:
+    from: ${spring.mail.username}
+    subject: seeccoder

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

@@ -4,6 +4,7 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
 import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 /**
@@ -12,6 +13,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @ConfigurationPropertiesScan("nju.seec.helper.config.properties")
 @EnableTransactionManagement
 @EnableAsync
+@EnableJpaAuditing
 @SpringBootApplication
 public class HelperApplication {
     public static void main(String[] args) {

+ 22 - 0
src/main/java/nju/seec/helper/config/PageableConfig.java

@@ -0,0 +1,22 @@
+package nju.seec.helper.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.web.config.PageableHandlerMethodArgumentResolverCustomizer;
+
+/**
+ * @ClassName PageableConfig
+ * @PackageName nju.seec.helper.config
+ * @Author SheentXu
+ * @Date 2019/12/24
+ * @Version 1.0
+ * @Description //TODO
+ **/
+@Configuration
+public class PageableConfig {
+
+    @Bean
+    PageableHandlerMethodArgumentResolverCustomizer pageableResolverCustomizer() {
+        return pageableResolver -> pageableResolver.setOneIndexedParameters(true);
+    }
+}

+ 94 - 0
src/main/java/nju/seec/helper/controller/QuestionController.java

@@ -0,0 +1,94 @@
+package nju.seec.helper.controller;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import nju.seec.helper.aspect.auth.Auth;
+import nju.seec.helper.controller.response.PageResponse;
+import nju.seec.helper.dao.CourseDAO;
+import nju.seec.helper.dao.SlideDAO;
+import nju.seec.helper.dto.LoginUser;
+import nju.seec.helper.service.QuestionService;
+import nju.seec.helper.util.enums.ExceptionType;
+import nju.seec.helper.util.enums.UserType;
+import nju.seec.helper.util.exception.HelperException;
+import nju.seec.helper.vo.quiz.BaseQuestionVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+/**
+ * 题库
+ *
+ * @doc http://47.100.18.120:3000/SEEC-BOK/API-DOC/src/master/helper/api/Quiz-%e6%b5%8b%e8%af%95.md
+ */
+@RestController
+@RequestMapping("/api/question")
+public class QuestionController {
+    @Autowired
+    private CourseDAO courseDAO;
+    @Autowired
+    private SlideDAO slideDAO;
+    @Autowired
+    private QuestionService questionService;
+    /**
+     * 获得题目列表
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "获得题目列表")
+    @GetMapping("")
+    public Map getQuestionList(LoginUser user, String stem,@PageableDefault(Integer.MAX_VALUE) Pageable pageable) throws JsonProcessingException {
+
+        final Page<BaseQuestionVO> page = questionService.getQuestionList(user, stem, pageable);
+        Map ret=new LinkedHashMap(2);
+        ret.put("questions",page.getContent());
+        ret.put("page",PageResponse.PageInfo.of(page.getNumber()+1, page.getSize(), page.getTotalPages(), Long.valueOf(page.getTotalElements()).intValue()));
+        return ret;
+    }
+
+    /**
+     * 获得题目列表
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "获得题目详情")
+    @GetMapping("/{questionId}")
+        public BaseQuestionVO getQuestion(LoginUser user, @PathVariable("questionId") String questionId) throws JsonProcessingException {
+        return questionService.getQuestion(questionId);
+    }
+
+
+    /**
+     * 创建题目
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "创建题目")
+    @PostMapping("")
+    public BaseQuestionVO postQuestion(LoginUser user, @RequestBody Map question) {
+        if(question.get("kind")==null){throw HelperException.of(ExceptionType.PARAM_ERROR,"无类型");}
+
+        return questionService.newQuestion(BaseQuestionVO.parse(question));
+    }
+
+    /**
+     * 更新题目
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "更新题目")
+    @PutMapping("/{questionId}")
+    public BaseQuestionVO updateQuestion(LoginUser user, @RequestBody Map question, @PathVariable("questionId") String questionId) {
+        if(question.get("kind")==null){throw HelperException.of(ExceptionType.PARAM_ERROR,"无类型");}
+
+        return questionService.updateQuestion(BaseQuestionVO.parse(question));
+    }
+
+
+    /**
+     * 删除题目
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "删除题目")
+    @DeleteMapping("/{questionId}")
+    public Map deleteQuestion(LoginUser user, @PathVariable("questionId") String questionId) {
+
+        questionService.deleteQuestion(questionId);
+        Map map=new HashMap();
+        map.put("message","success");
+        return map;
+    }
+}

+ 149 - 0
src/main/java/nju/seec/helper/controller/QuizController.java

@@ -0,0 +1,149 @@
+package nju.seec.helper.controller;
+
+import nju.seec.helper.aspect.auth.Auth;
+import nju.seec.helper.controller.response.PageResponse;
+import nju.seec.helper.dao.CourseDAO;
+import nju.seec.helper.dao.SlideDAO;
+import nju.seec.helper.dto.LoginUser;
+import nju.seec.helper.dto.QuizAnswerDTO;
+import nju.seec.helper.dto.QuizDTO;
+import nju.seec.helper.dto.QuizUpdateDTO;
+import nju.seec.helper.entity.Quiz;
+import nju.seec.helper.service.QuizService;
+import nju.seec.helper.util.BokUtil;
+import nju.seec.helper.util.enums.QuizState;
+import nju.seec.helper.util.enums.UserType;
+import nju.seec.helper.vo.quiz.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.web.bind.annotation.*;
+
+import javax.persistence.EntityNotFoundException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 测试
+ *
+ * @doc http://47.100.18.120:3000/SEEC-BOK/API-DOC/src/master/helper/api/Quiz-%e6%b5%8b%e8%af%95.md
+ */
+@RestController
+@RequestMapping("/api/quiz")
+public class QuizController {
+    @Autowired
+    private CourseDAO courseDAO;
+    @Autowired
+    private SlideDAO slideDAO;
+    @Autowired
+    private BokUtil bokUtil;
+    @Autowired
+    private QuizService quizService;
+
+    /**
+     * 获得测试列表
+     */
+    @Auth(roles = {UserType.STUDENT,UserType.TEACHER}, message = "获得测试列表")
+    @GetMapping("")
+    public Map getBasicQuizList(LoginUser user, String slideId,
+                      QuizState state	, @PageableDefault(Integer.MAX_VALUE) Pageable pageable) {
+        List<QuizVO> quizzes=new ArrayList<QuizVO>();
+        Map ret=new LinkedHashMap(2);
+        Page<Quiz> page;
+        if(slideId!=null){
+            page = quizService.getAllBasicQuizBySlide(Long.valueOf(slideId), pageable, state, user.getId());
+           }else{
+            page = quizService.getAllBasicQuizByUser( pageable, state, user.getId());
+        }
+        quizzes.addAll(page.getContent().stream().map(QuizVO::new).collect(Collectors.toList()));
+        ret.put("page",PageResponse.PageInfo.of(page.getNumber()+1, page.getSize(), page.getTotalPages(), Long.valueOf(page.getTotalElements()).intValue()));
+        ret.put("quizzes",quizzes);
+        return ret;
+    }
+
+    /**
+     * 获得某一测试的详细内容
+     */
+    @Auth(roles = {UserType.STUDENT,UserType.TEACHER}, message = "获得某一测试的详细内容")
+    @GetMapping("/{quizId}")
+    public QuizVO getOneQuiz(LoginUser user, @PathVariable("quizId") String quizId) {
+        QuizVO quizVO = quizService.combineQuizWithAnswer(Long.valueOf(quizId), user.getId(), user.getType());
+
+        if(user.getType().equals(UserType.STUDENT)&&!quizVO.getState().equals(QuizState.CLOSED)){
+            quizVO.excludeCorrectAnswer();
+        }
+        return quizVO;
+    }
+
+    /**
+     * 创建测试
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "创建测试")
+    @PostMapping("")
+    public QuizVO createQuiz(LoginUser user, @RequestBody QuizDTO newQuiz) {
+        System.out.println(newQuiz);
+        final Quiz quiz = quizService.createQuiz(newQuiz.getName(), user.getId(), newQuiz.getQuizTime(),
+                Long.valueOf(newQuiz.getSlideId()), newQuiz.getQuestions());
+        return quizService.combineQuiz(quiz.getId());
+    }
+
+    /**
+     * 修改测试 (仅限未开始时可以修改)
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "修改测试")
+    @PutMapping("/{quizId}")
+    public QuizVO updateQuiz(LoginUser user, @RequestBody QuizUpdateDTO newQuiz, @PathVariable("quizId") String quizId) {
+
+        Quiz ret = quizService.modifyQuiz(Long.valueOf(quizId),newQuiz.getName(),
+                newQuiz.getQuestions(),newQuiz.getQuizTime(),newQuiz.getSlideId());
+        return quizService.combineQuiz(ret.getId());
+    }
+
+    /**
+     * 删除测试 (仅限未开始时可以删除)
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "删除测试")
+    @DeleteMapping("/{quizId}")
+    public Map deleteQuiz(LoginUser user, @PathVariable("quizId") String quizId) {
+        Map map=new HashMap();
+        String message = quizService.deleteQuiz(Long.valueOf(quizId));
+        map.put("message",message);
+        return map;
+    }
+
+    /**
+     * 获得测试结果统计
+     */
+    @Auth(roles = {UserType.TEACHER}, message = "获得测试结果统计")
+    @GetMapping("/{quizId}/result")
+    public Map getQuizResult(LoginUser user, @PathVariable("quizId") String quizId) {
+        if(quizId==null){
+            throw new EntityNotFoundException("quiz can't be null");
+        }
+        Map map=new HashMap();
+
+        List<QuizResultVO> list= quizService.getQuizResult(Long.valueOf(quizId));
+        map.put("message","success");
+        map.put("average",list.stream().mapToInt(QuizResultVO::getScore).average());
+        map.put("data",list);
+        return map;
+    }
+    /**
+     * 提交答案
+     */
+
+    @Auth(roles = {UserType.STUDENT}, message = "提交答案")
+    @PutMapping("/{quizId}/student-answer")
+    public Map putStudentAnswer(LoginUser user, @PathVariable("quizId") String quizId, @RequestBody QuizAnswerDTO quizAnswerDTO) {
+
+        if(quizId==null){
+            throw new EntityNotFoundException("quiz can't be null");
+        }
+        Integer match = quizService.upStudentAnswer(Long.valueOf(quizId),user.getId(),quizAnswerDTO.getAnswers());
+        Map map=new HashMap();
+        map.put("message","success");
+        map.put("matchNum",match);
+        return map;
+    }
+}

+ 31 - 0
src/main/java/nju/seec/helper/dao/QuizDAO.java

@@ -0,0 +1,31 @@
+package nju.seec.helper.dao;
+
+import nju.seec.helper.entity.Course;
+import nju.seec.helper.entity.Quiz;
+import nju.seec.helper.entity.Slide;
+import nju.seec.helper.entity.User;
+import nju.seec.helper.util.enums.ExceptionType;
+import nju.seec.helper.util.enums.QuizState;
+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 java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public interface QuizDAO extends JpaRepository<Quiz,Long> {
+    default Quiz findQuizById(Long id) {
+        return this.findById(id)
+                .orElseThrow(() -> HelperException.of(ExceptionType.NOT_FOUND, "找不到测试"));
+    }
+    Page<Quiz> findAllBySlideAndState(Slide slide, QuizState state, Pageable pageable);
+    Page<Quiz> findAllBySlide(Slide slide, Pageable pageable);
+    List<Quiz> findAllBySlide(Slide slide);
+    Page<Quiz> findAllByCourseIn(Collection<Course> course, Pageable pageable);
+    Page<Quiz> findAllByCourseInAndState(Collection<Course> course, QuizState state,Pageable pageable);
+    Page<Quiz> findByTeacher(User teacher, Pageable pageable);
+    Page<Quiz> findByTeacherAndState(User teacher, QuizState state, Pageable pageable);
+}

+ 16 - 0
src/main/java/nju/seec/helper/dao/QuizStudentAnswerDAO.java

@@ -0,0 +1,16 @@
+package nju.seec.helper.dao;
+
+import nju.seec.helper.entity.Quiz;
+import nju.seec.helper.entity.QuizStudentAnswer;
+import nju.seec.helper.entity.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+import java.util.Set;
+
+public interface QuizStudentAnswerDAO extends JpaRepository<QuizStudentAnswer,Long> {
+    Set<QuizStudentAnswer> findAllByQuiz(Quiz quiz);
+    Set<QuizStudentAnswer> findAllByStudent(User student);
+    Set<QuizStudentAnswer> findByStudentAndQuiz(User student, Quiz quiz);
+    Set<QuizStudentAnswer> findByStudentAndQuestionId(User student, String questionId);
+}

+ 7 - 0
src/main/java/nju/seec/helper/dao/StudentScoreDAO.java

@@ -0,0 +1,7 @@
+package nju.seec.helper.dao;
+
+import nju.seec.helper.entity.StudentScore;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface StudentScoreDAO extends JpaRepository<StudentScore,Long> {
+}

+ 20 - 0
src/main/java/nju/seec/helper/dto/QuestionStudentAnswerDTO.java

@@ -0,0 +1,20 @@
+package nju.seec.helper.dto;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import nju.seec.helper.util.enums.QuestionKindState;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.Map;
+
+@Data
+public class QuestionStudentAnswerDTO {
+    @JsonProperty(value = "id")
+    @NotBlank
+    private String questionId;
+    @JsonProperty(value = "studentAnswer")
+    @NotNull
+    private Object studentAnswer;
+}

+ 10 - 0
src/main/java/nju/seec/helper/dto/QuizAnswerDTO.java

@@ -0,0 +1,10 @@
+package nju.seec.helper.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class QuizAnswerDTO {
+    List<QuestionStudentAnswerDTO> answers;
+}

+ 25 - 0
src/main/java/nju/seec/helper/dto/QuizDTO.java

@@ -0,0 +1,25 @@
+package nju.seec.helper.dto;
+
+import lombok.Data;
+import nju.seec.helper.util.enums.SlideState;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ *
+ * @author sheen
+ */
+@Data
+public class QuizDTO {
+    @NotBlank
+    String name;
+    @NotNull
+    SlideState quizTime;
+    @NotBlank
+    String slideId;
+    @NotBlank
+    List<String> questions;
+
+}

+ 22 - 0
src/main/java/nju/seec/helper/dto/QuizUpdateDTO.java

@@ -0,0 +1,22 @@
+package nju.seec.helper.dto;
+
+import lombok.Data;
+import nju.seec.helper.util.enums.SlideState;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * @author sheen
+ */
+
+@Data
+public class QuizUpdateDTO {
+    String id;
+    String name;
+    SlideState quizTime;
+    String slideId;
+    List<String> questions;
+
+}

+ 66 - 0
src/main/java/nju/seec/helper/entity/Quiz.java

@@ -0,0 +1,66 @@
+package nju.seec.helper.entity;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import nju.seec.helper.util.enums.QuizState;
+import nju.seec.helper.util.enums.SlideState;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Data
+@Entity
+@Table(name = "quiz",indexes = {@Index(columnList = "course"),@Index(columnList = "slide")}
+        , uniqueConstraints = @UniqueConstraint(name = "测试名约束", columnNames = {"teacher", "name"}))
+@EntityListeners(AuditingEntityListener.class)
+public class Quiz {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Long id;
+
+    @Basic
+    @Column(name = "name", nullable = false)
+    private String name;
+
+    @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
+    @JoinColumn(name="slide",referencedColumnName="id")
+    private Slide slide;
+
+    @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
+    @JoinColumn(name="course",referencedColumnName="id")
+    private Course course;
+
+    @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
+    @JoinColumn(name="teacher",referencedColumnName="id")
+    private User teacher;
+
+    @ElementCollection
+    private List<String> questions;
+
+    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+    @JoinColumn(name="studentScore")
+    private List<StudentScore> studentScore;
+
+    @Enumerated(value = EnumType.STRING)
+    @Column(name = "quizTime")
+    private SlideState quizTime = SlideState.BEFORE_CLASS;
+    @Enumerated(value = EnumType.STRING)
+    @Column(name = "state",nullable = false)
+    private QuizState state;
+
+    @Basic
+    @Column(name = "create_at",nullable = false)
+    @CreatedDate
+    private LocalDateTime createAt;
+
+    @Basic
+    @Column(name = "modified_at",nullable = false)
+    @CreatedDate
+    private LocalDateTime modifiedAt;
+}

+ 58 - 0
src/main/java/nju/seec/helper/entity/QuizStudentAnswer.java

@@ -0,0 +1,58 @@
+package nju.seec.helper.entity;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@Data
+@Entity
+@Builder
+@Table(name = "quiz_student_answer",
+        indexes = {@Index(columnList = "quiz"),
+                    @Index(columnList = "student ,quiz")})
+@EntityListeners(AuditingEntityListener.class)
+@AllArgsConstructor
+@NoArgsConstructor
+public class QuizStudentAnswer {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Long id;
+
+    @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
+    @JoinColumn(name="quiz",referencedColumnName="id")
+    private Quiz quiz;
+
+    @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
+    @JoinColumn(name="student",referencedColumnName="id")
+    private User student;
+
+    @Basic
+    @Column(name = "questionId",nullable = false)
+    private String questionId;
+
+    @Basic
+    @Column(name = "create_at",nullable = false)
+    @CreatedDate
+    private LocalDateTime createAt;
+
+    @Basic
+    @Column(name = "modified_at",nullable = false)
+    @CreatedDate
+    private LocalDateTime modifiedAt;
+
+    @Basic
+    @Column(name = "student_answer",nullable = true)
+    private String studentAnswer;
+
+    @Basic
+    @Column(name = "pass", nullable = true)
+    private Boolean pass;
+}

+ 26 - 0
src/main/java/nju/seec/helper/entity/StudentScore.java

@@ -0,0 +1,26 @@
+package nju.seec.helper.entity;
+
+import lombok.Data;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.Map;
+
+@Data
+@Entity
+@Table(name = "student_score")
+public class StudentScore implements Serializable {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Long id;
+
+    @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
+    @JoinColumn(name="student",referencedColumnName="id")
+    private User student;
+
+    @Column(name = "quiz_id")
+    private Long quizId;
+    @Column(name = "score")
+    private Integer score;
+}

+ 34 - 0
src/main/java/nju/seec/helper/service/QuestionService.java

@@ -0,0 +1,34 @@
+package nju.seec.helper.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import nju.seec.helper.dto.LoginUser;
+import nju.seec.helper.vo.quiz.BaseQuestionVO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface QuestionService {
+    /**
+     * @param user
+     * @param stem
+     * @param pageable
+     * @return
+     * @throws JsonProcessingException
+     */
+    Page<BaseQuestionVO> getQuestionList(LoginUser user, String stem, Pageable pageable) throws JsonProcessingException;
+
+    /**
+     * @param questionId
+     * @return
+     */
+    BaseQuestionVO getQuestion(String questionId);
+
+    /**
+     * @param newQuestion
+     * @return
+     */
+    BaseQuestionVO newQuestion(BaseQuestionVO newQuestion);
+
+    BaseQuestionVO updateQuestion(BaseQuestionVO newQuestion);
+
+    void deleteQuestion(String questionId);
+}

+ 111 - 0
src/main/java/nju/seec/helper/service/QuizService.java

@@ -0,0 +1,111 @@
+package nju.seec.helper.service;
+
+import nju.seec.helper.dto.QuestionStudentAnswerDTO;
+import nju.seec.helper.entity.Quiz;
+import nju.seec.helper.entity.Slide;
+import nju.seec.helper.util.enums.QuizState;
+import nju.seec.helper.util.enums.SlideState;
+import nju.seec.helper.util.enums.UserType;
+import nju.seec.helper.vo.quiz.QuizResultVO;
+import nju.seec.helper.vo.quiz.QuizVO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public interface QuizService {
+
+    /**
+     * @param name
+     * @param uid
+     * @param quizTime
+     * @param slideId
+     * @param questions
+     * @return
+     */
+    Quiz createQuiz(String name, Long uid, SlideState quizTime,
+                    Long slideId, List<String> questions);
+
+    /**
+     * @param quizId
+     * @param uid
+     * @param answers
+     * @return
+     */
+    Integer upStudentAnswer(Long quizId, Long uid, List<QuestionStudentAnswerDTO> answers);
+
+    /**
+     * @param slideId
+     * @param pageable
+     * @param state
+     * @param uid
+     * @return
+     */
+    Page<Quiz> getAllBasicQuizBySlide(Long slideId, Pageable pageable, QuizState state, Long uid);
+
+    /**
+     * @param quiz
+     * @param slide
+     * @return
+     */
+    QuizState checkQuizState(Quiz quiz, Slide slide);
+
+    /**
+     * @param quizId
+     * @param slideId
+     * @return
+     */
+    QuizState checkQuizState(Long quizId, Long slideId);
+
+    /**
+     * @param quizId
+     * @return
+     */
+    QuizVO combineQuiz(Long quizId);
+
+    /**
+     * @param quizId
+     * @param uid
+     * @param type
+     * @return
+     */
+    QuizVO combineQuizWithAnswer(Long quizId, Long uid, UserType type);
+
+    /**
+     * @param quizId
+     * @return
+     */
+    String deleteQuiz(Long quizId);
+
+    /**
+     * @param quizId
+     * @param name
+     * @param questions
+     * @param quizTime
+     * @param slideId
+     * @return
+     */
+    Quiz modifyQuiz(Long quizId, String name, List<String> questions, SlideState quizTime, String slideId);
+
+    /**
+     * @param pageable
+     * @param state
+     * @param uid
+     * @return
+     */
+    Page<Quiz> getAllBasicQuizByUser(Pageable pageable, QuizState state, Long uid);
+
+    /**
+     * @param aLong
+     * @return
+     */
+    List<QuizResultVO> getQuizResult(Long aLong);
+
+    /**
+     * @param slide
+     */
+    void modifySlideState(Slide slide);
+}

+ 3 - 3
src/main/java/nju/seec/helper/service/impl/CodeServiceImpl.java

@@ -1,7 +1,7 @@
 package nju.seec.helper.service.impl;
 
 import nju.seec.helper.service.CodeService;
-import nju.seec.helper.util.CacheUtils;
+import nju.seec.helper.util.RedisCacheUtils;
 import nju.seec.helper.util.Consts;
 import nju.seec.helper.util.MailUtils;
 import nju.seec.helper.util.SmsUtils;
@@ -20,9 +20,9 @@ public class CodeServiceImpl implements CodeService {
 
     private final MailUtils mailUtils;
     private final SmsUtils smsUtils;
-    private final CacheUtils cacheUtils;
+    private final RedisCacheUtils cacheUtils;
 
-    public CodeServiceImpl(MailUtils mailUtils, SmsUtils smsUtils, CacheUtils cacheUtils) {
+    public CodeServiceImpl(MailUtils mailUtils, SmsUtils smsUtils, RedisCacheUtils cacheUtils) {
         this.mailUtils = mailUtils;
         this.smsUtils = smsUtils;
         this.cacheUtils = cacheUtils;

+ 68 - 0
src/main/java/nju/seec/helper/service/impl/QuestionServiceImpl.java

@@ -0,0 +1,68 @@
+package nju.seec.helper.service.impl;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import nju.seec.helper.dto.LoginUser;
+import nju.seec.helper.service.QuestionService;
+import nju.seec.helper.util.BokUtil;
+import nju.seec.helper.util.enums.ExceptionType;
+import nju.seec.helper.util.exception.HelperException;
+import nju.seec.helper.vo.quiz.BaseQuestionVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class QuestionServiceImpl implements QuestionService {
+    private final BokUtil bokUtil;
+
+    @Autowired
+    public QuestionServiceImpl(BokUtil bokUtil) {
+        this.bokUtil = bokUtil;
+    }
+
+    @Override
+    public Page<BaseQuestionVO> getQuestionList(LoginUser user, String stem, Pageable pageable) {
+        //应该对象化
+        List<BaseQuestionVO> questions = new ArrayList<>();
+        Map page = new HashMap();
+
+        bokUtil.bokFindByStemLike(stem, questions,page,pageable);
+        return new PageImpl<>(questions,pageable,Long.valueOf((Integer)page.get("totalElements")));
+    }
+
+    @Override
+    public BaseQuestionVO getQuestion(String questionId) {
+        return bokUtil.bokFindById(questionId);
+    }
+
+    @Override
+    public BaseQuestionVO newQuestion(BaseQuestionVO newQuestion) {
+        try {
+            return bokUtil.postNewQuestion(newQuestion);
+        } catch (JsonProcessingException e) {
+            throw HelperException.of(ExceptionType.ERROR,"请求BOK题库数据错误:"+e.getMessage());
+        }
+    }
+
+    @Override
+    public BaseQuestionVO updateQuestion(BaseQuestionVO newQuestion) {
+        try {
+            return bokUtil.putQuestion(newQuestion);
+        } catch (JsonProcessingException e) {
+            throw HelperException.of(ExceptionType.ERROR,"请求BOK题库数据错误:"+e.getMessage());
+        }
+    }
+
+    @Override
+    public void deleteQuestion(String questionId) {
+        bokUtil.deleteQuestion(questionId);
+    }
+
+}

+ 347 - 0
src/main/java/nju/seec/helper/service/impl/QuizServiceImpl.java

@@ -0,0 +1,347 @@
+package nju.seec.helper.service.impl;
+
+import nju.seec.helper.dao.*;
+import nju.seec.helper.dto.QuestionStudentAnswerDTO;
+import nju.seec.helper.entity.*;
+import nju.seec.helper.service.QuizService;
+import nju.seec.helper.util.BokUtil;
+import nju.seec.helper.util.enums.ExceptionType;
+import nju.seec.helper.util.enums.QuizState;
+import nju.seec.helper.util.enums.SlideState;
+import nju.seec.helper.util.enums.UserType;
+import nju.seec.helper.util.exception.HelperException;
+import nju.seec.helper.vo.quiz.BaseQuestionVO;
+import nju.seec.helper.vo.quiz.QuizResultVO;
+import nju.seec.helper.vo.quiz.QuizVO;
+import org.springframework.beans.factory.annotation.Autowired;
+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.util.Assert;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class QuizServiceImpl implements QuizService {
+    private final SlideDAO slideDAO;
+    private final QuizDAO quizDAO;
+    private final QuizStudentAnswerDAO quizStudentAnswerDAO;
+    private final ChooseDAO chooseDAO;
+    private final CourseDAO courseDAO;
+    private final UserDAO userDAO;
+    private final BokUtil bokUtil;
+
+    @Autowired
+    public QuizServiceImpl(SlideDAO slideDAO, QuizDAO quizDAO, QuizStudentAnswerDAO quizStudentAnswerDAO, ChooseDAO chooseDAO, CourseDAO courseDAO, UserDAO userDAO, BokUtil bokUtil) {
+        this.slideDAO = slideDAO;
+        this.quizDAO = quizDAO;
+        this.quizStudentAnswerDAO = quizStudentAnswerDAO;
+        this.chooseDAO = chooseDAO;
+        this.courseDAO = courseDAO;
+        this.userDAO = userDAO;
+        this.bokUtil = bokUtil;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Quiz createQuiz(String name, Long uid, SlideState quizTime,
+                           Long slideId, List<String> questions){
+        User teacher =new User();
+        teacher.setId(uid);
+        Slide slide = slideDAO.findSlideById(slideId);
+        Course course = new Course();
+        course.setId(slide.getCourseId());
+        Quiz quiz = new Quiz();
+
+        quiz.setCourse(course);
+        quiz.setName(name);
+        quiz.setQuestions(questions);
+        quiz.setQuizTime(quizTime);
+        quiz.setSlide(slide);
+        quiz.setTeacher(teacher);
+
+        checkQuizState(quiz,slide);
+
+        quiz=quizDAO.saveAndFlush(quiz);
+        return quiz;
+    }
+    @Override
+    public String deleteQuiz(Long quizId) {
+        Quiz quiz =quizDAO.findQuizById(quizId);
+        if(quiz.getState()!=QuizState.NOT_STARTED){throw HelperException.of(ExceptionType.FORBIDDEN,"不能删除已经开始的测试!");}
+        quizDAO.delete(quiz);
+        return "delete success";
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Quiz modifyQuiz(Long quizId, String name, List<String> questions, SlideState quizTime, String slideId) {
+
+        Quiz quiz =quizDAO.findQuizById(quizId);
+        if(quiz.getState()!=QuizState.NOT_STARTED){throw HelperException.of(ExceptionType.FORBIDDEN,"不能修改已经开始的测试!");}
+        if(name!=null) {
+            quiz.setName(name);
+        }
+        if(questions!=null) {
+            quiz.setQuestions(questions);
+        }
+        if(quizTime!=null) {
+            quiz.setQuizTime(quizTime);
+            checkQuizState(quiz,quiz.getSlide());
+        }
+        return quizDAO.saveAndFlush(quiz);
+
+    }
+
+    @Override
+    public Page<Quiz> getAllBasicQuizByUser(Pageable pageable, QuizState state, Long uid) {
+        User user=userDAO.findUserById(uid);
+        //todo: 这里写的太狗屎了,可以用重写sql或者用实例查询
+        if(user.getType().equals(UserType.STUDENT)){
+            final Set<Long> courseIdsByStudentId = chooseDAO.findCourseIdsByStudentId(uid);
+            final Set<Course> collect = courseIdsByStudentId.stream().map(i -> {
+                Course c = new Course();
+                c.setId(i);
+                return c;
+            }).collect(Collectors.toSet());
+            if(state!=null){
+                return quizDAO.findAllByCourseInAndState(collect,state,pageable);
+            }else{
+                return quizDAO.findAllByCourseIn(collect,pageable);
+            }
+        }else{
+            if(state!=null){
+                return quizDAO.findByTeacherAndState(user,state,pageable);
+            }else{
+                return quizDAO.findByTeacher(user,pageable);
+            }
+
+        }
+
+    }
+
+    @Override
+    public List<QuizResultVO> getQuizResult(Long quizId) {
+        Assert.notNull(quizId,"错误的测试!");
+        final Quiz quiz = quizDAO.findQuizById(quizId);
+        if(quiz.getState()!=QuizState.CLOSED){throw new RuntimeException("测试未关闭,无法得到测试结果");}
+        List<QuizResultVO> retList=new ArrayList<>();
+        final List<StudentScore> studentScoreList = quiz.getStudentScore();
+        Map<Long,List<QuizStudentAnswer>> idQuizStudentAnswerMap=new HashMap<>();
+        for(QuizStudentAnswer qsa:quizStudentAnswerDAO.findAllByQuiz(quiz)){
+            idQuizStudentAnswerMap.computeIfAbsent(qsa.getStudent().getId(), k -> new ArrayList<>());
+            idQuizStudentAnswerMap.get(qsa.getStudent().getId()).add(qsa);
+        }
+        for(StudentScore studentScore:studentScoreList){
+            final User student = studentScore.getStudent();
+            final List<QuizStudentAnswer> list = idQuizStudentAnswerMap.get(student.getId());
+            retList.add(new QuizResultVO(studentScore,student,list));
+        }
+        return retList;
+    }
+
+    @Override
+    public void modifySlideState(Slide slide) {
+        final List<Quiz> quizzes = quizDAO.findAllBySlide(slide);
+        quizzes.forEach(quiz->checkQuizState(quiz,slide));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Integer upStudentAnswer(Long quizId, Long uid, List<QuestionStudentAnswerDTO> answers) {
+        Assert.notNull(quizId,"错误的测试!");
+        final Quiz quiz = quizDAO.findQuizById(quizId);
+        if(quiz.getState()!=QuizState.ONGOING){throw HelperException.of(ExceptionType.FORBIDDEN,"不能提交尚未开始的作业!");}
+        User student =new User();student.setId(uid);
+        Set<QuizStudentAnswer> quizAnswers = quizStudentAnswerDAO.findByStudentAndQuiz(student,quiz);
+        Set<String> questionIdSet = new HashSet<>(quiz.getQuestions());
+        Map<String,QuizStudentAnswer> qsaMap=new HashMap<>(questionIdSet.size());
+        for(QuizStudentAnswer qsa: quizAnswers){
+            qsaMap.put(qsa.getQuestionId(),qsa);
+        }
+        //确定被更新或者新建的学生答案提交
+        List<QuizStudentAnswer> quizStudentAnswers=new ArrayList<>();
+        for(QuestionStudentAnswerDTO studentAnswer:answers){
+            String id = studentAnswer.getQuestionId();
+            String answer = studentAnswer.getStudentAnswer().toString();
+            if(!questionIdSet.contains(id)){break;}
+            if(qsaMap.get(id)!=null){
+                if(qsaMap.get(id).getStudentAnswer().equals(answer)){
+                    continue;
+                }
+                qsaMap.get(id).setStudentAnswer(answer);
+                System.out.println(answer);
+                quizStudentAnswers.add(qsaMap.get(id));
+            }else {
+                QuizStudentAnswer qsa=QuizStudentAnswer.builder()
+                        .questionId(id).quiz(quiz).student(student).studentAnswer(answer)
+                        .build();
+                quizStudentAnswers.add(qsa);
+            }
+
+        }
+        Integer savedNum=quizStudentAnswers.size();
+        quizStudentAnswerDAO.saveAll(quizStudentAnswers);
+        return savedNum;
+    }
+
+    @Override
+    public Page<Quiz> getAllBasicQuizBySlide(Long slideId, Pageable pageable, QuizState state, Long uid) {
+        Assert.notNull(slideId,"错误的幻灯片!");
+        Slide slide=new Slide();
+        slide.setId(slideId);
+        if(state!=null) {
+            return quizDAO.findAllBySlideAndState(slide, state, pageable);
+        }
+        else {
+            return quizDAO.findAllBySlide(slide, pageable);
+        }
+    }
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public QuizState checkQuizState(Quiz quiz, Slide slide) {
+        int quizTime = quiz.getQuizTime().getNum();
+        int slideTime = slide.getState().getNum();
+        if (quiz.getId() == null){
+            //未持久化的quiz
+            quiz.setState(slideTime < quizTime ? QuizState.NOT_STARTED : QuizState.ONGOING);
+        }
+        else {
+            //持久化后,不仅仅需要推进状态,还需要判分,保存等步骤
+            if (slideTime < quizTime) {
+                if(quiz.getState().equals(QuizState.NOT_STARTED)){return QuizState.NOT_STARTED;}
+                //未开始
+                quiz.setState(QuizState.NOT_STARTED);
+            } else if (slideTime == quizTime) {
+                if(quiz.getState().equals(QuizState.ONGOING)){return QuizState.ONGOING;}
+                quiz.setState(QuizState.ONGOING);
+            } else {
+                if(quiz.getState().equals(QuizState.CLOSED)){return QuizState.CLOSED;}
+                quiz.setState(QuizState.CLOSED);
+                Set<QuizStudentAnswer> quizAnswers = quizStudentAnswerDAO.findAllByQuiz(quiz);
+                final Set<Long> studentIds = chooseDAO.findStudentIdsByCourseId(slide.getCourseId());
+                Student2QuestionId2Answer triadMap = new Student2QuestionId2Answer(studentIds, quiz.getQuestions().size());
+                //将已经作答的学生答案加入
+                for (QuizStudentAnswer answers : quizAnswers) {
+                    triadMap.addAnswer(answers);
+                }
+                //生成未作答的学生答案
+                triadMap.fullAnswer(quiz);
+                //检查答案,赋分
+                triadMap.checkAnswerAndFullPass(bokUtil.bokFindByIdIn(quiz.getQuestions()), quiz);
+                //保存现场
+                quizStudentAnswerDAO.saveAll(triadMap.getAll());
+            }
+
+            quizDAO.save(quiz);
+        }
+        return quiz.getState();
+    }
+    @Override
+    public QuizState checkQuizState(Long quizId, Long slideId){
+        Quiz quiz=quizDAO.findQuizById(quizId);
+        Slide slide=slideDAO.findSlideById(slideId);
+        return checkQuizState(quiz,slide);
+    }
+    private class Student2QuestionId2Answer {
+        Map<Long,Map<String,QuizStudentAnswer>> student2QuestionId2Answer;
+        Student2QuestionId2Answer(Set<Long> studentIds, Integer quizQuestionSize) {
+            student2QuestionId2Answer =new LinkedHashMap<>(studentIds.size());
+            for(Long studentId:studentIds){
+                Map<String,QuizStudentAnswer> questionId2Answer= new LinkedHashMap<>(quizQuestionSize);
+                student2QuestionId2Answer.put(studentId,questionId2Answer);
+            }
+        }
+        void addAnswer(QuizStudentAnswer answers){
+            Map<String, QuizStudentAnswer> stringQuizStudentAnswerMap = student2QuestionId2Answer.get(answers.getStudent().getId());
+            if(stringQuizStudentAnswerMap==null){
+                return ;
+            }
+            stringQuizStudentAnswerMap.put(answers.getQuestionId(),answers);
+        }
+        void fullAnswer(Quiz quiz){
+            List<String> questions=quiz.getQuestions();
+            student2QuestionId2Answer.forEach((studentId, map)->{
+                if(questions.size()!=map.size()){
+                    for(String questionId:questions){
+                        if(map.get(questionId)==null){
+                            User student =new User();student.setId(studentId);
+                            QuizStudentAnswer qsa=QuizStudentAnswer.builder()
+                                    .questionId(questionId).quiz(quiz).student(student)
+                                    .build();
+                            map.put(questionId,qsa);
+                        }
+                    }
+                }
+            });
+        }
+        void checkAnswerAndFullPass(List<BaseQuestionVO> questionVos, Quiz quiz){
+            if(quiz.getStudentScore()==null){quiz.setStudentScore(new ArrayList<>(student2QuestionId2Answer.size()));}
+            student2QuestionId2Answer.forEach((studentId, map)->{
+                int passCount=0;
+                for(BaseQuestionVO vo:questionVos){
+                    final String studentAnswer = map.get(vo.getQuestionId()).getStudentAnswer();
+                    boolean pass=vo.checkAnswer(studentAnswer);
+                    if(pass){passCount++;}
+                    map.get(vo.getQuestionId()).setPass(pass);
+                }
+                Double score = (100.0*passCount)/questionVos.size();
+                User student =new User();student.setId(studentId);
+                StudentScore studentScore=new StudentScore();
+                studentScore.setQuizId(quiz.getId());
+                studentScore.setScore(Long.valueOf(Math.round(score)).intValue());
+                studentScore.setStudent(student);
+                quiz.getStudentScore().add(studentScore);
+            });
+        }
+        List<QuizStudentAnswer> getAll(){
+            List<QuizStudentAnswer> list=new ArrayList<>();
+            for(Long sid: student2QuestionId2Answer.keySet()){
+                list.addAll(student2QuestionId2Answer.get(sid).values());
+            }
+            return list;
+        }
+    }
+    @Override
+    public QuizVO combineQuiz(Long quizId){
+        Assert.notNull(quizId,"错误的测试!");
+        final Quiz quizById = quizDAO.findQuizById(quizId);
+        final List<BaseQuestionVO> questionVoS = bokUtil.bokFindByIdIn(quizById.getQuestions());
+        return new QuizVO(quizById,questionVoS);
+    }
+    @Override
+    public QuizVO combineQuizWithAnswer(Long quizId, Long uid,UserType userType){
+        Assert.notNull(quizId,"错误的测试!");
+        final Quiz quizById = quizDAO.findQuizById(quizId);
+        User student =new User();
+        student.setId(uid);
+
+        if(userType==null||userType.equals(UserType.TEACHER)){
+            //向老师展示
+            List<BaseQuestionVO> questionVoS = bokUtil.bokFindByIdIn(quizById.getQuestions());
+            return new QuizVO(quizById,questionVoS);
+        }
+        if(quizById.getState().equals(QuizState.NOT_STARTED)){
+            //未开始看不到题目
+            return new QuizVO(quizById);
+        }
+
+        List<BaseQuestionVO> questionVos = bokUtil.bokFindByIdIn(quizById.getQuestions());
+        final Set<QuizStudentAnswer> quizAnswers = quizStudentAnswerDAO.findByStudentAndQuiz(student,quizById);
+        Map<String,QuizStudentAnswer> qsaMap=new HashMap<>(quizAnswers.size());
+        for(QuizStudentAnswer qsa: quizAnswers){
+            qsaMap.put(qsa.getQuestionId(),qsa);
+        }
+        for(BaseQuestionVO vo:questionVos){
+            if(qsaMap.get(vo.getQuestionId())!=null){
+                vo.exAddAnswer(qsaMap.get(vo.getQuestionId()));
+            }
+        }
+
+        return new QuizVO(quizById,questionVos);
+    }
+
+
+}

+ 8 - 3
src/main/java/nju/seec/helper/service/impl/SlideServiceImpl.java

@@ -14,10 +14,11 @@ import nju.seec.helper.dto.SlideStateDTO;
 import nju.seec.helper.entity.Course;
 import nju.seec.helper.entity.Slide;
 import nju.seec.helper.service.MessageService;
+import nju.seec.helper.service.QuizService;
 import nju.seec.helper.service.SlideService;
 import nju.seec.helper.service.util.AuthUtils;
 import nju.seec.helper.service.util.StringUtils;
-import nju.seec.helper.util.CacheUtils;
+import nju.seec.helper.util.RedisCacheUtils;
 import nju.seec.helper.util.Consts;
 import nju.seec.helper.util.FileUtils;
 import nju.seec.helper.util.enums.ExceptionType;
@@ -51,12 +52,13 @@ public class SlideServiceImpl implements SlideService {
     private final ChooseDAO chooseDAO;
     private final UserDAO userDAO;
 
+    private final QuizService quizService;
     private final FileUtils fileUtils;
-    private final CacheUtils cacheUtils;
+    private final RedisCacheUtils cacheUtils;
 
     private final MessageService messageService;
 
-    public SlideServiceImpl(CourseDAO courseDAO, SlideDAO slideDAO, ChooseDAO chooseDAO, UserDAO userDAO, FileUtils fileUtils, CacheUtils cacheUtils, MessageService messageService) {
+    public SlideServiceImpl(CourseDAO courseDAO, SlideDAO slideDAO, ChooseDAO chooseDAO, UserDAO userDAO, FileUtils fileUtils, RedisCacheUtils cacheUtils, MessageService messageService, QuizService quizService) {
         this.courseDAO = courseDAO;
         this.slideDAO = slideDAO;
         this.chooseDAO = chooseDAO;
@@ -64,6 +66,7 @@ public class SlideServiceImpl implements SlideService {
         this.fileUtils = fileUtils;
         this.cacheUtils = cacheUtils;
         this.messageService = messageService;
+        this.quizService = quizService;
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -126,6 +129,8 @@ public class SlideServiceImpl implements SlideService {
 
         slide.setState(slideStateDTO.getState());
         slide = slideDAO.save(slide);
+        // 修改Slide状态同时推进quiz状态
+        quizService.modifySlideState(slide);
 
         // 增加通知
         if (slide.getState() == SlideState.IN_CLASS) {

+ 3 - 3
src/main/java/nju/seec/helper/service/impl/UserServiceImpl.java

@@ -8,7 +8,7 @@ import nju.seec.helper.dto.UserDTO;
 import nju.seec.helper.entity.User;
 import nju.seec.helper.service.UserService;
 import nju.seec.helper.service.util.AuthUtils;
-import nju.seec.helper.util.CacheUtils;
+import nju.seec.helper.util.RedisCacheUtils;
 import nju.seec.helper.util.Consts;
 import nju.seec.helper.util.EncryptUtils;
 import nju.seec.helper.util.enums.ExceptionType;
@@ -25,7 +25,7 @@ import java.util.Map;
  */
 @Service
 public class UserServiceImpl implements UserService {
-    private final CacheUtils cacheUtils;
+    private final RedisCacheUtils cacheUtils;
     private final UserDAO userDAO;
     private final static Map<String, UserType> EMAIL_SUFFIX_AND_USER_TYPE_MAP =
             ImmutableMap.of(
@@ -33,7 +33,7 @@ public class UserServiceImpl implements UserService {
                     "nju.edu.cn", UserType.TEACHER
             );
 
-    public UserServiceImpl(UserDAO userDAO, CacheUtils cacheUtils) {
+    public UserServiceImpl(UserDAO userDAO, RedisCacheUtils cacheUtils) {
         this.userDAO = userDAO;
         this.cacheUtils = cacheUtils;
     }

+ 262 - 0
src/main/java/nju/seec/helper/util/BokUtil.java

@@ -0,0 +1,262 @@
+package nju.seec.helper.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import lombok.Data;
+import nju.seec.helper.util.enums.ExceptionType;
+import nju.seec.helper.util.enums.QuestionKindState;
+import nju.seec.helper.util.exception.HelperException;
+import nju.seec.helper.vo.quiz.BaseQuestionVO;
+import nju.seec.helper.vo.quiz.ChoiceQuestionVO;
+import nju.seec.helper.vo.quiz.TrueOrFalseQuestionVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Isolation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.HttpClientErrorException;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Component
+public class BokUtil {
+    @Autowired
+    private RestRequestUtil restRequestUtil;
+    @Autowired
+    private GuavaCacheUtil cacheUtils;
+    @Value("${bok.url}")
+    private String bokUrl="http://bok.seecoder.cn";
+    private String searchUrl=bokUrl+"/api/tq/search/";
+    private String tqUrl=bokUrl+"/api/tq/";
+
+    public void bokFindByStemLike(String stem, List<BaseQuestionVO> questions, Map page, Pageable pageable) {
+
+        Map<String,String> urlParams=new HashMap<>();
+        urlParams.put("content",stem);
+        urlParams.put("sort",pageable.getSort().toString().replaceAll(" ","").replaceAll(":",","));
+        urlParams.put("size",String.valueOf(pageable.getPageSize()));
+        urlParams.put("page",String.valueOf(1+pageable.getPageNumber()));
+        try {
+            Map ret = (Map)restRequestUtil.sendGetRequest(
+                    searchUrl+"findByStemLike?content={content}&sort={sort}&size={size}&page={page}", urlParams);
+            List<Map> tqs= ((List)((Map)ret.get("_embedded")).get("choiceQuestions"));
+
+            List collect = parseBody(tqs);
+            try {
+                page.putAll((Map)ret.get("page"));
+                questions.addAll(collect);
+            }catch (Exception e){
+                e.printStackTrace();
+            }
+
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+    private static String CACHE_NAME="BOK";
+    public List<BaseQuestionVO> bokFindByIdIn(List<String> ids) {
+        //todo: 从缓存中取Ids
+        Set<String> requestIds = new HashSet<>(ids);
+        final ImmutableMap<Object, Object> bok = cacheUtils.multiGet(CACHE_NAME,requestIds );
+
+        Map<String, BaseQuestionVO> questionsMap = new HashMap<>(ids.size());
+        bok.values().forEach(vo -> {
+            if (vo instanceof BaseQuestionVO) {
+                BaseQuestionVO questionVO = (BaseQuestionVO) vo;
+                questionsMap.put(questionVO.getQuestionId(), questionVO);
+                requestIds.remove(questionVO.getQuestionId());
+            }
+        });
+        //未缓存的去这里拿
+        if(requestIds.size()>0){
+            Map<String, String> urlParams = new HashMap<>();
+            String idsStr = requestIds.stream().reduce((a, b) -> {
+                return a + "," + b;
+            }).orElse("");
+            urlParams.put("id", idsStr);
+            urlParams.put("size", String.valueOf(requestIds.size()));
+            urlParams.put("page", "1");
+            List<Map> tqs = null;
+            try {
+                Map ret = (Map) restRequestUtil.sendGetRequest(
+                        searchUrl + "findByIdIn?id={id}&size={size}&page={page}", urlParams);
+                tqs = ((List) ((Map) ret.get("_embedded")).get("choiceQuestions"));
+            } catch (Exception e) {
+                e.printStackTrace();
+                throw new RuntimeException("bok request error!for findByIdIn:" + Arrays.toString(requestIds.toArray()));
+            }
+            List<BaseQuestionVO> collect = parseBody(tqs);
+            Map<String,Object> newGet=new HashMap<>(collect.size());
+            for (BaseQuestionVO vo : collect) {
+                questionsMap.put(vo.getQuestionId(), vo);
+                newGet.put(vo.getQuestionId(),vo);
+            }
+            cacheUtils.setAll(CACHE_NAME,newGet);
+        }
+
+        // todo: 按照Ids重排序
+        List<BaseQuestionVO> ret=new ArrayList<>();
+        for (String id:ids){
+            ret.add(questionsMap.get(id));
+        }
+        return (ret);
+    }
+
+    private List parseBody(List<Map> tqs) {
+        final List collect = tqs.stream().map(BokUtil::parseOneQuestion).collect(Collectors.toList());
+        return collect;
+    }
+
+    private static BaseQuestionVO parseOneQuestion(Map q){
+        switch ((String) q.get("type")) {
+            case "choice":
+                return ChoiceQuestionVO.builder().stem((String) q.get("stem")).kind(QuestionKindState.CHOICE)
+                        .options((Map<String, String>) q.get("options")).answer(((String) q.get("answer")).replaceAll("【答案】", ""))
+                        .analysis((String) q.get("analysis")).questionId(String.valueOf((Integer) q.get("tqId"))).build();
+
+            case "true_false":
+                return TrueOrFalseQuestionVO.builder().stem((String) q.get("stem")).kind(QuestionKindState.TRUE_FALSE)
+                        .answer(Boolean.valueOf((String) q.get("answer")))
+                        .analysis((String) q.get("analysis")).questionId(String.valueOf((Integer) q.get("tqId"))).build();
+            default:
+        }
+        throw HelperException.of(ExceptionType.ERROR,"unknown type:" + (String) q.get("type"));
+    }
+    public BaseQuestionVO bokFindById(String questionId)  {
+        Map<String,String> urlParams=new HashMap<>();
+        Object cachedVo = cacheUtils.get(CACHE_NAME, questionId);
+        if (cachedVo instanceof BaseQuestionVO) {
+            return (BaseQuestionVO) cachedVo;
+        }
+        try {
+            Map ret = (Map)restRequestUtil.sendGetRequest(tqUrl+questionId, urlParams);
+            final BaseQuestionVO vo = parseOneQuestion(ret);
+            cacheUtils.set(CACHE_NAME, vo.getQuestionId(), vo);
+            return vo;
+        } catch (JsonProcessingException e) {
+            throw HelperException.of(ExceptionType.ERROR,"请求BOK题库数据错误:"+e.getMessage());
+        }catch (HttpClientErrorException e){
+            throw HelperException.of(ExceptionType.valueOf(e.getStatusCode()),"请求BOK题库数据错误:"+e.getMessage());
+        }
+    }
+    @Transactional(isolation=Isolation.SERIALIZABLE,rollbackFor = Exception.class)
+    public BaseQuestionVO postNewQuestion(BaseQuestionVO vo) throws JsonProcessingException {
+        if(vo.getQuestionId()!=null) {
+            throw HelperException.of(ExceptionType.PARAM_ERROR, "should POST a new question");
+        }
+        String questionId = null;
+        //我当时应该用uuid的,但是线上已经部署了,只能下次再改了(如果有下次
+        //因为线上用的也是Long Id 并且es不提供自增主键,先这样尝试插入
+        //之后对于新增bok的题目,应该有某种限制,未补全知识点的题目不允许插入
+        int tryNumber=100;
+        int randomLength=4;
+        for(int i=0;i<tryNumber;i++){
+            Date date=new Date();
+            Calendar calendar = Calendar.getInstance();
+            int year=calendar.get(Calendar.YEAR);String month=String.valueOf(1+calendar.get(Calendar.MONTH));String day=String.valueOf(calendar.get(Calendar.DATE));
+            if(month.length()<2){month="0"+month;}
+            if(day.length()<2){month="0"+month;}
+            Random randomGet=new Random();
+            StringBuilder random= new StringBuilder(String.valueOf(randomGet.nextInt(10000)));
+            while (random.length()<randomLength){
+                random.insert(0, "0");
+            }
+            questionId=""+(year-2010)+(month)+day+(random);
+            try {
+                Map ret = (Map)restRequestUtil.sendGetRequest(tqUrl+questionId, new HashMap<>());
+            } catch (HttpClientErrorException e){
+                if(e.getStatusCode().equals(HttpStatus.NOT_FOUND)){
+                    break;
+                }
+            }
+            questionId=null;
+        }
+        if(questionId==null){throw HelperException.of(ExceptionType.ERROR,"暂时无法保存题目,请重试!");}
+
+        vo.setQuestionId(questionId);
+
+        BokTq bokTq=new BokTq(vo);
+        restRequestUtil.sendPutRequest(tqUrl+questionId, bokTq);
+
+        return vo;
+
+    }
+    @Transactional(isolation=Isolation.SERIALIZABLE,rollbackFor = Exception.class)
+    public BaseQuestionVO putQuestion(BaseQuestionVO vo) throws JsonProcessingException {
+        if(vo.getQuestionId()==null ){
+            throw HelperException.of(ExceptionType.PARAM_ERROR, "should POST a present question");
+        }
+        String questionId=vo.getQuestionId();
+        try {
+            restRequestUtil.sendGetRequest(tqUrl+questionId, new HashMap<>());
+        } catch (HttpClientErrorException e){
+            if(e.getStatusCode().equals(HttpStatus.NOT_FOUND)){
+                throw HelperException.of(ExceptionType.PARAM_ERROR,"should POST a present question");
+            }
+        }
+        restRequestUtil.sendPutRequest(tqUrl+questionId, new BokTq(vo));
+
+        cacheUtils.remove(CACHE_NAME,questionId);
+        return vo;
+    }
+    public void deleteQuestion(String questionId) {
+        try {
+            restRequestUtil.sendDeleteRequest(tqUrl+questionId);
+        } catch (JsonProcessingException e) {
+            throw HelperException.of(ExceptionType.ERROR,"请求BOK题库数据错误:"+e.getMessage());
+        }
+        cacheUtils.remove(CACHE_NAME,questionId);
+    }
+
+    /**
+     * 来自restBok接口的对象
+     */
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @Data
+    class BokTq {
+        String id;
+        String type;
+        String answer;
+        @JsonProperty(value = "key_points")
+        String keyPoints;
+        String analysis;
+        String lastModified;
+        String stem;
+        Map options;
+        List tags;
+        List knowledgeId;
+
+        private BokTq(BaseQuestionVO vo) {
+            this.id = vo.getQuestionId();
+            this.stem = vo.getStem();
+            switch(vo.getKind()){
+
+                case CHOICE:
+                    ChoiceQuestionVO cqvo= (ChoiceQuestionVO) vo;
+                    this.options = cqvo.getOptions();
+                    this.type = "choice";
+                    this.analysis=cqvo.getAnalysis();
+                    this.answer=cqvo.getAnswer();
+                    break;
+                case TRUE_FALSE:
+                    TrueOrFalseQuestionVO tfvo= (TrueOrFalseQuestionVO) vo;
+                    this.options = null;
+                    this.type = "true_false";
+                    this.analysis=tfvo.getAnalysis();
+                    this.answer=tfvo.getAnswer().toString();
+                    break;
+                default:
+            }
+
+            this.keyPoints = null;
+            this.lastModified = null;
+            this.tags = null;
+            this.knowledgeId = null;
+        }
+    }
+}

+ 64 - 0
src/main/java/nju/seec/helper/util/GuavaCacheUtil.java

@@ -0,0 +1,64 @@
+package nju.seec.helper.util;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableMap;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+@Component
+public class GuavaCacheUtil {
+    private Cache<Object, Object> cache;
+
+    @Value("${guavaCache.maximumSize}")
+    private Long maximumSize=1000L;
+
+    @Value("${guavaCache.expireAfterAccessInSeconds}")
+    private Long expireAfterAccessInSeconds=1800L;
+
+    @Value("${guavaCache.expireAfterWriteInSeconds}")
+    private Long expireAfterWriteInSeconds=3600L;
+
+    public GuavaCacheUtil() {
+        CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
+        builder.maximumSize(maximumSize);
+        builder.expireAfterAccess(expireAfterAccessInSeconds, TimeUnit.SECONDS);
+        builder.expireAfterWrite(expireAfterWriteInSeconds, TimeUnit.SECONDS);
+        cache = builder.build();
+    }
+
+    public void set(String cacheName, String key, Object value) {
+        cache.put(combineKey(cacheName,key),value);
+    }
+
+    public void setAll(String cacheName, Map<String,Object> putAll) {
+        final Map<String, Object> all = putAll.entrySet().stream().collect(Collectors.toMap(entry -> {
+            return combineKey(cacheName , entry.getKey());
+        }, Map.Entry::getValue));
+        cache.putAll(all);
+    }
+    public Object get(String cacheName, String key) {
+        return cache.getIfPresent(combineKey(cacheName,key));
+    }
+
+    public ImmutableMap<Object, Object> multiGet(String cacheName, Collection<String> keys) {
+        Collection<String> body=keys.stream().map(s->combineKey(cacheName,s)).collect(Collectors.toList());
+        return cache.getAllPresent(body);
+    }
+
+    public void remove(String cacheName, String key) {
+        cache.invalidate(combineKey(cacheName,key));
+    }
+
+    private String combineKey(String cacheName, String key){
+        return cacheName + ":" + key;
+    }
+}

+ 13 - 3
src/main/java/nju/seec/helper/util/CacheUtils.java → src/main/java/nju/seec/helper/util/RedisCacheUtils.java

@@ -2,8 +2,11 @@ package nju.seec.helper.util;
 
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
 
+import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * use redis
@@ -11,21 +14,28 @@ import java.util.concurrent.TimeUnit;
  * @author cst
  */
 @Component
-public class CacheUtils {
+public class RedisCacheUtils {
     private final RedisTemplate<String, String> redisTemplate;
 
-    public CacheUtils(RedisTemplate<String, String> redisTemplate) {
+    public RedisCacheUtils(RedisTemplate<String, String> redisTemplate) {
         this.redisTemplate = redisTemplate;
     }
 
     public void set(String cacheName, String key, String value, long expireTime, TimeUnit timeUnit) {
         redisTemplate.opsForValue().set(cacheName + ":" + key, value, expireTime, timeUnit);
     }
-
+    public void setAll(String cacheName, String key, String value, long expireTime, TimeUnit timeUnit) {
+        throw new NotImplementedException();
+    }
     public String get(String cacheName, String key) {
         return redisTemplate.opsForValue().get(cacheName + ":" + key);
     }
 
+    public List<String> multiGet(String cacheName, List<String> keys) {
+        List<String> body=keys.stream().map(s->cacheName + ":" + s).collect(Collectors.toList());
+        return redisTemplate.opsForValue().multiGet(body);
+    }
+
     public void remove(String cacheName, String key) {
         redisTemplate.delete(cacheName + ":" + key);
     }

+ 38 - 0
src/main/java/nju/seec/helper/util/RestRequestUtil.java

@@ -0,0 +1,38 @@
+package nju.seec.helper.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.List;
+import java.util.Map;
+@Component
+public class RestRequestUtil {
+
+    public List sendPostRequest(String url, Object params) throws JsonProcessingException {
+        RestTemplate client = new RestTemplate();
+        HttpHeaders headers = new HttpHeaders();
+        HttpMethod method = HttpMethod.POST;
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        HttpEntity<Object> requestEntity = new HttpEntity<>(params, headers);
+        ResponseEntity ret = client.postForEntity(url, requestEntity, List.class);
+        return (List) ret.getBody();
+    }
+    public void sendPutRequest(String url, Object params) throws JsonProcessingException {
+        RestTemplate client = new RestTemplate();
+        client.put(url, params);
+    }
+    public void sendDeleteRequest(String url) throws JsonProcessingException {
+        RestTemplate client = new RestTemplate();
+        client.delete(url);
+    }
+    public Object sendGetRequest(String url, Map<String, String> urlParams) throws JsonProcessingException {
+        RestTemplate client = new RestTemplate();
+        HttpHeaders headers = new HttpHeaders();
+        HttpMethod method = HttpMethod.GET;
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        ResponseEntity ret = client.getForEntity(url,Object.class,urlParams);
+        return ret.getBody();
+    }
+}

+ 9 - 0
src/main/java/nju/seec/helper/util/enums/ExceptionType.java

@@ -1,5 +1,6 @@
 package nju.seec.helper.util.enums;
 
+import lombok.AllArgsConstructor;
 import lombok.Getter;
 import org.springframework.http.HttpStatus;
 
@@ -17,4 +18,12 @@ public enum ExceptionType {
     ExceptionType(HttpStatus status) {
         this.status = status;
     }
+    public static ExceptionType valueOf(HttpStatus status){
+        for(ExceptionType exceptionType:ExceptionType.values()){
+            if(exceptionType.getStatus().equals(status)){
+                return exceptionType;
+            }
+        }
+        return ExceptionType.ERROR;
+    }
 }

+ 20 - 0
src/main/java/nju/seec/helper/util/enums/QuestionKindState.java

@@ -0,0 +1,20 @@
+package nju.seec.helper.util.enums;
+
+/**
+ * @author cst
+ */
+public enum QuestionKindState {
+    CHOICE("选择题"), TRUE_FALSE("判断题");
+    private String name;
+    QuestionKindState(String name){
+        this.name=name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}

+ 20 - 0
src/main/java/nju/seec/helper/util/enums/QuizState.java

@@ -0,0 +1,20 @@
+package nju.seec.helper.util.enums;
+
+/**
+ * @author cst
+ */
+public enum QuizState {
+    NOT_STARTED("未开始"), ONGOING("正在进行"), CLOSED("已结束");
+    private String name;
+    QuizState(String name){
+        this.name=name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}

+ 9 - 1
src/main/java/nju/seec/helper/util/enums/SlideState.java

@@ -4,5 +4,13 @@ package nju.seec.helper.util.enums;
  * @author cst
  */
 public enum SlideState {
-    DRAFT, BEFORE_CLASS, IN_CLASS, AFTER_CLASS, FINISH
+    DRAFT(1), BEFORE_CLASS(2), IN_CLASS(3), AFTER_CLASS(4), FINISH(5);
+    private Integer num;
+    SlideState(Integer num){
+        this.num=num;
+    }
+
+    public Integer getNum() {
+        return num;
+    }
 }

+ 96 - 0
src/main/java/nju/seec/helper/vo/quiz/BaseQuestionVO.java

@@ -0,0 +1,96 @@
+package nju.seec.helper.vo.quiz;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import nju.seec.helper.entity.QuizStudentAnswer;
+import nju.seec.helper.util.enums.ExceptionType;
+import nju.seec.helper.util.enums.QuestionKindState;
+import nju.seec.helper.util.exception.HelperException;
+
+import java.util.Map;
+
+/**
+ * @author XuShengTao
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@SuperBuilder
+public abstract class BaseQuestionVO {
+    @JsonProperty(value = "id")
+    private String questionId;
+    @JsonProperty(value = "kind")
+    private QuestionKindState kind;
+    @JsonProperty(value = "stem")
+    private String stem;
+    public abstract void exAddAnswer(QuizStudentAnswer answer);
+    public abstract boolean checkAnswer(Object answer);
+    public abstract void excludeCorrectAnswer();
+    private static ObjectMapper objectMapper = new ObjectMapper();
+    public static BaseQuestionVO parse(Map question){
+        String kind = (String)question.get("kind");
+        BaseQuestionVO newQuestion=null;
+        switch (kind){
+            case "CHOICE":
+                newQuestion= (objectMapper.convertValue(question, ChoiceQuestionVO.class));
+                return newQuestion;
+            case "TRUE_FALSE":
+                newQuestion = (objectMapper.convertValue(question, TrueOrFalseQuestionVO.class));
+                return newQuestion;
+            default:
+        }
+        throw HelperException.of(ExceptionType.PARAM_ERROR,"parse failed:"+question.toString());
+    }
+}
+//QuestionSerializer
+//问题的详细定义
+//
+//Attribute
+//type QuestionSerializer =
+//  | ChoiceQuestionSerializer
+//  | TrueOrFalseQuestionSerializer;
+//
+//type QuestionWithoutAnswerSerializer = Omit<
+//  QuestionSerializer,
+//  "answer" | "analysis"
+//>;
+//
+//type QuestionWithStudentAnswerSerializer =
+//  | ChoiceQuestionWithStudentAnswer
+//  | TrueOrFalseQuestionWithStudentAnswer;
+//
+//interface Question {
+//  id: number; // 题目id
+//  kind: "CHOICE" | "TRUE_FALSE";
+//  stem: string; // 题干
+//}
+//
+//interface ChoiceQuestionSerializer extends Question {
+//  kind: "CHOICE";
+//  options: {
+//    [key: string]: string;
+//  };
+//  answer: string; // 答案 [与 options 的 key 一致]
+//  analysis?: string; // 解析
+//}
+//
+//interface ChoiceQuestionWithStudentAnswer extends ChoiceQuestionSerializer {
+//  studentAnswer: string;
+//  pass: boolean; // 是否正确
+//}
+//
+//interface TrueOrFalseQuestionSerializer extends Question {
+//  kind: "TRUE_FALSE";
+//  answer: boolean; // 答案
+//  analysis?: string; // 解析
+//}
+//
+//interface TrueOrFalseQuestionWithStudentAnswer
+//  extends TrueOrFalseQuestionSerializer {
+//  studentAnswer: boolean;
+//  pass: boolean; // 是否正确
+//}

+ 26 - 0
src/main/java/nju/seec/helper/vo/quiz/BokQuestion.java

@@ -0,0 +1,26 @@
+package nju.seec.helper.vo.quiz;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * @author sheen
+ */
+@Data
+public class BokQuestion {
+    @JsonProperty(value = "tq_id")
+    String tqId;
+    String type;
+    String stem;
+    Map<String,String> options;
+    String answer;
+    @JsonProperty(value = "key_points")
+    String keyPoints;
+    String analysis;
+    Collection<String> tags;
+    Collection<String> knowledgeId;
+    String lastModified;
+}

+ 52 - 0
src/main/java/nju/seec/helper/vo/quiz/ChoiceQuestionVO.java

@@ -0,0 +1,52 @@
+package nju.seec.helper.vo.quiz;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import nju.seec.helper.entity.QuizStudentAnswer;
+
+import java.util.Map;
+
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ChoiceQuestionVO extends BaseQuestionVO {
+    @JsonProperty(value = "options")
+    private Map<String,String> options;
+    @JsonProperty(value = "answer")
+    private String answer;
+    @JsonProperty(value = "analysis")
+    private String analysis;
+    @JsonProperty(value = "studentAnswer")
+    private String studentAnswer;
+    @JsonProperty(value = "pass")
+    private Boolean pass;
+
+    @Override
+    public void exAddAnswer(QuizStudentAnswer answer) {
+        this.studentAnswer=answer.getStudentAnswer();
+        this.pass = answer.getPass();
+    }
+
+    @Override
+    public boolean checkAnswer(Object answer) {
+        if(answer==null){return false;}
+        return this.answer.toLowerCase().equals(answer.toString().toLowerCase());
+    }
+
+    @Override
+    public void excludeCorrectAnswer() {
+        this.answer=null;
+        this.pass=null;
+        this.analysis=null;
+    }
+}
+//kind: "CHOICE";
+//  options: {
+//    [key: string]: string;
+//  };
+//  answer: string; // 答案 [与 options 的 key 一致]
+//  analysis?: string; // 解析

+ 29 - 0
src/main/java/nju/seec/helper/vo/quiz/QuizResultVO.java

@@ -0,0 +1,29 @@
+package nju.seec.helper.vo.quiz;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import lombok.Data;
+import nju.seec.helper.entity.QuizStudentAnswer;
+import nju.seec.helper.entity.StudentScore;
+import nju.seec.helper.entity.User;
+import nju.seec.helper.vo.UserVO;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Data
+public class QuizResultVO {
+    @JsonUnwrapped
+    UserVO student;
+    Integer score;
+    Map<String,Boolean> detail;
+
+    public QuizResultVO(StudentScore studentScore, User student, List<QuizStudentAnswer> list) {
+        this.student=new UserVO(student);
+        this.score=studentScore.getScore();
+        this.detail=list.stream().collect(
+                Collectors.toMap(QuizStudentAnswer::getQuestionId,QuizStudentAnswer::getPass));
+    }
+}

+ 94 - 0
src/main/java/nju/seec/helper/vo/quiz/QuizVO.java

@@ -0,0 +1,94 @@
+package nju.seec.helper.vo.quiz;
+
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import nju.seec.helper.entity.Quiz;
+import nju.seec.helper.util.enums.QuizState;
+import nju.seec.helper.util.enums.SlideState;
+import nju.seec.helper.vo.CourseVO;
+import nju.seec.helper.vo.SlideVO;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class QuizVO {
+    @JsonProperty(value = "id")
+    private String quizId;
+    @JsonProperty(value = "name")
+    private String name;
+    @JsonProperty(value = "state")
+    private QuizState state;
+    @JsonProperty(value = "course")
+    private CourseVO courseVO;
+    @JsonProperty(value = "quizTime")
+    private SlideState quizTime;
+    @JsonProperty(value = "slide")
+    private SlideVO slide;
+    @JsonProperty(value = "questionNumber")
+    private Integer questionNumber;
+    @JsonProperty(value = "questions")
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private List<BaseQuestionVO> questions;
+
+    public QuizVO(Quiz quiz,List<BaseQuestionVO> questions) {
+        this.quizId = quiz.getId().toString();
+        this.name = quiz.getName();
+        this.state = quiz.getState();
+        this.courseVO = new CourseVO(quiz.getCourse());
+        this.quizTime = quiz.getQuizTime();
+        this.slide = new SlideVO(quiz.getSlide());
+        this.questionNumber = questions.size();
+        this.questions = questions;
+    }
+    public QuizVO(Quiz quiz) {
+        this.quizId = quiz.getId().toString();
+        this.name = quiz.getName();
+        this.state = quiz.getState();
+        this.courseVO = new CourseVO(quiz.getCourse());
+        this.quizTime = quiz.getQuizTime();
+        this.slide = new SlideVO(quiz.getSlide());
+        this.questionNumber = quiz.getQuestions().size();
+    }
+    public void excludeCorrectAnswer(){
+        questions.forEach(BaseQuestionVO::excludeCorrectAnswer);
+    }
+}
+// http://47.100.18.120:3000/SEEC-BOK/API-DOC/src/master/helper/serializer/QuizSerializer.md
+// /** 已实现的枚举类 */
+//    type SlideState =
+//  | "DRAFT"
+//          | "BEFORE_CLASS" // 课前
+//          | "IN_CLASS" // 课中
+//          | "AFTER_CLASS" // 课后
+//          | "FINISH"; // 最终截止
+//
+//          type QuizState =
+//          | "NOT_STARTED" // 未开始
+//          | "ONGOING" // 正在进行
+//          | "CLOSED"; // 已结束
+//
+//interface QuizSerializer<T extends QuizState> {
+//    id: number;
+//    name: string; // 测试名
+//    state: T; // 考试状态
+//    course: CourseSerializer; // 课程详情
+//    quizTime: SlideState;
+//    slide: SlideSerializer;
+//    questions: Array<
+//            T extends "CLOSED"
+//            ? QuestionWithStudentAnswerSerializer
+//      : QuestionWithoutAnswerSerializer
+//  >;
+//}
+//
+//interface QuizBasicSerialzer extends Omit<QuizSerialzier, "questions"> {
+//        questionNumber: number; // 题目数量
+//        }

+ 40 - 0
src/main/java/nju/seec/helper/vo/quiz/TrueOrFalseQuestionVO.java

@@ -0,0 +1,40 @@
+package nju.seec.helper.vo.quiz;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import nju.seec.helper.entity.QuizStudentAnswer;
+
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public class TrueOrFalseQuestionVO extends BaseQuestionVO {
+    @JsonProperty(value = "answer")
+    private Boolean answer;
+    @JsonProperty(value = "analysis")
+    private String analysis;
+    @JsonProperty(value = "studentAnswer")
+    private Boolean studentAnswer;
+    @JsonProperty(value = "pass")
+    private Boolean pass;
+
+    @Override
+    public void exAddAnswer(QuizStudentAnswer answer) {
+        this.pass = answer.getPass();
+        this.studentAnswer = Boolean.valueOf(answer.getStudentAnswer());
+    }
+    @Override
+    public boolean checkAnswer(Object answer) {
+        if(answer==null){return false;}
+        return this.answer.toString().toLowerCase().equals(answer.toString().toLowerCase());
+    }
+    @Override
+    public void excludeCorrectAnswer() {
+        this.answer=null;
+        this.pass=null;
+        this.analysis=null;
+    }
+}

+ 2 - 1
src/main/resources/application-dev.yml

@@ -50,7 +50,8 @@ aliyun:
     accessSecret: IBoC7KGkZHOvxZQIfQgp98ZY48AVjW
     signName: seeccoder
     templateCode: SMS_181555598
-
+bok:
+  url: http://bok.seecoder.cn
 # 自定义数据
 helper:
   mail: