1
0
Преглед на файлове

move questions to next stage

Bay преди 5 години
родител
ревизия
525334ff46
променени са 59 файла, в които са добавени 3036 реда и са изтрити 351 реда
  1. 45 0
      .editorconfig
  2. 13 0
      Dockerfile
  3. 2 0
      README.md
  4. 24 39
      pom.xml
  5. 15 9
      seecoder-bok-client/pom.xml
  6. 26 70
      seecoder-bok-client/src/main/java/cn/seecoder/bok/client/SeecoderBokClient.java
  7. 17 0
      seecoder-bok-client/src/test/java/cn/seecoder/bok/client/InteractionParseTest.java
  8. 1 1
      seecoder-bok-client/src/test/java/cn/seecoder/bok/client/SeecoderBokClientTest.java
  9. 34 0
      seecoder-bok-client/src/test/java/cn/seecoder/bok/common/QuestionVOMapperTest.java
  10. 49 3
      seecoder-bok-common/pom.xml
  11. 42 10
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/QuestionType.java
  12. 95 0
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/QuestionVOMapper.java
  13. 1 1
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokCodeQuestionApi.java
  14. 1 1
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokKnowledgeApi.java
  15. 16 7
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokQuestionApi.java
  16. 1 1
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokQuestionQueryApi.java
  17. 2 3
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokRecordApi.java
  18. 4 4
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokUserInteractiveApi.java
  19. 36 0
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/utility/RestPage.java
  20. 1 1
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/InteractionVO.java
  21. 0 53
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/PageVO.java
  22. 0 35
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/PageableQuestionListVO.java
  23. 0 34
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/PageableRecordQuestionVO.java
  24. 51 70
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/QuestionVO.java
  25. 13 3
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/RecordQuestionVO.java
  26. 27 6
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/RecordVO.java
  27. 157 0
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/AbstractQuestionVO.java
  28. 43 0
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/BlankQuestionVO.java
  29. 59 0
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/ChoiceQuestionVO.java
  30. 68 0
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/CodeQuestionVO.java
  31. 39 0
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/ShortAnswerQuestionVO.java
  32. 37 0
      seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/TrueFalseQuestionVO.java
  33. 117 0
      seecoder-bok-server/.mvn/wrapper/MavenWrapperDownloader.java
  34. BIN
      seecoder-bok-server/.mvn/wrapper/maven-wrapper.jar
  35. 2 0
      seecoder-bok-server/.mvn/wrapper/maven-wrapper.properties
  36. 310 0
      seecoder-bok-server/mvnw
  37. 182 0
      seecoder-bok-server/mvnw.cmd
  38. 94 0
      seecoder-bok-server/pom.xml
  39. 18 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/SeecoderBokServerApplication.java
  40. 63 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/config/ElasticConfig.java
  41. 109 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/domain/Interaction.java
  42. 223 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/domain/Question.java
  43. 134 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/domain/Record.java
  44. 27 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/mapper/InteractionMapper.java
  45. 28 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/mapper/QuestionMapper.java
  46. 28 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/mapper/RecordMapper.java
  47. 60 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/repository/InteractionRepository.java
  48. 11 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/repository/QuestionRepository.java
  49. 61 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/repository/RecordRepository.java
  50. 86 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/service/QuestionService.java
  51. 42 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/service/RecordService.java
  52. 95 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/service/UserInteractionService.java
  53. 22 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/utility/EmptyCheck.java
  54. 94 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/web/rest/QuestionController.java
  55. 78 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/web/rest/RecordController.java
  56. 141 0
      seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/web/rest/UserInteractionController.java
  57. 30 0
      seecoder-bok-server/src/main/resources/application.yml
  58. 27 0
      seecoder-bok-server/src/test/java/cn/seecoder/seecoderbokserver/SeecoderBokServerApplicationTests.java
  59. 35 0
      seecoder-bok-server/src/test/java/cn/seecoder/seecoderbokserver/repository/InteractionRepositoryTest.java

Файловите разлики са ограничени, защото са твърде много
+ 45 - 0
.editorconfig


+ 13 - 0
Dockerfile

@@ -0,0 +1,13 @@
+FROM maven:3.6.1 AS compile_stage
+ENV PROJECT_NAME app
+ENV WORK_PATH /opt/$PROJECT_NAME
+COPY settings.xml /root/.m2/settings.xml
+COPY . $WORK_PATH
+RUN cd $WORK_PATH && mvn clean install -DskipTests -DfinalName=$PROJECT_NAME
+
+FROM java:8-jre-alpine
+ENV PROJECT_NAME app
+ENV WORK_PATH /opt/$PROJECT_NAME
+WORKDIR /app
+COPY --from=compile_stage $WORK_PATH/seecoder-bok-server/target/${PROJECT_NAME}.jar .
+CMD ["sh", "-c", "java -Xdebug -Xrunjdwp:transport=dt_socket,address=8888,server=y,suspend=n -jar -server /app/${PROJECT_NAME}.jar"]

+ 2 - 0
README.md

@@ -0,0 +1,2 @@
+# SEEC-BOK
+

+ 24 - 39
pom.xml

@@ -6,10 +6,11 @@
 
     <groupId>cn.seecoder</groupId>
     <artifactId>seecoder-bok</artifactId>
-    <version>0.0.2</version>
+    <version>0.1.0</version>
     <modules>
         <module>seecoder-bok-client</module>
         <module>seecoder-bok-common</module>
+        <module>seecoder-bok-server</module>
     </modules>
     <packaging>pom</packaging>
 
@@ -18,7 +19,8 @@
         <maven.compiler.target>8</maven.compiler.target>
 
         <jackson.version>2.12.0</jackson.version>
-        <slf4j.version>2.14.0</slf4j.version>
+        <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
+        <seecoder-bok-version>0.1.0</seecoder-bok-version>
     </properties>
 
     <dependencies>
@@ -28,20 +30,11 @@
             <artifactId>validation-api</artifactId>
             <version>2.0.1.Final</version>
         </dependency>
+        <!-- https://mvnrepository.com/artifact/org.jetbrains/annotations -->
         <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-api</artifactId>
-            <version>${slf4j.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-core</artifactId>
-            <version>${slf4j.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-slf4j-impl</artifactId>
-            <version>${slf4j.version}</version>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <version>20.1.0</version>
         </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
@@ -60,45 +53,37 @@
             <version>5.7.0</version>
             <scope>test</scope>
         </dependency>
+
     </dependencies>
 
     <dependencyManagement>
         <dependencies>
             <dependency>
-                <groupId>com.squareup.okhttp3</groupId>
-                <artifactId>okhttp</artifactId>
-                <version>4.9.0</version>
-            </dependency>
-            <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-commons -->
-            <dependency>
-                <groupId>org.springframework.data</groupId>
-                <artifactId>spring-data-commons</artifactId>
-                <version>2.3.6.RELEASE</version>
-            </dependency>
-            <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
-            <dependency>
-                <groupId>com.fasterxml.jackson.core</groupId>
-                <artifactId>jackson-databind</artifactId>
-                <version>${jackson.version}</version>
+                <groupId>cn.seecoder</groupId>
+                <artifactId>seecoder-bok-common</artifactId>
+                <version>${seecoder-bok-version}</version>
             </dependency>
             <dependency>
-                <groupId>com.fasterxml.jackson.module</groupId>
-                <artifactId>jackson-module-parameter-names</artifactId>
-                <version>${jackson.version}</version>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct</artifactId>
+                <version>${org.mapstruct.version}</version>
             </dependency>
             <dependency>
-                <groupId>com.fasterxml.jackson.datatype</groupId>
-                <artifactId>jackson-datatype-jdk8</artifactId>
-                <version>${jackson.version}</version>
+                <groupId>com.squareup.okhttp3</groupId>
+                <artifactId>okhttp</artifactId>
+                <version>4.9.0</version>
             </dependency>
             <dependency>
-                <groupId>com.fasterxml.jackson.datatype</groupId>
-                <artifactId>jackson-datatype-jsr310</artifactId>
-                <version>${jackson.version}</version>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <type>pom</type>
+                <version>2.4.2</version>
+                <scope>import</scope>
             </dependency>
         </dependencies>
     </dependencyManagement>
 
+
     <distributionManagement>
         <repository>
             <id>nexus</id>

+ 15 - 9
seecoder-bok-client/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>seecoder-bok</artifactId>
         <groupId>cn.seecoder</groupId>
-        <version>0.0.2</version>
+        <version>0.1.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -18,12 +18,12 @@
 
     <dependencies>
         <dependency>
-            <groupId>com.squareup.okhttp3</groupId>
-            <artifactId>okhttp</artifactId>
+            <groupId>cn.seecoder</groupId>
+            <artifactId>seecoder-bok-common</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.springframework.data</groupId>
-            <artifactId>spring-data-commons</artifactId>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
         </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
@@ -42,10 +42,16 @@
             <artifactId>jackson-datatype-jsr310</artifactId>
         </dependency>
         <dependency>
-            <groupId>cn.seecoder</groupId>
-            <artifactId>seecoder-bok-common</artifactId>
-            <version>0.0.2</version>
-            <scope>compile</scope>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-slf4j-impl</artifactId>
         </dependency>
     </dependencies>
 

+ 26 - 70
seecoder-bok-client/src/main/java/cn/seecoder/bok/client/SeecoderBokClient.java

@@ -1,7 +1,9 @@
 package cn.seecoder.bok.client;
 
+import cn.seecoder.bok.common.api.*;
 import cn.seecoder.bok.common.dto.CollectDTO;
 import cn.seecoder.bok.common.dto.RateDTO;
+import cn.seecoder.bok.common.utility.RestPage;
 import cn.seecoder.bok.common.vo.*;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
@@ -28,9 +30,9 @@ import java.util.function.Function;
 public class SeecoderBokClient
         implements SeecoderBokQuestionApi, SeecoderBokQuestionQueryApi,
         SeecoderBokRecordApi, SeecoderBokUserInteractiveApi,
-        SeecoderBokKnowledgeApi, SeecoderBokCodeQuestionApi {
+        SeecoderBokKnowledgeApi {
 
-    private final Logger logger = LoggerFactory.getLogger(getClass());
+    private final Logger logger = LoggerFactory.getLogger(SeecoderBokClient.class);
 
     private final OkHttpClient client;
     private final ObjectMapper mapper;
@@ -63,18 +65,17 @@ public class SeecoderBokClient
         HttpUrl httpUrl = createUrlBuilder("/api/knowledge")
                 .addQueryParameter("name", name).build();
         Response response = get(httpUrl);
-        return getResponseBody(response, new TypeReference<List<KnowledgeNodeVO>>() {
-        })
+        return getResponseBody(response, new TypeReference<List<KnowledgeNodeVO>>() {})
                 .orElse(Collections.emptyList());
     }
 
     @Override
     public Page<QuestionVO> getQuestionList(
-            String stem, String tag, String knowledgeId, Pageable pageable) {
+            String stem, List<String> tags, List<String> knowledgeIds, Pageable pageable) {
         HttpUrl.Builder urlBuilder = createUrlBuilder("/api/question")
                 .addQueryParameter("stem", stem)
-                .addQueryParameter("tag", tag)
-                .addQueryParameter("knowledgeId", knowledgeId);
+                .addQueryParameter("tag", joinComma(tags))
+                .addQueryParameter("knowledgeId", joinComma(knowledgeIds));
         return getQuestionVOS(pageable, urlBuilder);
     }
 
@@ -85,6 +86,14 @@ public class SeecoderBokClient
         return getResponseBody(response, QuestionVO.class).orElseThrow(() -> SeecoderBokException.UNWRAP_EXCEPTION);
     }
 
+    @Override
+    public List<QuestionVO> createQuestions(@NotNull List<QuestionVO> questionVOS) {
+        HttpUrl httpUrl = createUrlBuilder("/api/questions").build();
+        Response response = post(httpUrl, writeRequestBody(questionVOS));
+        return getResponseBody(response, new TypeReference<List<QuestionVO>>() {})
+                .orElseThrow(() -> SeecoderBokException.UNWRAP_EXCEPTION);
+    }
+
     @Override
     public Optional<QuestionVO> getQuestionById(@NotBlank String questionId) {
         HttpUrl httpUrl = createUrlBuilder(MessageFormat.format("/api/question/{0}", questionId)).build();
@@ -135,7 +144,7 @@ public class SeecoderBokClient
                 .addQueryParameter("recordId", joinComma(recordIdList));
         HttpUrl httpUrl = addPageableToParams(urlBuilder, pageable).build();
         Response response = get(httpUrl);
-        return getResponseBody(response, new TypeReference<Page<RecordVO>>() {})
+        return getResponseBody(response, new TypeReference<RestPage<RecordVO>>() {})
                 .map(recordVOS -> new PageImpl<>(
                         recordVOS.getContent(),
                         parseBokToPageable(recordVOS.getNumber(), recordVOS.getSize(), pageable),
@@ -144,19 +153,13 @@ public class SeecoderBokClient
     }
 
     @Override
-    public PageableRecordQuestionVO getUserRecordQuestions(
+    public RecordQuestionVO getUserRecordQuestions(
             @NotNull String userId, Pageable pageable) {
         HttpUrl.Builder urlBuilder = createUrlBuilder(MessageFormat.format("/api/record/{0}/question", userId));
         HttpUrl httpUrl = addPageableToParams(urlBuilder, pageable).build();
         Response response = get(httpUrl);
-        return getResponseBody(response, PageableRecordQuestionVO.class)
-                .map(pageableRecordQuestionVO -> {
-                    PageVO page = pageableRecordQuestionVO.getPage();
-                    page.setNumber(parseBokToPageableNumber(page.getNumber()));
-                    pageableRecordQuestionVO.setPage(page);
-                    return pageableRecordQuestionVO;
-                })
-                .orElse(PageableRecordQuestionVO.ofEmpty());
+        return getResponseBody(response, RecordQuestionVO.class)
+                .orElse(RecordQuestionVO.empty());
     }
 
     @Override
@@ -185,72 +188,29 @@ public class SeecoderBokClient
     }
 
     @Override
-    public Optional<InteractiveVO> getUserInteractiveQuestion(
+    public Optional<InteractionVO> getUserInteractiveQuestion(
             @NotBlank String userId, @NotBlank String questionId) {
         HttpUrl httpUrl = createUrlBuilder(
                 MessageFormat.format("/api/user/{0}/question/{1}", userId, questionId)).build();
         Response response = get(httpUrl);
-        return getResponseBody(response, InteractiveVO.class);
+        return getResponseBody(response, InteractionVO.class);
     }
 
     @Override
-    public List<InteractiveVO> getUserInteractiveQuestionIn(
+    public List<InteractionVO> getUserInteractiveQuestionIn(
             @NotBlank String userId, List<String> questionIdList) {
         HttpUrl httpUrl = createUrlBuilder(
                 MessageFormat.format("/api/user/{0}/questionInList", userId)).build();
         Response response = get(httpUrl);
-        return getResponseBody(response, new TypeReference<List<InteractiveVO>>() {})
+        return getResponseBody(response, new TypeReference<List<InteractionVO>>() {})
                 .orElse(Collections.emptyList());
     }
 
     private Page<QuestionVO> getQuestionVOS(Pageable pageable, HttpUrl.Builder urlBuilder) {
         HttpUrl httpUrl = addPageableToParams(urlBuilder, pageable).build();
         Response response = get(httpUrl);
-        return getResponseBody(response, PageableQuestionListVO.class)
-                .map(pageableQuestionListVO ->
-                        new PageImpl<>(
-                                pageableQuestionListVO.getQuestions(),
-                                parseBokToPageable(pageableQuestionListVO.getPage(), pageable),
-                                pageableQuestionListVO.getPage().getTotalElements()))
-                .orElse(new PageImpl<>(Collections.emptyList()));
-    }
-
-
-    @Override
-    public Page<CodeQuestionVO> getCodeQuestions(String title, Pageable pageable) {
-        HttpUrl.Builder urlBuilder = createUrlBuilder("/api/codeQuestion")
-                .addQueryParameter("title", title);
-        HttpUrl httpUrl = addPageableToParams(urlBuilder, pageable).build();
-        Response response = get(httpUrl);
-        return getResponseBody(response, new TypeReference<Page<CodeQuestionVO>>() {})
-                .map(codeQuestionVOS ->
-                        new PageImpl<>(
-                                codeQuestionVOS.getContent(),
-                                parseBokToPageable(codeQuestionVOS.getNumber(), codeQuestionVOS.getSize(), pageable),
-                                codeQuestionVOS.getTotalElements()))
-                .orElse(new PageImpl<>(Collections.emptyList()));
-    }
-
-    @Override
-    public Optional<CodeQuestionVO> getCodeQuestionById(@NotBlank String id) {
-        HttpUrl httpUrl = createUrlBuilder(MessageFormat.format("/api/codeQuestion/{0}", id)).build();
-        Response response = get(httpUrl);
-        return getResponseBody(response, CodeQuestionVO.class);
-    }
-
-    @Override
-    public CodeQuestionVO createCodeQuestion(@NotNull CodeQuestionVO codeQuestionVO) {
-        HttpUrl httpUrl = createUrlBuilder("/api/codeQuestion").build();
-        Response response = post(httpUrl, writeRequestBody(codeQuestionVO));
-        return getResponseBody(response, CodeQuestionVO.class).orElseThrow(() -> SeecoderBokException.UNWRAP_EXCEPTION);
-    }
-
-    @Override
-    public CodeQuestionVO updateCodeQuestion(@NotNull CodeQuestionVO codeQuestionVO) {
-        HttpUrl httpUrl = createUrlBuilder(
-                MessageFormat.format("/api/codeQuestion/{0}", codeQuestionVO.getId())).build();
-        Response response = put(httpUrl, writeRequestBody(codeQuestionVO));
-        return getResponseBody(response, CodeQuestionVO.class).orElseThrow(() -> SeecoderBokException.UNWRAP_EXCEPTION);
+        return getResponseBody(response, new TypeReference<RestPage<QuestionVO>>() {})
+                .orElse(new RestPage<>());
     }
 
     private Response get(HttpUrl httpUrl) {
@@ -343,10 +303,6 @@ public class SeecoderBokClient
         return bokPageNumber - SeecoderBokConstants.PAGE_START + CONFIG_PAGE_START;
     }
 
-    private Pageable parseBokToPageable(PageVO pageVO, Pageable pageable) {
-        return parseBokToPageable(pageVO.getNumber(), pageVO.getSize(), pageable);
-    }
-
     private Pageable parseBokToPageable(int page, int size, Pageable pageable) {
         if (pageable == null) {
             return PageRequest.of(parseBokToPageableNumber(page), size);

+ 17 - 0
seecoder-bok-client/src/test/java/cn/seecoder/bok/client/InteractionParseTest.java

@@ -0,0 +1,17 @@
+package cn.seecoder.bok.client;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+/**
+ * @author Kunduin
+ */
+public class InteractionParseTest {
+    @Test
+    void parseCsv() {
+
+    }
+}

+ 1 - 1
seecoder-bok-client/src/test/java/cn/seecoder/bok/client/SeecoderBokClientTest.java

@@ -21,7 +21,7 @@ import static org.junit.jupiter.api.Assertions.*;
 
 class SeecoderBokClientTest {
 
-    private final SeecoderBokClient seecoderBokClient = new SeecoderBokClient("http://bok.seecoder.cn");
+    private final SeecoderBokClient seecoderBokClient = new SeecoderBokClient("http://localhost:8080");
 
     @Test
     void getKnowledgeNode() {

+ 34 - 0
seecoder-bok-client/src/test/java/cn/seecoder/bok/common/QuestionVOMapperTest.java

@@ -0,0 +1,34 @@
+package cn.seecoder.bok.common;
+
+import cn.seecoder.bok.common.vo.QuestionVO;
+import cn.seecoder.bok.common.vo.question.BlankQuestionVO;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.Date;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author Kunduin
+ */
+class QuestionVOMapperTest {
+
+    @Test
+    void questionToBlankQuestion() {
+        QuestionVO questionVO = new QuestionVO();
+        questionVO.setBlanks(new HashMap<String, String>() {{put("__1__", "选项1");}});
+        BlankQuestionVO blankQuestionVO = QuestionVOMapper.INSTANCE.questionToBlankQuestion(questionVO);
+        assertEquals("选项1", blankQuestionVO.getBlanks().get("__1__"));
+    }
+
+    @Test
+    void timeTest() {
+        long time = 1583596392523L;
+        Instant date = Instant.ofEpochMilli(time);
+        System.out.println(date.atOffset(ZoneOffset.of("+8")));
+    }
+}

+ 49 - 3
seecoder-bok-common/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>seecoder-bok</artifactId>
         <groupId>cn.seecoder</groupId>
-        <version>0.0.2</version>
+        <version>0.1.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -16,8 +16,54 @@
         <maven.compiler.target>8</maven.compiler.target>
     </properties>
 
-    <dependencyManagement>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-commons</artifactId>
+        </dependency>
 
-    </dependencyManagement>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+        </dependency>
 
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.module</groupId>
+            <artifactId>jackson-module-parameter-names</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jdk8</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>${maven.compiler.source}</source> <!-- depending on your project -->
+                    <target>${maven.compiler.target}</target> <!-- depending on your project -->
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.mapstruct</groupId>
+                            <artifactId>mapstruct-processor</artifactId>
+                            <version>${org.mapstruct.version}</version>
+                        </path>
+                        <!-- other annotation processors -->
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
 </project>

+ 42 - 10
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/QuestionType.java

@@ -1,17 +1,49 @@
 package cn.seecoder.bok.common;
 
+import com.fasterxml.jackson.annotation.JsonValue;
+
 /**
  * @author Kunduin
  */
 public enum QuestionType {
-    /** 选择题 */
-    choice,
-    /** 判断题 */
-    true_false,
-    /** 多选题 */
-    multiple_choice,
-    /** 填空题 */
-    blank,
-    /** 简答题 */
-    short_answer
+    /**
+     * 选择题
+     */
+    CHOICE("choice"),
+
+    /**
+     * 判断题
+     */
+    TRUE_FALSE("true_false"),
+
+    /**
+     * 多选题
+     */
+    MULTIPLE_CHOICE("multiple_choice"),
+
+    /**
+     * 填空题
+     */
+    BLANK("blank"),
+
+    /**
+     * 简答题
+     */
+    SHORT_ANSWER("short_answer"),
+    
+    /**
+     * 代码题
+     */
+    CODE("code");
+
+    private final String name;
+
+    QuestionType(String name) {
+        this.name = name;
+    }
+
+    @JsonValue
+    public String getName() {
+        return name;
+    }
 }

+ 95 - 0
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/QuestionVOMapper.java

@@ -0,0 +1,95 @@
+package cn.seecoder.bok.common;
+
+import cn.seecoder.bok.common.vo.QuestionVO;
+import cn.seecoder.bok.common.vo.question.*;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @author Kunduin
+ */
+@Mapper
+public interface QuestionVOMapper {
+
+    QuestionVOMapper INSTANCE = Mappers.getMapper(QuestionVOMapper.class);
+
+    /**
+     * 题目 -> 填空题
+     *
+     * @param questionVO QuestionVO
+     * @return BlankQuestionVO
+     */
+    BlankQuestionVO questionToBlankQuestion(QuestionVO questionVO);
+
+    /**
+     * 题目 -> 选择题
+     *
+     * @param questionVO QuestionVO
+     * @return ChoiceQuestionVO
+     */
+    ChoiceQuestionVO questionToChoiceQuestion(QuestionVO questionVO);
+
+    /**
+     * 题目 -> 代码题
+     *
+     * @param questionVO QuestionVO
+     * @return CodeQuestionVO
+     */
+    CodeQuestionVO questionToCodeQuestion(QuestionVO questionVO);
+
+    /**
+     * 题目 -> 简答题
+     *
+     * @param questionVO QuestionVO
+     * @return ShortAnswerQuestionVO
+     */
+    ShortAnswerQuestionVO questionToShortAnswerQuestionVO(QuestionVO questionVO);
+
+    /**
+     * 题目 -> 判断题
+     *
+     * @param questionVO QuestionVO
+     * @return TrueFalseQuestionVO
+     */
+    TrueFalseQuestionVO questionToTrueFalseQuestionVO(QuestionVO questionVO);
+
+    /**
+     * 简答题 -> 题目
+     *
+     * @param blankQuestionVO BlankQuestionVO
+     * @return QuestionVO
+     */
+    QuestionVO blankQuestionToQuestion(BlankQuestionVO blankQuestionVO);
+
+    /**
+     * 选择题 -> 题目
+     *
+     * @param choiceQuestionVO ChoiceQuestionVO
+     * @return QuestionVO
+     */
+    QuestionVO choiceQuestionToQuestion(ChoiceQuestionVO choiceQuestionVO);
+
+    /**
+     * 代码题 -> 题目
+     *
+     * @param codeQuestionVO CodeQuestionVO
+     * @return QuestionVO
+     */
+    QuestionVO codeQuestionToQuestion(CodeQuestionVO codeQuestionVO);
+
+    /**
+     * 简答题 -> 题目
+     *
+     * @param shortAnswerQuestionVO ShortAnswerQuestionVO
+     * @return QuestionVO
+     */
+    QuestionVO shortAnswerQuestionToQuestion(ShortAnswerQuestionVO shortAnswerQuestionVO);
+
+    /**
+     * 判断题 -> 题目
+     *
+     * @param trueFalseQuestionVO TrueFalseQuestionVO
+     * @return QuestionVO
+     */
+    QuestionVO trueFalseQuestionToQuestion(TrueFalseQuestionVO trueFalseQuestionVO);
+}

+ 1 - 1
seecoder-bok-client/src/main/java/cn/seecoder/bok/client/SeecoderBokCodeQuestionApi.java → seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokCodeQuestionApi.java

@@ -1,4 +1,4 @@
-package cn.seecoder.bok.client;
+package cn.seecoder.bok.common.api;
 
 import cn.seecoder.bok.common.vo.CodeQuestionVO;
 import org.springframework.data.domain.Page;

+ 1 - 1
seecoder-bok-client/src/main/java/cn/seecoder/bok/client/SeecoderBokKnowledgeApi.java → seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokKnowledgeApi.java

@@ -1,4 +1,4 @@
-package cn.seecoder.bok.client;
+package cn.seecoder.bok.common.api;
 
 import cn.seecoder.bok.common.vo.KnowledgeNodeVO;
 

+ 16 - 7
seecoder-bok-client/src/main/java/cn/seecoder/bok/client/SeecoderBokQuestionApi.java → seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokQuestionApi.java

@@ -1,4 +1,4 @@
-package cn.seecoder.bok.client;
+package cn.seecoder.bok.common.api;
 
 import cn.seecoder.bok.common.vo.QuestionVO;
 import org.springframework.data.domain.Page;
@@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable;
 
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
+import java.util.List;
 import java.util.Optional;
 
 /**
@@ -16,14 +17,14 @@ public interface SeecoderBokQuestionApi {
     /**
      * 获得问题列表
      *
-     * @param stem        模糊字符串匹配
-     * @param tag         标签
-     * @param knowledgeId 知识点
-     * @param pageable    分页
+     * @param stem         模糊字符串匹配
+     * @param tags         标签
+     * @param knowledgeIds 知识点
+     * @param pageable     分页
      * @return 分页后的题目列表
      */
-    Page<QuestionVO> getQuestionList(String stem, String tag,
-                                     String knowledgeId, Pageable pageable);
+    Page<QuestionVO> getQuestionList(String stem, List<String> tags,
+                                     List<String> knowledgeIds, Pageable pageable);
 
     /**
      * 创建题目
@@ -33,6 +34,14 @@ public interface SeecoderBokQuestionApi {
      */
     QuestionVO createQuestion(@NotNull QuestionVO question);
 
+    /**
+     * 创建很多题目
+     *
+     * @param questionVOS 很多题目
+     * @return 创建结果
+     */
+    List<QuestionVO> createQuestions(@NotNull List<QuestionVO> questionVOS);
+
     /**
      * 根据题目id获取题目
      *

+ 1 - 1
seecoder-bok-client/src/main/java/cn/seecoder/bok/client/SeecoderBokQuestionQueryApi.java → seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokQuestionQueryApi.java

@@ -1,4 +1,4 @@
-package cn.seecoder.bok.client;
+package cn.seecoder.bok.common.api;
 
 import cn.seecoder.bok.common.vo.QuestionVO;
 

+ 2 - 3
seecoder-bok-client/src/main/java/cn/seecoder/bok/client/SeecoderBokRecordApi.java → seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokRecordApi.java

@@ -1,6 +1,5 @@
-package cn.seecoder.bok.client;
+package cn.seecoder.bok.common.api;
 
-import cn.seecoder.bok.common.vo.PageableRecordQuestionVO;
 import cn.seecoder.bok.common.vo.RecordQuestionVO;
 import cn.seecoder.bok.common.vo.RecordVO;
 import org.springframework.data.domain.Page;
@@ -41,6 +40,6 @@ public interface SeecoderBokRecordApi {
      * @param pageable 分页
      * @return 按照做题记录(Record)分页
      */
-    PageableRecordQuestionVO getUserRecordQuestions(@NotNull String userId, Pageable pageable);
+    RecordQuestionVO getUserRecordQuestions(@NotNull String userId, Pageable pageable);
 
 }

+ 4 - 4
seecoder-bok-client/src/main/java/cn/seecoder/bok/client/SeecoderBokUserInteractiveApi.java → seecoder-bok-common/src/main/java/cn/seecoder/bok/common/api/SeecoderBokUserInteractiveApi.java

@@ -1,8 +1,8 @@
-package cn.seecoder.bok.client;
+package cn.seecoder.bok.common.api;
 
 import cn.seecoder.bok.common.dto.CollectDTO;
 import cn.seecoder.bok.common.dto.RateDTO;
-import cn.seecoder.bok.common.vo.InteractiveVO;
+import cn.seecoder.bok.common.vo.InteractionVO;
 import cn.seecoder.bok.common.vo.QuestionVO;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -53,7 +53,7 @@ public interface SeecoderBokUserInteractiveApi {
      * @param questionId 题目
      * @return 交互信息
      */
-    Optional<InteractiveVO> getUserInteractiveQuestion(@NotBlank String userId, @NotBlank String questionId);
+    Optional<InteractionVO> getUserInteractiveQuestion(@NotBlank String userId, @NotBlank String questionId);
 
     /**
      * 获得某用户交互的题目列表
@@ -62,6 +62,6 @@ public interface SeecoderBokUserInteractiveApi {
      * @param questionIdList 题目列表
      * @return 交互列表
      */
-    List<InteractiveVO> getUserInteractiveQuestionIn(@NotBlank String userId, List<String> questionIdList);
+    List<InteractionVO> getUserInteractiveQuestionIn(@NotBlank String userId, List<String> questionIdList);
 
 }

+ 36 - 0
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/utility/RestPage.java

@@ -0,0 +1,36 @@
+package cn.seecoder.bok.common.utility;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.data.domain.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Kunduin
+ */
+@JsonIgnoreProperties(value = {"pageable", "sort"}, ignoreUnknown = true)
+public class RestPage<T> extends PageImpl<T> {
+
+    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+    public RestPage(@JsonProperty("content") List<T> content,
+                    @JsonProperty("number") int number,
+                    @JsonProperty("size") int size,
+                    @JsonProperty("totalElements") Long totalElements) {
+        super(content, PageRequest.of(number, size), totalElements);
+    }
+
+    public RestPage(List<T> content, Pageable pageable, long total) {
+        super(content, pageable, total);
+    }
+
+    public RestPage(List<T> content) {
+        super(content);
+    }
+
+    public RestPage() {
+        super(Collections.emptyList());
+    }
+}

+ 1 - 1
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/InteractiveVO.java → seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/InteractionVO.java

@@ -3,7 +3,7 @@ package cn.seecoder.bok.common.vo;
 /**
  * @author Kunduin
  */
-public class InteractiveVO {
+public class InteractionVO {
     private String userId;
     private String questionId;
     private Boolean collection;

+ 0 - 53
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/PageVO.java

@@ -1,53 +0,0 @@
-package cn.seecoder.bok.common.vo;
-
-/**
- * @author Kunduin
- */
-public class PageVO {
-    private Integer size;
-    private Long totalElements;
-    private Integer totalPages;
-    private Integer number;
-    
-    public Integer getSize() {
-        return size;
-    }
-
-    public void setSize(Integer size) {
-        this.size = size;
-    }
-
-    public Long getTotalElements() {
-        return totalElements;
-    }
-
-    public void setTotalElements(Long totalElements) {
-        this.totalElements = totalElements;
-    }
-
-    public Integer getTotalPages() {
-        return totalPages;
-    }
-
-    public void setTotalPages(Integer totalPages) {
-        this.totalPages = totalPages;
-    }
-
-    public Integer getNumber() {
-        return number;
-    }
-
-    public void setNumber(Integer number) {
-        this.number = number;
-    }
-
-    @Override
-    public String toString() {
-        return "PageVO{" +
-                "size=" + size +
-                ", totalElements=" + totalElements +
-                ", totalPages=" + totalPages +
-                ", number=" + number +
-                '}';
-    }
-}

+ 0 - 35
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/PageableQuestionListVO.java

@@ -1,35 +0,0 @@
-package cn.seecoder.bok.common.vo;
-
-import java.util.List;
-
-/**
- * @author Kunduin
- */
-public class PageableQuestionListVO {
-    private List<QuestionVO> questions;
-    private PageVO page;
-
-    public List<QuestionVO> getQuestions() {
-        return questions;
-    }
-
-    public void setQuestions(List<QuestionVO> questions) {
-        this.questions = questions;
-    }
-
-    public PageVO getPage() {
-        return page;
-    }
-
-    public void setPage(PageVO page) {
-        this.page = page;
-    }
-
-    @Override
-    public String toString() {
-        return "QuestionListVO{" +
-                "questions=" + questions +
-                ", page=" + page +
-                '}';
-    }
-}

+ 0 - 34
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/PageableRecordQuestionVO.java

@@ -1,34 +0,0 @@
-package cn.seecoder.bok.common.vo;
-
-import java.util.Collections;
-import java.util.List;
-
-/**
- * @author Kunduin
- */
-public class PageableRecordQuestionVO extends RecordQuestionVO {
-    private PageVO page;
-
-    public PageVO getPage() {
-        return page;
-    }
-
-    public void setPage(PageVO page) {
-        this.page = page;
-    }
-
-    public static PageableRecordQuestionVO ofEmpty() {
-        PageableRecordQuestionVO pageableRecordQuestionVO = new PageableRecordQuestionVO();
-        pageableRecordQuestionVO.setQuestions(Collections.emptyList());
-        pageableRecordQuestionVO.setRecords(Collections.emptyList());
-        pageableRecordQuestionVO.setPage(new PageVO());
-        return pageableRecordQuestionVO;
-    }
-
-    @Override
-    public String toString() {
-        return "PageableRecordQuestionVO{" +
-                "page=" + page +
-                "} " + super.toString();
-    }
-}

+ 51 - 70
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/QuestionVO.java

@@ -1,55 +1,48 @@
 package cn.seecoder.bok.common.vo;
 
-import cn.seecoder.bok.common.QuestionType;
+import cn.seecoder.bok.common.QuestionVOMapper;
+import cn.seecoder.bok.common.vo.question.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
 
-import java.util.Date;
-import java.util.List;
 import java.util.Map;
 
 /**
+ * 题目信息的超集,所有属性都包含的统一返回值。
+ * 如果使用具体的题目,请参照 {@link QuestionVOMapper}
+ *
  * @author Kunduin
  */
-public class QuestionVO {
-    private String id;
-    private QuestionType type;
-    private String stem;
-
+public class QuestionVO extends AbstractQuestionVO {
+    /**
+     * 选择题的选项 {@link ChoiceQuestionVO}
+     */
     private Map<String, String> options;
 
-    /** 填空题 __1__ __2__ */
+    /**
+     * 填空题的答案 {@link BlankQuestionVO}
+     */
     private Map<String, String> blanks;
 
+    /**
+     * 选择题 {@link ChoiceQuestionVO},判断题 {@link TrueFalseQuestionVO},
+     * 简答题 {@link ShortAnswerQuestionVO} 的答案
+     */
     private String answer;
-    private String keyPoints;
-    private String analysis;
-
-    private List<String> tags;
-    private List<String> knowledgeId;
-    private Date lastModifiedTime;
 
-    public String getId() {
-        return id;
-    }
+    /**
+     * 代码题 {@link CodeQuestionVO} 输入输出
+     */
+    private Map<String, String> inputOutputMapping;
 
-    public void setId(String id) {
-        this.id = id;
-    }
+    /**
+     * 代码题 {@link CodeQuestionVO} 时间限制
+     */
+    private Integer millisecondLimit;
 
-    public QuestionType getType() {
-        return type;
-    }
-
-    public void setType(QuestionType type) {
-        this.type = type;
-    }
-
-    public String getStem() {
-        return stem;
-    }
-
-    public void setStem(String stem) {
-        this.stem = stem;
-    }
+    /**
+     * 代码题 {@link CodeQuestionVO} 内存限制
+     */
+    private Integer memoryLimit;
 
     public Map<String, String> getOptions() {
         return options;
@@ -75,60 +68,48 @@ public class QuestionVO {
         this.answer = answer;
     }
 
-    public String getKeyPoints() {
-        return keyPoints;
-    }
-
-    public void setKeyPoints(String keyPoints) {
-        this.keyPoints = keyPoints;
+    public Map<String, String> getInputOutputMapping() {
+        return inputOutputMapping;
     }
 
-    public String getAnalysis() {
-        return analysis;
+    public void setInputOutputMapping(Map<String, String> inputOutputMapping) {
+        this.inputOutputMapping = inputOutputMapping;
     }
 
-    public void setAnalysis(String analysis) {
-        this.analysis = analysis;
+    public Integer getMillisecondLimit() {
+        return millisecondLimit;
     }
 
-    public List<String> getTags() {
-        return tags;
+    public void setMillisecondLimit(Integer millisecondLimit) {
+        this.millisecondLimit = millisecondLimit;
     }
 
-    public void setTags(List<String> tags) {
-        this.tags = tags;
+    public Integer getMemoryLimit() {
+        return memoryLimit;
     }
 
-    public List<String> getKnowledgeId() {
-        return knowledgeId;
-    }
-
-    public void setKnowledgeId(List<String> knowledgeId) {
-        this.knowledgeId = knowledgeId;
-    }
-
-    public Date getLastModifiedTime() {
-        return lastModifiedTime;
-    }
-
-    public void setLastModifiedTime(Date lastModifiedTime) {
-        this.lastModifiedTime = lastModifiedTime;
+    public void setMemoryLimit(Integer memoryLimit) {
+        this.memoryLimit = memoryLimit;
     }
 
     @Override
     public String toString() {
         return "QuestionVO{" +
-                "id='" + id + '\'' +
-                ", type=" + type +
-                ", stem='" + stem + '\'' +
-                ", options=" + options +
+                "options=" + options +
                 ", blanks=" + blanks +
                 ", answer='" + answer + '\'' +
-                ", keyPoints='" + keyPoints + '\'' +
+                ", inputOutputMapping=" + inputOutputMapping +
+                ", millisecondLimit=" + millisecondLimit +
+                ", memoryLimit=" + memoryLimit +
+                ", id='" + id + '\'' +
+                ", type=" + type +
+                ", title='" + title + '\'' +
+                ", stem='" + stem + '\'' +
                 ", analysis='" + analysis + '\'' +
+                ", keyPoints='" + keyPoints + '\'' +
                 ", tags=" + tags +
                 ", knowledgeId=" + knowledgeId +
                 ", lastModifiedTime=" + lastModifiedTime +
-                '}';
+                "} ";
     }
 }

+ 13 - 3
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/RecordQuestionVO.java

@@ -1,5 +1,8 @@
 package cn.seecoder.bok.common.vo;
 
+import org.springframework.data.domain.Page;
+
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -7,7 +10,7 @@ import java.util.List;
  */
 public class RecordQuestionVO {
     protected List<QuestionVO> questions;
-    protected List<RecordVO> records;
+    protected Page<RecordVO> records;
 
     public List<QuestionVO> getQuestions() {
         return questions;
@@ -17,14 +20,21 @@ public class RecordQuestionVO {
         this.questions = questions;
     }
 
-    public List<RecordVO> getRecords() {
+    public Page<RecordVO> getRecords() {
         return records;
     }
 
-    public void setRecords(List<RecordVO> records) {
+    public void setRecords(Page<RecordVO> records) {
         this.records = records;
     }
 
+    public static RecordQuestionVO empty() {
+        RecordQuestionVO recordQuestionVO = new RecordQuestionVO();
+        recordQuestionVO.setQuestions(Collections.emptyList());
+        recordQuestionVO.setRecords(Page.empty());
+        return recordQuestionVO;
+    }
+
     @Override
     public String toString() {
         return "RecordQuestionVO{" +

+ 27 - 6
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/RecordVO.java

@@ -1,6 +1,7 @@
 package cn.seecoder.bok.common.vo;
 
 import java.time.LocalDateTime;
+import java.time.Instant;
 
 /**
  * @author Kunduin
@@ -11,8 +12,10 @@ public class RecordVO {
     private String questionId;
     private String studentAnswer;
     private Boolean pass;
-    private LocalDateTime startAt;
-    private LocalDateTime finishAt;
+    private Instant startAt;
+    private Instant finishAt;
+    private Instant createdAt;
+    private Instant updatedAt;
 
     public Long getId() {
         return id;
@@ -54,22 +57,38 @@ public class RecordVO {
         this.pass = pass;
     }
 
-    public LocalDateTime getStartAt() {
+    public Instant getStartAt() {
         return startAt;
     }
 
-    public void setStartAt(LocalDateTime startAt) {
+    public void setStartAt(Instant startAt) {
         this.startAt = startAt;
     }
 
-    public LocalDateTime getFinishAt() {
+    public Instant getFinishAt() {
         return finishAt;
     }
 
-    public void setFinishAt(LocalDateTime finishAt) {
+    public void setFinishAt(Instant finishAt) {
         this.finishAt = finishAt;
     }
 
+    public Instant getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(Instant createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    public Instant getUpdatedAt() {
+        return updatedAt;
+    }
+
+    public void setUpdatedAt(Instant updatedAt) {
+        this.updatedAt = updatedAt;
+    }
+
     @Override
     public String toString() {
         return "RecordVO{" +
@@ -80,6 +99,8 @@ public class RecordVO {
                 ", pass=" + pass +
                 ", startAt=" + startAt +
                 ", finishAt=" + finishAt +
+                ", createdAt=" + createdAt +
+                ", updatedAt=" + updatedAt +
                 '}';
     }
 }

+ 157 - 0
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/AbstractQuestionVO.java

@@ -0,0 +1,157 @@
+package cn.seecoder.bok.common.vo.question;
+
+import cn.seecoder.bok.common.QuestionType;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.time.Instant;
+import java.util.List;
+
+/**
+ * 题目公共属性
+ *
+ * @author Kunduin
+ */
+public abstract class AbstractQuestionVO {
+    protected String id;
+
+    /**
+     * 题目类型
+     */
+    protected QuestionType type;
+
+    /**
+     * 简单的标题,旧版本未出现,是可选字段
+     */
+    protected String title;
+
+    /**
+     * 详细介绍,题干
+     */
+    protected String stem;
+
+    /**
+     * 题目解析
+     */
+    protected String analysis;
+
+    /**
+     * TODO: 关键字?
+     */
+    protected String keyPoints;
+
+    /**
+     * 标签
+     */
+    protected List<String> tags;
+
+    /**
+     * 知识图谱id
+     */
+    protected List<String> knowledgeId;
+
+    /**
+     * 创建时间
+     */
+    protected Instant createdTime;
+
+    /**
+     * 修改时间
+     */
+    protected Instant lastModifiedTime;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public QuestionType getType() {
+        return type;
+    }
+
+    public void setType(QuestionType type) {
+        this.type = type;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getStem() {
+        return stem;
+    }
+
+    public void setStem(String stem) {
+        this.stem = stem;
+    }
+
+    public String getAnalysis() {
+        return analysis;
+    }
+
+    public void setAnalysis(String analysis) {
+        this.analysis = analysis;
+    }
+
+    public String getKeyPoints() {
+        return keyPoints;
+    }
+
+    public void setKeyPoints(String keyPoints) {
+        this.keyPoints = keyPoints;
+    }
+
+    public List<String> getTags() {
+        return tags;
+    }
+
+    public void setTags(List<String> tags) {
+        this.tags = tags;
+    }
+
+    public List<String> getKnowledgeId() {
+        return knowledgeId;
+    }
+
+    public void setKnowledgeId(List<String> knowledgeId) {
+        this.knowledgeId = knowledgeId;
+    }
+
+    public Instant getCreatedTime() {
+        return createdTime;
+    }
+
+    public void setCreatedTime(Instant createdTime) {
+        this.createdTime = createdTime;
+    }
+
+    public Instant getLastModifiedTime() {
+        return lastModifiedTime;
+    }
+
+    public void setLastModifiedTime(Instant lastModifiedTime) {
+        this.lastModifiedTime = lastModifiedTime;
+    }
+
+    @Override
+    public String toString() {
+        return "AbstractQuestionVO{" +
+                "id='" + id + '\'' +
+                ", type=" + type +
+                ", title='" + title + '\'' +
+                ", stem='" + stem + '\'' +
+                ", analysis='" + analysis + '\'' +
+                ", keyPoints='" + keyPoints + '\'' +
+                ", tags=" + tags +
+                ", knowledgeId=" + knowledgeId +
+                ", createdTime=" + createdTime +
+                ", lastModifiedTime=" + lastModifiedTime +
+                '}';
+    }
+}

+ 43 - 0
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/BlankQuestionVO.java

@@ -0,0 +1,43 @@
+package cn.seecoder.bok.common.vo.question;
+
+import cn.seecoder.bok.common.QuestionType;
+
+import java.util.Map;
+
+/**
+ * 填空题
+ *
+ * @author Kunduin
+ */
+public class BlankQuestionVO extends AbstractQuestionVO {
+    /**
+     * 每个空应填的答案
+     * <p>
+     * 示例:<tt>{"__1__": "软件", "__2__": 设计}</tt>
+     */
+    private Map<String, String> blanks;
+
+    public Map<String, String> getBlanks() {
+        return blanks;
+    }
+
+    public void setBlanks(Map<String, String> blanks) {
+        this.blanks = blanks;
+    }
+
+    @Override
+    public String toString() {
+        return "BlankQuestionVO{" +
+                "id='" + id + '\'' +
+                ", type=" + type +
+                ", title='" + title + '\'' +
+                ", stem='" + stem + '\'' +
+                ", analysis='" + analysis + '\'' +
+                ", keyPoints='" + keyPoints + '\'' +
+                ", tags=" + tags +
+                ", knowledgeId=" + knowledgeId +
+                ", lastModifiedTime=" + lastModifiedTime +
+                ", blanks=" + blanks +
+                "} ";
+    }
+}

+ 59 - 0
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/ChoiceQuestionVO.java

@@ -0,0 +1,59 @@
+package cn.seecoder.bok.common.vo.question;
+
+import java.util.Map;
+
+/**
+ * 单选或多选
+ *
+ * @author Kunduin
+ */
+public class ChoiceQuestionVO extends AbstractQuestionVO {
+    /**
+     * 选项
+     * <p>
+     * 示例:<tt>{"A": "选项一", "B":"选项二"}</tt>
+     */
+    private Map<String, String> options;
+
+    /**
+     * 答案
+     * <p>
+     * 单选题:<tt>"A"</tt>
+     * <p>
+     * 多选题:<tt>"A;B"</tt>
+     */
+    private String answer;
+
+    public Map<String, String> getOptions() {
+        return options;
+    }
+
+    public void setOptions(Map<String, String> options) {
+        this.options = options;
+    }
+
+    public String getAnswer() {
+        return answer;
+    }
+
+    public void setAnswer(String answer) {
+        this.answer = answer;
+    }
+
+    @Override
+    public String toString() {
+        return "ChoiceQuestionVO{" +
+                "id='" + id + '\'' +
+                ", type=" + type +
+                ", title='" + title + '\'' +
+                ", stem='" + stem + '\'' +
+                ", analysis='" + analysis + '\'' +
+                ", keyPoints='" + keyPoints + '\'' +
+                ", tags=" + tags +
+                ", knowledgeId=" + knowledgeId +
+                ", lastModifiedTime=" + lastModifiedTime +
+                ", options=" + options +
+                ", answer='" + answer + '\'' +
+                "} ";
+    }
+}

+ 68 - 0
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/CodeQuestionVO.java

@@ -0,0 +1,68 @@
+package cn.seecoder.bok.common.vo.question;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 代码题
+ *
+ * @author Kunduin
+ */
+public class CodeQuestionVO extends AbstractQuestionVO {
+    /**
+     * 输入输出
+     */
+    private Map<String, String> inputOutputMapping;
+
+    /**
+     * 时间限制
+     */
+    private Integer millisecondLimit;
+
+    /**
+     * 内存限制
+     */
+    private Integer memoryLimit;
+
+    public Map<String, String> getInputOutputMapping() {
+        return inputOutputMapping;
+    }
+
+    public void setInputOutputMapping(Map<String, String> inputOutputMapping) {
+        this.inputOutputMapping = inputOutputMapping;
+    }
+
+    public Integer getMillisecondLimit() {
+        return millisecondLimit;
+    }
+
+    public void setMillisecondLimit(Integer millisecondLimit) {
+        this.millisecondLimit = millisecondLimit;
+    }
+
+    public Integer getMemoryLimit() {
+        return memoryLimit;
+    }
+
+    public void setMemoryLimit(Integer memoryLimit) {
+        this.memoryLimit = memoryLimit;
+    }
+
+    @Override
+    public String toString() {
+        return "CodeQuestionVO{" +
+                "id='" + id + '\'' +
+                ", type=" + type +
+                ", title='" + title + '\'' +
+                ", stem='" + stem + '\'' +
+                ", analysis='" + analysis + '\'' +
+                ", keyPoints='" + keyPoints + '\'' +
+                ", tags=" + tags +
+                ", knowledgeId=" + knowledgeId +
+                ", lastModifiedTime=" + lastModifiedTime +
+                ", inputOutputMapping=" + inputOutputMapping +
+                ", millisecondLimit=" + millisecondLimit +
+                ", memoryLimit=" + memoryLimit +
+                "} ";
+    }
+}

+ 39 - 0
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/ShortAnswerQuestionVO.java

@@ -0,0 +1,39 @@
+package cn.seecoder.bok.common.vo.question;
+
+import cn.seecoder.bok.common.vo.question.AbstractQuestionVO;
+
+/**
+ * 简答题
+ *
+ * @author Kunduin
+ */
+public class ShortAnswerQuestionVO extends AbstractQuestionVO {
+    /**
+     * 简答题答案
+     */
+    private String answer;
+
+    public String getAnswer() {
+        return answer;
+    }
+
+    public void setAnswer(String answer) {
+        this.answer = answer;
+    }
+
+    @Override
+    public String toString() {
+        return "ShortAnswerQuestionVO{" +
+                "id='" + id + '\'' +
+                ", type=" + type +
+                ", title='" + title + '\'' +
+                ", stem='" + stem + '\'' +
+                ", analysis='" + analysis + '\'' +
+                ", keyPoints='" + keyPoints + '\'' +
+                ", tags=" + tags +
+                ", knowledgeId=" + knowledgeId +
+                ", lastModifiedTime=" + lastModifiedTime +
+                ", answer='" + answer + '\'' +
+                "} ";
+    }
+}

+ 37 - 0
seecoder-bok-common/src/main/java/cn/seecoder/bok/common/vo/question/TrueFalseQuestionVO.java

@@ -0,0 +1,37 @@
+package cn.seecoder.bok.common.vo.question;
+
+/**
+ * 判断题
+ *
+ * @author Kunduin
+ */
+public class TrueFalseQuestionVO extends AbstractQuestionVO {
+    /**
+     * 判断题答案 <tt>"true" | "false"</tt>
+     */
+    private String answer;
+
+    public String getAnswer() {
+        return answer;
+    }
+
+    public void setAnswer(String answer) {
+        this.answer = answer;
+    }
+
+    @Override
+    public String toString() {
+        return "TrueFalseQuestionVO{" +
+                "id='" + id + '\'' +
+                ", type=" + type +
+                ", title='" + title + '\'' +
+                ", stem='" + stem + '\'' +
+                ", analysis='" + analysis + '\'' +
+                ", keyPoints='" + keyPoints + '\'' +
+                ", tags=" + tags +
+                ", knowledgeId=" + knowledgeId +
+                ", lastModifiedTime=" + lastModifiedTime +
+                ", answer='" + answer + '\'' +
+                "} ";
+    }
+}

+ 117 - 0
seecoder-bok-server/.mvn/wrapper/MavenWrapperDownloader.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+    private static final String WRAPPER_VERSION = "0.5.6";
+    /**
+     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+     */
+    private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+        + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+    /**
+     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+     * use instead of the default one.
+     */
+    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+            ".mvn/wrapper/maven-wrapper.properties";
+
+    /**
+     * Path where the maven-wrapper.jar will be saved to.
+     */
+    private static final String MAVEN_WRAPPER_JAR_PATH =
+            ".mvn/wrapper/maven-wrapper.jar";
+
+    /**
+     * Name of the property which should be used to override the default download url for the wrapper.
+     */
+    private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+    public static void main(String args[]) {
+        System.out.println("- Downloader started");
+        File baseDirectory = new File(args[0]);
+        System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+        // If the maven-wrapper.properties exists, read it and check if it contains a custom
+        // wrapperUrl parameter.
+        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+        String url = DEFAULT_DOWNLOAD_URL;
+        if(mavenWrapperPropertyFile.exists()) {
+            FileInputStream mavenWrapperPropertyFileInputStream = null;
+            try {
+                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+                Properties mavenWrapperProperties = new Properties();
+                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+            } catch (IOException e) {
+                System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+            } finally {
+                try {
+                    if(mavenWrapperPropertyFileInputStream != null) {
+                        mavenWrapperPropertyFileInputStream.close();
+                    }
+                } catch (IOException e) {
+                    // Ignore ...
+                }
+            }
+        }
+        System.out.println("- Downloading from: " + url);
+
+        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+        if(!outputFile.getParentFile().exists()) {
+            if(!outputFile.getParentFile().mkdirs()) {
+                System.out.println(
+                        "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+            }
+        }
+        System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+        try {
+            downloadFileFromURL(url, outputFile);
+            System.out.println("Done");
+            System.exit(0);
+        } catch (Throwable e) {
+            System.out.println("- Error downloading");
+            e.printStackTrace();
+            System.exit(1);
+        }
+    }
+
+    private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+        if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+            String username = System.getenv("MVNW_USERNAME");
+            char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+            Authenticator.setDefault(new Authenticator() {
+                @Override
+                protected PasswordAuthentication getPasswordAuthentication() {
+                    return new PasswordAuthentication(username, password);
+                }
+            });
+        }
+        URL website = new URL(urlString);
+        ReadableByteChannel rbc;
+        rbc = Channels.newChannel(website.openStream());
+        FileOutputStream fos = new FileOutputStream(destination);
+        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+        fos.close();
+        rbc.close();
+    }
+
+}

BIN
seecoder-bok-server/.mvn/wrapper/maven-wrapper.jar


+ 2 - 0
seecoder-bok-server/.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar

+ 310 - 0
seecoder-bok-server/mvnw

@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`which java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 182 - 0
seecoder-bok-server/mvnw.cmd

@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%

+ 94 - 0
seecoder-bok-server/pom.xml

@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>seecoder-bok</artifactId>
+        <groupId>cn.seecoder</groupId>
+        <version>0.1.0</version>
+    </parent>
+    <artifactId>seecoder-bok-server</artifactId>
+    <version>0.1.0</version>
+    <name>seecoder-bok-server</name>
+    <description>Demo project for Spring Boot</description>
+    <properties>
+        <java.version>1.8</java.version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>${maven.compiler.source}</source> <!-- depending on your project -->
+                    <target>${maven.compiler.target}</target> <!-- depending on your project -->
+
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.mapstruct</groupId>
+                            <artifactId>mapstruct-processor</artifactId>
+                            <version>${org.mapstruct.version}</version>
+                        </path>
+                        <!-- other annotation processors -->
+                        <path>
+                            <groupId>org.hibernate</groupId>
+                            <artifactId>hibernate-jpamodelgen</artifactId>
+                            <version>5.4.28.Final</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.seecoder</groupId>
+            <artifactId>seecoder-bok-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <scope>runtime</scope>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 18 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/SeecoderBokServerApplication.java

@@ -0,0 +1,18 @@
+package cn.seecoder.seecoderbokserver;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+/**
+ * @author Kunduin
+ */
+@SpringBootApplication
+@EnableConfigurationProperties
+public class SeecoderBokServerApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(SeecoderBokServerApplication.class, args);
+    }
+
+}

+ 63 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/config/ElasticConfig.java

@@ -0,0 +1,63 @@
+package cn.seecoder.seecoderbokserver.config;
+
+import cn.seecoder.bok.common.QuestionType;
+import org.elasticsearch.client.RestClientBuilder;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.ReadingConverter;
+import org.springframework.data.convert.WritingConverter;
+import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;
+import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
+
+import java.util.Arrays;
+
+/**
+ * @author Kunduin
+ */
+@Configuration
+public class ElasticConfig extends AbstractElasticsearchConfiguration {
+
+    private final RestClientBuilder restClientBuilder;
+
+    public ElasticConfig(RestClientBuilder restClientBuilder) {this.restClientBuilder = restClientBuilder;}
+
+    @Override
+    public @NotNull RestHighLevelClient elasticsearchClient() {
+        return new RestHighLevelClient(restClientBuilder);
+    }
+
+    @Bean
+    @Override
+    public @NotNull ElasticsearchCustomConversions elasticsearchCustomConversions() {
+        return new ElasticsearchCustomConversions(Arrays.asList(
+                new QuestionTypeToStringConverter(),
+                new StringToQuestionTypeConverter()
+        ));
+    }
+
+    @WritingConverter
+    public static class QuestionTypeToStringConverter implements Converter<QuestionType, String> {
+
+        @Override
+        public String convert(QuestionType source) {
+            return source.getName();
+        }
+    }
+
+    @ReadingConverter
+    public static class StringToQuestionTypeConverter implements Converter<String, QuestionType> {
+
+        @Override
+        public QuestionType convert(@NotNull String source) {
+            for (QuestionType type : QuestionType.values()) {
+                if (type.getName().equals(source)) {
+                    return type;
+                }
+            }
+            return null;
+        }
+    }
+}

+ 109 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/domain/Interaction.java

@@ -0,0 +1,109 @@
+package cn.seecoder.seecoderbokserver.domain;
+
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+/**
+ * @author Kunduin
+ */
+@Entity
+@Table(name = "interaction", indexes = {
+        @Index(name = "interaction__user_id__question_id", columnList = "user_id, question_id")
+})
+public class Interaction {
+    @Id
+    @GeneratedValue
+    private Long id;
+
+    @Column(name = "user_id", nullable = false)
+    private String userId;
+
+    @Column(name = "question_id", nullable = false)
+    private String questionId;
+
+    @Column(name = "collection", nullable = false)
+    private Boolean collection = false;
+
+    @Column(name = "rate")
+    private Integer rate;
+
+    @Column(name = "created_at")
+    @CreationTimestamp
+    private Instant createdAt;
+
+    @Column(name = "updated_at")
+    @UpdateTimestamp
+    private Instant updatedAt;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(String questionId) {
+        this.questionId = questionId;
+    }
+
+    public Boolean getCollection() {
+        return collection;
+    }
+
+    public void setCollection(Boolean collection) {
+        this.collection = collection;
+    }
+
+    public Integer getRate() {
+        return rate;
+    }
+
+    public void setRate(Integer rate) {
+        this.rate = rate;
+    }
+
+    public Instant getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(Instant createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    public Instant getUpdatedAt() {
+        return updatedAt;
+    }
+
+    public void setUpdatedAt(Instant updatedAt) {
+        this.updatedAt = updatedAt;
+    }
+
+    @Override
+    public String toString() {
+        return "Interaction{" +
+                "id=" + id +
+                ", userId='" + userId + '\'' +
+                ", questionId='" + questionId + '\'' +
+                ", collection=" + collection +
+                ", rate=" + rate +
+                ", createdAt=" + createdAt +
+                ", updatedAt=" + updatedAt +
+                '}';
+    }
+}

+ 223 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/domain/Question.java

@@ -0,0 +1,223 @@
+package cn.seecoder.seecoderbokserver.domain;
+
+import cn.seecoder.bok.common.QuestionType;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.annotation.TypeAlias;
+import org.springframework.data.elasticsearch.annotations.DateFormat;
+import org.springframework.data.elasticsearch.annotations.Document;
+import org.springframework.data.elasticsearch.annotations.Field;
+import org.springframework.data.elasticsearch.annotations.FieldType;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Kunduin
+ */
+@Document(indexName = "question")
+@TypeAlias("question")
+public class Question {
+    @Id
+    @Field(name = "id", type = FieldType.Keyword)
+    private String id;
+
+    @Field(name = "type", type = FieldType.Keyword)
+    private QuestionType type;
+
+    @Field(name = "title", type = FieldType.Text)
+    private String title;
+
+    @Field(name = "stem", type = FieldType.Text)
+    private String stem;
+
+    @Field(name = "analysis", type = FieldType.Text)
+    private String analysis;
+
+    @Field(name = "key_points", type = FieldType.Text)
+    private String keyPoints;
+
+    @Field(name = "tags", type = FieldType.Text)
+    private List<String> tags;
+
+    @Field(name = "knowledge_id", type = FieldType.Text)
+    private List<String> knowledgeId;
+
+    @Field(name = "options", type = FieldType.Object)
+    private Map<String, String> options;
+
+    @Field(name = "blanks", type = FieldType.Object)
+    private Map<String, String> blanks;
+
+    @Field(name = "answer", type = FieldType.Text)
+    private String answer;
+
+    @Field(name = "input_output_mapping", type = FieldType.Object, index = false)
+    private Map<String, String> inputOutputMapping;
+
+    @Field(name = "millisecond_limit", type = FieldType.Integer)
+    private Integer millisecondLimit;
+
+    @Field(name = "memory_limit", type = FieldType.Integer)
+    private Integer memoryLimit;
+
+    @CreatedDate
+    @Field(name = "created_at", type = FieldType.Date)
+    private Instant createdTime;
+
+    @LastModifiedDate
+    @Field(name = "updated_at", type = FieldType.Date, format = DateFormat.basic_date_time)
+    private Instant lastModifiedTime;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public QuestionType getType() {
+        return type;
+    }
+
+    public void setType(QuestionType type) {
+        this.type = type;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getStem() {
+        return stem;
+    }
+
+    public void setStem(String stem) {
+        this.stem = stem;
+    }
+
+    public String getAnalysis() {
+        return analysis;
+    }
+
+    public void setAnalysis(String analysis) {
+        this.analysis = analysis;
+    }
+
+    public String getKeyPoints() {
+        return keyPoints;
+    }
+
+    public void setKeyPoints(String keyPoints) {
+        this.keyPoints = keyPoints;
+    }
+
+    public List<String> getTags() {
+        return tags;
+    }
+
+    public void setTags(List<String> tags) {
+        this.tags = tags;
+    }
+
+    public List<String> getKnowledgeId() {
+        return knowledgeId;
+    }
+
+    public void setKnowledgeId(List<String> knowledgeId) {
+        this.knowledgeId = knowledgeId;
+    }
+
+    public Map<String, String> getOptions() {
+        return options;
+    }
+
+    public void setOptions(Map<String, String> options) {
+        this.options = options;
+    }
+
+    public Map<String, String> getBlanks() {
+        return blanks;
+    }
+
+    public void setBlanks(Map<String, String> blanks) {
+        this.blanks = blanks;
+    }
+
+    public String getAnswer() {
+        return answer;
+    }
+
+    public void setAnswer(String answer) {
+        this.answer = answer;
+    }
+
+    public Map<String, String> getInputOutputMapping() {
+        return inputOutputMapping;
+    }
+
+    public void setInputOutputMapping(Map<String, String> inputOutputMapping) {
+        this.inputOutputMapping = inputOutputMapping;
+    }
+
+    public Integer getMillisecondLimit() {
+        return millisecondLimit;
+    }
+
+    public void setMillisecondLimit(Integer millisecondLimit) {
+        this.millisecondLimit = millisecondLimit;
+    }
+
+    public Integer getMemoryLimit() {
+        return memoryLimit;
+    }
+
+    public void setMemoryLimit(Integer memoryLimit) {
+        this.memoryLimit = memoryLimit;
+    }
+
+    public Instant getLastModifiedTime() {
+        return lastModifiedTime;
+    }
+
+    public void setLastModifiedTime(Instant lastModifiedTime) {
+        this.lastModifiedTime = lastModifiedTime;
+    }
+
+    public Instant getCreatedTime() {
+        return createdTime;
+    }
+
+    public void setCreatedTime(Instant createdTime) {
+        this.createdTime = createdTime;
+    }
+
+    @Override
+    public String toString() {
+        return "Question{" +
+                "id='" + id + '\'' +
+                ", type=" + type +
+                ", title='" + title + '\'' +
+                ", stem='" + stem + '\'' +
+                ", analysis='" + analysis + '\'' +
+                ", keyPoints='" + keyPoints + '\'' +
+                ", tags=" + tags +
+                ", knowledgeId=" + knowledgeId +
+                ", options=" + options +
+                ", blanks=" + blanks +
+                ", answer='" + answer + '\'' +
+                ", inputOutputMapping=" + inputOutputMapping +
+                ", millisecondLimit=" + millisecondLimit +
+                ", memoryLimit=" + memoryLimit +
+                ", createdTime=" + createdTime +
+                ", lastModifiedTime=" + lastModifiedTime +
+                '}';
+    }
+}

+ 134 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/domain/Record.java

@@ -0,0 +1,134 @@
+package cn.seecoder.seecoderbokserver.domain;
+
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.time.Instant;
+
+/**
+ * @author Kunduin
+ */
+@Entity
+@Table(name = "record", indexes = {
+        @Index(name = "record__user_id__question_id", columnList = "user_id, question_id")
+})
+public class Record {
+    @Id
+    @GeneratedValue
+    private Long id;
+
+    @Column(name = "user_id")
+    private String userId;
+
+    @Column(name = "question_id")
+    private String questionId;
+
+    @Column(name = "student_answer")
+    private String studentAnswer;
+
+    @Column(name = "pass")
+    private Boolean pass;
+
+    @Column(name = "start_at")
+    private Instant startAt;
+
+    @Column(name = "finish_at")
+    private Instant finishAt;
+
+    @Column(name = "created_at")
+    @CreationTimestamp
+    private Instant createdAt;
+
+    @Column(name = "updated_at")
+    @UpdateTimestamp
+    private Instant updatedAt;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(String questionId) {
+        this.questionId = questionId;
+    }
+
+    public String getStudentAnswer() {
+        return studentAnswer;
+    }
+
+    public void setStudentAnswer(String studentAnswer) {
+        this.studentAnswer = studentAnswer;
+    }
+
+    public Boolean getPass() {
+        return pass;
+    }
+
+    public void setPass(Boolean pass) {
+        this.pass = pass;
+    }
+
+    public Instant getStartAt() {
+        return startAt;
+    }
+
+    public void setStartAt(Instant startAt) {
+        this.startAt = startAt;
+    }
+
+    public Instant getFinishAt() {
+        return finishAt;
+    }
+
+    public void setFinishAt(Instant finishAt) {
+        this.finishAt = finishAt;
+    }
+
+    public Instant getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(Instant createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    public Instant getUpdatedAt() {
+        return updatedAt;
+    }
+
+    public void setUpdatedAt(Instant updatedAt) {
+        this.updatedAt = updatedAt;
+    }
+
+    @Override
+    public String toString() {
+        return "Record{" +
+                "id=" + id +
+                ", userId='" + userId + '\'' +
+                ", questionId='" + questionId + '\'' +
+                ", studentAnswer='" + studentAnswer + '\'' +
+                ", pass=" + pass +
+                ", startAt=" + startAt +
+                ", finishAt=" + finishAt +
+                ", createdAt=" + createdAt +
+                ", updatedAt=" + updatedAt +
+                '}';
+    }
+}

+ 27 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/mapper/InteractionMapper.java

@@ -0,0 +1,27 @@
+package cn.seecoder.seecoderbokserver.mapper;
+
+import cn.seecoder.bok.common.vo.InteractionVO;
+import cn.seecoder.seecoderbokserver.domain.Interaction;
+import org.mapstruct.Mapper;
+
+/**
+ * @author Kunduin
+ */
+@Mapper(componentModel = "spring")
+public interface InteractionMapper {
+    /**
+     * domain -> vo
+     *
+     * @param interaction Interaction
+     * @return InteractionVO
+     */
+    InteractionVO interactionToInteractionVO(Interaction interaction);
+
+    /**
+     * vo -> domain
+     *
+     * @param interactionVO InteractionVO
+     * @return Interaction
+     */
+    Interaction interactionVOToInteraction(InteractionVO interactionVO);
+}

+ 28 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/mapper/QuestionMapper.java

@@ -0,0 +1,28 @@
+package cn.seecoder.seecoderbokserver.mapper;
+
+import cn.seecoder.bok.common.vo.QuestionVO;
+import cn.seecoder.seecoderbokserver.domain.Question;
+import org.mapstruct.Mapper;
+
+/**
+ * @author Kunduin
+ */
+@Mapper(componentModel = "spring")
+public interface QuestionMapper {
+
+    /**
+     * domain -> vo
+     *
+     * @param question Question
+     * @return QuestionVO
+     */
+    QuestionVO questionToQuestionVO(Question question);
+
+    /**
+     * vo -> domain
+     *
+     * @param questionVO QuestionVO
+     * @return Question
+     */
+    Question questionVOTOQuestion(QuestionVO questionVO);
+}

+ 28 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/mapper/RecordMapper.java

@@ -0,0 +1,28 @@
+package cn.seecoder.seecoderbokserver.mapper;
+
+import cn.seecoder.bok.common.vo.RecordVO;
+import cn.seecoder.seecoderbokserver.domain.Record;
+import org.mapstruct.Mapper;
+
+/**
+ * @author Kunduin
+ */
+@Mapper(componentModel = "spring")
+public interface RecordMapper {
+
+    /**
+     * domain -> vo
+     *
+     * @param record Record
+     * @return RecordVO
+     */
+    RecordVO recordToRecordVO(Record record);
+
+    /**
+     * vo -> domain
+     *
+     * @param recordVO RecordVO
+     * @return Record
+     */
+    Record recordVOToRecord(RecordVO recordVO);
+}

+ 60 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/repository/InteractionRepository.java

@@ -0,0 +1,60 @@
+package cn.seecoder.seecoderbokserver.repository;
+
+import cn.seecoder.seecoderbokserver.domain.Interaction;
+import cn.seecoder.seecoderbokserver.domain.Interaction_;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+import javax.persistence.criteria.Path;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Kunduin
+ */
+public interface InteractionRepository extends JpaRepository<Interaction, Long>, JpaSpecificationExecutor<Interaction> {
+    /**
+     * 找到用户对题目的收藏
+     *
+     * @param userId     用户
+     * @param questionId 题目
+     * @return 交互信息
+     */
+    Optional<Interaction> findByUserIdAndQuestionId(String userId, String questionId);
+
+    default Specification<Interaction> hasRated(Boolean rated) {
+        return (root, query, cb) -> {
+            Path<Integer> ratePath = root.get(Interaction_.rate);
+            if (Boolean.TRUE.equals(rated)) {
+                return ratePath.isNotNull();
+            } else if (Boolean.FALSE.equals(rated)) {
+                return ratePath.isNull();
+            } else {
+                return null;
+            }
+        };
+    }
+
+    default Specification<Interaction> hasCollected(Boolean collected) {
+        return (root, query, cb) -> {
+            Path<Boolean> collectionPath = root.get(Interaction_.collection);
+            if (Boolean.TRUE.equals(collected)) {
+                return collectionPath.in(true);
+            } else if (Boolean.FALSE.equals(collected)) {
+                return collectionPath.in(false);
+            } else {
+                return null;
+            }
+        };
+    }
+
+    default Specification<Interaction> byUser(String userId) {
+        return (root, query, cb) -> root.get(Interaction_.userId).in(userId);
+    }
+
+    default Specification<Interaction> inQuestions(List<String> questionIds) {
+        return (root, query, cb) -> questionIds == null ? null
+                : root.get(Interaction_.questionId).in(questionIds);
+    }
+}

+ 11 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/repository/QuestionRepository.java

@@ -0,0 +1,11 @@
+package cn.seecoder.seecoderbokserver.repository;
+
+import cn.seecoder.seecoderbokserver.domain.Question;
+import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
+
+
+/**
+ * @author Kunduin
+ */
+public interface QuestionRepository extends ElasticsearchRepository<Question, String> {
+}

+ 61 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/repository/RecordRepository.java

@@ -0,0 +1,61 @@
+package cn.seecoder.seecoderbokserver.repository;
+
+import cn.seecoder.seecoderbokserver.domain.Record;
+import cn.seecoder.seecoderbokserver.domain.Record_;
+import cn.seecoder.seecoderbokserver.utility.EmptyCheck;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+import javax.persistence.criteria.Path;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Kunduin
+ */
+public interface RecordRepository extends JpaRepository<Record, Long>, JpaSpecificationExecutor<Record> {
+    /**
+     * 获得某个用户的交互数据
+     *
+     * @param userId   用户id
+     * @param pageable 分页
+     * @return 分页交互数据
+     */
+    Page<Record> findAllByUserId(String userId, Pageable pageable);
+
+    /**
+     * 在用户中
+     *
+     * @param userIds 用户id列表
+     * @return specification
+     */
+    default Specification<Record> inUsers(Collection<String> userIds) {
+        return (root, query, cb) -> EmptyCheck.instance.isEmpty(userIds) ? null
+                : root.get(Record_.userId).in(userIds);
+    }
+
+    /**
+     * 在题目中
+     *
+     * @param questionIds 题目id列表
+     * @return specification
+     */
+    default Specification<Record> inQuestions(Collection<String> questionIds) {
+        return (root, query, cb) -> EmptyCheck.instance.isEmpty(questionIds) ? null
+                : root.get(Record_.questionId).in(questionIds);
+    }
+
+    /**
+     * 在记录中
+     *
+     * @param recordIds 记录列表
+     * @return specification
+     */
+    default Specification<Record> inRecords(Collection<String> recordIds) {
+        return (root, query, cb) -> EmptyCheck.instance.isEmpty(recordIds) ? null
+                : root.get(Record_.id).in(recordIds);
+    }
+}

+ 86 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/service/QuestionService.java

@@ -0,0 +1,86 @@
+package cn.seecoder.seecoderbokserver.service;
+
+import cn.seecoder.seecoderbokserver.domain.Question;
+import cn.seecoder.seecoderbokserver.repository.QuestionRepository;
+import cn.seecoder.seecoderbokserver.utility.EmptyCheck;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.springframework.data.domain.*;
+import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.SearchHit;
+import org.springframework.data.elasticsearch.core.SearchHits;
+import org.springframework.data.elasticsearch.core.SearchOperations;
+import org.springframework.data.elasticsearch.core.query.Criteria;
+import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
+import org.springframework.data.elasticsearch.core.query.Query;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * @author Kunduin
+ */
+@Service
+public class QuestionService {
+    private final QuestionRepository questionRepository;
+    private final ElasticsearchOperations elasticsearchOperations;
+
+    public QuestionService(QuestionRepository questionRepository, ElasticsearchOperations elasticsearchOperations) {
+        this.questionRepository = questionRepository;
+        this.elasticsearchOperations = elasticsearchOperations;
+    }
+
+    public Optional<Question> findById(String id) {
+        return questionRepository.findById(id);
+    }
+
+    public Page<Question> find(String stem, List<String> tags, List<String> knowledgeIds, Pageable pageable) {
+        return completeQuery(stem, tags, knowledgeIds, pageable);
+    }
+
+    public Iterable<Question> findAllById(Iterable<String> ids) {
+        return questionRepository.findAllById(ids);
+    }
+
+    public Question save(Question question) {
+        if (question.getId() == null) {
+            question.setId(UUID.randomUUID().toString());
+        }
+        return questionRepository.save(question);
+    }
+
+    public Iterable<Question> saveAll(Iterable<Question> questions) {
+        return questionRepository.saveAll(questions);
+    }
+
+    public void delete(String id) {
+        questionRepository.deleteById(id);
+    }
+
+    public Page<Question> completeQuery(@Nullable String stem, @Nullable List<String> tags,
+                                        @Nullable List<String> knowledgeId, Pageable pageable) {
+        Criteria criteria = new Criteria();
+        if (!EmptyCheck.instance.isEmpty(stem)) { criteria = criteria.and("stem").matches(stem); }
+        if (!EmptyCheck.instance.isEmpty(tags)) { criteria = criteria.and("tags").matchesAll(String.join(",", tags)); }
+        if (!EmptyCheck.instance.isEmpty(knowledgeId)) {
+            criteria = criteria.and("knowledge_id").matchesAll(String.join(",", knowledgeId));
+        }
+
+        Query query = new CriteriaQuery(criteria);
+        Sort nextSort = Sort.by(pageable.getSort()
+                .map(order -> "id".equals(order.getProperty())
+                        ? new Sort.Order(order.getDirection(), "id.keyword")
+                        : order)
+                .toList());
+        query.setPageable(PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), nextSort));
+        SearchHits<Question> questionSearchHits = elasticsearchOperations.search(query, Question.class);
+        List<Question> questions = questionSearchHits.stream()
+                .map(SearchHit::getContent)
+                .collect(Collectors.toList());
+        return new PageImpl<>(questions, pageable, questionSearchHits.getTotalHits());
+    }
+}

+ 42 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/service/RecordService.java

@@ -0,0 +1,42 @@
+package cn.seecoder.seecoderbokserver.service;
+
+import cn.seecoder.seecoderbokserver.domain.Record;
+import cn.seecoder.seecoderbokserver.repository.RecordRepository;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+import static org.springframework.data.jpa.domain.Specification.where;
+
+/**
+ * @author Kunduin
+ */
+@Service
+public class RecordService {
+    private final RecordRepository recordRepository;
+
+    public RecordService(RecordRepository recordRepository) {
+        this.recordRepository = recordRepository;
+    }
+
+    public List<Record> saveAll(Iterable<Record> records) {
+        return recordRepository.saveAll(records);
+    }
+
+    public Page<Record> findRecordByUidAndQidAndRidIn(
+            List<String> userIdList,
+            List<String> questionIdList,
+            List<String> recordIdList,
+            Pageable of) {
+        return recordRepository.findAll(
+                where(recordRepository.inRecords(recordIdList))
+                        .and(recordRepository.inUsers(userIdList))
+                        .and(recordRepository.inQuestions(questionIdList)), of);
+    }
+
+    public Page<Record> findByUserId(String userId, Pageable pageable) {
+        return recordRepository.findAllByUserId(userId, pageable);
+    }
+}

+ 95 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/service/UserInteractionService.java

@@ -0,0 +1,95 @@
+package cn.seecoder.seecoderbokserver.service;
+
+import cn.seecoder.bok.common.dto.CollectDTO;
+import cn.seecoder.bok.common.dto.RateDTO;
+import cn.seecoder.seecoderbokserver.domain.Interaction;
+import cn.seecoder.seecoderbokserver.domain.Question;
+import cn.seecoder.seecoderbokserver.repository.InteractionRepository;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static org.springframework.data.jpa.domain.Specification.where;
+
+/**
+ * @author Kunduin
+ */
+@Service
+@Transactional(rollbackOn = RuntimeException.class)
+public class UserInteractionService {
+
+    private final InteractionRepository interactionRepository;
+    private final QuestionService questionService;
+
+    public UserInteractionService(InteractionRepository collectionInteractionRepository, QuestionService questionService) {
+        this.interactionRepository = collectionInteractionRepository;
+        this.questionService = questionService;
+    }
+
+    public Interaction collectQuestion(String userId, String questionId, CollectDTO collectDTO) {
+        return interactionRepository.findByUserIdAndQuestionId(userId, questionId)
+                .map(interaction -> {
+                    interaction.setCollection(collectDTO.getCollect());
+                    return interactionRepository.save(interaction);
+                })
+                .orElseGet(() -> {
+                    Interaction interaction = new Interaction();
+                    interaction.setUserId(userId);
+                    interaction.setQuestionId(questionId);
+                    interaction.setCollection(collectDTO.getCollect());
+
+                    return interactionRepository.save(interaction);
+                });
+    }
+
+    public Interaction rateQuestion(String userId, String questionId, RateDTO rateDTO) {
+        return interactionRepository.findByUserIdAndQuestionId(userId, questionId)
+                .map(interaction -> {
+                    interaction.setRate(rateDTO.getRate());
+                    return interactionRepository.save(interaction);
+                })
+                .orElseGet(() -> {
+                    Interaction interaction = new Interaction();
+                    interaction.setUserId(userId);
+                    interaction.setQuestionId(questionId);
+                    interaction.setRate(rateDTO.getRate());
+
+                    return interactionRepository.save(interaction);
+                });
+    }
+
+    public Page<Question> getUserInteractiveQuestions(String userId, Boolean collect, Boolean rate, Pageable pageable) {
+        Page<Interaction> interactions = interactionRepository.findAll(
+                where(interactionRepository.byUser(userId))
+                        .and(interactionRepository.hasCollected(collect))
+                        .and(interactionRepository.hasRated(rate)), pageable);
+        List<String> questionIds = interactions.getContent().stream()
+                .map(Interaction::getQuestionId).collect(Collectors.toList());
+        Iterable<Question> questions = questionService.findAllById(questionIds);
+        List<Question> questionList = StreamSupport.stream(questions.spliterator(), false)
+                .collect(Collectors.toList());
+        return new PageImpl<>(questionList, pageable, interactions.getTotalElements());
+    }
+
+    public Optional<Interaction> getUserInteractiveQuestion(String userId, String questionId) {
+        return interactionRepository.findByUserIdAndQuestionId(userId, questionId);
+    }
+
+    public List<Interaction> getUserInteractiveQuestionIn(String userId, List<String> questionIdList) {
+        return interactionRepository.findAll(where(interactionRepository.byUser(userId))
+                .and(interactionRepository.inQuestions(questionIdList)));
+    }
+
+    @Transactional(rollbackOn = Exception.class)
+    public List<Interaction> saveAll(Collection<Interaction> interactions) {
+        return interactionRepository.saveAll(interactions);
+    }
+}

+ 22 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/utility/EmptyCheck.java

@@ -0,0 +1,22 @@
+package cn.seecoder.seecoderbokserver.utility;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Kunduin
+ */
+public enum EmptyCheck {
+    /**
+     * 单例
+     */
+    instance;
+
+    public <T> boolean isEmpty(Collection<T> list) {
+        return list == null || list.isEmpty();
+    }
+
+    public boolean isEmpty(String s) {
+        return s == null || s.isEmpty();
+    }
+}

+ 94 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/web/rest/QuestionController.java

@@ -0,0 +1,94 @@
+package cn.seecoder.seecoderbokserver.web.rest;
+
+import cn.seecoder.bok.common.api.SeecoderBokQuestionApi;
+import cn.seecoder.bok.common.api.SeecoderBokQuestionQueryApi;
+import cn.seecoder.bok.common.vo.QuestionVO;
+import cn.seecoder.seecoderbokserver.domain.Question;
+import cn.seecoder.seecoderbokserver.mapper.QuestionMapper;
+import cn.seecoder.seecoderbokserver.service.QuestionService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/**
+ * @author Kunduin
+ */
+@RestController
+@RequestMapping("/api")
+public class QuestionController implements SeecoderBokQuestionApi, SeecoderBokQuestionQueryApi {
+
+    private final QuestionService questionService;
+    private final QuestionMapper questionMapper;
+
+    public QuestionController(QuestionService questionService, QuestionMapper questionMapper) {
+        this.questionService = questionService;
+        this.questionMapper = questionMapper;
+    }
+
+    @Override
+    @GetMapping("/question")
+    public Page<QuestionVO> getQuestionList(
+            @RequestParam(name = "stem", required = false) String stem,
+            @RequestParam(name = "tag", required = false) List<String> tags,
+            @RequestParam(name = "knowledgeId", required = false) List<String> knowledgeIds,
+            Pageable pageable) {
+        return questionService.find(stem, tags, knowledgeIds, pageable)
+                .map(questionMapper::questionToQuestionVO);
+    }
+
+    @Override
+    @PostMapping("/question")
+    public QuestionVO createQuestion(@NotNull @RequestBody QuestionVO question) {
+        Question rawQuestion = questionMapper.questionVOTOQuestion(question);
+        return questionMapper.questionToQuestionVO(
+                questionService.save(rawQuestion));
+    }
+
+    @Override
+    @PostMapping("/questions")
+    public List<QuestionVO> createQuestions(@NotNull @RequestBody List<QuestionVO> questionVOS) {
+        List<Question> questions = questionVOS.stream().map(questionMapper::questionVOTOQuestion)
+                .collect(Collectors.toList());
+        return StreamSupport.stream(questionService.saveAll(questions).spliterator(), false)
+                .map(questionMapper::questionToQuestionVO).collect(Collectors.toList());
+    }
+
+    @Override
+    @GetMapping("/question/{id}")
+    public Optional<QuestionVO> getQuestionById(
+            @NotBlank @PathVariable(name = "id") String questionId) {
+        return questionService.findById(questionId)
+                .map(questionMapper::questionToQuestionVO);
+    }
+
+    @Override
+    @PutMapping("/question/{id}")
+    public QuestionVO updateQuestion(
+            @NotNull @RequestBody QuestionVO question) {
+        Question rawQuestion = questionMapper.questionVOTOQuestion(question);
+        return questionMapper.questionToQuestionVO(
+                questionService.save(rawQuestion));
+    }
+
+    @Override
+    @DeleteMapping("/question/{id}")
+    public void deleteQuestionById(
+            @NotBlank @PathVariable(name = "id") String questionId) {
+        questionService.delete(questionId);
+    }
+
+    @Override
+    @GetMapping("/question-query/id")
+    public List<QuestionVO> getQuestionListByIds(@NotNull @RequestParam("id") List<String> ids) {
+        return StreamSupport.stream(questionService.findAllById(ids).spliterator(), false)
+                .map(questionMapper::questionToQuestionVO).collect(Collectors.toList());
+    }
+}

+ 78 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/web/rest/RecordController.java

@@ -0,0 +1,78 @@
+package cn.seecoder.seecoderbokserver.web.rest;
+
+import cn.seecoder.bok.common.api.SeecoderBokRecordApi;
+import cn.seecoder.bok.common.vo.RecordQuestionVO;
+import cn.seecoder.bok.common.vo.RecordVO;
+import cn.seecoder.seecoderbokserver.domain.Question;
+import cn.seecoder.seecoderbokserver.domain.Record;
+import cn.seecoder.seecoderbokserver.mapper.QuestionMapper;
+import cn.seecoder.seecoderbokserver.mapper.RecordMapper;
+import cn.seecoder.seecoderbokserver.service.QuestionService;
+import cn.seecoder.seecoderbokserver.service.RecordService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/**
+ * @author Kunduin
+ */
+@RestController
+@RequestMapping("/api/record")
+public class RecordController implements SeecoderBokRecordApi {
+
+    private final RecordService recordService;
+    private final QuestionService questionService;
+    private final RecordMapper recordMapper;
+    private final QuestionMapper questionMapper;
+
+    public RecordController(RecordService recordService, QuestionService questionService, RecordMapper recordMapper, QuestionMapper questionMapper) {
+        this.recordService = recordService;
+        this.questionService = questionService;
+        this.recordMapper = recordMapper;
+        this.questionMapper = questionMapper;
+    }
+
+    @Override
+    @PostMapping("")
+    public List<RecordVO> createUserRecord(@RequestBody @NotNull List<RecordVO> records) {
+        List<Record> recordDomains = records.stream()
+                .map(recordMapper::recordVOToRecord)
+                .collect(Collectors.toList());
+        return recordService.saveAll(recordDomains).stream()
+                .map(recordMapper::recordToRecordVO)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    @GetMapping("")
+    public Page<RecordVO> getUserRecord(
+            @RequestParam(value = "userId", required = false) List<String> userIdList,
+            @RequestParam(value = "questionId", required = false) List<String> questionIdList,
+            @RequestParam(value = "recordId", required = false) List<String> recordIdList,
+            Pageable pageable) {
+        return recordService.findRecordByUidAndQidAndRidIn(userIdList, questionIdList, recordIdList, pageable)
+                .map(recordMapper::recordToRecordVO);
+    }
+
+    @Override
+    @GetMapping("/{userId}/question")
+    public RecordQuestionVO getUserRecordQuestions(
+            @PathVariable(name = "userId") @NotNull String userId,
+            Pageable pageable) {
+        Page<Record> records = recordService.findByUserId(userId, pageable);
+        Iterable<Question> questions = questionService.findAllById(records.getContent().stream()
+                .map(Record::getQuestionId).collect(Collectors.toList()));
+        RecordQuestionVO recordQuestionVO = new RecordQuestionVO();
+        recordQuestionVO.setQuestions(
+                StreamSupport.stream(questions.spliterator(), false)
+                        .map(questionMapper::questionToQuestionVO)
+                        .collect(Collectors.toList()));
+        recordQuestionVO.setRecords(records.map(recordMapper::recordToRecordVO));
+        return recordQuestionVO;
+    }
+}

+ 141 - 0
seecoder-bok-server/src/main/java/cn/seecoder/seecoderbokserver/web/rest/UserInteractionController.java

@@ -0,0 +1,141 @@
+package cn.seecoder.seecoderbokserver.web.rest;
+
+import cn.seecoder.bok.common.api.SeecoderBokUserInteractiveApi;
+import cn.seecoder.bok.common.dto.CollectDTO;
+import cn.seecoder.bok.common.dto.RateDTO;
+import cn.seecoder.bok.common.vo.InteractionVO;
+import cn.seecoder.bok.common.vo.QuestionVO;
+import cn.seecoder.seecoderbokserver.domain.Interaction;
+import cn.seecoder.seecoderbokserver.mapper.InteractionMapper;
+import cn.seecoder.seecoderbokserver.mapper.QuestionMapper;
+import cn.seecoder.seecoderbokserver.service.UserInteractionService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author Kunduin
+ */
+@RestController
+@RequestMapping("/api/user")
+public class UserInteractionController implements SeecoderBokUserInteractiveApi {
+
+    private final UserInteractionService interactionService;
+    private final QuestionMapper questionMapper;
+    private final InteractionMapper interactionMapper;
+
+    public UserInteractionController(UserInteractionService interactionService, QuestionMapper questionMapper, InteractionMapper interactionMapper) {
+        this.interactionService = interactionService;
+        this.questionMapper = questionMapper;
+        this.interactionMapper = interactionMapper;
+    }
+
+    @Override
+    @PostMapping("/{userId}/question/{questionId}/collection")
+    public void collectQuestion(
+            @PathVariable("userId") @Valid @NotBlank String userId,
+            @PathVariable("questionId") @Valid @NotBlank String questionId,
+            @RequestBody @Valid @NotNull CollectDTO collectDTO) {
+        interactionService.collectQuestion(userId, questionId, collectDTO);
+    }
+
+    @Override
+    @PostMapping("/{userId}/question/{questionId}/rate")
+    public void rateQuestion(
+            @PathVariable("userId") @Valid @NotBlank String userId,
+            @PathVariable("questionId") @Valid @NotBlank String questionId,
+            @RequestBody @Valid @NotNull RateDTO rateDTO) {
+        interactionService.rateQuestion(userId, questionId, rateDTO);
+    }
+
+    @Override
+    @GetMapping("/{userId}/question")
+    public Page<QuestionVO> getUserInteractiveQuestions(
+            @PathVariable("userId") @Valid @NotBlank String userId,
+            @RequestParam(value = "collection", required = false) Boolean collect,
+            @RequestParam(value = "rate", required = false) Boolean rate,
+            Pageable pageable) {
+        return interactionService.getUserInteractiveQuestions(userId, collect, rate, pageable)
+                .map(questionMapper::questionToQuestionVO);
+    }
+
+    @Override
+    @GetMapping("/{userId}/question/{questionId}")
+    public Optional<InteractionVO> getUserInteractiveQuestion(
+            @PathVariable("userId") @Valid @NotBlank String userId,
+            @PathVariable("questionId") @Valid @NotBlank String questionId) {
+        return interactionService.getUserInteractiveQuestion(userId, questionId)
+                .map(interactionMapper::interactionToInteractionVO);
+    }
+
+    @Override
+    @GetMapping("/{userId}/questionInList")
+    public List<InteractionVO> getUserInteractiveQuestionIn(
+            @PathVariable("userId") @Valid @NotBlank String userId,
+            @RequestParam(value = "questionId", required = false) List<String> questionIdList) {
+        return interactionService.getUserInteractiveQuestionIn(userId, questionIdList)
+                .stream().map(interactionMapper::interactionToInteractionVO).collect(Collectors.toList());
+    }
+
+    @GetMapping("/init")
+    public void init() {
+
+        List<String> collectRaw = null;
+        List<String> rateRaw = null;
+        try {
+            collectRaw = Files.readAllLines(Paths.get("C:\\Users\\Kunduin\\Downloads\\interactive_collect.csv"));
+            rateRaw = Files.readAllLines(Paths.get("C:\\Users\\Kunduin\\Downloads\\interactive_rate.csv"));
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        assert collectRaw != null;
+        List<String> collectLines = collectRaw.subList(1, collectRaw.size());
+        assert rateRaw != null;
+        List<String> rateLines = rateRaw.subList(1, rateRaw.size());
+
+        Function<String, String> removeQuotation = raw -> raw.substring(1, raw.length() - 1);
+        List<Interaction> collectionInteractions = collectLines.stream().map(raw -> {
+            String[] items = raw.split(",");
+            Interaction interaction = new Interaction();
+            interaction.setQuestionId(removeQuotation.apply(items[0]));
+            interaction.setUserId(removeQuotation.apply(items[1]));
+            interaction.setCollection(Boolean.parseBoolean(removeQuotation.apply(items[2])));
+            return interaction;
+        }).collect(Collectors.toList());
+
+        List<Interaction> ratingWithCollectionInteractions = rateLines.stream().map(raw -> {
+            String[] items = raw.split(",");
+            Interaction ratingInteractions = new Interaction();
+            ratingInteractions.setQuestionId(removeQuotation.apply(items[0]));
+            ratingInteractions.setUserId(removeQuotation.apply(items[1]));
+            ratingInteractions.setRate(Integer.parseInt(removeQuotation.apply(items[2])));
+
+            collectionInteractions.stream()
+                    .filter(interaction -> interaction.getQuestionId().equals(ratingInteractions.getQuestionId())
+                            && interaction.getUserId().equals(ratingInteractions.getUserId()))
+                    .findFirst().ifPresent(interaction -> ratingInteractions.setCollection(interaction.getCollection()));
+            return ratingInteractions;
+        }).collect(Collectors.toList());
+
+        List<Interaction> collectResult = collectionInteractions.stream().filter(interaction -> {
+            long size = ratingWithCollectionInteractions.parallelStream()
+                    .filter(rateInteraction -> interaction.getUserId().equals(rateInteraction.getUserId())
+                            && interaction.getQuestionId().equals(rateInteraction.getQuestionId()))
+                    .count();
+            return size == 0;
+        }).collect(Collectors.toList());
+        ratingWithCollectionInteractions.addAll(collectResult);
+        interactionService.saveAll(ratingWithCollectionInteractions);
+    }
+}

+ 30 - 0
seecoder-bok-server/src/main/resources/application.yml

@@ -0,0 +1,30 @@
+spring:
+  jpa:
+    database: postgresql
+    open-in-view: false
+    show-sql: true
+    hibernate:
+      ddl-auto: update
+  datasource:
+    type: com.zaxxer.hikari.HikariDataSource
+    url: jdbc:postgresql://192.168.254.129:5432/bok
+    username: postgres
+    password: 12345678qwerQWER
+  elasticsearch:
+    rest:
+      uris: http://192.168.254.129:9200
+      username: elastic
+      password: 12345678qwerQWER
+
+  jackson:
+    serialization:
+      WRITE_DATES_AS_TIMESTAMPS: false
+
+debug: true
+logging:
+  level:
+    tracer: trace
+    org:
+      elasticsearch:
+        client:
+          RestClient: trace

+ 27 - 0
seecoder-bok-server/src/test/java/cn/seecoder/seecoderbokserver/SeecoderBokServerApplicationTests.java

@@ -0,0 +1,27 @@
+package cn.seecoder.seecoderbokserver;
+
+import cn.seecoder.seecoderbokserver.domain.Interaction;
+import cn.seecoder.seecoderbokserver.repository.InteractionRepository;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@SpringBootTest(classes = {InteractionRepository.class})
+class SeecoderBokServerApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}

+ 35 - 0
seecoder-bok-server/src/test/java/cn/seecoder/seecoderbokserver/repository/InteractionRepositoryTest.java

@@ -0,0 +1,35 @@
+package cn.seecoder.seecoderbokserver.repository;
+
+import cn.seecoder.seecoderbokserver.domain.Interaction;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author Kunduin
+ */
+
+@DataJpaTest
+@Transactional(propagation = Propagation.NOT_SUPPORTED)
+class InteractionRepositoryTest {
+
+    @Autowired
+    private InteractionRepository interactionRepository;
+
+
+    @Test
+    void readInteractions() throws IOException {
+    }
+}

Някои файлове не бяха показани, защото твърде много файлове са промени