Sfoglia il codice sorgente

Merge branch 'iterator5'

ChenSiTong 6 anni fa
parent
commit
9bb56ce0f3
99 ha cambiato i file con 2056 aggiunte e 1032 eliminazioni
  1. 23 17
      pom.xml
  2. 2 0
      src/main/java/nju/seec/helper/HelperApplication.java
  3. 17 18
      src/main/java/nju/seec/helper/api/bok/KnowledgeApi.java
  4. 63 117
      src/main/java/nju/seec/helper/api/bok/QuestionApi.java
  5. 66 0
      src/main/java/nju/seec/helper/api/bok/QuestionQueryApi.java
  6. 54 0
      src/main/java/nju/seec/helper/api/bok/RecordApi.java
  7. 73 0
      src/main/java/nju/seec/helper/api/bok/UserInteractiveApi.java
  8. 28 0
      src/main/java/nju/seec/helper/api/bok/constant/QuestionType.java
  9. 16 0
      src/main/java/nju/seec/helper/api/bok/dto/CollectDTO.java
  10. 23 0
      src/main/java/nju/seec/helper/api/bok/dto/RateDTO.java
  11. 20 0
      src/main/java/nju/seec/helper/api/bok/dto/RecordDTO.java
  12. 20 0
      src/main/java/nju/seec/helper/api/bok/vo/InteractiveVO.java
  13. 2 2
      src/main/java/nju/seec/helper/api/bok/vo/KnowledgeNodeVO.java
  14. 14 0
      src/main/java/nju/seec/helper/api/bok/vo/PageVO.java
  15. 14 0
      src/main/java/nju/seec/helper/api/bok/vo/PageableQuestionListVO.java
  16. 31 0
      src/main/java/nju/seec/helper/api/bok/vo/QuestionVO.java
  17. 21 0
      src/main/java/nju/seec/helper/api/bok/vo/RecordQuestionVO.java
  18. 25 0
      src/main/java/nju/seec/helper/api/bok/vo/RecordVO.java
  19. 91 25
      src/main/java/nju/seec/helper/api/dataanalysis/EventApi.java
  20. 0 17
      src/main/java/nju/seec/helper/api/dataanalysis/EventDTO.java
  21. 44 0
      src/main/java/nju/seec/helper/api/dataanalysis/ExerciseApi.java
  22. 10 1
      src/main/java/nju/seec/helper/api/dataanalysis/constant/EventAction.java
  23. 1 1
      src/main/java/nju/seec/helper/api/dataanalysis/constant/EventType.java
  24. 21 0
      src/main/java/nju/seec/helper/api/dataanalysis/model/Event.java
  25. 38 0
      src/main/java/nju/seec/helper/api/dataanalysis/model/ExerciseRecord.java
  26. 12 11
      src/main/java/nju/seec/helper/aspect/auth/AuthAspect.java
  27. 1 0
      src/main/java/nju/seec/helper/aspect/auth/LoginUser.java
  28. 18 0
      src/main/java/nju/seec/helper/aspect/event/Event.java
  29. 83 0
      src/main/java/nju/seec/helper/aspect/event/EventAspect.java
  30. 15 0
      src/main/java/nju/seec/helper/aspect/message/Message.java
  31. 129 0
      src/main/java/nju/seec/helper/aspect/message/MessageAspect.java
  32. 12 0
      src/main/java/nju/seec/helper/aspect/record/ExerciseRecord.java
  33. 79 0
      src/main/java/nju/seec/helper/aspect/record/ExerciseRecordAspect.java
  34. 8 1
      src/main/java/nju/seec/helper/controller/CommentController.java
  35. 9 15
      src/main/java/nju/seec/helper/controller/CommentWebsocket.java
  36. 4 0
      src/main/java/nju/seec/helper/controller/CourseController.java
  37. 7 2
      src/main/java/nju/seec/helper/controller/EventController.java
  38. 10 3
      src/main/java/nju/seec/helper/controller/KnowledgeController.java
  39. 60 10
      src/main/java/nju/seec/helper/controller/QuestionController.java
  40. 7 2
      src/main/java/nju/seec/helper/controller/QuizController.java
  41. 33 0
      src/main/java/nju/seec/helper/controller/RecordController.java
  42. 0 3
      src/main/java/nju/seec/helper/controller/UserController.java
  43. 22 22
      src/main/java/nju/seec/helper/dao/AdminDAO.java
  44. 16 1
      src/main/java/nju/seec/helper/dao/QuizDAO.java
  45. 8 0
      src/main/java/nju/seec/helper/dao/UserDAO.java
  46. 18 18
      src/main/java/nju/seec/helper/dto/common/LoginDTO.java
  47. 9 7
      src/main/java/nju/seec/helper/dto/quiz/QuizStudentAnswerDTO.java
  48. 30 30
      src/main/java/nju/seec/helper/dto/user/ResetPasswordDTO.java
  49. 39 39
      src/main/java/nju/seec/helper/dto/user/UserDTO.java
  50. 20 20
      src/main/java/nju/seec/helper/dto/validator/phone/Phone.java
  51. 21 21
      src/main/java/nju/seec/helper/dto/validator/phone/PhoneValidator.java
  52. 32 32
      src/main/java/nju/seec/helper/dto/validator/schoolemail/SchoolEmail.java
  53. 54 54
      src/main/java/nju/seec/helper/dto/validator/schoolemail/SchoolEmailValidator.java
  54. 0 23
      src/main/java/nju/seec/helper/entity/Admin.java
  55. 2 2
      src/main/java/nju/seec/helper/entity/Course.java
  56. 6 3
      src/main/java/nju/seec/helper/entity/CourseFile.java
  57. 6 3
      src/main/java/nju/seec/helper/entity/Notice.java
  58. 4 0
      src/main/java/nju/seec/helper/entity/QuestionRecord.java
  59. 2 2
      src/main/java/nju/seec/helper/entity/Quiz.java
  60. 2 2
      src/main/java/nju/seec/helper/entity/Slide.java
  61. 8 12
      src/main/java/nju/seec/helper/entity/User.java
  62. 1 1
      src/main/java/nju/seec/helper/enums/ExceptionType.java
  63. 0 39
      src/main/java/nju/seec/helper/enums/QuestionType.java
  64. 16 10
      src/main/java/nju/seec/helper/enums/QuizState.java
  65. 24 6
      src/main/java/nju/seec/helper/enums/SlideState.java
  66. 2 2
      src/main/java/nju/seec/helper/service/EventService.java
  67. 46 3
      src/main/java/nju/seec/helper/service/QuestionService.java
  68. 25 0
      src/main/java/nju/seec/helper/service/RecordService.java
  69. 2 6
      src/main/java/nju/seec/helper/service/impl/CommentServiceImpl.java
  70. 5 7
      src/main/java/nju/seec/helper/service/impl/CourseFileServiceImpl.java
  71. 14 30
      src/main/java/nju/seec/helper/service/impl/EventServiceImpl.java
  72. 3 15
      src/main/java/nju/seec/helper/service/impl/NoticeServiceImpl.java
  73. 71 15
      src/main/java/nju/seec/helper/service/impl/QuestionServiceImpl.java
  74. 10 25
      src/main/java/nju/seec/helper/service/impl/QuizServiceImpl.java
  75. 3 3
      src/main/java/nju/seec/helper/service/impl/QuizStudentAnswerServiceImpl.java
  76. 44 0
      src/main/java/nju/seec/helper/service/impl/RecordServiceImpl.java
  77. 3 14
      src/main/java/nju/seec/helper/service/impl/ReplyServiceImpl.java
  78. 9 27
      src/main/java/nju/seec/helper/service/impl/SlideServiceImpl.java
  79. 0 1
      src/main/java/nju/seec/helper/service/impl/UserServiceImpl.java
  80. 0 12
      src/main/java/nju/seec/helper/util/Consts.java
  81. 1 1
      src/main/java/nju/seec/helper/util/JsonUtils.java
  82. 11 3
      src/main/java/nju/seec/helper/util/OssUtils.java
  83. 22 2
      src/main/java/nju/seec/helper/util/RestRequestUtil.java
  84. 9 0
      src/main/java/nju/seec/helper/util/StringUtils.java
  85. 14 11
      src/main/java/nju/seec/helper/util/cache/CaffeineCacheUtils.java
  86. 43 43
      src/main/java/nju/seec/helper/util/cache/RedisCacheUtils.java
  87. 8 4
      src/main/java/nju/seec/helper/util/file/FileUtils.java
  88. 0 32
      src/main/java/nju/seec/helper/util/oss/OssObjectUrlUtils.java
  89. 0 21
      src/main/java/nju/seec/helper/vo/EventVO.java
  90. 0 4
      src/main/java/nju/seec/helper/vo/UserVO.java
  91. 20 15
      src/main/java/nju/seec/helper/vo/question/BaseQuestionVO.java
  92. 0 30
      src/main/java/nju/seec/helper/vo/question/BokQuestionVO.java
  93. 5 10
      src/main/java/nju/seec/helper/vo/question/ChoiceQuestionVO.java
  94. 5 7
      src/main/java/nju/seec/helper/vo/question/TrueOrFalseQuestionVO.java
  95. 1 1
      src/main/java/nju/seec/helper/vo/quiz/QuizStudentAnswerVO.java
  96. 2 4
      src/main/java/nju/seec/helper/vo/quiz/QuizVO.java
  97. 63 41
      src/main/resources/application-deploy.yml
  98. 62 42
      src/main/resources/application-dev.yml
  99. 9 9
      src/main/resources/hibernate.properties

+ 23 - 17
pom.xml

@@ -10,7 +10,7 @@
     </parent>
     <groupId>cn.seecoder</groupId>
     <artifactId>helper-backend</artifactId>
-    <version>5.0.0</version>
+    <version>5.3.0</version>
     <packaging>jar</packaging>
     <name>helper</name>
     <description>backend</description>
@@ -33,14 +33,14 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-jpa</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-data-redis</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-mail</artifactId>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.boot</groupId>-->
+<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.boot</groupId>-->
+<!--            <artifactId>spring-boot-starter-mail</artifactId>-->
+<!--        </dependency>-->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
@@ -63,6 +63,7 @@
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-websocket</artifactId>
+            <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -121,14 +122,14 @@
             <artifactId>hibernate-types-52</artifactId>
             <version>2.9.5</version>
         </dependency>
-        <dependency>
-            <groupId>org.hibernate</groupId>
-            <artifactId>hibernate-jcache</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.ehcache</groupId>
-            <artifactId>ehcache</artifactId>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>org.hibernate</groupId>-->
+<!--            <artifactId>hibernate-jcache</artifactId>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>org.ehcache</groupId>-->
+<!--            <artifactId>ehcache</artifactId>-->
+<!--        </dependency>-->
         <!--一个工具类库,不用自己造轮子了!!!-->
         <dependency>
             <groupId>cn.hutool</groupId>
@@ -140,6 +141,11 @@
             <artifactId>java-jwt</artifactId>
             <version>3.10.2</version>
         </dependency>
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+            <version>2.8.1</version>
+        </dependency>
     </dependencies>
 
     <build>

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

@@ -5,11 +5,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 /**
  * @author cst
  */
+@EnableScheduling
 @EnableCaching
 @EnableTransactionManagement
 @EnableAsync

+ 17 - 18
src/main/java/nju/seec/helper/api/bok/KnowledgeApi.java

@@ -2,11 +2,11 @@ package nju.seec.helper.api.bok;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
-import nju.seec.helper.util.Consts;
+import nju.seec.helper.api.bok.vo.KnowledgeNodeVO;
 import nju.seec.helper.util.RestRequestUtil;
-import nju.seec.helper.util.cache.GuavaCacheUtils;
-import nju.seec.helper.vo.knowledge.BokKnowledgeNodeVO;
+import nju.seec.helper.util.cache.CaffeineCacheUtils;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
 import java.util.List;
@@ -15,32 +15,31 @@ import java.util.List;
  * @author cst
  */
 @Component
+@ConfigurationProperties("bok.knowledge")
+@Data
 public class KnowledgeApi {
-    private final RestRequestUtil restRequestUtil;
-    private final GuavaCacheUtils cacheUtils;
-
-    @Value("${bok.knowledgeUrl}")
-    private String knowledgeUrl;
+    @Value("${bok.knowledge-cache-name}")
+    private String bokKnowledgeCacheName;
 
-    @Value("${bok.knowledgeGetAllUrl}")
-    private String knowledgeGetAllUrl;
+    private final RestRequestUtil restRequestUtil;
+    private final CaffeineCacheUtils cacheUtils;
 
-    private static final String BOK_KNOWLEDGE_CACHE_NAME = Consts.SYS_NAME + "_bok_knowledge";
+    private String url;
 
-    public KnowledgeApi(RestRequestUtil restRequestUtil, GuavaCacheUtils cacheUtils) {
+    public KnowledgeApi(RestRequestUtil restRequestUtil, CaffeineCacheUtils cacheUtils) {
         this.restRequestUtil = restRequestUtil;
         this.cacheUtils = cacheUtils;
     }
 
     @SuppressWarnings("unchecked")
-    public List<BokKnowledgeNodeVO> getKnowledgeNodes() {
-        Object cache = cacheUtils.get(BOK_KNOWLEDGE_CACHE_NAME, "all");
+    public List<KnowledgeNodeVO> getAllKnowledgeNodes() {
+        Object cache = cacheUtils.get(bokKnowledgeCacheName, "all");
         if (cache instanceof List) {
-            return (List<BokKnowledgeNodeVO>) cache;
+            return (List<KnowledgeNodeVO>) cache;
         }
-        List<BokKnowledgeNodeVO> bokKnowledgeNodeVOList = restRequestUtil.sendPostRequest(knowledgeGetAllUrl, KnowledgeGetDTO.of("", "part of"), List.class);
-        cacheUtils.set(BOK_KNOWLEDGE_CACHE_NAME, "all", bokKnowledgeNodeVOList);
-        return bokKnowledgeNodeVOList;
+        List<KnowledgeNodeVO> knowledgeNodeVOList = restRequestUtil.sendGetRequest(url, List.class);
+        cacheUtils.set(bokKnowledgeCacheName, "all", knowledgeNodeVOList);
+        return knowledgeNodeVOList;
     }
 
     @AllArgsConstructor(staticName = "of")

+ 63 - 117
src/main/java/nju/seec/helper/api/bok/QuestionApi.java

@@ -1,30 +1,27 @@
 package nju.seec.helper.api.bok;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import lombok.Data;
+import nju.seec.helper.api.bok.constant.QuestionType;
+import nju.seec.helper.api.bok.vo.PageableQuestionListVO;
+import nju.seec.helper.api.bok.vo.QuestionVO;
 import nju.seec.helper.dto.question.BaseQuestionDTO;
 import nju.seec.helper.dto.question.ChoiceQuestionDTO;
 import nju.seec.helper.dto.question.TrueOrFalseQuestionDTO;
 import nju.seec.helper.enums.ExceptionType;
-import nju.seec.helper.enums.QuestionType;
 import nju.seec.helper.exception.HelperException;
-import nju.seec.helper.util.Consts;
 import nju.seec.helper.util.RestRequestUtil;
-import nju.seec.helper.util.cache.GuavaCacheUtils;
-import nju.seec.helper.vo.question.BokQuestionVO;
+import nju.seec.helper.util.StringUtils;
+import nju.seec.helper.util.cache.CaffeineCacheUtils;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
 import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Component;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
@@ -33,139 +30,88 @@ import java.util.stream.Collectors;
  * updated by cst
  */
 @Component
+@ConfigurationProperties("bok.question")
+@Data
 public class QuestionApi {
+    @Value("${bok.question-cache-name}")
+    private String bokQuestionCacheName;
+
     private final RestRequestUtil restRequestUtil;
-    private final GuavaCacheUtils cacheUtils;
-    @Value("${bok.tqUrl}")
-    private String tqUrl;
-    @Value("${bok.tqStemSearchUrl}")
-    private String tqStemSearchUrl;
-    @Value("${bok.tqIdSearchUrl}")
-    private String tqIdSearchUrl;
+    private final CaffeineCacheUtils cacheUtils;
+    private String url;
+    private String searchUrl;
 
-    public QuestionApi(RestRequestUtil restRequestUtil, GuavaCacheUtils cacheUtils) {
+    public QuestionApi(RestRequestUtil restRequestUtil, CaffeineCacheUtils cacheUtils) {
         this.restRequestUtil = restRequestUtil;
         this.cacheUtils = cacheUtils;
     }
 
-    public Page<BokQuestionVO> bokFindByStemLike(String stem, Pageable pageable) {
-        Map<String, String> urlParams = ImmutableMap.of(
-                "content", stem,
-                "page", String.valueOf(1 + pageable.getPageNumber()),
-                "size", String.valueOf(pageable.getPageSize())
-        );
-        BokSearchResult result = restRequestUtil.sendGetRequest(tqStemSearchUrl, BokSearchResult.class, urlParams);
-
-        List<BokQuestionVO> bokQuestionVOList = result.getEmbedded().getQuestions();
-        cacheUtils.setAll(BOK_QUESTION_CACHE_NAME, bokQuestionVOList.parallelStream().collect(Collectors.toMap(BokQuestionVO::getId, bokQuestion -> bokQuestion)));
-        return new PageImpl<>(bokQuestionVOList, pageable, result.getPage().getTotalElements());
-    }
-
-    private static final String BOK_QUESTION_CACHE_NAME = Consts.SYS_NAME + "_bok_question";
-
-    @SuppressWarnings({"unchecked", "rawtypes"})
-    public List<BokQuestionVO> bokFindByIdIn(final List<String> ids) {
-        Map<String, BokQuestionVO> bokQuestionsMap = Maps.newHashMapWithExpectedSize(ids.size());
-
-        final Map<String, BokQuestionVO> cacheBokQuestionsMap = (Map) cacheUtils.multiGet(BOK_QUESTION_CACHE_NAME, ids);
-        bokQuestionsMap.putAll(cacheBokQuestionsMap);
-
-        List<String> requestIds = Lists.newArrayList(ids);
-        requestIds.removeAll(cacheBokQuestionsMap.keySet());
-        //未缓存的去这里拿
-        if (!requestIds.isEmpty()) {
-            Map<String, String> urlParams = ImmutableMap.of("id", requestIds.stream().reduce((a, b) -> a + "," + b).orElse(""));
-
-            BokSearchResult result = restRequestUtil.sendGetRequest(tqIdSearchUrl, BokSearchResult.class, urlParams);
-            List<BokQuestionVO> bokQuestionVOList = result.getEmbedded().getQuestions();
-
-            Map<String, BokQuestionVO> remoteBokQuestionsMap = bokQuestionVOList.parallelStream().collect(Collectors.toMap(BokQuestionVO::getId, bokQuestion -> bokQuestion));
-
-            bokQuestionsMap.putAll(remoteBokQuestionsMap);
-            cacheUtils.setAll(BOK_QUESTION_CACHE_NAME, (Map) remoteBokQuestionsMap);
-        }
-
-        return ids.stream()
-                .map(bokQuestionsMap::get)
-                .collect(Collectors.toList());
+    public Page<QuestionVO> getQuestionList(String stem, String tag, String knowledgeId, Pageable pageable) {
+        Map<String, ?> urlParams = ImmutableMap.<String, Object>builder()
+                .put("stem", stem)
+                .put("tag", tag)
+                .put("knowledgeId", knowledgeId)
+                .put("page", 1 + pageable.getPageNumber())
+                .put("size", pageable.getPageSize())
+                .put("sort", StringUtils.getSortString(pageable.getSort()))
+                .build();
+        PageableQuestionListVO result = restRequestUtil.sendGetRequest(searchUrl, PageableQuestionListVO.class, urlParams);
+
+        List<QuestionVO> questionVOList = result.getQuestions();
+        cacheUtils.setAll(bokQuestionCacheName, questionVOList.parallelStream().collect(Collectors.toMap(QuestionVO::getId, bokQuestion -> bokQuestion)));
+        return new PageImpl<>(questionVOList, pageable, result.getPage().getTotalElements());
     }
 
-    public BokQuestionVO bokFindById(String questionId) {
-        Object cachedBokQuestion = cacheUtils.get(BOK_QUESTION_CACHE_NAME, questionId);
-        if (cachedBokQuestion instanceof BokQuestionVO) {
-            return (BokQuestionVO) cachedBokQuestion;
+    public QuestionVO getQuestionsById(String questionId) {
+        Object cachedBokQuestion = cacheUtils.get(bokQuestionCacheName, questionId);
+        if (cachedBokQuestion instanceof QuestionVO) {
+            return (QuestionVO) cachedBokQuestion;
         }
-        BokQuestionVO bokQuestionVO = restRequestUtil.sendGetRequest(tqUrl + "/" + questionId, BokQuestionVO.class, Collections.emptyMap());
-        cacheUtils.set(BOK_QUESTION_CACHE_NAME, bokQuestionVO.getId(), bokQuestionVO);
-        return bokQuestionVO;
+        QuestionVO questionVO = restRequestUtil.sendGetRequest(url + "/" + questionId, QuestionVO.class);
+        cacheUtils.set(bokQuestionCacheName, questionVO.getId(), questionVO);
+        return questionVO;
     }
 
-    public BokQuestionVO createQuestion(String questionId, BaseQuestionDTO baseQuestionDTO) {
-        BokQuestionVO bokQuestionVO = getQuestion(baseQuestionDTO);
-        bokQuestionVO.setId(questionId);
-        bokQuestionVO = restRequestUtil.sendPostRequest(tqUrl, bokQuestionVO, BokQuestionVO.class);
-        return bokQuestionVO;
+    public QuestionVO createQuestion(String questionId, BaseQuestionDTO baseQuestionDTO) {
+        QuestionVO questionVO = createQuestionVO(baseQuestionDTO);
+        questionVO.setId(questionId);
+        questionVO = restRequestUtil.sendPostRequest(url, questionVO, QuestionVO.class);
+        return questionVO;
     }
 
-    public BokQuestionVO modifyQuestion(String questionId, BaseQuestionDTO baseQuestionDTO) {
-        BokQuestionVO bokQuestionVO = getQuestion(baseQuestionDTO);
-        bokQuestionVO.setId(questionId);
-        restRequestUtil.sendPutRequest(tqUrl + "/" + questionId, bokQuestionVO);
-        return bokFindById(questionId);
+    public QuestionVO updateQuestion(String questionId, BaseQuestionDTO baseQuestionDTO) {
+        QuestionVO questionVO = createQuestionVO(baseQuestionDTO);
+        questionVO.setId(questionId);
+        restRequestUtil.sendPutRequest(url + "/" + questionId, questionVO);
+        return getQuestionsById(questionId);
     }
 
     public void deleteQuestion(String questionId) {
-        restRequestUtil.sendDeleteRequest(tqUrl + "/" + questionId);
-    }
-
-    public void starQuestion(String userId, String questionId) {
-
+        restRequestUtil.sendDeleteRequest(url + "/" + questionId);
     }
 
-    private BokQuestionVO getQuestion(BaseQuestionDTO baseQuestionDTO) {
-        BokQuestionVO bokQuestionVO = new BokQuestionVO();
-        bokQuestionVO.setType(baseQuestionDTO.getType());
-        bokQuestionVO.setStem(baseQuestionDTO.getStem());
-        bokQuestionVO.setKeyPoints(baseQuestionDTO.getKeyPoints());
-        bokQuestionVO.setTags(baseQuestionDTO.getTags());
-        bokQuestionVO.setKnowledgeId(baseQuestionDTO.getKnowledgeId());
-
-        final QuestionType questionType = QuestionType.getQuestionType(baseQuestionDTO.getType());
-        switch (Objects.requireNonNull(questionType)) {
-            case CHOICE:
+    private QuestionVO createQuestionVO(BaseQuestionDTO baseQuestionDTO) {
+        QuestionVO questionVO = new QuestionVO();
+        questionVO.setType(QuestionType.valueOf(baseQuestionDTO.getType()));
+        questionVO.setStem(baseQuestionDTO.getStem());
+        questionVO.setKeyPoints(baseQuestionDTO.getKeyPoints());
+        questionVO.setTags(baseQuestionDTO.getTags());
+        questionVO.setKnowledgeId(baseQuestionDTO.getKnowledgeId());
+        questionVO.setAnalysis(baseQuestionDTO.getAnalysis());
+
+        switch (questionVO.getType()) {
+            case choice:
                 ChoiceQuestionDTO choiceQuestionDTO = (ChoiceQuestionDTO) baseQuestionDTO;
-                bokQuestionVO.setOptions(choiceQuestionDTO.getOptions());
-                bokQuestionVO.setAnswer(choiceQuestionDTO.getAnswer());
-                bokQuestionVO.setAnalysis(choiceQuestionDTO.getAnalysis());
+                questionVO.setOptions(choiceQuestionDTO.getOptions());
+                questionVO.setAnswer(choiceQuestionDTO.getAnswer());
                 break;
-            case TRUE_FALSE:
+            case true_false:
                 TrueOrFalseQuestionDTO trueOrFalseQuestionDTO = (TrueOrFalseQuestionDTO) baseQuestionDTO;
-                bokQuestionVO.setAnswer(String.valueOf(trueOrFalseQuestionDTO.getAnswer()));
-                bokQuestionVO.setAnalysis(trueOrFalseQuestionDTO.getAnalysis());
+                questionVO.setAnswer(String.valueOf(trueOrFalseQuestionDTO.getAnswer()));
                 break;
             default:
                 throw HelperException.of(ExceptionType.ERROR, "不支持的题目类型");
         }
-        return bokQuestionVO;
-    }
-
-    @Data
-    private static class BokSearchResult {
-        @JsonProperty("_embedded")
-        private Embedded embedded = new Embedded();
-        private Page page;
-
-        @Data
-        private static class Embedded {
-            private List<BokQuestionVO> questions = Collections.emptyList();
-        }
-
-        @Data
-        private static class Page {
-            private int size;
-            private int number;
-            private int totalPages;
-            private long totalElements;
-        }
+        return questionVO;
     }
 }

+ 66 - 0
src/main/java/nju/seec/helper/api/bok/QuestionQueryApi.java

@@ -0,0 +1,66 @@
+package nju.seec.helper.api.bok;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gson.reflect.TypeToken;
+import lombok.Data;
+import nju.seec.helper.api.bok.vo.QuestionVO;
+import nju.seec.helper.util.JsonUtils;
+import nju.seec.helper.util.RestRequestUtil;
+import nju.seec.helper.util.cache.CaffeineCacheUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author cst
+ */
+@Component
+@ConfigurationProperties("bok.question-query")
+@Data
+public class QuestionQueryApi {
+    @Value("${bok.question-cache-name}")
+    private String bokQuestionCacheName;
+
+    private final RestRequestUtil restRequestUtil;
+    private final CaffeineCacheUtils cacheUtils;
+
+    private String byIdUrl;
+
+    public QuestionQueryApi(RestRequestUtil restRequestUtil, CaffeineCacheUtils cacheUtils) {
+        this.restRequestUtil = restRequestUtil;
+        this.cacheUtils = cacheUtils;
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public List<QuestionVO> getQuestionListById(final List<String> ids) {
+        Map<String, QuestionVO> bokQuestionsMap = Maps.newHashMapWithExpectedSize(ids.size());
+
+        final Map<String, QuestionVO> cacheBokQuestionsMap = (Map) cacheUtils.multiGet(bokQuestionCacheName, ids);
+        bokQuestionsMap.putAll(cacheBokQuestionsMap);
+
+        List<String> requestIds = Lists.newArrayList(ids);
+        requestIds.removeAll(cacheBokQuestionsMap.keySet());
+        //未缓存的去这里拿
+        if (!requestIds.isEmpty()) {
+            Map<String, String> uriVariables = ImmutableMap.of("id", requestIds.stream().reduce((a, b) -> a + "," + b).orElse(""));
+
+            List<QuestionVO> questionVOList = JsonUtils.fromJson(restRequestUtil.sendGetRequest(byIdUrl, String.class, uriVariables), new TypeToken<List<QuestionVO>>() {
+            });
+
+            Map<String, QuestionVO> remoteBokQuestionsMap = questionVOList.parallelStream().collect(Collectors.toMap(QuestionVO::getId, bokQuestion -> bokQuestion));
+
+            bokQuestionsMap.putAll(remoteBokQuestionsMap);
+            cacheUtils.setAll(bokQuestionCacheName, (Map) remoteBokQuestionsMap);
+        }
+
+        return ids.stream()
+                .map(bokQuestionsMap::get)
+                .collect(Collectors.toList());
+    }
+}

+ 54 - 0
src/main/java/nju/seec/helper/api/bok/RecordApi.java

@@ -0,0 +1,54 @@
+package nju.seec.helper.api.bok;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.Data;
+import nju.seec.helper.api.bok.dto.RecordDTO;
+import nju.seec.helper.api.bok.vo.RecordQuestionVO;
+import nju.seec.helper.util.RestRequestUtil;
+import nju.seec.helper.util.StringUtils;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.data.domain.Pageable;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author cst
+ */
+@Component
+@ConfigurationProperties("bok.record")
+@Data
+public class RecordApi {
+    private String url;
+    private String questionUrl;
+    private final RestRequestUtil restRequestUtil;
+
+    @Async
+    public void reportUserRecord(List<RecordDTO> records) {
+        restRequestUtil.sendPostRequest(url, records, String.class);
+    }
+
+    public RecordDTO createRecordDTO(String studentId, String questionId, Object answer, Boolean pass, LocalDateTime startAt, LocalDateTime finishAt) {
+        return RecordDTO.builder()
+                .studentId(studentId)
+                .questionId(questionId)
+                .studentAnswer(answer)
+                .pass(pass)
+                .startAt(startAt)
+                .finishAt(finishAt)
+                .build();
+    }
+
+    public RecordQuestionVO getUserRecordQuestions(String userId, Pageable pageable) {
+        Map<String, ?> uriVariables = ImmutableMap.of(
+                "userId", userId,
+                "page", pageable.getPageNumber() + 1,
+                "size", pageable.getPageSize(),
+                "sort", StringUtils.getSortString(pageable.getSort())
+        );
+        return restRequestUtil.sendGetRequest(questionUrl, RecordQuestionVO.class, uriVariables);
+    }
+}

+ 73 - 0
src/main/java/nju/seec/helper/api/bok/UserInteractiveApi.java

@@ -0,0 +1,73 @@
+package nju.seec.helper.api.bok;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.reflect.TypeToken;
+import lombok.Data;
+import nju.seec.helper.api.bok.dto.CollectDTO;
+import nju.seec.helper.api.bok.dto.RateDTO;
+import nju.seec.helper.api.bok.vo.InteractiveVO;
+import nju.seec.helper.api.bok.vo.PageableQuestionListVO;
+import nju.seec.helper.api.bok.vo.QuestionVO;
+import nju.seec.helper.util.JsonUtils;
+import nju.seec.helper.util.RestRequestUtil;
+import nju.seec.helper.util.StringUtils;
+import nju.seec.helper.util.cache.CaffeineCacheUtils;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author cst
+ */
+@Component
+@ConfigurationProperties("bok.user")
+@Data
+public class UserInteractiveApi {
+    private final RestRequestUtil restRequestUtil;
+    private final CaffeineCacheUtils cacheUtils;
+
+    private String rateUrl;
+    private String collectionUrl;
+    private String questionUrl;
+    private String interactiveUrl;
+
+    public UserInteractiveApi(RestRequestUtil restRequestUtil, CaffeineCacheUtils cacheUtils) {
+        this.restRequestUtil = restRequestUtil;
+        this.cacheUtils = cacheUtils;
+    }
+
+    public void collectQuestion(String userId, String questionId, CollectDTO collectDTO) {
+        restRequestUtil.sendPostRequest(collectionUrl, collectDTO, Object.class, ImmutableMap.of("userId", userId, "questionId", questionId));
+    }
+
+    public void rateQuestion(String userId, String questionId, RateDTO rateDTO) {
+        restRequestUtil.sendPostRequest(rateUrl, rateDTO, Object.class, ImmutableMap.of("userId", userId, "questionId", questionId));
+    }
+
+    public Page<QuestionVO> getCollectAndRateQuestions(String userId, Boolean collect, Boolean rate, Pageable pageable) {
+        Map<String, ?> uriVariables = ImmutableMap.<String, Object>builder()
+                .put("userId", userId)
+                .put("collection", collect == null ? "" : collect)
+                .put("rate", rate == null ? "" : rate)
+                .put("page", pageable.getPageNumber() + 1)
+                .put("size", pageable.getPageSize())
+                .put("sort", StringUtils.getSortString(pageable.getSort()))
+                .build();
+        PageableQuestionListVO pageableQuestionListVO = restRequestUtil.sendGetRequest(questionUrl, PageableQuestionListVO.class, uriVariables);
+        return new PageImpl<>(pageableQuestionListVO.getQuestions(), pageable, pageableQuestionListVO.getPage().getTotalElements());
+    }
+
+    public List<InteractiveVO> getInteractive(String userId, String questionId) {
+        Map<String, ?> uriVariables = ImmutableMap.<String, Object>builder()
+                .put("userId", userId)
+                .put("questionId", questionId)
+                .build();
+        return JsonUtils.fromJson(restRequestUtil.sendGetRequest(interactiveUrl, String.class, uriVariables), new TypeToken<List<InteractiveVO>>() {
+        });
+    }
+}

+ 28 - 0
src/main/java/nju/seec/helper/api/bok/constant/QuestionType.java

@@ -0,0 +1,28 @@
+package nju.seec.helper.api.bok.constant;
+
+
+/**
+ * @author bok
+ */
+public enum QuestionType {
+    /**
+     * 选择题
+     */
+    choice,
+    /**
+     * 判断题
+     */
+    true_false,
+    /**
+     * 多选题
+     */
+    multiple_choice,
+    /**
+     * 填空题
+     */
+    blank,
+    /**
+     * 简答题
+     */
+    short_answer
+}

+ 16 - 0
src/main/java/nju/seec/helper/api/bok/dto/CollectDTO.java

@@ -0,0 +1,16 @@
+package nju.seec.helper.api.bok.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author bok
+ * <p>
+ * updated by cst
+ */
+@Data
+public class CollectDTO {
+    @NotNull(message = "缺少是否收藏")
+    Boolean collect;
+}

+ 23 - 0
src/main/java/nju/seec/helper/api/bok/dto/RateDTO.java

@@ -0,0 +1,23 @@
+package nju.seec.helper.api.bok.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author bok
+ * <p>
+ * updated by cst
+ */
+@Data
+public class RateDTO {
+    /**
+     * 取值 1-5
+     */
+    @Max(value = 5, message = "最大评分为5")
+    @Min(value = 1, message = "最小评分为1")
+    @NotNull(message = "缺少评分")
+    Integer rate;
+}

+ 20 - 0
src/main/java/nju/seec/helper/api/bok/dto/RecordDTO.java

@@ -0,0 +1,20 @@
+package nju.seec.helper.api.bok.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author Kunduin
+ */
+@Data
+@Builder
+public class RecordDTO {
+    String questionId;
+    String studentId;
+    Object studentAnswer;
+    Boolean pass;
+    LocalDateTime startAt;
+    LocalDateTime finishAt;
+}

+ 20 - 0
src/main/java/nju/seec/helper/api/bok/vo/InteractiveVO.java

@@ -0,0 +1,20 @@
+package nju.seec.helper.api.bok.vo;
+
+import lombok.Data;
+
+/**
+ * @author cst
+ */
+@Data
+public class InteractiveVO {
+    private String userId;
+    private String questionId;
+    /**
+     * 为null表示未对该题做过收藏处理
+     */
+    private Boolean collection;
+    /**
+     * 为null表示未对该题做过评价处理
+     */
+    private Integer rate;
+}

+ 2 - 2
src/main/java/nju/seec/helper/vo/knowledge/BokKnowledgeNodeVO.java → src/main/java/nju/seec/helper/api/bok/vo/KnowledgeNodeVO.java

@@ -1,4 +1,4 @@
-package nju.seec.helper.vo.knowledge;
+package nju.seec.helper.api.bok.vo;
 
 import lombok.Data;
 
@@ -6,7 +6,7 @@ import lombok.Data;
  * @author cst
  */
 @Data
-public class BokKnowledgeNodeVO {
+public class KnowledgeNodeVO {
     private String id;
     private String abbreviation;
     private String name;

+ 14 - 0
src/main/java/nju/seec/helper/api/bok/vo/PageVO.java

@@ -0,0 +1,14 @@
+package nju.seec.helper.api.bok.vo;
+
+import lombok.Data;
+
+/**
+ * @author Kunduin
+ */
+@Data
+public class PageVO {
+    Integer size;
+    Long totalElements;
+    Integer totalPages;
+    Integer number;
+}

+ 14 - 0
src/main/java/nju/seec/helper/api/bok/vo/PageableQuestionListVO.java

@@ -0,0 +1,14 @@
+package nju.seec.helper.api.bok.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author Kunduin
+ */
+@Data
+public class PageableQuestionListVO {
+    List<QuestionVO> questions;
+    PageVO page;
+}

+ 31 - 0
src/main/java/nju/seec/helper/api/bok/vo/QuestionVO.java

@@ -0,0 +1,31 @@
+package nju.seec.helper.api.bok.vo;
+
+import lombok.Data;
+import nju.seec.helper.api.bok.constant.QuestionType;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Kunduin
+ */
+@Data
+public class QuestionVO {
+    String id;
+    QuestionType type;
+    String stem;
+    Map<String, String> options;
+
+    /**
+     * 填空题 __1__ __2__
+     */
+    Map<String, String> blanks;
+
+    String answer;
+    String keyPoints;
+    String analysis;
+    List<String> tags;
+    List<String> knowledgeId;
+    Date lastModifiedTime;
+}

+ 21 - 0
src/main/java/nju/seec/helper/api/bok/vo/RecordQuestionVO.java

@@ -0,0 +1,21 @@
+package nju.seec.helper.api.bok.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author bok
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class RecordQuestionVO {
+    PageVO page;
+    List<QuestionVO> questions;
+    List<RecordVO> records;
+}

+ 25 - 0
src/main/java/nju/seec/helper/api/bok/vo/RecordVO.java

@@ -0,0 +1,25 @@
+package nju.seec.helper.api.bok.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author Kunduin
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class RecordVO {
+    String id;
+    String userId;
+    String questionId;
+    Object studentAnswer;
+    Boolean pass;
+    LocalDateTime startAt;
+    LocalDateTime finishAt;
+}

+ 91 - 25
src/main/java/nju/seec/helper/api/dataanalysis/EventApi.java

@@ -1,25 +1,91 @@
-//package nju.seec.helper.api.dataanalysis;
-//
-//import nju.seec.helper.util.RestRequestUtil;
-//import org.springframework.beans.factory.annotation.Value;
-//import org.springframework.scheduling.annotation.Async;
-//import org.springframework.stereotype.Component;
-//
-///**
-// * @author cst
-// */
-//@Component
-//public class EventApi {
-//    @Value("${data-analysis.eventReportUrl}")
-//    private String eventReportUrl;
-//    private final RestRequestUtil restRequestUtil;
-//
-//    public EventApi(RestRequestUtil restRequestUtil) {
-//        this.restRequestUtil = restRequestUtil;
-//    }
-//
-//    @Async
-//    public void reportEvent(EventDTO eventDTO) {
-//        restRequestUtil.sendPostRequest(eventReportUrl, eventDTO, Boolean.class);
-//    }
-//}
+package nju.seec.helper.api.dataanalysis;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.Data;
+import nju.seec.helper.api.dataanalysis.constant.EventAction;
+import nju.seec.helper.api.dataanalysis.constant.EventType;
+import nju.seec.helper.api.dataanalysis.model.Event;
+import nju.seec.helper.entity.Course;
+import nju.seec.helper.entity.Quiz;
+import nju.seec.helper.util.RestRequestUtil;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author cst
+ */
+@Component
+@ConfigurationProperties("data-analysis.event")
+@Data
+public class EventApi {
+    private String reportUrl;
+    private final RestRequestUtil restRequestUtil;
+
+    public EventApi(RestRequestUtil restRequestUtil) {
+        this.restRequestUtil = restRequestUtil;
+    }
+
+    @Async
+    public void reportEvent(Event event) {
+//        System.out.println(event);
+//            restRequestUtil.sendPostRequest(eventReportUrl, event, Boolean.class);
+    }
+
+    /**
+     * 取得参与课程事件
+     *
+     * @param userId
+     * @param time
+     * @param course
+     * @return
+     */
+    public Event createParticipateClassEventDTO(String userId, LocalDateTime time, Course course) {
+        return Event.builder()
+                .userId(userId).time(time)
+                .type(EventType.LESSON)
+                .action(EventAction.PARTICIPATE)
+                .description(course.getName())
+                .build();
+    }
+
+    /**
+     * 取得创建帖子事件
+     *
+     * @param userId
+     * @param time
+     * @param title
+     * @return
+     */
+    public Event createInitPostEventDTO(String userId, LocalDateTime time, String title) {
+        return Event
+                .builder()
+                .userId(userId)
+                .time(time)
+                .type(EventType.POST)
+                .action(EventAction.INITIATE)
+                .description(title)
+                .build();
+    }
+
+    /**
+     * 取得完成测验事件
+     *
+     * @param userId
+     * @param time
+     * @param quiz
+     * @return
+     */
+    public Event createCompleteQuizEventDTO(String userId, LocalDateTime time, Quiz quiz) {
+        return Event
+                .builder()
+                .userId(userId)
+                .time(time)
+                .type(EventType.QUIZ)
+                .action(EventAction.COMPLETE)
+                .description(StrUtil.join(":", quiz.getCourse().getName(), quiz.getName()))
+                .build();
+    }
+}

+ 0 - 17
src/main/java/nju/seec/helper/api/dataanalysis/EventDTO.java

@@ -1,17 +0,0 @@
-package nju.seec.helper.api.dataanalysis;
-
-import lombok.Data;
-
-import java.time.LocalDateTime;
-
-/**
- * @author cst
- */
-@Data
-public class EventDTO {
-    private String userId;
-    private EventType type;
-    private LocalDateTime time;
-    private EventAction action;
-    private String description;
-}

+ 44 - 0
src/main/java/nju/seec/helper/api/dataanalysis/ExerciseApi.java

@@ -0,0 +1,44 @@
+package nju.seec.helper.api.dataanalysis;
+
+import lombok.Data;
+import nju.seec.helper.api.dataanalysis.model.ExerciseRecord;
+import nju.seec.helper.util.RestRequestUtil;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * @author cst
+ */
+@Component
+@ConfigurationProperties("data-analysis.exercise")
+@Data
+public class ExerciseApi {
+    private String reportUrl;
+    private final RestRequestUtil restRequestUtil;
+
+    public ExerciseApi(RestRequestUtil restRequestUtil) {
+        this.restRequestUtil = restRequestUtil;
+    }
+
+    @Async
+    public void reportExercise(List<ExerciseRecord> exerciseRecords) {
+//        System.out.println(exerciseRecords);
+//        restRequestUtil.sendPostRequest(exerciseReportUrl, exerciseRecords, Boolean.class);
+    }
+
+    public ExerciseRecord createExerciseRecordDTO(String userId, String questionId, LocalDateTime startAt, LocalDateTime endAt, Boolean pass) {
+        return ExerciseRecord.builder()
+                .userId(userId)
+                .questionId(questionId)
+                .startAt(startAt)
+                .endAt(endAt)
+                .durationSeconds(Duration.between(startAt, endAt).getSeconds())
+                .pass(pass)
+                .build();
+    }
+}

+ 10 - 1
src/main/java/nju/seec/helper/api/dataanalysis/EventAction.java → src/main/java/nju/seec/helper/api/dataanalysis/constant/EventAction.java

@@ -1,4 +1,4 @@
-package nju.seec.helper.api.dataanalysis;
+package nju.seec.helper.api.dataanalysis.constant;
 
 import lombok.Getter;
 
@@ -7,8 +7,17 @@ import lombok.Getter;
  */
 @Getter
 public enum EventAction {
+    /**
+     * 参与
+     */
     PARTICIPATE("参与"),
+    /**
+     * 完成
+     */
     COMPLETE("完成"),
+    /**
+     * 创建
+     */
     INITIATE("创建"),
     ;
 

+ 1 - 1
src/main/java/nju/seec/helper/api/dataanalysis/EventType.java → src/main/java/nju/seec/helper/api/dataanalysis/constant/EventType.java

@@ -1,4 +1,4 @@
-package nju.seec.helper.api.dataanalysis;
+package nju.seec.helper.api.dataanalysis.constant;
 
 import lombok.Getter;
 

+ 21 - 0
src/main/java/nju/seec/helper/api/dataanalysis/model/Event.java

@@ -0,0 +1,21 @@
+package nju.seec.helper.api.dataanalysis.model;
+
+import lombok.Builder;
+import lombok.Data;
+import nju.seec.helper.api.dataanalysis.constant.EventAction;
+import nju.seec.helper.api.dataanalysis.constant.EventType;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author seec data analysis
+ */
+@Data
+@Builder
+public class Event {
+    private String userId;
+    private EventType type;
+    private LocalDateTime time;
+    private EventAction action;
+    private String description;
+}

+ 38 - 0
src/main/java/nju/seec/helper/api/dataanalysis/model/ExerciseRecord.java

@@ -0,0 +1,38 @@
+package nju.seec.helper.api.dataanalysis.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author seec data analysis
+ */
+@Data
+@Builder
+public class ExerciseRecord {
+    /**
+     * 问题的ID
+     */
+    private String questionId;
+    /**
+     * 用户的ID
+     */
+    private String userId;
+    /**
+     * 做题开始时间
+     */
+    private LocalDateTime startAt;
+    /**
+     * 做题结束时间
+     */
+    private LocalDateTime endAt;
+    /**
+     * 做题的时间(秒)
+     */
+    private Long durationSeconds;
+    /**
+     * 该题是否通过
+     */
+    private Boolean pass;
+}

+ 12 - 11
src/main/java/nju/seec/helper/aspect/auth/AuthAspect.java

@@ -1,5 +1,6 @@
 package nju.seec.helper.aspect.auth;
 
+import cn.hutool.core.util.StrUtil;
 import com.auth0.jwt.JWT;
 import com.auth0.jwt.JWTVerifier;
 import com.auth0.jwt.algorithms.Algorithm;
@@ -16,13 +17,13 @@ import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Before;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -32,6 +33,7 @@ import java.util.stream.Collectors;
  */
 @Aspect
 @Component
+@Order(1)
 public class AuthAspect {
     @Value("${jwt.secret}")
     private String jwtSecret;
@@ -50,12 +52,12 @@ public class AuthAspect {
                 .stream(auth.roles())
                 .collect(Collectors.toSet())
                 .contains(user.getRole())) {
-            throw HelperException.of(ExceptionType.FORBIDDEN, String.format("您暂时无法%s,请重新登录", auth.message()));
+            throw HelperException.of(ExceptionType.FORBIDDEN, StrUtil.format("您暂时无法{},请重新登录", auth.message()));
         }
 
         Object[] objects = joinPoint.getArgs();
         for (Object o : objects) {
-            if (o.getClass() == LoginUser.class) {
+            if (o instanceof LoginUser) {
                 BeanUtils.copyProperties(user, o);
                 break;
             }
@@ -76,20 +78,19 @@ public class AuthAspect {
         try {
             decodedJWT = jwtVerifier.verify(token);
         } catch (JWTVerificationException e) {
-//            e.printStackTrace();
             throw HelperException.of(ExceptionType.UNAUTHORIZED, "无效Token");
         }
 
-        boolean isExpired = JWT.decode(token).getExpiresAt().before(new Date());
-
-        if (isExpired) {
-            throw HelperException.of(ExceptionType.UNAUTHORIZED, "Token已过期");
-        }
+//        boolean isExpired = JWT.decode(token).getExpiresAt().before(new Date());
+//
+//        if (isExpired) {
+//            throw HelperException.of(ExceptionType.UNAUTHORIZED, "Token已过期");
+//        }
 
         UserInfo userInfo = decodedJWT.getClaim("user_info").as(UserInfo.class);
 
         if (userInfo.role == UserRole.ADMIN) {
-            return LoginUser.of(null, UserRole.ADMIN);
+            return LoginUser.of(null, userInfo.id, UserRole.ADMIN);
         }
 
         User user = userService.getUserByPhoneOrPid(userInfo.phone, userInfo.id).orElse(new User());
@@ -102,7 +103,7 @@ public class AuthAspect {
 
         user = userService.createOrUpdateUser(user);
 
-        return LoginUser.of(user.getId(), user.getRole());
+        return LoginUser.of(user.getId(), user.getPid(), user.getRole());
     }
 
     @Data

+ 1 - 0
src/main/java/nju/seec/helper/aspect/auth/LoginUser.java

@@ -13,5 +13,6 @@ import nju.seec.helper.enums.UserRole;
 @NoArgsConstructor
 public class LoginUser {
     private Long id;
+    private Long pid;
     private UserRole role;
 }

+ 18 - 0
src/main/java/nju/seec/helper/aspect/event/Event.java

@@ -0,0 +1,18 @@
+package nju.seec.helper.aspect.event;
+
+import nju.seec.helper.api.dataanalysis.constant.EventAction;
+import nju.seec.helper.api.dataanalysis.constant.EventType;
+
+import java.lang.annotation.*;
+
+/**
+ * @author cst
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Event {
+    EventType type();
+
+    EventAction action();
+}

+ 83 - 0
src/main/java/nju/seec/helper/aspect/event/EventAspect.java

@@ -0,0 +1,83 @@
+package nju.seec.helper.aspect.event;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.SneakyThrows;
+import nju.seec.helper.api.dataanalysis.EventApi;
+import nju.seec.helper.api.dataanalysis.constant.EventAction;
+import nju.seec.helper.api.dataanalysis.constant.EventType;
+import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.dao.CourseDAO;
+import nju.seec.helper.dao.QuizDAO;
+import nju.seec.helper.dto.comment.CommentDTO;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+/**
+ * @author cst
+ */
+@Aspect
+@Component
+public class EventAspect {
+    private final Map<EventType, Map<EventAction, EventHandler>> eventHandleMap;
+
+    public EventAspect(CourseDAO courseDAO, QuizDAO quizDAO, EventApi eventApi) {
+        this.eventHandleMap = ImmutableMap.of(
+                EventType.LESSON, ImmutableMap.of(
+                        EventAction.PARTICIPATE, ((joinPoint, user) -> Stream.of(joinPoint.getArgs())
+                                .filter(o -> o instanceof Long)
+                                .map(o -> (Long) o)
+                                .findFirst()
+                                .ifPresent(courseId ->
+                                        eventApi.reportEvent(eventApi.createParticipateClassEventDTO(String.valueOf(user.getPid()), LocalDateTime.now(), courseDAO.findCourseById(courseId)))
+                                )))
+                , EventType.POST, ImmutableMap.of(
+                        EventAction.INITIATE, ((joinPoint, user) -> Stream.of(joinPoint.getArgs())
+                                .filter(o -> o instanceof CommentDTO)
+                                .map(o -> (CommentDTO) o)
+                                .findFirst()
+                                .ifPresent(commentDTO ->
+                                        eventApi.reportEvent(eventApi.createInitPostEventDTO(String.valueOf(user.getPid()), LocalDateTime.now(), Objects.requireNonNull(commentDTO).getTitle()))
+                                )))
+                , EventType.QUIZ, ImmutableMap.of(
+                        EventAction.COMPLETE, ((joinPoint, user) -> Stream.of(joinPoint.getArgs())
+                                .filter(o -> o instanceof Long)
+                                .map(o -> (Long) o)
+                                .findFirst()
+                                .ifPresent(quizId ->
+                                        eventApi.reportEvent(eventApi.createCompleteQuizEventDTO(String.valueOf(user.getPid()), LocalDateTime.now(), quizDAO.findQuizById(quizId)))
+                                )))
+        );
+    }
+
+    @FunctionalInterface
+    private interface EventHandler {
+        /**
+         * 处理事件
+         *
+         * @param joinPoint 插入点
+         * @param user      登录用户
+         */
+        void handleEvent(JoinPoint joinPoint, LoginUser user);
+    }
+
+    private final EventHandler doNothing = (joinPoint, user) -> {
+    };
+
+    @Transactional(readOnly = true)
+    @SneakyThrows
+    @AfterReturning("@annotation(event) && args(user,..)")
+    public void reportEvent(JoinPoint joinPoint, Event event, LoginUser user) {
+        eventHandleMap.getOrDefault(event.type(), Collections.emptyMap())
+                .getOrDefault(event.action(), doNothing)
+                .handleEvent(joinPoint, user);
+    }
+}

+ 15 - 0
src/main/java/nju/seec/helper/aspect/message/Message.java

@@ -0,0 +1,15 @@
+package nju.seec.helper.aspect.message;
+
+import nju.seec.helper.enums.MessageType;
+
+import java.lang.annotation.*;
+
+/**
+ * @author cst
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Message {
+    MessageType value();
+}

+ 129 - 0
src/main/java/nju/seec/helper/aspect/message/MessageAspect.java

@@ -0,0 +1,129 @@
+package nju.seec.helper.aspect.message;
+
+import com.google.common.collect.ImmutableMap;
+import nju.seec.helper.dao.*;
+import nju.seec.helper.dto.reply.ReplyDTO;
+import nju.seec.helper.entity.Comment;
+import nju.seec.helper.entity.Course;
+import nju.seec.helper.entity.Quiz;
+import nju.seec.helper.entity.Slide;
+import nju.seec.helper.enums.MessageType;
+import nju.seec.helper.enums.QuizState;
+import nju.seec.helper.enums.SlideState;
+import nju.seec.helper.service.MessageService;
+import nju.seec.helper.vo.CommentVO;
+import nju.seec.helper.vo.NoticeVO;
+import nju.seec.helper.vo.quiz.QuizVO;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * @author cst
+ */
+@Component
+@Aspect
+public class MessageAspect {
+    private final Map<MessageType, MessageCreator> messageCreatorMap;
+
+    public MessageAspect(CourseDAO courseDAO, SlideDAO slideDAO, QuizDAO quizDAO, CommentDAO commentDAO, ChooseDAO chooseDAO, MessageService messageService) {
+        messageCreatorMap = ImmutableMap.<MessageType, MessageCreator>builder()
+                .put(MessageType.SLIDE_IN_CLASS, ((joinPoint, object) -> {
+                    Object[] args = joinPoint.getArgs();
+                    Stream.of(args)
+                            .filter(o -> o instanceof Long)
+                            .map(o -> (Long) o)
+                            .findFirst()
+                            .ifPresent(slideId -> {
+                                Slide slide = slideDAO.findSlideById(slideId);
+                                if (slide.getState() == SlideState.IN_CLASS) {
+                                    Set<Long> studentIds = chooseDAO.findStudentIdsByCourseId(slide.getCourse().getId());
+                                    messageService.createMessage(studentIds, MessageType.SLIDE_IN_CLASS, String.format("[%s] - [%s] 已开课", slide.getCourse().getName(), slide.getName()), slide.getId());
+                                }
+                            });
+                }))
+                .put(MessageType.COMMENT_NEW, (joinPoint, object) -> {
+                    if (object instanceof CommentVO) {
+                        CommentVO commentVO = (CommentVO) object;
+                        Slide slide = slideDAO.findSlideById(commentVO.getSlideId());
+                        messageService.createMessage(Collections.singleton(slide.getTeacher().getId()), MessageType.COMMENT_NEW, String.format("[%s] - [%s] 在第%d页有新评论", slide.getCourse().getName(), slide.getName(), commentVO.getPageNumber()), slide.getId());
+                    }
+                })
+                .put(MessageType.COMMENT_REPLY, (joinPoint, object) -> {
+                    Stream.of(joinPoint.getArgs())
+                            .filter(o -> o instanceof ReplyDTO)
+                            .map(o -> (ReplyDTO) o)
+                            .findFirst()
+                            .ifPresent(replyDTO -> {
+                                Comment comment = commentDAO.findCommentById(replyDTO.getCommentId());
+                                Slide slide = slideDAO.findSlideById(comment.getSlideId());
+                                messageService.createMessage(Collections.singleton(comment.getUser().getId()), MessageType.COMMENT_REPLY, String.format("您在 [%s] - [%s] 第%d页发表的讨论帖已收到回复", slide.getCourse().getName(), slide.getName(), comment.getPageNumber()), slide.getId());
+                            })
+                    ;
+                })
+                .put(MessageType.NOTICE_NEW, (joinPoint, object) -> {
+                    if (object instanceof NoticeVO) {
+                        NoticeVO noticeVO = (NoticeVO) object;
+                        Set<Long> studentIds = chooseDAO.findStudentIdsByCourseId(noticeVO.getCourseId());
+                        Course course = courseDAO.findCourseById(noticeVO.getCourseId());
+                        messageService.createMessage(studentIds, MessageType.NOTICE_NEW, String.format("[%s] 发布了新公告", course.getName()), course.getId());
+                    }
+                })
+                .put(MessageType.QUIZ_NEW, new MessageCreator() {
+                    @Override
+                    public void createMessage(JoinPoint joinPoint, Object object) {
+                        if (object instanceof QuizVO) {
+                            sendMessages(quizDAO.findQuizById(((QuizVO) object).getId()));
+                        } else {
+                            Stream.of(joinPoint.getArgs())
+                                    .filter(o -> o instanceof Slide)
+                                    .map(o -> (Slide) o)
+                                    .findFirst()
+                                    .ifPresent(slide -> {
+                                        List<Quiz> quizzes = quizDAO.findBySlide(slide);
+                                        quizzes.forEach(this::sendMessages);
+                                    })
+                            ;
+                        }
+                    }
+
+                    private void sendMessages(Quiz quiz) {
+                        if (quiz.getState() == QuizState.ONGOING) {
+                            Slide slide = quiz.getSlide();
+                            Course course = slide.getCourse();
+                            Set<Long> chooseStudentIds = chooseDAO.findStudentIdsByCourseId(course.getId());
+                            messageService.createMessage(chooseStudentIds, MessageType.QUIZ_NEW, String.format("[%s] - [%s] 的测试 [%s] 已开放", course.getName(), slide.getName(), quiz.getName()), quiz.getId());
+                        }
+                    }
+                })
+                .build();
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @AfterReturning(value = "@annotation(message)", returning = "object")
+    public void createMessage(JoinPoint joinPoint, Message message, Object object) {
+        messageCreatorMap.getOrDefault(message.value(), doNothing).createMessage(joinPoint, object);
+    }
+
+    @FunctionalInterface
+    interface MessageCreator {
+        /**
+         * 创建消息
+         *
+         * @param joinPoint
+         * @param object
+         */
+        void createMessage(JoinPoint joinPoint, Object object);
+    }
+
+    private final MessageCreator doNothing = ((joinPoint, object) -> {
+    });
+}

+ 12 - 0
src/main/java/nju/seec/helper/aspect/record/ExerciseRecord.java

@@ -0,0 +1,12 @@
+package nju.seec.helper.aspect.record;
+
+import java.lang.annotation.*;
+
+/**
+ * @author cst
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ExerciseRecord {
+}

+ 79 - 0
src/main/java/nju/seec/helper/aspect/record/ExerciseRecordAspect.java

@@ -0,0 +1,79 @@
+package nju.seec.helper.aspect.record;
+
+import nju.seec.helper.api.bok.RecordApi;
+import nju.seec.helper.api.dataanalysis.ExerciseApi;
+import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.dto.quiz.QuizStudentAnswerDTO;
+import nju.seec.helper.vo.quiz.QuizStudentAnswerVO;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author cst
+ */
+@Aspect
+@Component
+public class ExerciseRecordAspect {
+    private final ExerciseApi exerciseApi;
+    private final RecordApi recordApi;
+
+    public ExerciseRecordAspect(ExerciseApi exerciseApi, RecordApi recordApi) {
+        this.exerciseApi = exerciseApi;
+        this.recordApi = recordApi;
+    }
+
+    @Pointcut(value = "@annotation(exerciseRecord) && args(user,..,quizStudentAnswerDTO)", argNames = "exerciseRecord,user,quizStudentAnswerDTO")
+    public void pointCut(ExerciseRecord exerciseRecord, LoginUser user, QuizStudentAnswerDTO quizStudentAnswerDTO) {
+
+    }
+
+    @AfterReturning(value = "pointCut(exerciseRecord,user,quizStudentAnswerDTO)", returning = "quizStudentAnswerVO", argNames = "exerciseRecord,user,quizStudentAnswerDTO,quizStudentAnswerVO")
+    public void reportRecordToData(ExerciseRecord exerciseRecord, LoginUser user, QuizStudentAnswerDTO quizStudentAnswerDTO, QuizStudentAnswerVO quizStudentAnswerVO) {
+        String userId = String.valueOf(user.getPid());
+        Map<String, QuizStudentAnswerVO.QuestionStudentAnswerInfo> questionStudentAnswerInfoMap = quizStudentAnswerVO.getAnswers();
+        exerciseApi.reportExercise(
+                quizStudentAnswerDTO
+                        .getRecords()
+                        .entrySet()
+                        .stream()
+                        .map(answerRecordDTOEntry -> {
+                            String questionId = answerRecordDTOEntry.getKey();
+                            QuizStudentAnswerDTO.AnswerRecordDTO answerRecordDTO = answerRecordDTOEntry.getValue();
+                            QuizStudentAnswerVO.QuestionStudentAnswerInfo studentAnswerInfo = questionStudentAnswerInfoMap.get(questionId);
+                            return studentAnswerInfo != null
+                                    ? exerciseApi.createExerciseRecordDTO(userId, questionId, answerRecordDTO.getStartAt(), answerRecordDTO.getEndAt(), studentAnswerInfo.getCorrect())
+                                    : null;
+                        })
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toList())
+        );
+    }
+
+    @AfterReturning(value = "pointCut(exerciseRecord,user,quizStudentAnswerDTO)", returning = "quizStudentAnswerVO", argNames = "exerciseRecord,user,quizStudentAnswerDTO,quizStudentAnswerVO")
+    public void reportRecordToBok(ExerciseRecord exerciseRecord, LoginUser user, QuizStudentAnswerDTO quizStudentAnswerDTO, QuizStudentAnswerVO quizStudentAnswerVO) {
+        String userId = String.valueOf(user.getPid());
+        Map<String, QuizStudentAnswerVO.QuestionStudentAnswerInfo> questionStudentAnswerInfoMap = quizStudentAnswerVO.getAnswers();
+        recordApi.reportUserRecord(
+                quizStudentAnswerDTO
+                        .getRecords()
+                        .entrySet()
+                        .stream()
+                        .map(answerRecordDTOEntry -> {
+                            String questionId = answerRecordDTOEntry.getKey();
+                            QuizStudentAnswerDTO.AnswerRecordDTO answerRecordDTO = answerRecordDTOEntry.getValue();
+                            QuizStudentAnswerVO.QuestionStudentAnswerInfo studentAnswerInfo = questionStudentAnswerInfoMap.get(questionId);
+                            return studentAnswerInfo != null
+                                    ? recordApi.createRecordDTO(userId, questionId, studentAnswerInfo.getAnswer(), studentAnswerInfo.getCorrect(), answerRecordDTO.getStartAt(), answerRecordDTO.getEndAt())
+                                    : null;
+                        })
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toList())
+        );
+    }
+}

+ 8 - 1
src/main/java/nju/seec/helper/controller/CommentController.java

@@ -1,7 +1,10 @@
 package nju.seec.helper.controller;
 
+import nju.seec.helper.api.dataanalysis.constant.EventAction;
+import nju.seec.helper.api.dataanalysis.constant.EventType;
 import nju.seec.helper.aspect.auth.Auth;
 import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.aspect.event.Event;
 import nju.seec.helper.controller.response.PageResponse;
 import nju.seec.helper.dto.comment.CommentDTO;
 import nju.seec.helper.enums.UserRole;
@@ -33,12 +36,13 @@ public class CommentController {
     /**
      * 发表评论
      */
+    @Event(type = EventType.POST, action = EventAction.INITIATE)
     @Auth(roles = {UserRole.TEACHER, UserRole.STUDENT}, message = "发表评论")
     @PostMapping
     public CommentVO createComment(LoginUser user,
                                    @Validated @RequestBody CommentDTO commentDTO) {
         CommentVO commentVO = commentService.createComment(user, commentDTO);
-        CommentWebsocket.sendAllMessage(commentDTO.getSlideId(), JsonUtils.toJson(commentVO));
+        CommentWebsocket.sendAllMessageToOneSlide(commentDTO.getSlideId(), JsonUtils.toJson(commentVO));
         return commentVO;
     }
 
@@ -105,6 +109,9 @@ public class CommentController {
         return PageResponse.of(commentService.getSelfCommentsBySlideAndPageNumber(user, slideId, pageNumber, show, pageable));
     }
 
+    /**
+     * 修改评论的展示状态
+     */
     @Auth(roles = {UserRole.TEACHER, UserRole.STUDENT}, message = "修改评论展示状态")
     @PutMapping("/{commentId}/show")
     public void modifyCommentShow(LoginUser user,

+ 9 - 15
src/main/java/nju/seec/helper/controller/CommentWebsocket.java

@@ -12,6 +12,7 @@ import javax.websocket.server.PathParam;
 import javax.websocket.server.ServerEndpoint;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
@@ -24,7 +25,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
 @EqualsAndHashCode
 @Component
 public class CommentWebsocket {
-    private static Map<Long, Set<CommentWebsocket>> COMMENT_WEBSOCKET_MAP = new ConcurrentHashMap<>();
+    private static final Map<Long, Set<CommentWebsocket>> COMMENT_WEBSOCKET_MAP = new ConcurrentHashMap<>();
     private Long slideId;
     private Session session;
 
@@ -34,10 +35,10 @@ public class CommentWebsocket {
         session.setMaxIdleTimeout(3 * 60 * 60 * 1000);
         this.session = session;
         this.slideId = slideId;
-        if (!COMMENT_WEBSOCKET_MAP.containsKey(slideId)) {
-            COMMENT_WEBSOCKET_MAP.put(slideId, new CopyOnWriteArraySet<>());
-        }
-        COMMENT_WEBSOCKET_MAP.get(slideId).add(this);
+        Set<CommentWebsocket> websocketSet = new CopyOnWriteArraySet<>();
+        Optional.ofNullable(COMMENT_WEBSOCKET_MAP.putIfAbsent(slideId, websocketSet))
+                .orElse(websocketSet)
+                .add(this);
     }
 
     @OnClose
@@ -47,19 +48,12 @@ public class CommentWebsocket {
 
     @OnError
     public void onError(Throwable throwable) {
-        log.error(throwable.getLocalizedMessage(), throwable);
+        log.error(throwable.getLocalizedMessage());
     }
 
-    public static void sendAllMessage(Long slideId, String message) {
+    public static void sendAllMessageToOneSlide(Long slideId, String message) {
         COMMENT_WEBSOCKET_MAP
                 .getOrDefault(slideId, Collections.emptySet())
-                .parallelStream()
-                .forEach(commentWebsocket -> {
-                    try {
-                        commentWebsocket.session.getAsyncRemote().sendText(message);
-                    } catch (Exception e) {
-                        log.error(e.getLocalizedMessage());
-                    }
-                });
+                .forEach(commentWebsocket -> commentWebsocket.session.getAsyncRemote().sendText(message));
     }
 }

+ 4 - 0
src/main/java/nju/seec/helper/controller/CourseController.java

@@ -1,7 +1,10 @@
 package nju.seec.helper.controller;
 
+import nju.seec.helper.api.dataanalysis.constant.EventAction;
+import nju.seec.helper.api.dataanalysis.constant.EventType;
 import nju.seec.helper.aspect.auth.Auth;
 import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.aspect.event.Event;
 import nju.seec.helper.controller.response.PageResponse;
 import nju.seec.helper.dto.course.ChooseDTO;
 import nju.seec.helper.dto.course.CourseDTO;
@@ -64,6 +67,7 @@ public class CourseController {
     /**
      * 学生选课
      */
+    @Event(type = EventType.LESSON, action = EventAction.PARTICIPATE)
     @Auth(roles = UserRole.STUDENT, message = "选课")
     @PostMapping("/{courseId}/choose")
     public void chooseCourse(LoginUser user,

+ 7 - 2
src/main/java/nju/seec/helper/controller/EventController.java

@@ -1,6 +1,6 @@
 package nju.seec.helper.controller;
 
-import nju.seec.helper.vo.EventVO;
+import nju.seec.helper.api.dataanalysis.model.Event;
 import nju.seec.helper.service.EventService;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -9,6 +9,8 @@ import org.springframework.web.bind.annotation.RestController;
 import java.util.List;
 
 /**
+ * 事件
+ *
  * @author cst
  */
 @RestController
@@ -20,8 +22,11 @@ public class EventController {
         this.eventService = eventService;
     }
 
+    /**
+     * 获取所有事件
+     */
     @GetMapping
-    public List<EventVO> getAllEvents() {
+    public List<Event> getAllEvents() {
         return eventService.getAllEvents();
     }
 }

+ 10 - 3
src/main/java/nju/seec/helper/controller/KnowledgeController.java

@@ -1,7 +1,7 @@
 package nju.seec.helper.controller;
 
 import nju.seec.helper.api.bok.KnowledgeApi;
-import nju.seec.helper.vo.knowledge.BokKnowledgeNodeVO;
+import nju.seec.helper.api.bok.vo.KnowledgeNodeVO;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -9,6 +9,8 @@ import org.springframework.web.bind.annotation.RestController;
 import java.util.List;
 
 /**
+ * 知识点
+ *
  * @author cst
  */
 @RestController
@@ -20,8 +22,13 @@ public class KnowledgeController {
         this.knowledgeApi = knowledgeApi;
     }
 
+    /**
+     * 获取所有知识点
+     *
+     * @return
+     */
     @GetMapping
-    public List<BokKnowledgeNodeVO> getKnowledgeNodes() {
-        return knowledgeApi.getKnowledgeNodes();
+    public List<KnowledgeNodeVO> getAllKnowledgeNodes() {
+        return knowledgeApi.getAllKnowledgeNodes();
     }
 }

+ 60 - 10
src/main/java/nju/seec/helper/controller/QuestionController.java

@@ -1,5 +1,8 @@
 package nju.seec.helper.controller;
 
+import nju.seec.helper.api.bok.dto.CollectDTO;
+import nju.seec.helper.api.bok.dto.RateDTO;
+import nju.seec.helper.api.bok.vo.InteractiveVO;
 import nju.seec.helper.aspect.auth.Auth;
 import nju.seec.helper.aspect.auth.LoginUser;
 import nju.seec.helper.controller.response.PageResponse;
@@ -23,7 +26,6 @@ import java.util.Set;
  * @author xst
  * <p>
  * updated by cst
- * @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")
@@ -39,8 +41,12 @@ public class QuestionController {
      */
     @Auth(roles = {UserRole.TEACHER}, message = "获得题目列表")
     @GetMapping
-    public PageResponse<BaseQuestionVO> getQuestionList(LoginUser user, @RequestParam(required = false, defaultValue = "") String key, @PageableDefault(Integer.MAX_VALUE) Pageable pageable) {
-        return PageResponse.of(questionService.getQuestions(user, key, pageable));
+    public PageResponse<BaseQuestionVO> getQuestionList(LoginUser user,
+                                                        @RequestParam(required = false, defaultValue = "") String stem,
+                                                        @RequestParam(required = false, defaultValue = "") String tag,
+                                                        @RequestParam(required = false, defaultValue = "") String knowledgeId,
+                                                        @PageableDefault(Integer.MAX_VALUE) Pageable pageable) {
+        return PageResponse.of(questionService.getQuestions(user, stem, tag, knowledgeId, pageable));
     }
 
     /**
@@ -55,7 +61,7 @@ public class QuestionController {
     /**
      * 获取某一题目信息
      */
-    @Auth(roles = UserRole.TEACHER, message = "获取题目信息")
+    @Auth(roles = {UserRole.TEACHER}, message = "获取题目信息")
     @GetMapping("/{questionId}")
     public BaseQuestionVO getOneQuestion(LoginUser user, @PathVariable String questionId) {
         return questionService.getOneQuestion(user, questionId);
@@ -93,8 +99,7 @@ public class QuestionController {
      */
     @Auth(roles = UserRole.TEACHER, message = "获取创建的题目")
     @GetMapping("/created")
-    public PageResponse<BaseQuestionVO> getCreatedQuestions(LoginUser user,
-                                                            @PageableDefault(size = Integer.MAX_VALUE) Pageable pageable) {
+    public PageResponse<BaseQuestionVO> getCreatedQuestions(LoginUser user, @PageableDefault(size = Integer.MAX_VALUE) Pageable pageable) {
         return PageResponse.of(questionService.getCreatedQuestions(user, pageable));
     }
 
@@ -107,9 +112,54 @@ public class QuestionController {
         return questionService.getIdsOfCreatedQuestions(user);
     }
 
-    @Auth(roles = UserRole.STUDENT, message = "收藏题目")
-    @PostMapping("/{questionId}/star")
-    public void starQuestion(LoginUser user, @PathVariable String questionId) {
-        questionService.starQuestion(user, questionId);
+    /**
+     * 收藏题目
+     */
+    @Auth(roles = {UserRole.TEACHER, UserRole.STUDENT}, message = "收藏题目")
+    @PostMapping("/{questionId}/collect")
+    public void starQuestion(LoginUser user, @PathVariable String questionId, @Validated @RequestBody CollectDTO collectDTO) {
+        questionService.collectQuestion(user, questionId, collectDTO);
+    }
+
+    /**
+     * 评价题目
+     */
+    @Auth(roles = {UserRole.TEACHER, UserRole.STUDENT}, message = "评价题目")
+    @PostMapping("/{questionId}/rate")
+    public void rateQuestion(LoginUser user, @PathVariable String questionId, @Validated @RequestBody RateDTO rateDTO) {
+        questionService.rateQuestion(user, questionId, rateDTO);
+    }
+
+    /**
+     * 取得收藏评价的题目
+     */
+    @Auth(roles = {UserRole.TEACHER, UserRole.STUDENT}, message = "取得收藏评价的题目")
+    @GetMapping("/collected")
+    public PageResponse<BaseQuestionVO> getCollectedQuestions(LoginUser user, @PageableDefault(size = Integer.MAX_VALUE) Pageable pageable) {
+        return PageResponse.of(questionService.getCollectedAndRatedQuestions(user, true, null, pageable));
+    }
+
+    @Auth(roles = {UserRole.TEACHER, UserRole.STUDENT}, message = "取得收藏评价的题目")
+    @GetMapping("/rated")
+    public PageResponse<BaseQuestionVO> getRatedQuestions(LoginUser user, @PageableDefault(size = Integer.MAX_VALUE) Pageable pageable) {
+        return PageResponse.of(questionService.getCollectedAndRatedQuestions(user, null, true, pageable));
+    }
+
+    /**
+     * 取得完成的题目
+     */
+    @Auth(roles = UserRole.STUDENT, message = "取得完成的题目")
+    @GetMapping("/completed")
+    public PageResponse<BaseQuestionVO> getCompletedQuestions(LoginUser user, @PageableDefault(size = Integer.MAX_VALUE) Pageable pageable) {
+        return PageResponse.of(questionService.getCompletedQuestions(user, pageable));
+    }
+
+    /**
+     * 取得与题目的交互信息
+     */
+    @Auth(roles = {UserRole.TEACHER, UserRole.STUDENT}, message = "取得与题目的交互信息")
+    @GetMapping("/interactive")
+    public List<InteractiveVO> getInteractiveList(LoginUser user, String questionId) {
+        return questionService.getInteractiveList(user, questionId);
     }
 }

+ 7 - 2
src/main/java/nju/seec/helper/controller/QuizController.java

@@ -1,7 +1,11 @@
 package nju.seec.helper.controller;
 
+import nju.seec.helper.api.dataanalysis.constant.EventAction;
+import nju.seec.helper.api.dataanalysis.constant.EventType;
 import nju.seec.helper.aspect.auth.Auth;
 import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.aspect.event.Event;
+import nju.seec.helper.aspect.record.ExerciseRecord;
 import nju.seec.helper.controller.response.PageResponse;
 import nju.seec.helper.dto.groups.Create;
 import nju.seec.helper.dto.groups.Modify;
@@ -25,7 +29,6 @@ import org.springframework.web.bind.annotation.*;
  * @author xst
  * <p>
  * updated by cst
- * @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")
@@ -95,7 +98,7 @@ public class QuizController {
     }
 
     /**
-     * 修改测试 (仅限未开始时可以修改)
+     * 修改测试 (仅限无人提交时可以修改)
      */
     @Auth(roles = {UserRole.TEACHER}, message = "修改测试")
     @PutMapping("/{quizId}")
@@ -115,6 +118,8 @@ public class QuizController {
     /**
      * 提交作答
      */
+    @ExerciseRecord
+    @Event(type = EventType.QUIZ, action = EventAction.COMPLETE)
     @Auth(roles = {UserRole.STUDENT}, message = "提交作答")
     @PostMapping("/{quizId}/student-answer")
     public QuizStudentAnswerVO submitQuizStudentAnswer(LoginUser user, @PathVariable Long quizId, @Validated @RequestBody QuizStudentAnswerDTO quizStudentAnswerDTO) {

+ 33 - 0
src/main/java/nju/seec/helper/controller/RecordController.java

@@ -0,0 +1,33 @@
+package nju.seec.helper.controller;
+
+import nju.seec.helper.api.dataanalysis.model.ExerciseRecord;
+import nju.seec.helper.service.RecordService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * @author cst
+ */
+@RestController
+@RequestMapping("/api/record")
+public class RecordController {
+    private final RecordService recordService;
+
+    public RecordController(RecordService recordService) {
+        this.recordService = recordService;
+    }
+
+    @GetMapping
+    public List<ExerciseRecord> getAllRecords() {
+        return recordService.getAllRecords();
+    }
+
+    @GetMapping("/{userId}")
+    public List<ExerciseRecord> getRecordsByUserId(@PathVariable String userId) {
+        return recordService.getRecordsByUserId(userId);
+    }
+}

+ 0 - 3
src/main/java/nju/seec/helper/controller/UserController.java

@@ -17,9 +17,6 @@ import org.springframework.web.bind.annotation.RestController;
 @RestController
 @RequestMapping("/api/user")
 public class UserController {
-    //    @Value("${helper.user-timeout-seconds}")
-//    private int userTimeoutSeconds;
-//
     private final UserService userService;
 
     public UserController(UserService userService) {

+ 22 - 22
src/main/java/nju/seec/helper/dao/AdminDAO.java

@@ -1,22 +1,22 @@
-package nju.seec.helper.dao;
-
-import nju.seec.helper.entity.Admin;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.stereotype.Repository;
-
-import java.util.Optional;
-
-/**
- * @author cst
- */
-@Repository
-public interface AdminDAO extends JpaRepository<Admin, Long> {
-    /**
-     * 用户名密码查询
-     *
-     * @param username
-     * @param password
-     * @return
-     */
-    Optional<Admin> findByUsernameAndPassword(String username, String password);
-}
+//package nju.seec.helper.dao;
+//
+//import nju.seec.helper.entity.Admin;
+//import org.springframework.data.jpa.repository.JpaRepository;
+//import org.springframework.stereotype.Repository;
+//
+//import java.util.Optional;
+//
+///**
+// * @author cst
+// */
+//@Repository
+//public interface AdminDAO extends JpaRepository<Admin, Long> {
+//    /**
+//     * 用户名密码查询
+//     *
+//     * @param username
+//     * @param password
+//     * @return
+//     */
+//    Optional<Admin> findByUsernameAndPassword(String username, String password);
+//}

+ 16 - 1
src/main/java/nju/seec/helper/dao/QuizDAO.java

@@ -64,7 +64,7 @@ public interface QuizDAO extends JpaRepository<Quiz, Long>, JpaSpecificationExec
      * @param pageable
      * @return
      */
-    default Page<Quiz> findByCourseIdsAndStateAndNameContains(Set<Long> courseIds, QuizState quizState, String name, Pageable pageable) {
+    default Page<Quiz> findByCourseIdInAndStateAndNameContains(Set<Long> courseIds, QuizState quizState, String name, Pageable pageable) {
         return this.findAll(
                 (Specification<Quiz>) (root, query, cb) -> cb.and(
                         root.get("course").get("id").in(courseIds)
@@ -94,4 +94,19 @@ public interface QuizDAO extends JpaRepository<Quiz, Long>, JpaSpecificationExec
      * @return
      */
     Page<Quiz> findByTeacherAndStateAndNameContains(User teacher, QuizState quizState, String name, Pageable pageable);
+
+    /**
+     * 根据课程ID和状态获取测试
+     *
+     * @param courseIds
+     * @param states
+     * @return
+     */
+    default List<Quiz> findByCourseIdInAndStateIn(Set<Long> courseIds, Set<QuizState> states) {
+        return this.findAll(
+                (Specification<Quiz>) (root, query, cb) -> cb.and(
+                        root.get("course").get("id").in(courseIds)
+                        , root.get("state").in(states)
+                ));
+    }
 }

+ 8 - 0
src/main/java/nju/seec/helper/dao/UserDAO.java

@@ -9,6 +9,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 import org.springframework.stereotype.Repository;
 
+import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 
@@ -86,4 +87,11 @@ public interface UserDAO extends JpaRepository<User, Long>, JpaSpecificationExec
      * @return
      */
     Page<User> findByIdIn(Set<Long> ids, Pageable pageable);
+
+    /**
+     * 查询所有具有PID的用户
+     *
+     * @return
+     */
+    List<User> findByPidNotNull();
 }

+ 18 - 18
src/main/java/nju/seec/helper/dto/common/LoginDTO.java

@@ -1,18 +1,18 @@
-package nju.seec.helper.dto.common;
-
-import lombok.Data;
-
-import javax.validation.constraints.NotBlank;
-import java.io.Serializable;
-
-/**
- * @author cst
- */
-@Data
-public class LoginDTO implements Serializable {
-    private static final long serialVersionUID = -3359173125836901241L;
-    @NotBlank(message = "登录名不能为空")
-    private String username;
-    @NotBlank(message = "密码不能为空")
-    private String password;
-}
+//package nju.seec.helper.dto.common;
+//
+//import lombok.Data;
+//
+//import javax.validation.constraints.NotBlank;
+//import java.io.Serializable;
+//
+///**
+// * @author cst
+// */
+//@Data
+//public class LoginDTO implements Serializable {
+//    private static final long serialVersionUID = -3359173125836901241L;
+//    @NotBlank(message = "登录名不能为空")
+//    private String username;
+//    @NotBlank(message = "密码不能为空")
+//    private String password;
+//}

+ 9 - 7
src/main/java/nju/seec/helper/dto/quiz/QuizStudentAnswerDTO.java

@@ -5,6 +5,7 @@ import lombok.Data;
 import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Past;
 import java.io.Serializable;
 import java.time.LocalDateTime;
 import java.util.Map;
@@ -18,17 +19,18 @@ public class QuizStudentAnswerDTO implements Serializable {
     @NotEmpty(message = "回答不能为空")
     private Map<String, Object> answers;
 
+    @NotNull(message = "缺少做题记录")
     @Valid
     private Map<String, AnswerRecordDTO> records;
 
     @Data
     public static class AnswerRecordDTO {
-        @NotNull(message = "做题记录缺少做题日期")
-        private LocalDateTime answerAt;
-        /**
-         * 秒数
-         */
-        @NotNull(message = "做题记录缺少时长")
-        private Long answerSeconds;
+        @NotNull(message = "缺少做题开始时间")
+        @Past(message = "开始时间错误")
+        private LocalDateTime startAt;
+
+        @NotNull(message = "缺少做题结束时间")
+        @Past(message = "结束时间错误")
+        private LocalDateTime endAt;
     }
 }

+ 30 - 30
src/main/java/nju/seec/helper/dto/user/ResetPasswordDTO.java

@@ -1,30 +1,30 @@
-package nju.seec.helper.dto.user;
-
-import lombok.Data;
-import nju.seec.helper.dto.validator.schoolemail.SchoolEmail;
-import org.hibernate.validator.constraints.Length;
-
-import javax.validation.constraints.NotBlank;
-import java.io.Serializable;
-
-/**
- * @author cst
- */
-@Data
-public class ResetPasswordDTO implements Serializable {
-    private static final long serialVersionUID = 3884011149489695206L;
-    @NotBlank(message = "邮箱不能为空")
-    @SchoolEmail
-    private String username;
-    /**
-     * 验证码
-     */
-    @NotBlank(message = "邮箱验证码不能为空")
-    private String code;
-    /**
-     * 密码
-     */
-    @NotBlank(message = "密码不能为空")
-    @Length(max = 50, message = "密码长度不能超过50位")
-    private String password;
-}
+//package nju.seec.helper.dto.user;
+//
+//import lombok.Data;
+//import nju.seec.helper.dto.validator.schoolemail.SchoolEmail;
+//import org.hibernate.validator.constraints.Length;
+//
+//import javax.validation.constraints.NotBlank;
+//import java.io.Serializable;
+//
+///**
+// * @author cst
+// */
+//@Data
+//public class ResetPasswordDTO implements Serializable {
+//    private static final long serialVersionUID = 3884011149489695206L;
+//    @NotBlank(message = "邮箱不能为空")
+//    @SchoolEmail
+//    private String username;
+//    /**
+//     * 验证码
+//     */
+//    @NotBlank(message = "邮箱验证码不能为空")
+//    private String code;
+//    /**
+//     * 密码
+//     */
+//    @NotBlank(message = "密码不能为空")
+//    @Length(max = 50, message = "密码长度不能超过50位")
+//    private String password;
+//}

+ 39 - 39
src/main/java/nju/seec/helper/dto/user/UserDTO.java

@@ -1,39 +1,39 @@
-package nju.seec.helper.dto.user;
-
-import lombok.Data;
-import nju.seec.helper.dto.groups.Create;
-import nju.seec.helper.dto.validator.phone.Phone;
-import nju.seec.helper.dto.validator.schoolemail.SchoolEmail;
-import nju.seec.helper.enums.UserRole;
-import org.hibernate.validator.constraints.Length;
-
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.NotNull;
-import java.io.Serializable;
-
-/**
- * @author cst
- */
-@Data
-public class UserDTO implements Serializable {
-    private static final long serialVersionUID = -5962331511813309322L;
-    @NotBlank(message = "姓名不能为空")
-    @Length(min = 2, max = 50, message = "姓名长度必须在2到50之间")
-    private String name;
-    @NotBlank(message = "邮箱不能为空", groups = Create.class)
-    @SchoolEmail(groups = Create.class)
-    private String email;
-    @NotBlank(message = "邮箱验证码不能为空", groups = Create.class)
-    private String emailCode;
-    @NotBlank(message = "手机号不能为空", groups = Create.class)
-    @Phone(groups = Create.class)
-    private String phone;
-    @NotBlank(message = "手机号验证码不能为空", groups = Create.class)
-    private String phoneCode;
-    @NotBlank(message = "密码不能为空", groups = Create.class)
-    @Length(max = 50, message = "密码长度不能超过50位", groups = Create.class)
-    private String password;
-
-    @NotNull(message = "缺少用户类型", groups = Create.class)
-    private UserRole role;
-}
+//package nju.seec.helper.dto.user;
+//
+//import lombok.Data;
+//import nju.seec.helper.dto.groups.Create;
+//import nju.seec.helper.dto.validator.phone.Phone;
+//import nju.seec.helper.dto.validator.schoolemail.SchoolEmail;
+//import nju.seec.helper.enums.UserRole;
+//import org.hibernate.validator.constraints.Length;
+//
+//import javax.validation.constraints.NotBlank;
+//import javax.validation.constraints.NotNull;
+//import java.io.Serializable;
+//
+///**
+// * @author cst
+// */
+//@Data
+//public class UserDTO implements Serializable {
+//    private static final long serialVersionUID = -5962331511813309322L;
+//    @NotBlank(message = "姓名不能为空")
+//    @Length(min = 2, max = 50, message = "姓名长度必须在2到50之间")
+//    private String name;
+//    @NotBlank(message = "邮箱不能为空", groups = Create.class)
+//    @SchoolEmail(groups = Create.class)
+//    private String email;
+//    @NotBlank(message = "邮箱验证码不能为空", groups = Create.class)
+//    private String emailCode;
+//    @NotBlank(message = "手机号不能为空", groups = Create.class)
+//    @Phone(groups = Create.class)
+//    private String phone;
+//    @NotBlank(message = "手机号验证码不能为空", groups = Create.class)
+//    private String phoneCode;
+//    @NotBlank(message = "密码不能为空", groups = Create.class)
+//    @Length(max = 50, message = "密码长度不能超过50位", groups = Create.class)
+//    private String password;
+//
+//    @NotNull(message = "缺少用户类型", groups = Create.class)
+//    private UserRole role;
+//}

+ 20 - 20
src/main/java/nju/seec/helper/dto/validator/phone/Phone.java

@@ -1,20 +1,20 @@
-package nju.seec.helper.dto.validator.phone;
-
-import javax.validation.Constraint;
-import javax.validation.Payload;
-import java.lang.annotation.*;
-
-/**
- * @author cst
- */
-@Documented
-@Target({ElementType.PARAMETER, ElementType.FIELD})
-@Retention(RetentionPolicy.RUNTIME)
-@Constraint(validatedBy = PhoneValidator.class)
-public @interface Phone {
-    String message() default "手机号格式错误";
-
-    Class<?>[] groups() default {};
-
-    Class<? extends Payload>[] payload() default {};
-}
+//package nju.seec.helper.dto.validator.phone;
+//
+//import javax.validation.Constraint;
+//import javax.validation.Payload;
+//import java.lang.annotation.*;
+//
+///**
+// * @author cst
+// */
+//@Documented
+//@Target({ElementType.PARAMETER, ElementType.FIELD})
+//@Retention(RetentionPolicy.RUNTIME)
+//@Constraint(validatedBy = PhoneValidator.class)
+//public @interface Phone {
+//    String message() default "手机号格式错误";
+//
+//    Class<?>[] groups() default {};
+//
+//    Class<? extends Payload>[] payload() default {};
+//}

+ 21 - 21
src/main/java/nju/seec/helper/dto/validator/phone/PhoneValidator.java

@@ -1,21 +1,21 @@
-package nju.seec.helper.dto.validator.phone;
-
-import javax.validation.ConstraintValidator;
-import javax.validation.ConstraintValidatorContext;
-
-/**
- * @author cst
- */
-public class PhoneValidator implements ConstraintValidator<Phone, String> {
-    private static final String PHONE_REGEX = "1[3-9]\\d{9}";
-
-    @Override
-    public boolean isValid(String value, ConstraintValidatorContext context) {
-        return value != null && value.matches(PHONE_REGEX);
-    }
-
-    @Override
-    public void initialize(Phone constraintAnnotation) {
-
-    }
-}
+//package nju.seec.helper.dto.validator.phone;
+//
+//import javax.validation.ConstraintValidator;
+//import javax.validation.ConstraintValidatorContext;
+//
+///**
+// * @author cst
+// */
+//public class PhoneValidator implements ConstraintValidator<Phone, String> {
+//    private static final String PHONE_REGEX = "1[3-9]\\d{9}";
+//
+//    @Override
+//    public boolean isValid(String value, ConstraintValidatorContext context) {
+//        return value != null && value.matches(PHONE_REGEX);
+//    }
+//
+//    @Override
+//    public void initialize(Phone constraintAnnotation) {
+//
+//    }
+//}

+ 32 - 32
src/main/java/nju/seec/helper/dto/validator/schoolemail/SchoolEmail.java

@@ -1,32 +1,32 @@
-package nju.seec.helper.dto.validator.schoolemail;
-
-import javax.validation.Constraint;
-import javax.validation.Payload;
-import javax.validation.constraints.Email;
-import javax.validation.constraints.Pattern;
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.*;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-/**
- * @author cst
- * copy from {@link Email}
- */
-@Documented
-@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
-@Retention(RUNTIME)
-@Constraint(validatedBy = SchoolEmailValidator.class)
-public @interface SchoolEmail {
-    String message() default "该邮箱不是校邮";
-
-    Class<?>[] groups() default {};
-
-    Class<? extends Payload>[] payload() default {};
-
-    String regexp() default ".+@.*(edu.cn)";
-
-    Pattern.Flag[] flags() default {};
-}
+//package nju.seec.helper.dto.validator.schoolemail;
+//
+//import javax.validation.Constraint;
+//import javax.validation.Payload;
+//import javax.validation.constraints.Email;
+//import javax.validation.constraints.Pattern;
+//import java.lang.annotation.Documented;
+//import java.lang.annotation.Retention;
+//import java.lang.annotation.Target;
+//
+//import static java.lang.annotation.ElementType.*;
+//import static java.lang.annotation.RetentionPolicy.RUNTIME;
+//
+///**
+// * @author cst
+// * copy from {@link Email}
+// */
+//@Documented
+//@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
+//@Retention(RUNTIME)
+//@Constraint(validatedBy = SchoolEmailValidator.class)
+//public @interface SchoolEmail {
+//    String message() default "该邮箱不是校邮";
+//
+//    Class<?>[] groups() default {};
+//
+//    Class<? extends Payload>[] payload() default {};
+//
+//    String regexp() default ".+@.*(edu.cn)";
+//
+//    Pattern.Flag[] flags() default {};
+//}

+ 54 - 54
src/main/java/nju/seec/helper/dto/validator/schoolemail/SchoolEmailValidator.java

@@ -1,54 +1,54 @@
-package nju.seec.helper.dto.validator.schoolemail;
-
-import org.hibernate.validator.internal.constraintvalidators.AbstractEmailValidator;
-import org.hibernate.validator.internal.util.logging.Log;
-import org.hibernate.validator.internal.util.logging.LoggerFactory;
-
-import javax.validation.ConstraintValidatorContext;
-import java.lang.invoke.MethodHandles;
-import java.util.regex.Matcher;
-import java.util.regex.PatternSyntaxException;
-
-/**
- * @author cst
- * copy from {@link org.hibernate.validator.internal.constraintvalidators.bv.EmailValidator}
- */
-public class SchoolEmailValidator extends AbstractEmailValidator<SchoolEmail> {
-    private static final Log LOG = LoggerFactory.make(MethodHandles.lookup());
-    private java.util.regex.Pattern pattern;
-
-    @Override
-    public void initialize(SchoolEmail emailAnnotation) {
-        super.initialize(emailAnnotation);
-
-        javax.validation.constraints.Pattern.Flag[] flags = emailAnnotation.flags();
-        int intFlag = 0;
-        for (javax.validation.constraints.Pattern.Flag flag : flags) {
-            intFlag = intFlag | flag.getValue();
-        }
-
-        // we only apply the regexp if there is one to apply
-        if (!".*".equals(emailAnnotation.regexp()) || emailAnnotation.flags().length > 0) {
-            try {
-                pattern = java.util.regex.Pattern.compile(emailAnnotation.regexp(), intFlag);
-            } catch (PatternSyntaxException e) {
-                throw LOG.getInvalidRegularExpressionException(e);
-            }
-        }
-    }
-
-    @Override
-    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
-        if (value == null) {
-            return true;
-        }
-
-        boolean isValid = super.isValid(value, context);
-        if (pattern == null || !isValid) {
-            return isValid;
-        }
-
-        Matcher m = pattern.matcher(value);
-        return m.matches();
-    }
-}
+//package nju.seec.helper.dto.validator.schoolemail;
+//
+//import org.hibernate.validator.internal.constraintvalidators.AbstractEmailValidator;
+//import org.hibernate.validator.internal.util.logging.Log;
+//import org.hibernate.validator.internal.util.logging.LoggerFactory;
+//
+//import javax.validation.ConstraintValidatorContext;
+//import java.lang.invoke.MethodHandles;
+//import java.util.regex.Matcher;
+//import java.util.regex.PatternSyntaxException;
+//
+///**
+// * @author cst
+// * copy from {@link org.hibernate.validator.internal.constraintvalidators.bv.EmailValidator}
+// */
+//public class SchoolEmailValidator extends AbstractEmailValidator<SchoolEmail> {
+//    private static final Log LOG = LoggerFactory.make(MethodHandles.lookup());
+//    private java.util.regex.Pattern pattern;
+//
+//    @Override
+//    public void initialize(SchoolEmail emailAnnotation) {
+//        super.initialize(emailAnnotation);
+//
+//        javax.validation.constraints.Pattern.Flag[] flags = emailAnnotation.flags();
+//        int intFlag = 0;
+//        for (javax.validation.constraints.Pattern.Flag flag : flags) {
+//            intFlag = intFlag | flag.getValue();
+//        }
+//
+//        // we only apply the regexp if there is one to apply
+//        if (!".*".equals(emailAnnotation.regexp()) || emailAnnotation.flags().length > 0) {
+//            try {
+//                pattern = java.util.regex.Pattern.compile(emailAnnotation.regexp(), intFlag);
+//            } catch (PatternSyntaxException e) {
+//                throw LOG.getInvalidRegularExpressionException(e);
+//            }
+//        }
+//    }
+//
+//    @Override
+//    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
+//        if (value == null) {
+//            return true;
+//        }
+//
+//        boolean isValid = super.isValid(value, context);
+//        if (pattern == null || !isValid) {
+//            return isValid;
+//        }
+//
+//        Matcher m = pattern.matcher(value);
+//        return m.matches();
+//    }
+//}

+ 0 - 23
src/main/java/nju/seec/helper/entity/Admin.java

@@ -1,23 +0,0 @@
-package nju.seec.helper.entity;
-
-import lombok.Data;
-
-import javax.persistence.*;
-
-/**
- * @author cst
- */
-@Data
-@Entity
-@Table(name = "admin", uniqueConstraints = @UniqueConstraint(name = "username_unique", columnNames = "username"))
-public class Admin {
-    @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private Long id;
-
-    @Column(nullable = false)
-    private String username;
-
-    @Column(nullable = false)
-    private String password;
-}

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

@@ -21,8 +21,8 @@ import java.time.LocalDateTime;
 @DynamicUpdate
 @DynamicInsert
 @Where(clause = "delete_at = 0")
-@Cacheable
-@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
+//@Cacheable
+//@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 public class Course {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)

+ 6 - 3
src/main/java/nju/seec/helper/entity/CourseFile.java

@@ -2,8 +2,9 @@ package nju.seec.helper.entity;
 
 import lombok.Data;
 import lombok.experimental.Accessors;
-import org.hibernate.annotations.CacheConcurrencyStrategy;
 import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
 
 import javax.persistence.*;
 import java.time.LocalDateTime;
@@ -15,8 +16,10 @@ import java.time.LocalDateTime;
 @Accessors(chain = true)
 @Entity
 @Table(name = "course_file", uniqueConstraints = {@UniqueConstraint(name = "course_file_name_unique", columnNames = {"course_id", "file_name"})})
-@Cacheable
-@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
+@DynamicUpdate
+@DynamicInsert
+//@Cacheable
+//@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 public class CourseFile {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)

+ 6 - 3
src/main/java/nju/seec/helper/entity/Notice.java

@@ -2,8 +2,9 @@ package nju.seec.helper.entity;
 
 import lombok.Data;
 import lombok.experimental.Accessors;
-import org.hibernate.annotations.CacheConcurrencyStrategy;
 import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
 import org.hibernate.annotations.UpdateTimestamp;
 
 import javax.persistence.*;
@@ -16,8 +17,10 @@ import java.time.LocalDateTime;
 @Accessors(chain = true)
 @Entity
 @Table(name = "notice")
-@Cacheable
-@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
+@DynamicUpdate
+@DynamicInsert
+//@Cacheable
+//@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 public class Notice {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)

+ 4 - 0
src/main/java/nju/seec/helper/entity/QuestionRecord.java

@@ -2,6 +2,8 @@ package nju.seec.helper.entity;
 
 import lombok.Data;
 import lombok.experimental.Accessors;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
 
 import javax.persistence.*;
 
@@ -12,6 +14,8 @@ import javax.persistence.*;
 @Accessors(chain = true)
 @Entity
 @Table(name = "question_record", uniqueConstraints = @UniqueConstraint(name = "teacher_question_unique", columnNames = {"teacher_id", "question_id"}))
+@DynamicUpdate
+@DynamicInsert
 public class QuestionRecord {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)

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

@@ -26,8 +26,8 @@ import java.util.List;
 @DynamicInsert
 @DynamicUpdate
 @Where(clause = "delete_at = 0 and slide_id in (select s.id from slide s where s.delete_at = 0) and course_id in (select c.id from course c where c.delete_at = 0)")
-@Cacheable
-@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
+//@Cacheable
+//@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 public class Quiz {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)

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

@@ -22,8 +22,8 @@ import java.time.LocalDateTime;
 @DynamicUpdate
 @DynamicInsert
 @Where(clause = "delete_at = 0 and course_id in (select c.id from course c where c.delete_at = 0)")
-@Cacheable
-@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
+//@Cacheable
+//@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 public class Slide {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)

+ 8 - 12
src/main/java/nju/seec/helper/entity/User.java

@@ -3,7 +3,6 @@ package nju.seec.helper.entity;
 import lombok.Data;
 import lombok.experimental.Accessors;
 import nju.seec.helper.enums.UserRole;
-import org.hibernate.annotations.CacheConcurrencyStrategy;
 import org.hibernate.annotations.CreationTimestamp;
 import org.hibernate.annotations.DynamicInsert;
 import org.hibernate.annotations.DynamicUpdate;
@@ -17,12 +16,15 @@ import java.time.LocalDateTime;
 @Data
 @Accessors(chain = true)
 @Entity
-@Table(name = "user", uniqueConstraints = {@UniqueConstraint(name = "user_phone_unique", columnNames = "phone"),
-        @UniqueConstraint(name = "user_email_unique", columnNames = "email")})
+@Table(name = "user", uniqueConstraints = {
+        @UniqueConstraint(name = "user_phone_unique", columnNames = "phone")
+//        , @UniqueConstraint(name = "user_email_unique", columnNames = "email")
+}
+)
 @DynamicUpdate
 @DynamicInsert
-@Cacheable
-@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
+//@Cacheable
+//@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 public class User {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -35,23 +37,17 @@ public class User {
     private String email;
 
     @Enumerated(EnumType.STRING)
-    @Column(nullable = false, updatable = false)
+    @Column(nullable = false)
     private UserRole role;
 
     @Column(nullable = false)
     private String phone;
 
-    @Column(nullable = false)
     private String password;
 
     @Column(name = "create_at", updatable = false, nullable = false)
     @CreationTimestamp
     private LocalDateTime createAt;
 
-//    @ColumnDefault("'NORMAL'")
-//    @Enumerated(EnumType.STRING)
-//    @Column(nullable = false)
-//    private UserState state;
-
     private Long pid;
 }

+ 1 - 1
src/main/java/nju/seec/helper/enums/ExceptionType.java

@@ -12,7 +12,7 @@ public enum ExceptionType {
     // http code
     PARAM_ERROR(HttpStatus.BAD_REQUEST), UNAUTHORIZED(HttpStatus.UNAUTHORIZED), FORBIDDEN(HttpStatus.FORBIDDEN), NOT_FOUND(HttpStatus.NOT_FOUND), CONFLICT(HttpStatus.CONFLICT), ERROR(HttpStatus.INTERNAL_SERVER_ERROR);
 
-    private HttpStatus status;
+    private final HttpStatus status;
 
     ExceptionType(HttpStatus status) {
         this.status = status;

+ 0 - 39
src/main/java/nju/seec/helper/enums/QuestionType.java

@@ -1,39 +0,0 @@
-package nju.seec.helper.enums;
-
-import com.fasterxml.jackson.annotation.JsonValue;
-
-import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * @author cst
- */
-public enum QuestionType {
-    /**
-     * 单选
-     */
-    CHOICE("choice"),
-    /**
-     * 判断
-     */
-    TRUE_FALSE("true_false");
-
-    private String value;
-
-    private final static Map<String, QuestionType> VALUE_TYPE_MAP =
-            Stream.of(QuestionType.values()).collect(Collectors.toMap(QuestionType::getValue, questionType -> questionType));
-
-    QuestionType(String value) {
-        this.value = value;
-    }
-
-    @JsonValue
-    public String getValue() {
-        return value;
-    }
-
-    public static QuestionType getQuestionType(String value) {
-        return VALUE_TYPE_MAP.get(value);
-    }
-}

+ 16 - 10
src/main/java/nju/seec/helper/enums/QuizState.java

@@ -1,23 +1,29 @@
 package nju.seec.helper.enums;
 
+import lombok.Getter;
+
 /**
  * @author xst
  * <p>
  * updated by cst
  */
+@Getter
 public enum QuizState {
-    NOT_STARTED("未开始"), ONGOING("正在进行"), CLOSED("已结束");
-    private String name;
+    /**
+     * 未开始
+     */
+    NOT_STARTED("未开始"),
+    /**
+     * 正在进行
+     */
+    ONGOING("正在进行"),
+    /**
+     * 已结束
+     */
+    CLOSED("已结束");
+    private final String name;
 
     QuizState(String name) {
         this.name = name;
     }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
 }

+ 24 - 6
src/main/java/nju/seec/helper/enums/SlideState.java

@@ -1,17 +1,35 @@
 package nju.seec.helper.enums;
 
+import lombok.Getter;
+
 /**
  * @author cst
  */
+@Getter
 public enum SlideState {
-    DRAFT(1), BEFORE_CLASS(2), IN_CLASS(3), AFTER_CLASS(4), FINISH(5);
-    private Integer num;
+    /**
+     * 草稿
+     */
+    DRAFT(1),
+    /**
+     * 课前
+     */
+    BEFORE_CLASS(2),
+    /**
+     * 课中
+     */
+    IN_CLASS(3),
+    /**
+     * 课后
+     */
+    AFTER_CLASS(4),
+    /**
+     * 结束
+     */
+    FINISH(5);
+    private final Integer num;
 
     SlideState(Integer num) {
         this.num = num;
     }
-
-    public Integer getNum() {
-        return num;
-    }
 }

+ 2 - 2
src/main/java/nju/seec/helper/service/EventService.java

@@ -1,6 +1,6 @@
 package nju.seec.helper.service;
 
-import nju.seec.helper.vo.EventVO;
+import nju.seec.helper.api.dataanalysis.model.Event;
 
 import java.util.List;
 
@@ -13,5 +13,5 @@ public interface EventService {
      *
      * @return
      */
-    List<EventVO> getAllEvents();
+    List<Event> getAllEvents();
 }

+ 46 - 3
src/main/java/nju/seec/helper/service/QuestionService.java

@@ -1,5 +1,8 @@
 package nju.seec.helper.service;
 
+import nju.seec.helper.api.bok.dto.CollectDTO;
+import nju.seec.helper.api.bok.dto.RateDTO;
+import nju.seec.helper.api.bok.vo.InteractiveVO;
 import nju.seec.helper.aspect.auth.LoginUser;
 import nju.seec.helper.dto.question.BaseQuestionDTO;
 import nju.seec.helper.entity.Quiz;
@@ -21,10 +24,11 @@ public interface QuestionService {
      *
      * @param user
      * @param stem
-     * @param pageable
+     * @param tag
+     * @param knowledgeId * @param pageable
      * @return
      */
-    Page<BaseQuestionVO> getQuestions(LoginUser user, String stem, Pageable pageable);
+    Page<BaseQuestionVO> getQuestions(LoginUser user, String stem, String tag, String knowledgeId, Pageable pageable);
 
     /**
      * 根据测试获取题目
@@ -101,6 +105,45 @@ public interface QuestionService {
      *
      * @param user
      * @param questionId
+     * @param collectDTO
+     */
+    void collectQuestion(LoginUser user, String questionId, CollectDTO collectDTO);
+
+    /**
+     * 评价题目
+     *
+     * @param user
+     * @param questionId
+     * @param rateDTO
+     */
+    void rateQuestion(LoginUser user, String questionId, RateDTO rateDTO);
+
+    /**
+     * 取得收藏评价的题目
+     *
+     * @param user
+     * @param collect
+     * @param rate
+     * @param pageable
+     * @return
+     */
+    Page<BaseQuestionVO> getCollectedAndRatedQuestions(LoginUser user, Boolean collect, Boolean rate, Pageable pageable);
+
+    /**
+     * 取得完成的题目
+     *
+     * @param user
+     * @param pageable
+     * @return
+     */
+    Page<BaseQuestionVO> getCompletedQuestions(LoginUser user, Pageable pageable);
+
+    /**
+     * 取得题目交互信息
+     *
+     * @param user
+     * @param questionId
+     * @return
      */
-    void starQuestion(LoginUser user, String questionId);
+    List<InteractiveVO> getInteractiveList(LoginUser user, String questionId);
 }

+ 25 - 0
src/main/java/nju/seec/helper/service/RecordService.java

@@ -0,0 +1,25 @@
+package nju.seec.helper.service;
+
+import nju.seec.helper.api.dataanalysis.model.ExerciseRecord;
+
+import java.util.List;
+
+/**
+ * @author cst
+ */
+public interface RecordService {
+    /**
+     * 取得所有学生的做题记录
+     *
+     * @return
+     */
+    List<ExerciseRecord> getAllRecords();
+
+    /**
+     * 取得某个学生的做题记录
+     *
+     * @param userId
+     * @return
+     */
+    List<ExerciseRecord> getRecordsByUserId(String userId);
+}

+ 2 - 6
src/main/java/nju/seec/helper/service/impl/CommentServiceImpl.java

@@ -1,6 +1,7 @@
 package nju.seec.helper.service.impl;
 
 import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.aspect.message.Message;
 import nju.seec.helper.dao.CommentDAO;
 import nju.seec.helper.dao.SlideDAO;
 import nju.seec.helper.dao.UserDAO;
@@ -18,7 +19,6 @@ import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.Collections;
 import java.util.stream.Collectors;
 
 /**
@@ -39,6 +39,7 @@ public class CommentServiceImpl implements CommentService {
         this.messageService = messageService;
     }
 
+    @Message(MessageType.COMMENT_NEW)
     @Transactional(rollbackFor = Exception.class)
     @Override
     public CommentVO createComment(LoginUser user, CommentDTO commentDTO) {
@@ -53,11 +54,6 @@ public class CommentServiceImpl implements CommentService {
                         .setContent(commentDTO.getContent())
         );
 
-        // 增加通知
-        if (!user.getId().equals(slide.getTeacher().getId())) {
-            messageService.createMessage(Collections.singleton(slide.getTeacher().getId()), MessageType.COMMENT_NEW, String.format("[%s] - [%s] 在第%d页有新评论", slide.getCourse().getName(), slide.getName(), comment.getPageNumber()), slide.getId());
-        }
-
         return new CommentVO(comment);
     }
 

+ 5 - 7
src/main/java/nju/seec/helper/service/impl/CourseFileServiceImpl.java

@@ -1,5 +1,6 @@
 package nju.seec.helper.service.impl;
 
+import cn.hutool.core.util.StrUtil;
 import lombok.SneakyThrows;
 import nju.seec.helper.aspect.auth.LoginUser;
 import nju.seec.helper.dao.CourseDAO;
@@ -11,8 +12,7 @@ import nju.seec.helper.enums.ExceptionType;
 import nju.seec.helper.exception.HelperException;
 import nju.seec.helper.service.CourseFileService;
 import nju.seec.helper.service.CourseService;
-import nju.seec.helper.util.oss.OssObjectUrlUtils;
-import nju.seec.helper.util.oss.OssUtils;
+import nju.seec.helper.util.OssUtils;
 import nju.seec.helper.vo.CourseFileVO;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -35,14 +35,12 @@ public class CourseFileServiceImpl implements CourseFileService {
     private final CourseService courseService;
 
     private final OssUtils ossUtils;
-    private final OssObjectUrlUtils ossObjectUrlUtils;
 
-    public CourseFileServiceImpl(CourseDAO courseDAO, CourseFileDAO courseFileDAO, CourseService courseService, OssUtils ossUtils, OssObjectUrlUtils ossObjectUrlUtils) {
+    public CourseFileServiceImpl(CourseDAO courseDAO, CourseFileDAO courseFileDAO, CourseService courseService, OssUtils ossUtils) {
         this.courseDAO = courseDAO;
         this.courseFileDAO = courseFileDAO;
         this.courseService = courseService;
         this.ossUtils = ossUtils;
-        this.ossObjectUrlUtils = ossObjectUrlUtils;
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -106,11 +104,11 @@ public class CourseFileServiceImpl implements CourseFileService {
             throw HelperException.of(ExceptionType.FORBIDDEN, "您无权获取该附件的链接");
         }
 
-        return ossObjectUrlUtils.getUrl(courseFile.getObjectName());
+        return ossUtils.getUrl(courseFile.getObjectName());
     }
 
     private String getCourseFileObjectName(Long courseId, String fileName) {
-        return COURSE_FILE_STORE_DIR + File.separator + courseId + File.separator + fileName;
+        return StrUtil.join(File.separator, COURSE_FILE_STORE_DIR, courseId, fileName);
     }
 
     @Override

+ 14 - 30
src/main/java/nju/seec/helper/service/impl/EventServiceImpl.java

@@ -1,18 +1,18 @@
 package nju.seec.helper.service.impl;
 
-import cn.hutool.core.text.StrBuilder;
 import com.google.common.collect.Lists;
-import nju.seec.helper.api.dataanalysis.EventAction;
-import nju.seec.helper.api.dataanalysis.EventType;
+import nju.seec.helper.api.dataanalysis.EventApi;
+import nju.seec.helper.api.dataanalysis.model.Event;
 import nju.seec.helper.dao.*;
 import nju.seec.helper.entity.*;
 import nju.seec.helper.enums.UserRole;
 import nju.seec.helper.service.EventService;
-import nju.seec.helper.vo.EventVO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * @author cst
@@ -25,18 +25,21 @@ public class EventServiceImpl implements EventService {
     private final CommentDAO commentDAO;
     private final QuizStudentAnswerDAO quizStudentAnswerDAO;
 
-    public EventServiceImpl(CourseDAO courseDAO, ChooseDAO chooseDAO, UserDAO userDAO, CommentDAO commentDAO, QuizStudentAnswerDAO quizStudentAnswerDAO) {
+    private final EventApi eventApi;
+
+    public EventServiceImpl(CourseDAO courseDAO, ChooseDAO chooseDAO, UserDAO userDAO, CommentDAO commentDAO, QuizStudentAnswerDAO quizStudentAnswerDAO, EventApi eventApi) {
         this.courseDAO = courseDAO;
         this.chooseDAO = chooseDAO;
         this.userDAO = userDAO;
         this.commentDAO = commentDAO;
         this.quizStudentAnswerDAO = quizStudentAnswerDAO;
+        this.eventApi = eventApi;
     }
 
     @Transactional(readOnly = true)
     @Override
-    public List<EventVO> getAllEvents() {
-        List<EventVO> eventVOList = Lists.newArrayList();
+    public List<Event> getAllEvents() {
+        List<Event> events = Lists.newArrayList();
         List<Course> courses = courseDAO.findAll();
         courses.forEach(course -> {
             List<Choose> chooses = chooseDAO.findByCourseId(course.getId());
@@ -44,13 +47,7 @@ public class EventServiceImpl implements EventService {
                     choose -> {
                         User student = userDAO.findUserById(choose.getStudentId());
                         if (student.getPid() != null) {
-                            eventVOList.add(new EventVO()
-                                    .setUserId(String.valueOf(student.getPid()))
-                                    .setTime(choose.getChooseAt())
-                                    .setType(EventType.LESSON)
-                                    .setAction(EventAction.PARTICIPATE)
-                                    .setDescription(course.getName())
-                            );
+                            events.add(eventApi.createParticipateClassEventDTO(String.valueOf(Objects.requireNonNull(student).getPid()), LocalDateTime.now(), course));
                         }
                     }
             );
@@ -59,13 +56,7 @@ public class EventServiceImpl implements EventService {
         comments.forEach(comment -> {
             User user = comment.getUser();
             if (user.getPid() != null && user.getRole() == UserRole.STUDENT) {
-                eventVOList.add(new EventVO()
-                        .setUserId(String.valueOf(user.getPid()))
-                        .setTime(comment.getCreateAt())
-                        .setType(EventType.POST)
-                        .setAction(EventAction.INITIATE)
-                        .setDescription(comment.getTitle())
-                );
+                events.add(eventApi.createInitPostEventDTO(String.valueOf(user.getPid()), comment.getCreateAt(), comment.getTitle()));
             }
         });
         List<QuizStudentAnswer> quizStudentAnswers = quizStudentAnswerDAO.findAll();
@@ -73,16 +64,9 @@ public class EventServiceImpl implements EventService {
             User student = quizStudentAnswer.getStudent();
             if (student.getPid() != null) {
                 Quiz quiz = quizStudentAnswer.getQuiz();
-                Course course = quiz.getCourse();
-                eventVOList.add(new EventVO()
-                        .setUserId(String.valueOf(student.getPid()))
-                        .setTime(quizStudentAnswer.getSubmitAt())
-                        .setType(EventType.QUIZ)
-                        .setAction(EventAction.COMPLETE)
-                        .setDescription(StrBuilder.create().append(course.getName()).append(";").append(quiz.getName()).toStringAndReset())
-                );
+                events.add(eventApi.createCompleteQuizEventDTO(String.valueOf(student.getPid()), quizStudentAnswer.getSubmitAt(), quiz));
             }
         });
-        return eventVOList;
+        return events;
     }
 }

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

@@ -1,16 +1,14 @@
 package nju.seec.helper.service.impl;
 
 import nju.seec.helper.aspect.auth.LoginUser;
-import nju.seec.helper.dao.ChooseDAO;
+import nju.seec.helper.aspect.message.Message;
 import nju.seec.helper.dao.CourseDAO;
 import nju.seec.helper.dao.NoticeDAO;
 import nju.seec.helper.dto.notice.NoticeDTO;
-import nju.seec.helper.entity.Course;
 import nju.seec.helper.entity.Notice;
 import nju.seec.helper.enums.ExceptionType;
 import nju.seec.helper.enums.MessageType;
 import nju.seec.helper.exception.HelperException;
-import nju.seec.helper.service.MessageService;
 import nju.seec.helper.service.NoticeService;
 import nju.seec.helper.vo.NoticeVO;
 import org.springframework.data.domain.Page;
@@ -18,31 +16,23 @@ import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.Set;
-
 /**
  * @author cst
  */
 @Service
 public class NoticeServiceImpl implements NoticeService {
     private final CourseDAO courseDAO;
-    private final ChooseDAO chooseDAO;
     private final NoticeDAO noticeDAO;
 
-    private final MessageService messageService;
-
-    public NoticeServiceImpl(CourseDAO courseDAO, ChooseDAO chooseDAO, NoticeDAO noticeDAO, MessageService messageService) {
+    public NoticeServiceImpl(CourseDAO courseDAO, NoticeDAO noticeDAO) {
         this.courseDAO = courseDAO;
-        this.chooseDAO = chooseDAO;
         this.noticeDAO = noticeDAO;
-        this.messageService = messageService;
     }
 
+    @Message(MessageType.NOTICE_NEW)
     @Transactional(rollbackFor = Exception.class)
     @Override
     public NoticeVO createNotice(LoginUser user, NoticeDTO noticeDTO) {
-        Course course = courseDAO.findCourseById(noticeDTO.getCourseId());
-
         Notice notice = new Notice()
                 .setCourseId(noticeDTO.getCourseId())
                 .setTeacherId(user.getId())
@@ -53,8 +43,6 @@ public class NoticeServiceImpl implements NoticeService {
             throw HelperException.of(ExceptionType.FORBIDDEN, "您无权创建该公告");
         }
 
-        Set<Long> studentIds = chooseDAO.findStudentIdsByCourseId(noticeDTO.getCourseId());
-        messageService.createMessage(studentIds, MessageType.NOTICE_NEW, String.format("[%s] 发布了新公告", course.getName()), course.getId());
         return new NoticeVO(noticeDAO.save(notice));
     }
 

+ 71 - 15
src/main/java/nju/seec/helper/service/impl/QuestionServiceImpl.java

@@ -2,7 +2,16 @@ package nju.seec.helper.service.impl;
 
 import cn.hutool.core.util.IdUtil;
 import nju.seec.helper.api.bok.QuestionApi;
+import nju.seec.helper.api.bok.QuestionQueryApi;
+import nju.seec.helper.api.bok.RecordApi;
+import nju.seec.helper.api.bok.UserInteractiveApi;
+import nju.seec.helper.api.bok.dto.CollectDTO;
+import nju.seec.helper.api.bok.dto.RateDTO;
+import nju.seec.helper.api.bok.vo.InteractiveVO;
+import nju.seec.helper.api.bok.vo.QuestionVO;
+import nju.seec.helper.api.bok.vo.RecordQuestionVO;
 import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.dao.ChooseDAO;
 import nju.seec.helper.dao.QuestionRecordDAO;
 import nju.seec.helper.dao.QuizDAO;
 import nju.seec.helper.dao.UserDAO;
@@ -15,7 +24,6 @@ import nju.seec.helper.enums.UserRole;
 import nju.seec.helper.exception.HelperException;
 import nju.seec.helper.service.QuestionService;
 import nju.seec.helper.vo.question.BaseQuestionVO;
-import nju.seec.helper.vo.question.BokQuestionVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
@@ -23,6 +31,7 @@ import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -37,22 +46,36 @@ public class QuestionServiceImpl implements QuestionService {
     private final QuizDAO quizDAO;
     private final UserDAO userDAO;
     private final QuestionRecordDAO questionRecordDAO;
+    private final ChooseDAO chooseDAO;
 
     private final QuestionApi questionApi;
+    private final QuestionQueryApi questionQueryApi;
+    private final UserInteractiveApi userInteractiveApi;
+    private final RecordApi recordApi;
 
     @Autowired
     public QuestionServiceImpl(QuizDAO quizDAO
             , UserDAO userDAO
-            , QuestionRecordDAO questionRecordDAO, QuestionApi questionApi) {
+            , QuestionRecordDAO questionRecordDAO
+            , ChooseDAO chooseDAO
+            , QuestionApi questionApi
+            , QuestionQueryApi questionQueryApi
+            , UserInteractiveApi userInteractiveApi
+            , RecordApi recordApi
+    ) {
         this.quizDAO = quizDAO;
         this.userDAO = userDAO;
         this.questionRecordDAO = questionRecordDAO;
+        this.chooseDAO = chooseDAO;
         this.questionApi = questionApi;
+        this.questionQueryApi = questionQueryApi;
+        this.userInteractiveApi = userInteractiveApi;
+        this.recordApi = recordApi;
     }
 
     @Override
-    public Page<BaseQuestionVO> getQuestions(LoginUser user, String stem, Pageable pageable) {
-        return questionApi.bokFindByStemLike(stem, pageable).map(bokQuestion -> BaseQuestionVO.convertBokQuestionToVO(bokQuestion, true));
+    public Page<BaseQuestionVO> getQuestions(LoginUser user, String stem, String tag, String knowledgeId, Pageable pageable) {
+        return questionApi.getQuestionList(stem, tag, knowledgeId, pageable).map(bokQuestion -> BaseQuestionVO.convertBokQuestionToVO(bokQuestion, true));
     }
 
     @Transactional(readOnly = true)
@@ -60,7 +83,7 @@ public class QuestionServiceImpl implements QuestionService {
     public List<BaseQuestionVO> getQuestionsByQuiz(LoginUser user, Long quizId) {
         Quiz quiz = quizDAO.findQuizById(quizId);
         List<String> questionIds = quiz.getQuestions();
-        return questionApi.bokFindByIdIn(questionIds)
+        return questionQueryApi.getQuestionListById(questionIds)
                 .stream()
                 .map(bokQuestion -> BaseQuestionVO.convertBokQuestionToVO(bokQuestion, quiz.getState() == QuizState.CLOSED || user.getRole() == UserRole.TEACHER))
                 .collect(Collectors.toList());
@@ -70,7 +93,7 @@ public class QuestionServiceImpl implements QuestionService {
     @Override
     public List<BaseQuestionVO> getQuestionsByQuiz(Quiz quiz) {
         List<String> questionIds = quiz.getQuestions();
-        return questionApi.bokFindByIdIn(questionIds)
+        return questionQueryApi.getQuestionListById(questionIds)
                 .stream()
                 .map(bokQuestion -> BaseQuestionVO.convertBokQuestionToVO(bokQuestion, true))
                 .collect(Collectors.toList());
@@ -78,7 +101,7 @@ public class QuestionServiceImpl implements QuestionService {
 
     @Override
     public BaseQuestionVO getOneQuestion(LoginUser user, String questionId) {
-        return BaseQuestionVO.convertBokQuestionToVO(questionApi.bokFindById(questionId), true);
+        return BaseQuestionVO.convertBokQuestionToVO(questionApi.getQuestionsById(questionId), true);
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -90,11 +113,11 @@ public class QuestionServiceImpl implements QuestionService {
             questionId = IdUtil.fastSimpleUUID();
         } while (questionRecordDAO.existsByQuestionId(questionId));
 
-        BokQuestionVO bokQuestionVO = questionApi.createQuestion(questionId, baseQuestionDTO);
+        QuestionVO questionVO = questionApi.createQuestion(questionId, baseQuestionDTO);
         questionRecordDAO.save(new QuestionRecord()
                 .setTeacher(userDAO.findUserById(user.getId()))
-                .setQuestionId(bokQuestionVO.getId()));
-        return BaseQuestionVO.convertBokQuestionToVO(bokQuestionVO, true);
+                .setQuestionId(questionVO.getId()));
+        return BaseQuestionVO.convertBokQuestionToVO(questionVO, true);
     }
 
     @Transactional(readOnly = true)
@@ -102,9 +125,9 @@ public class QuestionServiceImpl implements QuestionService {
     public BaseQuestionVO modifyQuestion(LoginUser user, String questionId, BaseQuestionDTO baseQuestionDTO) {
         QuestionRecord questionRecord = questionRecordDAO.findByTeacherAndQuestionId(userDAO.findUserById(user.getId()), questionId)
                 .orElseThrow(() -> HelperException.of(ExceptionType.FORBIDDEN, "您无权修改该问题"));
-        BokQuestionVO bokQuestionVO = questionApi.modifyQuestion(questionId, baseQuestionDTO);
+        QuestionVO questionVO = questionApi.updateQuestion(questionId, baseQuestionDTO);
         questionRecordDAO.save(questionRecord);
-        return BaseQuestionVO.convertBokQuestionToVO(bokQuestionVO, true);
+        return BaseQuestionVO.convertBokQuestionToVO(questionVO, true);
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -122,7 +145,7 @@ public class QuestionServiceImpl implements QuestionService {
         Page<String> questionIdPage = questionRecordDAO.findQuestionIdsByTeacher(userDAO.findUserById(user.getId()), pageable);
 
         return new PageImpl<>(
-                questionApi.bokFindByIdIn(questionIdPage.getContent())
+                questionQueryApi.getQuestionListById(questionIdPage.getContent())
                         .stream()
                         .map(bokQuestion -> BaseQuestionVO.convertBokQuestionToVO(bokQuestion, true))
                         .collect(Collectors.toList())
@@ -138,7 +161,40 @@ public class QuestionServiceImpl implements QuestionService {
     }
 
     @Override
-    public void starQuestion(LoginUser user, String questionId) {
-        questionApi.starQuestion(String.valueOf(user.getId()), questionId);
+    public void collectQuestion(LoginUser user, String questionId, CollectDTO collectDTO) {
+        userInteractiveApi.collectQuestion(String.valueOf(user.getPid()), questionId, collectDTO);
+    }
+
+    @Override
+    public void rateQuestion(LoginUser user, String questionId, RateDTO rateDTO) {
+        userInteractiveApi.rateQuestion(String.valueOf(user.getPid()), questionId, rateDTO);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public Page<BaseQuestionVO> getCollectedAndRatedQuestions(LoginUser user, Boolean collect, Boolean rate, Pageable pageable) {
+        return convert(user, userInteractiveApi.getCollectAndRateQuestions(String.valueOf(user.getPid()), collect, rate, pageable));
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public Page<BaseQuestionVO> getCompletedQuestions(LoginUser user, Pageable pageable) {
+        RecordQuestionVO recordQuestionVO = recordApi.getUserRecordQuestions(String.valueOf(user.getPid()), pageable);
+        return convert(user, new PageImpl<>(recordQuestionVO.getQuestions(), pageable, recordQuestionVO.getPage().getTotalElements()));
+    }
+
+    @Override
+    public List<InteractiveVO> getInteractiveList(LoginUser user, String questionId) {
+        return userInteractiveApi.getInteractive(String.valueOf(user.getPid()), questionId);
+    }
+
+    private static final Set<QuizState> HIDE_QUESTION_ANSWER_STATES = Collections.singleton(QuizState.ONGOING);
+
+    private Page<BaseQuestionVO> convert(LoginUser user, Page<QuestionVO> questionVOList) {
+        List<String> hideQuestionIds = quizDAO.findByCourseIdInAndStateIn(chooseDAO.findCourseIdsByStudentId(user.getId()), HIDE_QUESTION_ANSWER_STATES)
+                .stream()
+                .flatMap(quiz -> quiz.getQuestions().stream())
+                .collect(Collectors.toList());
+        return questionVOList.map(questionVO -> BaseQuestionVO.convertBokQuestionToVO(questionVO, !hideQuestionIds.contains(questionVO.getId())));
     }
 }

+ 10 - 25
src/main/java/nju/seec/helper/service/impl/QuizServiceImpl.java

@@ -3,14 +3,13 @@ package nju.seec.helper.service.impl;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.aspect.message.Message;
 import nju.seec.helper.dao.*;
 import nju.seec.helper.dto.quiz.QuizDTO;
-import nju.seec.helper.entity.Course;
 import nju.seec.helper.entity.Quiz;
 import nju.seec.helper.entity.Slide;
 import nju.seec.helper.enums.*;
 import nju.seec.helper.exception.HelperException;
-import nju.seec.helper.service.MessageService;
 import nju.seec.helper.service.QuizService;
 import nju.seec.helper.vo.quiz.QuizVO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -38,19 +37,16 @@ public class QuizServiceImpl implements QuizService {
     private final ChooseDAO chooseDAO;
     private final UserDAO userDAO;
 
-    private final MessageService messageService;
-
     @Autowired
-    public QuizServiceImpl(SlideDAO slideDAO, QuizDAO quizDAO, QuizStudentAnswerDAO quizStudentAnswerDAO, ChooseDAO chooseDAO, UserDAO userDAO, MessageService messageService) {
+    public QuizServiceImpl(SlideDAO slideDAO, QuizDAO quizDAO, QuizStudentAnswerDAO quizStudentAnswerDAO, ChooseDAO chooseDAO, UserDAO userDAO) {
         this.slideDAO = slideDAO;
         this.quizDAO = quizDAO;
         this.quizStudentAnswerDAO = quizStudentAnswerDAO;
         this.chooseDAO = chooseDAO;
         this.userDAO = userDAO;
-        this.messageService = messageService;
     }
 
-    private static Map<SlideState, Map<QuizType, QuizState>> MAP = ImmutableMap.of(
+    private static final Map<SlideState, Map<QuizType, QuizState>> MAP = ImmutableMap.of(
             SlideState.DRAFT, ImmutableMap.of(
                     QuizType.BEFORE_CLASS, QuizState.NOT_STARTED,
                     QuizType.IN_CLASS, QuizState.NOT_STARTED,
@@ -78,6 +74,7 @@ public class QuizServiceImpl implements QuizService {
             )
     );
 
+    @Message(MessageType.QUIZ_NEW)
     @Transactional(rollbackFor = Exception.class)
     @Override
     public QuizVO createQuiz(LoginUser user, QuizDTO quizDTO) {
@@ -99,13 +96,13 @@ public class QuizServiceImpl implements QuizService {
 
         try {
             quiz = quizDAO.save(quiz);
-            sendMessages(quiz);
             return new QuizVO(quiz);
         } catch (DataIntegrityViolationException e) {
             throw HelperException.of(ExceptionType.CONFLICT, "创建失败,请检查测试信息是否正确且不与已有测试冲突");
         }
     }
 
+    @Message(MessageType.QUIZ_NEW)
     @Transactional(rollbackFor = Exception.class)
     @Override
     public QuizVO modifyQuiz(LoginUser user, Long quizId, QuizDTO quizDTO) {
@@ -126,7 +123,6 @@ public class QuizServiceImpl implements QuizService {
                     .setName(quizDTO.getName())
                     .setMaxSubmitNumber(quizDTO.getMaxSubmitTime())
                     .setQuestions(quizDTO.getQuestions()));
-            sendMessages(quiz);
             return new QuizVO(quiz);
         } catch (DataIntegrityViolationException e) {
             throw HelperException.of(ExceptionType.CONFLICT, "修改失败,请检查测试信息是否正确且不与已有测试冲突");
@@ -171,7 +167,7 @@ public class QuizServiceImpl implements QuizService {
             throw HelperException.of(ExceptionType.FORBIDDEN, "您暂时无法获取该状态的测试");
         }
         Set<Long> courseIds = chooseDAO.findCourseIdsByStudentId(user.getId());
-        return quizDAO.findByCourseIdsAndStateAndNameContains(courseIds, state, key, pageable).map(QuizVO::new);
+        return quizDAO.findByCourseIdInAndStateAndNameContains(courseIds, state, key, pageable).map(QuizVO::new);
     }
 
     @Transactional(readOnly = true)
@@ -184,23 +180,12 @@ public class QuizServiceImpl implements QuizService {
         return new QuizVO(quiz);
     }
 
+    @Message(MessageType.QUIZ_NEW)
     @Transactional(rollbackFor = Exception.class)
     @Override
     public void modifyQuizState(Slide slide) {
         List<Quiz> quizzes = quizDAO.findBySlide(slide);
-        quizzes.forEach(quiz -> {
-            quiz.setState(MAP.get(slide.getState()).get(quiz.getType()));
-            sendMessages(quiz);
-        });
-    }
-
-    private void sendMessages(Quiz quiz) {
-        Slide slide = quiz.getSlide();
-        Course course = slide.getCourse();
-        Set<Long> chooseStudentIds = chooseDAO.findStudentIdsByCourseId(course.getId());
-        if (quiz.getState() == QuizState.ONGOING) {
-            messageService.createMessage(chooseStudentIds, MessageType.QUIZ_NEW, String.format("[%s] - [%s] 的测试 [%s] 已开放", course.getName(), slide.getName(), quiz.getName()), quiz.getId());
-        }
+        quizzes.forEach(quiz -> quiz.setState(MAP.get(slide.getState()).get(quiz.getType())));
     }
 
     @Override
@@ -220,7 +205,7 @@ public class QuizServiceImpl implements QuizService {
 
     @Override
     public boolean existsGetAuth(LoginUser user, Quiz quiz) {
-        return quiz.getState() != QuizState.NOT_STARTED
-                || user.getRole() == UserRole.TEACHER;
+        return user.getRole() == UserRole.TEACHER
+                || !STATES_NOT_GET_BY_STUDENT_SET.contains(quiz.getState());
     }
 }

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

@@ -25,7 +25,7 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * @author cst
@@ -70,13 +70,13 @@ public class QuizStudentAnswerServiceImpl implements QuizStudentAnswerService {
         // 计算得分
         List<BaseQuestionVO> questionVOList = questionService.getQuestionsByQuiz(quiz);
 
-        AtomicReference<Integer> correct = new AtomicReference<>(0);
+        AtomicInteger correct = new AtomicInteger();
 
         Map<String, Object> answers = quizStudentAnswerDTO.getAnswers();
         questionVOList.forEach(baseQuestionVO -> {
             Object answer;
             if ((answer = answers.get(baseQuestionVO.getId())) != null && baseQuestionVO.pass(answer)) {
-                correct.getAndSet(correct.get() + 1);
+                correct.getAndIncrement();
             }
         });
 

+ 44 - 0
src/main/java/nju/seec/helper/service/impl/RecordServiceImpl.java

@@ -0,0 +1,44 @@
+package nju.seec.helper.service.impl;
+
+import nju.seec.helper.api.bok.RecordApi;
+import nju.seec.helper.api.bok.vo.RecordQuestionVO;
+import nju.seec.helper.api.dataanalysis.ExerciseApi;
+import nju.seec.helper.api.dataanalysis.model.ExerciseRecord;
+import nju.seec.helper.dao.UserDAO;
+import nju.seec.helper.service.RecordService;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author cst
+ */
+@Service
+public class RecordServiceImpl implements RecordService {
+    private final UserDAO userDAO;
+
+    private final RecordApi recordApi;
+    private final ExerciseApi exerciseApi;
+
+    public RecordServiceImpl(UserDAO userDAO, RecordApi recordApi, ExerciseApi exerciseApi) {
+        this.userDAO = userDAO;
+        this.recordApi = recordApi;
+        this.exerciseApi = exerciseApi;
+    }
+
+    @Override
+    public List<ExerciseRecord> getAllRecords() {
+        return userDAO.findByPidNotNull()
+                .stream()
+                .flatMap(user -> getRecordsByUserId(String.valueOf(user.getPid())).stream())
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<ExerciseRecord> getRecordsByUserId(String userId) {
+        RecordQuestionVO recordQuestionVO = recordApi.getUserRecordQuestions(userId, PageRequest.of(0, Integer.MAX_VALUE));
+        return recordQuestionVO.getRecords().stream().map(recordVO -> exerciseApi.createExerciseRecordDTO(recordVO.getUserId(), recordVO.getQuestionId(), recordVO.getStartAt(), recordVO.getFinishAt(), recordVO.getPass())).collect(Collectors.toList());
+    }
+}

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

@@ -2,6 +2,7 @@ package nju.seec.helper.service.impl;
 
 import lombok.extern.slf4j.Slf4j;
 import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.aspect.message.Message;
 import nju.seec.helper.dao.CommentDAO;
 import nju.seec.helper.dao.ReplyDAO;
 import nju.seec.helper.dao.SlideDAO;
@@ -9,18 +10,14 @@ import nju.seec.helper.dao.UserDAO;
 import nju.seec.helper.dto.reply.ReplyDTO;
 import nju.seec.helper.entity.Comment;
 import nju.seec.helper.entity.Reply;
-import nju.seec.helper.entity.Slide;
 import nju.seec.helper.enums.ExceptionType;
 import nju.seec.helper.enums.MessageType;
 import nju.seec.helper.exception.HelperException;
-import nju.seec.helper.service.MessageService;
 import nju.seec.helper.service.ReplyService;
 import nju.seec.helper.vo.ReplyVO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.Collections;
-
 /**
  * @author cst
  */
@@ -32,16 +29,14 @@ public class ReplyServiceImpl implements ReplyService {
     private final ReplyDAO replyDAO;
     private final UserDAO userDAO;
 
-    private final MessageService messageService;
-
-    public ReplyServiceImpl(SlideDAO slideDAO, CommentDAO commentDAO, ReplyDAO replyDAO, UserDAO userDAO, MessageService messageService) {
+    public ReplyServiceImpl(SlideDAO slideDAO, CommentDAO commentDAO, ReplyDAO replyDAO, UserDAO userDAO) {
         this.slideDAO = slideDAO;
         this.commentDAO = commentDAO;
         this.replyDAO = replyDAO;
         this.userDAO = userDAO;
-        this.messageService = messageService;
     }
 
+    @Message(MessageType.COMMENT_REPLY)
     @Transactional(rollbackFor = Exception.class)
     @Override
     public ReplyVO createReply(LoginUser user, ReplyDTO replyDTO) {
@@ -61,12 +56,6 @@ public class ReplyServiceImpl implements ReplyService {
         comment.setReply(reply);
         reply = replyDAO.save(reply);
 
-        // 增加通知
-        if (!user.getId().equals(comment.getUser().getId())) {
-            Slide slide = slideDAO.findSlideById(comment.getSlideId());
-            messageService.createMessage(Collections.singleton(comment.getUser().getId()), MessageType.COMMENT_REPLY, String.format("您在 [%s] - [%s] 第%d页发表的讨论帖已收到回复", slide.getCourse().getName(), slide.getName(), comment.getPageNumber()), slide.getId());
-        }
-
         return new ReplyVO(reply);
     }
 

+ 9 - 27
src/main/java/nju/seec/helper/service/impl/SlideServiceImpl.java

@@ -1,9 +1,10 @@
 package nju.seec.helper.service.impl;
 
-import cn.hutool.core.text.StrBuilder;
+import cn.hutool.core.util.StrUtil;
 import com.google.common.collect.ImmutableSet;
 import lombok.SneakyThrows;
 import nju.seec.helper.aspect.auth.LoginUser;
+import nju.seec.helper.aspect.message.Message;
 import nju.seec.helper.dao.ChooseDAO;
 import nju.seec.helper.dao.CourseDAO;
 import nju.seec.helper.dao.SlideDAO;
@@ -18,13 +19,11 @@ import nju.seec.helper.enums.MessageType;
 import nju.seec.helper.enums.SlideState;
 import nju.seec.helper.exception.HelperException;
 import nju.seec.helper.service.CourseService;
-import nju.seec.helper.service.MessageService;
 import nju.seec.helper.service.QuizService;
 import nju.seec.helper.service.SlideService;
+import nju.seec.helper.util.OssUtils;
 import nju.seec.helper.util.file.FileInfo;
 import nju.seec.helper.util.file.FileUtils;
-import nju.seec.helper.util.oss.OssObjectUrlUtils;
-import nju.seec.helper.util.oss.OssUtils;
 import nju.seec.helper.vo.SlideVO;
 import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.data.domain.Page;
@@ -50,21 +49,17 @@ public class SlideServiceImpl implements SlideService {
     private final UserDAO userDAO;
 
     private final OssUtils ossUtils;
-    private final OssObjectUrlUtils ossObjectUrlUtils;
 
     private final QuizService quizService;
-    private final MessageService messageService;
     private final CourseService courseService;
 
-    public SlideServiceImpl(CourseDAO courseDAO, SlideDAO slideDAO, ChooseDAO chooseDAO, UserDAO userDAO, OssUtils ossUtils, OssObjectUrlUtils ossObjectUrlUtils, QuizService quizService, MessageService messageService, CourseService courseService) {
+    public SlideServiceImpl(CourseDAO courseDAO, SlideDAO slideDAO, ChooseDAO chooseDAO, UserDAO userDAO, OssUtils ossUtils, QuizService quizService, CourseService courseService) {
         this.courseDAO = courseDAO;
         this.slideDAO = slideDAO;
         this.chooseDAO = chooseDAO;
         this.userDAO = userDAO;
         this.ossUtils = ossUtils;
-        this.ossObjectUrlUtils = ossObjectUrlUtils;
         this.quizService = quizService;
-        this.messageService = messageService;
         this.courseService = courseService;
     }
 
@@ -131,6 +126,7 @@ public class SlideServiceImpl implements SlideService {
         }
     }
 
+    @Message(MessageType.SLIDE_IN_CLASS)
     @Transactional(rollbackFor = Exception.class)
     @Override
     public void modifySlideState(LoginUser user, Long slideId, SlideStateDTO slideStateDTO) {
@@ -143,12 +139,6 @@ public class SlideServiceImpl implements SlideService {
         slide.setState(slideStateDTO.getState());
         slide = slideDAO.save(slide);
 
-        // 增加通知
-        if (slide.getState() == SlideState.IN_CLASS) {
-            Set<Long> studentIds = chooseDAO.findStudentIdsByCourseId(slide.getCourse().getId());
-            messageService.createMessage(studentIds, MessageType.SLIDE_IN_CLASS, String.format("[%s] - [%s] 已开课", slide.getCourse().getName(), slide.getName()), slide.getId());
-        }
-
         // 修改Slide状态同时推进quiz状态
         quizService.modifyQuizState(slide);
     }
@@ -197,16 +187,7 @@ public class SlideServiceImpl implements SlideService {
     }
 
     private String getObjectName(Long slideId, String slideName, String extension) {
-        return StrBuilder
-                .create()
-                .append(SLIDE_STORE_DIR)
-                .append(File.separator)
-                .append(slideId)
-                .append(File.separator)
-                .append(slideName)
-                .append(".")
-                .append(extension)
-                .toStringAndReset();
+        return StrUtil.join(File.separator, SLIDE_STORE_DIR, slideId, slideName + "." + extension);
     }
 
     @Transactional(readOnly = true)
@@ -245,7 +226,7 @@ public class SlideServiceImpl implements SlideService {
             throw HelperException.of(ExceptionType.FORBIDDEN, "您无权获取该课件的链接");
         }
 
-        return ossObjectUrlUtils.getUrl(slide.getObjectName());
+        return ossUtils.getUrl(slide.getObjectName());
     }
 
     @Transactional(readOnly = true)
@@ -277,6 +258,7 @@ public class SlideServiceImpl implements SlideService {
 
     @Override
     public boolean existsGetAuth(LoginUser user, Slide slide) {
-        return courseService.existsGetAuth(user, courseDAO.findCourseById(slide.getCourse().getId()));
+        return slide.getCourse().getTeacher().getId().equals(user.getId())
+                || (!STATES_NOT_GET_BY_STUDENT_SET.contains(slide.getState()) && chooseDAO.existsByStudentIdAndCourseId(user.getId(), slide.getCourse().getId()));
     }
 }

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

@@ -15,7 +15,6 @@ import java.util.Optional;
  */
 @Service
 public class UserServiceImpl implements UserService {
-    //    private final RedisCacheUtils cacheUtils;
     private final UserDAO userDAO;
 
     public UserServiceImpl(UserDAO userDAO

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

@@ -1,12 +0,0 @@
-package nju.seec.helper.util;
-
-/**
- * @author cst
- */
-public class Consts {
-    public static final String SYS_NAME = "helper";
-
-//    public static final String SESSION_USER_NAME = SYS_NAME + "_user";
-//    public static final String EMAIL_CACHE_NAME = SYS_NAME + "_email";
-//    public static final String PHONE_CACHE_NAME = SYS_NAME + "_phone";
-}

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

@@ -15,7 +15,7 @@ import java.time.format.DateTimeFormatter;
  */
 @UtilityClass
 public class JsonUtils {
-    private static final Gson GSON = new GsonBuilder()
+    private final Gson GSON = new GsonBuilder()
             .setPrettyPrinting()
             .registerTypeAdapter(LocalDateTime.class, (JsonSerializer<LocalDateTime>) (localDateTime, type, jsonSerializationContext) -> new JsonPrimitive(localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)))
             .create();

+ 11 - 3
src/main/java/nju/seec/helper/util/oss/OssUtils.java → src/main/java/nju/seec/helper/util/OssUtils.java

@@ -1,4 +1,4 @@
-package nju.seec.helper.util.oss;
+package nju.seec.helper.util;
 
 import com.aliyun.oss.ClientException;
 import com.aliyun.oss.OSS;
@@ -9,6 +9,7 @@ import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import nju.seec.helper.enums.ExceptionType;
 import nju.seec.helper.exception.HelperException;
+import nju.seec.helper.util.cache.CaffeineCacheUtils;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Component;
@@ -31,6 +32,9 @@ public class OssUtils {
     private String accessKeyId;
     private String accessKeySecret;
     private String bucketName;
+    private long objectUrlLivingSeconds;
+    private String objectUrlCacheName;
+    private final CaffeineCacheUtils cacheUtils;
 
     public void upload(String objectName, InputStream inputStream) {
         OSS ossClient = getOss();
@@ -47,9 +51,13 @@ public class OssUtils {
         }
     }
 
-    public String getUrl(String objectName, long seconds) {
+    public String getUrl(String objectName) {
+        String cacheUrl = (String) cacheUtils.get(objectUrlCacheName, objectName);
+        if (cacheUrl != null) {
+            return cacheUrl;
+        }
         OSS ossClient = getOss();
-        Date expiration = new Date(System.currentTimeMillis() + seconds * 1000);
+        Date expiration = new Date(System.currentTimeMillis() + objectUrlLivingSeconds * 1000);
         try {
             URL url = ossClient.generatePresignedUrl(bucketName, objectName, expiration);
             return url.toString();

+ 22 - 2
src/main/java/nju/seec/helper/util/RestRequestUtil.java

@@ -12,6 +12,26 @@ import java.util.Map;
  */
 @Component
 public class RestRequestUtil {
+    public <T> T sendPostRequest(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) {
+        RestTemplate client = new RestTemplate();
+        return client.postForObject(url, request, responseType, uriVariables);
+    }
+
+    public void sendPutRequest(String url, Object request, Map<String, ?> uriVariables) {
+        RestTemplate client = new RestTemplate();
+        client.put(url, request, uriVariables);
+    }
+
+    public void sendDeleteRequest(String url, Map<String, ?> uriVariables) {
+        RestTemplate client = new RestTemplate();
+        client.delete(url, uriVariables);
+    }
+
+    public <T> T sendGetRequest(String url, Class<T> responseType, Map<String, ?> uriVariables) {
+        RestTemplate client = new RestTemplate();
+        return client.getForObject(url, responseType, uriVariables);
+    }
+
     public <T> T sendPostRequest(String url, Object request, Class<T> responseType) {
         RestTemplate client = new RestTemplate();
         return client.postForObject(url, request, responseType);
@@ -27,8 +47,8 @@ public class RestRequestUtil {
         client.delete(url);
     }
 
-    public <T> T sendGetRequest(String url, Class<T> responseType, Map<String, ?> uriVariables) {
+    public <T> T sendGetRequest(String url, Class<T> responseType) {
         RestTemplate client = new RestTemplate();
-        return client.getForObject(url, responseType, uriVariables);
+        return client.getForObject(url, responseType);
     }
 }

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

@@ -1,6 +1,8 @@
 package nju.seec.helper.util;
 
+import cn.hutool.core.util.StrUtil;
 import lombok.experimental.UtilityClass;
+import org.springframework.data.domain.Sort;
 
 /**
  * @author cst
@@ -10,4 +12,11 @@ public class StringUtils {
     public String keyPattern(String key) {
         return '%' + key + '%';
     }
+
+    public String getSortString(Sort sort) {
+        return sort.stream()
+                .map(order -> StrUtil.concat(true, "sort=", order.getProperty(), ",", order.getDirection().toString()))
+                .reduce((o1, o2) -> StrUtil.join("&", o1, o2))
+                .orElse("");
+    }
 }

+ 14 - 11
src/main/java/nju/seec/helper/util/cache/GuavaCacheUtils.java → src/main/java/nju/seec/helper/util/cache/CaffeineCacheUtils.java

@@ -1,8 +1,7 @@
 package nju.seec.helper.util.cache;
 
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.collect.ImmutableMap;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
@@ -17,13 +16,13 @@ import java.util.stream.Collectors;
  * updated by cst
  */
 @Component
-public class GuavaCacheUtils {
-    private Cache<Object, Object> cache;
+public class CaffeineCacheUtils {
+    private final Cache<String, Object> cache;
 
-    public GuavaCacheUtils(@Value("${guavaCache.maximumSize}") Long maximumSize,
-                           @Value("${guavaCache.expireAfterAccessInSeconds}") Long expireAfterAccessInSeconds,
-                           @Value("${guavaCache.expireAfterWriteInSeconds}") Long expireAfterWriteInSeconds) {
-        cache = CacheBuilder
+    public CaffeineCacheUtils(@Value("${helper.cache.maximumSize}") Long maximumSize,
+                              @Value("${helper.cache.expireAfterAccessInSeconds}") Long expireAfterAccessInSeconds,
+                              @Value("${helper.cache.expireAfterWriteInSeconds}") Long expireAfterWriteInSeconds) {
+        cache = Caffeine
                 .newBuilder()
                 .maximumSize(maximumSize)
                 .expireAfterAccess(expireAfterAccessInSeconds, TimeUnit.SECONDS)
@@ -44,15 +43,19 @@ public class GuavaCacheUtils {
         return cache.getIfPresent(combineKey(cacheName, key));
     }
 
-    public ImmutableMap<Object, Object> multiGet(String cacheName, Collection<String> keys) {
+    public Map<String, 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) {
+    public void invalidate(String cacheName, String key) {
         cache.invalidate(combineKey(cacheName, key));
     }
 
+    public void invalidateAll() {
+        cache.invalidateAll();
+    }
+
     private String combineKey(String cacheName, String key) {
         return cacheName + ":" + key;
     }

+ 43 - 43
src/main/java/nju/seec/helper/util/cache/RedisCacheUtils.java

@@ -1,43 +1,43 @@
-package nju.seec.helper.util.cache;
-
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-/**
- * use redis
- *
- * @author cst
- */
-@Component
-public class RedisCacheUtils {
-    private final 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(combineKey(cacheName, key), value, expireTime, timeUnit);
-    }
-
-    public String get(String cacheName, String key) {
-        return redisTemplate.opsForValue().get(combineKey(cacheName, key));
-    }
-
-    public List<String> multiGet(String cacheName, List<String> keys) {
-        List<String> body = keys.stream().map(key -> combineKey(cacheName, key)).collect(Collectors.toList());
-        return redisTemplate.opsForValue().multiGet(body);
-    }
-
-    public void remove(String cacheName, String key) {
-        redisTemplate.delete(cacheName + ":" + key);
-    }
-
-    private String combineKey(String cacheName, String key) {
-        return cacheName + ":" + key;
-    }
-}
+//package nju.seec.helper.util.cache;
+//
+//import org.springframework.data.redis.core.RedisTemplate;
+//import org.springframework.stereotype.Component;
+//
+//import java.util.List;
+//import java.util.concurrent.TimeUnit;
+//import java.util.stream.Collectors;
+//
+///**
+// * use redis
+// *
+// * @author cst
+// */
+//@Component
+//public class RedisCacheUtils {
+//    private final 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(combineKey(cacheName, key), value, expireTime, timeUnit);
+//    }
+//
+//    public String get(String cacheName, String key) {
+//        return redisTemplate.opsForValue().get(combineKey(cacheName, key));
+//    }
+//
+//    public List<String> multiGet(String cacheName, List<String> keys) {
+//        List<String> body = keys.stream().map(key -> combineKey(cacheName, key)).collect(Collectors.toList());
+//        return redisTemplate.opsForValue().multiGet(body);
+//    }
+//
+//    public void remove(String cacheName, String key) {
+//        redisTemplate.delete(cacheName + ":" + key);
+//    }
+//
+//    private String combineKey(String cacheName, String key) {
+//        return cacheName + ":" + key;
+//    }
+//}

+ 8 - 4
src/main/java/nju/seec/helper/util/file/FileUtils.java

@@ -2,12 +2,12 @@ package nju.seec.helper.util.file;
 
 import cn.hutool.core.io.FileTypeUtil;
 import com.google.common.collect.ImmutableMap;
+import lombok.Cleanup;
 import lombok.SneakyThrows;
 import lombok.experimental.UtilityClass;
 import nju.seec.helper.enums.ExceptionType;
 import nju.seec.helper.exception.HelperException;
 import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.text.PDFTextStripper;
 import org.apache.tomcat.util.http.fileupload.IOUtils;
 
 import java.io.ByteArrayInputStream;
@@ -22,8 +22,12 @@ import java.util.Optional;
  */
 @UtilityClass
 public class FileUtils {
-    private static final Map<String, FileInfoParser> FILE_INFO_PARSER_MAP = ImmutableMap.of(
-            "pdf", inputStream -> FileInfo.of(PDDocument.load(inputStream).getNumberOfPages(), "pdf")
+    private final Map<String, FileInfoParser> FILE_INFO_PARSER_MAP = ImmutableMap.of(
+            "pdf", inputStream -> {
+                try (PDDocument pdDocument = PDDocument.load(inputStream)) {
+                    return FileInfo.of(pdDocument.getNumberOfPages(), "pdf");
+                }
+            }
     );
 
 //    private static final Map<String, FileContentParser> FILE_CONTENT_PARSER_MAP = ImmutableMap.of(
@@ -32,7 +36,7 @@ public class FileUtils {
 
     @SneakyThrows
     public FileInfo getFileInfo(InputStream inputStream) {
-        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        @Cleanup ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         IOUtils.copy(inputStream, byteArrayOutputStream);
         return Optional.ofNullable(FILE_INFO_PARSER_MAP.get(FileTypeUtil.getType(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()))))
                 .orElseThrow(() -> HelperException.of(ExceptionType.FORBIDDEN, "不支持的文件类型"))

+ 0 - 32
src/main/java/nju/seec/helper/util/oss/OssObjectUrlUtils.java

@@ -1,32 +0,0 @@
-package nju.seec.helper.util.oss;
-
-import nju.seec.helper.util.Consts;
-import nju.seec.helper.util.cache.RedisCacheUtils;
-import org.springframework.stereotype.Component;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * @author cst
- */
-@Component
-public class OssObjectUrlUtils {
-    private static final String OSS_OBJECT_URL_NAME = Consts.SYS_NAME + "_oss_object_url";
-    private static final long OSS_OBJECT_URL_LIVING_SECONDS = 20 * 60;
-    private final RedisCacheUtils cacheUtils;
-    private final OssUtils ossUtils;
-
-    public OssObjectUrlUtils(RedisCacheUtils cacheUtils, OssUtils ossUtils) {
-        this.cacheUtils = cacheUtils;
-        this.ossUtils = ossUtils;
-    }
-
-    public String getUrl(String objectName) {
-        String url = cacheUtils.get(OSS_OBJECT_URL_NAME, objectName);
-        if (url == null) {
-            url = ossUtils.getUrl(objectName, OSS_OBJECT_URL_LIVING_SECONDS);
-            cacheUtils.set(OSS_OBJECT_URL_NAME, objectName, url, OSS_OBJECT_URL_LIVING_SECONDS, TimeUnit.SECONDS);
-        }
-        return url;
-    }
-}

+ 0 - 21
src/main/java/nju/seec/helper/vo/EventVO.java

@@ -1,21 +0,0 @@
-package nju.seec.helper.vo;
-
-import lombok.Data;
-import lombok.experimental.Accessors;
-import nju.seec.helper.api.dataanalysis.EventAction;
-import nju.seec.helper.api.dataanalysis.EventType;
-
-import java.time.LocalDateTime;
-
-/**
- * @author cst
- */
-@Data
-@Accessors(chain = true)
-public class EventVO {
-    private String userId;
-    private EventType type;
-    private LocalDateTime time;
-    private EventAction action;
-    private String description;
-}

+ 0 - 4
src/main/java/nju/seec/helper/vo/UserVO.java

@@ -19,8 +19,6 @@ public class UserVO {
     private UserRole role;
     private LocalDateTime createAt;
 
-//    private UserState state;
-
     public UserVO(@NonNull User user) {
         this.id = user.getId();
         this.name = user.getName();
@@ -28,7 +26,5 @@ public class UserVO {
         this.phone = user.getPhone();
         this.role = user.getRole();
         this.createAt = user.getCreateAt();
-
-//        this.state = user.getState();
     }
 }

+ 20 - 15
src/main/java/nju/seec/helper/vo/question/BaseQuestionVO.java

@@ -2,10 +2,12 @@ package nju.seec.helper.vo.question;
 
 import lombok.Data;
 import lombok.NonNull;
-import nju.seec.helper.enums.QuestionType;
+import nju.seec.helper.api.bok.constant.QuestionType;
+import nju.seec.helper.api.bok.vo.QuestionVO;
 
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author XuShengTao
@@ -16,31 +18,34 @@ import java.util.List;
 public abstract class BaseQuestionVO {
     protected String id;
     protected QuestionType type;
+    protected Map<String, String> options;
     protected String stem;
     protected String analysis;
     protected String keyPoints;
     protected List<String> tags;
     protected List<String> knowledgeId;
-    private Date lastModified;
+    protected Date lastModified;
 
-    public static BaseQuestionVO convertBokQuestionToVO(@NonNull BokQuestionVO bokQuestionVO, boolean withAnswer) {
-        switch (bokQuestionVO.getType()) {
-            case "choice":
-                return new ChoiceQuestionVO(bokQuestionVO, withAnswer);
-            case "true_false":
-                return new TrueOrFalseQuestionVO(bokQuestionVO, withAnswer);
+    public static BaseQuestionVO convertBokQuestionToVO(@NonNull QuestionVO questionVO, boolean withAnswer) {
+        switch (questionVO.getType()) {
+            case choice:
+                return new ChoiceQuestionVO(questionVO, withAnswer);
+            case true_false:
+                return new TrueOrFalseQuestionVO(questionVO, withAnswer);
             default:
                 return null;
         }
     }
 
-    protected BaseQuestionVO(BokQuestionVO bokQuestionVO) {
-        this.id = bokQuestionVO.getId();
-        this.stem = bokQuestionVO.getStem();
-        this.keyPoints = bokQuestionVO.getKeyPoints();
-        this.tags = bokQuestionVO.getTags();
-        this.knowledgeId = bokQuestionVO.getKnowledgeId();
-        this.lastModified = bokQuestionVO.getLastModified();
+    protected BaseQuestionVO(QuestionVO questionVO) {
+        this.id = questionVO.getId();
+        this.type = questionVO.getType();
+        this.options = questionVO.getOptions();
+        this.stem = questionVO.getStem();
+        this.keyPoints = questionVO.getKeyPoints();
+        this.tags = questionVO.getTags();
+        this.knowledgeId = questionVO.getKnowledgeId();
+        this.lastModified = questionVO.getLastModifiedTime();
     }
 
     /**

+ 0 - 30
src/main/java/nju/seec/helper/vo/question/BokQuestionVO.java

@@ -1,30 +0,0 @@
-package nju.seec.helper.vo.question;
-
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-
-/**
- * @author sheen
- * <p>
- * updated by cst
- */
-@Data
-public class BokQuestionVO {
-    private String id;
-    private String type;
-    private String stem;
-    private Map<String, String> options;
-    private String answer;
-    @JsonProperty(value = "key_points")
-    private String keyPoints;
-    private String analysis;
-    private List<String> tags;
-    private List<String> knowledgeId;
-    @JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss")
-    private Date lastModified;
-}

+ 5 - 10
src/main/java/nju/seec/helper/vo/question/ChoiceQuestionVO.java

@@ -3,9 +3,7 @@ package nju.seec.helper.vo.question;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NonNull;
-import nju.seec.helper.enums.QuestionType;
-
-import java.util.Map;
+import nju.seec.helper.api.bok.vo.QuestionVO;
 
 /**
  * @author xst
@@ -15,17 +13,14 @@ import java.util.Map;
 @EqualsAndHashCode(callSuper = true)
 @Data
 public class ChoiceQuestionVO extends BaseQuestionVO {
-    private Map<String, String> options;
     private String answer;
 
-    public ChoiceQuestionVO(@NonNull BokQuestionVO bokQuestionVO, boolean withAnswer) {
-        super(bokQuestionVO);
-        this.type = QuestionType.CHOICE;
-        this.options = bokQuestionVO.getOptions();
+    public ChoiceQuestionVO(@NonNull QuestionVO questionVO, boolean withAnswer) {
+        super(questionVO);
 
         if (withAnswer) {
-            this.answer = bokQuestionVO.getAnswer();
-            this.analysis = bokQuestionVO.getAnalysis();
+            this.answer = questionVO.getAnswer();
+            this.analysis = questionVO.getAnalysis();
         }
     }
 

+ 5 - 7
src/main/java/nju/seec/helper/vo/question/TrueOrFalseQuestionVO.java

@@ -4,7 +4,7 @@ import cn.hutool.core.convert.Convert;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NonNull;
-import nju.seec.helper.enums.QuestionType;
+import nju.seec.helper.api.bok.vo.QuestionVO;
 
 /**
  * @author xst
@@ -15,20 +15,18 @@ import nju.seec.helper.enums.QuestionType;
 @Data
 public class TrueOrFalseQuestionVO extends BaseQuestionVO {
     private Boolean answer;
-    private String analysis;
 
     @Override
     public boolean pass(Object answer) {
         return this.answer.equals(Convert.toBool(answer));
     }
 
-    public TrueOrFalseQuestionVO(@NonNull BokQuestionVO bokQuestionVO, boolean withAnswer) {
-        super(bokQuestionVO);
-        this.type = QuestionType.TRUE_FALSE;
+    public TrueOrFalseQuestionVO(@NonNull QuestionVO questionVO, boolean withAnswer) {
+        super(questionVO);
 
         if (withAnswer) {
-            this.answer = Boolean.valueOf(bokQuestionVO.getAnswer());
-            this.analysis = bokQuestionVO.getAnalysis();
+            this.answer = Boolean.valueOf(questionVO.getAnswer());
+            this.analysis = questionVO.getAnalysis();
         }
     }
 }

+ 1 - 1
src/main/java/nju/seec/helper/vo/quiz/QuizStudentAnswerVO.java

@@ -62,7 +62,7 @@ public class QuizStudentAnswerVO {
 
     @Data
     @AllArgsConstructor(staticName = "of")
-    private static class QuestionStudentAnswerInfo {
+    public static class QuestionStudentAnswerInfo {
         private String questionId;
         private Object answer;
         private Boolean correct;

+ 2 - 4
src/main/java/nju/seec/helper/vo/quiz/QuizVO.java

@@ -1,7 +1,8 @@
 package nju.seec.helper.vo.quiz;
 
 
-import lombok.*;
+import lombok.Data;
+import lombok.NonNull;
 import nju.seec.helper.entity.Quiz;
 import nju.seec.helper.enums.QuizState;
 import nju.seec.helper.enums.QuizType;
@@ -14,9 +15,6 @@ import java.time.LocalDateTime;
  * updated by cst
  */
 @Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
 public class QuizVO {
     private Long id;
     private String name;

+ 63 - 41
src/main/resources/application-deploy.yml

@@ -1,6 +1,8 @@
+server:
+  port: 9095
 spring:
   datasource:
-    url: jdbc:mysql://localhost:13306/helper?setUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
+    url: jdbc:mysql://localhost:33306/helper?setUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
     password: NJU67helper
@@ -14,31 +16,31 @@ spring:
   http:
     encoding:
       force: true
-  redis:
-    host: localhost
-    port: 6379
+  #  redis:
+  #    host: localhost
+  #    port: 6379
   data:
-    redis:
-      repositories:
-        enabled: false
+    #    redis:
+    #      repositories:
+    #        enabled: false
     web:
       pageable:
         max-page-size: 1000
         one-indexed-parameters: true
-  mail:
-    host: smtp.exmail.qq.com
-    username: noreply@seecoder.cn
-    password: eNrZcqucgcFtLC29
-    properties:
-      mail:
-        smtp:
-          auth: true
-          socketFactory:
-            class: javax.net.ssl.SSLSocketFactory
-            port: 465
-          starttls:
-            enable: true
-            required: true
+  #  mail:
+  #    host: smtp.exmail.qq.com
+  #    username: noreply@seecoder.cn
+  #    password: eNrZcqucgcFtLC29
+  #    properties:
+  #      mail:
+  #        smtp:
+  #          auth: true
+  #          socketFactory:
+  #            class: javax.net.ssl.SSLSocketFactory
+  #            port: 465
+  #          starttls:
+  #            enable: true
+  #            required: true
   servlet:
     multipart:
       max-file-size: 50MB
@@ -49,32 +51,52 @@ aliyun:
     accessKeyId: LTAI4Fi1qmL4iuhH8t7G7r2H
     accessKeySecret: 7tJWyQqa0cMkQGaPMMvtH6c0VLiOO2
     bucketName: seec-helper-pdf
+    object-url-living-seconds: 1200
+    object-url-cache-name: ${helper.sys-name}_oss_object_url
 #  sms:
 #    accessKeyId: LTAI4FiAXXyNfYJLweBXpKPx
 #    accessSecret: IBoC7KGkZHOvxZQIfQgp98ZY48AVjW
 #    signName: seeccoder
 #    templateCode: SMS_181555598
 bok:
-  url: http://bok.seecoder.cn
-  tqUrl: ${bok.url}/api/question
-  tqSearchUrl: ${bok.tqUrl}/search
-  tqStemSearchUrl: ${bok.tqSearchUrl}/findByStemLike?content={content}&size={size}&page={page}
-  tqIdSearchUrl: ${bok.tqSearchUrl}/findByIdIn?id={id}
-  knowledgeUrl: ${bok.url}/api/neo
-  knowledgeGetAllUrl: ${bok.knowledgeUrl}/getAll
-#data-analysis:
-#  url: http://dataanalysis.seecoder.cn
-#  eventUrl: ${data-analysis.url}/api/event
-#  eventReportUrl: ${data-analysis.eventUrl}/report
-guavaCache:
-  maximumSize: 1000
-  expireAfterAccessInSeconds: 0
-  expireAfterWriteInSeconds: 600
+  api-url: http://localhost:3909/api
+  question-cache-name: ${helper.sys-name}_bok_question
+  knowledge-cache-name: ${helper.sys-name}_bok_knowledge
+  question:
+    url: ${bok.api-url}/question
+    search-url: ${bok.question.url}?stem={stem}&tag={tag}&knowledgeId={knowledgeId}&page={page}&size={size}&{sort}
+  question-query:
+    url: ${bok.api-url}/question-query
+    by-id-url: ${bok.question-query.url}/id?id={id}
+  knowledge:
+    url: ${bok.api-url}/knowledge
+  user:
+    url: ${bok.api-url}/user
+    rate-url: ${bok.user.url}/{userId}/question/{questionId}/rate
+    collection-url: ${bok.user.url}/{userId}/question/{questionId}/collection
+    question-url: ${bok.user.url}/{userId}/question?collection={collection}&rate={rate}&page={page}&size={size}&{sort}
+    interactive-url: ${bok.user.url}/{userId}/questionInList?questionId={questionId}
+  record:
+    url: ${bok.api-url}/record
+    question-url: ${bok.record.url}/{userId}/question?page={page}&size={size}&{sort}
+data-analysis:
+  api-url: http://dataanalysis.seecoder.cn/api
+  event:
+    url: ${data-analysis.api-url}/event
+    report-url: ${data-analysis.event.url}/report
+  exercise:
+    url: ${data-analysis.api-url}/exercise
+    report-url: ${data-analysis.exercise.url}/report
 jwt:
   secret: SEEC-1919810-OIDC-114514
 # 自定义数据
-#helper:
-#  mail:
-#    from: ${spring.mail.username}
-#    subject: seecoder
-#  user-timeout-seconds: 43200
+helper:
+  sys-name: helper
+  #  mail:
+  #    from: ${spring.mail.username}
+  #    subject: seecoder
+  #  user-timeout-seconds: 43200
+  cache:
+    maximumSize: 1000
+    expireAfterAccessInSeconds: 0
+    expireAfterWriteInSeconds: 900

+ 62 - 42
src/main/resources/application-dev.yml

@@ -1,6 +1,6 @@
 spring:
   datasource:
-    url: jdbc:mysql://localhost:3306/helper?setUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
+    url: jdbc:mysql://localhost:3306/helper?setUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
     password: 123456
@@ -10,35 +10,35 @@ spring:
       ddl-auto: update
     database: mysql
     database-platform: org.hibernate.dialect.MySQL8Dialect
-    show-sql: true
+  #    show-sql: true
   http:
     encoding:
       force: true
-  redis:
-    host: localhost
-    port: 6379
+  #  redis:
+  #    host: localhost
+  #    port: 6379
   data:
-    redis:
-      repositories:
-        enabled: false
+    #    redis:
+    #      repositories:
+    #        enabled: false
     web:
       pageable:
         max-page-size: 1000
         one-indexed-parameters: true
-  mail:
-    host: smtp.exmail.qq.com
-    username: noreply@seecoder.cn
-    password: eNrZcqucgcFtLC29
-    properties:
-      mail:
-        smtp:
-          auth: true
-          socketFactory:
-            class: javax.net.ssl.SSLSocketFactory
-            port: 465
-          starttls:
-            enable: true
-            required: true
+  #  mail:
+  #    host: smtp.exmail.qq.com
+  #    username: noreply@seecoder.cn
+  #    password: eNrZcqucgcFtLC29
+  #    properties:
+  #      mail:
+  #        smtp:
+  #          auth: true
+  #          socketFactory:
+  #            class: javax.net.ssl.SSLSocketFactory
+  #            port: 465
+  #          starttls:
+  #            enable: true
+  #            required: true
   servlet:
     multipart:
       max-file-size: 50MB
@@ -49,32 +49,52 @@ aliyun:
     accessKeyId: LTAI4Fi1qmL4iuhH8t7G7r2H
     accessKeySecret: 7tJWyQqa0cMkQGaPMMvtH6c0VLiOO2
     bucketName: seec-helper-dev
+    object-url-living-seconds: 1200
+    object-url-cache-name: ${helper.sys-name}_oss_object_url
 #  sms:
 #    accessKeyId: LTAI4FiAXXyNfYJLweBXpKPx
 #    accessSecret: IBoC7KGkZHOvxZQIfQgp98ZY48AVjW
 #    signName: seeccoder
 #    templateCode: SMS_181555598
 bok:
-  url: http://bok.seecoder.cn
-  tqUrl: ${bok.url}/api/question
-  tqSearchUrl: ${bok.tqUrl}/search
-  tqStemSearchUrl: ${bok.tqSearchUrl}/findByStemLike?content={content}&size={size}&page={page}
-  tqIdSearchUrl: ${bok.tqSearchUrl}/findByIdIn?id={id}
-  knowledgeUrl: ${bok.url}/api/neo
-  knowledgeGetAllUrl: ${bok.knowledgeUrl}/getAll
-#data-analysis:
-#  url: http://dataanalysis.seecoder.cn
-#  eventUrl: ${data-analysis.url}/api/event
-#  eventReportUrl: ${data-analysis.eventUrl}/report
-guavaCache:
-  maximumSize: 1000
-  expireAfterAccessInSeconds: 0
-  expireAfterWriteInSeconds: 600
+  api-url: http://bok.seecoder.cn/api
+  question-cache-name: ${helper.sys-name}_bok_question
+  knowledge-cache-name: ${helper.sys-name}_bok_knowledge
+  question:
+    url: ${bok.api-url}/question
+    search-url: ${bok.question.url}?stem={stem}&tag={tag}&knowledgeId={knowledgeId}&page={page}&size={size}&{sort}
+  question-query:
+    url: ${bok.api-url}/question-query
+    by-id-url: ${bok.question-query.url}/id?id={id}
+  knowledge:
+    url: ${bok.api-url}/knowledge
+  user:
+    url: ${bok.api-url}/user
+    rate-url: ${bok.user.url}/{userId}/question/{questionId}/rate
+    collection-url: ${bok.user.url}/{userId}/question/{questionId}/collection
+    question-url: ${bok.user.url}/{userId}/question?collection={collection}&rate={rate}&page={page}&size={size}&{sort}
+    interactive-url: ${bok.user.url}/{userId}/questionInList?questionId={questionId}
+  record:
+    url: ${bok.api-url}/record
+    question-url: ${bok.record.url}/{userId}/question?page={page}&size={size}&{sort}
+data-analysis:
+  api-url: http://dataanalysis.seecoder.cn/api
+  event:
+    url: ${data-analysis.api-url}/event
+    report-url: ${data-analysis.event.url}/report
+  exercise:
+    url: ${data-analysis.api-url}/exercise
+    report-url: ${data-analysis.exercise.url}/report
 jwt:
   secret: SEEC-1919810-OIDC-114514
 # 自定义数据
-#helper:
-#  mail:
-#    from: ${spring.mail.username}
-#    subject: seecoder
-#  user-timeout-seconds: 43200
+helper:
+  sys-name: helper
+  #  mail:
+  #    from: ${spring.mail.username}
+  #    subject: seecoder
+  #  user-timeout-seconds: 43200
+  cache:
+    maximumSize: 1000
+    expireAfterAccessInSeconds: 0
+    expireAfterWriteInSeconds: 900

+ 9 - 9
src/main/resources/hibernate.properties

@@ -1,9 +1,9 @@
-hibernate.format_sql=true
-hibernate.cache.use_second_level_cache=true
-hibernate.cache.use_query_cache=true
-hibernate.cache.region_prefix=helper
-hibernate.cache.region.factory_class=org.hibernate.cache.jcache.internal.JCacheRegionFactory
-hibernate.cache.provider_configuration_file_resource_path=ehcache.xml
-hibernate.cache.use_structured_entries=true
-hibernate.generate_statistics=false
-hibernate.javax.cache.missing_cache_strategy=create
+#hibernate.format_sql=true
+#hibernate.cache.use_second_level_cache=true
+#hibernate.cache.use_query_cache=true
+#hibernate.cache.region_prefix=helper
+#hibernate.cache.region.factory_class=org.hibernate.cache.jcache.internal.JCacheRegionFactory
+#hibernate.cache.provider_configuration_file_resource_path=ehcache.xml
+#hibernate.cache.use_structured_entries=true
+#hibernate.generate_statistics=false
+#hibernate.javax.cache.missing_cache_strategy=create