Browse Source

Merge branch 'seec' into coder

# Conflicts:
#	seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/CourseService.java
#	seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/CourseServiceImpl.java
#	seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/CourseController.java
Leone 4 years ago
parent
commit
fb8c2de2ed
58 changed files with 2260 additions and 152 deletions
  1. 6 2
      pom.xml
  2. 38 0
      seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/dto/GroupDTO.java
  3. 44 0
      seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/group/GroupMemberVO.java
  4. 72 0
      seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/group/GroupRuleVO.java
  5. 44 0
      seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/group/GroupVO.java
  6. 24 10
      seecoder-portal-server/pom.xml
  7. 4 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/SeecoderPortalServerApplication.java
  8. 15 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/config/SpringSecurityConfiguration.java
  9. 1 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/AnnouncementRecord.java
  10. 1 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/Course.java
  11. 48 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/CourseProject.java
  12. 1 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/CourseSelection.java
  13. 1 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/MessageRecord.java
  14. 26 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/group/Group.java
  15. 25 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/group/GroupMember.java
  16. 85 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/group/GroupRule.java
  17. 1 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/key/AnnouncementRecordKey.java
  18. 1 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/key/CourseSelectionKey.java
  19. 1 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/key/MessageRecordKey.java
  20. 27 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/mapper/group/GroupMapper.java
  21. 27 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/mapper/group/GroupMemberMapper.java
  22. 31 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/mapper/group/GroupRuleMapper.java
  23. 4 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/mapper/impl/CourseMapperImpl.java
  24. 1 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/AnnouncementRecordRepository.java
  25. 28 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/CourseProjectRepository.java
  26. 1 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/CourseSelectionRepository.java
  27. 1 5
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/MessageRecordRepository.java
  28. 58 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/group/GroupMemberRepository.java
  29. 57 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/group/GroupRepository.java
  30. 19 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/group/GroupRuleRepository.java
  31. 17 5
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/CourseService.java
  32. 62 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/GroupService.java
  33. 80 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/ProjectService.java
  34. 25 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/subsystem/DevcloudService.java
  35. 1 1
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/subsystem/SubsystemService.java
  36. 15 4
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/AnnouncementServiceImpl.java
  37. 56 20
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/CourseServiceImpl.java
  38. 2 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/ExamServiceImpl.java
  39. 195 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/GroupServiceImpl.java
  40. 1 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/HttpServiceImpl.java
  41. 1 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/MessageServiceImpl.java
  42. 155 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/ProjectServiceImpl.java
  43. 2 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/subsystem/CoderService.java
  44. 106 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/subsystem/DevcloudServiceImpl.java
  45. 2 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/subsystem/EvalService.java
  46. 2 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/subsystem/HelperService.java
  47. 107 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/utility/OssUtil.java
  48. 17 10
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/CourseController.java
  49. 54 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/GroupController.java
  50. 75 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/ProjectController.java
  51. 17 0
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/TestController.java
  52. 8 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/UserJWTController.java
  53. 34 2
      seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/errors/CustomErrorException.java
  54. 14 4
      seecoder-portal-server/src/main/resources/application-dev.yml
  55. 5 0
      seecoder-portal-server/src/main/resources/application.yml
  56. 0 8
      seecoder-portal-server/src/main/resources/localData.sql
  57. 515 0
      seecoder-portal-server/src/main/resources/portal.sql
  58. 0 55
      seecoder-portal-server/src/main/resources/set_fk.sql

+ 6 - 2
pom.xml

@@ -74,7 +74,6 @@
             <artifactId>judgement-common</artifactId>
             <version>0.2.0</version>
         </dependency>
-
         <dependency>
             <groupId>cn.seecoder</groupId>
             <artifactId>judgement-client</artifactId>
@@ -92,6 +91,12 @@
             <artifactId>springfox-swagger2</artifactId>
             <version>2.4.0</version>
         </dependency>
+        <!--json-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.79</version>
+        </dependency>
     </dependencies>
 
     <dependencyManagement>
@@ -123,7 +128,6 @@
                 <version>2.4.4</version>
                 <scope>import</scope>
             </dependency>
-
             <dependency>
                 <groupId>io.jsonwebtoken</groupId>
                 <artifactId>jjwt-api</artifactId>

+ 38 - 0
seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/dto/GroupDTO.java

@@ -0,0 +1,38 @@
+package cn.seecoder.seecoderportalcommon.vo.dto;
+
+import cn.seecoder.seecoderportalcommon.vo.UserVO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupVO;
+
+import java.util.List;
+
+/**
+ * @author wind
+ */
+public class GroupDTO {
+    private GroupVO groupVO;
+    private List<UserVO> members;
+
+    public GroupVO getGroupVO() {
+        return groupVO;
+    }
+
+    public void setGroupVO(GroupVO groupVO) {
+        this.groupVO = groupVO;
+    }
+
+    public List<UserVO> getMembers() {
+        return members;
+    }
+
+    public void setMembers(List<UserVO> members) {
+        this.members = members;
+    }
+
+    @Override
+    public String toString() {
+        return "GroupDTO{" +
+                "groupVO=" + groupVO +
+                ", members=" + members +
+                '}';
+    }
+}

+ 44 - 0
seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/group/GroupMemberVO.java

@@ -0,0 +1,44 @@
+package cn.seecoder.seecoderportalcommon.vo.group;
+
+/**
+ * @author wind
+ */
+public class GroupMemberVO {
+    private Long userId;
+    private Long groupId;
+    private Long courseId;
+
+    @Override
+    public String toString() {
+        return "GroupMemberVO{" +
+                "userId=" + userId +
+                ", groupId=" + groupId +
+                ", courseId=" + courseId +
+                '}';
+    }
+
+    public Long getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Long courseId) {
+        this.courseId = courseId;
+    }
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public Long getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(Long groupId) {
+        this.groupId = groupId;
+    }
+
+}

+ 72 - 0
seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/group/GroupRuleVO.java

@@ -0,0 +1,72 @@
+package cn.seecoder.seecoderportalcommon.vo.group;
+
+
+import cn.seecoder.seecoderportalcommon.vo.CourseVO;
+
+import java.time.OffsetDateTime;
+
+/**
+ * @author wind
+ */
+public class GroupRuleVO {
+    private Long id;
+
+    private CourseVO courseVO;
+
+    private Integer maxSize;
+
+    private Integer maxNameLen;
+
+    private OffsetDateTime createdTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public CourseVO getCourseVO() {
+        return courseVO;
+    }
+
+    public void setCourseVO(CourseVO courseVO) {
+        this.courseVO = courseVO;
+    }
+
+    public Integer getMaxSize() {
+        return maxSize;
+    }
+
+    public void setMaxSize(Integer maxSize) {
+        this.maxSize = maxSize;
+    }
+
+    public Integer getMaxNameLen() {
+        return maxNameLen;
+    }
+
+    public void setMaxNameLen(Integer maxNameLen) {
+        this.maxNameLen = maxNameLen;
+    }
+
+    public OffsetDateTime getCreatedTime() {
+        return createdTime;
+    }
+
+    public void setCreatedTime(OffsetDateTime createdTime) {
+        this.createdTime = createdTime;
+    }
+
+    @Override
+    public String toString() {
+        return "GroupRuleVO{" +
+                "id=" + id +
+                ", courseVO=" + courseVO +
+                ", maxSize=" + maxSize +
+                ", maxNameLen=" + maxNameLen +
+                ", createdTime=" + createdTime +
+                '}';
+    }
+}

+ 44 - 0
seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/group/GroupVO.java

@@ -0,0 +1,44 @@
+package cn.seecoder.seecoderportalcommon.vo.group;
+
+/**
+ *
+ * @author wind
+ */
+public class GroupVO {
+    private Long id;
+    private String name;
+    private Long courseId;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Long getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Long courseId) {
+        this.courseId = courseId;
+    }
+
+    @Override
+    public String toString() {
+        return "GroupVO{" +
+                "id=" + id +
+                ", name='" + name + '\'' +
+                ", courseId='" + courseId + '\'' +
+                '}';
+    }
+}

+ 24 - 10
seecoder-portal-server/pom.xml

@@ -88,11 +88,6 @@
             <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>
@@ -102,7 +97,6 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
-
         <dependency>
             <groupId>io.jsonwebtoken</groupId>
             <artifactId>jjwt-api</artifactId>
@@ -121,11 +115,31 @@
             <groupId>com.aliyun</groupId>
             <artifactId>aliyun-java-sdk-core</artifactId>
         </dependency>
-
         <dependency>
-            <groupId>com.alibaba</groupId>
-            <artifactId>fastjson</artifactId>
-            <version>1.2.79</version>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+            <version>2.8.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>3.10.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>com.nju.edu</groupId>
+            <artifactId>PaasMonitor-client</artifactId>
+            <version>0.1.1</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>3.4.2</version>
         </dependency>
     </dependencies>
 

+ 4 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/SeecoderPortalServerApplication.java

@@ -1,16 +1,18 @@
 package cn.seecoder.seecoderportalserver;
 
 import cn.seecoder.seecoderportalserver.config.property.AliProperties;
+import cn.seecoder.seecoderportalserver.utility.OssUtil;
+import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 
 /**
  * @author Kunduin
  */
 @SpringBootApplication
-@EnableConfigurationProperties({AliProperties.class})
+@EnableConfigurationProperties({AliProperties.class, OssUtil.class})
+@MapperScan("cn.seecoder.seecoderportalserver.repository")
 public class SeecoderPortalServerApplication {
     public static void main(String[] args) {
         SpringApplication.run(SeecoderPortalServerApplication.class, args);

+ 15 - 1
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/config/SpringSecurityConfiguration.java

@@ -51,6 +51,7 @@ public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
                 .and()
                 .authorizeRequests()
                 .antMatchers(
+                        "/api/test/upload",
                         "/api/user/sendMessage",
                         "/api/user/login",
                         "/api/user/register",
@@ -74,6 +75,7 @@ public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
                         "/api/course/selection/confirm",
                         "/api/course/selection/cancel",
                         "/api/course/checkName",
+                        "/api/course/uploadImage",
                         "/api/eval/exam/student",
                         "/api/eval/exam/student/groupByCourse",
                         "/api/eval/exam/teacher",
@@ -98,7 +100,19 @@ public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
                         "/api/message/getUnreadByUser",
                         "/api/message/getByCourseId",
                         "/api/message/getView",
-                        "/api/message/read"
+                        "/api/message/read",
+                        "/api/project",
+                        "/api/project/findAll",
+                        "/api/project/findCourse",
+                        "/api/project/findNonCourse",
+                        "/api/project/findById",
+                        "/api/project/findByCourse",
+                        "/api/project/updateState",
+                        "/api/group",
+                        "/api/group/one",
+                        "/api/group/all",
+                        "/api/group/member/add",
+                        "/api/group/rule"
                 )
                 .permitAll() // authenticate
                 .and()

+ 1 - 1
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/AnnouncementRecord.java

@@ -1,6 +1,6 @@
 package cn.seecoder.seecoderportalserver.domain;
 
-import cn.seecoder.seecoderportalcommon.vo.AnnouncementRecordKey;
+import cn.seecoder.seecoderportalserver.domain.key.AnnouncementRecordKey;
 
 import javax.persistence.*;
 import javax.validation.constraints.NotNull;

+ 1 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/Course.java

@@ -7,6 +7,7 @@ import org.hibernate.annotations.CreationTimestamp;
 import javax.persistence.*;
 import javax.validation.constraints.NotNull;
 import java.time.OffsetDateTime;
+import java.util.Collection;
 
 @Entity
 @Table(name = "course")

+ 48 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/CourseProject.java

@@ -0,0 +1,48 @@
+package cn.seecoder.seecoderportalserver.domain;
+
+import org.hibernate.annotations.ColumnDefault;
+
+import javax.persistence.*;
+
+/**
+ * @author wind
+ */
+@Entity
+@Table(name = "course_project")
+public class CourseProject {
+    @ManyToOne
+    @JoinColumn(name = "course_id", nullable = false,
+            foreignKey = @ForeignKey(name = "course_project_course_id_fk"))
+    private Course course;
+
+    @Id
+    @Column(unique = true, nullable = false)
+    private Integer projectId;
+
+    @Column(nullable = false)
+    private Boolean isPublic;
+
+    public Course getCourse() {
+        return course;
+    }
+
+    public void setCourse(Course course) {
+        this.course = course;
+    }
+
+    public Integer getProjectId() {
+        return projectId;
+    }
+
+    public void setProjectId(Integer projectId) {
+        this.projectId = projectId;
+    }
+
+    public Boolean getPublic() {
+        return isPublic;
+    }
+
+    public void setPublic(Boolean aPublic) {
+        isPublic = aPublic;
+    }
+}

+ 1 - 1
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/CourseSelection.java

@@ -1,6 +1,6 @@
 package cn.seecoder.seecoderportalserver.domain;
 
-import cn.seecoder.seecoderportalcommon.vo.CourseSelectionKey;
+import cn.seecoder.seecoderportalserver.domain.key.CourseSelectionKey;
 
 import javax.persistence.*;
 import javax.validation.constraints.NotNull;

+ 1 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/MessageRecord.java

@@ -1,8 +1,7 @@
 package cn.seecoder.seecoderportalserver.domain;
 
 
-import cn.seecoder.seecoderportalcommon.vo.MessageRecordKey;
-import org.hibernate.validator.constraints.br.CPF;
+import cn.seecoder.seecoderportalserver.domain.key.MessageRecordKey;
 
 import javax.persistence.*;
 import javax.validation.constraints.NotNull;

+ 26 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/group/Group.java

@@ -0,0 +1,26 @@
+package cn.seecoder.seecoderportalserver.domain.group;
+
+/**
+ * @author wind
+ */
+
+public class Group {
+    private Long id;
+    private String name;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}

+ 25 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/group/GroupMember.java

@@ -0,0 +1,25 @@
+package cn.seecoder.seecoderportalserver.domain.group;
+
+/**
+ * @author wind
+ */
+public class GroupMember {
+    private Long userId;
+    private Long groupId;
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public Long getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(Long groupId) {
+        this.groupId = groupId;
+    }
+}

+ 85 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/group/GroupRule.java

@@ -0,0 +1,85 @@
+package cn.seecoder.seecoderportalserver.domain.group;
+
+import cn.seecoder.seecoderportalserver.domain.Course;
+import org.hibernate.annotations.CreationTimestamp;
+
+import javax.persistence.*;
+import java.time.OffsetDateTime;
+
+/**
+ * @author mjh
+ */
+
+@Entity
+@Table(name = "group_rule")
+public class GroupRule {
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    private Long id;
+
+    @OneToOne
+    @JoinColumn(name = "course_id", nullable = false, unique = true,
+            foreignKey = @ForeignKey(name = "group_rule_course_id_fk"))
+    private Course course;
+
+    @Column(nullable = false)
+    private Integer maxSize;
+
+    @Column(nullable = false)
+    private Integer maxNameLen;
+
+    @Column(nullable = false)
+    @CreationTimestamp
+    private OffsetDateTime createdTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Course getCourse() {
+        return course;
+    }
+
+    public void setCourse(Course course) {
+        this.course = course;
+    }
+
+    public Integer getMaxSize() {
+        return maxSize;
+    }
+
+    public void setMaxSize(Integer maxSize) {
+        this.maxSize = maxSize;
+    }
+
+    public Integer getMaxNameLen() {
+        return maxNameLen;
+    }
+
+    public void setMaxNameLen(Integer maxNameLen) {
+        this.maxNameLen = maxNameLen;
+    }
+
+    public OffsetDateTime getCreatedTime() {
+        return createdTime;
+    }
+
+    public void setCreatedTime(OffsetDateTime createdTime) {
+        this.createdTime = createdTime;
+    }
+
+    @Override
+    public String toString() {
+        return "GroupRule{" +
+                "id=" + id +
+                ", course=" + course +
+                ", maxSize=" + maxSize +
+                ", maxNameLen=" + maxNameLen +
+                ", createdTime=" + createdTime +
+                '}';
+    }
+}

+ 1 - 1
seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/AnnouncementRecordKey.java → seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/key/AnnouncementRecordKey.java

@@ -1,4 +1,4 @@
-package cn.seecoder.seecoderportalcommon.vo;
+package cn.seecoder.seecoderportalserver.domain.key;
 
 import java.io.Serializable;
 

+ 1 - 1
seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/CourseSelectionKey.java → seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/key/CourseSelectionKey.java

@@ -1,4 +1,4 @@
-package cn.seecoder.seecoderportalcommon.vo;
+package cn.seecoder.seecoderportalserver.domain.key;
 
 import java.io.Serializable;
 

+ 1 - 1
seecoder-portal-common/src/main/java/cn/seecoder/seecoderportalcommon/vo/MessageRecordKey.java → seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/domain/key/MessageRecordKey.java

@@ -1,4 +1,4 @@
-package cn.seecoder.seecoderportalcommon.vo;
+package cn.seecoder.seecoderportalserver.domain.key;
 
 import java.io.Serializable;
 

+ 27 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/mapper/group/GroupMapper.java

@@ -0,0 +1,27 @@
+package cn.seecoder.seecoderportalserver.mapper.group;
+
+import cn.seecoder.seecoderportalcommon.vo.group.GroupVO;
+import cn.seecoder.seecoderportalserver.domain.group.Group;
+import org.mapstruct.Mapper;
+
+/**
+ * @author wind
+ */
+@Mapper(componentModel = "spring")
+public interface GroupMapper {
+    /**
+     * vo2po
+     *
+     * @param groupVO vo
+     * @return po
+     */
+    Group toGroup(GroupVO groupVO);
+
+    /**
+     * po2vo
+     *
+     * @param group po
+     * @return vo
+     */
+    GroupVO toGroupVO(Group group);
+}

+ 27 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/mapper/group/GroupMemberMapper.java

@@ -0,0 +1,27 @@
+package cn.seecoder.seecoderportalserver.mapper.group;
+
+import cn.seecoder.seecoderportalcommon.vo.group.GroupMemberVO;
+import cn.seecoder.seecoderportalserver.domain.group.GroupMember;
+import org.mapstruct.Mapper;
+
+/**
+ * @author wind
+ */
+@Mapper(componentModel = "spring")
+public interface GroupMemberMapper {
+    /**
+     * po2vo
+     *
+     * @param groupMember po
+     * @return vo
+     */
+    GroupMemberVO toGroupMemberVO(GroupMember groupMember);
+
+    /**
+     * vo2po
+     *
+     * @param groupMemberVO vo
+     * @return po
+     */
+    GroupMember toGroupMember(GroupMemberVO groupMemberVO);
+}

+ 31 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/mapper/group/GroupRuleMapper.java

@@ -0,0 +1,31 @@
+package cn.seecoder.seecoderportalserver.mapper.group;
+
+import cn.seecoder.seecoderportalcommon.vo.group.GroupRuleVO;
+import cn.seecoder.seecoderportalserver.domain.group.GroupRule;
+import cn.seecoder.seecoderportalserver.mapper.CourseMapper;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+/**
+ * @author wind
+ */
+@Mapper(componentModel = "spring", uses = {CourseMapper.class})
+public interface GroupRuleMapper {
+    /**
+     * po2vo
+     *
+     * @param groupRule po
+     * @return vo
+     */
+    @Mapping(source = "course", target = "courseVO")
+    GroupRuleVO toGroupRuleVO(GroupRule groupRule);
+
+    /**
+     * vo2po
+     *
+     * @param groupRuleVO vo
+     * @return po
+     */
+    @Mapping(source = "courseVO", target = "course")
+    GroupRule toGroupRule(GroupRuleVO groupRuleVO);
+}

+ 4 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/mapper/impl/CourseMapperImpl.java

@@ -25,7 +25,9 @@ public class CourseMapperImpl implements CourseMapper {
 
         courseVO.setId(course.getId());
         courseVO.setName(course.getName());
-        courseVO.setTeacher(userService.findUserById(course.getTeacherId()));
+        courseVO.setTeacher(
+                course.getTeacherId() == null ?
+                        null : userService.findUserById(course.getTeacherId()));
         courseVO.setSemester(course.getSemester());
         courseVO.setImageUrl(course.getImageUrl());
         courseVO.setDescription(course.getDescription());
@@ -53,7 +55,7 @@ public class CourseMapperImpl implements CourseMapper {
 
         course.setId(courseVO.getId());
         course.setName(courseVO.getName());
-        course.setTeacherId(courseVO.getTeacher().getId());
+        course.setTeacherId(courseVO.getTeacher() == null ? null : courseVO.getTeacher().getId());
         course.setSemester(courseVO.getSemester());
         course.setImageUrl(courseVO.getImageUrl());
         course.setDescription(courseVO.getDescription());

+ 1 - 1
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/AnnouncementRecordRepository.java

@@ -1,6 +1,6 @@
 package cn.seecoder.seecoderportalserver.repository;
 
-import cn.seecoder.seecoderportalcommon.vo.AnnouncementRecordKey;
+import cn.seecoder.seecoderportalserver.domain.key.AnnouncementRecordKey;
 import cn.seecoder.seecoderportalserver.domain.AnnouncementRecord;
 import org.springframework.data.jpa.repository.JpaRepository;
 

+ 28 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/CourseProjectRepository.java

@@ -0,0 +1,28 @@
+package cn.seecoder.seecoderportalserver.repository;
+
+import cn.seecoder.seecoderportalserver.domain.CourseProject;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+/**
+ * @author wind
+ */
+public interface CourseProjectRepository extends JpaRepository<CourseProject, Integer> {
+    /**
+     * 根据课程id查找课程项目
+     *
+     * @param courseId 课程id
+     * @return List,包含了当前课程下的所有项目id
+     */
+    List<CourseProject> findByCourse_Id(Long courseId);
+
+    /**
+     * 根据课程id与是否公开查找课程项目
+     *
+     * @param course_id Long
+     * @param isPublic Boolean
+     * @return List
+     */
+    List<CourseProject> findByCourse_IdAndIsPublic(Long course_id, Boolean isPublic);
+}

+ 1 - 1
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/CourseSelectionRepository.java

@@ -1,7 +1,7 @@
 package cn.seecoder.seecoderportalserver.repository;
 
 import cn.seecoder.seecoderportalserver.domain.CourseSelection;
-import cn.seecoder.seecoderportalcommon.vo.CourseSelectionKey;
+import cn.seecoder.seecoderportalserver.domain.key.CourseSelectionKey;
 import org.springframework.data.jpa.repository.JpaRepository;
 
 import java.util.List;

+ 1 - 5
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/MessageRecordRepository.java

@@ -1,12 +1,8 @@
 package cn.seecoder.seecoderportalserver.repository;
 
-import cn.seecoder.seecoderportalcommon.vo.MessageRecordKey;
-import cn.seecoder.seecoderportalserver.domain.Message;
+import cn.seecoder.seecoderportalserver.domain.key.MessageRecordKey;
 import cn.seecoder.seecoderportalserver.domain.MessageRecord;
 import org.springframework.data.jpa.repository.JpaRepository;
 
-import javax.validation.constraints.NotNull;
-import java.util.Optional;
-
 public interface MessageRecordRepository extends JpaRepository<MessageRecord, MessageRecordKey> {
 }

+ 58 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/group/GroupMemberRepository.java

@@ -0,0 +1,58 @@
+package cn.seecoder.seecoderportalserver.repository.group;
+
+import cn.seecoder.seecoderportalserver.domain.group.GroupMember;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * @author wind
+ */
+@Repository
+public interface GroupMemberRepository extends BaseMapper<GroupMember> {
+    /**
+     * 新建课程小组成员表
+     *
+     * @param name      String 表名称
+     * @param groupName String 小组表名称
+     */
+    @Insert("create table if not exists ${name} (\n" +
+            "user_id bigint not null references portal_user(id),\n" +
+            "group_id bigint not null references ${groupName}(id),\n" +
+            "primary key (user_id)\n" +
+            ")")
+    void createGroupMemberTable(@Param("name") String name, @Param("groupName") String groupName);
+
+    /**
+     * 根据userId查找课程小组成员
+     *
+     * @param name   String 表名称
+     * @param userId Long
+     * @return GroupMember
+     */
+    @Select("select * from ${name} where user_id=${userId}")
+    GroupMember findByUserId(@Param("name") String name, @Param("userId") Long userId);
+
+    /**
+     * 新建课程小组成员
+     *
+     * @param name  String
+     * @param group Group
+     */
+    @Insert("insert into ${name} (user_id, group_id) values (${vo.userId}, ${vo.groupId})")
+    void createGroupMember(@Param("name") String name, @Param("vo") GroupMember group);
+
+    /**
+     * 根据groupId查找所有课程小组成员
+     *
+     * @param name String 表名称
+     * @param groupId Long
+     * @return List
+     */
+    @Select("select * from ${name} where group_id=${groupId}")
+    List<GroupMember> findByGroupId(@Param("name") String name, @Param("groupId") Long groupId);
+}

+ 57 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/group/GroupRepository.java

@@ -0,0 +1,57 @@
+package cn.seecoder.seecoderportalserver.repository.group;
+
+import cn.seecoder.seecoderportalserver.domain.group.Group;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.*;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * @author wind
+ */
+
+@Repository
+public interface GroupRepository extends BaseMapper<Group> {
+    /**
+     * 新建表
+     *
+     * @param name 表名
+     * @param len  名称长度
+     */
+    @Insert("create table if not exists ${name} (\n" +
+            "id bigserial primary key,\n" +
+            "name varchar(${len}) not null,\n" +
+            "unique (name)" +
+            ")")
+    void createGroupTable(@Param("name") String name, @Param("len") String len);
+
+    /**
+     * 创建小组
+     *
+     * @param group 小组信息
+     * @param name  表名
+     */
+    @Insert("insert into ${name} (name) values ('${group.name}')")
+    @Options(useGeneratedKeys = true, keyProperty = "group.id")
+    void createGroup(@Param("name") String name, @Param("group") Group group);
+
+    /**
+     * 根据Id查找课程小组
+     *
+     * @param name String 表名
+     * @param id Long
+     * @return Group
+     */
+    @Select("select * from ${name} where id=${id}")
+    Group findGroupById(@Param("name") String name, @Param("id") Long id);
+
+    /**
+     * 查找全部课程小组
+     *
+     * @param name String 表名称
+     * @return List
+     */
+    @Select("select * from ${name}")
+    List<Group> findAll(@Param("name") String name);
+}

+ 19 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/repository/group/GroupRuleRepository.java

@@ -0,0 +1,19 @@
+package cn.seecoder.seecoderportalserver.repository.group;
+
+import cn.seecoder.seecoderportalserver.domain.group.GroupRule;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+/**
+ * @author wind
+ */
+public interface GroupRuleRepository extends JpaRepository<GroupRule, Long> {
+    /**
+     * 根据课程id查找课程规则
+     *
+     * @param courseId Long 课程id
+     * @return GroupRule
+     */
+    Optional<GroupRule> findByCourse_Id(Long courseId);
+}

+ 17 - 5
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/CourseService.java

@@ -1,9 +1,8 @@
 package cn.seecoder.seecoderportalserver.service;
 
 import cn.seecoder.seecoderportalcommon.vo.*;
-import cn.seecoder.seecoderportalserver.domain.Course;
-import cn.seecoder.seecoderportalserver.web.rest.errors.CustomErrorException;
-import com.alibaba.fastjson.JSONObject;
+import cn.seecoder.seecoderportalserver.domain.key.CourseSelectionKey;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
@@ -17,8 +16,6 @@ public interface CourseService {
 
     List<CourseVO> findAllCourses();
 
-    JSONObject findAllCoderCourses(int page, int limit);
-
     List<CourseVO> findTeacherCourses(Long teacherId);
 
     List<CourseVO> findStudentCourses(Long studentId);
@@ -43,4 +40,19 @@ public interface CourseService {
 
     Boolean isCourseNameExisted(String name);
 
+    /**
+     * 上传课程图片
+     * @param name 课程名称
+     * @param file 课程图片文件
+     * @return 图片上传到阿里云oss后返回的url
+     */
+    String uploadCourseImage(String name, MultipartFile file);
+
+    /**
+     * 根据课程id判断是否可以访问
+     *
+     * @param id Long
+     * @return Boolean
+     */
+    Boolean isCourseAccessible(Long id);
 }

+ 62 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/GroupService.java

@@ -0,0 +1,62 @@
+package cn.seecoder.seecoderportalserver.service;
+
+
+import cn.seecoder.seecoderportalcommon.vo.dto.GroupDTO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupMemberVO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupRuleVO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupVO;
+
+import java.util.List;
+
+/**
+ * @author wind
+ */
+public interface GroupService {
+    /**
+     * 创建课程小组规则
+     *
+     * @param ruleVO 课程小组规则
+     * @return GroupRuleVO
+     */
+    GroupRuleVO createGroupRule(GroupRuleVO ruleVO);
+
+    /**
+     * 根据课程id查找课程小组规则
+     *
+     * @param courseId Long
+     * @return GroupRuleVO
+     */
+    GroupRuleVO findGroupRuleByCourseId(Long courseId);
+
+    /**
+     * 创建小组
+     *
+     * @param groupVO 小组信息(不包含Id)
+     * @return 小组信息(包含Id)
+     */
+    GroupDTO createGroup(GroupVO groupVO);
+
+    /**
+     * 添加小组成员
+     *
+     * @param groupMemberVO GroupMemberVO
+     * @return GroupMemberVO
+     */
+    GroupDTO addGroupMember(GroupMemberVO groupMemberVO);
+
+    /**
+     * 查找某个课程小组
+     *
+     * @param courseId Long
+     * @return GroupDTO
+     */
+    GroupDTO findGroup(Long courseId);
+
+    /**
+     * 查找课程下的全部小组
+     *
+     * @param courseId Long
+     * @return List
+     */
+    List<GroupDTO> findAllGroupsByCourse(Long courseId);
+}

+ 80 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/ProjectService.java

@@ -0,0 +1,80 @@
+package cn.seecoder.seecoderportalserver.service;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+import java.util.HashMap;
+
+/**
+ * @author wind
+ */
+public interface ProjectService {
+    /**
+     * 查找全部项目
+     *
+     * @return JSONArray from devcloud
+     */
+    JSONArray findAllProjects();
+
+    /**
+     * 查找全部课程项目
+     *
+     * @return HashMap course: JSONArray
+     */
+    HashMap<String, JSONArray> findCourseProjects();
+
+    /**
+     * 查找全部非课程项目
+     *
+     * @return JSONArray
+     */
+    JSONArray findNonCourseProjects();
+
+    /**
+     * 根据Id查找项目
+     *
+     * @param id Integer projectId
+     * @return JSONObject project
+     */
+    JSONObject findProjectById(Integer id);
+
+    /**
+     * 根据课程id查找全部课程项目
+     *
+     * @param courseId 课程id
+     * @return JSONObject,public:课程下的公开项目,private:课程下的个人项目(因人而异)
+     */
+    JSONObject findProjectsByCourse(Long courseId);
+
+    /**
+     * 新建项目
+     *
+     * @param project 用户填写的项目信息,是否存在字段courseId表示是否为课程项目
+     * @return JSONObject 新建完成的项目信息
+     */
+    JSONObject createProject(JSONObject project);
+
+    /**
+     * 切换课程项目的公开或私有状态
+     *
+     * @param projectId Integer
+     * @return Boolean
+     */
+    Boolean switchCourseProjectState(Integer projectId);
+
+    /**
+     * fork公开项目
+     *
+     * @param projectId Integer
+     * @return JSONObject
+     */
+    JSONObject forkPublicProject(Integer projectId);
+
+    /**
+     * 查找项目成员
+     *
+     * @param projectId Integer
+     * @return JSONArray
+     */
+    JSONArray findProjectMembers(Integer projectId);
+}

+ 25 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/subsystem/DevcloudService.java

@@ -0,0 +1,25 @@
+package cn.seecoder.seecoderportalserver.service.subsystem;
+
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+public interface DevcloudService {
+    JSONArray listProjectsByUserId();
+
+    JSONObject getProjectById(Integer id);
+
+    /**
+     * 根据项目id获取项目成员
+     * @param id 项目id
+     * @return JSONArray,包含了所有的项目成员
+     */
+    JSONArray getProjectMembers(Integer id);
+
+    /**
+     * 调用devcloud系统新建项目
+     * @param project 无id项目信息
+     * @return 有id项目信息
+     */
+    JSONObject createProject(JSONObject project);
+}

+ 1 - 1
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/SubsystemService.java → seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/service/subsystem/SubsystemService.java

@@ -1,4 +1,4 @@
-package cn.seecoder.seecoderportalserver.service;
+package cn.seecoder.seecoderportalserver.service.subsystem;
 
 import cn.seecoder.seecoderportalcommon.vo.CourseVO;
 import com.alibaba.fastjson.JSONObject;

+ 15 - 4
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/AnnouncementServiceImpl.java

@@ -3,6 +3,7 @@ package cn.seecoder.seecoderportalserver.serviceImpl;
 import cn.seecoder.seecoderportalcommon.vo.*;
 import cn.seecoder.seecoderportalserver.domain.Announcement;
 import cn.seecoder.seecoderportalserver.domain.AnnouncementRecord;
+import cn.seecoder.seecoderportalserver.domain.key.AnnouncementRecordKey;
 import cn.seecoder.seecoderportalserver.mapper.AnnouncementMapper;
 import cn.seecoder.seecoderportalserver.repository.AnnouncementRecordRepository;
 import cn.seecoder.seecoderportalserver.repository.AnnouncementRepository;
@@ -19,6 +20,9 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 
+/**
+ * @author wind
+ */
 @Service
 public class AnnouncementServiceImpl implements AnnouncementService {
     private final AnnouncementRepository announcementRepository;
@@ -35,6 +39,7 @@ public class AnnouncementServiceImpl implements AnnouncementService {
         this.userService = userService;
     }
 
+    @Override
     public AnnouncementVO create(AnnouncementVO announcementVO, Long userId) {
         announcementVO.getSender().setId(userId);
         Announcement announcement = announcementMapper.toAnnouncement(announcementVO);
@@ -43,6 +48,7 @@ public class AnnouncementServiceImpl implements AnnouncementService {
         return announcementMapper.toAnnouncementVO(result);
     }
 
+    @Override
     public String delete(Long id, Long userId) {
         AnnouncementVO announcement = this.findAnnouncementById(id);
         if (!announcement.getSender().getId().equals(userId)) {
@@ -53,6 +59,7 @@ public class AnnouncementServiceImpl implements AnnouncementService {
         return "公告删除完成。";
     }
 
+    @Override
     public AnnouncementVO update(AnnouncementVO announcementVO, Long userId) {
         AnnouncementVO announcement = this.findAnnouncementById(announcementVO.getId());
         if (!userId.equals(announcement.getSender().getId())) {
@@ -65,6 +72,7 @@ public class AnnouncementServiceImpl implements AnnouncementService {
         return announcementMapper.toAnnouncementVO(result);
     }
 
+    @Override
     public AnnouncementVO findAnnouncementById(Long id, Long userId) {
         Announcement announcement = announcementRepository.findById(id).orElseThrow(CustomErrorException::announcementNotFound);
         UserVO user = userService.findUserById(userId);
@@ -73,6 +81,7 @@ public class AnnouncementServiceImpl implements AnnouncementService {
         return result;
     }
 
+    @Override
     public AnnouncementVO findAnnouncementById(Long id) {
         Announcement announcement = announcementRepository.findById(id).orElseThrow(CustomErrorException::announcementNotFound);
         AnnouncementVO result = announcementMapper.toAnnouncementVO(announcement);
@@ -80,6 +89,7 @@ public class AnnouncementServiceImpl implements AnnouncementService {
         return result;
     }
 
+    @Override
     public List<AnnouncementVO> findAnnouncementsByCourseId(Long courseId) {
         courseService.findCourseById(courseId);
         DomainUserDetails user = SecurityUtils.getCurrentUser();
@@ -88,12 +98,11 @@ public class AnnouncementServiceImpl implements AnnouncementService {
                 .filter(announcement -> announcement.getState() != ObjectState.DELETED)
                 .map(announcementMapper::toAnnouncementVO)
                 .collect(Collectors.toList());
-        result.forEach(announcementVO -> {
-            announcementVO.setRead(userService.isTeacher(user.getRole()) || this.getReadState(announcementVO.getId(), user.getId()));
-        });
+        result.forEach(announcementVO -> announcementVO.setRead(userService.isTeacher(user.getRole()) || this.getReadState(announcementVO.getId(), user.getId())));
         return result;
     }
 
+    @Override
     public List<AnnouncementVO> findUnreadAnnouncementsByUserId(Long userId) {
         UserVO user = userService.findUserById(userId);
         if (userService.isStudent(user.getRole())) {
@@ -134,7 +143,7 @@ public class AnnouncementServiceImpl implements AnnouncementService {
             }
             announcementList.addAll(recordButUnread);
             return announcementList.stream()
-                    .map(id -> {return this.findAnnouncementById(id, userId);})
+                    .map(id -> this.findAnnouncementById(id, userId))
                     .collect(Collectors.toList());
         } else {
             return new ArrayList<>();
@@ -150,12 +159,14 @@ public class AnnouncementServiceImpl implements AnnouncementService {
         }
     }
 
+    @Override
     public List<AnnouncementVO> findAnnouncementsByUserId(Long userId) {
         return announcementRepository.findAnnouncementsBySenderIdAndStateNot(userId, ObjectState.DELETED).stream()
                 .map(announcementMapper::toAnnouncementVO)
                 .collect(Collectors.toList());
     }
 
+    @Override
     public String readAnnouncement(Long id, Long userId) {
         AnnouncementRecord record = announcementRecordRepository.findById(new AnnouncementRecordKey(id, userId)).orElseThrow(CustomErrorException::announcementRecordNotFound);
         record.setState(true);

+ 56 - 20
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/CourseServiceImpl.java

@@ -1,6 +1,6 @@
 package cn.seecoder.seecoderportalserver.serviceImpl;
 
-import cn.seecoder.seecoderportalcommon.vo.CourseSelectionKey;
+import cn.seecoder.seecoderportalserver.domain.key.CourseSelectionKey;
 import cn.seecoder.seecoderportalcommon.vo.CourseVO;
 import cn.seecoder.seecoderportalcommon.vo.ObjectState;
 import cn.seecoder.seecoderportalcommon.vo.UserVO;
@@ -9,13 +9,20 @@ import cn.seecoder.seecoderportalserver.domain.CourseSelection;
 import cn.seecoder.seecoderportalserver.mapper.CourseMapper;
 import cn.seecoder.seecoderportalserver.repository.CourseRepository;
 import cn.seecoder.seecoderportalserver.repository.CourseSelectionRepository;
+import cn.seecoder.seecoderportalserver.security.DomainUserDetails;
+import cn.seecoder.seecoderportalserver.security.SecurityUtils;
 import cn.seecoder.seecoderportalserver.service.CourseService;
 import cn.seecoder.seecoderportalserver.service.UserService;
+import cn.seecoder.seecoderportalserver.serviceImpl.subsystem.CoderService;
+import cn.seecoder.seecoderportalserver.serviceImpl.subsystem.EvalService;
+import cn.seecoder.seecoderportalserver.serviceImpl.subsystem.HelperService;
+import cn.seecoder.seecoderportalserver.utility.OssUtil;
 import cn.seecoder.seecoderportalserver.web.rest.errors.CustomErrorException;
-import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -28,8 +35,9 @@ public class CourseServiceImpl implements CourseService {
     private final CoderService coderService;
     private final HelperService helperService;
     private final EvalService evalService;
+    private final OssUtil ossUtil;
 
-    public CourseServiceImpl(CourseRepository courseRepository, CourseMapper courseMapper, CourseSelectionRepository courseSelectionRepository, CoderService coderService, HelperService helperService, EvalService evalService, UserService userService) {
+    public CourseServiceImpl(CourseRepository courseRepository, CourseMapper courseMapper, CourseSelectionRepository courseSelectionRepository, CoderService coderService, HelperService helperService, EvalService evalService, UserService userService, OssUtil ossUtil) {
         this.courseRepository = courseRepository;
         this.courseMapper = courseMapper;
         this.courseSelectionRepository = courseSelectionRepository;
@@ -37,8 +45,10 @@ public class CourseServiceImpl implements CourseService {
         this.coderService = coderService;
         this.helperService = helperService;
         this.evalService = evalService;
+        this.ossUtil = ossUtil;
     }
 
+    @Override
     public CourseVO createCourse(CourseVO courseVO, Long userId) {
         // 确认授课教师
         UserVO teacher = userService.findUserById(userId);
@@ -46,9 +56,9 @@ public class CourseServiceImpl implements CourseService {
             throw new CustomErrorException("当前用户无权创建课程");
         }
 
+        // 子系统:eval、coder
         Integer coderId;
         Integer evalId;
-
         // 创建Coder课程
         if (courseVO.getCoderId() != null) {
             JSONObject coderRes = coderService.createCourse(courseVO);
@@ -71,12 +81,14 @@ public class CourseServiceImpl implements CourseService {
                 courseVO.setEvalId(evalId);
             }
         }
+
         Course course = courseMapper.courseVOToCourse(courseVO);
         course.setState(ObjectState.NORMAL);
         Course result = courseRepository.save(course);
         return courseMapper.courseToCourseVO(result);
     }
 
+    @Override
     public CourseVO updateCourse(CourseVO courseVO, Long userId) {
         CourseVO course = this.findCourseById(courseVO.getId());
         if (!userId.equals(course.getTeacher().getId())) {
@@ -95,6 +107,7 @@ public class CourseServiceImpl implements CourseService {
         return courseMapper.courseToCourseVO(result);
     }
 
+    @Override
     public String deleteCourse(Long courseId, Long userId) {
         CourseVO course = this.findCourseById(courseId);
         if (!userId.equals(course.getTeacher().getId())) {
@@ -107,6 +120,7 @@ public class CourseServiceImpl implements CourseService {
         return "课程删除成功";
     }
 
+    @Override
     public List<CourseVO> findAllCourses() {
         List<Course> courses = courseRepository.findAll();
         return courses.stream()
@@ -115,18 +129,7 @@ public class CourseServiceImpl implements CourseService {
                 .collect(Collectors.toList());
     }
 
-    /**
-     * 查询Coder中的所有课程。TODO 子系统课程与门户系统课程中一致
-     * @param page
-     * @param limit
-     * @return
-     */
     @Override
-    public JSONObject findAllCoderCourses(int page, int limit) {
-        return coderService.getAllCourse(page, limit);
-    }
-
-
     public List<CourseVO> findTeacherCourses(Long teacherId) {
         List<Course> courses = courseRepository.findCoursesByTeacherId(teacherId);
         return courses.stream()
@@ -135,6 +138,7 @@ public class CourseServiceImpl implements CourseService {
                 .collect(Collectors.toList());
     }
 
+    @Override
     public List<CourseVO> findStudentCourses(Long studentId) {
         List<CourseSelection> courseSelections = courseSelectionRepository.findCourseSelectionsByUserId(studentId);
         return courseSelections.stream()
@@ -143,6 +147,7 @@ public class CourseServiceImpl implements CourseService {
                 .collect(Collectors.toList());
     }
 
+    @Override
     public List<CourseVO> findRecommendCourses() {
         List<Course> courses = courseRepository.findCoursesByState(ObjectState.EXAMPLE);
         return courses.stream()
@@ -150,24 +155,29 @@ public class CourseServiceImpl implements CourseService {
                 .collect(Collectors.toList());
     }
 
+    @Override
     public CourseVO findCourseByCoderId(Integer coderId) {
         Course course = courseRepository.findCourseByCoderId(coderId).orElseThrow(CustomErrorException::courseNotFound);
         return course.getState() == ObjectState.DELETED ? null : courseMapper.courseToCourseVO(course);
     }
 
+    @Override
     public Boolean checkCourseNameUnique(Long id, String name) {
         Course course = courseRepository.findCourseByName(name).orElse(null);
         return course == null || course.getId().equals(id);
     }
 
+    @Override
     public CourseVO findCourseById(Long id) {
         Course course = courseRepository.findById(id)
                 .orElseThrow(CustomErrorException::courseNotFound);
-        if (course.getState().equals(ObjectState.DELETED))
+        if (course.getState() == ObjectState.DELETED) {
             throw CustomErrorException.courseNotFound();
+        }
         return courseMapper.courseToCourseVO(course);
     }
 
+    @Override
     public String confirmCourseSelection(Long courseId, Long userId) {
         UserVO userVO = userService.findUserById(userId);
         assert userService.isStudent(userVO.getRole());
@@ -176,9 +186,6 @@ public class CourseServiceImpl implements CourseService {
         // coder系统选课
         if (course.getCoderId() != null) {
             JSONObject coderRes = coderService.chooseCourse(course.getInvitationCode());
-//            if (!coderRes.containsKey("res")) {
-//                throw new CustomErrorException("Coder: " + coderRes.get("msg"));
-//            }
         }
         // eval系统选课
         if (course.getEvalId() != null) {
@@ -194,6 +201,7 @@ public class CourseServiceImpl implements CourseService {
         return "选课成功";
     }
 
+    @Override
     public String cancelCourseSelection(CourseSelectionKey courseSelectionKey, Long userId) {
         UserVO user = userService.findUserById(userId);
         if (!user.getId().equals(courseSelectionKey.getUserId())) {
@@ -209,26 +217,54 @@ public class CourseServiceImpl implements CourseService {
         }
     }
 
+    @Override
     public Boolean checkSelection(Long userId, Long courseId) {
         CourseSelectionKey key = new CourseSelectionKey(userId, courseId);
         CourseSelection record = courseSelectionRepository.findById(key).orElse(null);
         return record != null && record.getState();
     }
 
+    @Override
     public Boolean checkCourseCodeUnique(Long id, String code) {
         Course course = courseRepository.findCourseByInvitationCode(code).orElse(null);
         return course == null || course.getId().equals(id);
     }
 
-    public CourseVO findCourseByEvalId(int evalId) throws Exception{
+    @Override
+    public CourseVO findCourseByEvalId(int evalId) {
         Course course = courseRepository.findCourseByEvalId(evalId).orElseThrow(CustomErrorException::courseNotFound);
         return course.getState() == ObjectState.DELETED ? null : courseMapper.courseToCourseVO(course);
     }
 
+    @Override
     public Boolean isCourseNameExisted(String name) {
         Course course = courseRepository.findCourseByName(name).orElse(null);
         return course != null;
     }
 
+    @Override
+    public String uploadCourseImage(String name, MultipartFile file) {
+        try {
+            Course course = courseRepository.findCourseByName(name).orElseThrow(CustomErrorException::courseNotFound);
+            if (!course.getTeacherId().equals(SecurityUtils.getCurrentUser().getId())) {
+                throw CustomErrorException.unauthorizedUser();
+            }
+            String imgUrl = ossUtil.upload(name + ".jpg", file.getInputStream());
+            course.setImageUrl(imgUrl);
+            courseRepository.save(course);
+            return imgUrl;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
 
+    @Override
+    public Boolean isCourseAccessible(Long id) {
+        DomainUserDetails user = SecurityUtils.getCurrentUser();
+        CourseVO courseVO = findCourseById(id);
+        if(user.getId().equals(courseVO.getTeacher().getId())){
+            return true;
+        }
+        return checkSelection(user.getId(), id);
+    }
 }

+ 2 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/ExamServiceImpl.java

@@ -1,15 +1,15 @@
 package cn.seecoder.seecoderportalserver.serviceImpl;
 
 import cn.seecoder.seecoderportalcommon.vo.CourseVO;
-import cn.seecoder.seecoderportalserver.domain.Course;
 import cn.seecoder.seecoderportalserver.service.CourseService;
 import cn.seecoder.seecoderportalserver.service.ExamService;
+import cn.seecoder.seecoderportalserver.serviceImpl.subsystem.CoderService;
+import cn.seecoder.seecoderportalserver.serviceImpl.subsystem.EvalService;
 import cn.seecoder.seecoderportalserver.web.rest.errors.CustomErrorException;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import org.springframework.stereotype.Service;
 
-import java.util.ArrayList;
 import java.util.List;
 
 @Service

+ 195 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/GroupServiceImpl.java

@@ -0,0 +1,195 @@
+package cn.seecoder.seecoderportalserver.serviceImpl;
+
+import cn.seecoder.seecoderportalcommon.vo.dto.GroupDTO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupMemberVO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupRuleVO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupVO;
+import cn.seecoder.seecoderportalserver.domain.group.Group;
+import cn.seecoder.seecoderportalserver.domain.group.GroupMember;
+import cn.seecoder.seecoderportalserver.domain.group.GroupRule;
+import cn.seecoder.seecoderportalserver.mapper.group.GroupMapper;
+import cn.seecoder.seecoderportalserver.mapper.group.GroupMemberMapper;
+import cn.seecoder.seecoderportalserver.mapper.group.GroupRuleMapper;
+import cn.seecoder.seecoderportalserver.repository.group.GroupMemberRepository;
+import cn.seecoder.seecoderportalserver.repository.group.GroupRepository;
+import cn.seecoder.seecoderportalserver.repository.group.GroupRuleRepository;
+import cn.seecoder.seecoderportalserver.security.DomainUserDetails;
+import cn.seecoder.seecoderportalserver.security.SecurityUtils;
+import cn.seecoder.seecoderportalserver.service.CourseService;
+import cn.seecoder.seecoderportalserver.service.GroupService;
+import cn.seecoder.seecoderportalserver.service.UserService;
+import cn.seecoder.seecoderportalserver.web.rest.errors.CustomErrorException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author wind
+ */
+@Service
+public class GroupServiceImpl implements GroupService {
+    private final static Logger log = LoggerFactory.getLogger(GroupServiceImpl.class);
+    private final GroupRepository groupRepository;
+    private final GroupMapper groupMapper;
+    private final GroupRuleRepository groupRuleRepository;
+    private final GroupRuleMapper groupRuleMapper;
+    private final GroupMemberRepository groupMemberRepository;
+    private final GroupMemberMapper groupMemberMapper;
+    private final CourseService courseService;
+    private final UserService userService;
+
+    public GroupServiceImpl(GroupRepository groupRepository, GroupMapper groupMapper, GroupRuleRepository groupRuleRepository, GroupRuleMapper groupRuleMapper, GroupMemberRepository groupMemberRepository, GroupMemberMapper groupMemberMapper, CourseService courseService, UserService userService) {
+        this.groupRepository = groupRepository;
+        this.groupMapper = groupMapper;
+        this.groupRuleRepository = groupRuleRepository;
+        this.groupRuleMapper = groupRuleMapper;
+        this.groupMemberRepository = groupMemberRepository;
+        this.groupMemberMapper = groupMemberMapper;
+        this.courseService = courseService;
+        this.userService = userService;
+    }
+
+    @Override
+    @Transactional(rollbackFor = RuntimeException.class)
+    public GroupDTO createGroup(GroupVO groupVO) {
+        DomainUserDetails user = SecurityUtils.getCurrentUser();
+        // 验证选课
+        if (!courseService.checkSelection(user.getId(), groupVO.getCourseId())) {
+            throw CustomErrorException.courseSelectionNotFound();
+        }
+        // 验证尚未加入小组
+        if (groupMemberRepository.findByUserId(getGroupMemberTableName(groupVO.getCourseId()), user.getId()) != null) {
+            throw CustomErrorException.groupMemberAlreadyExists();
+        }
+        // 创建小组
+        Group group = groupMapper.toGroup(groupVO);
+        groupRepository.createGroup(getGroupTableName(groupVO.getCourseId()), group);
+        log.info("Create Group: {}", group);
+        // 添加小组成员(当前用户)
+        GroupMemberVO groupMemberVO = new GroupMemberVO();
+        groupMemberVO.setUserId(user.getId());
+        groupMemberVO.setGroupId(group.getId());
+        groupMemberVO.setCourseId(groupVO.getCourseId());
+        addGroupMember(groupMemberVO);
+        return generateGroupDTO(groupVO.getCourseId(), group);
+    }
+
+    @Override
+    @Transactional(rollbackFor = RuntimeException.class)
+    public GroupDTO addGroupMember(GroupMemberVO groupMemberVO) {
+        Long courseId = groupMemberVO.getCourseId();
+        String tableName = getGroupMemberTableName(courseId);
+        DomainUserDetails user = SecurityUtils.getCurrentUser();
+        // 验证选课
+        if (!courseService.checkSelection(user.getId(), courseId)) {
+            throw CustomErrorException.courseSelectionNotFound();
+        }
+        // 验证尚未加入小组
+        if (groupMemberRepository.findByUserId(tableName, user.getId()) != null) {
+            throw CustomErrorException.groupMemberAlreadyExists();
+        }
+        // 验证小组未满员
+        GroupRule groupRule = groupRuleRepository.findByCourse_Id(courseId)
+                .orElseThrow(CustomErrorException::groupRuleNotFound);
+        int currentSize = groupMemberRepository.findByGroupId(tableName, groupMemberVO.getGroupId()).size();
+        if (currentSize == groupRule.getMaxSize()) {
+            throw CustomErrorException.groupAlreadyMaxSize();
+        }
+        // 添加小组成员
+        GroupMember groupMember = groupMemberMapper.toGroupMember(groupMemberVO);
+        groupMemberRepository.createGroupMember(tableName, groupMember);
+        log.info("Create GroupMember: {}", groupMember);
+        Group group = groupRepository.findGroupById(getGroupTableName(courseId), groupMemberVO.getGroupId());
+        return generateGroupDTO(courseId, group);
+    }
+
+    @Override
+    public GroupDTO findGroup(Long courseId) {
+        DomainUserDetails user = SecurityUtils.getCurrentUser();
+        if (!courseService.checkSelection(user.getId(), courseId)) {
+            throw CustomErrorException.courseSelectionNotFound();
+        }
+        GroupMember groupMember = groupMemberRepository.findByUserId(
+                getGroupMemberTableName(courseId),
+                user.getId());
+        if (groupMember == null) {
+            throw CustomErrorException.groupNotFound();
+        }
+        Group group = groupRepository.findGroupById(
+                getGroupTableName(courseId),
+                groupMember.getGroupId());
+        return generateGroupDTO(courseId, group);
+    }
+
+    @Override
+    public List<GroupDTO> findAllGroupsByCourse(Long courseId) {
+        List<GroupDTO> groupDTOList = new ArrayList<>();
+        List<Group> groups = groupRepository.findAll(getGroupTableName(courseId));
+        for (Group group : groups) {
+            groupDTOList.add(generateGroupDTO(courseId, group));
+        }
+        return groupDTOList;
+    }
+
+    @Override
+    @Transactional(rollbackFor = RuntimeException.class)
+    public GroupRuleVO createGroupRule(GroupRuleVO ruleVO) {
+        Long teacherId = courseService
+                .findCourseById(ruleVO.getCourseVO().getId())
+                .getTeacher().getId();
+        if (!SecurityUtils.getCurrentUser().getId().equals(teacherId)) {
+            throw CustomErrorException.unauthorizedUser();
+        }
+        GroupRule groupRule;
+        groupRule = groupRuleRepository.save(groupRuleMapper.toGroupRule(ruleVO));
+        log.info("Edit Course Group Rule: {}", groupRule);
+        log.info("Create Group Table: {}", createGroupTable(ruleVO));
+        log.info("Create Group Member Table: {}", createGroupMemberTable(ruleVO.getCourseVO().getId()));
+        return groupRuleMapper.toGroupRuleVO(groupRule);
+    }
+
+    @Override
+    public GroupRuleVO findGroupRuleByCourseId(Long courseId) {
+        return groupRuleMapper.toGroupRuleVO(groupRuleRepository.findByCourse_Id(courseId).orElseThrow(CustomErrorException::groupRuleNotFound));
+    }
+
+    private String getGroupTableName(Long courseId) {
+        return "group_" + courseId;
+    }
+
+    private String getGroupMemberTableName(Long courseId) {
+        return "group_member_" + courseId;
+    }
+
+    private GroupDTO generateGroupDTO(Long courseId, Group group) {
+        List<GroupMember> members = groupMemberRepository.findByGroupId(
+                getGroupMemberTableName(courseId),
+                group.getId());
+        GroupDTO dto = new GroupDTO();
+        dto.setGroupVO(groupMapper.toGroupVO(group));
+        dto.setMembers(
+                members.stream()
+                        .map(member -> userService.findUserById(member.getUserId()))
+                        .collect(Collectors.toList()));
+        return dto;
+    }
+
+    @Transactional(rollbackFor = RuntimeException.class)
+    protected String createGroupTable(GroupRuleVO groupRuleVO) {
+        String tableName = getGroupTableName(groupRuleVO.getCourseVO().getId());
+        groupRepository.createGroupTable(tableName, groupRuleVO.getMaxNameLen().toString());
+        return tableName;
+    }
+
+    @Transactional(rollbackFor = RuntimeException.class)
+    protected String createGroupMemberTable(Long courseId) {
+        String tableName = getGroupMemberTableName(courseId);
+        groupMemberRepository.createGroupMemberTable(tableName, getGroupTableName(courseId));
+        return tableName;
+    }
+}

+ 1 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/HttpServiceImpl.java

@@ -27,7 +27,6 @@ import java.util.Optional;
 
 @Service
 public class HttpServiceImpl implements HttpService {
-
     public String sendPost(String uri, JSONObject requestBody, String token) throws Exception {
         StringEntity requestEntity = new StringEntity(JSON.toJSONString(requestBody));
         HttpPost httpPost = new HttpPost(uri);
@@ -45,7 +44,7 @@ public class HttpServiceImpl implements HttpService {
 
     public String sendGet(String uri, List<NameValuePair> params, String token) throws Exception {
         String completedUri;
-        if (!params.isEmpty()) {
+        if (params != null && !params.isEmpty()) {
             String requestParam = EntityUtils.toString(new UrlEncodedFormEntity(params, Consts.UTF_8));
             completedUri = uri + "?" + requestParam;
         } else {

+ 1 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/MessageServiceImpl.java

@@ -4,6 +4,7 @@ import cn.seecoder.seecoderportalcommon.vo.*;
 import cn.seecoder.seecoderportalcommon.vo.dto.CourseMessageDTO;
 import cn.seecoder.seecoderportalserver.domain.Message;
 import cn.seecoder.seecoderportalserver.domain.MessageRecord;
+import cn.seecoder.seecoderportalserver.domain.key.MessageRecordKey;
 import cn.seecoder.seecoderportalserver.mapper.MessageMapper;
 import cn.seecoder.seecoderportalserver.repository.MessageRecordRepository;
 import cn.seecoder.seecoderportalserver.repository.MessageRepository;

+ 155 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/ProjectServiceImpl.java

@@ -0,0 +1,155 @@
+package cn.seecoder.seecoderportalserver.serviceImpl;
+
+import cn.seecoder.seecoderportalcommon.vo.UserRole;
+import cn.seecoder.seecoderportalserver.domain.CourseProject;
+import cn.seecoder.seecoderportalserver.mapper.CourseMapper;
+import cn.seecoder.seecoderportalserver.repository.CourseProjectRepository;
+import cn.seecoder.seecoderportalserver.security.SecurityUtils;
+import cn.seecoder.seecoderportalserver.service.CourseService;
+import cn.seecoder.seecoderportalserver.service.ProjectService;
+import cn.seecoder.seecoderportalserver.service.subsystem.DevcloudService;
+import cn.seecoder.seecoderportalserver.web.rest.errors.CustomErrorException;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author wind
+ */
+@Service
+public class ProjectServiceImpl implements ProjectService {
+    private final static Logger log = LoggerFactory.getLogger(GroupServiceImpl.class);
+    private final DevcloudService devcloudService;
+    private final CourseService courseService;
+    private final CourseMapper courseMapper;
+    private final CourseProjectRepository courseProjectRepository;
+
+    public ProjectServiceImpl(DevcloudService devcloudService, CourseService courseService, CourseMapper courseMapper, CourseProjectRepository courseProjectRepository) {
+        this.devcloudService = devcloudService;
+        this.courseService = courseService;
+        this.courseMapper = courseMapper;
+        this.courseProjectRepository = courseProjectRepository;
+    }
+
+    @Override
+    public JSONArray findAllProjects() {
+        return devcloudService.listProjectsByUserId();
+    }
+
+    @Override
+    public HashMap<String, JSONArray> findCourseProjects() {
+        JSONArray projectArray = findAllProjects();
+        HashMap<String, JSONArray> courseProjectsMap = new HashMap<>(16);
+        for (Object o : projectArray) {
+            JSONObject project = (JSONObject) o;
+            CourseProject courseProject = courseProjectRepository.findById((project.getInteger("id"))).orElse(null);
+            if (courseProject != null) {
+                String courseName = courseProject.getCourse().getName();
+                if (!courseProjectsMap.containsKey(courseName)) {
+                    courseProjectsMap.put(courseName, new JSONArray());
+                }
+                courseProjectsMap.get(courseName).add(project);
+            }
+        }
+        return courseProjectsMap;
+    }
+
+    @Override
+    public JSONArray findNonCourseProjects() {
+        JSONArray projectArray = findAllProjects();
+        JSONArray nonCourseProjects = new JSONArray();
+        for (Object o : projectArray) {
+            JSONObject project = (JSONObject) o;
+            CourseProject courseProject = courseProjectRepository.findById((project.getInteger("id"))).orElse(null);
+            if (courseProject == null) {
+                nonCourseProjects.add(project);
+            }
+        }
+        return nonCourseProjects;
+    }
+
+    @Override
+    public JSONObject findProjectById(Integer id) {
+        return devcloudService.getProjectById(id);
+    }
+
+    @Override
+    public JSONObject findProjectsByCourse(Long courseId) {
+        if (!courseService.isCourseAccessible(courseId)) {
+            throw CustomErrorException.unauthorizedUser();
+        }
+        JSONObject projects = new JSONObject();
+        projects.put("public", new JSONArray());
+        projects.put("private", new JSONArray());
+        // public
+        List<CourseProject> publicProjects = courseProjectRepository.findByCourse_IdAndIsPublic(courseId, true);
+        for (CourseProject courseProject : publicProjects) {
+            if (SecurityUtils.getCurrentUser().getRole() == UserRole.TEACHER) {
+                projects.getJSONArray("public").add(findProjectById(courseProject.getProjectId()));
+            } else {
+                JSONObject object = new JSONObject();
+                object.put("id", courseProject.getProjectId());
+                projects.getJSONArray("public").add(object);
+            }
+        }
+        // private
+        JSONArray allProjects = findAllProjects();
+        for (Object o : allProjects) {
+            JSONObject project = (JSONObject) o;
+            CourseProject courseProject = courseProjectRepository.findById(project.getInteger("id")).orElse(null);
+            if (courseProject != null && !courseProject.getPublic()) {
+                projects.getJSONArray("private").add(project);
+            }
+
+        }
+        return projects;
+    }
+
+    @Override
+    @Transactional(rollbackFor = RuntimeException.class)
+    public JSONObject createProject(JSONObject project) {
+        JSONObject devcloudResult = devcloudService.createProject(project);
+        log.info("new project {}", devcloudResult);
+        CourseProject courseProject = new CourseProject();
+        courseProject.setProjectId(devcloudResult.getInteger("id"));
+        courseProject.setCourse(courseMapper.courseVOToCourse(courseService.findCourseById(project.getLong("courseId"))));
+        courseProject.setPublic(project.getBoolean("isPublic"));
+        courseProjectRepository.save(courseProject);
+        log.info("new course project relation {}", courseProject);
+        return devcloudResult;
+    }
+
+    @Override
+    @Transactional(rollbackFor = RuntimeException.class)
+    public Boolean switchCourseProjectState(Integer projectId) {
+        CourseProject courseProject = courseProjectRepository.findById(projectId)
+                .orElseThrow(CustomErrorException::courseProjectNotFound);
+        courseProject.setPublic(!courseProject.getPublic());
+        courseProjectRepository.save(courseProject);
+        return courseProject.getPublic();
+    }
+
+    @Override
+    public JSONObject forkPublicProject(Integer projectId) {
+        // 验证项目存在且公共
+        CourseProject courseProject = courseProjectRepository.findById(projectId)
+                .orElseThrow(CustomErrorException::courseProjectNotFound);
+        if(!courseProject.getPublic()){throw CustomErrorException.courseSelectionNotFound();}
+        // 验证当前用户已选课
+        if(!courseService.checkSelection(SecurityUtils.getCurrentUser().getId(), courseProject.getCourse().getId())){
+            throw CustomErrorException.unauthorizedUser();
+        }
+        return null;
+    }
+
+    @Override
+    public JSONArray findProjectMembers(Integer projectId) {
+        return devcloudService.getProjectMembers(projectId);
+    }
+}

+ 2 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/CoderService.java → seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/subsystem/CoderService.java

@@ -1,8 +1,8 @@
-package cn.seecoder.seecoderportalserver.serviceImpl;
+package cn.seecoder.seecoderportalserver.serviceImpl.subsystem;
 
 import cn.seecoder.seecoderportalcommon.vo.CourseVO;
 import cn.seecoder.seecoderportalserver.service.HttpService;
-import cn.seecoder.seecoderportalserver.service.SubsystemService;
+import cn.seecoder.seecoderportalserver.service.subsystem.SubsystemService;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;

+ 106 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/subsystem/DevcloudServiceImpl.java

@@ -0,0 +1,106 @@
+package cn.seecoder.seecoderportalserver.serviceImpl.subsystem;
+
+import cn.seecoder.seecoderportalserver.service.subsystem.DevcloudService;
+import cn.seecoder.seecoderportalserver.service.HttpService;
+import cn.seecoder.seecoderportalserver.web.rest.errors.CustomErrorException;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import org.apache.http.NameValuePair;
+import org.apache.http.message.BasicNameValuePair;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class DevcloudServiceImpl implements DevcloudService {
+    private final String devcloudUri = "https://devcloud-server.seec.seecoder.cn";
+    private final HttpService httpService;
+
+    public DevcloudServiceImpl(HttpService httpService) {this.httpService = httpService;}
+
+    @Override
+    public JSONArray listProjectsByUserId() {
+        JSONObject result = null;
+        try {
+            result = JSON.parseObject(
+                    httpService.sendGet(
+                            devcloudUri + "/project/listByUser",
+                            null,
+                            httpService.getToken()));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        assert result != null;
+        if (result.containsKey("code") && result.getInteger("code") == 200) {
+            return result.getJSONArray("data");
+        } else {
+            throw CustomErrorException.devcloudError(result.toString());
+        }
+    }
+
+    @Override
+    public JSONObject getProjectById(Integer id) {
+        JSONObject result = null;
+        try {
+            result = JSON.parseObject(
+                    httpService.sendGet(
+                            devcloudUri + "/project/" + id,
+                            null,
+                            httpService.getToken()));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        assert result != null;
+        if (result.containsKey("code") && result.getInteger("code") == 200) {
+            return result.getJSONObject("data");
+        } else {
+            throw CustomErrorException.devcloudError(result.toString());
+        }
+    }
+
+    @Override
+    public JSONArray getProjectMembers(Integer id) {
+        JSONObject result = null;
+        List<NameValuePair> params = new ArrayList<>();
+        params.add(new BasicNameValuePair("projectId", String.valueOf(id)));
+        try {
+            result = JSON.parseObject(
+                    httpService.sendGet(
+                            devcloudUri + "/project/members",
+                            params,
+                            httpService.getToken()));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        assert result != null;
+        if (result.containsKey("code") && result.getInteger("code") == 200) {
+            return result.getJSONArray("data");
+        } else {
+            throw CustomErrorException.devcloudError(result.toString());
+        }
+    }
+
+    @Override
+    public JSONObject createProject(JSONObject project) {
+        JSONObject result = null;
+        try {
+            result = JSONObject.parseObject(
+                    httpService.sendPost(
+                            devcloudUri + "/project/create",
+                            project,
+                            httpService.getToken()
+                    )
+            );
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        assert result != null;
+        if (result.containsKey("code") && result.getInteger("code") == 200) {
+            return result.getJSONObject("data");
+        } else {
+            throw CustomErrorException.devcloudError(result.toString());
+        }
+    }
+}

+ 2 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/EvalService.java → seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/subsystem/EvalService.java

@@ -1,8 +1,8 @@
-package cn.seecoder.seecoderportalserver.serviceImpl;
+package cn.seecoder.seecoderportalserver.serviceImpl.subsystem;
 
 import cn.seecoder.seecoderportalcommon.vo.CourseVO;
 import cn.seecoder.seecoderportalserver.service.HttpService;
-import cn.seecoder.seecoderportalserver.service.SubsystemService;
+import cn.seecoder.seecoderportalserver.service.subsystem.SubsystemService;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import org.apache.http.NameValuePair;

+ 2 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/HelperService.java → seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/serviceImpl/subsystem/HelperService.java

@@ -1,7 +1,7 @@
-package cn.seecoder.seecoderportalserver.serviceImpl;
+package cn.seecoder.seecoderportalserver.serviceImpl.subsystem;
 
 import cn.seecoder.seecoderportalcommon.vo.CourseVO;
-import cn.seecoder.seecoderportalserver.service.SubsystemService;
+import cn.seecoder.seecoderportalserver.service.subsystem.SubsystemService;
 import com.alibaba.fastjson.JSONObject;
 import org.springframework.stereotype.Service;
 

+ 107 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/utility/OssUtil.java

@@ -0,0 +1,107 @@
+package cn.seecoder.seecoderportalserver.utility;
+
+import cn.seecoder.seecoderportalserver.web.rest.errors.CustomErrorException;
+import com.aliyun.oss.*;
+import com.aliyun.oss.model.Bucket;
+import com.aliyun.oss.model.BucketInfo;
+import com.aliyun.oss.model.CannedAccessControlList;
+import com.aliyun.oss.model.PutObjectRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.io.InputStream;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author wind
+ */
+@Component
+@ConfigurationProperties("aliyun.oss")
+public class OssUtil {
+    private static final Logger log = LoggerFactory.getLogger(OssUtil.class);
+    private String endpoint;
+    private String accessKeyId;
+    private String accessKeySecret;
+    private String bucketName;
+
+    public String upload(String objectName, InputStream inputStream) {
+        OSS ossClient = getOss();
+        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
+        try {
+            ossClient.putObject(putObjectRequest);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            throw new CustomErrorException("文件上传失败");
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+        return ossClient.generatePresignedUrl(bucketName, objectName, new Date()).toString().split("\\?Expires")[0];
+    }
+
+    @Async
+    public void delete(String objectName) {
+        OSS ossClient = getOss();
+        try {
+            ossClient.deleteObject(bucketName, objectName);
+        } catch (OSSException | ClientException e) {
+            log.error(e.getMessage());
+            throw new CustomErrorException("文件删除失败");
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
+    private OSS getOss() {
+        return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
+    }
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    public void setEndpoint(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    public String getAccessKeyId() {
+        return accessKeyId;
+    }
+
+    public void setAccessKeyId(String accessKeyId) {
+        this.accessKeyId = accessKeyId;
+    }
+
+    public String getAccessKeySecret() {
+        return accessKeySecret;
+    }
+
+    public void setAccessKeySecret(String accessKeySecret) {
+        this.accessKeySecret = accessKeySecret;
+    }
+
+    public String getBucketName() {
+        return bucketName;
+    }
+
+    public void setBucketName(String bucketName) {
+        this.bucketName = bucketName;
+    }
+
+    public static void main(String[] args) {
+        OssUtil ossUtil = new OssUtil();
+        ossUtil.setEndpoint("https://oss-cn-hangzhou.aliyuncs.com");
+        ossUtil.setAccessKeyId("LTAI4Fi1qmL4iuhH8t7G7r2H");
+        ossUtil.setAccessKeySecret("7tJWyQqa0cMkQGaPMMvtH6c0VLiOO2");
+        ossUtil.setBucketName("seec-portal");
+        OSS oss = ossUtil.getOss();
+        oss.setBucketAcl(ossUtil.getBucketName(), CannedAccessControlList.PublicRead);
+    }
+}

+ 17 - 10
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/CourseController.java

@@ -1,12 +1,15 @@
 package cn.seecoder.seecoderportalserver.web.rest;
 
-import cn.seecoder.seecoderportalcommon.vo.CourseSelectionKey;
+import cn.seecoder.seecoderportalserver.domain.key.CourseSelectionKey;
 import cn.seecoder.seecoderportalcommon.vo.CourseVO;
 import cn.seecoder.seecoderportalserver.security.SecurityUtils;
 import cn.seecoder.seecoderportalserver.service.CourseService;
 import cn.seecoder.seecoderportalserver.utility.OkResponse;
-import com.alibaba.fastjson.JSONObject;
+import com.nju.edu.paasMonitorClient.MonitorClient;
+import com.nju.edu.paasMonitorClient.entity.Call;
+import com.nju.edu.paasMonitorCommon.enums.SeecSystem;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
@@ -19,7 +22,11 @@ public class CourseController {
 
     @GetMapping("/getCourseById")
     public OkResponse<CourseVO> findCourseById(@RequestParam Long id) {
-        return OkResponse.of(courseService.findCourseById(id));
+        Call call = MonitorClient.createCall(SeecSystem.PORTAL,"seec-portal","/api/course/getCourseById");
+        call.start();
+        OkResponse<CourseVO> res = OkResponse.of(courseService.findCourseById(id));
+        call.end();
+        return res;
     }
 
     @PostMapping("/create")
@@ -42,11 +49,6 @@ public class CourseController {
         return OkResponse.of(courseService.findAllCourses());
     }
 
-    @GetMapping("/coder_all")
-    public JSONObject findAllCoderCourses(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int limit) {
-        return courseService.findAllCoderCourses(page, limit);
-    }
-
     @GetMapping("/teacher")
     public OkResponse<List<CourseVO>> findTeacherCourses() {
         return OkResponse.of(courseService.findTeacherCourses(SecurityUtils.getCurrentUser().getId()));
@@ -88,12 +90,12 @@ public class CourseController {
     }
 
     @GetMapping("/checkCode")
-    public OkResponse<Boolean> checkCourseCodeUnique(@RequestParam Long id, @RequestParam String code){
+    public OkResponse<Boolean> checkCourseCodeUnique(@RequestParam Long id, @RequestParam String code) {
         return OkResponse.of(courseService.checkCourseCodeUnique(id, code));
     }
 
     @GetMapping("/getCourseByEvalId")
-    public OkResponse<Long> findCourseByEvalId(@RequestParam int evalId){
+    public OkResponse<Long> findCourseByEvalId(@RequestParam int evalId) {
         try {
             return OkResponse.of(courseService.findCourseByEvalId(evalId).getId());
         } catch (Exception e) {
@@ -101,4 +103,9 @@ public class CourseController {
         }
     }
 
+    @PostMapping("/uploadImage")
+    public OkResponse<String> uploadCourseImage(@RequestPart("name") String name,
+                                                @RequestPart("file") MultipartFile file) {
+        return OkResponse.of(courseService.uploadCourseImage(name, file));
+    }
 }

+ 54 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/GroupController.java

@@ -0,0 +1,54 @@
+package cn.seecoder.seecoderportalserver.web.rest;
+
+import cn.seecoder.seecoderportalcommon.vo.dto.GroupDTO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupMemberVO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupRuleVO;
+import cn.seecoder.seecoderportalcommon.vo.group.GroupVO;
+import cn.seecoder.seecoderportalserver.service.GroupService;
+import cn.seecoder.seecoderportalserver.utility.OkResponse;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author wind
+ */
+@RestController
+@RequestMapping("/api/group")
+public class GroupController {
+    private final GroupService groupService;
+
+    public GroupController(GroupService groupService) {
+        this.groupService = groupService;
+    }
+
+    @PostMapping("/rule")
+    public OkResponse<GroupRuleVO> createGroupRule(@RequestBody GroupRuleVO ruleVO) {
+        return OkResponse.of(groupService.createGroupRule(ruleVO));
+    }
+
+    @GetMapping("/rule")
+    public OkResponse<GroupRuleVO> findGroupRule(@RequestParam Long courseId){
+        return OkResponse.of(groupService.findGroupRuleByCourseId(courseId));
+    }
+
+    @PostMapping
+    public OkResponse<GroupDTO> createGroup(@RequestBody GroupVO groupVO){
+        return OkResponse.of(groupService.createGroup(groupVO));
+    }
+
+    @PostMapping("/member/add")
+    public OkResponse<GroupDTO> addGroupMember(@RequestBody GroupMemberVO groupMemberVO){
+        return OkResponse.of(groupService.addGroupMember(groupMemberVO));
+    }
+
+    @GetMapping
+    public OkResponse<GroupDTO> findGroup(@RequestParam Long courseId){
+        return OkResponse.of(groupService.findGroup(courseId));
+    }
+
+    @GetMapping("/all")
+    public OkResponse<List<GroupDTO>> findAllGroupsByCourse(@RequestParam Long courseId){
+        return OkResponse.of(groupService.findAllGroupsByCourse(courseId));
+    }
+}

+ 75 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/ProjectController.java

@@ -0,0 +1,75 @@
+package cn.seecoder.seecoderportalserver.web.rest;
+
+import cn.seecoder.seecoderportalserver.service.ProjectService;
+import cn.seecoder.seecoderportalserver.utility.OkResponse;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+
+/**
+ * @author wind
+ */
+@RestController
+@RequestMapping("/api/project")
+public class ProjectController {
+    private final ProjectService projectService;
+
+    public ProjectController(ProjectService projectService) {this.projectService = projectService;}
+
+    @ApiOperation(value = "查找全部项目", httpMethod = "GET")
+    @GetMapping("/findAll")
+    public OkResponse<JSONArray> findAllProjects() {
+        return OkResponse.of(projectService.findAllProjects());
+    }
+
+    @ApiOperation(value = "查找全部课程项目", httpMethod = "GET")
+    @GetMapping("/findCourse")
+    public OkResponse<HashMap<String, JSONArray>> findCourseProjects() {
+        return OkResponse.of(projectService.findCourseProjects());
+    }
+
+    @ApiOperation(value = "查找全部非课程项目", httpMethod = "GET")
+    @GetMapping("/findNonCourse")
+    public OkResponse<JSONArray> findNonCourseProjects() {
+        return OkResponse.of(projectService.findNonCourseProjects());
+    }
+
+    @ApiOperation(value = "根据id查找项目", httpMethod = "GET")
+    @GetMapping("/findById")
+    public OkResponse<JSONObject> findProjectById(@RequestParam Integer id) {
+        return OkResponse.of(projectService.findProjectById(id));
+    }
+
+    @ApiOperation(value = "根据课程id查找全部课程项目", httpMethod = "GET")
+    @GetMapping("findByCourse")
+    public OkResponse<JSONObject> findProjectsByCourse(@RequestParam Long courseId) {
+        return OkResponse.of(projectService.findProjectsByCourse(courseId));
+    }
+
+    @ApiOperation(value = "新建项目", httpMethod = "POST")
+    @PostMapping
+    public OkResponse<JSONObject> createProject(@RequestBody JSONObject projectInfo) {
+        return OkResponse.of(projectService.createProject(projectInfo));
+    }
+
+    @ApiOperation(value = "修改项目状态", httpMethod = "PUT")
+    @PutMapping("/updateState")
+    public OkResponse<Boolean> updateCourseProjectState(@RequestParam Integer id) {
+        return OkResponse.of(projectService.switchCourseProjectState(id));
+    }
+
+    @ApiOperation(value = "查找项目成员", httpMethod = "GET")
+    @GetMapping("/findMembers")
+    public OkResponse<JSONArray> findProjectMembers(@RequestParam Integer id) {
+        return OkResponse.of(projectService.findProjectMembers(id));
+    }
+
+    @ApiOperation(value = "Fork公开项目", httpMethod = "POST")
+    @PostMapping("/fork")
+    public OkResponse<JSONObject> forkProject(@RequestParam Integer projectId){
+        return OkResponse.of(projectService.forkPublicProject(projectId));
+    }
+}

+ 17 - 0
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/TestController.java

@@ -0,0 +1,17 @@
+package cn.seecoder.seecoderportalserver.web.rest;
+
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 临时接口测试
+ * @author wind
+ */
+@RestController
+@RequestMapping("/api/test")
+public class TestController {
+    @PostMapping("/upload")
+    public void upload(@RequestPart("file") MultipartFile file){
+        System.out.println(file.getSize());
+    }
+}

+ 8 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/UserJWTController.java

@@ -10,8 +10,10 @@ import cn.seecoder.seecoderportalserver.security.SecurityUtils;
 import cn.seecoder.seecoderportalserver.security.jwt.TokenProvider;
 import cn.seecoder.seecoderportalserver.service.SuperService;
 import cn.seecoder.seecoderportalserver.utility.OkResponse;
+import cn.seecoder.seecoderportalserver.web.rest.errors.CustomErrorException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.InternalAuthenticationServiceException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.core.Authentication;
@@ -60,8 +62,12 @@ public class UserJWTController {
                 new UsernamePasswordAuthenticationToken(
                         loginVO.getPhone(), loginVO.getPassword());
 
-        Authentication authentication =
-                authenticationManagerBuilder.getObject().authenticate(authenticationToken);
+        Authentication authentication;
+        try {
+            authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
+        }catch (InternalAuthenticationServiceException exception) {
+            throw CustomErrorException.userPhoneNotFound(loginVO.getPhone());
+        }
         SecurityContextHolder.getContext().setAuthentication(authentication);
         String jwt = tokenProvider.createToken(authentication);
 

+ 34 - 2
seecoder-portal-server/src/main/java/cn/seecoder/seecoderportalserver/web/rest/errors/CustomErrorException.java

@@ -44,13 +44,45 @@ public class CustomErrorException extends RuntimeException {
 
     public static CustomErrorException announcementNotFound() {return new CustomErrorException("公告不存在");}
 
-    public static CustomErrorException messageNotFound(){return new CustomErrorException("消息不存在");}
+    public static CustomErrorException messageNotFound() {return new CustomErrorException("消息不存在");}
 
     public static CustomErrorException messageTargetNotFound() {return new CustomErrorException("消息目标不存在");}
 
-    public static CustomErrorException announcementRecordNotFound(){
+    public static CustomErrorException announcementRecordNotFound() {
         return new CustomErrorException("公告记录不存在");
     }
 
     public static CustomErrorException messageRecordNotFound() {return new CustomErrorException("消息记录不存在");}
+
+    public static CustomErrorException devcloudError(String info) {
+        return new CustomErrorException("devcloud: " + info);
+    }
+
+    public static CustomErrorException userPhoneNotFound(String phone) {
+        return new CustomErrorException("手机号 \"" + phone + "\" 不存在!");
+    }
+
+    public static CustomErrorException unauthorizedUser() {
+        return new CustomErrorException("未授权用户!");
+    }
+
+    public static CustomErrorException groupMemberAlreadyExists() {
+        return new CustomErrorException("小组成员记录已存在!");
+    }
+
+    public static CustomErrorException groupRuleNotFound(){
+        return new CustomErrorException("小组规则不存在!");
+    }
+
+    public static CustomErrorException groupAlreadyMaxSize(){
+        return new CustomErrorException("当前小组已经满员!");
+    }
+
+    public static CustomErrorException groupNotFound(){
+        return new CustomErrorException("小组不存在!");
+    }
+
+    public static CustomErrorException courseProjectNotFound(){
+        return new CustomErrorException("课程项目记录不存在!");
+    }
 }

+ 14 - 4
seecoder-portal-server/src/main/resources/application-dev.yml

@@ -1,11 +1,21 @@
 debug: true
 
 spring:
+  servlet:
+    multipart:
+      max-file-size: 3MB
   jpa:
     show-sql: true
   datasource:
     url: jdbc:postgresql://localhost:5432/portal
-    username: root
-    password: 123456
-#    username: postgres
-#    password: postgres
+#    username: root
+#    password: 123456
+    username: postgres
+    password: postgres
+
+aliyun:
+  oss:
+    endpoint: https://oss-cn-hangzhou.aliyuncs.com
+    accessKeyId: LTAI4Fi1qmL4iuhH8t7G7r2H
+    accessKeySecret: 7tJWyQqa0cMkQGaPMMvtH6c0VLiOO2
+    bucketName: seec-portal

+ 5 - 0
seecoder-portal-server/src/main/resources/application.yml

@@ -12,6 +12,11 @@ aliyun:
     region-id: cn-hangzhou
     sys-domain: dysmsapi.aliyuncs.com
     sys-version: 2017-05-25
+  oss:
+    endpoint: https://oss-cn-hangzhou.aliyuncs.com
+    accessKeyId: LTAI4Fi1qmL4iuhH8t7G7r2H
+    accessKeySecret: 7tJWyQqa0cMkQGaPMMvtH6c0VLiOO2
+    bucketName: seec-portal
 
 spring:
   jpa:

+ 0 - 8
seecoder-portal-server/src/main/resources/localData.sql

@@ -1,8 +0,0 @@
--- 用于用于本地做测试的数据
-
--- user
-INSERT INTO public.portal_user (id, created_time, email, is_valid, name, password, phone, photo, role, signature, username) VALUES (1480, '2021-08-26 14:21:25.848000', 'qinliu@nju.edu.cn', true, 'liuqin', '$2a$10$fk2NGp/Dp6VZgtpKJoWM1uDPZgRDQsnw/X/cSCJGHpYm./jVlDlb6', '18001585267', null, 'TEACHER', '无', 'kenny67nju');
-INSERT INTO public.portal_user (id, created_time, email, is_valid, name, password, phone, photo, role, signature, username) VALUES (3117, '2022-02-21 13:21:26.171000', 'm15256072859@163.com', true, 'PortalTestTeacher', '$2a$10$y1kkNo3r2W3PKp4i.n64TeJz2Uj38r5ShahFh/3wRugFm27wz9eBe', '13382405535', null, 'TEACHER', '无', 'PortalTestTeacher-username');
-INSERT INTO public.portal_user (id, created_time, email, is_valid, name, password, phone, photo, role, signature, username) VALUES (3187, '2022-04-13 08:29:20.874000', '2570235928@qq.com', true, 'StudentWZX ', '$2a$10$4g2YNGEMgMJlY8YSVLZfh.GEdEiyKVpFdABfMwYHi0Sdow87z7PnS', '13704400822', null, 'STUDENT ', '无        ', 'StudentWZX');
--- course
-INSERT INTO public.course (id, coder_id, created_time, description, end_time, eval_id, helper_id, image_url, invitation_code, mooc_url, name, price, semester, start_time, state, teacher_id) VALUES (4, 26, '2022-04-15 04:13:53.596', '开放课程,面向所有网站用户。', '2022-05-30 16:00:00', 1, NULL, NULL, 'OpenCourse', NULL, '开放课程', 0, '2022春', '2022-03-31 16:00:00', 'EXAMPLE', 1480);

+ 515 - 0
seecoder-portal-server/src/main/resources/portal.sql

@@ -0,0 +1,515 @@
+--
+-- PostgreSQL database dump
+--
+
+-- Dumped from database version 13.4
+-- Dumped by pg_dump version 13.4
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+
+SET default_tablespace = '';
+
+SET default_table_access_method = heap;
+
+--
+-- Name: announcement; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.announcement(
+    id bigint NOT NULL,
+    content character varying(255) NOT NULL,
+    course_id bigint NOT NULL,
+    created_at timestamp without time zone NOT NULL,
+    sender_id bigint NOT NULL,
+    state character varying(255) NOT NULL,
+    title character varying(255) NOT NULL,
+    type integer NOT NULL
+);
+
+
+ALTER TABLE public.announcement OWNER TO postgres;
+
+--
+-- Name: announcement_record; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.announcement_record (
+    announcement_id bigint NOT NULL,
+    recipient_id bigint NOT NULL,
+    created_at timestamp without time zone NOT NULL,
+    read_at timestamp without time zone,
+    state boolean NOT NULL
+);
+
+
+ALTER TABLE public.announcement_record OWNER TO postgres;
+
+--
+-- Name: course; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.course (
+    id bigint NOT NULL,
+    coder_id integer,
+    created_time timestamp without time zone NOT NULL,
+    description character varying(255) NOT NULL,
+    end_time timestamp without time zone NOT NULL,
+    eval_id integer,
+    helper_id bigint,
+    image_url character varying(255),
+    invitation_code character varying(255) NOT NULL,
+    mooc_url character varying(255),
+    name character varying(255) NOT NULL,
+    price double precision NOT NULL,
+    semester character varying(255) NOT NULL,
+    start_time timestamp without time zone NOT NULL,
+    state character varying(255) NOT NULL,
+    teacher_id bigint NOT NULL
+);
+
+
+ALTER TABLE public.course OWNER TO postgres;
+
+--
+-- Name: course_generation; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.course_generation (
+    pk_course_id_name character varying(255) NOT NULL,
+    pk_course_id_value bigint
+);
+
+
+ALTER TABLE public.course_generation OWNER TO postgres;
+
+--
+-- Name: course_selection; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.course_selection (
+    course_id bigint NOT NULL,
+    user_id bigint NOT NULL,
+    state boolean NOT NULL
+);
+
+
+ALTER TABLE public.course_selection OWNER TO postgres;
+
+--
+-- Name: filled_info; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.filled_info (
+    id bigint NOT NULL,
+    filled integer,
+    phone character varying(64) NOT NULL
+);
+
+
+ALTER TABLE public.filled_info OWNER TO postgres;
+
+--
+-- Name: hibernate_sequence; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE public.hibernate_sequence
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+ALTER TABLE public.hibernate_sequence OWNER TO postgres;
+
+--
+-- Name: message; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.message (
+    id bigint NOT NULL,
+    action_type character varying(255) NOT NULL,
+    content character varying(255) NOT NULL,
+    created_at timestamp without time zone NOT NULL,
+    mnt_id bigint NOT NULL,
+    sender_id bigint NOT NULL,
+    state character varying(255) NOT NULL,
+    target_id bigint NOT NULL,
+    target_type character varying(255) NOT NULL
+);
+
+
+ALTER TABLE public.message OWNER TO postgres;
+
+--
+-- Name: message_record; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.message_record (
+    message_id bigint NOT NULL,
+    recipient_id bigint NOT NULL,
+    created_at timestamp without time zone NOT NULL,
+    read_at timestamp without time zone,
+    state boolean NOT NULL
+);
+
+
+ALTER TABLE public.message_record OWNER TO postgres;
+
+--
+-- Name: phone_verification; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.phone_verification (
+    id bigint NOT NULL,
+    code character varying(64) NOT NULL,
+    code_time timestamp without time zone,
+    phone character varying(64) NOT NULL
+);
+
+
+ALTER TABLE public.phone_verification OWNER TO postgres;
+
+--
+-- Name: portal_user; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.portal_user (
+    id bigint NOT NULL,
+    created_time timestamp without time zone,
+    email character varying(512),
+    is_valid boolean,
+    name character varying(64),
+    password character varying(512) NOT NULL,
+    phone character varying(64) NOT NULL,
+    photo character varying(512),
+    role character varying(255) NOT NULL,
+    signature character varying(512),
+    username character varying(64) NOT NULL
+);
+
+
+ALTER TABLE public.portal_user OWNER TO postgres;
+
+--
+-- Name: super; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.super (
+    id bigint NOT NULL,
+    certificate character varying(512),
+    created_time timestamp without time zone NOT NULL,
+    job_number character varying(64) NOT NULL,
+    organization character varying(64) NOT NULL,
+    real_name character varying(64) NOT NULL,
+    role integer NOT NULL,
+    state character varying(255) NOT NULL,
+    uid bigint NOT NULL
+);
+
+
+ALTER TABLE public.super OWNER TO postgres;
+
+--
+-- Name: user_generation; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE IF NOT EXISTS public.user_generation (
+    pk_user_id_name character varying(255) NOT NULL,
+    pk_user_id_value bigint
+);
+
+
+ALTER TABLE public.user_generation OWNER TO postgres;
+
+--
+-- Name: announcement announcement_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.announcement
+    ADD CONSTRAINT announcement_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: announcement_record announcement_record_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.announcement_record
+    ADD CONSTRAINT announcement_record_pkey PRIMARY KEY (announcement_id, recipient_id);
+
+
+--
+-- Name: course_generation course_generation_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course_generation
+    ADD CONSTRAINT course_generation_pkey PRIMARY KEY (pk_course_id_name);
+
+
+--
+-- Name: course course_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course
+    ADD CONSTRAINT course_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: course_selection course_selection_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course_selection
+    ADD CONSTRAINT course_selection_pkey PRIMARY KEY (course_id, user_id);
+
+
+--
+-- Name: filled_info filled_info_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.filled_info
+    ADD CONSTRAINT filled_info_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: message message_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.message
+    ADD CONSTRAINT message_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: message_record message_record_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.message_record
+    ADD CONSTRAINT message_record_pkey PRIMARY KEY (message_id, recipient_id);
+
+
+--
+-- Name: phone_verification phone_verification_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.phone_verification
+    ADD CONSTRAINT phone_verification_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: portal_user portal_user_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.portal_user
+    ADD CONSTRAINT portal_user_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: super super_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.super
+    ADD CONSTRAINT super_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: phone_verification uk_1ebndp9bn7nuiyqujcmeifojg; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.phone_verification
+    ADD CONSTRAINT uk_1ebndp9bn7nuiyqujcmeifojg UNIQUE (phone);
+
+
+--
+-- Name: course uk_4xqvdpkafb91tt3hsb67ga3fj; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course
+    ADD CONSTRAINT uk_4xqvdpkafb91tt3hsb67ga3fj UNIQUE (name);
+
+
+--
+-- Name: filled_info uk_66l7r4p268ua5cdhp06luoni5; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.filled_info
+    ADD CONSTRAINT uk_66l7r4p268ua5cdhp06luoni5 UNIQUE (phone);
+
+
+--
+-- Name: portal_user uk_86nctyu9don6c0ja9n2nnpudy; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.portal_user
+    ADD CONSTRAINT uk_86nctyu9don6c0ja9n2nnpudy UNIQUE (phone);
+
+
+--
+-- Name: portal_user uk_9vbggjygrxxob1bwqarsen47l; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.portal_user
+    ADD CONSTRAINT uk_9vbggjygrxxob1bwqarsen47l UNIQUE (name);
+
+
+--
+-- Name: portal_user uk_lasdi4ow5ujm6tmww2rfviwbk; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.portal_user
+    ADD CONSTRAINT uk_lasdi4ow5ujm6tmww2rfviwbk UNIQUE (username);
+
+
+--
+-- Name: course uk_oh8yv6gdqu1nonas8bf9666if; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course
+    ADD CONSTRAINT uk_oh8yv6gdqu1nonas8bf9666if UNIQUE (coder_id);
+
+
+--
+-- Name: course uk_rsy0nmfm5erp14m7suwbnna07; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course
+    ADD CONSTRAINT uk_rsy0nmfm5erp14m7suwbnna07 UNIQUE (eval_id);
+
+
+--
+-- Name: course uk_s56iq6sjdp4tx9g89efphd7uo; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course
+    ADD CONSTRAINT uk_s56iq6sjdp4tx9g89efphd7uo UNIQUE (invitation_code);
+
+
+--
+-- Name: course uk_totwnh6y5qe9axhrns6jgrlf; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course
+    ADD CONSTRAINT uk_totwnh6y5qe9axhrns6jgrlf UNIQUE (helper_id);
+
+
+--
+-- Name: user_generation user_generation_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.user_generation
+    ADD CONSTRAINT user_generation_pkey PRIMARY KEY (pk_user_id_name);
+
+
+--
+-- Name: user_phone_index; Type: INDEX; Schema: public; Owner: postgres
+--
+
+CREATE INDEX user_phone_index ON public.portal_user USING btree (phone);
+
+
+--
+-- Name: verification_phone_index; Type: INDEX; Schema: public; Owner: postgres
+--
+
+CREATE INDEX verification_phone_index ON public.phone_verification USING btree (phone);
+
+
+--
+-- Name: announcement announcement_course_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.announcement
+    ADD CONSTRAINT announcement_course_id_fk FOREIGN KEY (course_id) REFERENCES public.course(id) ON DELETE CASCADE;
+
+
+--
+-- Name: announcement announcement_portal_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.announcement
+    ADD CONSTRAINT announcement_portal_user_id_fk FOREIGN KEY (sender_id) REFERENCES public.portal_user(id) ON DELETE CASCADE;
+
+
+--
+-- Name: announcement_record announcement_record_announcement_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.announcement_record
+    ADD CONSTRAINT announcement_record_announcement_id_fk FOREIGN KEY (announcement_id) REFERENCES public.announcement(id) ON DELETE CASCADE;
+
+
+--
+-- Name: announcement_record announcement_record_portal_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.announcement_record
+    ADD CONSTRAINT announcement_record_portal_user_id_fk FOREIGN KEY (announcement_id) REFERENCES public.portal_user(id) ON DELETE CASCADE;
+
+
+--
+-- Name: course course_portal_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course
+    ADD CONSTRAINT course_portal_user_id_fk FOREIGN KEY (teacher_id) REFERENCES public.portal_user(id) ON DELETE RESTRICT;
+
+
+--
+-- Name: course_selection course_selection_course_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course_selection
+    ADD CONSTRAINT course_selection_course_id_fk FOREIGN KEY (course_id) REFERENCES public.course(id) ON DELETE CASCADE;
+
+
+--
+-- Name: course_selection course_selection_portal_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.course_selection
+    ADD CONSTRAINT course_selection_portal_user_id_fk FOREIGN KEY (user_id) REFERENCES public.portal_user(id) ON DELETE CASCADE;
+
+
+--
+-- Name: super fkkm5iyxbchlm1hjhs7tyqi6knh; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.super
+    ADD CONSTRAINT fkkm5iyxbchlm1hjhs7tyqi6knh FOREIGN KEY (uid) REFERENCES public.portal_user(id);
+
+
+--
+-- Name: message message_portal_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.message
+    ADD CONSTRAINT message_portal_user_id_fk FOREIGN KEY (sender_id) REFERENCES public.portal_user(id) ON DELETE CASCADE;
+
+
+--
+-- Name: message_record message_record_message_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.message_record
+    ADD CONSTRAINT message_record_message_id_fk FOREIGN KEY (message_id) REFERENCES public.message(id) ON DELETE CASCADE;
+
+
+--
+-- Name: message_record message_record_portal_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.message_record
+    ADD CONSTRAINT message_record_portal_user_id_fk FOREIGN KEY (recipient_id) REFERENCES public.portal_user(id) ON DELETE CASCADE;
+
+
+--
+-- PostgreSQL database dump complete
+--
+

+ 0 - 55
seecoder-portal-server/src/main/resources/set_fk.sql

@@ -1,55 +0,0 @@
--- announcement
-alter table announcement
-    add constraint announcement___fk_course
-        foreign key (course_id) references course
-            on update restrict on delete restrict;
-
-alter table announcement
-    add constraint announcement___fk_sender
-        foreign key (sender_id) references portal_user
-            on update restrict on delete restrict;
-
--- announcement-record
-alter table announcement_record
-    add constraint announcement_record___fk_announcement
-        foreign key (announcement_id) references announcement
-            on update restrict on delete restrict;
-
-alter table announcement_record
-    add constraint announcement_record_portal_user_id_fk
-        foreign key (recipient_id) references portal_user
-            on update restrict on delete restrict;
-
--- course
-alter table course
-    add constraint course___fk_teacher
-        foreign key (teacher_id) references portal_user
-            on update restrict on delete restrict;
-
--- course_selection
-alter table course_selection
-    add constraint course_selection___fk_course
-        foreign key (course_id) references course
-            on update restrict on delete restrict;
-
-alter table course_selection
-    add constraint course_selection___fk_user
-        foreign key (user_id) references portal_user
-            on update restrict on delete restrict;
-
--- message
-alter table message
-    add constraint message___fk_sender
-        foreign key (sender_id) references portal_user
-            on update restrict on delete restrict;
-
--- message_record
-alter table message_record
-    add constraint message_record___fk_message
-        foreign key (message_id) references message
-            on update restrict on delete restrict;
-
-alter table message_record
-    add constraint message_record___fk_user
-        foreign key (recipient_id) references portal_user
-            on update restrict on delete restrict;