#3 创建作业合并到主分支。

Scalone
ZhangYi scala 8 commity/ów z SEEC/codeAssignment do SEEC/master 7 lat temu
50 zmienionych plików z 1288 dodań i 36 usunięć
  1. BIN
      problem/64-three-sum.zip
  2. BIN
      problem/66-two-sum.zip
  3. 57 0
      src/main/java/nju/seec/SEECdemo/data/convertor/ScoreDetailSetConverter.java
  4. 15 0
      src/main/java/nju/seec/SEECdemo/data/dao/assignment/CodeDAO.java
  5. 13 0
      src/main/java/nju/seec/SEECdemo/data/dao/assignment/CodeResultDAO.java
  6. 72 0
      src/main/java/nju/seec/SEECdemo/data/entity/assignment/Code.java
  7. 60 0
      src/main/java/nju/seec/SEECdemo/data/entity/assignment/CodeResult.java
  8. 12 0
      src/main/java/nju/seec/SEECdemo/logic/api/GitlabApi.java
  9. 4 0
      src/main/java/nju/seec/SEECdemo/logic/api/JenkinsApi.java
  10. 29 0
      src/main/java/nju/seec/SEECdemo/logic/api/impl/GitlabApiImpl.java
  11. 11 0
      src/main/java/nju/seec/SEECdemo/logic/api/impl/JenkinsApiImpl.java
  12. 10 0
      src/main/java/nju/seec/SEECdemo/logic/api/vo/GitlabGroup.java
  13. 0 7
      src/main/java/nju/seec/SEECdemo/logic/service/AssignmentService.java
  14. 19 0
      src/main/java/nju/seec/SEECdemo/logic/service/CodeService.java
  15. 0 4
      src/main/java/nju/seec/SEECdemo/logic/service/impl/AssignmentServiceImpl.java
  16. 133 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/AssignmentPaperBuilder.java
  17. 139 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/CodeServiceImpl.java
  18. 50 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/GitHelper.java
  19. 16 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/mock/MockCourse.java
  20. 24 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/mock/MockGroup.java
  21. 18 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/mock/MockQuestion.java
  22. 25 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/mock/MockUser.java
  23. 20 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/stub/CourseServiceStub.java
  24. 28 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/stub/GroupServiceStub.java
  25. 20 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/stub/QuestionServiceStub.java
  26. 28 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/stub/UserServiceStub.java
  27. 12 0
      src/main/java/nju/seec/SEECdemo/logic/storage/StorageProvider.java
  28. 32 0
      src/main/java/nju/seec/SEECdemo/logic/storage/StorageProviderConfiguration.java
  29. 43 0
      src/main/java/nju/seec/SEECdemo/logic/storage/impl/LocalStorageProvider.java
  30. 0 7
      src/main/java/nju/seec/SEECdemo/logic/vo/AssignmentVO.java
  31. 54 0
      src/main/java/nju/seec/SEECdemo/logic/vo/CodeWorkVO.java
  32. 1 1
      src/main/java/nju/seec/SEECdemo/logic/vo/InterCodeAppInfoVO.java
  33. 3 0
      src/main/java/nju/seec/SEECdemo/util/ApplicationProperties.java
  34. 29 0
      src/main/java/nju/seec/SEECdemo/util/AsyncWrapper.java
  35. 35 0
      src/main/java/nju/seec/SEECdemo/util/ExecutorConfiguration.java
  36. 6 0
      src/main/java/nju/seec/SEECdemo/util/Method.java
  37. 30 0
      src/main/java/nju/seec/SEECdemo/util/enums/AssignmentStatus.java
  38. 12 1
      src/main/java/nju/seec/SEECdemo/util/enums/AssignmentType.java
  39. 23 0
      src/main/java/nju/seec/SEECdemo/util/enums/UserRole.java
  40. 32 0
      src/main/java/nju/seec/SEECdemo/util/serializer/TimestampLocalDateDeserializer.java
  41. 29 0
      src/main/java/nju/seec/SEECdemo/util/serializer/TimestampLocalDateTimeSerializer.java
  42. 0 9
      src/main/java/nju/seec/SEECdemo/web/controller/AssignmentController.java
  43. 28 0
      src/main/java/nju/seec/SEECdemo/web/controller/CodeController.java
  44. 0 7
      src/main/java/nju/seec/SEECdemo/web/dto/AssignmentDTO.java
  45. 26 0
      src/main/java/nju/seec/SEECdemo/web/dto/CodeWorkDTO.java
  46. 18 0
      src/main/java/nju/seec/SEECdemo/web/response/EmptyResponse.java
  47. 17 0
      src/main/java/nju/seec/SEECdemo/web/response/ErrorResponse.java
  48. 33 0
      src/main/java/nju/seec/SEECdemo/web/response/PageResourceResponse.java
  49. 16 0
      src/main/java/nju/seec/SEECdemo/web/response/ResourceResponse.java
  50. 6 0
      src/main/java/nju/seec/SEECdemo/web/response/Response.java

BIN
problem/64-three-sum.zip


BIN
problem/66-two-sum.zip


+ 57 - 0
src/main/java/nju/seec/SEECdemo/data/convertor/ScoreDetailSetConverter.java

@@ -0,0 +1,57 @@
+package nju.seec.SEECdemo.data.convertor;
+
+import nju.seec.SEECdemo.data.entity.assignment.CodeResult.Score;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import javax.persistence.AttributeConverter;
+import java.util.HashSet;
+import java.util.Set;
+
+public class ScoreDetailSetConverter implements AttributeConverter<Set<Score>, String> {
+	@Override
+	public String convertToDatabaseColumn(Set<Score> attribute) {
+		return convertSetToJsonArrayString(attribute);
+	}
+
+	private String convertSetToJsonArrayString(Set<Score> scoreSet){
+		JSONArray array = new JSONArray();
+		if(scoreSet.size()!=0){
+			for (Score score : scoreSet) {
+				JSONObject object = new JSONObject();
+				object.put("id", score.getId());
+				object.put("name", score.getName());
+				object.put("score", score.getScore());
+				object.put("detail", convertSetToJsonArrayString(score.getDetail()));
+				array.put(object);
+			}
+		}
+		return array.toString();
+	}
+
+	@Override
+	public Set<Score> convertToEntityAttribute(String dbData) {
+		return convertJsonArrayStringToSet(dbData);
+	}
+
+	private Set<Score> convertJsonArrayStringToSet(String jsonArrayString){
+		Set<Score> scores = new HashSet<>();
+		JSONArray array = new JSONArray(jsonArrayString);
+		if(array.length()!=0){
+			for (Object object : array) {
+				if (object instanceof JSONObject) {
+					try {
+						Score score = new Score();
+						score.setId(((JSONObject) object).getInt("id"));
+						score.setName(((JSONObject) object).getString("name"));
+						score.setScore(((JSONObject) object).getInt("score"));
+						score.setDetail(convertJsonArrayStringToSet(((JSONObject) object).getString("detail")));
+						scores.add(score);
+					} catch (JSONException ignore) { }
+				}
+			}
+		}
+		return scores;
+	}
+}

+ 15 - 0
src/main/java/nju/seec/SEECdemo/data/dao/assignment/CodeDAO.java

@@ -0,0 +1,15 @@
+package nju.seec.SEECdemo.data.dao.assignment;
+
+import nju.seec.SEECdemo.data.entity.assignment.Code;
+import nju.seec.SEECdemo.util.enums.AssignmentStatus;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface CodeDAO extends JpaRepository<Code, Integer> {
+    Code findById(int id);
+
+    List<Code> findByCourseIdAndStatusIs(int courseId, AssignmentStatus status);
+
+    List<Code> findByCourseIdAndStatusIn(int courseId, List<AssignmentStatus> statuses);
+}

+ 13 - 0
src/main/java/nju/seec/SEECdemo/data/dao/assignment/CodeResultDAO.java

@@ -0,0 +1,13 @@
+package nju.seec.SEECdemo.data.dao.assignment;
+
+import nju.seec.SEECdemo.data.entity.assignment.Code;
+import nju.seec.SEECdemo.data.entity.assignment.CodeResult;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Set;
+
+public interface CodeResultDAO extends JpaRepository<CodeResult, Integer> {
+    CodeResult findById(int id);
+
+    CodeResult findByCodeAndGroupId(Code code, int groupId);
+}

+ 72 - 0
src/main/java/nju/seec/SEECdemo/data/entity/assignment/Code.java

@@ -0,0 +1,72 @@
+package nju.seec.SEECdemo.data.entity.assignment;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockCourse;
+import nju.seec.SEECdemo.util.enums.AssignmentStatus;
+import nju.seec.SEECdemo.util.enums.AssignmentType;
+import org.hibernate.annotations.LazyCollection;
+import org.hibernate.annotations.LazyCollectionOption;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+@EqualsAndHashCode(exclude = {"resultSet"})
+@ToString(exclude = {"resultSet"})
+@Entity
+@Table(name = "ENTITY_CODE")
+public class Code {
+
+    @Id
+    @Column(name = "id")
+    private Integer id;
+
+    @Basic
+    @Column(name = "name", nullable = false)
+    private String name;
+
+    @Basic
+    @Column(name = "description", nullable = false)
+    private String description;
+
+    @Enumerated(value = EnumType.STRING)
+    @Column(name = "type", columnDefinition = "VARCHAR(255) NOT NULL")
+    private AssignmentType type = AssignmentType.CODE;
+
+    @Enumerated(value = EnumType.STRING)
+    @Column(name = "status", columnDefinition = "VARCHAR(255) NOT NULL DEFAULT 'PREPARING'")
+    private AssignmentStatus status = AssignmentStatus.PREPARING;
+
+    @Basic
+    @Column(name = "start_at", nullable = false)
+    private LocalDateTime startAt;
+
+    @Basic
+    @Column(name = "end_at", nullable = false)
+    private LocalDateTime endAt;
+
+    @Basic
+    @Column(name = "question", nullable = false)
+    private Long questionId;
+
+    @Basic
+    @Column(name = "hash")
+    private String initHash;
+
+    @Basic
+    @Column(name = "uuid", nullable = false)
+    private String uuid;
+
+    @Basic
+    @Column(name = "course", nullable = false)
+    private int courseId;
+
+    @OneToMany(targetEntity = CodeResult.class, mappedBy = "code", cascade = CascadeType.REMOVE)
+    @LazyCollection(LazyCollectionOption.EXTRA)
+    private Set<CodeResult> resultSet = new HashSet<>();
+
+}

+ 60 - 0
src/main/java/nju/seec/SEECdemo/data/entity/assignment/CodeResult.java

@@ -0,0 +1,60 @@
+package nju.seec.SEECdemo.data.entity.assignment;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import nju.seec.SEECdemo.data.convertor.ScoreDetailSetConverter;
+
+import javax.persistence.*;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+@EqualsAndHashCode(exclude = {"code"})
+@ToString(exclude = {"code"})
+@Entity
+@Table(name = "ENTITY_CODE_RESULT")
+public class CodeResult {
+
+    @Id
+    @Column(name = "id")
+    private Integer id;
+
+    @ManyToOne(targetEntity = Code.class, fetch = FetchType.LAZY)
+    @JoinColumn(name = "code", referencedColumnName = "id")
+    private Code code;
+
+    @Basic
+    @Column(name = "groupId")
+    private Integer groupId;
+
+    @Basic
+    @Column(name = "buildJob")
+    private String buildJob;
+
+    @Basic
+    @Column(name = "deployNS")
+    private Long deployNamespace;
+
+    @Basic
+    @Column(name = "testJob")
+    private String testJob;
+
+    @Basic
+    @Column(name = "score", columnDefinition = "INT(11) NOT NULL DEFAULT 0")
+    private Integer score = 0;
+
+    @Basic
+    @Column(name = "details", columnDefinition = "JSON NOT NULL")
+    @Convert(converter = ScoreDetailSetConverter.class)
+    private Set<Score> detailSet = new HashSet<>();
+
+    @Data
+    public static class Score {
+        private Integer id;
+        private String name;
+        private Integer score;
+        private Set<Score> detail = new HashSet<>();
+    }
+
+}

+ 12 - 0
src/main/java/nju/seec/SEECdemo/logic/api/GitlabApi.java

@@ -1,5 +1,6 @@
 package nju.seec.SEECdemo.logic.api;
 
+import nju.seec.SEECdemo.logic.api.vo.GitlabGroup;
 import nju.seec.SEECdemo.logic.api.vo.GitlabUser;
 import org.gitlab4j.api.GitLabApiException;
 import org.gitlab4j.api.models.CommitAction;
@@ -11,6 +12,10 @@ import java.util.Set;
 public interface GitlabApi {
 	GitlabUser createUser(String username, String email, String password) throws GitLabApiException;
 
+	GitlabGroup createGroup(String groupName) throws GitLabApiException;
+
+	void addMemberToGroup(int groupId, int userId) throws GitLabApiException;
+
 	Integer createProject(String name) throws GitLabApiException;
 
 	Integer forkProject(int userId, int projectId, int namespaceId) throws GitLabApiException;
@@ -45,4 +50,11 @@ public interface GitlabApi {
 	 */
 	void createCommit(int projectId, String branch, String commitMessage, String email, String username, List<CommitAction> actions) throws GitLabApiException;
 
+	/**
+     * 获取root用户的id
+	 * @Param
+	 * @return
+	 **/
+	Integer getRootUserId();
+
 }

+ 4 - 0
src/main/java/nju/seec/SEECdemo/logic/api/JenkinsApi.java

@@ -15,4 +15,8 @@ public interface JenkinsApi {
 
 	// waitFinished: 是否等待直到构建完全完成
 	BuildDetails fetchBuildDetails(int id, int buildNumber, String target, boolean waitFinished) throws IOException;
+
+	void createBuildJob(String jobName) throws IOException;
+
+	void createTestJob(String jobName) throws IOException;
 }

+ 29 - 0
src/main/java/nju/seec/SEECdemo/logic/api/impl/GitlabApiImpl.java

@@ -1,6 +1,7 @@
 package nju.seec.SEECdemo.logic.api.impl;
 
 import nju.seec.SEECdemo.logic.api.GitlabApi;
+import nju.seec.SEECdemo.logic.api.vo.GitlabGroup;
 import nju.seec.SEECdemo.logic.api.vo.GitlabUser;
 import nju.seec.SEECdemo.util.ApplicationProperties;
 import nju.seec.SEECdemo.util.ApplicationProperties.Gitlab;
@@ -73,6 +74,29 @@ public class GitlabApiImpl implements GitlabApi {
 		return project != null ? project.getId() : null;
 	}
 
+	public synchronized GitlabGroup createGroup(String name) throws GitLabApiException {
+		Group group = server.getGroupApi().addGroup(name, name);
+		GitlabGroup gitlabGroup = new GitlabGroup();
+		gitlabGroup.setId(group.getId());
+		gitlabGroup.setNamespace(fetchGroupNamespace(group.getName()));
+		gitlabGroup.setFullPath(group.getFullPath());
+		return gitlabGroup;
+	}
+
+	private Integer fetchGroupNamespace(String groupName) throws GitLabApiException {
+		Namespace namespace;
+		namespace = server.getNamespaceApi().findNamespaces(groupName)
+				.stream()
+				.filter(ns -> "group".equals(ns.getKind()))
+				.reduce((ns1, ns2) -> ns1.getId() > ns2.getId() ? ns1 : ns2)
+				.orElse(null);
+		return namespace != null ? namespace.getId() : null;
+	}
+
+	public synchronized void addMemberToGroup(int groupId, int userId) throws GitLabApiException {
+	    Member member = server.getGroupApi().addMember(groupId, userId, AccessLevel.MASTER);
+    }
+
 	@Override
 	public synchronized Integer forkProject(int userId, int projectId, int namespaceId) throws GitLabApiException {
 		Project project;
@@ -147,4 +171,9 @@ public class GitlabApiImpl implements GitlabApi {
         commitsApi.createCommit(projectId, branch, commitMessage, null, email, username,actions);
     }
 
+    @Override
+	public Integer getRootUserId() {
+		return gitlab.getUserId();
+	}
+
 }

+ 11 - 0
src/main/java/nju/seec/SEECdemo/logic/api/impl/JenkinsApiImpl.java

@@ -55,6 +55,17 @@ public class JenkinsApiImpl implements JenkinsApi {
 		server.createJob(String.valueOf(id), configTemplate);
 	}
 
+	@Override
+	public void createBuildJob(String jobName) throws IOException{
+	    // TODO 构建脚本
+		server.createJob(jobName, configTemplate);
+	}
+
+	@Override
+	public void createTestJob(String jobName) throws IOException{
+		server.createJob(jobName, configTemplate);
+	}
+
 	@Override
 	public QueueItem invokeBuild(int id, Map<String, String> paramMap) throws IOException {
 		QueueReference reference;

+ 10 - 0
src/main/java/nju/seec/SEECdemo/logic/api/vo/GitlabGroup.java

@@ -0,0 +1,10 @@
+package nju.seec.SEECdemo.logic.api.vo;
+
+import lombok.Data;
+
+@Data
+public class GitlabGroup {
+    private int id;
+    private Integer namespace;
+    private String fullPath;
+}

+ 0 - 7
src/main/java/nju/seec/SEECdemo/logic/service/AssignmentService.java

@@ -1,7 +0,0 @@
-package nju.seec.SEECdemo.logic.service;
-
-public interface AssignmentService {
-
-
-
-}

+ 19 - 0
src/main/java/nju/seec/SEECdemo/logic/service/CodeService.java

@@ -0,0 +1,19 @@
+package nju.seec.SEECdemo.logic.service;
+
+import nju.seec.SEECdemo.logic.vo.CodeWorkVO;
+import nju.seec.SEECdemo.web.dto.CodeWorkDTO;
+
+public interface CodeService {
+
+    CodeWorkVO createAssignment(int courseId, CodeWorkDTO codeWorkDTO);
+
+    /**
+     * @Author zhangyi
+     * @Description 将新建的组,加入到作业中
+     * @Date
+     * @Param
+     * @return
+     **/
+    void groupAddedCheck(int groupId);
+
+}

+ 0 - 4
src/main/java/nju/seec/SEECdemo/logic/service/impl/AssignmentServiceImpl.java

@@ -1,4 +0,0 @@
-package nju.seec.SEECdemo.logic.service.impl;
-
-public class AssignmentServiceImpl {
-}

+ 133 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/AssignmentPaperBuilder.java

@@ -0,0 +1,133 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment;
+
+import nju.seec.SEECdemo.data.dao.assignment.CodeDAO;
+import nju.seec.SEECdemo.data.dao.assignment.CodeResultDAO;
+import nju.seec.SEECdemo.data.entity.assignment.Code;
+import nju.seec.SEECdemo.data.entity.assignment.CodeResult;
+import nju.seec.SEECdemo.logic.service.ProjectService;
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockGroup;
+import nju.seec.SEECdemo.logic.api.GitlabApi;
+import nju.seec.SEECdemo.logic.api.JenkinsApi;
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockQuestion;
+import nju.seec.SEECdemo.logic.service.impl.assignment.stub.GroupServiceStub;
+import nju.seec.SEECdemo.logic.service.impl.assignment.stub.QuestionServiceStub;
+import nju.seec.SEECdemo.logic.storage.StorageProvider;
+import nju.seec.SEECdemo.util.ApplicationProperties;
+import nju.seec.SEECdemo.util.ToolKit;
+import nju.seec.SEECdemo.util.enums.AssignmentStatus;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.gitlab4j.api.GitLabApiException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.zeroturnaround.zip.ZipUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.time.ZoneId;
+import java.util.Date;
+
+@Component
+public class AssignmentPaperBuilder {
+
+    private static final String TEMP_PREFIX = "MOOCODER-PAPER-";
+    private final GitlabApi gitlabApi;
+    private final GitHelper helper;
+    private final JenkinsApi jenkinsApi;
+    private final CodeDAO codeDAO;
+    private final CodeResultDAO codeResultDAO;
+    private final StorageProvider storageProvider;
+
+    private final QuestionServiceStub questionServiceStub;
+    private final GroupServiceStub groupServiceStub;
+    private final ProjectService projectService;
+
+    @Autowired
+    public AssignmentPaperBuilder(GitlabApi gitlabApi, GitHelper helper, JenkinsApi jenkinsApi, CodeDAO codeDAO, CodeResultDAO codeResultDAO, StorageProvider storageProvider,
+                                  QuestionServiceStub questionServiceStub, GroupServiceStub groupServiceStub, ProjectService projectService){
+        this.gitlabApi = gitlabApi;
+        this.helper = helper;
+        this.jenkinsApi = jenkinsApi;
+        this.codeDAO = codeDAO;
+        this.codeResultDAO = codeResultDAO;
+        this.storageProvider = storageProvider;
+        this.questionServiceStub = questionServiceStub;
+        this.groupServiceStub = groupServiceStub;
+        this.projectService = projectService;
+    }
+
+    @Transactional
+    public void buildCodePaper(Code code) throws IOException, GitAPIException, GitLabApiException {
+        File tempDir = Files.createTempDirectory(TEMP_PREFIX).toFile();
+        try {
+            code.setId(gitlabApi.createProject(code.getUuid()));
+            releaseQuestion(code.getQuestionId(), tempDir);
+            code.setInitHash(helper.push(tempDir, code.getUuid()));
+        } catch (GitAPIException | GitLabApiException e) {
+            code.setStatus(AssignmentStatus.UNAVAILABLE);
+            throw e;
+        } finally {
+            FileUtils.deleteDirectory(tempDir);
+        }
+    }
+
+    @Transactional
+    public void addGroupsToPaper(int codeId) {
+        Code code = codeDAO.findById(codeId);
+        if (code.getStatus() != AssignmentStatus.PREPARING) return;
+        // 为群组创建项目,并将组员加入到项目中
+        try {
+            for(MockGroup group: groupServiceStub.getGroupListByCourse(code.getCourseId())){
+                addGroupToPaper(code, group);
+            }
+            code.setStatus(AssignmentStatus.AVAILABLE);
+        } catch (IOException | GitLabApiException e) {
+            System.out.println("将小组加入作业时发生异常" + ToolKit.format(e));
+            code.setStatus(AssignmentStatus.UNAVAILABLE);
+        }
+        codeDAO.save(code);
+    }
+
+    public void addGroupToPaper(Code code, MockGroup group) throws IOException, GitLabApiException {
+        // fork项目的时候,将项目设置成private的,因为组员设置成了Master权限,而只有组的Owner才能够更项目可见性
+        // (private、internal、public),所以此处用root用户来fork
+        int projectId = gitlabApi.forkProject(gitlabApi.getRootUserId(), code.getId(), group.getNamespace());
+        CodeResult codeResult = new CodeResult();
+        codeResult.setId(projectId);
+        codeResult.setGroupId(group.getId());
+        codeResult.setCode(code);
+        // TODO
+        // 创建jenkins上的构建job
+        String buildJob = projectId + "-build";
+        jenkinsApi.createBuildJob(buildJob);
+        codeResult.setBuildJob(buildJob);
+        // 调用kube Api创建该项目的部署空间,并记录其id
+        Long projectDeployNameSpace = projectService.createSeecIIProject(formProjectName(group.getId(), code.getId()),
+                "", // 暂时留空
+                Date.from(code.getStartAt().atZone(ZoneId.systemDefault()).toInstant()),
+                Date.from(code.getEndAt().atZone(ZoneId.systemDefault()).toInstant()));
+        codeResult.setDeployNamespace(projectDeployNameSpace);
+        // 创建jenkins上的测试job
+        String testJob = projectId + "-test";
+        jenkinsApi.createTestJob(testJob);
+        codeResult.setTestJob(testJob);
+        codeResultDAO.save(codeResult);
+    }
+
+    private void releaseQuestion(Long questionId, File codeDir) throws IOException {
+        FileUtils.cleanDirectory(codeDir);
+        MockQuestion question = questionServiceStub.getQuestion(questionId);
+        byte[] content = storageProvider.fetch(question.getUniqueName() + ".zip");
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
+        ZipUtil.unpack(inputStream, codeDir);
+    }
+
+    private String formProjectName(int groupId, int codeWorkId){
+        return groupId + "c" + codeWorkId;
+    }
+
+
+}

+ 139 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/CodeServiceImpl.java

@@ -0,0 +1,139 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment;
+
+import nju.seec.SEECdemo.data.dao.assignment.CodeDAO;
+import nju.seec.SEECdemo.data.dao.assignment.CodeResultDAO;
+import nju.seec.SEECdemo.data.entity.assignment.Code;
+import nju.seec.SEECdemo.data.entity.assignment.CodeResult;
+import nju.seec.SEECdemo.logic.api.GitlabApi;
+import nju.seec.SEECdemo.logic.service.CodeService;
+import nju.seec.SEECdemo.logic.service.InterCodeService;
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockGroup;
+import nju.seec.SEECdemo.logic.service.impl.assignment.stub.CourseServiceStub;
+import nju.seec.SEECdemo.logic.service.impl.assignment.stub.GroupServiceStub;
+import nju.seec.SEECdemo.logic.vo.CodeWorkVO;
+import nju.seec.SEECdemo.logic.vo.InterCodeAppInfoVO;
+import nju.seec.SEECdemo.util.AsyncWrapper;
+import nju.seec.SEECdemo.util.ToolKit;
+import nju.seec.SEECdemo.util.enums.AssignmentStatus;
+import nju.seec.SEECdemo.web.dto.CodeWorkDTO;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.gitlab4j.api.GitLabApiException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+public class CodeServiceImpl implements CodeService, InterCodeService {
+
+    private final CodeDAO codeDAO;
+    private final CodeResultDAO codeResultDAO;
+    private final AssignmentPaperBuilder assignmentPaperBuilder;
+    private final AsyncWrapper asyncWrapper;
+    private final CourseServiceStub courseServiceStub;
+    private final GroupServiceStub groupServiceStub;
+
+    @Autowired
+    public CodeServiceImpl(CodeDAO codeDAO, CodeResultDAO codeResultDAO, AssignmentPaperBuilder assignmentPaperBuilder,
+                           AsyncWrapper asyncWrapper, CourseServiceStub courseServiceStub, GroupServiceStub groupServiceStub){
+        this.codeDAO = codeDAO;
+        this.codeResultDAO = codeResultDAO;
+        this.assignmentPaperBuilder = assignmentPaperBuilder;
+        this.asyncWrapper = asyncWrapper;
+        this.courseServiceStub = courseServiceStub;
+        this.groupServiceStub = groupServiceStub;
+    }
+
+
+    @Override
+    public CodeWorkVO createAssignment(int courseId, CodeWorkDTO codeWorkDTO) {
+        Code codework = new Code();
+        codework.setName(codeWorkDTO.getName());
+        codework.setDescription(codeWorkDTO.getDescription());
+        codework.setQuestionId(codeWorkDTO.getQuestion());
+        codework.setUuid(ToolKit.randomUUID());
+        codework.setCourseId(courseId);
+        codework.setStartAt(codeWorkDTO.getStartAt());
+        codework.setEndAt(codeWorkDTO.getEndAt());
+        try {
+            assignmentPaperBuilder.buildCodePaper(codework);
+        } catch (IOException | GitAPIException | GitLabApiException e) {
+            System.out.println("创建作业时发生异常" + ToolKit.format(e));
+        }
+        asyncWrapper.asyncInvoke(()->assignmentPaperBuilder.addGroupsToPaper(codework.getId()), 2000);
+        return new CodeWorkVO(codeDAO.save(codework),courseServiceStub.getCourse(courseId).getName(),
+                groupServiceStub.getGroupListByCourse(courseId));
+    }
+
+    /**
+     * 获取jenkins的构建job名称
+     * @param codeAssignmentId
+     * @param groupId
+     * @return
+     */
+    @Override
+    public String getJenkinsBuildJobName(int codeAssignmentId, int groupId) {
+        Code code = codeDAO.findById(codeAssignmentId);
+        CodeResult codeResult = codeResultDAO.findByCodeAndGroupId(code, groupId);
+        return codeResult!=null?codeResult.getBuildJob():null;
+    }
+
+    /**
+     * 查询一个作业下一个小组的应部署应用必要信息
+     * @param codeAssignmentId 作业id
+     * @param groupId 小组id
+     * @return
+     */
+    @Override
+    public InterCodeAppInfoVO getInfo(int codeAssignmentId, int groupId) {
+        Code code = codeDAO.findById(codeAssignmentId);
+        CodeResult codeResult = codeResultDAO.findByCodeAndGroupId(code, groupId);
+        InterCodeAppInfoVO codeAppInfoVO = new InterCodeAppInfoVO();
+        codeAppInfoVO.setId(codeResult.getId());
+        codeAppInfoVO.setProjectId(codeResult.getDeployNamespace());
+        codeAppInfoVO.setApplicationName(codeResult.getGroupId() + code.getUuid());
+        return codeAppInfoVO;
+    }
+
+    /**
+     * 获取jenkins的测试job名称
+     * @param codeAssignmentId
+     * @param groupId
+     * @return
+     */
+    @Override
+    public String getJenkinsTestJobName(int codeAssignmentId, int groupId) {
+        Code code = codeDAO.findById(codeAssignmentId);
+        CodeResult codeResult = codeResultDAO.findByCodeAndGroupId(code, groupId);
+        return codeResult!=null?codeResult.getTestJob():null;
+    }
+
+
+    public void groupAddedCheck(int groupId){
+        MockGroup group = groupServiceStub.getGroup(groupId);
+        List<AssignmentStatus> statuses = new ArrayList(){{
+            add(AssignmentStatus.AVAILABLE);
+            add(AssignmentStatus.PREPARING);
+        }};
+        try {
+            List<Code> codeList = codeDAO.findByCourseIdAndStatusIn(group.getCourseId(), statuses);
+            List<Code> preparingList = new ArrayList<>();
+            // TODO
+            // 对于AVAILABLE的作业,直接将组加入;对于PREPARING的作业,需要等到其状态变更结束后(AVAILABLE或UNAVAILABLE),
+            // 再根据其状态判断:AVAILABLE,判断该组是否已经在作业中,不在则加入;UNAVAILABLE,则不做操作。
+            for (Code code:codeList) {
+                if (code.getStatus()==AssignmentStatus.AVAILABLE) {
+                    assignmentPaperBuilder.addGroupToPaper(code, group);
+                } else {
+                    preparingList.add(code);
+                }
+            }
+        } catch (IOException | GitLabApiException e) {
+            System.out.println("将小组加入作业时发生异常" + ToolKit.format(e));
+        }
+    }
+
+
+}

+ 50 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/GitHelper.java

@@ -0,0 +1,50 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment;
+
+import nju.seec.SEECdemo.util.ApplicationProperties;
+import nju.seec.SEECdemo.util.ApplicationProperties.Gitlab;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+@Component
+public class GitHelper {
+	private static final String TEMP_PREFIX = "MOOCODER-REPOSITORY-";
+	private static final String COMMIT_MESSAGE = "初始化";
+
+	private final Gitlab gitlab;
+	private final CredentialsProvider provider;
+
+	@Autowired
+	public GitHelper(ApplicationProperties properties) {
+		this.gitlab = properties.getGitlab();
+		this.provider = new UsernamePasswordCredentialsProvider(gitlab.getUsername(), gitlab.getToken());
+	}
+
+	public String push(File sourceDir, String project) throws GitAPIException, IOException {
+		String uri = gitlab.getHost() + "/" + gitlab.getUsername() + "/" + project + ".git";
+		File tempDir = Files.createTempDirectory(TEMP_PREFIX).toFile();
+		try {
+			return push(sourceDir, tempDir, uri);
+		} finally {
+			FileUtils.deleteDirectory(tempDir);
+		}
+	}
+
+	private String push(File sourceDir, File tempDir, String uri) throws GitAPIException, IOException {
+		Git targetRepo = Git.cloneRepository().setURI(uri).setDirectory(tempDir).setCredentialsProvider(provider).call();
+		FileUtils.copyDirectory(sourceDir, tempDir);
+		targetRepo.add().addFilepattern(".").call();
+		String hash = targetRepo.commit().setMessage(COMMIT_MESSAGE).call().getName();
+		targetRepo.push().setRemote("origin").setForce(true).setPushAll().setCredentialsProvider(provider).call();
+		targetRepo.close();
+		return hash;
+	}
+}

+ 16 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/mock/MockCourse.java

@@ -0,0 +1,16 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment.mock;
+
+import lombok.Data;
+
+@Data
+public class MockCourse {
+    private Integer id;
+    private String name;
+    private Integer creator;
+
+    public MockCourse(Integer id, String name, Integer creator) {
+        this.id = id;
+        this.name = name;
+        this.creator = creator;
+    }
+}

+ 24 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/mock/MockGroup.java

@@ -0,0 +1,24 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment.mock;
+
+import lombok.Data;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+public class MockGroup {
+    private Integer id;
+    private String name;
+    private Integer namespace;
+    private String fullPath;
+    private Set<Integer> memberSet = new HashSet<>();
+    private Integer courseId;
+
+    public MockGroup(Integer id, String name, Integer namespace, String fullPath, Set<Integer> memberSet, Integer courseId) {
+        this.id = id;
+        this.name = name;
+        this.namespace = namespace;
+        this.fullPath = fullPath;
+        this.memberSet = memberSet;
+        this.courseId = courseId;
+    }
+}

+ 18 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/mock/MockQuestion.java

@@ -0,0 +1,18 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment.mock;
+
+import lombok.Data;
+
+@Data
+public class MockQuestion {
+    private Long id;
+    private String name;
+
+    public MockQuestion(Long id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    public String getUniqueName(){
+        return this.id + "-" + this.name;
+    }
+}

+ 25 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/mock/MockUser.java

@@ -0,0 +1,25 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment.mock;
+
+import lombok.Data;
+import nju.seec.SEECdemo.util.enums.UserRole;
+
+@Data
+public class MockUser {
+    private Integer id;
+    private String username;
+    private String password;
+    private String email;
+    private UserRole role;
+    private Integer namespace;
+    private String token;
+
+    public MockUser(Integer id, String username, String password, String email, UserRole role, Integer namespace, String token) {
+        this.id = id;
+        this.username = username;
+        this.password = password;
+        this.email = email;
+        this.role = role;
+        this.namespace = namespace;
+        this.token = token;
+    }
+}

+ 20 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/stub/CourseServiceStub.java

@@ -0,0 +1,20 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment.stub;
+
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockCourse;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+public class CourseServiceStub {
+
+    private static List<MockCourse> courseList = new ArrayList(){{
+       add(new MockCourse(1, "软工二", 3));
+    }};
+
+    public MockCourse getCourse(int courseId){
+        return courseList.stream().filter(c -> c.getId()==courseId).findFirst().get();
+    }
+
+}

+ 28 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/stub/GroupServiceStub.java

@@ -0,0 +1,28 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment.stub;
+
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockGroup;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+public class GroupServiceStub {
+
+    private static List<MockGroup> groupList = new ArrayList(){{
+        add(new MockGroup(8, "softwareII", 8, "softwareII", new HashSet<Integer>(){{
+            add(5);
+            add(6);
+        }}, 1));
+    }};
+
+    public List<MockGroup> getGroupListByCourse(int courseId){
+        return groupList.stream().filter(g -> g.getCourseId()==courseId).collect(Collectors.toList());
+    }
+
+    public MockGroup getGroup(int groupId){
+        return groupList.stream().filter(g -> g.getId()==groupId).findFirst().get();
+    }
+}

+ 20 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/stub/QuestionServiceStub.java

@@ -0,0 +1,20 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment.stub;
+
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockQuestion;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+public class QuestionServiceStub {
+
+    private static List<MockQuestion> questionList = new ArrayList(){{
+        add(new MockQuestion(64L, "three-sum"));
+    }};
+
+    public MockQuestion getQuestion(Long questionId){
+        return questionList.stream().filter(q -> q.getId()==questionId).findFirst().get();
+    }
+
+}

+ 28 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/assignment/stub/UserServiceStub.java

@@ -0,0 +1,28 @@
+package nju.seec.SEECdemo.logic.service.impl.assignment.stub;
+
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockUser;
+import nju.seec.SEECdemo.util.enums.UserRole;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+public class UserServiceStub {
+
+    private static List<MockUser> userList = new ArrayList(){{
+        add(new MockUser(3, "TeaCherZ", "tEAcHERz123456", "zhangyi1994vector@163.com", UserRole.TEACHER,
+                4, "YYdRhe1dbEdPxSwQ9ddL"));
+
+        add(new MockUser(5, "qaz321", "123456789", "abc@qq.com", UserRole.STUDENT,
+                6, "bMvEuzDEQ4R25-Mk9qLx"));
+
+        add(new MockUser(6, "wsx321", "123456789", "mf1732176@smail.nju.edu.cn", UserRole.STUDENT,
+                7, "xsscdoUxvbyt2enC2SA6"));
+    }};
+
+    public MockUser getUser(int userId){
+        return userList.stream().filter(u -> u.getId()==userId).findFirst().get();
+    }
+
+}

+ 12 - 0
src/main/java/nju/seec/SEECdemo/logic/storage/StorageProvider.java

@@ -0,0 +1,12 @@
+package nju.seec.SEECdemo.logic.storage;
+
+import java.io.IOException;
+import java.util.Map;
+
+public interface StorageProvider {
+	void initialize(Map<String, String> properties) throws IOException;
+
+	void save(byte[] content, String name) throws IOException;
+
+	byte[] fetch(String name) throws IOException;
+}

+ 32 - 0
src/main/java/nju/seec/SEECdemo/logic/storage/StorageProviderConfiguration.java

@@ -0,0 +1,32 @@
+package nju.seec.SEECdemo.logic.storage;
+
+import nju.seec.SEECdemo.logic.storage.impl.LocalStorageProvider;
+import nju.seec.SEECdemo.util.ApplicationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.io.IOException;
+
+@Configuration
+public class StorageProviderConfiguration {
+	private final ApplicationProperties properties;
+
+	public StorageProviderConfiguration(ApplicationProperties properties) {
+		this.properties = properties;
+	}
+
+	@Bean
+	public StorageProvider storageProvider() throws IOException {
+		ApplicationProperties.Storage storage = properties.getStorage();
+		StorageProvider storageProvider;
+		switch (storage.getType()) {
+			case "local":
+				storageProvider = new LocalStorageProvider();
+				break;
+			default:
+				throw new IllegalArgumentException("没有类型为[" + storage.getType() + "]的文件存储实现!");
+		}
+		storageProvider.initialize(storage.getProperties());
+		return storageProvider;
+	}
+}

+ 43 - 0
src/main/java/nju/seec/SEECdemo/logic/storage/impl/LocalStorageProvider.java

@@ -0,0 +1,43 @@
+package nju.seec.SEECdemo.logic.storage.impl;
+
+import nju.seec.SEECdemo.logic.storage.StorageProvider;
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+public class LocalStorageProvider implements StorageProvider {
+	private String location;
+
+	@Override
+	public void initialize(Map<String, String> properties) throws IOException {
+		location = properties.get("location");
+		if (location == null) {
+			throw new IllegalArgumentException("未设定文件存储位置!");
+		}
+		while (location.endsWith(File.separator)) {
+			location = location.substring(0, location.length() - 1);
+		}
+		File file = new File(location);
+		if (file.exists()) {
+			if (!file.isDirectory()) {
+				throw new IOException("存储目标不能为非文件夹!");
+			}
+		} else if (!file.mkdirs()) {
+			throw new IOException("创建文件夹失败!");
+		}
+	}
+
+	@Override
+	public void save(byte[] content, String name) throws IOException {
+		File file = new File(location + File.separator + name);
+		FileUtils.writeByteArrayToFile(file, content);
+	}
+
+	@Override
+	public byte[] fetch(String name) throws IOException {
+		File file = new File(location + File.separator + name);
+		return FileUtils.readFileToByteArray(file);
+	}
+}

+ 0 - 7
src/main/java/nju/seec/SEECdemo/logic/vo/AssignmentVO.java

@@ -1,7 +0,0 @@
-package nju.seec.SEECdemo.logic.vo;
-
-import lombok.Data;
-
-@Data
-public class AssignmentVO {
-}

+ 54 - 0
src/main/java/nju/seec/SEECdemo/logic/vo/CodeWorkVO.java

@@ -0,0 +1,54 @@
+package nju.seec.SEECdemo.logic.vo;
+
+import lombok.Data;
+import nju.seec.SEECdemo.data.entity.assignment.Code;
+import nju.seec.SEECdemo.data.entity.assignment.CodeResult;
+import nju.seec.SEECdemo.logic.service.impl.assignment.mock.MockGroup;
+import nju.seec.SEECdemo.util.enums.AssignmentStatus;
+import org.springframework.beans.BeanUtils;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Data
+public class CodeWorkVO {
+    private Integer id;
+    private String name;
+    private String description;
+    private AssignmentStatus status;
+    private String courseName;
+    private Set<NestedRecord> records;
+
+    public CodeWorkVO(Code assignment, String courseName, List<MockGroup> groupList){
+        BeanUtils.copyProperties(assignment, this);
+        this.courseName = courseName;
+        this.records = assignment.getResultSet().stream().map(c->{
+            MockGroup group = groupList.stream().filter(g->g.getId()==c.getGroupId()).findFirst().get();
+            return new NestedRecord(c, group);
+        }).collect(Collectors.toSet());
+    }
+
+    @Data
+    public static class NestedRecord {
+        private Integer id;
+        private NestedGroup group;
+
+        public NestedRecord(CodeResult assignmentResult, MockGroup group){
+            this.id = assignmentResult.getId();
+            this.group = new NestedGroup(group);
+        }
+    }
+
+    @Data
+    public static class NestedGroup {
+        private Integer id;
+        private String name;
+
+        public NestedGroup(MockGroup group){
+            this.id = group.getId();
+            this.name = group.getName();
+        }
+    }
+
+}

+ 1 - 1
src/main/java/nju/seec/SEECdemo/logic/vo/InterCodeAppInfoVO.java

@@ -5,6 +5,6 @@ import lombok.Data;
 @Data
 public class InterCodeAppInfoVO {
     private int id;
-    private int projectId;
+    private Long projectId;
     private String applicationName;
 }

+ 3 - 0
src/main/java/nju/seec/SEECdemo/util/ApplicationProperties.java

@@ -10,6 +10,7 @@ import org.springframework.validation.annotation.Validated;
 import javax.validation.Valid;
 import javax.validation.constraints.Email;
 import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
 import javax.validation.constraints.Pattern;
 import java.util.HashMap;
 import java.util.Map;
@@ -46,6 +47,8 @@ public class ApplicationProperties {
 		private String webHookProxy = "http://localhost:3000";
 		@NotEmpty
 		private String username = "root";
+		@NotNull
+		private Integer userId = 1;
 		@NotEmpty
 		private String token;
 

+ 29 - 0
src/main/java/nju/seec/SEECdemo/util/AsyncWrapper.java

@@ -0,0 +1,29 @@
+package nju.seec.SEECdemo.util;
+
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+@Component
+@CommonsLog
+public class AsyncWrapper {
+
+	@Async
+	public void asyncInvoke(Method method) {
+		try {
+			method.invoke();
+		} catch (Exception e) {
+			log.error("进行异步调用时发生异常[" + e.getClass() + "]:" + e.getMessage());
+		}
+	}
+
+	@Async
+	public void asyncInvoke(Method method, long delay) {
+		try {
+			Thread.sleep(delay);
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+		}
+		asyncInvoke(method);
+	}
+}

+ 35 - 0
src/main/java/nju/seec/SEECdemo/util/ExecutorConfiguration.java

@@ -0,0 +1,35 @@
+package nju.seec.SEECdemo.util;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@EnableScheduling
+@EnableAsync
+@Configuration
+public class ExecutorConfiguration {
+	private static final int PROCESSOR_THREAD = Runtime.getRuntime().availableProcessors();
+	private static final int MAX_THREAD = 16;
+
+	@Bean
+	public ScheduledExecutorService scheduledExecutor() {
+		return Executors.newScheduledThreadPool(Math.min(2 * PROCESSOR_THREAD, MAX_THREAD));
+	}
+
+	@Bean
+	public TaskExecutor taskExecutor() {
+		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
+		taskExecutor.setCorePoolSize(Math.min(2 * PROCESSOR_THREAD, MAX_THREAD));
+		taskExecutor.setMaxPoolSize(Math.min(4 * PROCESSOR_THREAD, MAX_THREAD));
+		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+		taskExecutor.initialize();
+		return taskExecutor;
+	}
+}

+ 6 - 0
src/main/java/nju/seec/SEECdemo/util/Method.java

@@ -0,0 +1,6 @@
+package nju.seec.SEECdemo.util;
+
+@FunctionalInterface
+public interface Method {
+	void invoke() throws Exception;
+}

+ 30 - 0
src/main/java/nju/seec/SEECdemo/util/enums/AssignmentStatus.java

@@ -0,0 +1,30 @@
+package nju.seec.SEECdemo.util.enums;
+
+/**
+ * 考试状态
+ * 考试生命周期中对外表现的状态的顺序为PREPARING->READY->AVAILABLE->FINISHED->CLOSED
+ * 考试生命周期中持久化的状态的顺序为PREPARING->AVAILABLE->CLOSED
+ * UNAVAILABLE表示考试初始化失败
+ */
+public enum AssignmentStatus {
+	/**
+	 * 正在准备试题,通常是正在PUSH试题
+	 */
+	PREPARING,
+	/**
+	 * 考试不可用,通常是PUSH试题失败
+	 */
+	UNAVAILABLE,
+	/**
+	 * 考试已准备好,在考试尚未开始时使用,不持久化到数据库中
+	 */
+	READY,
+	/**
+	 * 考试可用,表示考试正在进行,持久化时考试从准备好到考试被关闭均为此状态
+	 */
+	AVAILABLE,
+	/**
+	 * 考试已完成,在考试结束后使用,不持久化到数据库中
+	 */
+	FINISHED
+}

+ 12 - 1
src/main/java/nju/seec/SEECdemo/util/enums/AssignmentType.java

@@ -1,5 +1,16 @@
 package nju.seec.SEECdemo.util.enums;
 
+import lombok.Getter;
+
+@Getter
 public enum AssignmentType {
-    CODE
+    CODE("CODE", "团队代码作业");
+
+    AssignmentType(String name, String value){
+        this.name = name;
+        this.value = value;
+    }
+
+    private String name;
+    private String value;
 }

+ 23 - 0
src/main/java/nju/seec/SEECdemo/util/enums/UserRole.java

@@ -0,0 +1,23 @@
+package nju.seec.SEECdemo.util.enums;
+
+public enum UserRole {
+	/**
+	 * 学生角色
+	 */
+	STUDENT("学生"),
+	/**
+	 * 教师角色
+	 */
+	TEACHER("教师");
+
+	private final String name;
+
+	UserRole(String name) {
+		this.name = name;
+	}
+
+	@Override
+	public String toString() {
+		return name;
+	}
+}

+ 32 - 0
src/main/java/nju/seec/SEECdemo/util/serializer/TimestampLocalDateDeserializer.java

@@ -0,0 +1,32 @@
+package nju.seec.SEECdemo.util.serializer;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+public class TimestampLocalDateDeserializer extends JsonDeserializer<LocalDateTime> {
+	@Override
+	public LocalDateTime deserialize(JsonParser p, DeserializationContext context) throws IOException {
+		Long epochSecond = null;
+		if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
+			epochSecond = p.getLongValue();
+		} else if (p.hasToken(JsonToken.VALUE_STRING)) {
+			String value = p.getText().trim();
+			try {
+				epochSecond = Long.valueOf(value);
+			} catch (NumberFormatException e) {
+				epochSecond = null;
+			}
+		}
+		if (epochSecond != null) {
+			return LocalDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), ZoneId.systemDefault());
+		}
+		return null;
+	}
+}

+ 29 - 0
src/main/java/nju/seec/SEECdemo/util/serializer/TimestampLocalDateTimeSerializer.java

@@ -0,0 +1,29 @@
+package nju.seec.SEECdemo.util.serializer;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+public class TimestampLocalDateTimeSerializer extends LocalDateTimeSerializer {
+	@Override
+	protected LocalDateTimeSerializer withFormat(Boolean useTimestamp, DateTimeFormatter f, JsonFormat.Shape shape) {
+		return this;
+	}
+
+	@Override
+	public void serialize(LocalDateTime value, JsonGenerator g, SerializerProvider provider) throws IOException {
+		g.writeNumber(value.atZone(ZoneId.systemDefault()).toEpochSecond());
+	}
+
+	@Override
+	public void serializeWithType(LocalDateTime value, JsonGenerator g, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
+		serialize(value, g, provider);
+	}
+}

+ 0 - 9
src/main/java/nju/seec/SEECdemo/web/controller/AssignmentController.java

@@ -1,9 +0,0 @@
-package nju.seec.SEECdemo.web.controller;
-
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/api")
-public class AssignmentController {
-}

+ 28 - 0
src/main/java/nju/seec/SEECdemo/web/controller/CodeController.java

@@ -0,0 +1,28 @@
+package nju.seec.SEECdemo.web.controller;
+
+import nju.seec.SEECdemo.logic.service.CodeService;
+import nju.seec.SEECdemo.web.dto.CodeWorkDTO;
+import nju.seec.SEECdemo.web.response.EmptyResponse;
+import nju.seec.SEECdemo.web.response.ResourceResponse;
+import nju.seec.SEECdemo.web.response.Response;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/assignment")
+public class CodeController {
+
+    @Autowired
+    private final CodeService codeService;
+
+    public CodeController(CodeService codeService){
+        this.codeService = codeService;
+    }
+
+    @PostMapping("/course/{courseId:\\d+}/code")
+    public Response create(@PathVariable int courseId, @RequestBody @Validated(PostMapping.class) CodeWorkDTO codeWorkDTO){
+        return new ResourceResponse(codeService.createAssignment(courseId, codeWorkDTO));
+    }
+
+}

+ 0 - 7
src/main/java/nju/seec/SEECdemo/web/dto/AssignmentDTO.java

@@ -1,7 +0,0 @@
-package nju.seec.SEECdemo.web.dto;
-
-import lombok.Data;
-
-@Data
-public class AssignmentDTO {
-}

+ 26 - 0
src/main/java/nju/seec/SEECdemo/web/dto/CodeWorkDTO.java

@@ -0,0 +1,26 @@
+package nju.seec.SEECdemo.web.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import lombok.Data;
+import nju.seec.SEECdemo.util.serializer.TimestampLocalDateDeserializer;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+@Data
+public class CodeWorkDTO {
+    private String name;
+    private String description;
+    private Long question;
+    @JsonProperty("start_at")
+    @JsonDeserialize(using = TimestampLocalDateDeserializer.class)
+    @NotNull(message = "请填写考试开始时间!", groups = {PostMapping.class, PutMapping.class})
+    private LocalDateTime startAt;
+    @JsonProperty("end_at")
+    @JsonDeserialize(using = TimestampLocalDateDeserializer.class)
+    @NotNull(message = "请填写考试结束时间!", groups = {PostMapping.class, PutMapping.class})
+    private LocalDateTime endAt;
+}

+ 18 - 0
src/main/java/nju/seec/SEECdemo/web/response/EmptyResponse.java

@@ -0,0 +1,18 @@
+package nju.seec.SEECdemo.web.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class EmptyResponse implements Response {
+	@JsonProperty("err")
+	private int error;
+
+	public EmptyResponse() {
+		this.error = 0;
+	}
+
+	protected EmptyResponse(int error) {
+		this.error = error;
+	}
+}

+ 17 - 0
src/main/java/nju/seec/SEECdemo/web/response/ErrorResponse.java

@@ -0,0 +1,17 @@
+package nju.seec.SEECdemo.web.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ErrorResponse extends EmptyResponse {
+	@JsonProperty("msg")
+	private String message;
+
+	public ErrorResponse(int err, String message) {
+		super(err);
+		this.message = message;
+	}
+}

+ 33 - 0
src/main/java/nju/seec/SEECdemo/web/response/PageResourceResponse.java

@@ -0,0 +1,33 @@
+package nju.seec.SEECdemo.web.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.data.domain.Page;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class PageResourceResponse extends ResourceResponse {
+	@JsonProperty("page")
+	private PageInfo pageInfo;
+
+	public PageResourceResponse(Page page) {
+		super(page.getContent());
+		this.pageInfo = new PageInfo(page);
+	}
+
+	@Data
+	private static class PageInfo {
+		private int page;
+		private int limit;
+		private int size;
+		private long total;
+
+		PageInfo(Page page) {
+			this.page = page.getNumber() + 1;
+			this.limit = page.getSize();
+			this.size = page.getContent().size();
+			this.total = page.getTotalElements();
+		}
+	}
+}

+ 16 - 0
src/main/java/nju/seec/SEECdemo/web/response/ResourceResponse.java

@@ -0,0 +1,16 @@
+package nju.seec.SEECdemo.web.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ResourceResponse extends EmptyResponse {
+	@JsonProperty("res")
+	private Object resource;
+
+	public ResourceResponse(Object resource) {
+		this.resource = resource;
+	}
+}

+ 6 - 0
src/main/java/nju/seec/SEECdemo/web/response/Response.java

@@ -0,0 +1,6 @@
+package nju.seec.SEECdemo.web.response;
+
+@FunctionalInterface
+public interface Response {
+	int getError();
+}