Ver Fonte

Merge branch 'wph' into dev

370774330@qq.com há 5 anos atrás
pai
commit
6d7e529b90
76 ficheiros alterados com 2269 adições e 749 exclusões
  1. 2 1
      api/pom.xml
  2. 18 47
      api/src/main/java/seecoder/devcloud/api/ApplicationProperties.java
  3. 2 1
      api/src/main/java/seecoder/devcloud/api/gitlab/impl/GitlabApiImpl.java
  4. 12 1
      api/src/main/java/seecoder/devcloud/api/k8s/K8sConstants.java
  5. 3 6
      api/src/main/java/seecoder/devcloud/api/k8s/model/ConfigMap.java
  6. 4 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/Container.java
  7. 8 4
      api/src/main/java/seecoder/devcloud/api/k8s/model/Deployment.java
  8. 3 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/Ingress.java
  9. 2 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/K8sObjectRequest.java
  10. 3 4
      api/src/main/java/seecoder/devcloud/api/k8s/model/PersistentVolumeClaim.java
  11. 6 7
      api/src/main/java/seecoder/devcloud/api/k8s/model/Service.java
  12. 5 0
      common/pom.xml
  13. 13 0
      common/src/main/java/seecoder/devcloud/common/serializer/CustomLocalDateTimeDeserializer.java
  14. 13 0
      common/src/main/java/seecoder/devcloud/common/serializer/CustomLocalDateTimeSerializer.java
  15. 32 0
      common/src/main/java/seecoder/devcloud/common/serializer/TimestampLocalDateDeserializer.java
  16. 29 0
      common/src/main/java/seecoder/devcloud/common/serializer/TimestampLocalDateTimeSerializer.java
  17. 41 0
      common/src/main/java/seecoder/devcloud/common/validate/FieldCompare.java
  18. 39 0
      common/src/main/java/seecoder/devcloud/common/validate/FieldEquals.java
  19. 38 0
      common/src/main/java/seecoder/devcloud/common/validate/validator/FieldCompareValidator.java
  20. 30 0
      common/src/main/java/seecoder/devcloud/common/validate/validator/FieldEqualsValidator.java
  21. 1 3
      core/src/main/java/seecoder/devcloud/core/CoreApplication.java
  22. 41 0
      core/src/main/java/seecoder/devcloud/core/pipeline/Context.java
  23. 8 2
      core/src/main/java/seecoder/devcloud/core/pipeline/Handler.java
  24. 5 15
      core/src/main/java/seecoder/devcloud/core/pipeline/Pipeline.java
  25. 15 0
      core/src/main/java/seecoder/devcloud/core/pipeline/PipelineConfig.java
  26. 0 15
      core/src/main/java/seecoder/devcloud/core/pipeline/TestHandler.java
  27. 39 0
      core/src/main/java/seecoder/devcloud/core/pipeline/handler/AbstractHandler.java
  28. 48 0
      core/src/main/java/seecoder/devcloud/core/pipeline/handler/DockerImageBuildHandler.java
  29. 120 0
      core/src/main/java/seecoder/devcloud/core/pipeline/handler/K8sDeploymentHandler.java
  30. 11 9
      core/src/main/java/seecoder/devcloud/core/pipeline/handler/K8sIngressHandler.java
  31. 55 0
      core/src/main/java/seecoder/devcloud/core/pipeline/handler/K8sNamespaceHandler.java
  32. 67 0
      core/src/main/java/seecoder/devcloud/core/pipeline/handler/K8sServiceHandler.java
  33. 1 1
      core/src/main/java/seecoder/devcloud/core/pipeline/handler/MavenPackageHandler.java
  34. 1 1
      core/src/main/java/seecoder/devcloud/core/pipeline/handler/MavenTestHandler.java
  35. 0 15
      core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/CommonServiceHandler.java
  36. 0 16
      core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/DockerImageBuildHandler.java
  37. 0 41
      core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/JavaAppDeploymentHandler.java
  38. 0 10
      core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/MysqlDeploymentHandler.java
  39. 39 0
      core/src/main/resources/application-local.yml
  40. 4 44
      core/src/main/resources/application.yml
  41. 35 14
      core/src/test/java/seecoder/devcloud/core/CoreApplicationTests.java
  42. 19 23
      web/pom.xml
  43. 6 1
      web/src/main/java/seecoder/devcloud/web/DevcloudWebApplication.java
  44. 153 0
      web/src/main/java/seecoder/devcloud/web/dao/InsertUpdateSqlProvider.java
  45. 48 0
      web/src/main/java/seecoder/devcloud/web/dao/user/GroupMapper.java
  46. 37 0
      web/src/main/java/seecoder/devcloud/web/dao/user/GroupMemberMapper.java
  47. 39 0
      web/src/main/java/seecoder/devcloud/web/dao/user/UserMapper.java
  48. 19 0
      web/src/main/java/seecoder/devcloud/web/enums/GroupType.java
  49. 23 0
      web/src/main/java/seecoder/devcloud/web/enums/UserIdentity.java
  50. 1 7
      web/src/main/java/seecoder/devcloud/web/infrastructure/config/SwaggerConfig.java
  51. 81 0
      web/src/main/java/seecoder/devcloud/web/infrastructure/security/WebSecurityConfiguration.java
  52. 16 0
      web/src/main/java/seecoder/devcloud/web/infrastructure/security/WebSecurityConstants.java
  53. 50 0
      web/src/main/java/seecoder/devcloud/web/po/user/Group.java
  54. 53 0
      web/src/main/java/seecoder/devcloud/web/po/user/User.java
  55. 92 0
      web/src/main/java/seecoder/devcloud/web/service/impl/user/GroupServiceImp.java
  56. 86 84
      web/src/main/java/seecoder/devcloud/web/service/impl/user/MailServiceImpl.java
  57. 151 232
      web/src/main/java/seecoder/devcloud/web/service/impl/user/UserServiceImpl.java
  58. 27 0
      web/src/main/java/seecoder/devcloud/web/service/user/GroupService.java
  59. 11 1
      web/src/main/java/seecoder/devcloud/web/service/user/MailService.java
  60. 28 37
      web/src/main/java/seecoder/devcloud/web/service/user/UserService.java
  61. 23 0
      web/src/main/java/seecoder/devcloud/web/vo/user/ChangePasswordVO.java
  62. 85 0
      web/src/main/java/seecoder/devcloud/web/vo/user/CustomUserDetails.java
  63. 24 0
      web/src/main/java/seecoder/devcloud/web/vo/user/GroupDTO.java
  64. 37 0
      web/src/main/java/seecoder/devcloud/web/vo/user/GroupVO.java
  65. 31 0
      web/src/main/java/seecoder/devcloud/web/vo/user/InnerGroupVO.java
  66. 48 0
      web/src/main/java/seecoder/devcloud/web/vo/user/RegisterVO.java
  67. 24 0
      web/src/main/java/seecoder/devcloud/web/vo/user/UserDTO.java
  68. 32 0
      web/src/main/java/seecoder/devcloud/web/vo/user/UserVO.java
  69. 10 0
      web/src/main/resources/SQLscripts/group.sql
  70. 11 0
      web/src/main/resources/SQLscripts/group_member.sql
  71. 14 0
      web/src/main/resources/SQLscripts/user.sql
  72. 6 6
      web/src/main/resources/application-dev.yml
  73. 28 100
      web/src/main/resources/application-wph.yml
  74. 1 1
      web/src/test/java/seecoder/devcloud/web/DevcloudWebApplicationTests.java
  75. 49 0
      web/src/test/java/seecoder/devcloud/web/service/user/MailServiceTest.java
  76. 113 0
      web/src/test/java/seecoder/devcloud/web/service/user/UserServiceTest.java

+ 2 - 1
api/pom.xml

@@ -51,7 +51,8 @@
         <dependency>
             <groupId>org.gitlab4j</groupId>
             <artifactId>gitlab4j-api</artifactId>
-            <version>4.8.7</version>
+            <version>4.15.7</version>
+<!--            <version>4.8.7</version>-->
         </dependency>
         <dependency>
             <groupId>com.offbytwo.jenkins</groupId>

+ 18 - 47
api/src/main/java/seecoder/devcloud/api/ApplicationProperties.java

@@ -20,9 +20,7 @@ import java.util.Map;
 @ConfigurationProperties("seecoder")
 @Validated
 public class ApplicationProperties {
-	@Pattern(regexp = "^[a-zA-Z0-9]+$")
-	@NotEmpty
-	private String secret;
+
 	@Valid
 	private Gitlab gitlab = new Gitlab();
 	@Valid
@@ -32,33 +30,39 @@ public class ApplicationProperties {
 	@Valid
 	private Mail mail = new Mail();
 	@Valid
-	private Storage storage = new Storage();
-	@Valid
 	private K8s k8s = new K8s();
 	@Valid
 	private Registry registry = new Registry();
-	@Valid
-	private Auth auth = new Auth();
+
 
 	@Data
 	public static class Gitlab {
-		@URL
+
+		//@URL todo 测试环境没有真实host,先注释,生产环境需要还原
 		@NotEmpty
 		private String host;
-		private String gitProxy;
+
+		//private String gitProxy;
+		/**
+		 * 回调地址
+		 */
 		@URL
 		@NotEmpty
 		private String webHookProxy;
-		@NotEmpty
-		private String username = "root";
+
+
         @NotNull
         private Integer userId = 1;
+
+		/**
+		 * 采用的Personal/project access tokens的认证方式
+		 * 这里使用管理员的token
+		 * https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
+		 */
 		@NotEmpty
 		private String token;
 
-		public String getGitProxy() {
-			return StringUtils.defaultString(gitProxy, host);
-		}
+
 	}
 
 	@Data
@@ -70,8 +74,6 @@ public class ApplicationProperties {
 		private String username = "root";
 		@NotEmpty
 		private String token;
-		@NotEmpty
-		private String baseImage = "172.19.51.9:18082/build-base";
 	}
 
 	@Data
@@ -91,11 +93,6 @@ public class ApplicationProperties {
 		private String personal = "MOOCODER";
 	}
 
-	@Data
-	public static class Storage {
-		private String type = "local";
-		private Map<String, String> properties = new HashMap<>();
-	}
 
 	@Data
 	public static class K8s{
@@ -104,18 +101,6 @@ public class ApplicationProperties {
 		private String apiServer;
 		@NotEmpty
 		private String token;
-		@NotEmpty
-		private String ingressHost;
-		@NotEmpty
-		private String publicNamespace;
-		@NotEmpty
-		private String publicMysqlUrl;
-		@NotEmpty
-		private String publicMysqlPod;
-		@NotEmpty
-		private String publicMysqlUsername;
-
-		private String publicMysqlPassword;
 
 		private Boolean validateSSL = false;
 
@@ -143,18 +128,4 @@ public class ApplicationProperties {
 		private String cleanUpTask = "4da7e1e1-2b28-46b5-a080-9d834cc96019";
 	}
 
-	@Data
-	public static class Auth {
-		@NotEmpty
-		private String signingKey;
-		@NotEmpty
-		private String iss = "p.seecoder.cn";
-		@NotEmpty
-		private String aud = "seec-seec";
-		@NotEmpty
-		private String portal = "http://p.seecoder.cn";
-		@NotEmpty
-		private String redirectUri;
-
-	}
 }

+ 2 - 1
api/src/main/java/seecoder/devcloud/api/gitlab/impl/GitlabApiImpl.java

@@ -178,7 +178,8 @@ public class GitlabApiImpl implements GitlabApi {
     @Override
     public synchronized void changePassword(int userId, String password) throws GitLabApiException {
         User user = server.getUserApi().getUser(userId);
-        server.getUserApi().modifyUser(user, password, user.getProjectsLimit());
+        //server.getUserApi().modifyUser(user, password, user.getProjectsLimit());
+        server.getUserApi().updateUser(user, password);
     }
 
     @Override

+ 12 - 1
api/src/main/java/seecoder/devcloud/api/k8s/K8sConstants.java

@@ -5,7 +5,7 @@ public class K8sConstants {
     public static final String NAME_PATTERN = "^[0-9a-zA-Z-]{8,}$";
 
     //标签域名前缀
-    private static final String LABEL_PREFIX = "demo.seec.nju.cn";
+    private static final String LABEL_PREFIX = "devcloud";
     //课程前缀
     public static final String TYPE_LABEL = LABEL_PREFIX + "/type";
     //描述
@@ -43,4 +43,15 @@ public class K8sConstants {
     public static final String METADATA_NAME_SELECTOR = "metadata.name=";
 
     public static final String DB_SECRET_NAME = "db-secret";
+
+    /**
+     * 部署应用时的名字后缀
+     */
+    public final static String CONTAINER_SUFFIX = "-container";
+
+    public final static String DEPLOYMENT_SUFFIX = "-deployment";
+
+    public final static String SERVICE_SUFFIX = "-service";
+
+    public final static String INGRESS_SUFFIX = "-suffix";
 }

+ 3 - 6
api/src/main/java/seecoder/devcloud/api/k8s/model/ConfigMap.java

@@ -4,10 +4,7 @@ package seecoder.devcloud.api.k8s.model;
 
 import io.kubernetes.client.openapi.models.V1ConfigMap;
 import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -16,18 +13,18 @@ import java.util.Map;
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder
+@EqualsAndHashCode(callSuper=false)
 public class ConfigMap extends K8sAbstractObject<V1ConfigMap> {
 
     public static final String API_VERSION = "v1";
 
     public static final String KIND = "ConfigMap";
-
+    @Builder.Default
     private Map<String, String> data = new HashMap<>();
 
 
     public ConfigMap(V1ConfigMap v1ConfigMap) {
         super(v1ConfigMap);
-        this.data = v1ConfigMap.getData();
     }
 
     public void merge(V1ConfigMap v1ConfigMap) {

+ 4 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/Container.java

@@ -32,11 +32,14 @@ public class Container implements K8sObjectTransformer<V1Container> {
     /* 启动指令 */
     private List<String> command;
 
+    @Builder.Default
     /* 环境变量 */
     private Map<String, String> env = new HashMap<>();
 
+    @Builder.Default
     private Set<ContainerPort> ports = new HashSet<>();
 
+    @Builder.Default
     private List<VolumeMount> volumeMounts = new ArrayList<>();
 
     @Data
@@ -51,6 +54,7 @@ public class Container implements K8sObjectTransformer<V1Container> {
 
         String subPath;
 
+        @Builder.Default
         Boolean readOnly = true;
 
         public VolumeMount(V1VolumeMount v1VolumeMount) {

+ 8 - 4
api/src/main/java/seecoder/devcloud/api/k8s/model/Deployment.java

@@ -1,10 +1,7 @@
 package seecoder.devcloud.api.k8s.model;
 
 import io.kubernetes.client.openapi.models.*;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.*;
 import org.hibernate.validator.constraints.Range;
 import org.springframework.data.annotation.ReadOnlyProperty;
 
@@ -22,6 +19,7 @@ import java.util.stream.Collectors;
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
+@EqualsAndHashCode(callSuper=false)
 public class Deployment extends K8sAbstractObject<V1Deployment> {
 
     public static final String API_VERSION = "apps/v1";
@@ -34,12 +32,16 @@ public class Deployment extends K8sAbstractObject<V1Deployment> {
 
     public static final String DEFAULT_UPDATE_STRATEGY = "Recreate";
 
+    @Builder.Default
     private List<Container> containers = new ArrayList<>();
 
+    @Builder.Default
     private List<String> imagePullSecrets = new ArrayList<>();
 
+    @Builder.Default
     private List<V1Volume> volumes = new ArrayList<>();
 
+    @Builder.Default
     private Map<String, String> nodeSelectors = new HashMap<>();
     @Range(min = 1, max = 3)
     private int replicas;
@@ -47,11 +49,13 @@ public class Deployment extends K8sAbstractObject<V1Deployment> {
     /**
      * 超过timeout时间未完成启动则提示启动超时
      */
+    @Builder.Default
     private int timeout = DEFAULT_PROGRESS_DEADLINE_SECONDS;
 
     /**
      * 保留历史replica set的个数
      */
+    @Builder.Default
     private int revisionHistoryLimit = DEFAULT_REVISION_HISTORY_LIMIT;
 
     @ReadOnlyProperty

+ 3 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/Ingress.java

@@ -16,6 +16,7 @@ import java.util.stream.Collectors;
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
+@EqualsAndHashCode(callSuper=false)
 public class Ingress extends K8sAbstractObject<ExtensionsV1beta1Ingress> {
 
     public static final String API_VERSION = "extensions/v1beta1";
@@ -29,6 +30,7 @@ public class Ingress extends K8sAbstractObject<ExtensionsV1beta1Ingress> {
     //重定向的路径,默认为根路径
     private static final String redirectPath = DEFAULT_REWRITE_PATH;
 
+    @Builder.Default
     /*Ingress能暴露HTTP和HTTPS协议口,目前暂不支持HTTPS协议*/
     private List<IngressRule> httpRules = new ArrayList<>();
 
@@ -100,6 +102,7 @@ public class Ingress extends K8sAbstractObject<ExtensionsV1beta1Ingress> {
 
         private String host;
 
+        @Builder.Default
         private Set<IngressPath> ruleValues = new HashSet<>();
 
         public IngressRule(String host) {

+ 2 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/K8sObjectRequest.java

@@ -16,8 +16,10 @@ public class K8sObjectRequest {
 
     private String namespace;
 
+    @Builder.Default
     private Map<String, String> labels = new HashMap<>();
 
+    @Builder.Default
     private Map<String, String> annotations = new HashMap<>();
 
     private LabelSelector selector;

+ 3 - 4
api/src/main/java/seecoder/devcloud/api/k8s/model/PersistentVolumeClaim.java

@@ -5,10 +5,7 @@ import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
 import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimBuilder;
 import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimSpecBuilder;
 import io.kubernetes.client.openapi.models.V1ResourceRequirementsBuilder;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -18,6 +15,7 @@ import java.util.List;
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
+@EqualsAndHashCode(callSuper=false)
 public class PersistentVolumeClaim extends K8sAbstractObject<V1PersistentVolumeClaim> {
 
     public static final String API_VERSION = "v1";
@@ -28,6 +26,7 @@ public class PersistentVolumeClaim extends K8sAbstractObject<V1PersistentVolumeC
 
     private String volumeMode;
 
+    @Builder.Default
     private List<String> accessModes = new ArrayList<>();
 
     private Quantity quantity;

+ 6 - 7
api/src/main/java/seecoder/devcloud/api/k8s/model/Service.java

@@ -4,10 +4,7 @@ import io.kubernetes.client.openapi.models.V1Service;
 import io.kubernetes.client.openapi.models.V1ServiceBuilder;
 import io.kubernetes.client.openapi.models.V1ServiceSpec;
 import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -19,6 +16,7 @@ import java.util.stream.Collectors;
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
+@EqualsAndHashCode(callSuper=false)
 public class Service extends K8sAbstractObject<V1Service> {
 
     public static final String API_VERSION = "v1";
@@ -29,12 +27,13 @@ public class Service extends K8sAbstractObject<V1Service> {
 
     private String namespace;
 
+    @Builder.Default
     private Map<String, String> labels = new HashMap<>();
-
+    @Builder.Default
     private Map<String, String> annotations = new HashMap<>();
-
+    @Builder.Default
     private Set<ServicePort> servicePorts = new HashSet<>();
-
+    @Builder.Default
     private Map<String, String> selectors = new HashMap<>();
 
     public Service(V1Service v1Service) {

+ 5 - 0
common/pom.xml

@@ -29,6 +29,11 @@
             <scope>compile</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>

+ 13 - 0
common/src/main/java/seecoder/devcloud/common/serializer/CustomLocalDateTimeDeserializer.java

@@ -0,0 +1,13 @@
+package seecoder.devcloud.common.serializer;
+
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+
+import java.time.format.DateTimeFormatter;
+
+public class CustomLocalDateTimeDeserializer extends LocalDateTimeDeserializer {
+	private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
+
+	public CustomLocalDateTimeDeserializer() {
+		super(FORMATTER);
+	}
+}

+ 13 - 0
common/src/main/java/seecoder/devcloud/common/serializer/CustomLocalDateTimeSerializer.java

@@ -0,0 +1,13 @@
+package seecoder.devcloud.common.serializer;
+
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+
+import java.time.format.DateTimeFormatter;
+
+public class CustomLocalDateTimeSerializer extends LocalDateTimeSerializer {
+	private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
+
+	public CustomLocalDateTimeSerializer() {
+		super(FORMATTER);
+	}
+}

+ 32 - 0
common/src/main/java/seecoder/devcloud/common/serializer/TimestampLocalDateDeserializer.java

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

+ 29 - 0
common/src/main/java/seecoder/devcloud/common/serializer/TimestampLocalDateTimeSerializer.java

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

+ 41 - 0
common/src/main/java/seecoder/devcloud/common/validate/FieldCompare.java

@@ -0,0 +1,41 @@
+package seecoder.devcloud.common.validate;
+
+
+import seecoder.devcloud.common.validate.validator.FieldCompareValidator;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Target({TYPE, ANNOTATION_TYPE})
+@Retention(RUNTIME)
+@Repeatable(FieldCompare.List.class)
+@Constraint(validatedBy = FieldCompareValidator.class)
+@Documented
+public @interface FieldCompare {
+	String message() default "{com.moekr.moocoder.util.validate.FieldCompare}";
+
+	Class<?>[] groups() default {};
+
+	Class<? extends Payload>[] payload() default {};
+
+	String lessField();
+
+	String greaterField();
+
+	boolean allowEqual() default false;
+
+	@Target({TYPE, ANNOTATION_TYPE})
+	@Retention(RUNTIME)
+	@Documented
+	@interface List {
+		FieldCompare[] value();
+	}
+}

+ 39 - 0
common/src/main/java/seecoder/devcloud/common/validate/FieldEquals.java

@@ -0,0 +1,39 @@
+package seecoder.devcloud.common.validate;
+
+import seecoder.devcloud.common.validate.validator.FieldEqualsValidator;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Target({TYPE, ANNOTATION_TYPE})
+@Retention(RUNTIME)
+@Repeatable(FieldEquals.List.class)
+@Constraint(validatedBy = FieldEqualsValidator.class)
+@Documented
+public @interface FieldEquals {
+	String message() default "{com.moekr.moocoder.util.validate.FieldEquals}";
+
+	Class<?>[] groups() default {};
+
+	Class<? extends Payload>[] payload() default {};
+
+	String firstField();
+
+	String secondField();
+
+	@Target({TYPE, ANNOTATION_TYPE})
+	@Retention(RUNTIME)
+	@Documented
+	@interface List {
+		FieldEquals[] value();
+	}
+
+}

+ 38 - 0
common/src/main/java/seecoder/devcloud/common/validate/validator/FieldCompareValidator.java

@@ -0,0 +1,38 @@
+package seecoder.devcloud.common.validate.validator;
+
+import org.springframework.beans.BeanUtils;
+import seecoder.devcloud.common.validate.FieldCompare;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class FieldCompareValidator implements ConstraintValidator<FieldCompare, Object> {
+	private String lessField;
+	private String greaterField;
+	private boolean allowEqual;
+
+
+	@Override
+	public void initialize(FieldCompare constraintAnnotation) {
+		this.lessField = constraintAnnotation.lessField();
+		this.greaterField = constraintAnnotation.greaterField();
+		this.allowEqual = constraintAnnotation.allowEqual();
+	}
+
+	@Override
+	@SuppressWarnings("unchecked")
+	public boolean isValid(Object value, ConstraintValidatorContext context) {
+		try {
+			Comparable lessObject = (Comparable) BeanUtils.getPropertyDescriptor(value.getClass(), lessField).getReadMethod().invoke(value);
+			Comparable greaterObject = (Comparable) BeanUtils.getPropertyDescriptor(value.getClass(), greaterField).getReadMethod().invoke(value);
+			int result = lessObject.compareTo(greaterObject);
+			if (allowEqual && result == 0) {
+				return true;
+			} else {
+				return result < 0;
+			}
+		} catch (Exception e) {
+			return false;
+		}
+	}
+}

+ 30 - 0
common/src/main/java/seecoder/devcloud/common/validate/validator/FieldEqualsValidator.java

@@ -0,0 +1,30 @@
+package seecoder.devcloud.common.validate.validator;
+
+import org.springframework.beans.BeanUtils;
+import seecoder.devcloud.common.validate.FieldEquals;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Objects;
+
+public class FieldEqualsValidator implements ConstraintValidator<FieldEquals, Object> {
+	private String firstField;
+	private String secondField;
+
+	@Override
+	public void initialize(FieldEquals constraintAnnotation) {
+		this.firstField = constraintAnnotation.firstField();
+		this.secondField = constraintAnnotation.secondField();
+	}
+
+	@Override
+	public boolean isValid(Object value, ConstraintValidatorContext context) {
+		try {
+			Object firstObject = BeanUtils.getPropertyDescriptor(value.getClass(), firstField).getReadMethod().invoke(value);
+			Object secondObject = BeanUtils.getPropertyDescriptor(value.getClass(), secondField).getReadMethod().invoke(value);
+			return Objects.equals(firstObject, secondObject);
+		} catch (Exception e) {
+			return false;
+		}
+	}
+}

+ 1 - 3
core/src/main/java/seecoder/devcloud/core/CoreApplication.java

@@ -4,7 +4,6 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
-import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
@@ -20,8 +19,7 @@ import java.util.TimeZone;
 @ComponentScan({"seecoder.devcloud.api","seecoder.devcloud.common"})
 @SpringBootApplication(exclude = {
         DataSourceAutoConfiguration.class,
-        DataSourceTransactionManagerAutoConfiguration.class,
-        HibernateJpaAutoConfiguration.class})
+        DataSourceTransactionManagerAutoConfiguration.class})
 public class CoreApplication {
 
     public static void main(String[] args) {

+ 41 - 0
core/src/main/java/seecoder/devcloud/core/pipeline/Context.java

@@ -0,0 +1,41 @@
+package seecoder.devcloud.core.pipeline;
+
+import lombok.Builder;
+import lombok.Data;
+import seecoder.devcloud.api.ApplicationProperties;
+
+import java.util.HashMap;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/2/2
+ * @description: 流水线上下文
+ */
+@Data
+@Builder
+public class Context {
+
+    private ApplicationProperties applicationProperties;
+
+    private String pipelineId;
+
+    private String pipelineName;
+
+    /**
+     * 流水线所属小组
+     */
+    private String groupName;
+
+    /**
+     * 项目名称
+     */
+    private String projectName;
+
+
+    /**
+     * 用来存放一些杂项信息,根据key去取
+     */
+    private HashMap<String, String> configs;
+
+
+}

+ 8 - 2
core/src/main/java/seecoder/devcloud/core/pipeline/Handler.java

@@ -5,6 +5,12 @@ package seecoder.devcloud.core.pipeline;
  * @date 2021/1/18
  * @description:
  */
-public interface Handler<I,O> {
-    O process(I input);
+public interface Handler {
+
+    void invoke(Context context);
+
+    void process(Context context);
+
+    void invokeNext(Context context);
+
 }

+ 5 - 15
core/src/main/java/seecoder/devcloud/core/pipeline/Pipeline.java

@@ -3,21 +3,11 @@ package seecoder.devcloud.core.pipeline;
 /**
  * @author PuHong Weng
  * @date 2021/1/18
- * @description: 流水线 I代表in,流水线的输入、O代表out,流水线的输出
+ * @description:
  */
-public class Pipeline<I, O> {
+public interface Pipeline {
 
-    private final Handler<I, O> currentHandler;
-
-    public Pipeline(Handler<I, O> currentHandler) {
-        this.currentHandler = currentHandler;
-    }
-
-    public <K> Pipeline<I, K> addHandler(Handler<O, K> newHandler) {
-        return new Pipeline<>(input -> newHandler.process(currentHandler.process(input)));
-    }
-
-    public O execute(I input) {
-        return currentHandler.process(input);
-    }
+    void init(PipelineConfig config);
+    void start();
+    Context getContext();
 }

+ 15 - 0
core/src/main/java/seecoder/devcloud/core/pipeline/PipelineConfig.java

@@ -0,0 +1,15 @@
+package seecoder.devcloud.core.pipeline;
+
+import lombok.Data;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/2/2
+ * @description:
+ */
+@Data
+public class PipelineConfig {
+
+
+    private String configJson;
+}

+ 0 - 15
core/src/main/java/seecoder/devcloud/core/pipeline/TestHandler.java

@@ -1,15 +0,0 @@
-package seecoder.devcloud.core.pipeline;
-
-/**
- * @author PuHong Weng
- * @date 2021/1/18
- * @description:
- */
-public class TestHandler implements Handler<String,String> {
-
-    @Override
-    public String process(String input) {
-        System.out.println("test");
-        return input.substring(1);
-    }
-}

+ 39 - 0
core/src/main/java/seecoder/devcloud/core/pipeline/handler/AbstractHandler.java

@@ -0,0 +1,39 @@
+package seecoder.devcloud.core.pipeline.handler;
+
+import lombok.Data;
+import seecoder.devcloud.core.pipeline.Context;
+import seecoder.devcloud.core.pipeline.Handler;
+
+import java.util.HashMap;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/2/2
+ * @description:
+ */
+@Data
+public abstract class AbstractHandler implements Handler {
+
+
+    public AbstractHandler nextHandler;
+
+//    /**
+//     * 用来存放Handler的配置
+//     */
+//    public HashMap<String,String> configs = new HashMap<>();
+
+    @Override
+    public void invoke(Context context) {
+        process(context);
+        invokeNext(context);
+    }
+
+    @Override
+    public void invokeNext(Context context) {
+        nextHandler.invoke(context);
+    }
+
+    public void setNextHandler(AbstractHandler nextHandler) {
+        this.nextHandler = nextHandler;
+    }
+}

+ 48 - 0
core/src/main/java/seecoder/devcloud/core/pipeline/handler/DockerImageBuildHandler.java

@@ -0,0 +1,48 @@
+package seecoder.devcloud.core.pipeline.handler;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
+import seecoder.devcloud.api.docker.DockerApi;
+import seecoder.devcloud.common.util.SpringUtil;
+import seecoder.devcloud.core.pipeline.Context;
+import seecoder.devcloud.core.pipeline.Handler;
+
+import java.io.*;
+import java.nio.file.Files;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/1/18
+ * @description: docker镜像构建
+ */
+@Slf4j
+public class DockerImageBuildHandler extends AbstractHandler {
+
+    private static final String TEMP_PREFIX = "SEECODER-DEVCLOUD-";
+
+    public DockerApi dockerApi;
+
+    public DockerImageBuildHandler(){
+        dockerApi = SpringUtil.getBean(DockerApi.class);
+    }
+
+    @Override
+    public void process(Context context){
+//        File tempDir = null;
+//        try {
+//            tempDir = Files.createTempDirectory(TEMP_PREFIX).toFile();
+//            //
+//            File dockerFile = new File(tempDir, "Dockerfile");
+//            try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dockerFile)))) {
+//                writer.write(question.getQuestionType().getHelper().dockerFile(question.getName()));
+//            }
+//            //dockerApi.build(tempDir.getAbsolutePath(), question.getImageName(), question.getImageTag());
+//        } catch (IOException e){
+//            e.printStackTrace();
+//            log.error("镜像构建失败, 产生IO错误, {}", e.getMessage());
+//        } finally {
+//            FileUtils.deleteDirectory(tempDir);
+//        }
+    }
+
+}

+ 120 - 0
core/src/main/java/seecoder/devcloud/core/pipeline/handler/K8sDeploymentHandler.java

@@ -0,0 +1,120 @@
+package seecoder.devcloud.core.pipeline.handler;
+
+import lombok.extern.slf4j.Slf4j;
+import seecoder.devcloud.api.ApplicationProperties;
+import seecoder.devcloud.api.k8s.DeploymentApi;
+import seecoder.devcloud.api.k8s.K8sConstants;
+import seecoder.devcloud.api.k8s.SecretApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.Container;
+import seecoder.devcloud.api.k8s.model.ContainerPort;
+import seecoder.devcloud.api.k8s.model.Deployment;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.util.SecretTypeEnum;
+import seecoder.devcloud.api.k8s.vo.SecretVO;
+import seecoder.devcloud.common.util.SpringUtil;
+import seecoder.devcloud.core.pipeline.Context;
+import seecoder.devcloud.core.pipeline.Handler;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/1/18
+ * @description: 创建一个k8s Deployment
+ * 因为是学生的Deployment构建的过程,有以下约束:
+ * 1. 每个Deployment只允许创建一个Pod,防止资源消耗太多
+ * 2. 以Group Name,作为Namespace, Project Name来作为Deployment name
+ * 3. 镜像存放在seecoder的内部私有镜像仓库,所以需要设置
+ */
+@Slf4j
+public class K8sDeploymentHandler extends AbstractHandler {
+
+    private final SecretApi secretApi;
+
+    private final DeploymentApi deploymentApi;
+
+
+    //容器所使用的镜像
+    private String image;
+
+    //副本数
+    private int replicas;
+
+    //
+    private int port;
+
+
+    public K8sDeploymentHandler() {
+        secretApi = SpringUtil.getBean(SecretApi.class);
+        deploymentApi = SpringUtil.getBean(DeploymentApi.class);
+    }
+
+    /**
+     * 部署一个java项目
+     * 以小组名称作为namespace
+     *
+     * @param context
+     */
+    @Override
+    public void process(Context context) {
+        //配置初始化
+        initConfig(context);
+        String groupName = context.getGroupName();
+        String projectName = context.getProjectName();
+
+        //配置容器暴露端口
+        ContainerPort containerPort = ContainerPort.builder()
+                .port(port)
+                .build();
+        //配置容器
+        Container container = Container.builder()
+                .image(image)
+                .name(projectName + K8sConstants.CONTAINER_SUFFIX)
+                .ports(Collections.singleton(containerPort))
+                .build();
+
+        //配置deployment
+        Deployment deployment = Deployment.builder()
+                .replicas(replicas)
+                .containers(Collections.singletonList(container))
+                .build();
+        deployment.setName(projectName + K8sConstants.DEPLOYMENT_SUFFIX);
+        deployment.setNamespace(groupName);
+        deployment.setLabel(K8sConstants.APPLICATION_LABEL, projectName);
+
+        // 密钥来源:在命名空间创建时,创建对应的secret存放密钥,见 K8sNamespaceHandler.class
+        // 将registry密钥作为imagePullSecrets注入, 使得此deployment可以访问私有镜像仓库
+        List<String> registrySecrets = secretApi
+                .getSecretListByType(
+                        groupName,
+                        SecretTypeEnum.REGISTRY)
+                .stream()
+                .map(SecretVO::getName)
+                .collect(Collectors.toList()
+                );
+        deployment.setImagePullSecrets(registrySecrets);
+
+
+        try {
+            deploymentApi.create(deployment);
+            log.info("deployment创建成功");
+        } catch (K8sApiException e){
+            if (e.getCode() == K8sApiException.ALREADY_EXIST){
+                deploymentApi.update(deployment);
+                log.info("deployment更新成功");
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    private void initConfig(Context context) {
+        this.image = context.getConfigs().getOrDefault("image", "");
+        this.port = Integer.parseInt(context.getConfigs().getOrDefault("port", "8000"));
+        this.replicas = Integer.parseInt(context.getConfigs().getOrDefault("replicas", "1"));
+    }
+}

+ 11 - 9
core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/CommonIngressHandler.java → core/src/main/java/seecoder/devcloud/core/pipeline/handler/K8sIngressHandler.java

@@ -1,7 +1,9 @@
-//package seecoder.devcloud.core.pipeline.handlerImpl;
+//package seecoder.devcloud.core.pipeline.handler;
 //
 //import seecoder.devcloud.api.k8s.model.Ingress;
-//import Handler;
+//import seecoder.devcloud.core.pipeline.Context;
+//import seecoder.devcloud.core.pipeline.Handler;
+//
 //
 //import java.util.Collection;
 //import java.util.HashSet;
@@ -14,20 +16,20 @@
 // * @date 2021/1/18
 // * @description:
 // */
-//public class CommonIngressHandler implements Handler<String,String> {
+//public class K8sIngressHandler extends AbstractHandler {
 //
 //    @Override
-//    public String process(String input) {
+//    public void process(Context context) {
+//
+//
 //
+//        String groupName = context.getGroupName();
 //
-//        String appName = applicationDTO.getName();
-//        String projectName = applicationDTO.getProjectName();
+//        String projectName = context.getProjectName();
 //
 //        Ingress ingress = new Ingress();
-//        ingress.setName(appName + "-" + applicationDTO.getDeployEnvironment());
-//        String environment = applicationDTO.getDeployEnvironment() == DeployEnvironment.PRODUCTION ? "prod" : "test";
-//        ingress.setNamespace(projectName);
 //
+//        ingress.setName(appName + "-" + applicationDTO.getDeployEnvironment());
 //        Set<String> alreadyTaken = new HashSet<>();
 //        List<Ingress.IngressRule> ingressRules = applicationDTO.getContainers()
 //                .stream()

+ 55 - 0
core/src/main/java/seecoder/devcloud/core/pipeline/handler/K8sNamespaceHandler.java

@@ -0,0 +1,55 @@
+package seecoder.devcloud.core.pipeline.handler;
+
+import com.sun.xml.internal.stream.events.NamespaceImpl;
+import lombok.extern.slf4j.Slf4j;
+import seecoder.devcloud.api.ApplicationProperties;
+import seecoder.devcloud.api.k8s.NamespaceApi;
+import seecoder.devcloud.api.k8s.SecretApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.Namespace;
+import seecoder.devcloud.common.util.SpringUtil;
+import seecoder.devcloud.core.pipeline.Context;
+
+import javax.swing.*;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/2/2
+ * @description: 创建k8sNamespace。这里以小组名称为单位创建。
+ */
+@Slf4j
+public class K8sNamespaceHandler extends AbstractHandler {
+
+    private NamespaceApi namespaceApi;
+
+    private SecretApi secretApi;
+
+    public K8sNamespaceHandler() {
+        namespaceApi = SpringUtil.getBean(NamespaceApi.class);
+        secretApi = SpringUtil.getBean(SecretApi.class);
+    }
+
+    @Override
+    public void process(Context context) {
+        Namespace namespace = new Namespace();
+        namespace.setName(context.getGroupName());
+
+        //todo namespace在底下资源未删除时,不能轻易删除重建,只能直接通过异常查是否已创建,丑陋,待修改
+        try {
+            namespace = namespaceApi.create(namespace);
+            log.info("Namespace创建成功, {}", namespace);
+            //为命名空间配置私有镜像仓库密钥,使得此命名空间可以访问镜像仓库来部署容器
+            ApplicationProperties.Registry registry = context.getApplicationProperties().getRegistry();
+            secretApi.createPrivateRegistrySecret(context.getGroupName(), registry.getHost(), registry.getUsername(), registry.getPassword());
+        } catch (K8sApiException e){
+            if (e.getCode() == K8sApiException.ALREADY_EXIST){
+                log.info("Namespace已经存在, 无需新建");
+            } else{
+                throw e;
+            }
+        }
+
+
+    }
+}

+ 67 - 0
core/src/main/java/seecoder/devcloud/core/pipeline/handler/K8sServiceHandler.java

@@ -0,0 +1,67 @@
+package seecoder.devcloud.core.pipeline.handler;
+
+import lombok.extern.slf4j.Slf4j;
+import seecoder.devcloud.api.k8s.K8sConstants;
+import seecoder.devcloud.api.k8s.ServiceApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.K8sObject;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.Service;
+import seecoder.devcloud.api.k8s.model.ServicePort;
+import seecoder.devcloud.common.util.SpringUtil;
+import seecoder.devcloud.core.pipeline.Context;
+import seecoder.devcloud.core.pipeline.Handler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Collectors;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.APPLICATION_LABEL;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/1/18
+ * @description: 通用的k8s建立service
+ */
+@Slf4j
+public class K8sServiceHandler extends AbstractHandler {
+
+    private ServiceApi serviceApi;
+
+    public K8sServiceHandler() {
+        serviceApi = SpringUtil.getBean(ServiceApi.class);
+    }
+
+
+    @Override
+    public void process(Context context){
+
+        //pod内容器暴露的端口
+        int targetPort =  Integer.parseInt(context.getConfigs().get("port"));
+        //service对集群内暴露的端口,这里默认与targetPort一致
+        int servicePort = targetPort;
+        context.getConfigs().put("servicePort", String.valueOf(servicePort));
+
+        //配置K8s的service
+        Service service = new Service();
+        service.setNamespace(context.getGroupName());
+        service.setName(context.getProjectName()+ K8sConstants.SERVICE_SUFFIX);
+        //标签选择器选择 此项目对应的deployment的name
+        service.setSelectors(Collections.singletonMap(K8sConstants.APPLICATION_LABEL, context.getProjectName()));
+        service.setServicePorts(
+                Collections.singleton(ServicePort.builder().port(servicePort).targetPort(targetPort).build())
+        );
+
+        try {
+            serviceApi.create(service);
+            log.info("service创建成功");
+        } catch (K8sApiException e){
+            if (e.getCode() == K8sApiException.ALREADY_EXIST){
+                serviceApi.update(service);
+                log.info("service更新成功");
+            } else {
+                throw e;
+            }
+        }
+    }
+}

+ 1 - 1
core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/MavenPackageHandler.java → core/src/main/java/seecoder/devcloud/core/pipeline/handler/MavenPackageHandler.java

@@ -1,4 +1,4 @@
-package seecoder.devcloud.core.pipeline.handlerImpl;
+package seecoder.devcloud.core.pipeline.handler;
 
 /**
  * @author PuHong Weng

+ 1 - 1
core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/MavenTestHandler.java → core/src/main/java/seecoder/devcloud/core/pipeline/handler/MavenTestHandler.java

@@ -1,4 +1,4 @@
-package seecoder.devcloud.core.pipeline.handlerImpl;
+package seecoder.devcloud.core.pipeline.handler;
 
 /**
  * @author PuHong Weng

+ 0 - 15
core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/CommonServiceHandler.java

@@ -1,15 +0,0 @@
-package seecoder.devcloud.core.pipeline.handlerImpl;
-
-import seecoder.devcloud.core.pipeline.Handler;
-
-/**
- * @author PuHong Weng
- * @date 2021/1/18
- * @description: 通用的k8s建立service
- */
-public class CommonServiceHandler implements Handler<String,String> {
-    @Override
-    public String process(String input) {
-        return null;
-    }
-}

+ 0 - 16
core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/DockerImageBuildHandler.java

@@ -1,16 +0,0 @@
-package seecoder.devcloud.core.pipeline.handlerImpl;
-
-import seecoder.devcloud.core.pipeline.Handler;
-
-/**
- * @author PuHong Weng
- * @date 2021/1/18
- * @description: docker镜像构建
- */
-public class DockerImageBuildHandler implements Handler<String,String> {
-
-    @Override
-    public String process(String input) {
-        return null;
-    }
-}

+ 0 - 41
core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/JavaAppDeploymentHandler.java

@@ -1,41 +0,0 @@
-package seecoder.devcloud.core.pipeline.handlerImpl;
-
-import seecoder.devcloud.api.k8s.DeploymentApi;
-import seecoder.devcloud.api.k8s.SecretApi;
-import seecoder.devcloud.api.k8s.impl.DeploymentApiImpl;
-import seecoder.devcloud.api.k8s.impl.SecretApiImpl;
-import seecoder.devcloud.api.k8s.model.Container;
-import seecoder.devcloud.api.k8s.model.Deployment;
-import seecoder.devcloud.common.util.SpringUtil;
-import seecoder.devcloud.core.pipeline.Handler;
-
-import java.util.Collections;
-
-/**
- * @author PuHong Weng
- * @date 2021/1/18
- * @description:
- */
-public class JavaAppDeploymentHandler implements Handler<String,String> {
-
-    private final SecretApi secretApi;
-
-    private final DeploymentApi deploymentApi;
-
-    public JavaAppDeploymentHandler() {
-        secretApi = SpringUtil.getBean(SecretApi.class);
-        deploymentApi = SpringUtil.getBean(DeploymentApi.class);
-    }
-
-    @Override
-    public String process(String input) {
-
-
-        Deployment deployment = Deployment.builder()
-                .replicas(1)
-                .containers(Collections.singletonList(new Container()))
-                .build();
-        deploymentApi.create(deployment);
-        return null;
-    }
-}

+ 0 - 10
core/src/main/java/seecoder/devcloud/core/pipeline/handlerImpl/MysqlDeploymentHandler.java

@@ -1,10 +0,0 @@
-package seecoder.devcloud.core.pipeline.handlerImpl;
-
-/**
- * @author PuHong Weng
- * @date 2021/1/18
- * @description:
- */
-public class MysqlDeploymentHandler {
-
-}

+ 39 - 0
core/src/main/resources/application-local.yml

@@ -0,0 +1,39 @@
+# 用于core包的本地测试,连的是本地搭的minikube
+seecoder:
+  secret: 1qaz2wsx
+  auth:
+    signingKey: SEEC-1919810-OIDC-114514
+    iss: p.seecoder.cn
+    aud: seec-seec
+    portal: http://p.seecoder.cn/api/interaction
+    redirectUri: http://seec.seecoder.cn/api/auth/receive
+  gitlab:
+    host: http://gitlab.seec.seecoder.cn
+    git-proxy: http://gitlab.seec.seecoder.cn
+    webHookProxy: http://localhost:3000
+    username: root
+    token: x7Hwx-zq8LFkHznGku9a
+  jenkins:
+    host: http://192.168.68.79:8082
+    username: root
+    token: 11d7b9e4c1acac481904c25813e2ec1f0a
+    baseImage: 172.19.51.9:18082/build-base
+  docker:
+    registry: 192.168.68.79:18082
+  mail:
+    from: seecoder@163.com
+  storage:
+    type: local
+    properties:
+      location: ./problem
+  k8s:
+    api-server: https://192.168.99.101:8443
+    token: "eyJhbGciOiJSUzI1NiIsImtpZCI6IkM0R1hBTlZ3UjU1eVlKemNocUxBMUtxcUEwaWR3NW9Tb0pzYjA3RktTcjAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXZjbG91ZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZWZhdWx0LXRva2VuLWI3OXJjIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRlZmF1bHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIwODdkMGI1ZC1mYjBjLTQxYjgtYTM2MC1kMGVlNjI5ZjI1MTUiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGV2Y2xvdWQ6ZGVmYXVsdCJ9.NQ33155-zupAnzeyCon8NxhJq3MWTNYDZpfcLy0Y03fvrzCerrs76hFiBBvgagbFO8OvK4LVb-TXnn4ypSfGUvY-cxP_9KesI98BA8B598do36EMaonr0ixLHkErAm7aq_olAmVIa-dOSRnHgFVA9iNCUzLRQFV0fj1PwtCOX-X9gf6_J7pXngo9b4AdeKGVtNEzoNae1pT9q5F1mWBTBkES7Jj-g61HyeqoZKOgK-BNmKdOTXSm4HbTtnGVAXK5xMLmJjJ4UzDgzQzA20XKQv-TC1pUxJ3eyS5QcEUGB8fVv9-fHoDPUloQ6jAm8SNtktrJ5yzPHYH2ZZGY-HiLuA"
+    debugging: false
+  registry:
+    host: 192.168.68.79:18082
+    username: admin
+    password: admin123
+    nexus-server: 192.168.68.79:18081
+    repo-name: sec-repo
+    clean-up-task: 83bb8249-71e8-4367

+ 4 - 44
core/src/main/resources/application.yml

@@ -1,44 +1,4 @@
-seecoder:
-  secret: 1qaz2wsx
-  auth:
-    signingKey: SEEC-1919810-OIDC-114514
-    iss: p.seecoder.cn
-    aud: seec-seec
-    portal: http://p.seecoder.cn/api/interaction
-    redirectUri: http://seec.seecoder.cn/api/auth/receive
-  gitlab:
-    host: http://gitlab.seec.seecoder.cn
-    git-proxy: http://gitlab.seec.seecoder.cn
-    webHookProxy: http://localhost:3000
-    username: root
-    token: x7Hwx-zq8LFkHznGku9a
-  jenkins:
-    host: http://192.168.68.79:8082
-    username: root
-    token: 11d7b9e4c1acac481904c25813e2ec1f0a
-    baseImage: 172.19.51.9:18082/build-base
-  docker:
-    registry: 192.168.68.79:18082
-  mail:
-    from: seecoder@163.com
-  storage:
-    type: local
-    properties:
-      location: ./problem
-  k8s:
-    api-server: https://192.168.99.101:8443
-    token: "eyJhbGciOiJSUzI1NiIsImtpZCI6IkM0R1hBTlZ3UjU1eVlKemNocUxBMUtxcUEwaWR3NW9Tb0pzYjA3RktTcjAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tZHo5bHYiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjE5NTg0N2NhLTlhYjEtNGQ0Yi05Yjk3LTFhYmQ0MmFlMTQwYyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.bEBgxEKWiATzkFu3gb2pljEHYcg5SZXd3fEH9RREbUGCzMQ7T2mibJ-MEMXnUJbEmft7L2OobUs1w_qPe2DSKgBbW97R6XYsillDhsORoDEhHbT-LIn2uK8hKa-1BYMoZ8g3Eo0FR2Ltkod5OrLEhldlkTMcylF_wtg64LlNHfG8MbGHFOZyjhHJRlMeZRrVDg9_c2KBHb_t42tD4NeaUcVrV8t3rA6qT69H8SvNCD4tioE1Pe3jJ840ZDXiJSzo7Qqk9zfyzkGsxC8ND6tc8UbaTt3tZqU9_TmDcptWGlE5wf1cu-F56PmotaQjOwTF1q4_XeY5Vbg_pVknR8-d7Q"
-    ingress-host: seecii.cn
-    public-namespace: dev
-    public-mysql-url: jdbc:mysql://seecii-mysql.dev.svc.cluster.local:3306
-    public-mysql-pod: seecii-mysql-579dc7c857-8m2fd
-    public-mysql-username: root
-    public-mysql-password: NJU67software
-    debugging: false
-  registry:
-    host: 192.168.68.79:18082
-    username: admin
-    password: admin123
-    nexus-server: 192.168.68.79:18081
-    repo-name: sec-repo
-    clean-up-task: 83bb8249-71e8-4367
+spring:
+  profiles:
+    active: local
+

+ 35 - 14
core/src/test/java/seecoder/devcloud/core/CoreApplicationTests.java

@@ -1,15 +1,22 @@
 package seecoder.devcloud.core;
 
+import org.junit.Before;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.xml.NamespaceHandler;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.junit4.SpringRunner;
 import seecoder.devcloud.api.ApplicationProperties;
 import seecoder.devcloud.api.k8s.DeploymentApi;
-import seecoder.devcloud.core.pipeline.handlerImpl.JavaAppDeploymentHandler;
+import seecoder.devcloud.core.pipeline.Context;
+import seecoder.devcloud.core.pipeline.handler.K8sDeploymentHandler;
 import seecoder.devcloud.core.pipeline.Pipeline;
-import seecoder.devcloud.core.pipeline.TestHandler;
+import seecoder.devcloud.core.pipeline.handler.K8sNamespaceHandler;
+
+import java.util.Collection;
 
 @RunWith(SpringRunner.class)
 @SpringBootTest
@@ -21,20 +28,34 @@ class CoreApplicationTests {
     @Autowired
     private DeploymentApi deploymentApi;
 
+    private Context context;
+
+
+    @BeforeEach
+    void setUp() {
+        mockContext();
+    }
+
     @Test
-    void contextLoads() {
-
-        Pipeline<String,String> filters = new Pipeline(new TestHandler())
-                .addHandler(new TestHandler())
-                .addHandler(new TestHandler());
-        filters.execute("GoYankees123!");
-//        System.out.printf(applicationProperties.toString());
-//        System.out.println("tset");
-////        System.out.printf(deploymentApi.toString());
-////        System.out.printf(SpringUtil.getBean(DeploymentApi.class).toString());
-        JavaAppDeploymentHandler handler = new JavaAppDeploymentHandler();
-//        handler.process("test");
+    void k8sDeployment() {
+
 
     }
 
+    @Test
+    void k8sNamespace() {
+
+        K8sNamespaceHandler k8sNamespaceHandler = new K8sNamespaceHandler();
+        k8sNamespaceHandler.process(context);
+
+    }
+
+
+    private void mockContext(){
+        this.context = Context.builder()
+                .groupName("test-group")
+                .projectName("test-project")
+                .applicationProperties(applicationProperties)
+                .configs(null).build();
+    }
 }

+ 19 - 23
web/pom.xml

@@ -22,18 +22,6 @@
     </properties>
 
     <dependencies>
-        <!--MybatisTest依赖包-->
-        <dependency>
-            <groupId>org.mybatis.spring.boot</groupId>
-            <artifactId>mybatis-spring-boot-starter-test</artifactId>
-            <version>2.1.0</version>
-        </dependency>
-        <dependency>
-            <groupId>mysql</groupId>
-            <artifactId>mysql-connector-java</artifactId>
-            <scope>runtime</scope>
-        </dependency>
-
         <!-- Swagger -->
         <!-- https://mvnrepository.com/artifact/io.springfox/springfox-boot-starter -->
         <dependency>
@@ -53,10 +41,6 @@
             <artifactId>api</artifactId>
             <version>0.0.1-SNAPSHOT</version>
         </dependency>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-data-jpa</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-jdbc</artifactId>
@@ -75,23 +59,34 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-mail</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-tomcat</artifactId>
-            <scope>provided</scope>
-        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.vintage</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
-
         <dependency>
             <groupId>org.mybatis.spring.boot</groupId>
             <artifactId>mybatis-spring-boot-starter</artifactId>
             <version>2.1.4</version>
         </dependency>
-
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.boot</groupId>-->
+<!--            <artifactId>spring-boot-starter-security</artifactId>-->
+<!--        </dependency>-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
     </dependencies>
 
     <dependencyManagement>
@@ -124,6 +119,7 @@
                 <version>2.3.4.RELEASE</version>
                 <configuration>
                     <mainClass>seecoder.devcloud.web.devcloudWebApplication</mainClass>
+                    <!--  todo 打包时需排除一些无法回滚的测试类  -->
                 </configuration>
                 <executions>
                     <execution>

+ 6 - 1
web/src/main/java/seecoder/devcloud/web/DevcloudWebApplication.java

@@ -1,10 +1,13 @@
 package seecoder.devcloud.web;
 
+import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 import java.time.ZoneId;
 import java.util.TimeZone;
@@ -12,8 +15,10 @@ import java.util.TimeZone;
 
 @EnableScheduling
 @EnableAsync
-@SpringBootApplication
+@SpringBootApplication(exclude = {HibernateJpaAutoConfiguration.class})
 @ComponentScan({"seecoder.devcloud.api","seecoder.devcloud.common","seecoder.devcloud.web"})
+@MapperScan(basePackages = {"seecoder.devcloud.web.dao"})
+@EnableTransactionManagement //开始事务
 public class DevcloudWebApplication {
 
     public static void main(String[] args) {

+ 153 - 0
web/src/main/java/seecoder/devcloud/web/dao/InsertUpdateSqlProvider.java

@@ -0,0 +1,153 @@
+package seecoder.devcloud.web.dao;
+
+import org.apache.ibatis.jdbc.SQL;
+
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+/**
+ * @author PuHong Weng
+ * @date 2021/3/1
+ * @description: 帮助快速写插入和更新的,注意 pojo类的字段要和数据库表对应, 用法示例见 UserMapper.class
+ */
+public class InsertUpdateSqlProvider {
+
+    /**
+     * @param obj 插入对象, 用来获取字段和表名
+     * @param ignoredCols 插入时需要忽略的列名 列名格式是Underscore,如created_at
+     * @return sql
+     */
+    public static String insert(Object obj,String... ignoredCols){
+        Map<String, String> map;
+        try {
+            map = getFieldsMap(obj, true, false);
+            // 创建 需要赋当前的时间的timestamp字段 在这里排除,否则无法正常赋值
+            for (String col: ignoredCols){
+                map.remove(col);
+            }
+        } catch (IllegalArgumentException | IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+        return getInsertSQL(obj, map);
+    }
+
+    private static String getInsertSQL(Object obj, Map<String, String> map){
+        return new SQL() {
+            {
+                INSERT_INTO(getTableName(obj));
+                for (String col : map.keySet()) {
+                    VALUES(col, map.get(col));
+                }
+            }
+        }.toString();
+    }
+
+    /**
+     * 更新obj所含的字段,但是不包括为null的字段
+     */
+    public static String updateById(Object obj) {
+        return updateById(obj, false);
+    }
+
+    /**
+     * 更新obj所含的字段,但是包括为null的字段
+     */
+    public static String updateWithNullById(Object obj) {
+        return updateById(obj, true);
+    }
+
+    /**
+     * 按传入的列名更新字段
+     */
+    public static String updateWithColsById(Object obj, String... cols) {
+        return updateById(obj, true, cols);
+    }
+
+    private static String updateById(Object obj, boolean includeNullValueField, String... cols) {
+        Map<String, String> map;
+        try {
+            map = getFieldsMap(obj, includeNullValueField, cols.length == 0);
+            map.remove("id");
+            //有可变长参数就保留可边长参数
+            if (cols.length!=0){
+                Set<String> colsSet = Arrays.stream(cols).collect(Collectors.toSet());
+                map = map.entrySet().stream()
+                        .filter(e -> colsSet.contains(e.getKey()))
+                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+            }
+        } catch (IllegalArgumentException | IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+        return getUpdateSQL(obj,map);
+    }
+
+    private static String getUpdateSQL(Object obj, Map<String, String> map){
+        return new SQL() {
+            {
+                UPDATE(getTableName(obj));
+                for (String col : map.keySet()) {
+                    SET(col + "=" + map.get(col));
+                }
+                WHERE("id = #{id}");
+            }
+        }.toString();
+    }
+
+
+    /**
+     *
+     * @param obj 获取字段名的对象
+     * @param includeNullValue 是否要获取为null的字段名
+     * @param singleParam sql参数只有一个实体类的时候 设置为true,多个参数的时候设置false
+     * @return
+     * @throws IllegalArgumentException
+     * @throws IllegalAccessException
+     */
+    private static Map<String, String> getFieldsMap(Object obj, boolean includeNullValue, boolean singleParam)
+            throws IllegalArgumentException, IllegalAccessException {
+        HashMap<String, String> result = new HashMap<>();
+        Class<?> cls = obj.getClass();
+        Field[] fields = cls.getDeclaredFields(); // getDeclaredFields
+        for (Field f : fields) {
+            String col = f.getName();
+            String colName = col.substring(0, 1).toUpperCase() + col.substring(1);
+            f.setAccessible(true);
+            try {
+                cls.getMethod("get" + colName);
+                cls.getMethod("set" + colName, f.getType());
+            } catch (NoSuchMethodException | SecurityException e) {
+                continue;
+            }
+            /**
+             * 单参数的时候
+             * #{colname}可以直接取到实体的属性
+             * 多参数的时候
+             * #{param1.colname}来取实体的属性
+             */
+            String paramPrefix =  singleParam? "#{" : "#{param1.";
+            if ((!"id".equals(col) && includeNullValue) || f.get(obj) != null) {
+
+
+
+                result.put(camelCase2Underscore(col), paramPrefix + col + "}");
+            }
+        }
+        return result;
+    }
+
+    public static String getTableName(Object obj) {
+        return camelCase2Underscore(obj.getClass().getSimpleName());
+    }
+
+    public static String camelCase2Underscore(String s) {
+        StringBuffer buf = new StringBuffer();
+        for (String w : s.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
+            buf.append("_").append(w.toLowerCase());
+        }
+        return buf.substring(1);
+    }
+
+}

+ 48 - 0
web/src/main/java/seecoder/devcloud/web/dao/user/GroupMapper.java

@@ -0,0 +1,48 @@
+package seecoder.devcloud.web.dao.user;
+
+
+
+import org.apache.ibatis.annotations.*;
+import seecoder.devcloud.web.dao.InsertUpdateSqlProvider;
+import seecoder.devcloud.web.po.user.Group;
+
+import java.awt.geom.GeneralPath;
+import java.util.List;
+
+/**
+ * Description:
+ *
+ * @author xxz
+ * Created on 10/30/2018
+ */
+@Mapper
+public interface GroupMapper {
+
+
+    /**
+     * id并非由此后端生成,而是gitlab创建后,将gitlab groupid填入
+     */
+    @InsertProvider(type = InsertUpdateSqlProvider.class, method = "insert")
+    int insert(Group group,String... ignoredCols);
+
+    @UpdateProvider(type= InsertUpdateSqlProvider.class, method="updateById")
+    int update(Group group);
+
+    @Select("select * from group where id = #{id}")
+    Group findGroupById(Integer id);
+
+    @Select("select * from group where id = #{id}")
+    @Result(column = "id", property = "member",many = @Many(
+            select = ""
+    ))
+    Group findWithMembersById(Integer id);
+
+    @Deprecated
+    Group findGroupByShareLink(String shareLink);
+
+
+
+
+
+
+}

+ 37 - 0
web/src/main/java/seecoder/devcloud/web/dao/user/GroupMemberMapper.java

@@ -0,0 +1,37 @@
+package seecoder.devcloud.web.dao.user;
+
+import jnr.ffi.annotations.In;
+import org.apache.ibatis.annotations.*;
+import seecoder.devcloud.web.po.user.Group;
+import seecoder.devcloud.web.po.user.User;
+
+import java.util.List;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/3/4
+ * @description:
+ */
+public interface GroupMemberMapper {
+
+    @Insert("insert into group_member (group_id,user_id) values (#{groupId},#{userId}) ")
+    int insert(int groupId,int userId);
+
+    @Delete("delete from group_member where group_id = #{groupId} and user_id = #{userId}")
+    int delete(int groupId, int userId);
+
+    @Select("select user_id from group_member where group_id = #{groupId}")
+    List<Integer> selectUserIdsByGroupId(int groupId);
+
+    @Select("select group_id from group_member where user_id = #{userId}")
+    List<Integer> selectGroupIdsByUserId(int groupId);
+
+    @Select("select user_id from group_member where group_id = #{groupId}")
+    @Result(column = "user_id", one = @One(select = "seecoder.devcloud.web.service.impl.user.findById"))
+    List<User> selectUserByGroupId(int groupId);
+
+
+    @Select("select group_id from group_member where user_id = #{userId}")
+    List<Group> selectGroupByUserId(int groupId);
+
+}

+ 39 - 0
web/src/main/java/seecoder/devcloud/web/dao/user/UserMapper.java

@@ -0,0 +1,39 @@
+package seecoder.devcloud.web.dao.user;
+
+
+import org.apache.ibatis.annotations.*;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import seecoder.devcloud.web.dao.InsertUpdateSqlProvider;
+import seecoder.devcloud.web.po.user.User;
+
+import java.sql.Timestamp;
+
+
+@Mapper
+public interface UserMapper {
+
+	/**
+	 * 这里的id是取自gitlab的用户id,所以不需要自增和回填
+	 */
+	@InsertProvider(type = InsertUpdateSqlProvider.class, method = "insert")
+	int insert(User user,String... ignoredCols);
+
+	@UpdateProvider(type= InsertUpdateSqlProvider.class, method="updateById")
+	int updateById(User user);
+
+	@Delete("delete from user where id = #{id}")
+	int delete(User user);
+
+
+	@Select("select * from user where id = #{id}")
+	User findById(int id);
+
+	@Select("select * from user where username = #{username}")
+	User findByUsername(String username);
+
+	@Select("select * from user where email = #{email}")
+	User findByEmail(String email);
+
+
+}

+ 19 - 0
web/src/main/java/seecoder/devcloud/web/enums/GroupType.java

@@ -0,0 +1,19 @@
+package seecoder.devcloud.web.enums;
+
+/**
+ * Description:
+ * 群组类型
+ *
+ * @author xxz
+ * Created on 10/26/2018
+ */
+public enum GroupType {
+    /**
+     * 课程内由学生组成的小组
+     */
+    GROUP,
+    /**
+     * 课程
+     */
+    CLASS
+}

+ 23 - 0
web/src/main/java/seecoder/devcloud/web/enums/UserIdentity.java

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

+ 1 - 7
web/src/main/java/seecoder/devcloud/web/config/SwaggerConfig.java → web/src/main/java/seecoder/devcloud/web/infrastructure/config/SwaggerConfig.java

@@ -1,4 +1,4 @@
-package seecoder.devcloud.web.config;
+package seecoder.devcloud.web.infrastructure.config;
 
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
@@ -12,12 +12,6 @@ import springfox.documentation.oas.annotations.EnableOpenApi;
 import springfox.documentation.service.ApiInfo;
 import springfox.documentation.spi.DocumentationType;
 import springfox.documentation.spring.web.plugins.Docket;
-import springfox.documentation.swagger2.annotations.EnableSwagger2;
-import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
-
-import java.util.function.Predicate;
-
-import static springfox.documentation.builders.PathSelectors.regex;
 
 /**
  * @author PuHong Weng

+ 81 - 0
web/src/main/java/seecoder/devcloud/web/infrastructure/security/WebSecurityConfiguration.java

@@ -0,0 +1,81 @@
+//package seecoder.devcloud.web.infrastructure.security;
+//
+//
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+//import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+//import org.springframework.security.config.annotation.web.builders.WebSecurity;
+//import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+//import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+//import org.springframework.security.core.userdetails.UserDetailsService;
+//import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+//import seecoder.devcloud.api.ApplicationProperties;
+//import seecoder.devcloud.web.dao.user.UserMapper;
+//import seecoder.devcloud.web.service.user.UserService;
+//
+//
+//@EnableWebSecurity
+//@EnableGlobalMethodSecurity(jsr250Enabled = true)
+//public class WebSecurityConfiguration {
+////	@Configuration
+////	@Order(Ordered.LOWEST_PRECEDENCE)
+////	public static class ApiWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
+////		@Override
+////		protected void configure(HttpSecurity http) throws Exception {
+////			http.antMatcher("/api/**")
+////					.authorizeRequests()
+////					.anyRequest().authenticated()
+////					.and()
+////					.httpBasic()
+////					.and()
+////					.csrf().disable()
+////					.cors();
+////		}
+////	}
+//
+//	@Configuration
+//	public static class DefaultWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
+//		private final UserDetailsService userDetailsService;
+//		private final LoginRedirectHandler loginRedirectHandler;
+//		@Autowired
+//		private UserMapper userMapper;
+//		@Autowired
+//		private ApplicationProperties applicationProperties;
+//		@Autowired
+//		private UserService userService;
+//
+//		@Autowired
+//		public DefaultWebSecurityConfiguration(UserDetailsServiceImpl userDetailsService, LoginRedirectHandler loginRedirectHandler) {
+//			this.userDetailsService = userDetailsService;
+//			this.loginRedirectHandler = loginRedirectHandler;
+//		}
+//		@Override
+//		protected void configure(HttpSecurity http) throws Exception {
+//			JwtAuthenticationFilter jwtAuthenticationFilter =
+//					new JwtAuthenticationFilter(authenticationManager(),
+//							userDetailsService, new RestAuthorEntry(),
+//							new AntPathRequestMatcher("/api/auth/receive", "GET"),
+//							applicationProperties,
+//							userMapper,
+//							userService);
+//			http
+//					.authorizeRequests()
+//						.antMatchers("/api/auth/register","/test" ,"/internal/**").permitAll()
+//			 			.antMatchers("/file/**").hasAnyAuthority(TEACHER_AUTHORITY.getAuthority(), ADMIN_AUTHORITY.getAuthority())
+//						.anyRequest().authenticated()
+//						.and()
+//						.addFilter(jwtAuthenticationFilter)
+//						.cors()
+//						.and()
+//						.csrf().disable();
+//		}
+//
+//		@Override
+//		public void configure(WebSecurity web) throws Exception {
+//			web
+//					.ignoring()
+//					.antMatchers("/api/auth/register","/test" ,"/internal/**");
+//		}
+//	}
+//}

+ 16 - 0
web/src/main/java/seecoder/devcloud/web/infrastructure/security/WebSecurityConstants.java

@@ -0,0 +1,16 @@
+package seecoder.devcloud.web.infrastructure.security;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+public abstract class WebSecurityConstants {
+	private static final String ROLE_PREFIX = "ROLE_";
+
+	public static final String STUDENT_ROLE = "STUDENT";
+	public static final String TEACHER_ROLE = "TEACHER";
+	public static final String ADMIN_ROLE = "ADMIN";
+
+	public static final GrantedAuthority STUDENT_AUTHORITY = new SimpleGrantedAuthority(ROLE_PREFIX + STUDENT_ROLE);
+	public static final GrantedAuthority TEACHER_AUTHORITY = new SimpleGrantedAuthority(ROLE_PREFIX + TEACHER_ROLE);
+	public static final GrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority(ROLE_PREFIX + ADMIN_ROLE);
+}

+ 50 - 0
web/src/main/java/seecoder/devcloud/web/po/user/Group.java

@@ -0,0 +1,50 @@
+package seecoder.devcloud.web.po.user;
+
+
+import lombok.Builder;
+import lombok.Data;
+import seecoder.devcloud.web.enums.GroupType;
+
+import java.util.List;
+
+/**
+ * Description:
+ * 小组的实体类
+ *
+ * @author xxz
+ * Created on 10/26/2018
+ */
+@Data
+@Builder
+public class Group {
+
+    /**
+     * id并非由此后端生成,而是gitlab创建后,将gitlab groupid填入
+     */
+    private Integer id;
+
+    /**
+     * 来源于gitlab
+     */
+    private Integer namespace;
+    /**
+     * 小组类型
+     */
+    private GroupType type;
+
+    /**
+     * 小组的加组链接
+     */
+    private String shareLink;
+
+    /**
+     * 组名
+     */
+    private String name;
+
+    /**
+     * 小组成员
+     */
+    private List<User> members;
+
+}

+ 53 - 0
web/src/main/java/seecoder/devcloud/web/po/user/User.java

@@ -0,0 +1,53 @@
+package seecoder.devcloud.web.po.user;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.type.Alias;
+import org.hibernate.annotations.LazyCollection;
+import org.hibernate.annotations.LazyCollectionOption;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+import seecoder.devcloud.web.enums.UserIdentity;
+
+import javax.persistence.*;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+public class User {
+
+	/**
+	 * 注意用户id并非由此后端数据库生成,而是gitlab创建后填入,以确保两者的关联性的正确
+	 */
+	private Integer id;
+
+	private String username;
+
+	private String nickname="";
+
+	private String password;
+
+	private String phone;
+
+	private String email;
+
+	private UserIdentity role;
+
+	private Timestamp createdAt;
+
+	/**
+	 * 这个属性来自于gitlab
+	 */
+	private Integer namespace;
+
+	/**
+	 * 这个属性来自于gitlab
+	 */
+	private String token;
+}

+ 92 - 0
web/src/main/java/seecoder/devcloud/web/service/impl/user/GroupServiceImp.java

@@ -0,0 +1,92 @@
+package seecoder.devcloud.web.service.impl.user;
+
+
+import org.gitlab4j.api.GitLabApiException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import seecoder.devcloud.api.gitlab.GitlabApi;
+import seecoder.devcloud.api.gitlab.model.GitlabGroup;
+import seecoder.devcloud.common.exceptions.EntityNotFoundException;
+import seecoder.devcloud.web.dao.user.GroupMapper;
+import seecoder.devcloud.web.dao.user.GroupMemberMapper;
+import seecoder.devcloud.web.dao.user.UserMapper;
+import seecoder.devcloud.web.enums.GroupType;
+import seecoder.devcloud.web.po.user.Group;
+import seecoder.devcloud.web.po.user.User;
+import seecoder.devcloud.web.service.user.GroupService;
+import seecoder.devcloud.web.vo.user.CustomUserDetails;
+import seecoder.devcloud.web.vo.user.GroupVO;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+
+@Service
+public class GroupServiceImp implements GroupService {
+    private final GroupMapper groupMapper;
+    private final UserMapper userMapper;
+    private final GroupMemberMapper groupMemberMapper;
+    private final GitlabApi gitlabApi;
+
+    @Autowired
+    public GroupServiceImp(GroupMapper groupMapper, UserMapper userMapper, GroupMemberMapper groupMemberMapper, GitlabApi gitlabApi) {
+        this.groupMapper = groupMapper;
+        this.userMapper = userMapper;
+        this.groupMemberMapper = groupMemberMapper;
+        this.gitlabApi = gitlabApi;
+    }
+
+    @Override
+    @Transactional
+    public GroupVO createGroup(String name,  Integer userId) throws EntityNotFoundException {
+        User user = userMapper.findById(userId);
+
+        List<User> members = new ArrayList<>();
+        members.add(user);
+        GitlabGroup gitGroup;
+        try {
+            gitGroup = gitlabApi.createGroup(name);
+            gitlabApi.addMemberToGroup(gitGroup.getId(), user.getId());
+        } catch (GitLabApiException e) {
+            throw new RuntimeException("创建Group时发生异常[" + e.getMessage() + "]");
+        }
+        Group group = Group.builder()
+                .id(gitGroup.getId())
+                .name(name)
+                .namespace(gitGroup.getNamespace())
+                .shareLink(UUID.randomUUID().toString().replace("-", ""))
+                .type(GroupType.GROUP)
+                .members(members)
+                .build();
+        groupMapper.insert(group, "members");
+        groupMemberMapper.insert(gitGroup.getId(),userId);
+        return new GroupVO(group);
+    }
+
+
+    @Override
+    @Transactional
+    public GroupVO addMember(Integer userId, Integer invitedUserId, Integer groupId) throws EntityNotFoundException {
+        List<Integer> userIds = groupMemberMapper.selectUserIdsByGroupId(groupId);
+        if (!userIds.contains(userId)) {
+            throw new EntityNotFoundException("小组邀请发生错误,用户没有权限!");
+        }
+        if (userIds.contains(invitedUserId)) {
+            throw new RuntimeException("邀请的用户已经加入小组!");
+        }
+        try {
+            gitlabApi.addMemberToGroup(groupId, invitedUserId);
+        } catch (GitLabApiException e) {
+            throw new RuntimeException("将成员加入Group时发生异常[" + e.getMessage() + "]");
+        }
+        groupMemberMapper.insert(groupId,invitedUserId);
+
+        return new GroupVO(group);
+        }
+
+    }
+
+}

+ 86 - 84
web/src/main/java/seecoder/devcloud/web/service/impl/user/MailServiceImpl.java

@@ -1,84 +1,86 @@
-//package seecoder.devcloud.web.service.impl.user;
-//
-//
-//import lombok.extern.apachecommons.CommonsLog;
-//import org.apache.commons.lang3.StringUtils;
-//import org.springframework.beans.factory.annotation.Autowired;
-//
-//import org.springframework.mail.MailException;
-//import org.springframework.mail.SimpleMailMessage;
-//import org.springframework.mail.javamail.JavaMailSender;
-//import org.springframework.mail.javamail.MimeMessageHelper;
-//import org.springframework.scheduling.annotation.Async;
-//import org.springframework.stereotype.Service;
-//import org.springframework.web.servlet.ModelAndView;
-//import org.thymeleaf.TemplateEngine;
-//import org.thymeleaf.context.Context;
-//import seecoder.devcloud.api.ApplicationProperties;
-//import seecoder.devcloud.web.service.user.MailService;
-//
-//import javax.mail.MessagingException;
-//import javax.mail.internet.InternetAddress;
-//import javax.mail.internet.MimeMessage;
-//import java.io.UnsupportedEncodingException;
-//import java.util.Locale;
-//
-//@Service
-//@CommonsLog
-//public class MailServiceImpl implements MailService {
-//	private final TemplateEngine templateEngine;
-//	private final JavaMailSender mailSender;
-//
-//	private final String from;
-//	private final String personal;
-//
-//	@Autowired
-//	public MailServiceImpl(TemplateEngine templateEngine, JavaMailSender mailSender, ApplicationProperties properties) {
-//		this.templateEngine = templateEngine;
-//		this.mailSender = mailSender;
-//
-//		ApplicationProperties.Mail mail = properties.getMail();
-//		this.from = mail.getFrom();
-//		this.personal = mail.getPersonal();
-//	}
-//
-//	@Override
-//	@Async
-//	public void send(String to, String personal, String subject, ModelAndView modelAndView) {
-//		if (StringUtils.isBlank(from)) return;
-//		MimeMessage message = mailSender.createMimeMessage();
-//		MimeMessageHelper helper = new MimeMessageHelper(message);
-//		try {
-//			try {
-//				helper.setFrom(from, this.personal);
-//			} catch (UnsupportedEncodingException e) {
-//				helper.setFrom(from);
-//			}
-//			try {
-//				helper.setTo(new InternetAddress(to, personal));
-//			} catch (UnsupportedEncodingException e) {
-//				helper.setTo(to);
-//			}
-//			helper.setSubject(subject);
-//			helper.setText(templateEngine.process(modelAndView.getViewName(), new Context(Locale.CHINA, modelAndView.getModel())), true);
-//		} catch (MessagingException e) {
-//			log.warn("生成发送给[" + to + "(" + personal + ")]的邮件[" + subject + "]时发生异常:" + e.getMessage());
-//			return;
-//		}
-//		try {
-//			mailSender.send(message);
-//		} catch (MailException e) {
-//			log.warn("投递发送给[" + to + "(" + personal + ")]的邮件[" + subject + "]时发生异常:" + e.getMessage());
-//		}
-//	}
-//
-//	@Async
-//	public void send(String text, String subject,String... to) {
-//		SimpleMailMessage message = new SimpleMailMessage();
-//		message.setFrom(this.from);
-//		message.setTo(to);
-//		message.setSubject(subject);
-//		message.setText(text);
-//		mailSender.send(message);
-//	}
-//}
+package seecoder.devcloud.web.service.impl.user;
+
+
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.springframework.mail.MailException;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.ModelAndView;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.context.Context;
+import seecoder.devcloud.api.ApplicationProperties;
+import seecoder.devcloud.web.service.user.MailService;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import java.io.UnsupportedEncodingException;
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+
+@Service
+@CommonsLog
+public class MailServiceImpl implements MailService {
+	private final TemplateEngine templateEngine;
+	private final JavaMailSender mailSender;
+
+	private final String from;
+	private final String personal;
+
+	@Autowired
+	public MailServiceImpl(TemplateEngine templateEngine, JavaMailSender mailSender, ApplicationProperties properties) {
+		this.templateEngine = templateEngine;
+		this.mailSender = mailSender;
+
+		ApplicationProperties.Mail mail = properties.getMail();
+		this.from = mail.getFrom();
+		this.personal = mail.getPersonal();
+	}
+
+	@Override
+	@Async
+	public void send(String to, String personal, String subject, ModelAndView modelAndView) {
+		if (StringUtils.isBlank(from)) return;
+		MimeMessage message = mailSender.createMimeMessage();
+		MimeMessageHelper helper = new MimeMessageHelper(message);
+		try {
+			try {
+				helper.setFrom(from, this.personal);
+			} catch (UnsupportedEncodingException e) {
+				helper.setFrom(from);
+			}
+			try {
+				helper.setTo(new InternetAddress(to, personal));
+			} catch (UnsupportedEncodingException e) {
+				helper.setTo(to);
+			}
+			helper.setSubject(subject);
+			helper.setText(templateEngine.process(modelAndView.getViewName(), new Context(Locale.CHINA, modelAndView.getModel())), true);
+		} catch (MessagingException e) {
+			log.warn("生成发送给[" + to + "(" + personal + ")]的邮件[" + subject + "]时发生异常:" + e.getMessage());
+			return;
+		}
+		try {
+			mailSender.send(message);
+		} catch (MailException e) {
+			log.warn("投递发送给[" + to + "(" + personal + ")]的邮件[" + subject + "]时发生异常:" + e.getMessage());
+		}
+	}
+
+	@Async
+	public CompletableFuture<Void> send(String text, String subject, String... to) {
+		SimpleMailMessage message = new SimpleMailMessage();
+		message.setFrom(this.from);
+		message.setTo(to);
+		message.setSubject(subject);
+		message.setText(text);
+		mailSender.send(message);
+		return null;
+	}
+}

+ 151 - 232
web/src/main/java/seecoder/devcloud/web/service/impl/user/UserServiceImpl.java

@@ -1,232 +1,151 @@
-//package seecoder.devcloud.web.service.impl.user;
-//
-//
-//import org.apache.commons.codec.digest.DigestUtils;
-//import org.gitlab4j.api.GitLabApiException;
-//import org.springframework.beans.BeanUtils;
-//import org.springframework.beans.factory.annotation.Autowired;
-//import org.springframework.data.domain.Page;
-//import org.springframework.data.domain.PageRequest;
-//import org.springframework.data.domain.Pageable;
-//import org.springframework.data.domain.Sort;
-//import org.springframework.stereotype.Service;
-//import org.springframework.transaction.annotation.Transactional;
-//import org.springframework.util.Assert;
-//import seecoder.devcloud.api.gitlab.GitlabApi;
-//import seecoder.devcloud.api.gitlab.model.GitlabUser;
-//import seecoder.devcloud.api.jenkins.JenkinsApi;
-//import seecoder.devcloud.common.exceptions.Asserts;
-//import seecoder.devcloud.common.exceptions.ServiceException;
-//import seecoder.devcloud.common.util.ToolKit;
-//import seecoder.devcloud.web.service.user.MailService;
-//
-//@Service
-//public class UserServiceImpl implements UserService {
-//	private static final Sort PAGE_SORT = Sort.by(Sort.Direction.ASC, "id");
-//
-//	private final UserDAO userDAO;
-//	private final MailService mailService;
-//	private final GitlabApi gitlabApi;
-//	private final JenkinsApi jenkinsApi;
-//
-//	@Autowired
-//	public UserServiceImpl(UserDAO userDAO, MailService mailService, GitlabApi gitlabApi, JenkinsApi jenkinsApi) {
-//		this.userDAO = userDAO;
-//		this.mailService = mailService;
-//		this.gitlabApi = gitlabApi;
-//		this.jenkinsApi = jenkinsApi;
-//	}
-//
-//	@Override
-//	@Transactional
-//	public UserVO create(UserDTO userDTO) throws ServiceException {
-//		return register(userDTO.getUsername(), userDTO.getEmail(), null,userDTO.getNickname(), userDTO.getRole());
-//	}
-//
-//	@Override
-//	@Transactional
-//	public User register(String username, String name, String email, String password, String phone, UserIdentity role, Integer pid) throws ServiceException {
-//		User user = userDAO.findByUsername(username);
-//		Asserts.isNull(user, "用户名已被使用!");
-//		user = userDAO.findByEmail(email);
-//		Asserts.isNull(user, "邮箱已被使用!");
-//		String realPassword = password == null ? ToolKit.randomPassword() : password;
-//		GitlabUser gitlabUser;
-//		try {
-//			gitlabUser = gitlabApi.createUser(username, email, realPassword);
-//		} catch (GitLabApiException e) {
-//			throw new ServiceException("创建GitLab用户时发生异常[" + e.getMessage() + "]");
-//		}
-//		user = new User();
-//		BeanUtils.copyProperties(gitlabUser, user);
-//		if(name!=null){
-//			user.setNickname(name);
-//		}
-//
-//		user.setUsername(username);
-//		user.setEmail(email);
-//		user.setPassword(DigestUtils.sha256Hex(realPassword));
-//		user.setPid(pid);
-//		user.setPhone(phone);
-//		user.setRole(role);
-//		user = userDAO.save(user);
-//		if (password == null) {
-//			mailService.send("尊敬的" + username + "您好, 您的初始密码为" + realPassword + ", 请及时修改", "注册成功 | SEECODER", email);
-//		}
-//		return user;
-//	}
-//
-//	@Override
-//	public Page<UserVO> retrievePage(int page, int limit, String search) {
-//		Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
-//		Page<User> pageResult;
-//		if (search == null || search.isEmpty()) {
-//			pageResult = userDAO.findAll(pageable);
-//		} else {
-//			pageResult = userDAO.findAllByUsernameLike("%" + search + "%", pageable);
-//		}
-//		return pageResult.map(UserVO::new);
-//	}
-//
-//	@Override
-//	public UserVO retrieve(int userId) throws ServiceException {
-//		User user = userDAO.findById(userId);
-//		Asserts.notNull(user, "所选用户不存在");
-//		return new UserVO(user);
-//	}
-//
-//	@Override
-//	@Transactional
-//	public void delete(int userId) throws ServiceException {
-//		User user = userDAO.findById(userId);
-//		Assert.notNull(user, "找不到要删除的用户");
-//		Assert.isTrue(user.getRole() == UserIdentity.STUDENT, "目标用户只能是学生");
-//		try {
-//			gitlabApi.deleteUser(userId);
-//		} catch (Exception e) {
-//			throw new ServiceException("删除GitLab用户时发生异常[" + e.getMessage() + "]");
-//		}
-//		for (Result result : user.getResultSet()) {
-//			if (!result.isDeleted()) {
-//				try {
-//					jenkinsApi.deleteJob(result.getId());
-//				} catch (Exception e) {
-//					throw new ServiceException("删除Jenkins项目时发生异常[" + e.getMessage() + "]");
-//				}
-//			}
-//		}
-//		userDAO.delete(user);
-//	}
-//
-//	@Override
-//	@Transactional
-//	public UserSerializer register(RegisterForm form) throws ServiceException {
-//		return new UserSerializer(register(form.getUsername(), form.getEmail(), form.getPassword(),form.getNickname(), UserIdentity.STUDENT));
-//	}
-//	@Override
-//	@Transactional
-//	public UserSerializer registerTeacher(RegisterForm form) throws ServiceException {
-//		return new UserSerializer(register(form.getUsername(), form.getEmail(), form.getPassword(),form.getNickname(), UserIdentity.TEACHER));
-//	}
-//	private UserVO register(String username, String email, String password, String nickname,UserIdentity role) throws ServiceException {
-//		User user = userDAO.findByUsername(username);
-//		Asserts.isNull(user, "用户名已被使用!");
-//		user = userDAO.findByEmail(email);
-//		Asserts.isNull(user, "邮箱已被使用!");
-//		String realPassword = password == null ? ToolKit.randomPassword() : password;
-//		GitlabUser gitlabUser;
-//		try {
-//			gitlabUser = gitlabApi.createUser(username, email, realPassword);
-//		} catch (GitLabApiException e) {
-//			throw new ServiceException("创建GitLab用户时发生异常[" + e.getMessage() + "]");
-//		}
-//		user = new User();
-//		BeanUtils.copyProperties(gitlabUser, user);
-//		if(nickname!=null){
-//			user.setNickname(nickname);
-//		}
-//
-//		user.setUsername(username);
-//		user.setEmail(email);
-//		user.setPassword(DigestUtils.sha256Hex(realPassword));
-//		user.setRole(role);
-//		final UserVO userVO = new UserVO(userDAO.save(user));
-//		if (password == null) {
-//			mailService.send("尊敬的" + username + "您好, 您的初始密码为" + realPassword + ", 请及时修改", "注册成功 | SEECODER", email);
-//		}
-//		return userVO;
-//	}
-//
-//
-//	@Override
-//	public UserSerializer getUserById(Integer userid) {
-//		return new UserSerializer(userDAO.getOne(userid));
-//	}
-//
-//	@Override
-//	public UserSerializer changeUserInfo(Integer userid,String nickName, String bio) throws ServiceException {
-//		User one = userDAO.findById(userid).orElse(null);
-//		if (one == null) {
-//			throw new ServiceException("用户不存在");
-//		}
-//		if(nickName!=null){
-//			one.setNickname(nickName);
-//		}
-//		if(bio!=null){
-//			one.setBio(bio);
-//		}
-//		return new UserSerializer(userDAO.save(one));
-//	}
-//
-//	@Override
-//	public UserSerializer changeUserPassword(Integer userid,String oldPassword, String newPassword) throws ServiceException {
-//		User one = userDAO.findById(userid).orElse(null);
-//		if (one == null) {
-//			throw new ServiceException("用户不存在");
-//		}
-//		if(!one.getPassword().equals(DigestUtils.sha256Hex(oldPassword))){
-//			throw new ServiceException("原密码错误");
-//		}else{
-//			one.setPassword(DigestUtils.sha256Hex(newPassword));
-//		}
-//		try {
-//			gitlabApi.changePassword(userid, newPassword);
-//		} catch (GitLabApiException e) {
-//			throw new ServiceException("修改GitLab用户密码时发生异常[" + e.getMessage() + "]");
-//		}
-//		mailService.send( "您的新密码为"+newPassword,"密码变更提醒 | SEECODER",one.getEmail());
-//		return new UserSerializer(userDAO.save(one));
-//	}
-//
-//	@Override
-//	public void changePassword(int userId, ChangePasswordForm form) throws ServiceException {
-//		User user = userDAO.findById(userId);
-//		if (!user.getPassword().equals(DigestUtils.sha256Hex(form.getOrigin()))) {
-//			throw new InvalidRequestException("原密码验证失败,如忘记密码请联系管理员重置");
-//		}
-//		changePassword(user, form.getPassword(), false);
-//	}
-//
-//	@Override
-//	public void resetPassword(int userId) throws ServiceException {
-//		User user = userDAO.findById(userId);
-//		Asserts.notNull(user, "所选用户不存在");
-//		String password = ToolKit.randomPassword();
-//		changePassword(user, password, true);
-//	}
-//
-//
-//
-//	private void changePassword(User user, String password, boolean reset) throws ServiceException {
-//		try {
-//			gitlabApi.changePassword(user.getId(), password);
-//		} catch (GitLabApiException e) {
-//			throw new ServiceException("修改GitLab用户密码时发生异常[" + e.getMessage() + "]");
-//		}
-//		user.setPassword(DigestUtils.sha256Hex(password));
-//		user = userDAO.save(user);
-//		if (reset) {
-//			mailService.send( "您的新密码为"+password,"密码变更提醒 | SEECODER",user.getEmail());
-//		}
-//	}
-//}
+package seecoder.devcloud.web.service.impl.user;
+
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.gitlab4j.api.GitLabApiException;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+import seecoder.devcloud.api.gitlab.GitlabApi;
+import seecoder.devcloud.api.gitlab.model.GitlabUser;
+import seecoder.devcloud.api.jenkins.JenkinsApi;
+import seecoder.devcloud.common.exceptions.Asserts;
+import seecoder.devcloud.common.exceptions.InvalidRequestException;
+import seecoder.devcloud.common.exceptions.ServiceException;
+import seecoder.devcloud.common.util.ToolKit;
+import seecoder.devcloud.web.dao.user.UserMapper;
+import seecoder.devcloud.web.vo.user.ChangePasswordVO;
+import seecoder.devcloud.web.vo.user.RegisterVO;
+
+import seecoder.devcloud.web.enums.UserIdentity;
+import seecoder.devcloud.web.po.user.User;
+import seecoder.devcloud.web.vo.user.UserVO;
+import seecoder.devcloud.web.service.user.MailService;
+import seecoder.devcloud.web.service.user.UserService;
+
+@Service
+public class UserServiceImpl implements UserService {
+	private static final Sort PAGE_SORT = Sort.by(Sort.Direction.ASC, "id");
+
+	private final UserMapper userMapper;
+	private final MailService mailService;
+	private final GitlabApi gitlabApi;
+	private final JenkinsApi jenkinsApi;
+
+	@Autowired
+	public UserServiceImpl(UserMapper userMapper, MailService mailService, GitlabApi gitlabApi, JenkinsApi jenkinsApi) {
+		this.userMapper = userMapper;
+		this.mailService = mailService;
+		this.gitlabApi = gitlabApi;
+		this.jenkinsApi = jenkinsApi;
+	}
+
+	@Override
+	@Transactional
+	public UserVO register(RegisterVO form) throws ServiceException {
+		return register(form.getUsername(), form.getEmail(), form.getPassword(),form.getNickname(), UserIdentity.STUDENT);
+	}
+
+	@Override
+	@Transactional
+	public UserVO registerTeacher(RegisterVO form) throws ServiceException {
+		return register(form.getUsername(), form.getEmail(), form.getPassword(),form.getNickname(), UserIdentity.TEACHER);
+	}
+
+	private UserVO register(String username, String email, String password, String nickname,UserIdentity role) throws ServiceException {
+		User user = userMapper.findByUsername(username);
+		Asserts.isNull(user, "用户名已被使用!");
+		user = userMapper.findByEmail(email);
+		Asserts.isNull(user, "邮箱已被使用!");
+		String realPassword = password == null ? ToolKit.randomPassword() : password;
+		GitlabUser gitlabUser;
+		try {
+			gitlabUser = gitlabApi.createUser(username, email, realPassword);
+		} catch (GitLabApiException e) {
+			e.printStackTrace();
+			throw new ServiceException("创建GitLab用户时发生异常[" + e.getMessage() + "]");
+		}
+		user = new User();
+		BeanUtils.copyProperties(gitlabUser, user);
+		if(nickname!=null){
+			user.setNickname(nickname);
+		}
+
+		user.setUsername(username);
+		user.setEmail(email);
+		//在gitlab注册后,直接加密
+		user.setPassword(DigestUtils.sha256Hex(realPassword));
+		user.setRole(role);
+		userMapper.insert(user, "created_at");
+		final UserVO userVO = new UserVO(user);
+		if (password == null) {
+			mailService.send("尊敬的" + username + "您好, 您的初始密码为" + realPassword + ", 请及时修改", "注册成功 | SEECODER", email);
+		}
+		return userVO;
+	}
+
+	@Override
+	@Transactional
+	public void delete(int userId) throws ServiceException {
+		User user = userMapper.findById(userId);
+		Assert.notNull(user, "找不到要删除的用户");
+		Assert.isTrue(user.getRole() == UserIdentity.STUDENT, "目标用户只能是学生");
+		try {
+			gitlabApi.deleteUser(userId);
+		} catch (Exception e) {
+			throw new ServiceException("删除GitLab用户时发生异常[" + e.getMessage() + "]");
+		}
+		//todo 删除用户需要连带删除什么
+		userMapper.delete(user);
+	}
+
+	@Override
+	public UserVO getUserById(Integer userid) {
+		return new UserVO(userMapper.findById(userid));
+	}
+
+	@Override
+	public UserVO changeUserInfo(Integer userid, String nickName) throws ServiceException {
+		User one = userMapper.findById(userid);
+		if (one == null) {
+			throw new ServiceException("用户不存在");
+		}
+		one.setNickname(nickName);
+		userMapper.updateById(one);
+		return new UserVO(one);
+	}
+
+	@Override
+	public void changePassword(int userId, ChangePasswordVO form) throws ServiceException {
+		User user = userMapper.findById(userId);
+
+		if (!user.getPassword().equals(DigestUtils.sha256Hex(form.getOrigin()))) {
+			throw new InvalidRequestException("原密码验证失败,如忘记密码请联系管理员重置");
+		}
+		changePassword(user, form.getPassword(), false);
+	}
+
+	private void changePassword(User user, String password, boolean reset) throws ServiceException {
+		try {
+			/**
+			 * todo 修改密码容易导致gitlab与devcloud不一致
+			 * 管理员每次修改密码,会导致下次普通用户登陆gitlab的时候强制重新改密码
+			 * 如果这时候学生在gitlab那边改的密码与这里改的不一样,就产生了密码不一致的结果
+			 * 而且目前官方gitlab client api不支持普通用户通过api改密码
+			 * 见 https://docs.gitlab.com/ce/api/users.html
+			 * 所以这个bug暂时无解,最好是让学生建立用户完毕之后不要乱改密码
+			 */
+			gitlabApi.changePassword(user.getId(), password);
+		} catch (GitLabApiException e) {
+			throw new ServiceException("修改GitLab用户密码时发生异常[" + e.getMessage() + "]");
+		}
+		user.setPassword(DigestUtils.sha256Hex(password));
+		userMapper.updateById(user);
+		if (reset) {
+			mailService.send( "您的新密码为"+password,"密码变更提醒 | SEECODER",user.getEmail());
+		}
+	}
+}

+ 27 - 0
web/src/main/java/seecoder/devcloud/web/service/user/GroupService.java

@@ -0,0 +1,27 @@
+package seecoder.devcloud.web.service.user;
+
+
+
+import seecoder.devcloud.common.exceptions.EntityNotFoundException;
+import seecoder.devcloud.web.vo.user.CustomUserDetails;
+import seecoder.devcloud.web.vo.user.GroupVO;
+import seecoder.devcloud.web.vo.user.InnerGroupVO;
+
+import java.util.List;
+
+/**
+ * @InterfaceName GroupService
+ * @PackageName com.moekr.moocoder.logic.service.impl
+ * @Author sheen
+ * @Date 2019/1/9
+ * @Version 1.0
+ * @Description
+ **/
+public interface GroupService {
+
+    GroupVO createGroup(String name,  Integer userId) throws EntityNotFoundException;
+
+    GroupVO addMember(String code, CustomUserDetails userDetails) throws EntityNotFoundException;
+
+    GroupVO getGroupsById(Integer groupId);
+}

+ 11 - 1
web/src/main/java/seecoder/devcloud/web/service/user/MailService.java

@@ -2,7 +2,17 @@ package seecoder.devcloud.web.service.user;
 
 import org.springframework.web.servlet.ModelAndView;
 
+import java.util.concurrent.CompletableFuture;
+
 public interface MailService {
+
+	@Deprecated
 	void send(String to, String personal, String subject, ModelAndView modelAndView);
-	void send(String text, String subject, String ...to);
+
+	/**
+	 * @param text 正文
+	 * @param subject 标题
+	 * @param to 收件人
+	 */
+	CompletableFuture<Void> send(String text, String subject, String ...to);
 }

+ 28 - 37
web/src/main/java/seecoder/devcloud/web/service/user/UserService.java

@@ -1,37 +1,28 @@
-//package seecoder.devcloud.web.service.user;
-//
-//import com.moekr.moocoder.data.entity.user.User;
-//import com.moekr.moocoder.logic.Serializer.UserSerializer;
-//import com.moekr.moocoder.logic.vo.user.UserVO;
-//import com.moekr.moocoder.util.enums.UserIdentity;
-//import com.moekr.moocoder.util.exceptions.ServiceException;
-//import com.moekr.moocoder.web.dto.form.ChangePasswordForm;
-//import com.moekr.moocoder.web.dto.form.RegisterForm;
-//import com.moekr.moocoder.web.dto.user.UserDTO;
-//import org.springframework.data.domain.Page;
-//
-//public interface UserService {
-//	UserVO create(UserDTO userDTO) throws ServiceException;
-//
-//	User register(String username, String name, String email, String password, String phone, UserIdentity role, Integer pid) throws ServiceException;
-//
-//	Page<UserVO> retrievePage(int page, int limit, String search) throws ServiceException;
-//
-//	UserVO retrieve(int userId) throws ServiceException;
-//
-//	void delete(int userId) throws ServiceException;
-//
-//	UserSerializer register(RegisterForm form) throws ServiceException;
-//
-//	UserSerializer registerTeacher(RegisterForm form) throws ServiceException;
-//
-//	void changePassword(int userId, ChangePasswordForm form) throws ServiceException;
-//
-//	void resetPassword(int userId) throws ServiceException;
-//
-//	UserSerializer getUserById(Integer userid);
-//
-//	UserSerializer changeUserInfo(Integer userid,String nickName, String bio) throws ServiceException;
-//
-//	UserSerializer changeUserPassword(Integer userid,String oldPassword, String newPassword) throws ServiceException;
-//}
+package seecoder.devcloud.web.service.user;
+
+
+import seecoder.devcloud.common.exceptions.ServiceException;
+
+import seecoder.devcloud.web.vo.user.ChangePasswordVO;
+import seecoder.devcloud.web.vo.user.RegisterVO;
+import seecoder.devcloud.web.vo.user.UserVO;
+
+public interface UserService {
+
+	UserVO register(RegisterVO form) throws ServiceException;
+
+	/**
+	 * 暂时没这个需求,先保留
+	 */
+	@Deprecated
+	UserVO registerTeacher(RegisterVO form) throws ServiceException;
+
+	void delete(int userId) throws ServiceException;
+
+	UserVO getUserById(Integer userid);
+
+	UserVO changeUserInfo(Integer userid, String nickName) throws ServiceException;
+
+	void changePassword(int userId, ChangePasswordVO form) throws ServiceException;
+
+}

+ 23 - 0
web/src/main/java/seecoder/devcloud/web/vo/user/ChangePasswordVO.java

@@ -0,0 +1,23 @@
+package seecoder.devcloud.web.vo.user;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import seecoder.devcloud.common.validate.FieldEquals;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+@Data
+@FieldEquals(firstField = "password", secondField = "confirm", message = "两次输入的新密码不一致!")
+@AllArgsConstructor
+public class ChangePasswordVO {
+	@NotNull(message = "请填写原密码!")
+	private String origin;
+	@Pattern(regexp = "[0-9a-zA-Z_]{8,16}", message = "密码只能包含大小写字母、数字和下划线,且必须为8-16位!")
+	@NotNull(message = "请填写新密码!")
+	private String password;
+	/**
+	 * 新密码要输入两遍,做确认
+	 */
+	private String confirm;
+}

+ 85 - 0
web/src/main/java/seecoder/devcloud/web/vo/user/CustomUserDetails.java

@@ -0,0 +1,85 @@
+package seecoder.devcloud.web.vo.user;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import lombok.*;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import seecoder.devcloud.web.po.user.User;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static seecoder.devcloud.web.enums.UserIdentity.STUDENT;
+import static seecoder.devcloud.web.enums.UserIdentity.TEACHER;
+import static seecoder.devcloud.web.infrastructure.security.WebSecurityConstants.*;
+
+
+@Data
+@EqualsAndHashCode
+@ToString
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CustomUserDetails implements UserDetails {
+	private Integer id;
+	private String username;
+	private String password;
+	private Set<GrantedAuthority> authorities;
+
+	CustomUserDetails(User user) {
+		BeanUtils.copyProperties(user, this);
+		switch (user.getRole()) {
+			case STUDENT:
+				this.authorities = Collections.singleton(STUDENT_AUTHORITY);
+				break;
+			case TEACHER:
+				this.authorities = Collections.singleton(TEACHER_AUTHORITY);
+				break;
+			default:
+				throw new UsernameNotFoundException(username);
+		}
+	}
+
+	CustomUserDetails(String username, String password) {
+		this.id = 0;
+		this.username = username;
+		this.password = DigestUtils.sha256Hex(password);
+		this.authorities = Collections.singleton(ADMIN_AUTHORITY);
+	}
+
+	@Override
+	public boolean isAccountNonExpired() {
+		return true;
+	}
+
+	@Override
+	public boolean isAccountNonLocked() {
+		return true;
+	}
+
+	@Override
+	public boolean isCredentialsNonExpired() {
+		return true;
+	}
+
+	@Override
+	public boolean isEnabled() {
+		return true;
+	}
+
+	public boolean isStudent() {
+		return authorities.contains(STUDENT_AUTHORITY);
+	}
+
+	public boolean isTeacher() {
+		return authorities.contains(TEACHER_AUTHORITY);
+	}
+
+	public boolean isAdmin() {
+		return authorities.contains(ADMIN_AUTHORITY);
+	}
+}

+ 24 - 0
web/src/main/java/seecoder/devcloud/web/vo/user/GroupDTO.java

@@ -0,0 +1,24 @@
+package seecoder.devcloud.web.vo.user;
+
+import lombok.Data;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * @ClassName GroupDTO
+ * @PackageName com.moekr.moocoder.web.dto
+ * @Author sheen
+ * @Date 2019/1/9
+ * @Version 1.0
+ * @Description //TODO
+ **/
+@Data
+public class GroupDTO {
+    private Integer id;
+    @NotNull(message = "小组名称不能为空!", groups = {PostMapping.class, PutMapping.class})
+    private String name;
+    @NotNull(message = "课程ID不能为空!", groups = {PostMapping.class, PutMapping.class})
+    private Integer courseId;
+}

+ 37 - 0
web/src/main/java/seecoder/devcloud/web/vo/user/GroupVO.java

@@ -0,0 +1,37 @@
+package seecoder.devcloud.web.vo.user;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import seecoder.devcloud.web.po.user.Group;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @EnumName GroupVO
+ * @PackageName com.moekr.moocoder.logic.Serializer
+ * @Author sheen
+ * @Date 2019/1/9
+ * @Version 1.0
+ * @Description //TODO
+ **/
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class GroupVO {
+    private Integer id;
+    private String name;
+    private String code;
+    private List<UserVO> members;
+
+    public GroupVO(Group group) {
+        this.id=group.getId();
+        this.name=group.getName();
+        this.code=group.getShareLink();
+        this.members=group.getMembers().stream()
+                .map(UserVO::new)
+                .collect(Collectors.toList());
+    }
+
+}

+ 31 - 0
web/src/main/java/seecoder/devcloud/web/vo/user/InnerGroupVO.java

@@ -0,0 +1,31 @@
+package seecoder.devcloud.web.vo.user;
+
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import seecoder.devcloud.web.po.user.Group;
+import seecoder.devcloud.web.po.user.User;
+
+import java.util.List;
+
+/**
+ * 暂定为系统内部给作业部分使用的VO
+ */
+@Data
+@EqualsAndHashCode(exclude = {"members"})
+@ToString(exclude = {"members"})
+public class InnerGroupVO {
+    private Integer id;
+    private Integer namespace;
+    private String name;
+    private List<User> members;
+
+
+    public InnerGroupVO(Group group) {
+        this.id = group.getId();
+        this.namespace = group.getNamespace();
+        this.name = group.getName();
+        this.members = group.getMembers();
+    }
+}

+ 48 - 0
web/src/main/java/seecoder/devcloud/web/vo/user/RegisterVO.java

@@ -0,0 +1,48 @@
+package seecoder.devcloud.web.vo.user;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class RegisterVO {
+	//@Email(message = "用户名也要填邮箱名称!")
+	@NotNull(groups = {PostMapping.class},message = "请填写用户名!")
+	private String username="default";
+	@Email(message = "邮箱格式不正确!")
+//	@NotNull(message = "请填写邮箱!")
+	@JsonIgnore
+	private String email;
+	@Pattern(groups = {PostMapping.class},regexp = "[0-9a-zA-Z_]{8,16}", message = "密码只能包含大小写字母、数字和下划线,且必须为8-16位!")
+	@NotNull(groups = {PostMapping.class},message = "请填写密码!")
+	private String password;
+	private String nickname="nickname";
+
+	public RegisterVO(@Email(message = "邮箱格式不正确!") @NotNull(message = "请填写邮箱!") String username, @Pattern(regexp = "[0-9a-zA-Z_]{8,16}", message = "密码只能包含大小写字母、数字和下划线,且必须为8-16位!") @NotNull(message = "请填写密码!") String password, String nickname) {
+		this.email = username;
+		this.username=buildUsername(username);
+		this.password = password;
+		this.nickname = nickname;
+	}
+	public void buildUsername(){
+		this.username=buildUsername(this.email);
+	}
+	public static String buildUsername(@Email(groups = {PostMapping.class},message = "邮箱格式不正确!") @NotNull(groups = {PostMapping.class},message = "请填写用户名!")String email){
+		return email.replace("@","_").replaceAll("\\.","_");
+	}
+
+	public void setUsername(String username) {
+		this.email = username;
+		this.username=buildUsername(username);
+	}
+}

+ 24 - 0
web/src/main/java/seecoder/devcloud/web/vo/user/UserDTO.java

@@ -0,0 +1,24 @@
+package seecoder.devcloud.web.vo.user;
+
+import lombok.Data;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import seecoder.devcloud.web.enums.UserIdentity;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+@Data
+public class UserDTO {
+	@Pattern(regexp = "[0-9a-zA-Z_]{4,16}", message = "用户名只能包含大小写字母、数字和下划线,且必须为4-16位!", groups = PostMapping.class)
+	@NotNull(message = "请填写用户名!", groups = PostMapping.class)
+	private String username;
+	@Email(message = "邮箱格式不正确!", groups = {PostMapping.class, PutMapping.class})
+	@NotNull(message = "请填写邮箱!", groups = {PostMapping.class, PutMapping.class})
+	private String email;
+//	@NotNull(message = "请选择用户角色!", groups = PostMapping.class)
+	private UserIdentity role;
+	//string | null
+	private String nickname;
+}

+ 32 - 0
web/src/main/java/seecoder/devcloud/web/vo/user/UserVO.java

@@ -0,0 +1,32 @@
+package seecoder.devcloud.web.vo.user;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.beans.BeanUtils;
+import seecoder.devcloud.common.serializer.TimestampLocalDateTimeSerializer;
+import seecoder.devcloud.web.enums.UserIdentity;
+import seecoder.devcloud.web.po.user.User;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserVO {
+    private Integer id;
+    private UserIdentity role;
+    private String username;
+    private String nickname;
+    private String email;
+    private String phone;
+    @JsonProperty("created_at")
+    private Timestamp createdAt;
+
+    public UserVO(User user) {
+        BeanUtils.copyProperties(user,this);
+    }
+}

+ 10 - 0
web/src/main/resources/SQLscripts/group.sql

@@ -0,0 +1,10 @@
+create table if not exists devcloud.`group`
+(
+    id         int                      not null
+        primary key,
+    name       varchar(23) charset utf8 not null,
+    namespace  int                      not null,
+    share_link varchar(63)              not null,
+    type       varchar(15) charset utf8 null
+);
+

+ 11 - 0
web/src/main/resources/SQLscripts/group_member.sql

@@ -0,0 +1,11 @@
+create table if not exists devcloud.group_member
+(
+    group_id int not null,
+    user_id  int not null,
+    constraint group_member_group_id_fk
+        foreign key (group_id) references devcloud.`group` (id)
+            on delete cascade,
+    constraint group_member_user_id_fk
+        foreign key (user_id) references devcloud.user (id)
+            on delete cascade
+);

+ 14 - 0
web/src/main/resources/SQLscripts/user.sql

@@ -0,0 +1,14 @@
+create table if not exists devcloud.user
+(
+    id         int                                 not null
+        primary key,
+    username   varchar(23)                         not null,
+    nickname   varchar(23)                         null,
+    password   char(64)                            not null,
+    phone      varchar(23)                         null,
+    email      varchar(63)                         not null,
+    role       varchar(15) charset utf8            null,
+    created_at timestamp default CURRENT_TIMESTAMP null,
+    namespace  int                                 null,
+    token      varchar(63)                         not null
+);

+ 6 - 6
web/src/main/resources/application-dev.yml

@@ -1,11 +1,11 @@
 spring:
 
-  datasource:
-    # url: jdbc:mysql://192.168.68.76:13306/seecoder_ii?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
-    url: jdbc:mysql://localhost:3306/seecoder?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
-#    url: jdbc:mysql://218.94.159.108:13306/seec_ii?useUnicode=true&characterEncoding=UTF-8&useSSL=false
-    username: root
-    password: xizhiheng921
+#  datasource:
+#    # url: jdbc:mysql://192.168.68.76:13306/seecoder_ii?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
+#    url: jdbc:mysql://localhost:3306/seecoder?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
+##    url: jdbc:mysql://218.94.159.108:13306/seec_ii?useUnicode=true&characterEncoding=UTF-8&useSSL=false
+#    username: root
+#    password: 123456
   jpa:
     hibernate:
       ddl-auto: update

+ 28 - 100
web/src/main/resources/application-wph.yml

@@ -1,123 +1,51 @@
-mybatis:
-  #标注mybatis配置文件的位置
-  config-location: classpath:mybatis-config.xml
-
 spring:
   datasource:
-    url: jdbc:mysql://localhost:3306/seecoder?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false&allowMultiQueries=true
+    url: jdbc:mysql://192.168.99.105:30006/devcloud?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
     username: root
-    password: 123456
-  jpa:
-    hibernate:
-      ddl-auto: update
-      naming:
-        implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
-        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
-    database-platform: org.hibernate.dialect.MySQL57InnoDBDialect
-  security:
-    user:
-      name: root
-      password: NJU67software
+    password: root
+
   mail:
-    host: smtp.163.com
-    username: seecoder@163.com
-    password: seec67NJU
+    host: smtp.qq.com
+    username: 370774330@qq.com
+    password: qkoecnzivukvbibi
+    port: 465
+    protocol: smtp
     properties:
       mail:
         smtp:
-          auth: true
-          socketFactory:
-            class: javax.net.ssl.SSLSocketFactory
-            port: 465
-          starttls:
+          ssl:
             enable: true
-            required: true
-  resources:
-    chain:
-      strategy:
-        content:
-          enabled: true
-          paths:
-            - /moocoder-frontend/**
-            - /js/**
-            - /css/**
-  servlet:
-    multipart:
-      max-file-size: 16MB
-      max-request-size: 16MB
-  redis:
-    host: http://192.168.68.76
-    port: 6379
-    password: NJU67redis
-    timeout: 1000
-    database: 0
-    lettuce:
-      pool:
-        min-idle: 5
-        max-active: 8
-        max-idle: 10
-        max-wait: 1ms
-
-      shutdown-timeout: 100ms
-
-
+          socketFactory.class: javax.net.ssl.SSLSocketFactory
+        debug: true
 server:
   port: 8080
-  tomcat:
-    remoteip:
-      remote-ip-header: X-Forwarded-For
-      protocol-header: X-Forwarded-Proto
-  servlet:
-    session:
-      persistent: true
-      store-dir: ${java.io.tmpdir}/moocoder-session
-      timeout: 1d
-  use-forward-headers: true
-management:
-  endpoints:
-    web:
-      base-path: /api/actuator
-  endpoint:
-    shutdown:
-      enabled: true
+
+mybatis:
+  configuration:
+    map-underscore-to-camel-case: true
 
 seecoder:
-  secret: 1qaz2wsx
-  auth:
-    signingKey: SEEC-1919810-OIDC-114514
-    iss: p.seecoder.cn
-    aud: seec-seec
-    portal: http://p.seecoder.cn/api/interaction
-    redirectUri: http://seec.seecoder.cn/api/auth/receive
+  k8s:
+    api-server: https://192.168.99.105:8443
+    token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjBmSXc4RlRjNUpBYVpGYWc4QzRDY1MzUWJad1JMdFhKcDI1cngzWGtiRlkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXZjbG91ZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZWZhdWx0LXRva2VuLXNicXY4Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRlZmF1bHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiI5NmIxNzU1ZS0zZjE3LTQ1ZTctODFmYi01MTRhODA3NDRhMzQiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGV2Y2xvdWQ6ZGVmYXVsdCJ9.W0Y1lR6LBi1ZjylBx35YeqYuq2u2AqH-ayK4FD-LDfC26E89DanOwSv9XLrRMnnDBZFS_ZCRBumbnDeMiS5vw2x7-QnrDqj1M5u9FB2fIJsz2LyUkOhATZZmTzj_Uhewvvg09aDKh16ttj6ZX27m-KVk_6MP-VfiuvZnCVxDbqRkRjK-zsor4ty1CEATPyKK-tvugAwS0RFNHUL94DamfZAq_t1PSnH5ZtEHq0uEMZxY2c7j6u9d_aD9lphm_ZpCy_C8OCHfanM21CQPDHlcJTW_aiOO6bAp8qdc1egPO5uIamHl6Zm0N3RkcsRp4DGOHrltTV9y_cGZHXSWmRHfxw
+    debugging: true
   gitlab:
-    host: http://gitlab.seec.seecoder.cn
-    git-proxy: http://gitlab.seec.seecoder.cn
+    host: http://gitlab.192.168.99.105.nip.io
     webHookProxy: http://localhost:3000
-    username: root
-    token: x7Hwx-zq8LFkHznGku9a
+    token: wXDeETv2Kmt-JhiApJ9H
+  mail:
+    from: 370774330@qq.com
+
+
   jenkins:
     host: http://192.168.68.79:8082
     username: root
     token: 11d7b9e4c1acac481904c25813e2ec1f0a
-    baseImage: 172.19.51.9:18082/build-base
   docker:
     registry: 192.168.68.79:18082
-  mail:
-    from: seecoder@163.com
-  storage:
-    type: local
-    properties:
-      location: ./problem
-  k8s:
-    api-server: https://192.168.68.76:6443
-    token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi10b2tlbi1zdzlwOCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJhZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImY3MzQ2NzVlLWZmYmItMTFlOS1iYTdjLTIyNmUyZmY2ZTdlYyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTphZG1pbiJ9.GvBP7iNM_WNJN7dPm8ZPszDJeykifhHwBYcF58c4W73iAet1ZcZPrmiurqn4mSHcavUHNR8FqPaafnS3OD4eq5dR-i7frdIBt7yiME7AIGA4pDKWNJ3NykhyfG75omCvtQ2Gp-UvFIdf_4peAilxbwDR2NtWpky4sJQOTukZIYzN9fCeYV8My85XDBl7ze20QBsEKfPxTGZkqmytX-4VmjKkQH9ob7YB90NR2ug5VK8CA16_2ZPjf8Fx4QsdTbkyZKdFo7zIrIID0fa2lpOc2TpCKWeYCl335-ULllpxds6kiVwkRXYP-5zkIOfpI_0SshK7kbBjd238OLM3KFDf6A
-    ingress-host: seecii.cn
-    public-namespace: dev
-    public-mysql-url: jdbc:mysql://seecii-mysql.dev.svc.cluster.local:3306
-    public-mysql-pod: seecii-mysql-579dc7c857-8m2fd
-    public-mysql-username: root
-    public-mysql-password: NJU67software
-    debugging: true
+
+
+
   registry:
     host: 192.168.68.79:18082
     username: admin

+ 1 - 1
web/src/test/java/seecoder/devcloud/web/ArchitectWebApplicationTests.java → web/src/test/java/seecoder/devcloud/web/DevcloudWebApplicationTests.java

@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
 
 @SpringBootTest
-class devcloudWebApplicationTests {
+class DevcloudWebApplicationTests {
 
 
 }

+ 49 - 0
web/src/test/java/seecoder/devcloud/web/service/user/MailServiceTest.java

@@ -0,0 +1,49 @@
+package seecoder.devcloud.web.service.user;
+
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+import org.junit.runner.RunWith;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.test.context.junit4.SpringRunner;
+import seecoder.devcloud.common.exceptions.ServiceException;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+import java.util.concurrent.CompletableFuture;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/3/3
+ * @description:
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@ComponentScan({"seecoder.devcloud.api","seecoder.devcloud.common","seecoder.devcloud.web"})
+@MapperScan(basePackages = {"seecoder.devcloud.web.dao"})
+class MailServiceTest {
+
+    @Autowired
+    MailService mailService;
+
+    @Autowired
+    JavaMailSender javaMailSender;
+    @Test
+    void send() {
+        try {
+            CompletableFuture<Void> result = mailService.send( "你的课程作业","按时","2717718789@qq.com");
+            CompletableFuture.allOf(result).join();
+        } catch (Exception e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+    }
+}

+ 113 - 0
web/src/test/java/seecoder/devcloud/web/service/user/UserServiceTest.java

@@ -0,0 +1,113 @@
+package seecoder.devcloud.web.service.user;
+
+import org.gitlab4j.api.GitLabApi;
+import org.gitlab4j.api.models.AbstractUser;
+import org.junit.Assert;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.runner.RunWith;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.test.context.junit4.SpringRunner;
+import seecoder.devcloud.api.ApplicationProperties;
+import seecoder.devcloud.common.exceptions.ServiceException;
+import seecoder.devcloud.web.vo.user.ChangePasswordVO;
+import seecoder.devcloud.web.vo.user.RegisterVO;
+import seecoder.devcloud.web.vo.user.UserVO;
+
+import java.util.stream.Collectors;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/3/2
+ * @description: 与gitlab交互,没有办法配事务,只能手动测试。mvn test的时候请排除这个类
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@ComponentScan({"seecoder.devcloud.api","seecoder.devcloud.common","seecoder.devcloud.web"})
+@MapperScan(basePackages = {"seecoder.devcloud.web.dao"})
+class UserServiceTest {
+
+    @Autowired
+    UserService userService;
+
+    @Autowired
+    ApplicationProperties properties;
+
+    /**
+     * 注意 GitlabApi 和 GitLabApi 区别
+     * GitlabApi 是我们对 GitLabApi 的一个小封装
+     * 这里作辅助测试用
+     */
+    GitLabApi gitLabApi;
+
+    @BeforeEach
+    void setUp() {
+        gitLabApi = new GitLabApi(properties.getGitlab().getHost(), properties.getGitlab().getToken());
+    }
+
+    @Test
+    void register() {
+        RegisterVO registerVO = RegisterVO.builder()
+                .username("wphwphtest")
+                .password("wphwphtest")
+                .email("370774330@qq.com")
+                .nickname("kennyday")
+                .build();
+        try {
+            System.out.println(userService.register(registerVO));
+        } catch (ServiceException e){
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+    }
+
+    @Test
+    void delete() {
+        try {
+            //userService.delete(33);
+            gitLabApi.getUserApi().deleteUser(35);
+            System.out.println(gitLabApi.getUserApi().getActiveUsers().stream().map(AbstractUser::getId).collect(Collectors.toList()));
+        } catch (Exception e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+
+    }
+
+    @Test
+    void getUserById() {
+        try {
+            UserVO u = userService.getUserById(25);
+            Assert.assertNotNull(u);
+            System.out.println(u);
+        } catch (Exception e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+    }
+
+    @Test
+    void changeUserInfo() {
+        try {
+            userService.changeUserInfo(31,"gogoing");
+        } catch (ServiceException e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+    }
+
+
+    @Test
+    void changeUserPassword() {
+        try {
+
+            userService.changePassword(25, new ChangePasswordVO("1234qwer","1234qwer1","1234qwer1"));
+        } catch (ServiceException e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+    }
+}