sunyj 8 years ago
commit
d517837336
73 changed files with 6481 additions and 0 deletions
  1. 21 0
      kanban-auth/pom.xml
  2. 19 0
      kanban-auth/src/main/java/com/uas/kanban/controller/UserController.java
  3. 17 0
      kanban-auth/src/main/java/com/uas/kanban/dao/UserDao.java
  4. 50 0
      kanban-auth/src/main/java/com/uas/kanban/model/User.java
  5. 17 0
      kanban-auth/src/main/java/com/uas/kanban/service/impl/UserServiceImpl.java
  6. 48 0
      kanban-common/pom.xml
  7. 22 0
      kanban-common/src/main/java/com/uas/kanban/annotation/FieldProperty.java
  8. 19 0
      kanban-common/src/main/java/com/uas/kanban/annotation/Log.java
  9. 24 0
      kanban-common/src/main/java/com/uas/kanban/annotation/NotEmpty.java
  10. 108 0
      kanban-common/src/main/java/com/uas/kanban/aop/ArgumentsCheckAspect.java
  11. 70 0
      kanban-common/src/main/java/com/uas/kanban/aop/ExceptionHandlerAdvice.java
  12. 82 0
      kanban-common/src/main/java/com/uas/kanban/aop/WebLogAspect.java
  13. 125 0
      kanban-common/src/main/java/com/uas/kanban/base/BaseController.java
  14. 574 0
      kanban-common/src/main/java/com/uas/kanban/base/BaseDao.java
  15. 80 0
      kanban-common/src/main/java/com/uas/kanban/base/BaseEntity.java
  16. 106 0
      kanban-common/src/main/java/com/uas/kanban/base/BaseService.java
  17. 47 0
      kanban-common/src/main/java/com/uas/kanban/base/Coded.java
  18. 136 0
      kanban-common/src/main/java/com/uas/kanban/base/PageResult.java
  19. 33 0
      kanban-common/src/main/java/com/uas/kanban/exception/OperationException.java
  20. 46 0
      kanban-common/src/main/java/com/uas/kanban/util/ArrayUtils.java
  21. 21 0
      kanban-common/src/main/java/com/uas/kanban/util/CollectionUtils.java
  22. 73 0
      kanban-common/src/main/java/com/uas/kanban/util/ContextUtils.java
  23. 44 0
      kanban-common/src/main/java/com/uas/kanban/util/ExceptionUtils.java
  24. 56 0
      kanban-common/src/main/java/com/uas/kanban/util/IpHelper.java
  25. 36 0
      kanban-common/src/main/java/com/uas/kanban/util/NumberGenerator.java
  26. 53 0
      kanban-common/src/main/java/com/uas/kanban/util/ObjectUtils.java
  27. 31 0
      kanban-common/src/main/java/com/uas/kanban/util/StringUtils.java
  28. 178 0
      kanban-console/pom.xml
  29. 37 0
      kanban-console/src/main/java/com/uas/kanban/Application.java
  30. 58 0
      kanban-console/src/main/java/com/uas/kanban/MongoConfiguration.java
  31. 74 0
      kanban-console/src/main/java/com/uas/kanban/WebAppConfiguration.java
  32. 36 0
      kanban-console/src/main/java/com/uas/kanban/controller/DataSourceController.java
  33. 19 0
      kanban-console/src/main/java/com/uas/kanban/controller/KanbanController.java
  34. 72 0
      kanban-console/src/main/java/com/uas/kanban/controller/KanbanInstanceController.java
  35. 90 0
      kanban-console/src/main/java/com/uas/kanban/controller/TemplateController.java
  36. 57 0
      kanban-console/src/main/java/com/uas/kanban/controller/UploadController.java
  37. 17 0
      kanban-console/src/main/java/com/uas/kanban/dao/DataSourceDao.java
  38. 17 0
      kanban-console/src/main/java/com/uas/kanban/dao/KanbanDao.java
  39. 17 0
      kanban-console/src/main/java/com/uas/kanban/dao/KanbanInstanceDao.java
  40. 17 0
      kanban-console/src/main/java/com/uas/kanban/dao/TemplateDao.java
  41. 92 0
      kanban-console/src/main/java/com/uas/kanban/model/DataSource.java
  42. 52 0
      kanban-console/src/main/java/com/uas/kanban/model/Kanban.java
  43. 113 0
      kanban-console/src/main/java/com/uas/kanban/model/KanbanInstance.java
  44. 83 0
      kanban-console/src/main/java/com/uas/kanban/model/Template.java
  45. 177 0
      kanban-console/src/main/java/com/uas/kanban/model/TemplateParameter.java
  46. 24 0
      kanban-console/src/main/java/com/uas/kanban/service/DataSourceService.java
  47. 54 0
      kanban-console/src/main/java/com/uas/kanban/service/KanbanInstanceService.java
  48. 55 0
      kanban-console/src/main/java/com/uas/kanban/service/TemplateService.java
  49. 35 0
      kanban-console/src/main/java/com/uas/kanban/service/impl/DataSourceServiceImpl.java
  50. 155 0
      kanban-console/src/main/java/com/uas/kanban/service/impl/KanbanInstanceServiceImpl.java
  51. 66 0
      kanban-console/src/main/java/com/uas/kanban/service/impl/KanbanServiceImpl.java
  52. 160 0
      kanban-console/src/main/java/com/uas/kanban/service/impl/TemplateServiceImpl.java
  53. 23 0
      kanban-console/src/main/resources/bootstrap.yml
  54. 44 0
      kanban-console/src/main/resources/logback.xml
  55. 77 0
      kanban-console/src/main/webapp/WEB-INF/views/console.html
  56. 27 0
      kanban-console/src/main/webapp/WEB-INF/views/fileUpload.html
  57. 10 0
      kanban-console/src/main/webapp/WEB-INF/views/index.html
  58. 17 0
      kanban-console/src/main/webapp/resources/css/console.css
  59. 15 0
      kanban-console/src/main/webapp/resources/css/fileUpload.css
  60. 3 0
      kanban-console/src/main/webapp/resources/js/console/app.js
  61. 62 0
      kanban-console/src/main/webapp/resources/js/upload/app.js
  62. 88 0
      kanban-console/src/main/webapp/resources/js/util/utils.js
  63. 2086 0
      kanban-console/src/main/webapp/resources/lib/fontawesome/css/font-awesome.css
  64. 3 0
      kanban-console/src/main/webapp/resources/lib/fontawesome/css/font-awesome.min.css
  65. BIN
      kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/FontAwesome.otf
  66. BIN
      kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.eot
  67. 196 0
      kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.svg
  68. BIN
      kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.ttf
  69. BIN
      kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.woff
  70. BIN
      kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.woff2
  71. 1 0
      kanban-console/src/main/webapp/resources/lib/jquery/jquery.min.js
  72. 1 0
      kanban-console/src/main/webapp/resources/lib/spin/spin.min.js
  73. 115 0
      pom.xml

+ 21 - 0
kanban-auth/pom.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<project
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>com.uas.kanban</groupId>
+		<artifactId>kanban-parent</artifactId>
+		<version>0.0.1-SNAPSHOT</version>
+	</parent>
+	<artifactId>kanban-auth</artifactId>
+	<packaging>jar</packaging>
+
+	<dependencies>
+		<dependency>
+			<groupId>com.uas.kanban</groupId>
+			<artifactId>kanban-common</artifactId>
+			<version>${kanban.common.version}</version>
+		</dependency>
+	</dependencies>
+</project>

+ 19 - 0
kanban-auth/src/main/java/com/uas/kanban/controller/UserController.java

@@ -0,0 +1,19 @@
+package com.uas.kanban.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.uas.kanban.base.BaseController;
+import com.uas.kanban.model.User;
+
+/**
+ * 用户
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:42:10
+ */
+@Controller
+@RequestMapping("/user")
+public class UserController extends BaseController<User> {
+
+}

+ 17 - 0
kanban-auth/src/main/java/com/uas/kanban/dao/UserDao.java

@@ -0,0 +1,17 @@
+package com.uas.kanban.dao;
+
+import org.springframework.stereotype.Component;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.User;
+
+/**
+ * 用户
+ * 
+ * @author sunyj
+ * @since 2017年8月29日 上午10:08:50
+ */
+@Component
+public class UserDao extends BaseDao<User> {
+
+}

+ 50 - 0
kanban-auth/src/main/java/com/uas/kanban/model/User.java

@@ -0,0 +1,50 @@
+package com.uas.kanban.model;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+
+/**
+ * 用户
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午5:19:15
+ */
+public class User extends BaseEntity {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 名称
+	 */
+	@FieldProperty(nullable = false)
+	private String name;
+
+	/**
+	 * 密码
+	 */
+	@FieldProperty(nullable = false)
+	private String password;
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getPassword() {
+		return password;
+	}
+
+	public void setPassword(String password) {
+		this.password = password;
+	}
+
+	@Override
+	public String toString() {
+		return "User [name=" + name + ", password=" + password + ", id=" + id + ", code=" + code + ", createTime="
+				+ createTime + ", lastModified=" + lastModified + ", version=" + version + "]";
+	}
+
+}

+ 17 - 0
kanban-auth/src/main/java/com/uas/kanban/service/impl/UserServiceImpl.java

@@ -0,0 +1,17 @@
+package com.uas.kanban.service.impl;
+
+import org.springframework.stereotype.Service;
+
+import com.uas.kanban.base.BaseService;
+import com.uas.kanban.model.User;
+
+/**
+ * 用户
+ * 
+ * @author sunyj
+ * @since 2017年9月2日 下午8:47:20
+ */
+@Service
+public class UserServiceImpl extends BaseService<User> {
+
+}

+ 48 - 0
kanban-common/pom.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<project
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>com.uas.kanban</groupId>
+		<artifactId>kanban-parent</artifactId>
+		<version>0.0.1-SNAPSHOT</version>
+	</parent>
+	<artifactId>kanban-common</artifactId>
+	<packaging>jar</packaging>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-beans</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-context</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-web</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.aspectj</groupId>
+			<artifactId>aspectjweaver</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>javax.servlet-api</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>log4j-over-slf4j</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.mongodb.morphia</groupId>
+			<artifactId>morphia</artifactId>
+		</dependency>
+	</dependencies>
+</project>

+ 22 - 0
kanban-common/src/main/java/com/uas/kanban/annotation/FieldProperty.java

@@ -0,0 +1,22 @@
+package com.uas.kanban.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 字段的属性,用于mongo文档的字段
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午3:36:24
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface FieldProperty {
+
+	/**
+	 * 是否可为空
+	 */
+	boolean nullable() default true;
+}

+ 19 - 0
kanban-common/src/main/java/com/uas/kanban/annotation/Log.java

@@ -0,0 +1,19 @@
+package com.uas.kanban.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 记录日志
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午3:36:24
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Log {
+
+	// TODO
+}

+ 24 - 0
kanban-common/src/main/java/com/uas/kanban/annotation/NotEmpty.java

@@ -0,0 +1,24 @@
+package com.uas.kanban.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 自定义注解,监测方法的参数不为空
+ * 
+ * @author sunyj
+ * @since 2017年1月3日 上午11:31:01
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface NotEmpty {
+
+	/**
+	 * 参数名称
+	 * 
+	 * @return
+	 */
+	String value() default "";
+}

+ 108 - 0
kanban-common/src/main/java/com/uas/kanban/aop/ArgumentsCheckAspect.java

@@ -0,0 +1,108 @@
+package com.uas.kanban.aop;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.util.ObjectUtils;
+import com.uas.kanban.util.StringUtils;
+
+/**
+ * 利用AOP监测方法的参数(使用自定义注解NotEmpty)不为空
+ * 
+ * @author sunyj
+ * @since 2017年1月3日 上午11:33:27
+ */
+@Aspect
+@Component
+public class ArgumentsCheckAspect {
+
+	@Pointcut("execution(* com.uas..*.*(..))")
+	public void checkArguments() {
+
+	}
+
+	@Before("checkArguments()")
+	public void before(JoinPoint joinPoint) {
+		Method method = getMethod(joinPoint);
+
+		// 参数值
+		Object[] args = joinPoint.getArgs();
+		if (args == null || args.length < 1) {
+			return;
+		}
+		Class<?>[] parameterTypes = method.getParameterTypes();
+		// 获取所有参数注解
+		Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+		// 遍历所有参数的注解
+		for (int i = 0; i < parameterAnnotations.length; i++) {
+			Object arg = args[i];
+			Annotation[] annotations = parameterAnnotations[i];
+			Class<?> parameterType = parameterTypes[i];
+			for (int j = 0; annotations != null && j < annotations.length; j++) {
+				Annotation annotation = annotations[j];
+				// 如果有注解为自定义的NotEmpty注解,则对参数值进行非空判断
+				if (annotation.annotationType() == NotEmpty.class) {
+					NotEmpty notEmptyAnnotation = (NotEmpty) annotation;
+					// 对字符串、Collection、Map进行判断
+					if (ObjectUtils.isEmpty(arg)) {
+						throw new IllegalArgumentException(joinPoint.getTarget().getClass().getSimpleName() + "."
+								+ method.getName() + ": " + "参数为空:" + notEmptyAnnotation.value() + "("
+								+ parameterType.getSimpleName() + ")");
+					}
+				}
+			}
+		}
+
+	}
+
+	/**
+	 * 获取切点所拦截的方法
+	 * 
+	 * @param joinPoint
+	 * @return
+	 */
+	private Method getMethod(JoinPoint joinPoint) {
+		Method method = null;
+		Object target = joinPoint.getTarget();
+		Class<?> clazz = target.getClass();
+		Signature signature = joinPoint.getSignature();
+		// 方法全名
+		String methodString = signature.toLongString();
+		// 取出参数类型
+		String substring = methodString.substring(methodString.indexOf("(") + 1, methodString.lastIndexOf(")"));
+		try {
+			if (!StringUtils.isEmpty(substring)) {
+				// Class.forName()不可用于java.lang.Long[]等类似的情况
+				if (!substring.contains("[]")) {
+					String[] strs = substring.split(",");
+					Class<?>[] clazzes = new Class[strs.length];
+					for (int i = 0; i < strs.length; i++) {
+						clazzes[i] = Class.forName(strs[i]);
+					}
+					// 根据签名获取方法
+					method = clazz.getMethod(signature.getName(), clazzes);
+				} else {
+					// java.lang.Long[]等类似的情况下,通过比较方法名称,获取方法(可能会获取到错误的方法)
+					for (Method m : clazz.getDeclaredMethods()) {
+						if (m.getName().equals(signature.getName())) {
+							method = m;
+						}
+					}
+				}
+			} else {
+				method = clazz.getMethod(signature.getName());
+			}
+		} catch (NoSuchMethodException | SecurityException | ClassNotFoundException e) {
+			throw new IllegalStateException("参数检查时出错", e);
+		}
+		return method;
+	}
+}

+ 70 - 0
kanban-common/src/main/java/com/uas/kanban/aop/ExceptionHandlerAdvice.java

@@ -0,0 +1,70 @@
+/*CopyRright (c)2014: <www.usoftchina.com>
+ */
+package com.uas.kanban.aop;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.kanban.util.ExceptionUtils;
+
+/**
+ * 基于Application的异常处理,以AOP的形式注册到SpringMVC的处理链
+ * 
+ * @author sunyj
+ * @since 2017年7月13日 上午10:28:37
+ */
+@ControllerAdvice
+public class ExceptionHandlerAdvice {
+
+	private static Logger logger = LoggerFactory.getLogger(ExceptionHandlerAdvice.class);
+
+	/**
+	 * 处理已捕获异常,明确传达给客户端错误码、错误信息
+	 * 
+	 * @param e
+	 * @return
+	 */
+	@ExceptionHandler(Throwable.class)
+	public ResponseEntity<ModelMap> handleError(Throwable e) {
+		logger.error("", e);
+		ModelMap map = new ModelMap();
+		map.put("success", false);
+		map.put("message", e.getMessage());
+		map.put("detailedMessage", ExceptionUtils.getDetailedMessage(e));
+		HttpHeaders headers = new HttpHeaders();
+		headers.add("Content-Type", "application/json; charset=utf-8");
+		return new ResponseEntity<ModelMap>(map, headers, HttpStatus.INTERNAL_SERVER_ERROR);
+	}
+
+	/**
+	 * 处理错误
+	 * 
+	 * @param response
+	 * @param status
+	 * @param message
+	 * @param ip
+	 * @throws IOException
+	 */
+	public static void handleError(HttpServletResponse response, Integer status, String message, String ip)
+			throws IOException {
+		logger.error(message + ":" + ip);
+		response.setHeader("Content-type", "text/html;charset=UTF-8");
+		response.setStatus(status);
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.put("success", false);
+		jsonObject.put("message", message);
+		response.getWriter().println(jsonObject);
+	}
+
+}

+ 82 - 0
kanban-common/src/main/java/com/uas/kanban/aop/WebLogAspect.java

@@ -0,0 +1,82 @@
+package com.uas.kanban.aop;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.stereotype.Component;
+
+import com.uas.kanban.util.IpHelper;
+import com.uas.kanban.util.StringUtils;
+
+/**
+ * 利用AOP处理Web请求日志
+ * 
+ * @author sunyj
+ * @since 2016年12月20日 下午5:46:56
+ */
+@Aspect
+@Component
+@EnableAspectJAutoProxy
+public class WebLogAspect {
+
+	private Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
+
+	@Pointcut("execution(* com.uas..controller.*.*(..))")
+	public void log() {
+
+	}
+
+	@Before("log()")
+	public void before(JoinPoint joinPoint) {
+		StringBuilder stringBuilder = new StringBuilder("request... ");
+
+		// 类名
+		Object target = joinPoint.getTarget();
+		Class<?> clazz = target.getClass();
+		stringBuilder.append(clazz.getSimpleName());
+
+		// 方法名
+		Signature signature = joinPoint.getSignature();
+		String methodName = signature.getName();
+		stringBuilder.append(".").append(methodName);
+
+		// 方法全名
+		String methodString = signature.toString();
+		// 取出参数类型
+		String substring = methodString.substring(methodString.indexOf("(") + 1, methodString.lastIndexOf(")"));
+		if (!StringUtils.isEmpty(substring)) {
+			stringBuilder.append(": ");
+			String[] strs = substring.split(",");
+			Object[] args = joinPoint.getArgs();
+			for (int i = 0; i < strs.length; i++) {
+				if (strs[i].equals("HttpServletRequest")) {
+					// 如果参数是HttpServletRequest,打印ip
+					stringBuilder.append("ip=").append(IpHelper.getIp((HttpServletRequest) args[i]));
+				} else if (strs[i].equals("HttpServletResponse")) {
+					// 如果参数是HttpServletResponse,不做处理
+					continue;
+				} else {
+					// 参数类型和参数值
+					stringBuilder.append(strs[i]).append("=").append(args[i]);
+				}
+				if (i != strs.length - 1) {
+					stringBuilder.append(",");
+				}
+			}
+		}
+		logger.info(stringBuilder.toString());
+	}
+
+	@AfterReturning(value = "log()", returning = "result")
+	public void afterReturning(JoinPoint joinPoint, Object result) {
+		logger.info("result... " + result + "\n");
+	}
+}

+ 125 - 0
kanban-common/src/main/java/com/uas/kanban/base/BaseController.java

@@ -0,0 +1,125 @@
+package com.uas.kanban.base;
+
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.exception.OperationException;
+
+/**
+ * 基本接口
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:41:47
+ * @param <T>
+ */
+public abstract class BaseController<T extends BaseEntity> {
+
+	@Autowired
+	private BaseService<T> baseService;
+
+	/**
+	 * 保存数据
+	 * 
+	 * @param json
+	 *            json格式的数据
+	 * @param request
+	 * @return 保存的数据
+	 */
+	@RequestMapping("/save")
+	@ResponseBody
+	public T save(@NotEmpty("json") String json, HttpServletRequest request) {
+		return baseService.save(json);
+	}
+
+	/**
+	 * 删除所有数据
+	 * 
+	 * @param request
+	 * @return 删除的数据条数
+	 */
+	@RequestMapping("/delete/all")
+	@ResponseBody
+	public int deleteAll(HttpServletRequest request) {
+		return baseService.deleteAll();
+	}
+
+	/**
+	 * Delete the given entity (by code)
+	 * 
+	 * @param code
+	 *            the code to delete
+	 * @param request
+	 * @return results of the delete
+	 */
+	@RequestMapping("/delete/{code}")
+	@ResponseBody
+	public int deleteOne(@PathVariable("code") String code, HttpServletRequest request) {
+		return baseService.deleteOne(code);
+	}
+
+	/**
+	 * 根据id更新数据
+	 * 
+	 * @param json
+	 *            json格式的数据
+	 * @param request
+	 * @return 更新的数据条数
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 */
+	@RequestMapping("/update")
+	@ResponseBody
+	public int update(@NotEmpty("json") String json, HttpServletRequest request)
+			throws IllegalArgumentException, OperationException {
+		return baseService.update(json);
+	}
+
+	/**
+	 * 获取所有数据
+	 * 
+	 * @param request
+	 * @return 所有数据
+	 */
+	@RequestMapping("/get/all")
+	@ResponseBody
+	public List<T> getAll(HttpServletRequest request) {
+		return baseService.getAll();
+	}
+
+	/**
+	 * 根据code查询数据
+	 * 
+	 * @param code
+	 *            code
+	 * @param request
+	 * @return 数据
+	 */
+	@RequestMapping("/get/{code}")
+	@ResponseBody
+	public T getOne(@PathVariable("code") String code, HttpServletRequest request) {
+		return baseService.getOne(code);
+	}
+
+	/**
+	 * 分页获取所有数据
+	 * 
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页面大小
+	 * @param request
+	 * @return 获取的数据
+	 */
+	@RequestMapping("/get")
+	@ResponseBody
+	public PageResult<T> get(Integer page, Integer size, HttpServletRequest request) {
+		return baseService.get(page, size);
+	}
+}

+ 574 - 0
kanban-common/src/main/java/com/uas/kanban/base/BaseDao.java

@@ -0,0 +1,574 @@
+package com.uas.kanban.base;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.bson.types.ObjectId;
+import org.mongodb.morphia.Datastore;
+import org.mongodb.morphia.Key;
+import org.mongodb.morphia.annotations.Embedded;
+import org.mongodb.morphia.annotations.Reference;
+import org.mongodb.morphia.query.FindOptions;
+import org.mongodb.morphia.query.MorphiaIterator;
+import org.mongodb.morphia.query.Query;
+import org.mongodb.morphia.query.UpdateOperations;
+import org.mongodb.morphia.query.UpdateResults;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.alibaba.fastjson.JSONObject;
+import com.mongodb.DBCollection;
+import com.mongodb.WriteResult;
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.util.ArrayUtils;
+import com.uas.kanban.util.CollectionUtils;
+import com.uas.kanban.util.ObjectUtils;
+import com.uas.kanban.util.StringUtils;
+
+/**
+ * 基本Dao操作
+ * 
+ * @author sunyj
+ * @since 2017年8月29日 上午10:06:32
+ */
+public class BaseDao<T extends BaseEntity> {
+
+	@Autowired
+	protected Datastore datastore;
+
+	private Class<T> entityClass;
+
+	@SuppressWarnings("unchecked")
+	protected BaseDao() throws IllegalStateException {
+		Class<?> clazz = this.getClass();
+		Type genericSuperclass = clazz.getGenericSuperclass();
+		if (!(genericSuperclass instanceof ParameterizedType)) {
+			// 如果子类继承时未指定<T>的类型
+			throw new IllegalStateException("泛型类型解析错误");
+		}
+		Type[] actualTypeArguments = ((ParameterizedType) genericSuperclass).getActualTypeArguments();
+		if (ArrayUtils.isEmpty(actualTypeArguments)) {
+			throw new IllegalStateException("泛型类型解析错误");
+		}
+		entityClass = (Class<T>) actualTypeArguments[0];
+	}
+
+	/**
+	 * Returns a new query bound to the collection (a specific
+	 * {@link DBCollection})
+	 * 
+	 * @return the query
+	 */
+	public Query<T> createQuery() {
+		return datastore.createQuery(entityClass);
+	}
+
+	/**
+	 * Returns a new query bound to the collection (a specific
+	 * {@link DBCollection})
+	 * 
+	 * @param code
+	 *            the code to query
+	 * @return the query
+	 */
+	public Query<T> createQuery(@NotEmpty("code") String code) {
+		return createQuery().field("code").equal(code);
+	}
+
+	/**
+	 * The builder for all update operations
+	 * 
+	 * @return the new UpdateOperations instance
+	 */
+	public UpdateOperations<T> createUpdateOperations() {
+		return datastore.createUpdateOperations(entityClass);
+	}
+
+	/**
+	 * The builder for all update operations (by the entity)
+	 * 
+	 * @param t
+	 *            the entity to update
+	 * @return the new UpdateOperations instance
+	 */
+	public UpdateOperations<T> createUpdateOperations(@NotEmpty("t") T t) {
+		return createUpdateOperations(t, null);
+	}
+
+	/**
+	 * The builder for all update operations (by the entity)
+	 * 
+	 * @param t
+	 *            the entity to update
+	 * @param ignoreFields
+	 *            不更新的字段
+	 * @return the new UpdateOperations instance
+	 */
+	public UpdateOperations<T> createUpdateOperations(@NotEmpty("t") T t, Set<String> ignoreFields) {
+		UpdateOperations<T> operations = createUpdateOperations();
+		Field[] declaredFields = entityClass.getDeclaredFields();
+		// 通过反射遍历对象的成员变量,自动构造UpdateOperations
+		for (Field field : declaredFields) {
+			String name = field.getName();
+			// 不更新 ignoreFields 中指定的字段
+			if (!CollectionUtils.isEmpty(ignoreFields) && ignoreFields.contains(name)) {
+				continue;
+			}
+
+			Object value = null;
+			int modifiers = field.getModifiers();
+			// 不处理static或final修饰的变量(只处理普通成员变量)
+			if (Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) {
+				continue;
+			}
+			try {
+				if (!field.isAccessible()) {
+					field.setAccessible(true);
+					value = field.get(t);
+					field.setAccessible(false);
+				} else {
+					value = field.get(t);
+				}
+			} catch (IllegalArgumentException | IllegalAccessException e) {
+				throw new IllegalStateException("通过反射自动构造 UpdateOperations 时出错", e);
+			}
+			// 如果值为空,则移除文档中的该字段
+			if (ObjectUtils.isEmpty(value)) {
+				operations.unset(name);
+			} else {
+				operations.set(name, value);
+			}
+		}
+		return operations;
+	}
+
+	/**
+	 * Saves an entity (Object) and updates the @Id field
+	 * 
+	 * @param t
+	 *            the entity to save
+	 * @return the entity saved
+	 * @throws IllegalArgumentException
+	 * @throws IllegalStateException
+	 */
+	public T save(@NotEmpty("t") T t) throws IllegalArgumentException, IllegalStateException {
+		if (!StringUtils.isEmpty(t.getCode())) {
+			throw new IllegalArgumentException("不能指定 code:" + t);
+		}
+		checkField(t);
+		t.init();
+		Key<T> key = datastore.save(t);
+		Object id = key.getId();
+		if (id == null || !(id instanceof ObjectId)) {
+			throw new IllegalStateException("保存数据出错:id 不存在或者并非 ObjectId 类型");
+		}
+		return t;
+	}
+
+	/**
+	 * 对对象的字段进行检查,包括是否为空等
+	 * 
+	 * @param <K>
+	 *            要检查的对象的类型,可与 {@link T} 不同
+	 * 
+	 * @param k
+	 *            要检查的对象
+	 * @throws IllegalArgumentException
+	 * @throws IllegalStateException
+	 */
+	public <K> void checkField(@NotEmpty("k") K k) throws IllegalArgumentException, IllegalStateException {
+		Class<? extends Object> clazz = k.getClass();
+		Field[] declaredFields = clazz.getDeclaredFields();
+		for (Field field : declaredFields) {
+			int modifiers = field.getModifiers();
+			// 不处理static修饰的变量
+			if (Modifier.isStatic(modifiers)) {
+				continue;
+			}
+
+			Embedded embedded = field.getAnnotation(Embedded.class);
+			Reference reference = field.getAnnotation(Reference.class);
+			FieldProperty fieldProperty = field.getAnnotation(FieldProperty.class);
+			// 只有使用 {@link FieldProperty} 指定不可为空,或者是嵌入或引用对象,才处理
+			if ((fieldProperty == null || fieldProperty.nullable()) && embedded == null && reference == null) {
+				continue;
+			}
+			Object value = null;
+			try {
+				if (!field.isAccessible()) {
+					field.setAccessible(true);
+					value = field.get(k);
+					field.setAccessible(false);
+				} else {
+					value = field.get(k);
+				}
+			} catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
+				throw new IllegalStateException("通过反射检查字段是否为空时出错", e);
+			}
+
+			// 如果使用 {@link FieldProperty} 指定不可为空,但是值为空,则抛出异常
+			if (fieldProperty != null && !fieldProperty.nullable() && ObjectUtils.isEmpty(value)) {
+				throw new IllegalArgumentException("字段为空:" + clazz.getName() + "." + field.getName() + " = " + value);
+			}
+			// 如果是嵌入或引用对象,并且不为空,则递归检测
+			if ((embedded != null || reference != null) && !ObjectUtils.isEmpty(value)) {
+				// 如果是Collection
+				if (value instanceof Collection) {
+					Collection<?> colletion = (Collection<?>) value;
+					for (Object obj : colletion) {
+						checkField(obj);
+					}
+				} else if (value instanceof Map) {
+					Map<?, ?> map = (Map<?, ?>) value;
+					for (Object key : map.keySet()) {
+						checkField(key);
+					}
+					for (Object v : map.values()) {
+						checkField(v);
+					}
+				} else {
+					checkField(value);
+				}
+			}
+		}
+	}
+
+	/**
+	 * 删除所有数据
+	 * 
+	 * @return results of the delete
+	 */
+	public int deleteAll() {
+		Query<T> query = createQuery();
+		return delete(query);
+	}
+
+	/**
+	 * Delete the given entity (by code)
+	 * 
+	 * @param code
+	 *            the code to delete
+	 * @return results of the delete
+	 */
+	public int deleteOne(@NotEmpty("code") String code) {
+		Query<T> query = createQuery(code);
+		return delete(query);
+	}
+
+	/**
+	 * 根据指定的条件删除数据
+	 * 
+	 * @param filters
+	 *            过滤条件,key-value的形式
+	 * @return results of the delete
+	 */
+	public int delete(@NotEmpty("filters") Map<String, Object> filters) {
+		Query<T> query = createQuery();
+		filter(query, filters);
+		return delete(query);
+	}
+
+	/**
+	 * Deletes entities based on the query
+	 * 
+	 * @param query
+	 *            the query used to match the documents to update
+	 * @return results of the delete
+	 */
+	public int delete(@NotEmpty("query") Query<T> query) {
+		WriteResult writeResult = datastore.delete(query);
+		return writeResult.getN();
+	}
+
+	/**
+	 * Update the given entity (by code)
+	 * 
+	 * @param t
+	 *            the entity to update
+	 * @return the entity updated
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 */
+	public int update(@NotEmpty("t") T t) throws IllegalArgumentException, OperationException {
+		return update(t, null);
+	}
+
+	/**
+	 * Update the given entity (by code)
+	 * 
+	 * @param t
+	 *            the entity to update
+	 * @param ignoreFields
+	 *            不更新的字段
+	 * @return the entity updated
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 */
+	public int update(@NotEmpty("t") T t, Set<String> ignoreFields)
+			throws IllegalArgumentException, OperationException {
+		String code = t.getCode();
+		if (StringUtils.isEmpty(code)) {
+			throw new IllegalArgumentException("没有指定 code:" + t);
+		}
+		checkField(t);
+		UpdateOperations<T> operations = createUpdateOperations(t, ignoreFields);
+		return update(code, operations);
+	}
+
+	/**
+	 * Update the given entity (by code)
+	 * 
+	 * @param code
+	 *            the code to update
+	 * @return number updated
+	 */
+	public int update(@NotEmpty("code") String code, @NotEmpty("operations") UpdateOperations<T> operations) {
+		Query<T> query = createQuery(code);
+		return update(query, operations);
+	}
+
+	/**
+	 * Updates all entities found with the operations; this is an atomic
+	 * operation per entity
+	 * 
+	 * @param query
+	 *            the query used to match the documents to update
+	 * @param operations
+	 *            the update operations to perform
+	 * @return number updated
+	 */
+	private int update(@NotEmpty("query") Query<T> query, @NotEmpty("operations") UpdateOperations<T> operations) {
+		operations.set("lastModified", new Date());
+		operations.inc("version");
+		UpdateResults updateResults = datastore.update(query, operations);
+		return updateResults.getUpdatedCount();
+	}
+
+	/**
+	 * Find all instances
+	 * 
+	 * @return The results
+	 */
+	public List<T> findAll() {
+		Query<T> query = createQuery();
+		return find(query);
+	}
+
+	/**
+	 * Find the given entity (by code); shorthand for
+	 * {@code find("code ", code)}
+	 * 
+	 * @param code
+	 *            the code to query
+	 * @return the matched entity. may be null.
+	 */
+	public T findOne(@NotEmpty("code") String code) {
+		return createQuery(code).get();
+	}
+
+	/**
+	 * Find the given entities (by code);
+	 * 
+	 * @param codes
+	 *            the codes to query
+	 * @return the matched entities. may be null.
+	 */
+	public List<T> findIn(@NotEmpty("codes") Iterable<String> codes) {
+		return createQuery().field("code").in(codes).asList();
+	}
+
+	/**
+	 * 根据指定的条件查询数据
+	 * 
+	 * @param filters
+	 *            过滤条件,key-value的形式
+	 * @return The results
+	 */
+	public List<T> find(@NotEmpty("filters") Map<String, Object> filters) {
+		Query<T> query = createQuery();
+		filter(query, filters);
+		return find(query);
+	}
+
+	/**
+	 * Execute the query and get the results
+	 * 
+	 * @param query
+	 *            the query used to match the documents to update
+	 * @return The results
+	 */
+	public List<T> find(@NotEmpty("query") Query<T> query) {
+		List<T> result = new ArrayList<>();
+		MorphiaIterator<T, T> morphiaIterator = query.fetch();
+		while (morphiaIterator.hasNext()) {
+			result.add(morphiaIterator.next());
+		}
+		return result;
+	}
+
+	/**
+	 * 分页获取数据
+	 * 
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页面大小
+	 * @return 获取到的数据
+	 * @throws IllegalArgumentException
+	 * @throws IllegalStateException
+	 */
+	public PageResult<T> findPage(@NotEmpty("page") Integer page, @NotEmpty("size") Integer size)
+			throws IllegalArgumentException, IllegalStateException {
+		Query<T> query = createQuery();
+		return findPage(query, page, size);
+	}
+
+	/**
+	 * 根据指定的条件分页查询数据
+	 * 
+	 * @param filters
+	 *            过滤条件,key-value的形式
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页面大小
+	 * @return 查询到的数据
+	 * @throws IllegalArgumentException
+	 * @throws IllegalStateException
+	 */
+	public PageResult<T> findPage(@NotEmpty("filters") Map<String, Object> filters, @NotEmpty("page") Integer page,
+			@NotEmpty("size") Integer size) throws IllegalArgumentException, IllegalStateException {
+		Query<T> query = createQuery();
+		filter(query, filters);
+		return findPage(query, page, size);
+	}
+
+	/**
+	 * 分页查询数据
+	 * 
+	 * @param query
+	 *            the query used to match the documents to update
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页面大小
+	 * @return 查询到的数据
+	 * @throws IllegalArgumentException
+	 * @throws IllegalStateException
+	 */
+	public PageResult<T> findPage(@NotEmpty("query") Query<T> query, @NotEmpty("page") Integer page,
+			@NotEmpty("size") Integer size) throws IllegalArgumentException, IllegalStateException {
+		PageResult<T> result = new PageResult<>();
+		if (page < 1) {
+			throw new IllegalArgumentException("page 小于1");
+		}
+		if (size < 1) {
+			throw new IllegalArgumentException("size 小于1");
+		}
+		result.setPage(page);
+		result.setSize(size);
+
+		// 获取数据总数
+		long count = datastore.getCount(entityClass);
+		result.setTotalElement(count);
+		if (count == 0) {
+			return result;
+		}
+
+		int offset = (page - 1) * size;
+		if (offset >= count) {
+			throw new IllegalStateException("当前页码不存在");
+		}
+		if (page == 1) {
+			result.setFirst(true);
+		}
+		int totalPage = (int) Math.ceil(count / (1.0 * size));
+		result.setTotalPage(totalPage);
+		if (totalPage == page) {
+			result.setLast(true);
+		}
+
+		// 获取指定页的数据
+		FindOptions findOptions = new FindOptions();
+		findOptions.skip(offset);
+		findOptions.limit(size);
+		MorphiaIterator<T, T> morphiaIterator = query.fetch(findOptions);
+		List<T> content = new ArrayList<>();
+		while (morphiaIterator.hasNext()) {
+			content.add(morphiaIterator.next());
+		}
+		result.setContent(content);
+		return result;
+	}
+
+	/**
+	 * 解析json为指定对象
+	 * 
+	 * @param json
+	 *            json数据
+	 * @return 解析得到的对象
+	 */
+	public T parse(@NotEmpty("json") String json) {
+		return JSONObject.parseObject(json, entityClass);
+	}
+
+	/**
+	 * 为查询增加过滤条件
+	 * 
+	 * @param query
+	 *            the query used to match the documents to update
+	 * @param filters
+	 *            过滤条件,key-value的形式
+	 *            <p>
+	 *            <b>Note</b>: Property is in the form of "name op" ("age >").
+	 *            <p/>
+	 *            <p>
+	 *            Valid operators are ["=", "==","!=", "<>", ">", "<", ">=",
+	 *            "<=", "in", "nin", "all", "size", "exists"]
+	 *            </p>
+	 *            <p/>
+	 *            <p>
+	 *            Examples:
+	 *            </p>
+	 *            <p/>
+	 *            <ul>
+	 *            <li>{@code filter("yearsOfOperation >", 5)}</li>
+	 *            <li>{@code filter("rooms.maxBeds >=", 2)}</li>
+	 *            <li>{@code filter("rooms.bathrooms exists", 1)}</li>
+	 *            <li>{@code filter("stars in", new Long[]{3, 4}) //3 and 4
+	 *            stars (midrange?)}</li>
+	 *            <li>{@code filter("quantity mod", new Long[]{4, 0}) //
+	 *            customers ordered in packs of 4)}</li>
+	 *            <li>{@code filter("age >=", age)}</li>
+	 *            <li>{@code filter("age =", age)}</li>
+	 *            <li>{@code filter("age", age)} (if no operator, = is assumed)
+	 *            </li>
+	 *            <li>{@code filter("age !=", age)}</li>
+	 *            <li>{@code filter("age in", ageList)}</li>
+	 *            <li>{@code filter("customers.loyaltyYears in", yearsList)}
+	 *            </li>
+	 *            </ul>
+	 *            <p/>
+	 *            <p>
+	 *            You can filter on id properties <strong>if</strong> this query
+	 *            is restricted to a Class<T>.
+	 * @return the query used to match the documents to update
+	 */
+	protected Query<T> filter(@NotEmpty("query") Query<T> query, @NotEmpty("filters") Map<String, Object> filters) {
+		Set<Entry<String, Object>> entrySet = filters.entrySet();
+		for (Entry<String, Object> entry : entrySet) {
+			query.filter(entry.getKey(), entry.getValue());
+		}
+		return query;
+	}
+}

+ 80 - 0
kanban-common/src/main/java/com/uas/kanban/base/BaseEntity.java

@@ -0,0 +1,80 @@
+package com.uas.kanban.base;
+
+import java.util.Date;
+
+import org.bson.types.ObjectId;
+import org.mongodb.morphia.annotations.Id;
+
+/**
+ * 基本实体,定义 id、lastModified 等属性
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:43:37
+ */
+public abstract class BaseEntity extends Coded {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 初始版本
+	 */
+	public static final long VERSION = 1L;
+
+	/**
+	 * 如果使用 ObjectId ,在 HTTP 请求过程中,接收 json 形式的 id 并转换为 ObjectId
+	 * 时,会首先调用默认无参构造器方法构造对象 {@link org.bson.types.ObjectId.ObjectId()}
+	 * ,此时会使用默认值初始化 timestamp 等变量,而 timestamp 等变量由 final 修饰,之后不能再改变,所以得到的
+	 * ObjectId 并非预想的对象, 与实际传过来的 json 里的值不一样
+	 */
+	@Id
+	protected ObjectId id;
+
+	/**
+	 * 数据生成的时间
+	 */
+	protected Date createTime;
+
+	/**
+	 * 数据最后修改的时间
+	 */
+	protected Date lastModified;
+
+	/**
+	 * 数据的版本号,每次修改加1
+	 */
+	protected Long version;
+
+	@Override
+	public void init() {
+		Date now = new Date();
+		createTime = now;
+		lastModified = now;
+		version = VERSION;
+		super.init();
+	}
+
+	public Date getCreateTime() {
+		return createTime;
+	}
+
+	public void setCreateTime(Date createTime) {
+		this.createTime = createTime;
+	}
+
+	public Date getLastModified() {
+		return lastModified;
+	}
+
+	public void setLastModified(Date lastModified) {
+		this.lastModified = lastModified;
+	}
+
+	public Long getVersion() {
+		return version;
+	}
+
+	public void setVersion(Long version) {
+		this.version = version;
+	}
+
+}

+ 106 - 0
kanban-common/src/main/java/com/uas/kanban/base/BaseService.java

@@ -0,0 +1,106 @@
+package com.uas.kanban.base;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.exception.OperationException;
+
+/**
+ * 基本 Service
+ * 
+ * @author sunyj
+ * @since 2017年9月2日 下午8:18:16
+ * @param <T>
+ */
+public abstract class BaseService<T extends BaseEntity> {
+
+	@Autowired
+	private BaseDao<T> baseDao;
+
+	/**
+	 * 保存数据
+	 * 
+	 * @param json
+	 *            json格式的数据
+	 * @return 保存的数据
+	 */
+	public T save(@NotEmpty("json") String json) {
+		T t = baseDao.parse(json);
+		return baseDao.save(t);
+	}
+
+	/**
+	 * 删除所有数据
+	 * 
+	 * @return 删除的数据条数
+	 */
+	public int deleteAll() {
+		return baseDao.deleteAll();
+	}
+
+	/**
+	 * Delete the given entity (by code)
+	 * 
+	 * @param code
+	 *            the code to delete
+	 * @return results of the delete
+	 */
+	public int deleteOne(@NotEmpty("code") String code) {
+		return baseDao.deleteOne(code);
+	}
+
+	/**
+	 * 根据id更新数据
+	 * 
+	 * @param json
+	 *            json格式的数据
+	 * @return 更新的数据条数
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 */
+	public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+		T t = baseDao.parse(json);
+		return baseDao.update(t);
+	}
+
+	/**
+	 * 获取所有数据
+	 * 
+	 * @return 所有数据
+	 */
+	public List<T> getAll() {
+		return baseDao.findAll();
+	}
+
+	/**
+	 * 根据code查询数据
+	 * 
+	 * @param code
+	 *            code
+	 * @return 数据
+	 */
+	public T getOne(@NotEmpty("code") String code) {
+		return baseDao.findOne(code);
+	}
+
+	/**
+	 * 分页获取所有数据
+	 * 
+	 * @param p
+	 *            页码
+	 * @param s
+	 *            页面大小
+	 * @return 获取的数据
+	 */
+	public PageResult<T> get(Integer p, Integer s) {
+		if (p == null) {
+			p = PageResult.PAGE_INDEX;
+		}
+		if (s == null) {
+			s = PageResult.PAGE_SIZE;
+		}
+		return baseDao.findPage(p, s);
+	}
+}

+ 47 - 0
kanban-common/src/main/java/com/uas/kanban/base/Coded.java

@@ -0,0 +1,47 @@
+package com.uas.kanban.base;
+
+import java.io.Serializable;
+
+import org.springframework.util.StringUtils;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.util.NumberGenerator;
+
+/**
+ * 抽象类,表示含有 code 字段
+ * 
+ * @author sunyj
+ * @since 2017年9月3日 下午6:35:37
+ */
+public abstract class Coded implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 参数 code, 用作标识
+	 */
+	@FieldProperty(nullable = false)
+	protected String code;
+
+	/**
+	 * 初始化
+	 * 
+	 * @throws IllegalArgumentException
+	 *             已指定 code 时,不可再初始化
+	 */
+	public void init() throws IllegalArgumentException {
+		if (!StringUtils.isEmpty(code)) {
+			throw new IllegalArgumentException("不能指定code:" + this.toString());
+		}
+		code = NumberGenerator.generateId();
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+}

+ 136 - 0
kanban-common/src/main/java/com/uas/kanban/base/PageResult.java

@@ -0,0 +1,136 @@
+package com.uas.kanban.base;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 分页查询的结果
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:45:33
+ * @param <T>
+ */
+public class PageResult<T> implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 默认的页码
+	 */
+	public static final int PAGE_INDEX = 1;
+
+	/**
+	 * 默认每页的大小
+	 */
+	public static final int PAGE_SIZE = 20;
+
+	/**
+	 * 总页数
+	 */
+	private int totalPage;
+
+	/**
+	 * 总数目
+	 */
+	private long totalElement;
+
+	/**
+	 * 页码
+	 */
+	private int page;
+
+	/**
+	 * 页面大小
+	 */
+	private int size;
+
+	/**
+	 * 是否为第一页
+	 */
+	private boolean first;
+
+	/**
+	 * 是否为最后一页
+	 */
+	private boolean last;
+
+	/**
+	 * 数据
+	 */
+	private List<T> content;
+
+	public PageResult() {
+	}
+
+	public PageResult(int totalPage, long totalElement, int page, int size, boolean first, boolean last) {
+		this.totalPage = totalPage;
+		this.totalElement = totalElement;
+		this.page = page;
+		this.size = size;
+		this.first = first;
+		this.last = last;
+	}
+
+	public int getTotalPage() {
+		return totalPage;
+	}
+
+	public void setTotalPage(int totalPage) {
+		this.totalPage = totalPage;
+	}
+
+	public long getTotalElement() {
+		return totalElement;
+	}
+
+	public void setTotalElement(long totalElement) {
+		this.totalElement = totalElement;
+	}
+
+	public int getPage() {
+		return page;
+	}
+
+	public void setPage(int page) {
+		this.page = page;
+	}
+
+	public int getSize() {
+		return size;
+	}
+
+	public void setSize(int size) {
+		this.size = size;
+	}
+
+	public boolean isFirst() {
+		return first;
+	}
+
+	public void setFirst(boolean first) {
+		this.first = first;
+	}
+
+	public boolean isLast() {
+		return last;
+	}
+
+	public void setLast(boolean last) {
+		this.last = last;
+	}
+
+	public List<T> getContent() {
+		return content;
+	}
+
+	public void setContent(List<T> content) {
+		this.content = content;
+	}
+
+	@Override
+	public String toString() {
+		return "PageResult [totalPage=" + totalPage + ", totalElement=" + totalElement + ", page=" + page + ", size="
+				+ size + ", first=" + first + ", last=" + last + ", content=" + content + "]";
+	}
+
+}

+ 33 - 0
kanban-common/src/main/java/com/uas/kanban/exception/OperationException.java

@@ -0,0 +1,33 @@
+package com.uas.kanban.exception;
+
+/**
+ * 操作时的异常,如非法操作等
+ * 
+ * @author sunyj
+ * @since 2017年9月2日 下午5:48:44
+ */
+public class OperationException extends Exception {
+
+	private static final long serialVersionUID = 1L;
+
+	public OperationException() {
+		super();
+	}
+
+	public OperationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+	public OperationException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public OperationException(String message) {
+		super(message);
+	}
+
+	public OperationException(Throwable cause) {
+		super(cause);
+	}
+
+}

+ 46 - 0
kanban-common/src/main/java/com/uas/kanban/util/ArrayUtils.java

@@ -0,0 +1,46 @@
+package com.uas.kanban.util;
+
+/**
+ * 数组工具类
+ * 
+ * @author sunyj
+ * @since 2017年4月21日 下午9:01:53
+ */
+public class ArrayUtils {
+
+	public static boolean isEmpty(Object[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(long[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(int[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(short[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(char[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(byte[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(double[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(float[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(boolean[] array) {
+		return array == null || array.length == 0;
+	}
+}

+ 21 - 0
kanban-common/src/main/java/com/uas/kanban/util/CollectionUtils.java

@@ -0,0 +1,21 @@
+package com.uas.kanban.util;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * 集合工具类
+ * 
+ * @author sunyj
+ * @since 2017年4月21日 下午9:43:31
+ */
+public class CollectionUtils {
+
+	public static boolean isEmpty(Collection<?> coll) {
+		return (coll == null || coll.isEmpty());
+	}
+
+	public static boolean isEmpty(Map<?, ?> map) {
+		return (map == null || map.isEmpty());
+	}
+}

+ 73 - 0
kanban-common/src/main/java/com/uas/kanban/util/ContextUtils.java

@@ -0,0 +1,73 @@
+package com.uas.kanban.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * spring容器上下文对象
+ * 
+ * @author yingp
+ *
+ */
+public class ContextUtils {
+	private static ApplicationContext applicationContext;
+
+	private static Logger logger = LoggerFactory.getLogger(ContextUtils.class);
+
+	public static void setApplicationContext(ApplicationContext applicationContext) {
+		synchronized (ContextUtils.class) {
+			logger.debug("setApplicationContext, notifyAll");
+			ContextUtils.applicationContext = applicationContext;
+			ContextUtils.class.notifyAll();
+		}
+	}
+
+	public static ApplicationContext getApplicationContext() {
+		synchronized (ContextUtils.class) {
+			while (applicationContext == null) {
+				try {
+					logger.debug("getApplicationContext, wait...");
+					ContextUtils.class.wait(60000);
+					if (applicationContext == null) {
+						logger.warn("Have been waiting for ApplicationContext to be set for 1 minute", new Exception());
+					}
+				} catch (InterruptedException ex) {
+					logger.debug("getApplicationContext, wait interrupted");
+				}
+			}
+			return applicationContext;
+		}
+	}
+
+	/**
+	 * 获取bean
+	 * 
+	 * @param name
+	 * @return
+	 */
+	public static Object getBean(String name) {
+		return getApplicationContext().getBean(name);
+	}
+
+	/**
+	 * 获取bean
+	 * 
+	 * @param cls
+	 * @return
+	 */
+	public static <T> T getBean(Class<T> cls) {
+		return getApplicationContext().getBean(cls);
+	}
+
+	/**
+	 * 触发事件
+	 * 
+	 * @param event
+	 */
+	public static void publishEvent(ApplicationEvent event) {
+		getApplicationContext().publishEvent(event);
+	}
+
+}

+ 44 - 0
kanban-common/src/main/java/com/uas/kanban/util/ExceptionUtils.java

@@ -0,0 +1,44 @@
+package com.uas.kanban.util;
+
+/**
+ * 处理异常的工具类
+ * 
+ * @author sunyj
+ * @since 2017年8月10日 下午3:26:55
+ */
+public class ExceptionUtils {
+
+	/**
+	 * 获取异常及其Cause拼接成的字符串
+	 * 
+	 * @param e
+	 *            异常
+	 * @return 拼接后的结果
+	 */
+	public static String getMessage(Throwable e) {
+		StringBuilder sb = new StringBuilder(e.toString());
+		if (e.getCause() != null) {
+			sb.append("\nCaused by: ").append(getMessage(e.getCause()));
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * 获取异常及其Cause的StackTrace拼接成的字符串
+	 * 
+	 * @param e
+	 *            异常
+	 * @return 拼接后的结果
+	 */
+	public static String getDetailedMessage(Throwable e) {
+		StringBuilder sb = new StringBuilder(e.toString());
+		StackTraceElement[] stackTraceElements = e.getStackTrace();
+		for (StackTraceElement stackTraceElement : stackTraceElements) {
+			sb.append("\n\t").append(stackTraceElement.toString());
+		}
+		if (e.getCause() != null) {
+			sb.append("\nCaused by: ").append(getDetailedMessage(e.getCause()));
+		}
+		return sb.toString();
+	}
+}

+ 56 - 0
kanban-common/src/main/java/com/uas/kanban/util/IpHelper.java

@@ -0,0 +1,56 @@
+package com.uas.kanban.util;
+
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * <p>
+ * 获取IP地址类
+ * </p>
+ * 
+ */
+public class IpHelper {
+
+	/**
+	 * 获取客户端IP
+	 * 
+	 * @param request
+	 * @return
+	 */
+	public static String getIp(HttpServletRequest request) {
+		String ipAddress = null;
+		ipAddress = request.getHeader("X-Forwarded-For");
+		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+			ipAddress = request.getHeader("Proxy-Client-IP");
+		}
+		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+			ipAddress = request.getHeader("WL-Proxy-Client-IP");
+		}
+		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+			ipAddress = request.getRemoteAddr();
+		}
+		// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+		if (ipAddress != null && ipAddress.length() > 15) {
+			if (ipAddress.indexOf(",") > 0) {
+				ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+			}
+		}
+		// window7系统下,用localhost访问时,ip会变成0:0:0:0:0:0:0:1
+		if (ipAddress != null && "0:0:0:0:0:0:0:1".equals(ipAddress)) {
+			ipAddress = "127.0.0.1";
+		}
+		return ipAddress;
+	}
+
+	/**
+	 * 判断指定的ip地址是否在局域网内
+	 * 
+	 * @param ipAddress
+	 * @return
+	 */
+	public static boolean isLAN(String ipAddress) {
+		return Pattern.matches("^((192.168.)|(10.)|(172.16)|(127.0))+[0-9.]+$", ipAddress);
+	}
+
+}

+ 36 - 0
kanban-common/src/main/java/com/uas/kanban/util/NumberGenerator.java

@@ -0,0 +1,36 @@
+package com.uas.kanban.util;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 数字生成工具类
+ * 
+ * @author sunyj
+ * @since 2017年8月22日 上午11:34:44
+ */
+public class NumberGenerator {
+
+	/**
+	 * 生成id时的偏移量
+	 */
+	private static int offset = 0;
+
+	/**
+	 * @return 产生唯一的id(一毫秒内不超过100个则不重复)
+	 */
+	public static String generateId() {
+		// 偏移量,不超过100个
+		offset = (offset + 1) % 100;
+		// 当前毫秒数
+		long now = new Date().getTime();
+		// 起始时间 2017-01-01 00:00:00:000
+		Calendar startCalendar = Calendar.getInstance();
+		startCalendar.set(2017, 0, 1, 0, 0, 0);
+		startCalendar.set(Calendar.MILLISECOND, 0);
+		// 偏移量加16,保证得到的是两位16进制字符
+		String hex = Long.toHexString(now - startCalendar.getTimeInMillis()) + Integer.toHexString(offset + 16);
+		// 最后反转并转为大写
+		return hex.toUpperCase();
+	}
+}

+ 53 - 0
kanban-common/src/main/java/com/uas/kanban/util/ObjectUtils.java

@@ -0,0 +1,53 @@
+package com.uas.kanban.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * 对象工具类
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午3:54:26
+ */
+public class ObjectUtils {
+
+	/**
+	 * 判断是否为null、空串或者空集合
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	public static boolean isEmpty(Object obj) {
+		return obj == null || (obj instanceof String && StringUtils.isEmpty((String) obj))
+				|| (obj instanceof Collection && CollectionUtils.isEmpty((Collection<?>) obj))
+				|| (obj instanceof Map && CollectionUtils.isEmpty((Map<?, ?>) obj));
+	}
+
+	/**
+	 * 深克隆对象
+	 * 
+	 * @param t
+	 *            要克隆的对象
+	 * @return 克隆得到的对象
+	 * @throws IOException
+	 * @throws ClassNotFoundException
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T extends Serializable> T clone(T t)
+			throws IOException, ClassNotFoundException, NotSerializableException {
+		if (t == null) {
+			return null;
+		}
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		new ObjectOutputStream(out).writeObject(t);
+		ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
+		return (T) in.readObject();
+	}
+}

+ 31 - 0
kanban-common/src/main/java/com/uas/kanban/util/StringUtils.java

@@ -0,0 +1,31 @@
+package com.uas.kanban.util;
+
+/**
+ * 字符串工具类
+ * 
+ * @author sunyj
+ * @since 2017年5月25日 上午11:09:36
+ */
+public class StringUtils {
+
+	public static boolean isEmpty(Object str) {
+		return (str == null || "".equals(str));
+	}
+
+	public static String reverse(String str) {
+		if (isEmpty(str)) {
+			return str;
+		}
+		char[] array = str.toCharArray();
+		int begin = 0;
+		int end = str.length() - 1;
+		while (begin < end) {
+			array[begin] = (char) (array[begin] ^ array[end]);
+			array[end] = (char) (array[begin] ^ array[end]);
+			array[begin] = (char) (array[begin] ^ array[end]);
+			begin++;
+			end--;
+		}
+		return new String(array);
+	}
+}

+ 178 - 0
kanban-console/pom.xml

@@ -0,0 +1,178 @@
+<?xml version="1.0"?>
+<project
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>com.uas.kanban</groupId>
+		<artifactId>kanban-parent</artifactId>
+		<version>0.0.1-SNAPSHOT</version>
+	</parent>
+	<artifactId>kanban-console</artifactId>
+	<packaging>war</packaging>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-config</artifactId>
+		</dependency>
+		<!-- spring boot -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-security</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-tx</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.mongodb</groupId>
+			<artifactId>mongo-java-driver</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.uas.kanban</groupId>
+			<artifactId>kanban-auth</artifactId>
+			<version>${kanban.auth.version}</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>com.google.code.maven-svn-revision-number-plugin</groupId>
+				<artifactId>svn-revision-number-maven-plugin</artifactId>
+				<version>1.13</version>
+				<executions>
+					<execution>
+						<goals>
+							<goal>revision</goal>
+						</goals>
+					</execution>
+				</executions>
+				<configuration>
+					<entries>
+						<entry>
+							<prefix>svn</prefix>
+						</entry>
+					</entries>
+				</configuration>
+				<dependencies>
+					<dependency>
+						<groupId>org.tmatesoft.svnkit</groupId>
+						<artifactId>svnkit</artifactId>
+						<version>1.8.10</version>
+					</dependency>
+				</dependencies>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<encoding>${project.build.sourceEncoding}</encoding>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<artifactId>maven-resources-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>copy-resources</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>copy-resources</goal>
+						</goals>
+						<configuration>
+							<outputDirectory>${project.build.directory}/minify</outputDirectory>
+							<overwrite>false</overwrite>
+							<resources>
+								<resource>
+									<directory>${basedir}/src/main/webapp</directory>
+								</resource>
+							</resources>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>com.uas.plugins</groupId>
+				<artifactId>static-maven-plugin</artifactId>
+				<version>0.0.2-SNAPSHOT</version>
+				<!-- 静态资源分离 -->
+				<executions>
+					<execution>
+						<id>default-static</id>
+						<phase>prepare-package</phase>
+						<configuration>
+							<webappDir>${project.build.directory}/minify</webappDir>
+							<sourceDir>/</sourceDir>
+							<targetDir>${project.build.directory}/statics</targetDir>
+							<staticDir>/</staticDir>
+							<sourceIncludes>
+								<sourceInclude>resources/js/**/*.js</sourceInclude>
+								<sourceInclude>resources/css/**/*.css</sourceInclude>
+								<sourceInclude>WEB-INF/views/**/*.html</sourceInclude>
+							</sourceIncludes>
+							<!-- http://static.ubtoc.com/css/index.css?_v=1450321871828 -->
+							<versionSuffix>
+								<suffix>?_v=${svn.revision}</suffix>
+								<exclude>resources/lib/**/*</exclude>
+							</versionSuffix>
+						</configuration>
+						<goals>
+							<goal>static</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+			</plugin>
+		</plugins>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>org.eclipse.m2e</groupId>
+					<artifactId>lifecycle-mapping</artifactId>
+					<version>1.0.0</version>
+					<configuration>
+						<lifecycleMappingMetadata>
+							<pluginExecutions>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>
+											com.google.code.maven-svn-revision-number-plugin
+										</groupId>
+										<artifactId>
+											svn-revision-number-maven-plugin
+										</artifactId>
+										<versionRange>
+											[1.13,)
+										</versionRange>
+										<goals>
+											<goal>
+												revision
+											</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<ignore></ignore>
+									</action>
+								</pluginExecution>
+							</pluginExecutions>
+						</lifecycleMappingMetadata>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
+</project>

+ 37 - 0
kanban-console/src/main/java/com/uas/kanban/Application.java

@@ -0,0 +1,37 @@
+package com.uas.kanban;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.event.ApplicationPreparedEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+import com.uas.kanban.util.ContextUtils;
+
+/**
+ * 应用入口
+ * 
+ * @author sunyj
+ * @since 2017年8月16日 下午4:00:03
+ */
+@EnableTransactionManagement(proxyTargetClass = true)
+@SpringBootApplication(scanBasePackages = "com.uas.kanban")
+@EnableWebMvc
+public class Application {
+	public static void main(String[] args) throws FileNotFoundException {
+		System.setErr(new PrintStream(new FileOutputStream("logs/log.log", true)));
+		SpringApplication application = new SpringApplication(Application.class);
+		application.addListeners(new ApplicationListener<ApplicationPreparedEvent>() {
+			@Override
+			public void onApplicationEvent(ApplicationPreparedEvent event) {
+				ContextUtils.setApplicationContext(event.getApplicationContext());
+			}
+		});
+		application.run(args);
+	}
+}

+ 58 - 0
kanban-console/src/main/java/com/uas/kanban/MongoConfiguration.java

@@ -0,0 +1,58 @@
+package com.uas.kanban;
+
+import org.mongodb.morphia.Datastore;
+import org.mongodb.morphia.Morphia;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import com.mongodb.MongoClient;
+import com.mongodb.MongoClientURI;
+import com.uas.kanban.model.Template;
+import com.uas.kanban.util.ContextUtils;
+
+/**
+ * mongo配置
+ * 
+ * @author sunyj
+ * @since 2017年8月28日 下午3:16:39
+ */
+@Configuration
+public class MongoConfiguration {
+
+	@Value("${mongo.host}")
+	private String host;
+
+	@Value("${mongo.port}")
+	private Integer port;
+
+	@Value("${mongo.username}")
+	private String username;
+
+	@Value("${mongo.password}")
+	private String password;
+
+	@Value("${mongo.dbName}")
+	private String dbName;
+
+	@Bean
+	@Primary
+	public MongoClient mongoClient() {
+		String uri = String.format("mongodb://%s:%s@%s:%d/%s", username, password, host, port, dbName);
+		MongoClientURI mongoClientURI = new MongoClientURI(uri);
+		MongoClient mongoClient = new MongoClient(mongoClientURI);
+		return mongoClient;
+	}
+
+	@Bean
+	@Primary
+	public Datastore datastore() {
+		Morphia morphia = new Morphia();
+		morphia.mapPackage(Template.class.getPackage().getName());
+		MongoClient mongoClient = ContextUtils.getBean(MongoClient.class);
+		Datastore datastore = morphia.createDatastore(mongoClient, dbName);
+		datastore.ensureIndexes();
+		return datastore;
+	}
+}

+ 74 - 0
kanban-console/src/main/java/com/uas/kanban/WebAppConfiguration.java

@@ -0,0 +1,74 @@
+package com.uas.kanban;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
+import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+import org.springframework.web.servlet.view.InternalResourceViewResolver;
+
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.alibaba.fastjson.support.config.FastJsonConfig;
+import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
+
+/**
+ * Web相关配置
+ * 
+ * @author sunyj
+ * @since 2017年2月17日 下午5:45:38
+ */
+@Configuration
+@ComponentScan(basePackages = "com.uas.kanban.controller")
+public class WebAppConfiguration extends WebMvcConfigurerAdapter {
+
+	@Override
+	public void addResourceHandlers(ResourceHandlerRegistry registry) {
+		// Spring boot默认资源路径在src/main/resources下,而非/src/main/webapp
+		// 使用/src/main/webapp下资源均需在此添加
+		registry.addResourceHandler("/static/**").addResourceLocations("/resources/");
+		registry.addResourceHandler("/WEB-INF/**").addResourceLocations("/WEB-INF/");
+		super.addResourceHandlers(registry);
+	}
+
+	@Override
+	public void configureViewResolvers(ViewResolverRegistry registry) {
+		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver("/WEB-INF/views/", ".html");
+		viewResolver.setContentType("text/html;charset=UTF-8");
+		registry.viewResolver(viewResolver);
+		super.configureViewResolvers(registry);
+	}
+
+	@Override
+	public void addViewControllers(ViewControllerRegistry registry) {
+		registry.addViewController("/").setViewName("index");
+		registry.addViewController("/index").setViewName("index");
+		registry.addViewController("/console").setViewName("console");
+		registry.addViewController("/fileUpload").setViewName("fileUpload");
+		super.addViewControllers(registry);
+	}
+
+	@Override
+	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+		FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
+		fastJsonHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON_UTF8));
+		FastJsonConfig fastJsonConfig = new FastJsonConfig();
+		fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
+		fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
+		converters.add(fastJsonHttpMessageConverter);
+
+		StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(
+				Charset.forName("UTF-8"));
+		stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML));
+		converters.add(stringHttpMessageConverter);
+		super.configureMessageConverters(converters);
+	}
+
+}

+ 36 - 0
kanban-console/src/main/java/com/uas/kanban/controller/DataSourceController.java

@@ -0,0 +1,36 @@
+package com.uas.kanban.controller;
+
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.uas.kanban.base.BaseController;
+import com.uas.kanban.model.DataSource;
+import com.uas.kanban.service.DataSourceService;
+
+/**
+ * 数据源
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:42:10
+ */
+@Controller
+@RequestMapping("/datasource")
+public class DataSourceController extends BaseController<DataSource> {
+
+	@Autowired
+	private DataSourceService dataSourceService;
+
+	@RequestMapping("/get/un/{username}")
+	@ResponseBody
+	public List<DataSource> getByUsername(@PathVariable("username") String un, HttpServletRequest request) {
+		return dataSourceService.getByUsername(un);
+	}
+
+}

+ 19 - 0
kanban-console/src/main/java/com/uas/kanban/controller/KanbanController.java

@@ -0,0 +1,19 @@
+package com.uas.kanban.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.uas.kanban.base.BaseController;
+import com.uas.kanban.model.Kanban;
+
+/**
+ * 看板
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:42:10
+ */
+@Controller
+@RequestMapping("/kanban")
+public class KanbanController extends BaseController<Kanban> {
+
+}

+ 72 - 0
kanban-console/src/main/java/com/uas/kanban/controller/KanbanInstanceController.java

@@ -0,0 +1,72 @@
+package com.uas.kanban.controller;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseController;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.KanbanInstance;
+import com.uas.kanban.service.KanbanInstanceService;
+
+/**
+ * 看板
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:42:10
+ */
+@Controller
+@RequestMapping("/kanbanInstance")
+public class KanbanInstanceController extends BaseController<KanbanInstance> {
+
+	@Autowired
+	private KanbanInstanceService kanbanInstanceService;
+
+	/**
+	 * 填写参数
+	 * 
+	 * @param code
+	 *            看板实例 code
+	 * @param templateCode
+	 *            模版 code
+	 * @param values
+	 *            填写的参数值,key-value类型 <br/>
+	 *            (虽然看板实例中参数可为空,但是这里却不可为空,否则就不必调用该接口)
+	 *            <p/>
+	 *            <table border=1 cellpadding=5 cellspacing=0 summary=
+	 *            "Key and value">
+	 *            <tr>
+	 *            <th>key</th>
+	 *            <th>value</th>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>参数 code</td>
+	 *            <td>所填写的参数的值,须与参数类型一致
+	 *            {@link com.uas.kanban.model.TemplateParameter.Type}</td>
+	 *            </tr>
+	 *            </table>
+	 * @param request
+	 * @return 更新的看板实例数量
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 */
+	@RequestMapping("/fillParameters")
+	@ResponseBody
+	public int fillParameters(@NotEmpty("code") String code, @NotEmpty("templateCode") String templateCode,
+			@NotEmpty("values") String values, HttpServletRequest request)
+			throws IllegalArgumentException, OperationException {
+		return kanbanInstanceService.fillParameters(code, templateCode, JSONObject.parseObject(values));
+	}
+
+	@RequestMapping("/parseData/{code}")
+	@ResponseBody
+	public String parseData(@PathVariable("code") String code, HttpServletRequest request) {
+		return kanbanInstanceService.parseData(code);
+	}
+}

+ 90 - 0
kanban-console/src/main/java/com/uas/kanban/controller/TemplateController.java

@@ -0,0 +1,90 @@
+package com.uas.kanban.controller;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseController;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.Template;
+import com.uas.kanban.model.TemplateParameter;
+import com.uas.kanban.service.TemplateService;
+
+/**
+ * 用户
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:42:10
+ */
+@Controller
+@RequestMapping("/template")
+public class TemplateController extends BaseController<Template> {
+
+	@Autowired
+	private TemplateService templateService;
+
+	/**
+	 * 添加模版参数
+	 * 
+	 * @param code
+	 *            模版的 code
+	 * @param parameter
+	 *            要添加的参数(json 格式)
+	 * @param request
+	 * @return 更新的模版数量
+	 */
+	@RequestMapping("/parameter/add")
+	@ResponseBody
+	public int addParameter(@NotEmpty("code") String code, @NotEmpty("parameter") String parameter,
+			HttpServletRequest request) {
+		TemplateParameter templateParameter = JSONObject.parseObject(parameter, TemplateParameter.class);
+		return templateService.addParameter(code, templateParameter);
+	}
+
+	/**
+	 * 删除模版参数
+	 * 
+	 * @param code
+	 *            模版的 code
+	 * @param parameterCode
+	 *            模版参数的 code
+	 * @param request
+	 * @return 更新的模版数量
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 * @throws IllegalStateException
+	 */
+	@RequestMapping("/parameter/delete")
+	@ResponseBody
+	public int deleteParameter(@NotEmpty("code") String code, @NotEmpty("parameterCode") String parameterCode,
+			HttpServletRequest request) throws IllegalStateException, IllegalArgumentException, OperationException {
+		return templateService.deleteParameter(code, parameterCode);
+	}
+
+	/**
+	 * 更新模版参数
+	 * 
+	 * @param code
+	 *            模版的 code
+	 * @param parameter
+	 *            要更新的参数(json 格式)
+	 * @param request
+	 * @return 更新的模版数量
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 * @throws IllegalStateException
+	 */
+	@RequestMapping("/parameter/update")
+	@ResponseBody
+	public int updateParameter(@NotEmpty("code") String code, @NotEmpty("parameter") String parameter,
+			HttpServletRequest request) throws IllegalStateException, IllegalArgumentException, OperationException {
+		TemplateParameter templateParameter = JSONObject.parseObject(parameter, TemplateParameter.class);
+		return templateService.updateParameter(code, templateParameter);
+	}
+
+}

+ 57 - 0
kanban-console/src/main/java/com/uas/kanban/controller/UploadController.java

@@ -0,0 +1,57 @@
+package com.uas.kanban.controller;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 文件上传
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:42:45
+ */
+@Controller
+@RequestMapping("/upload")
+public class UploadController {
+
+	private static final String UPLOAD_DIR = System.getProperty("java.io.tmpdir");
+
+	private Logger logger = LoggerFactory.getLogger(UploadController.class);
+
+	@RequestMapping
+	@ResponseBody
+	public String upload(MultipartFile file) {
+		String message = "";
+
+		if (file == null || file.isEmpty()) {
+			message = "文件为空,无法进行上传!";
+			logger.error(message);
+			return message;
+		}
+
+		String fileName = file.getOriginalFilename();
+		String targetFilePath = (UPLOAD_DIR.endsWith(File.separator) ? UPLOAD_DIR : UPLOAD_DIR + "/") + fileName;
+		File targetFile = new File(targetFilePath);
+		if (!targetFile.getParentFile().exists()) {
+			targetFile.getParentFile().mkdirs();
+		}
+		try {
+			file.transferTo(targetFile);
+			message = "成功上传文件至 :" + targetFile.getCanonicalPath();
+			logger.info(message);
+			return message;
+		} catch (IllegalStateException | IOException e) {
+			logger.error("", e);
+		}
+
+		message = "上传文件失败: " + fileName;
+		logger.error(message);
+		return message;
+	}
+}

+ 17 - 0
kanban-console/src/main/java/com/uas/kanban/dao/DataSourceDao.java

@@ -0,0 +1,17 @@
+package com.uas.kanban.dao;
+
+import org.springframework.stereotype.Component;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.DataSource;
+
+/**
+ * 数据源
+ * 
+ * @author sunyj
+ * @since 2017年8月29日 上午10:08:50
+ */
+@Component
+public class DataSourceDao extends BaseDao<DataSource> {
+
+}

+ 17 - 0
kanban-console/src/main/java/com/uas/kanban/dao/KanbanDao.java

@@ -0,0 +1,17 @@
+package com.uas.kanban.dao;
+
+import org.springframework.stereotype.Component;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.Kanban;
+
+/**
+ * 看板
+ * 
+ * @author sunyj
+ * @since 2017年8月29日 上午10:08:50
+ */
+@Component
+public class KanbanDao extends BaseDao<Kanban> {
+
+}

+ 17 - 0
kanban-console/src/main/java/com/uas/kanban/dao/KanbanInstanceDao.java

@@ -0,0 +1,17 @@
+package com.uas.kanban.dao;
+
+import org.springframework.stereotype.Component;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.KanbanInstance;
+
+/**
+ * 看板实例
+ * 
+ * @author sunyj
+ * @since 2017年8月29日 上午10:08:50
+ */
+@Component
+public class KanbanInstanceDao extends BaseDao<KanbanInstance> {
+
+}

+ 17 - 0
kanban-console/src/main/java/com/uas/kanban/dao/TemplateDao.java

@@ -0,0 +1,17 @@
+package com.uas.kanban.dao;
+
+import org.springframework.stereotype.Component;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.Template;
+
+/**
+ * 用户
+ * 
+ * @author sunyj
+ * @since 2017年8月29日 上午10:08:50
+ */
+@Component
+public class TemplateDao extends BaseDao<Template> {
+
+}

+ 92 - 0
kanban-console/src/main/java/com/uas/kanban/model/DataSource.java

@@ -0,0 +1,92 @@
+package com.uas.kanban.model;
+
+import org.mongodb.morphia.annotations.Entity;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+
+/**
+ * 数据源
+ * 
+ * @author sunyj
+ * @since 2017年8月29日 上午10:06:55
+ */
+@Entity
+public class DataSource extends BaseEntity {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 驱动名
+	 */
+	@FieldProperty(nullable = false)
+	private String driverClassName;
+
+	/**
+	 * url
+	 */
+	@FieldProperty(nullable = false)
+	private String url;
+
+	/**
+	 * 用户名
+	 */
+	@FieldProperty(nullable = false)
+	private String username;
+
+	/**
+	 * 密码
+	 */
+	@FieldProperty(nullable = false)
+	private String password;
+
+	public DataSource() {
+	}
+
+	public DataSource(String driverClassName, String url, String username, String password) {
+		this.driverClassName = driverClassName;
+		this.url = url;
+		this.username = username;
+		this.password = password;
+	}
+
+	public String getDriverClassName() {
+		return driverClassName;
+	}
+
+	public void setDriverClassName(String driverClassName) {
+		this.driverClassName = driverClassName;
+	}
+
+	public String getUrl() {
+		return url;
+	}
+
+	public void setUrl(String url) {
+		this.url = url;
+	}
+
+	public String getUsername() {
+		return username;
+	}
+
+	public void setUsername(String username) {
+		this.username = username;
+	}
+
+	public String getPassword() {
+		return password;
+	}
+
+	public void setPassword(String password) {
+		this.password = password;
+	}
+
+	@Override
+	public String toString() {
+		return "DataSource [driverClassName=" + driverClassName + ", url=" + url + ", username=" + username
+				+ ", password=" + password + ", id=" + id + ", code=" + code + ", createTime=" + createTime
+				+ ", lastModified=" + lastModified + ", version=" + version + "]";
+	}
+
+}

+ 52 - 0
kanban-console/src/main/java/com/uas/kanban/model/Kanban.java

@@ -0,0 +1,52 @@
+package com.uas.kanban.model;
+
+import java.util.Set;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+
+/**
+ * 看板
+ * 
+ * @author sunyj
+ * @since 2017年9月3日 下午4:20:53
+ */
+public class Kanban extends BaseEntity {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 看板名称
+	 */
+	@FieldProperty(nullable = false)
+	private String name;
+
+	/**
+	 * 模版的 code(不可重复,因此采用 Set)
+	 */
+	@FieldProperty(nullable = false)
+	private Set<String> templateCodes;
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public Set<String> getTemplateCodes() {
+		return templateCodes;
+	}
+
+	public void setTemplateCodes(Set<String> templateCodes) {
+		this.templateCodes = templateCodes;
+	}
+
+	@Override
+	public String toString() {
+		return "Kanban [name=" + name + ", templateCodes=" + templateCodes + ", id=" + id + ", code=" + code
+				+ ", createTime=" + createTime + ", lastModified=" + lastModified + ", version=" + version + "]";
+	}
+
+}

+ 113 - 0
kanban-console/src/main/java/com/uas/kanban/model/KanbanInstance.java

@@ -0,0 +1,113 @@
+package com.uas.kanban.model;
+
+import java.util.List;
+import java.util.Map;
+
+import org.mongodb.morphia.annotations.Embedded;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+
+/**
+ * 看板实例
+ * 
+ * @author sunyj
+ * @since 2017年9月3日 下午5:21:06
+ */
+public class KanbanInstance extends BaseEntity {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 看板的 code
+	 */
+	@FieldProperty(nullable = false)
+	private String kanbanCode;
+
+	/**
+	 * 展示方式
+	 */
+	@FieldProperty(nullable = false)
+	private Display display;
+
+	/**
+	 * 切换频率(ms)
+	 */
+	private Long switchFrequent;
+
+	/**
+	 * 看板实例参数
+	 * <p/>
+	 * <table border=1 cellpadding=5 cellspacing=0 summary= "Key and value">
+	 * <tr>
+	 * <th>key</th>
+	 * <th>value</th>
+	 * </tr>
+	 * <tr>
+	 * <td>模版 code</td>
+	 * <td>模版参数</td>
+	 * </tr>
+	 * </table>
+	 */
+	@Embedded
+	private Map<String, List<TemplateParameter>> parameters;
+
+	public String getKanbanCode() {
+		return kanbanCode;
+	}
+
+	public void setKanbanCode(String kanbanCode) {
+		this.kanbanCode = kanbanCode;
+	}
+
+	public Display getDisplay() {
+		return display;
+	}
+
+	public void setDisplay(Display display) {
+		this.display = display;
+	}
+
+	public Long getSwitchFrequent() {
+		return switchFrequent;
+	}
+
+	public void setSwitchFrequent(Long switchFrequent) {
+		this.switchFrequent = switchFrequent;
+	}
+
+	public Map<String, List<TemplateParameter>> getParameters() {
+		return parameters;
+	}
+
+	public void setParameters(Map<String, List<TemplateParameter>> parameters) {
+		this.parameters = parameters;
+	}
+
+	@Override
+	public String toString() {
+		return "KanbanInstance [kanbanCode=" + kanbanCode + ", display=" + display + ", switchFrequent="
+				+ switchFrequent + ", parameters=" + parameters + ", id=" + id + ", createTime=" + createTime
+				+ ", lastModified=" + lastModified + ", version=" + version + ", code=" + code + "]";
+	}
+
+	/**
+	 * 展示方式
+	 * 
+	 * @author sunyj
+	 * @since 2017年9月1日 下午8:01:53
+	 */
+	public enum Display {
+
+		/**
+		 * 自动切换
+		 */
+		AutoSwitch,
+
+		/**
+		 * 分屏展示
+		 */
+		SplitScreen;
+	}
+
+}

+ 83 - 0
kanban-console/src/main/java/com/uas/kanban/model/Template.java

@@ -0,0 +1,83 @@
+package com.uas.kanban.model;
+
+import java.util.List;
+
+import org.mongodb.morphia.annotations.Embedded;
+import org.mongodb.morphia.annotations.Entity;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+
+/**
+ * 数据源
+ * 
+ * @author sunyj
+ * @since 2017年8月29日 上午10:06:55
+ */
+@Entity
+public class Template extends BaseEntity {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 模版名称
+	 */
+	@FieldProperty(nullable = false)
+	private String name;
+
+	/**
+	 * 模版内容
+	 */
+	@FieldProperty(nullable = false)
+	private String content;
+
+	/**
+	 * 数据源 code {@link DataSource} <br/>
+	 * 不使用 @Reference,原因请见 {@link BaseEntity.id}
+	 */
+	@FieldProperty(nullable = false)
+	private String dataSourceCode;
+
+	@Embedded
+	private List<TemplateParameter> parameters;
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
+	public String getDataSourceCode() {
+		return dataSourceCode;
+	}
+
+	public void setDataSourceCode(String dataSourceCode) {
+		this.dataSourceCode = dataSourceCode;
+	}
+
+	public List<TemplateParameter> getParameters() {
+		return parameters;
+	}
+
+	public void setParameters(List<TemplateParameter> parameters) {
+		this.parameters = parameters;
+	}
+
+	@Override
+	public String toString() {
+		return "Template [name=" + name + ", content=" + content + ", dataSourceCode=" + dataSourceCode
+				+ ", parameters=" + parameters + ", id=" + id + ", code=" + code + ", createTime=" + createTime
+				+ ", lastModified=" + lastModified + ", version=" + version + "]";
+	}
+
+}

+ 177 - 0
kanban-console/src/main/java/com/uas/kanban/model/TemplateParameter.java

@@ -0,0 +1,177 @@
+package com.uas.kanban.model;
+
+import java.util.Date;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.Coded;
+import com.uas.kanban.util.ObjectUtils;
+
+/**
+ * 模版参数
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午5:19:15
+ */
+public class TemplateParameter extends Coded {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 参数名称,需要和模板中保持一致
+	 */
+	@FieldProperty(nullable = false)
+	private String name;
+
+	/**
+	 * 参数类型
+	 */
+	@FieldProperty(nullable = false)
+	private Type type;
+
+	/**
+	 * 应用模版时,参数是否必须赋值
+	 */
+	@FieldProperty(nullable = false)
+	private Boolean mandatory;
+
+	/**
+	 * 应用模版时,参数的值(字符串型,根据 {@link type} 决定取哪个值)
+	 */
+	private String stringValue;
+
+	/**
+	 * 应用模版时,参数的值(数值型,根据 {@link type} 决定取哪个值)
+	 */
+	private Double numberValue;
+
+	/**
+	 * 应用模版时,参数的值(日期型,根据 {@link type} 决定取哪个值)
+	 */
+	private Date dateValue;
+
+	// TODO 分组,参数分组主要考虑部分参数可能名称相同
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public Type getType() {
+		return type;
+	}
+
+	public void setType(Type type) {
+		this.type = type;
+	}
+
+	public Boolean getMandatory() {
+		return mandatory;
+	}
+
+	public void setMandatory(Boolean mandatory) {
+		this.mandatory = mandatory;
+	}
+
+	public void setStringValue(String stringValue) {
+		this.stringValue = stringValue;
+	}
+
+	public void setNumberValue(Double numberValue) {
+		this.numberValue = numberValue;
+	}
+
+	public void setDateValue(Date dateValue) {
+		this.dateValue = dateValue;
+	}
+
+	@Override
+	public String toString() {
+		return "TemplateParameter [name=" + name + ", type=" + type + ", mandatory=" + mandatory + ", stringValue="
+				+ stringValue + ", numberValue=" + numberValue + ", dateValue=" + dateValue + ", code=" + code + "]";
+	}
+
+	public void setValue(Object value) {
+		if (value == null) {
+			return;
+		}
+		// 对参数类型进行处理
+		Class<?> clazz = value.getClass();
+		switch (type) {
+		case String:
+			stringValue = value.toString();
+			break;
+		case Number:
+			// 如果参数值的类型不是指定的类型的子类
+			// 如指定的是 Number.class,而参数值的类型是 Integer.class,是符合要求的
+			if (!Number.class.isAssignableFrom(clazz)) {
+				throw new IllegalArgumentException("参数值应为数值,实际为" + clazz.getName());
+			}
+			numberValue = ((Number) value).doubleValue();
+			break;
+		case Date:
+			if (clazz == Date.class) {
+				dateValue = (Date) value;
+			} else if (Number.class.isAssignableFrom(clazz)) {
+				dateValue = new Date(((Number) value).longValue());
+			} else {
+				throw new IllegalArgumentException("参数值应为时间戳,实际为" + clazz.getName());
+			}
+			break;
+		}
+	}
+
+	public Object getValue() {
+		switch (type) {
+		case String:
+			return stringValue;
+		case Number:
+			return numberValue;
+		case Date:
+			return dateValue;
+		default:
+			throw new IllegalArgumentException("未知的参数类型:" + type);
+		}
+	}
+
+	/**
+	 * 检查参数
+	 * 
+	 * @throws IllegalArgumentException
+	 *             必填参数没有填写时,报错
+	 */
+	public void checkValue() throws IllegalArgumentException {
+		// 检查参数值是否已填写
+		if (mandatory && ObjectUtils.isEmpty(getValue())) {
+			throw new IllegalArgumentException("需填写参数:code=" + code + ", name=" + name);
+		}
+	}
+
+	/**
+	 * 参数类型
+	 * 
+	 * @author sunyj
+	 * @since 2017年9月1日 下午8:01:53
+	 */
+	public enum Type {
+
+		/**
+		 * 字符串
+		 */
+		String,
+
+		/**
+		 * 数值
+		 */
+		Number,
+
+		/**
+		 * 日期,以时间戳的形式传入
+		 */
+		Date;
+
+	}
+
+}

+ 24 - 0
kanban-console/src/main/java/com/uas/kanban/service/DataSourceService.java

@@ -0,0 +1,24 @@
+package com.uas.kanban.service;
+
+import java.util.List;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.model.DataSource;
+
+/**
+ * 数据源
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:45:50
+ */
+public interface DataSourceService {
+
+	/**
+	 * 根据用户名查询数据
+	 * 
+	 * @param username
+	 *            用户名
+	 * @return 数据
+	 */
+	List<DataSource> getByUsername(@NotEmpty("username") String username);
+}

+ 54 - 0
kanban-console/src/main/java/com/uas/kanban/service/KanbanInstanceService.java

@@ -0,0 +1,54 @@
+package com.uas.kanban.service;
+
+import java.util.Map;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.exception.OperationException;
+
+/**
+ * 看板实例
+ * 
+ * @author sunyj
+ * @since 2017年9月2日 下午6:47:41
+ */
+public interface KanbanInstanceService {
+
+	/**
+	 * 填写参数
+	 * 
+	 * @param code
+	 *            看板实例 code
+	 * @param templateCode
+	 *            模版 code
+	 * @param values
+	 *            填写的参数值,key-value类型 <br/>
+	 *            (虽然看板实例中参数可为空,但是这里却不可为空,否则就不必调用该接口)
+	 *            <p/>
+	 *            <table border=1 cellpadding=5 cellspacing=0 summary=
+	 *            "Key and value">
+	 *            <tr>
+	 *            <th>key</th>
+	 *            <th>value</th>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>参数 code</td>
+	 *            <td>所填写的参数的值,须与参数类型一致
+	 *            {@link com.uas.kanban.model.TemplateParameter.Type}</td>
+	 *            </tr>
+	 *            </table>
+	 * @return 更新的看板实例数量
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 */
+	int fillParameters(@NotEmpty("code") String code, @NotEmpty("templateCode") String templateCode,
+			@NotEmpty("values") Map<String, Object> values) throws IllegalArgumentException, OperationException;
+
+	/**
+	 * 根据看板实例,解析生成json数据传给前台
+	 * 
+	 * @param code
+	 *            看板实例 code
+	 * @return 解析生成的json
+	 */
+	String parseData(@NotEmpty("code") String code);
+}

+ 55 - 0
kanban-console/src/main/java/com/uas/kanban/service/TemplateService.java

@@ -0,0 +1,55 @@
+package com.uas.kanban.service;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.TemplateParameter;
+
+/**
+ * 模版
+ * 
+ * @author sunyj
+ * @since 2017年9月2日 下午6:47:41
+ */
+public interface TemplateService {
+
+	/**
+	 * 添加模版参数
+	 * 
+	 * @param code
+	 *            模版的 code
+	 * @param parameter
+	 *            要添加的参数
+	 * @return 更新的模版数量
+	 */
+	int addParameter(@NotEmpty("code") String code, @NotEmpty("parameter") TemplateParameter parameter);
+
+	/**
+	 * 删除模版参数
+	 * 
+	 * @param code
+	 *            模版的 code
+	 * @param parameterCode
+	 *            模版参数的 code
+	 * @return 更新的模版数量
+	 * @throws IllegalStateException
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 */
+	int deleteParameter(@NotEmpty("code") String code, @NotEmpty("parameterCode") String parameterCode)
+			throws IllegalStateException, IllegalArgumentException, OperationException;
+
+	/**
+	 * 更新模版参数
+	 * 
+	 * @param code
+	 *            模版的 code
+	 * @param parameter
+	 *            要更新的参数
+	 * @return 更新的模版数量
+	 * @throws OperationException
+	 * @throws IllegalArgumentException
+	 * @throws IllegalStateException
+	 */
+	int updateParameter(@NotEmpty("code") String code, @NotEmpty("parameter") TemplateParameter parameter)
+			throws IllegalStateException, IllegalArgumentException, OperationException;
+}

+ 35 - 0
kanban-console/src/main/java/com/uas/kanban/service/impl/DataSourceServiceImpl.java

@@ -0,0 +1,35 @@
+package com.uas.kanban.service.impl;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseService;
+import com.uas.kanban.dao.DataSourceDao;
+import com.uas.kanban.model.DataSource;
+import com.uas.kanban.service.DataSourceService;
+
+/**
+ * 数据源
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午4:46:01
+ */
+@Service
+public class DataSourceServiceImpl extends BaseService<DataSource> implements DataSourceService {
+
+	@Autowired
+	private DataSourceDao dataSourceDao;
+
+	@Override
+	public List<DataSource> getByUsername(@NotEmpty("username") String username) {
+		Map<String, Object> filters = new HashMap<>();
+		filters.put("username =", username);
+		return dataSourceDao.find(filters);
+	}
+
+}

File diff suppressed because it is too large
+ 155 - 0
kanban-console/src/main/java/com/uas/kanban/service/impl/KanbanInstanceServiceImpl.java


+ 66 - 0
kanban-console/src/main/java/com/uas/kanban/service/impl/KanbanServiceImpl.java

@@ -0,0 +1,66 @@
+package com.uas.kanban.service.impl;
+
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseService;
+import com.uas.kanban.dao.KanbanDao;
+import com.uas.kanban.dao.TemplateDao;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.Kanban;
+import com.uas.kanban.model.Template;
+import com.uas.kanban.util.CollectionUtils;
+
+/**
+ * 看板
+ * 
+ * @author sunyj
+ * @since 2017年9月3日 下午4:25:43
+ */
+@Service
+public class KanbanServiceImpl extends BaseService<Kanban> {
+
+	@Autowired
+	private KanbanDao kanbanDao;
+
+	@Autowired
+	private TemplateDao templateDao;
+
+	@Override
+	public Kanban save(@NotEmpty("json") String json) {
+		Kanban kanban = kanbanDao.parse(json);
+		checkTemplates(kanban.getTemplateCodes());
+		return kanbanDao.save(kanban);
+	}
+
+	@Override
+	public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+		Kanban kanban = kanbanDao.parse(json);
+		checkTemplates(kanban.getTemplateCodes());
+		return kanbanDao.update(kanban);
+	}
+
+	/**
+	 * 检查模版是否存在
+	 * 
+	 * @param templateCodes
+	 *            模版的 code
+	 * @throws IllegalArgumentException
+	 *             模版不存在
+	 */
+	private void checkTemplates(Set<String> templateCodes) throws IllegalArgumentException {
+		if (CollectionUtils.isEmpty(templateCodes)) {
+			return;
+		}
+		for (String templateCode : templateCodes) {
+			Template template = templateDao.findOne(templateCode);
+			if (template == null) {
+				throw new IllegalArgumentException("模版不存在:" + templateCode);
+			}
+		}
+	}
+
+}

+ 160 - 0
kanban-console/src/main/java/com/uas/kanban/service/impl/TemplateServiceImpl.java

@@ -0,0 +1,160 @@
+package com.uas.kanban.service.impl;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.mongodb.morphia.query.Query;
+import org.mongodb.morphia.query.UpdateOperations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseService;
+import com.uas.kanban.dao.DataSourceDao;
+import com.uas.kanban.dao.TemplateDao;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.DataSource;
+import com.uas.kanban.model.Template;
+import com.uas.kanban.model.TemplateParameter;
+import com.uas.kanban.service.TemplateService;
+import com.uas.kanban.util.CollectionUtils;
+import com.uas.kanban.util.StringUtils;
+
+/**
+ * 模版
+ * 
+ * @author sunyj
+ * @since 2017年9月2日 下午6:54:58
+ */
+@Service
+public class TemplateServiceImpl extends BaseService<Template> implements TemplateService {
+
+	@Autowired
+	private TemplateDao templateDao;
+
+	@Autowired
+	private DataSourceDao dataSourceDao;
+
+	@Override
+	public Template save(@NotEmpty("json") String json) {
+		Template template = templateDao.parse(json);
+		checkDataSource(template.getDataSourceCode());
+		// 保存时,不允许指定参数的 code
+		List<TemplateParameter> parameters = template.getParameters();
+		if (!CollectionUtils.isEmpty(parameters)) {
+			for (TemplateParameter parameter : parameters) {
+				parameter.init();
+			}
+		}
+		return templateDao.save(template);
+	}
+
+	@Override
+	public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+		Template template = templateDao.parse(json);
+		String code = template.getCode();
+		if (StringUtils.isEmpty(code)) {
+			throw new IllegalArgumentException("没有指定code:" + template);
+		}
+		checkDataSource(template.getDataSourceCode());
+		List<TemplateParameter> parameters = template.getParameters();
+		if (!CollectionUtils.isEmpty(parameters)) {
+			throw new OperationException("请单独更新模版参数");
+		}
+		Set<String> ignoreFields = new HashSet<>();
+		// 不更新模版参数
+		ignoreFields.add("parameters");
+		UpdateOperations<Template> operations = templateDao.createUpdateOperations(template, ignoreFields);
+		return templateDao.update(code, operations);
+	}
+
+	/**
+	 * 检查数据源是否存在
+	 * 
+	 * @param dataSourceCode
+	 *            数据源的 code
+	 * @throws IllegalArgumentException
+	 *             数据源不存在
+	 */
+	private void checkDataSource(@NotEmpty("dataSourceCode") String dataSourceCode) throws IllegalArgumentException {
+		DataSource dataSource = dataSourceDao.findOne(dataSourceCode);
+		if (dataSource == null) {
+			throw new IllegalArgumentException("数据源不存在:" + dataSourceCode);
+		}
+	}
+
+	@Override
+	public int addParameter(@NotEmpty("code") String code, @NotEmpty("parameter") TemplateParameter parameter) {
+		parameter.init();
+		templateDao.checkField(parameter);
+		UpdateOperations<Template> operations = templateDao.createUpdateOperations();
+		operations.addToSet("parameters", parameter);
+		return templateDao.update(code, operations);
+	}
+
+	@Override
+	public int deleteParameter(@NotEmpty("code") String code, @NotEmpty("parameterCode") String parameterCode)
+			throws IllegalStateException, IllegalArgumentException, OperationException {
+		// 先获取模版
+		Template template = getTemplate(code, parameterCode);
+		if (template == null) {
+			return 0;
+		}
+		List<TemplateParameter> parameters = template.getParameters();
+		// 删除获取到的模版的指定参数,再更新到数据库
+		for (int i = parameters.size() - 1; i >= 0; i--) {
+			TemplateParameter parameter = parameters.get(i);
+			if (parameterCode.equals(parameter.getCode())) {
+				parameters.remove(i);
+			}
+		}
+		return templateDao.update(template);
+	}
+
+	/**
+	 * 根据模版的 code 和模版参数的 code 获取模版
+	 * 
+	 * @param code
+	 *            模版的 code
+	 * @param parameterCode
+	 *            模版参数的 code
+	 * @return 模版
+	 * @throws IllegalStateException
+	 */
+	private Template getTemplate(@NotEmpty("code") String code, @NotEmpty("parameterCode") String parameterCode)
+			throws IllegalStateException {
+		Query<Template> query = templateDao.createQuery();
+		query.field("code").equal(code);
+		query.field("parameters.code").equal(parameterCode);
+		List<Template> templates = templateDao.find(query);
+		if (CollectionUtils.isEmpty(templates)) {
+			return null;
+		} else if (templates.size() > 1) {
+			throw new IllegalStateException("存在不止一个模版");
+		}
+		return templates.get(0);
+	}
+
+	@Override
+	public int updateParameter(@NotEmpty("code") String code, @NotEmpty("parameter") TemplateParameter parameter)
+			throws IllegalStateException, IllegalArgumentException, OperationException {
+		String parameterCode = parameter.getCode();
+		templateDao.checkField(parameter);
+		// 先获取模版
+		Template template = getTemplate(code, parameterCode);
+		if (template == null) {
+			return 0;
+		}
+		List<TemplateParameter> parameters = template.getParameters();
+		// 删除获取到的模版的指定参数,再更新到数据库
+		for (int i = parameters.size() - 1; i >= 0; i--) {
+			TemplateParameter templateParameter = parameters.get(i);
+			if (parameterCode.equals(templateParameter.getCode())) {
+				parameters.set(i, parameter);
+			}
+		}
+		return templateDao.update(template);
+	}
+
+}

+ 23 - 0
kanban-console/src/main/resources/bootstrap.yml

@@ -0,0 +1,23 @@
+spring:
+ application:
+  name: kanban
+ cloud:
+  config:
+   uri: http://10.10.100.145:8080
+ http:
+  encoding:
+   force: true
+  multipart:
+   enabled: true
+   max-file-size: 512MB
+   max-request-size: 512MB
+
+security:
+ basic:
+  enabled: true
+  path: /console, /*/delete/all
+ user:
+  name: admin
+  password: select111***
+  role: ADMIN
+ ignored: false

+ 44 - 0
kanban-console/src/main/resources/logback.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+	<appender name="FILE"
+		class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<File>logs/log.log</File>
+		<encoder>
+			<pattern>
+				%date{yyyy-MM-dd HH:mm:ss:SSS} [%relative ms] %-5level [%50.50(%logger{36}.%method:%line)] ---- %msg%n
+			</pattern>
+			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
+		</encoder>
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<!-- daily rollover -->
+			<FileNamePattern>logs/log.%d{yyyy-MM-dd}.log</FileNamePattern>
+			<!-- keep 10 days' worth of history -->
+			<maxHistory>10</maxHistory>
+		</rollingPolicy>
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<level>INFO</level>
+		</filter>
+	</appender>
+
+	<!-- Console output -->
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
+		<encoder>
+			<pattern>
+				%date{yyyy-MM-dd HH:mm:ss:SSS} [%relative ms] %-5level [%50.50(%logger{36}.%method:%line)] ---- %msg%n
+			</pattern>
+			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
+		</encoder>
+		<!-- Only log level WARN and above -->
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<level>INFO</level>
+		</filter>
+	</appender>
+
+	<!-- Enable FILE and STDOUT appenders for all log messages. By default, 
+		only log at level INFO and above. -->
+	<root level="INFO">
+		<appender-ref ref="FILE" />
+		<appender-ref ref="STDOUT" />
+	</root>
+</configuration>

+ 77 - 0
kanban-console/src/main/webapp/WEB-INF/views/console.html

@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Kanban</title>
+<link rel="stylesheet" href="static/css/console.css">
+</head>
+<body>
+	<div id="mainContainer">
+		<ol>
+			<strong><li class="title">用户</li></strong>
+			<ol>
+				<li><a target="_blank">user/save?json={"name": "name","password": "password"}</a></li>
+				<li><a target="_blank">user/update?json={"code":"4EC2735D343","name": "name","password": "password"}</a></li>
+				<li><a target="_blank">user/delete/all</a></li>
+				<li><a target="_blank">user/delete/4EC2735D343</a></li>
+				<li><a target="_blank">user/get/all</a></li>
+				<li><a target="_blank">user/get/4EC2735D343</a></li>
+				<li><a target="_blank">user/get?page=1&size=10</a></li>
+			</ol>
+			<strong><li class="title">数据源</li></strong>
+			<ol>
+				<li><a target="_blank">datasource/save?json={"driverClassName": "driver_class_name","password": "password","url": "url","username": "username"}</a></li>
+				<li><a target="_blank">datasource/update?json={"driverClassName": "driver_class_name","code":"4EC2735D343","password": "password","url": "url","username": "username"}</a></li>
+				<li><a target="_blank">datasource/delete/all</a></li>
+				<li><a target="_blank">datasource/delete/4EC2735D343</a></li>
+				<li><a target="_blank">datasource/get/all</a></li>
+				<li><a target="_blank">datasource/get/4EC2735D343</a></li>
+				<li><a target="_blank">datasource/get?page=1&size=10</a></li>
+				<br/>
+				<li><a target="_blank">datasource/get/un/username_123</a></li>
+			</ol>
+			<strong><li class="title">模版</li></strong>
+			<ol>
+				<li><a target="_blank">template/save?json={"name": "name","content": "&lt;content&gt;&lt;/content&gt;","dataSourceCode":"4EC3C69D011","parameters":[{"name":"PU_NAME","type":"String","mandatory":true},{"name":"PU_DATE","type":"Date","mandatory":true}]}</a></li>
+				<li><a target="_blank">template/update?json={"code":"4EC2735D343","name": "name","content": "&lt;content&gt;&lt;/content&gt;","dataSourceCode":"4EC3C69D011"}</a></li>
+				<li><a target="_blank">template/delete/all</a></li>
+				<li><a target="_blank">template/delete/4EC2735D343</a></li>
+				<li><a target="_blank">template/get/all</a></li>
+				<li><a target="_blank">template/get/4EC2735D343</a></li>
+				<li><a target="_blank">template/get?page=1&size=10</a></li>
+				<br/>
+				<li><a target="_blank">template/parameter/add?code=4EC5D6B2716&amp;parameter={"mandatory": true,"name": "PU_NAME","type": "String"}</a></li>
+				<li><a target="_blank">template/parameter/delete?code=4EC5D6B2716&amp;parameterCode=4EC9340EE11</a></li>
+				<li><a target="_blank">template/parameter/update?code=4EC5D6B2716&amp;parameter={"code": "4EC9340EE11","mandatory": true,"name": "PU_NAME","type": "String"}</a></li>
+			</ol>
+			<strong><li class="title">看版</li></strong>
+			<ol>
+				<li><a target="_blank">kanban/save?json={"name": "name","templateCodes":["4ED1EA76E16","4ED1EA1CE13","4F15DBE5B16"]}</a></li>
+				<!-- "templateCodes":"4ED1EA76E16" 或 "templateCodes":["4ED1EA76E16"]-->
+				<li><a target="_blank">kanban/update?json={"code":"4F15E57841D","name": "name","templateCodes":["4ED1EA76E16","4ED1EA1CE13"]}</a></li>
+				<li><a target="_blank">kanban/delete/all</a></li>
+				<li><a target="_blank">kanban/delete/4EC2735D343</a></li>
+				<li><a target="_blank">kanban/get/all</a></li>
+				<li><a target="_blank">kanban/get/4EC2735D343</a></li>
+				<li><a target="_blank">kanban/get?page=1&size=10</a></li>
+			</ol>
+			<strong><li class="title">看版实例</li></strong>
+			<ol>
+				<li><a target="_blank">kanbanInstance/save?json={"kanbanCode": "4F16BC4A813","display":"AutoSwitch"}</a></li>
+				<li><a target="_blank">kanbanInstance/update?json={"code":"4F15E57841D","kanbanCode": "4F16BC4A813","display":"SplitScreen","switchFrequent":5000}</a></li>
+				<li><a target="_blank">kanbanInstance/delete/all</a></li>
+				<li><a target="_blank">kanbanInstance/delete/4EC2735D343</a></li>
+				<li><a target="_blank">kanbanInstance/get/all</a></li>
+				<li><a target="_blank">kanbanInstance/get/4EC2735D343</a></li>
+				<li><a target="_blank">kanbanInstance/get?page=1&size=10</a></li>
+				<br/>
+				<li><a target="_blank">kanbanInstance/fillParameters?code=4F6BDFB4213&templateCode=4ED1EA76E16&values={"4ED1EA76D15": "789","4F123A93312": 45,"4F123D86313":1504530537000}</a></li>
+				<li><a target="_blank">kanbanInstance/parseData/4F6BDFB4213</a></li>
+			</ol>
+		</ol>
+	</div>
+</body>
+
+<script src="static/lib/jquery/jquery.min.js"></script>
+<script src="static/js/console/app.js"></script>
+</html>

+ 27 - 0
kanban-console/src/main/webapp/WEB-INF/views/fileUpload.html

@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>文件上传</title>
+<link rel="stylesheet" href="static/css/fileUpload.css">
+<link rel="stylesheet"
+	href="static/lib/fontawesome/css/font-awesome.min.css">
+</head>
+<body>
+	<form method="post" enctype="multipart/form-data">
+		选择要上传的文件<br /> <br /> <input id="file" type="file" name="file" /><br />
+		<br />
+	</form>
+	<button id="upload">上传</button>
+	<br />
+	<div id="progressDiv" hidden="true">
+		<br /> 上传进度:
+		<progress></progress>
+		<br />
+		<p id="progress">0 bytes</p>
+	</div>
+	<p id="result"></p>
+</body>
+<script src="static/lib/jquery/jquery.min.js"></script>
+<script src="static/js/upload/app.js"></script>
+</html>

+ 10 - 0
kanban-console/src/main/webapp/WEB-INF/views/index.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Kanban</title>
+</head>
+<body>
+	<p>Kanban Home</p>
+</body>
+</html>

+ 17 - 0
kanban-console/src/main/webapp/resources/css/console.css

@@ -0,0 +1,17 @@
+body {
+	font-family: courier;
+}
+
+#mainContainer {
+	margin: 20px;
+}
+
+li.title {
+	margin-top: 20px;
+}
+
+li {
+	margin-top: 10px;
+	margin-bottom: 10px;
+	color: #404040;
+}

+ 15 - 0
kanban-console/src/main/webapp/resources/css/fileUpload.css

@@ -0,0 +1,15 @@
+* {
+	font-family: microsoft yahei ui;
+}
+
+body {
+	margin: 20px;
+}
+
+#upload {
+	padding: 1px 15px 1px 15px;
+	cursor: pointer;
+	outline: none;
+	border-radius: 10%;
+	background-color: #eee;
+}

+ 3 - 0
kanban-console/src/main/webapp/resources/js/console/app.js

@@ -0,0 +1,3 @@
+$("a").attr("href", function() {
+	return this.textContent;
+});

+ 62 - 0
kanban-console/src/main/webapp/resources/js/upload/app.js

@@ -0,0 +1,62 @@
+//上传文件最大值(MB)
+var fileMaxSize = 512;
+$("#upload").click(upload);
+
+/**
+ * 上传文件
+ */
+function upload() {
+	$('#result').html("");
+	var input = document.getElementById("file");
+	if (!input.value) {
+		$('#progressDiv').css("display", "hidden");
+		$("#result").html("文件为空,无法进行上传!");
+		return;
+	}
+	if (input.files[0].size > fileMaxSize * 1024 * 1024) {
+		$('#progressDiv').css("display", "hidden");
+		$("#result").html("文件大小不能超过" + fileMaxSize + "MB!");
+		return;
+	}
+	// 创建FormData对象,初始化为form表单中的数据
+	var formData = new FormData($('form')[0]);
+
+	// 显示进度
+	$('#progressDiv').css("display", "block");
+	// $('#progressDiv').toggle();
+	// ajax异步上传
+	$.ajax({
+		url : "upload",
+		type : "POST",
+		data : formData,
+		xhr : function() {
+			myXhr = $.ajaxSettings.xhr();
+			if (myXhr.upload) {
+				// 绑定progress事件的回调函数
+				myXhr.upload.addEventListener('progress',
+						progressHandlingFunction, false);
+			}
+			return myXhr;
+		},
+		success : function(result) {
+			$("#result").html(result);
+		},
+		contentType : false,
+		processData : false
+	});
+
+}
+
+// 上传进度回调函数:
+function progressHandlingFunction(e) {
+	if (e.lengthComputable) {
+		$('progress').attr({
+			value : e.loaded,
+			max : e.total
+		}); // 更新数据到进度条
+		var percent = e.loaded / e.total * 100;
+		$('#progress').html(
+				e.loaded + "/" + e.total + " bytes. " + percent.toFixed(2)
+						+ "%");
+	}
+}

+ 88 - 0
kanban-console/src/main/webapp/resources/js/util/utils.js

@@ -0,0 +1,88 @@
+document.write("<script src='static/lib/spin/spin.min.js'></script>");
+
+/**
+ * 获取链接参数
+ * 
+ * @param key
+ *            参数的键
+ * @returns 参数的值
+ */
+function getParameter(key) {
+	var reg = new RegExp("(^|&)" + key + "=([^&]*)(&|$)");
+	var r = window.location.search.substr(1).match(reg);
+	if (r != null)
+		return decodeURI(r[2]);
+	return null;
+};
+
+/**
+ * 显示正在载入的动画
+ * 
+ * @param spinner
+ *            载入的动画
+ * @param container
+ *            动画所在的容器
+ * @returns 动画
+ */
+function showLoading(spinner, container) {
+	if (spinner) {
+		return spinner;
+	}
+	var opts = {
+		lines : 9 // The number of lines to draw
+		,
+		length : 17 // The length of each line
+		,
+		width : 10 // The line thickness
+		,
+		radius : 26 // The radius of the inner circle
+		,
+		scale : 1 // Scales overall size of the spinner
+		,
+		corners : 1 // Corner roundness (0..1)
+		,
+		color : '#000' // #rgb or #rrggbb or array of colors
+		,
+		opacity : 0.5 // Opacity of the lines
+		,
+		rotate : 0 // The rotation offset
+		,
+		direction : 1 // 1: clockwise, -1: counterclockwise
+		,
+		speed : 1 // Rounds per second
+		,
+		trail : 60 // Afterglow percentage
+		,
+		fps : 20 // Frames per second when using setTimeout() as a fallback
+		// for CSS
+		,
+		zIndex : 2e9 // The z-index (defaults to 2000000000)
+		,
+		className : 'spinner' // The CSS class to assign to the spinner
+		,
+		top : '50%' // Top position relative to parent
+		,
+		left : '50%' // Left position relative to parent
+		,
+		shadow : false // Whether to render a shadow
+		,
+		hwaccel : false // Whether to use hardware acceleration
+		,
+		position : 'absolute' // Element positioning
+	}
+	return new Spinner(opts).spin(container);
+}
+
+/**
+ * 关闭正在载入的动画
+ * 
+ * @param spinner
+ *            载入的动画
+ * @returns 动画
+ */
+function hideLoading(spinner) {
+	if (spinner) {
+		spinner.stop();
+	}
+	return null;
+}

+ 2086 - 0
kanban-console/src/main/webapp/resources/lib/fontawesome/css/font-awesome.css

@@ -0,0 +1,2086 @@
+/*!
+ *  Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
+ *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+/* FONT PATH
+ * -------------------------- */
+@font-face {
+  font-family: 'FontAwesome';
+  src: url('../fonts/fontawesome-webfont.eot?v=4.5.0');
+  src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');
+  font-weight: normal;
+  font-style: normal;
+}
+.fa {
+  display: inline-block;
+  font: normal normal normal 14px/1 FontAwesome;
+  font-size: inherit;
+  text-rendering: auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+/* makes the font 33% larger relative to the icon container */
+.fa-lg {
+  font-size: 1.33333333em;
+  line-height: 0.75em;
+  vertical-align: -15%;
+}
+.fa-2x {
+  font-size: 2em;
+}
+.fa-3x {
+  font-size: 3em;
+}
+.fa-4x {
+  font-size: 4em;
+}
+.fa-5x {
+  font-size: 5em;
+}
+.fa-fw {
+  width: 1.28571429em;
+  text-align: center;
+}
+.fa-ul {
+  padding-left: 0;
+  margin-left: 2.14285714em;
+  list-style-type: none;
+}
+.fa-ul > li {
+  position: relative;
+}
+.fa-li {
+  position: absolute;
+  left: -2.14285714em;
+  width: 2.14285714em;
+  top: 0.14285714em;
+  text-align: center;
+}
+.fa-li.fa-lg {
+  left: -1.85714286em;
+}
+.fa-border {
+  padding: .2em .25em .15em;
+  border: solid 0.08em #eeeeee;
+  border-radius: .1em;
+}
+.fa-pull-left {
+  float: left;
+}
+.fa-pull-right {
+  float: right;
+}
+.fa.fa-pull-left {
+  margin-right: .3em;
+}
+.fa.fa-pull-right {
+  margin-left: .3em;
+}
+/* Deprecated as of 4.4.0 */
+.pull-right {
+  float: right;
+}
+.pull-left {
+  float: left;
+}
+.fa.pull-left {
+  margin-right: .3em;
+}
+.fa.pull-right {
+  margin-left: .3em;
+}
+.fa-spin {
+  -webkit-animation: fa-spin 2s infinite linear;
+  animation: fa-spin 2s infinite linear;
+}
+.fa-pulse {
+  -webkit-animation: fa-spin 1s infinite steps(8);
+  animation: fa-spin 1s infinite steps(8);
+}
+@-webkit-keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(359deg);
+    transform: rotate(359deg);
+  }
+}
+@keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(359deg);
+    transform: rotate(359deg);
+  }
+}
+.fa-rotate-90 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
+  -webkit-transform: rotate(90deg);
+  -ms-transform: rotate(90deg);
+  transform: rotate(90deg);
+}
+.fa-rotate-180 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
+  -webkit-transform: rotate(180deg);
+  -ms-transform: rotate(180deg);
+  transform: rotate(180deg);
+}
+.fa-rotate-270 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
+  -webkit-transform: rotate(270deg);
+  -ms-transform: rotate(270deg);
+  transform: rotate(270deg);
+}
+.fa-flip-horizontal {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);
+  -webkit-transform: scale(-1, 1);
+  -ms-transform: scale(-1, 1);
+  transform: scale(-1, 1);
+}
+.fa-flip-vertical {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);
+  -webkit-transform: scale(1, -1);
+  -ms-transform: scale(1, -1);
+  transform: scale(1, -1);
+}
+:root .fa-rotate-90,
+:root .fa-rotate-180,
+:root .fa-rotate-270,
+:root .fa-flip-horizontal,
+:root .fa-flip-vertical {
+  filter: none;
+}
+.fa-stack {
+  position: relative;
+  display: inline-block;
+  width: 2em;
+  height: 2em;
+  line-height: 2em;
+  vertical-align: middle;
+}
+.fa-stack-1x,
+.fa-stack-2x {
+  position: absolute;
+  left: 0;
+  width: 100%;
+  text-align: center;
+}
+.fa-stack-1x {
+  line-height: inherit;
+}
+.fa-stack-2x {
+  font-size: 2em;
+}
+.fa-inverse {
+  color: #ffffff;
+}
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+   readers do not read off random characters that represent icons */
+.fa-glass:before {
+  content: "\f000";
+}
+.fa-music:before {
+  content: "\f001";
+}
+.fa-search:before {
+  content: "\f002";
+}
+.fa-envelope-o:before {
+  content: "\f003";
+}
+.fa-heart:before {
+  content: "\f004";
+}
+.fa-star:before {
+  content: "\f005";
+}
+.fa-star-o:before {
+  content: "\f006";
+}
+.fa-user:before {
+  content: "\f007";
+}
+.fa-film:before {
+  content: "\f008";
+}
+.fa-th-large:before {
+  content: "\f009";
+}
+.fa-th:before {
+  content: "\f00a";
+}
+.fa-th-list:before {
+  content: "\f00b";
+}
+.fa-check:before {
+  content: "\f00c";
+}
+.fa-remove:before,
+.fa-close:before,
+.fa-times:before {
+  content: "\f00d";
+}
+.fa-search-plus:before {
+  content: "\f00e";
+}
+.fa-search-minus:before {
+  content: "\f010";
+}
+.fa-power-off:before {
+  content: "\f011";
+}
+.fa-signal:before {
+  content: "\f012";
+}
+.fa-gear:before,
+.fa-cog:before {
+  content: "\f013";
+}
+.fa-trash-o:before {
+  content: "\f014";
+}
+.fa-home:before {
+  content: "\f015";
+}
+.fa-file-o:before {
+  content: "\f016";
+}
+.fa-clock-o:before {
+  content: "\f017";
+}
+.fa-road:before {
+  content: "\f018";
+}
+.fa-download:before {
+  content: "\f019";
+}
+.fa-arrow-circle-o-down:before {
+  content: "\f01a";
+}
+.fa-arrow-circle-o-up:before {
+  content: "\f01b";
+}
+.fa-inbox:before {
+  content: "\f01c";
+}
+.fa-play-circle-o:before {
+  content: "\f01d";
+}
+.fa-rotate-right:before,
+.fa-repeat:before {
+  content: "\f01e";
+}
+.fa-refresh:before {
+  content: "\f021";
+}
+.fa-list-alt:before {
+  content: "\f022";
+}
+.fa-lock:before {
+  content: "\f023";
+}
+.fa-flag:before {
+  content: "\f024";
+}
+.fa-headphones:before {
+  content: "\f025";
+}
+.fa-volume-off:before {
+  content: "\f026";
+}
+.fa-volume-down:before {
+  content: "\f027";
+}
+.fa-volume-up:before {
+  content: "\f028";
+}
+.fa-qrcode:before {
+  content: "\f029";
+}
+.fa-barcode:before {
+  content: "\f02a";
+}
+.fa-tag:before {
+  content: "\f02b";
+}
+.fa-tags:before {
+  content: "\f02c";
+}
+.fa-book:before {
+  content: "\f02d";
+}
+.fa-bookmark:before {
+  content: "\f02e";
+}
+.fa-print:before {
+  content: "\f02f";
+}
+.fa-camera:before {
+  content: "\f030";
+}
+.fa-font:before {
+  content: "\f031";
+}
+.fa-bold:before {
+  content: "\f032";
+}
+.fa-italic:before {
+  content: "\f033";
+}
+.fa-text-height:before {
+  content: "\f034";
+}
+.fa-text-width:before {
+  content: "\f035";
+}
+.fa-align-left:before {
+  content: "\f036";
+}
+.fa-align-center:before {
+  content: "\f037";
+}
+.fa-align-right:before {
+  content: "\f038";
+}
+.fa-align-justify:before {
+  content: "\f039";
+}
+.fa-list:before {
+  content: "\f03a";
+}
+.fa-dedent:before,
+.fa-outdent:before {
+  content: "\f03b";
+}
+.fa-indent:before {
+  content: "\f03c";
+}
+.fa-video-camera:before {
+  content: "\f03d";
+}
+.fa-photo:before,
+.fa-image:before,
+.fa-picture-o:before {
+  content: "\f03e";
+}
+.fa-pencil:before {
+  content: "\f040";
+}
+.fa-map-marker:before {
+  content: "\f041";
+}
+.fa-adjust:before {
+  content: "\f042";
+}
+.fa-tint:before {
+  content: "\f043";
+}
+.fa-edit:before,
+.fa-pencil-square-o:before {
+  content: "\f044";
+}
+.fa-share-square-o:before {
+  content: "\f045";
+}
+.fa-check-square-o:before {
+  content: "\f046";
+}
+.fa-arrows:before {
+  content: "\f047";
+}
+.fa-step-backward:before {
+  content: "\f048";
+}
+.fa-fast-backward:before {
+  content: "\f049";
+}
+.fa-backward:before {
+  content: "\f04a";
+}
+.fa-play:before {
+  content: "\f04b";
+}
+.fa-pause:before {
+  content: "\f04c";
+}
+.fa-stop:before {
+  content: "\f04d";
+}
+.fa-forward:before {
+  content: "\f04e";
+}
+.fa-fast-forward:before {
+  content: "\f050";
+}
+.fa-step-forward:before {
+  content: "\f051";
+}
+.fa-eject:before {
+  content: "\f052";
+}
+.fa-chevron-left:before {
+  content: "\f053";
+}
+.fa-chevron-right:before {
+  content: "\f054";
+}
+.fa-plus-circle:before {
+  content: "\f055";
+}
+.fa-minus-circle:before {
+  content: "\f056";
+}
+.fa-times-circle:before {
+  content: "\f057";
+}
+.fa-check-circle:before {
+  content: "\f058";
+}
+.fa-question-circle:before {
+  content: "\f059";
+}
+.fa-info-circle:before {
+  content: "\f05a";
+}
+.fa-crosshairs:before {
+  content: "\f05b";
+}
+.fa-times-circle-o:before {
+  content: "\f05c";
+}
+.fa-check-circle-o:before {
+  content: "\f05d";
+}
+.fa-ban:before {
+  content: "\f05e";
+}
+.fa-arrow-left:before {
+  content: "\f060";
+}
+.fa-arrow-right:before {
+  content: "\f061";
+}
+.fa-arrow-up:before {
+  content: "\f062";
+}
+.fa-arrow-down:before {
+  content: "\f063";
+}
+.fa-mail-forward:before,
+.fa-share:before {
+  content: "\f064";
+}
+.fa-expand:before {
+  content: "\f065";
+}
+.fa-compress:before {
+  content: "\f066";
+}
+.fa-plus:before {
+  content: "\f067";
+}
+.fa-minus:before {
+  content: "\f068";
+}
+.fa-asterisk:before {
+  content: "\f069";
+}
+.fa-exclamation-circle:before {
+  content: "\f06a";
+}
+.fa-gift:before {
+  content: "\f06b";
+}
+.fa-leaf:before {
+  content: "\f06c";
+}
+.fa-fire:before {
+  content: "\f06d";
+}
+.fa-eye:before {
+  content: "\f06e";
+}
+.fa-eye-slash:before {
+  content: "\f070";
+}
+.fa-warning:before,
+.fa-exclamation-triangle:before {
+  content: "\f071";
+}
+.fa-plane:before {
+  content: "\f072";
+}
+.fa-calendar:before {
+  content: "\f073";
+}
+.fa-random:before {
+  content: "\f074";
+}
+.fa-comment:before {
+  content: "\f075";
+}
+.fa-magnet:before {
+  content: "\f076";
+}
+.fa-chevron-up:before {
+  content: "\f077";
+}
+.fa-chevron-down:before {
+  content: "\f078";
+}
+.fa-retweet:before {
+  content: "\f079";
+}
+.fa-shopping-cart:before {
+  content: "\f07a";
+}
+.fa-folder:before {
+  content: "\f07b";
+}
+.fa-folder-open:before {
+  content: "\f07c";
+}
+.fa-arrows-v:before {
+  content: "\f07d";
+}
+.fa-arrows-h:before {
+  content: "\f07e";
+}
+.fa-bar-chart-o:before,
+.fa-bar-chart:before {
+  content: "\f080";
+}
+.fa-twitter-square:before {
+  content: "\f081";
+}
+.fa-facebook-square:before {
+  content: "\f082";
+}
+.fa-camera-retro:before {
+  content: "\f083";
+}
+.fa-key:before {
+  content: "\f084";
+}
+.fa-gears:before,
+.fa-cogs:before {
+  content: "\f085";
+}
+.fa-comments:before {
+  content: "\f086";
+}
+.fa-thumbs-o-up:before {
+  content: "\f087";
+}
+.fa-thumbs-o-down:before {
+  content: "\f088";
+}
+.fa-star-half:before {
+  content: "\f089";
+}
+.fa-heart-o:before {
+  content: "\f08a";
+}
+.fa-sign-out:before {
+  content: "\f08b";
+}
+.fa-linkedin-square:before {
+  content: "\f08c";
+}
+.fa-thumb-tack:before {
+  content: "\f08d";
+}
+.fa-external-link:before {
+  content: "\f08e";
+}
+.fa-sign-in:before {
+  content: "\f090";
+}
+.fa-trophy:before {
+  content: "\f091";
+}
+.fa-github-square:before {
+  content: "\f092";
+}
+.fa-upload:before {
+  content: "\f093";
+}
+.fa-lemon-o:before {
+  content: "\f094";
+}
+.fa-phone:before {
+  content: "\f095";
+}
+.fa-square-o:before {
+  content: "\f096";
+}
+.fa-bookmark-o:before {
+  content: "\f097";
+}
+.fa-phone-square:before {
+  content: "\f098";
+}
+.fa-twitter:before {
+  content: "\f099";
+}
+.fa-facebook-f:before,
+.fa-facebook:before {
+  content: "\f09a";
+}
+.fa-github:before {
+  content: "\f09b";
+}
+.fa-unlock:before {
+  content: "\f09c";
+}
+.fa-credit-card:before {
+  content: "\f09d";
+}
+.fa-feed:before,
+.fa-rss:before {
+  content: "\f09e";
+}
+.fa-hdd-o:before {
+  content: "\f0a0";
+}
+.fa-bullhorn:before {
+  content: "\f0a1";
+}
+.fa-bell:before {
+  content: "\f0f3";
+}
+.fa-certificate:before {
+  content: "\f0a3";
+}
+.fa-hand-o-right:before {
+  content: "\f0a4";
+}
+.fa-hand-o-left:before {
+  content: "\f0a5";
+}
+.fa-hand-o-up:before {
+  content: "\f0a6";
+}
+.fa-hand-o-down:before {
+  content: "\f0a7";
+}
+.fa-arrow-circle-left:before {
+  content: "\f0a8";
+}
+.fa-arrow-circle-right:before {
+  content: "\f0a9";
+}
+.fa-arrow-circle-up:before {
+  content: "\f0aa";
+}
+.fa-arrow-circle-down:before {
+  content: "\f0ab";
+}
+.fa-globe:before {
+  content: "\f0ac";
+}
+.fa-wrench:before {
+  content: "\f0ad";
+}
+.fa-tasks:before {
+  content: "\f0ae";
+}
+.fa-filter:before {
+  content: "\f0b0";
+}
+.fa-briefcase:before {
+  content: "\f0b1";
+}
+.fa-arrows-alt:before {
+  content: "\f0b2";
+}
+.fa-group:before,
+.fa-users:before {
+  content: "\f0c0";
+}
+.fa-chain:before,
+.fa-link:before {
+  content: "\f0c1";
+}
+.fa-cloud:before {
+  content: "\f0c2";
+}
+.fa-flask:before {
+  content: "\f0c3";
+}
+.fa-cut:before,
+.fa-scissors:before {
+  content: "\f0c4";
+}
+.fa-copy:before,
+.fa-files-o:before {
+  content: "\f0c5";
+}
+.fa-paperclip:before {
+  content: "\f0c6";
+}
+.fa-save:before,
+.fa-floppy-o:before {
+  content: "\f0c7";
+}
+.fa-square:before {
+  content: "\f0c8";
+}
+.fa-navicon:before,
+.fa-reorder:before,
+.fa-bars:before {
+  content: "\f0c9";
+}
+.fa-list-ul:before {
+  content: "\f0ca";
+}
+.fa-list-ol:before {
+  content: "\f0cb";
+}
+.fa-strikethrough:before {
+  content: "\f0cc";
+}
+.fa-underline:before {
+  content: "\f0cd";
+}
+.fa-table:before {
+  content: "\f0ce";
+}
+.fa-magic:before {
+  content: "\f0d0";
+}
+.fa-truck:before {
+  content: "\f0d1";
+}
+.fa-pinterest:before {
+  content: "\f0d2";
+}
+.fa-pinterest-square:before {
+  content: "\f0d3";
+}
+.fa-google-plus-square:before {
+  content: "\f0d4";
+}
+.fa-google-plus:before {
+  content: "\f0d5";
+}
+.fa-money:before {
+  content: "\f0d6";
+}
+.fa-caret-down:before {
+  content: "\f0d7";
+}
+.fa-caret-up:before {
+  content: "\f0d8";
+}
+.fa-caret-left:before {
+  content: "\f0d9";
+}
+.fa-caret-right:before {
+  content: "\f0da";
+}
+.fa-columns:before {
+  content: "\f0db";
+}
+.fa-unsorted:before,
+.fa-sort:before {
+  content: "\f0dc";
+}
+.fa-sort-down:before,
+.fa-sort-desc:before {
+  content: "\f0dd";
+}
+.fa-sort-up:before,
+.fa-sort-asc:before {
+  content: "\f0de";
+}
+.fa-envelope:before {
+  content: "\f0e0";
+}
+.fa-linkedin:before {
+  content: "\f0e1";
+}
+.fa-rotate-left:before,
+.fa-undo:before {
+  content: "\f0e2";
+}
+.fa-legal:before,
+.fa-gavel:before {
+  content: "\f0e3";
+}
+.fa-dashboard:before,
+.fa-tachometer:before {
+  content: "\f0e4";
+}
+.fa-comment-o:before {
+  content: "\f0e5";
+}
+.fa-comments-o:before {
+  content: "\f0e6";
+}
+.fa-flash:before,
+.fa-bolt:before {
+  content: "\f0e7";
+}
+.fa-sitemap:before {
+  content: "\f0e8";
+}
+.fa-umbrella:before {
+  content: "\f0e9";
+}
+.fa-paste:before,
+.fa-clipboard:before {
+  content: "\f0ea";
+}
+.fa-lightbulb-o:before {
+  content: "\f0eb";
+}
+.fa-exchange:before {
+  content: "\f0ec";
+}
+.fa-cloud-download:before {
+  content: "\f0ed";
+}
+.fa-cloud-upload:before {
+  content: "\f0ee";
+}
+.fa-user-md:before {
+  content: "\f0f0";
+}
+.fa-stethoscope:before {
+  content: "\f0f1";
+}
+.fa-suitcase:before {
+  content: "\f0f2";
+}
+.fa-bell-o:before {
+  content: "\f0a2";
+}
+.fa-coffee:before {
+  content: "\f0f4";
+}
+.fa-cutlery:before {
+  content: "\f0f5";
+}
+.fa-file-text-o:before {
+  content: "\f0f6";
+}
+.fa-building-o:before {
+  content: "\f0f7";
+}
+.fa-hospital-o:before {
+  content: "\f0f8";
+}
+.fa-ambulance:before {
+  content: "\f0f9";
+}
+.fa-medkit:before {
+  content: "\f0fa";
+}
+.fa-fighter-jet:before {
+  content: "\f0fb";
+}
+.fa-beer:before {
+  content: "\f0fc";
+}
+.fa-h-square:before {
+  content: "\f0fd";
+}
+.fa-plus-square:before {
+  content: "\f0fe";
+}
+.fa-angle-double-left:before {
+  content: "\f100";
+}
+.fa-angle-double-right:before {
+  content: "\f101";
+}
+.fa-angle-double-up:before {
+  content: "\f102";
+}
+.fa-angle-double-down:before {
+  content: "\f103";
+}
+.fa-angle-left:before {
+  content: "\f104";
+}
+.fa-angle-right:before {
+  content: "\f105";
+}
+.fa-angle-up:before {
+  content: "\f106";
+}
+.fa-angle-down:before {
+  content: "\f107";
+}
+.fa-desktop:before {
+  content: "\f108";
+}
+.fa-laptop:before {
+  content: "\f109";
+}
+.fa-tablet:before {
+  content: "\f10a";
+}
+.fa-mobile-phone:before,
+.fa-mobile:before {
+  content: "\f10b";
+}
+.fa-circle-o:before {
+  content: "\f10c";
+}
+.fa-quote-left:before {
+  content: "\f10d";
+}
+.fa-quote-right:before {
+  content: "\f10e";
+}
+.fa-spinner:before {
+  content: "\f110";
+}
+.fa-circle:before {
+  content: "\f111";
+}
+.fa-mail-reply:before,
+.fa-reply:before {
+  content: "\f112";
+}
+.fa-github-alt:before {
+  content: "\f113";
+}
+.fa-folder-o:before {
+  content: "\f114";
+}
+.fa-folder-open-o:before {
+  content: "\f115";
+}
+.fa-smile-o:before {
+  content: "\f118";
+}
+.fa-frown-o:before {
+  content: "\f119";
+}
+.fa-meh-o:before {
+  content: "\f11a";
+}
+.fa-gamepad:before {
+  content: "\f11b";
+}
+.fa-keyboard-o:before {
+  content: "\f11c";
+}
+.fa-flag-o:before {
+  content: "\f11d";
+}
+.fa-flag-checkered:before {
+  content: "\f11e";
+}
+.fa-terminal:before {
+  content: "\f120";
+}
+.fa-code:before {
+  content: "\f121";
+}
+.fa-mail-reply-all:before,
+.fa-reply-all:before {
+  content: "\f122";
+}
+.fa-star-half-empty:before,
+.fa-star-half-full:before,
+.fa-star-half-o:before {
+  content: "\f123";
+}
+.fa-location-arrow:before {
+  content: "\f124";
+}
+.fa-crop:before {
+  content: "\f125";
+}
+.fa-code-fork:before {
+  content: "\f126";
+}
+.fa-unlink:before,
+.fa-chain-broken:before {
+  content: "\f127";
+}
+.fa-question:before {
+  content: "\f128";
+}
+.fa-info:before {
+  content: "\f129";
+}
+.fa-exclamation:before {
+  content: "\f12a";
+}
+.fa-superscript:before {
+  content: "\f12b";
+}
+.fa-subscript:before {
+  content: "\f12c";
+}
+.fa-eraser:before {
+  content: "\f12d";
+}
+.fa-puzzle-piece:before {
+  content: "\f12e";
+}
+.fa-microphone:before {
+  content: "\f130";
+}
+.fa-microphone-slash:before {
+  content: "\f131";
+}
+.fa-shield:before {
+  content: "\f132";
+}
+.fa-calendar-o:before {
+  content: "\f133";
+}
+.fa-fire-extinguisher:before {
+  content: "\f134";
+}
+.fa-rocket:before {
+  content: "\f135";
+}
+.fa-maxcdn:before {
+  content: "\f136";
+}
+.fa-chevron-circle-left:before {
+  content: "\f137";
+}
+.fa-chevron-circle-right:before {
+  content: "\f138";
+}
+.fa-chevron-circle-up:before {
+  content: "\f139";
+}
+.fa-chevron-circle-down:before {
+  content: "\f13a";
+}
+.fa-html5:before {
+  content: "\f13b";
+}
+.fa-css3:before {
+  content: "\f13c";
+}
+.fa-anchor:before {
+  content: "\f13d";
+}
+.fa-unlock-alt:before {
+  content: "\f13e";
+}
+.fa-bullseye:before {
+  content: "\f140";
+}
+.fa-ellipsis-h:before {
+  content: "\f141";
+}
+.fa-ellipsis-v:before {
+  content: "\f142";
+}
+.fa-rss-square:before {
+  content: "\f143";
+}
+.fa-play-circle:before {
+  content: "\f144";
+}
+.fa-ticket:before {
+  content: "\f145";
+}
+.fa-minus-square:before {
+  content: "\f146";
+}
+.fa-minus-square-o:before {
+  content: "\f147";
+}
+.fa-level-up:before {
+  content: "\f148";
+}
+.fa-level-down:before {
+  content: "\f149";
+}
+.fa-check-square:before {
+  content: "\f14a";
+}
+.fa-pencil-square:before {
+  content: "\f14b";
+}
+.fa-external-link-square:before {
+  content: "\f14c";
+}
+.fa-share-square:before {
+  content: "\f14d";
+}
+.fa-compass:before {
+  content: "\f14e";
+}
+.fa-toggle-down:before,
+.fa-caret-square-o-down:before {
+  content: "\f150";
+}
+.fa-toggle-up:before,
+.fa-caret-square-o-up:before {
+  content: "\f151";
+}
+.fa-toggle-right:before,
+.fa-caret-square-o-right:before {
+  content: "\f152";
+}
+.fa-euro:before,
+.fa-eur:before {
+  content: "\f153";
+}
+.fa-gbp:before {
+  content: "\f154";
+}
+.fa-dollar:before,
+.fa-usd:before {
+  content: "\f155";
+}
+.fa-rupee:before,
+.fa-inr:before {
+  content: "\f156";
+}
+.fa-cny:before,
+.fa-rmb:before,
+.fa-yen:before,
+.fa-jpy:before {
+  content: "\f157";
+}
+.fa-ruble:before,
+.fa-rouble:before,
+.fa-rub:before {
+  content: "\f158";
+}
+.fa-won:before,
+.fa-krw:before {
+  content: "\f159";
+}
+.fa-bitcoin:before,
+.fa-btc:before {
+  content: "\f15a";
+}
+.fa-file:before {
+  content: "\f15b";
+}
+.fa-file-text:before {
+  content: "\f15c";
+}
+.fa-sort-alpha-asc:before {
+  content: "\f15d";
+}
+.fa-sort-alpha-desc:before {
+  content: "\f15e";
+}
+.fa-sort-amount-asc:before {
+  content: "\f160";
+}
+.fa-sort-amount-desc:before {
+  content: "\f161";
+}
+.fa-sort-numeric-asc:before {
+  content: "\f162";
+}
+.fa-sort-numeric-desc:before {
+  content: "\f163";
+}
+.fa-thumbs-up:before {
+  content: "\f164";
+}
+.fa-thumbs-down:before {
+  content: "\f165";
+}
+.fa-youtube-square:before {
+  content: "\f166";
+}
+.fa-youtube:before {
+  content: "\f167";
+}
+.fa-xing:before {
+  content: "\f168";
+}
+.fa-xing-square:before {
+  content: "\f169";
+}
+.fa-youtube-play:before {
+  content: "\f16a";
+}
+.fa-dropbox:before {
+  content: "\f16b";
+}
+.fa-stack-overflow:before {
+  content: "\f16c";
+}
+.fa-instagram:before {
+  content: "\f16d";
+}
+.fa-flickr:before {
+  content: "\f16e";
+}
+.fa-adn:before {
+  content: "\f170";
+}
+.fa-bitbucket:before {
+  content: "\f171";
+}
+.fa-bitbucket-square:before {
+  content: "\f172";
+}
+.fa-tumblr:before {
+  content: "\f173";
+}
+.fa-tumblr-square:before {
+  content: "\f174";
+}
+.fa-long-arrow-down:before {
+  content: "\f175";
+}
+.fa-long-arrow-up:before {
+  content: "\f176";
+}
+.fa-long-arrow-left:before {
+  content: "\f177";
+}
+.fa-long-arrow-right:before {
+  content: "\f178";
+}
+.fa-apple:before {
+  content: "\f179";
+}
+.fa-windows:before {
+  content: "\f17a";
+}
+.fa-android:before {
+  content: "\f17b";
+}
+.fa-linux:before {
+  content: "\f17c";
+}
+.fa-dribbble:before {
+  content: "\f17d";
+}
+.fa-skype:before {
+  content: "\f17e";
+}
+.fa-foursquare:before {
+  content: "\f180";
+}
+.fa-trello:before {
+  content: "\f181";
+}
+.fa-female:before {
+  content: "\f182";
+}
+.fa-male:before {
+  content: "\f183";
+}
+.fa-gittip:before,
+.fa-gratipay:before {
+  content: "\f184";
+}
+.fa-sun-o:before {
+  content: "\f185";
+}
+.fa-moon-o:before {
+  content: "\f186";
+}
+.fa-archive:before {
+  content: "\f187";
+}
+.fa-bug:before {
+  content: "\f188";
+}
+.fa-vk:before {
+  content: "\f189";
+}
+.fa-weibo:before {
+  content: "\f18a";
+}
+.fa-renren:before {
+  content: "\f18b";
+}
+.fa-pagelines:before {
+  content: "\f18c";
+}
+.fa-stack-exchange:before {
+  content: "\f18d";
+}
+.fa-arrow-circle-o-right:before {
+  content: "\f18e";
+}
+.fa-arrow-circle-o-left:before {
+  content: "\f190";
+}
+.fa-toggle-left:before,
+.fa-caret-square-o-left:before {
+  content: "\f191";
+}
+.fa-dot-circle-o:before {
+  content: "\f192";
+}
+.fa-wheelchair:before {
+  content: "\f193";
+}
+.fa-vimeo-square:before {
+  content: "\f194";
+}
+.fa-turkish-lira:before,
+.fa-try:before {
+  content: "\f195";
+}
+.fa-plus-square-o:before {
+  content: "\f196";
+}
+.fa-space-shuttle:before {
+  content: "\f197";
+}
+.fa-slack:before {
+  content: "\f198";
+}
+.fa-envelope-square:before {
+  content: "\f199";
+}
+.fa-wordpress:before {
+  content: "\f19a";
+}
+.fa-openid:before {
+  content: "\f19b";
+}
+.fa-institution:before,
+.fa-bank:before,
+.fa-university:before {
+  content: "\f19c";
+}
+.fa-mortar-board:before,
+.fa-graduation-cap:before {
+  content: "\f19d";
+}
+.fa-yahoo:before {
+  content: "\f19e";
+}
+.fa-google:before {
+  content: "\f1a0";
+}
+.fa-reddit:before {
+  content: "\f1a1";
+}
+.fa-reddit-square:before {
+  content: "\f1a2";
+}
+.fa-stumbleupon-circle:before {
+  content: "\f1a3";
+}
+.fa-stumbleupon:before {
+  content: "\f1a4";
+}
+.fa-delicious:before {
+  content: "\f1a5";
+}
+.fa-digg:before {
+  content: "\f1a6";
+}
+.fa-pied-piper:before {
+  content: "\f1a7";
+}
+.fa-pied-piper-alt:before {
+  content: "\f1a8";
+}
+.fa-drupal:before {
+  content: "\f1a9";
+}
+.fa-joomla:before {
+  content: "\f1aa";
+}
+.fa-language:before {
+  content: "\f1ab";
+}
+.fa-fax:before {
+  content: "\f1ac";
+}
+.fa-building:before {
+  content: "\f1ad";
+}
+.fa-child:before {
+  content: "\f1ae";
+}
+.fa-paw:before {
+  content: "\f1b0";
+}
+.fa-spoon:before {
+  content: "\f1b1";
+}
+.fa-cube:before {
+  content: "\f1b2";
+}
+.fa-cubes:before {
+  content: "\f1b3";
+}
+.fa-behance:before {
+  content: "\f1b4";
+}
+.fa-behance-square:before {
+  content: "\f1b5";
+}
+.fa-steam:before {
+  content: "\f1b6";
+}
+.fa-steam-square:before {
+  content: "\f1b7";
+}
+.fa-recycle:before {
+  content: "\f1b8";
+}
+.fa-automobile:before,
+.fa-car:before {
+  content: "\f1b9";
+}
+.fa-cab:before,
+.fa-taxi:before {
+  content: "\f1ba";
+}
+.fa-tree:before {
+  content: "\f1bb";
+}
+.fa-spotify:before {
+  content: "\f1bc";
+}
+.fa-deviantart:before {
+  content: "\f1bd";
+}
+.fa-soundcloud:before {
+  content: "\f1be";
+}
+.fa-database:before {
+  content: "\f1c0";
+}
+.fa-file-pdf-o:before {
+  content: "\f1c1";
+}
+.fa-file-word-o:before {
+  content: "\f1c2";
+}
+.fa-file-excel-o:before {
+  content: "\f1c3";
+}
+.fa-file-powerpoint-o:before {
+  content: "\f1c4";
+}
+.fa-file-photo-o:before,
+.fa-file-picture-o:before,
+.fa-file-image-o:before {
+  content: "\f1c5";
+}
+.fa-file-zip-o:before,
+.fa-file-archive-o:before {
+  content: "\f1c6";
+}
+.fa-file-sound-o:before,
+.fa-file-audio-o:before {
+  content: "\f1c7";
+}
+.fa-file-movie-o:before,
+.fa-file-video-o:before {
+  content: "\f1c8";
+}
+.fa-file-code-o:before {
+  content: "\f1c9";
+}
+.fa-vine:before {
+  content: "\f1ca";
+}
+.fa-codepen:before {
+  content: "\f1cb";
+}
+.fa-jsfiddle:before {
+  content: "\f1cc";
+}
+.fa-life-bouy:before,
+.fa-life-buoy:before,
+.fa-life-saver:before,
+.fa-support:before,
+.fa-life-ring:before {
+  content: "\f1cd";
+}
+.fa-circle-o-notch:before {
+  content: "\f1ce";
+}
+.fa-ra:before,
+.fa-rebel:before {
+  content: "\f1d0";
+}
+.fa-ge:before,
+.fa-empire:before {
+  content: "\f1d1";
+}
+.fa-git-square:before {
+  content: "\f1d2";
+}
+.fa-git:before {
+  content: "\f1d3";
+}
+.fa-y-combinator-square:before,
+.fa-yc-square:before,
+.fa-hacker-news:before {
+  content: "\f1d4";
+}
+.fa-tencent-weibo:before {
+  content: "\f1d5";
+}
+.fa-qq:before {
+  content: "\f1d6";
+}
+.fa-wechat:before,
+.fa-weixin:before {
+  content: "\f1d7";
+}
+.fa-send:before,
+.fa-paper-plane:before {
+  content: "\f1d8";
+}
+.fa-send-o:before,
+.fa-paper-plane-o:before {
+  content: "\f1d9";
+}
+.fa-history:before {
+  content: "\f1da";
+}
+.fa-circle-thin:before {
+  content: "\f1db";
+}
+.fa-header:before {
+  content: "\f1dc";
+}
+.fa-paragraph:before {
+  content: "\f1dd";
+}
+.fa-sliders:before {
+  content: "\f1de";
+}
+.fa-share-alt:before {
+  content: "\f1e0";
+}
+.fa-share-alt-square:before {
+  content: "\f1e1";
+}
+.fa-bomb:before {
+  content: "\f1e2";
+}
+.fa-soccer-ball-o:before,
+.fa-futbol-o:before {
+  content: "\f1e3";
+}
+.fa-tty:before {
+  content: "\f1e4";
+}
+.fa-binoculars:before {
+  content: "\f1e5";
+}
+.fa-plug:before {
+  content: "\f1e6";
+}
+.fa-slideshare:before {
+  content: "\f1e7";
+}
+.fa-twitch:before {
+  content: "\f1e8";
+}
+.fa-yelp:before {
+  content: "\f1e9";
+}
+.fa-newspaper-o:before {
+  content: "\f1ea";
+}
+.fa-wifi:before {
+  content: "\f1eb";
+}
+.fa-calculator:before {
+  content: "\f1ec";
+}
+.fa-paypal:before {
+  content: "\f1ed";
+}
+.fa-google-wallet:before {
+  content: "\f1ee";
+}
+.fa-cc-visa:before {
+  content: "\f1f0";
+}
+.fa-cc-mastercard:before {
+  content: "\f1f1";
+}
+.fa-cc-discover:before {
+  content: "\f1f2";
+}
+.fa-cc-amex:before {
+  content: "\f1f3";
+}
+.fa-cc-paypal:before {
+  content: "\f1f4";
+}
+.fa-cc-stripe:before {
+  content: "\f1f5";
+}
+.fa-bell-slash:before {
+  content: "\f1f6";
+}
+.fa-bell-slash-o:before {
+  content: "\f1f7";
+}
+.fa-trash:before {
+  content: "\f1f8";
+}
+.fa-copyright:before {
+  content: "\f1f9";
+}
+.fa-at:before {
+  content: "\f1fa";
+}
+.fa-eyedropper:before {
+  content: "\f1fb";
+}
+.fa-paint-brush:before {
+  content: "\f1fc";
+}
+.fa-birthday-cake:before {
+  content: "\f1fd";
+}
+.fa-area-chart:before {
+  content: "\f1fe";
+}
+.fa-pie-chart:before {
+  content: "\f200";
+}
+.fa-line-chart:before {
+  content: "\f201";
+}
+.fa-lastfm:before {
+  content: "\f202";
+}
+.fa-lastfm-square:before {
+  content: "\f203";
+}
+.fa-toggle-off:before {
+  content: "\f204";
+}
+.fa-toggle-on:before {
+  content: "\f205";
+}
+.fa-bicycle:before {
+  content: "\f206";
+}
+.fa-bus:before {
+  content: "\f207";
+}
+.fa-ioxhost:before {
+  content: "\f208";
+}
+.fa-angellist:before {
+  content: "\f209";
+}
+.fa-cc:before {
+  content: "\f20a";
+}
+.fa-shekel:before,
+.fa-sheqel:before,
+.fa-ils:before {
+  content: "\f20b";
+}
+.fa-meanpath:before {
+  content: "\f20c";
+}
+.fa-buysellads:before {
+  content: "\f20d";
+}
+.fa-connectdevelop:before {
+  content: "\f20e";
+}
+.fa-dashcube:before {
+  content: "\f210";
+}
+.fa-forumbee:before {
+  content: "\f211";
+}
+.fa-leanpub:before {
+  content: "\f212";
+}
+.fa-sellsy:before {
+  content: "\f213";
+}
+.fa-shirtsinbulk:before {
+  content: "\f214";
+}
+.fa-simplybuilt:before {
+  content: "\f215";
+}
+.fa-skyatlas:before {
+  content: "\f216";
+}
+.fa-cart-plus:before {
+  content: "\f217";
+}
+.fa-cart-arrow-down:before {
+  content: "\f218";
+}
+.fa-diamond:before {
+  content: "\f219";
+}
+.fa-ship:before {
+  content: "\f21a";
+}
+.fa-user-secret:before {
+  content: "\f21b";
+}
+.fa-motorcycle:before {
+  content: "\f21c";
+}
+.fa-street-view:before {
+  content: "\f21d";
+}
+.fa-heartbeat:before {
+  content: "\f21e";
+}
+.fa-venus:before {
+  content: "\f221";
+}
+.fa-mars:before {
+  content: "\f222";
+}
+.fa-mercury:before {
+  content: "\f223";
+}
+.fa-intersex:before,
+.fa-transgender:before {
+  content: "\f224";
+}
+.fa-transgender-alt:before {
+  content: "\f225";
+}
+.fa-venus-double:before {
+  content: "\f226";
+}
+.fa-mars-double:before {
+  content: "\f227";
+}
+.fa-venus-mars:before {
+  content: "\f228";
+}
+.fa-mars-stroke:before {
+  content: "\f229";
+}
+.fa-mars-stroke-v:before {
+  content: "\f22a";
+}
+.fa-mars-stroke-h:before {
+  content: "\f22b";
+}
+.fa-neuter:before {
+  content: "\f22c";
+}
+.fa-genderless:before {
+  content: "\f22d";
+}
+.fa-facebook-official:before {
+  content: "\f230";
+}
+.fa-pinterest-p:before {
+  content: "\f231";
+}
+.fa-whatsapp:before {
+  content: "\f232";
+}
+.fa-server:before {
+  content: "\f233";
+}
+.fa-user-plus:before {
+  content: "\f234";
+}
+.fa-user-times:before {
+  content: "\f235";
+}
+.fa-hotel:before,
+.fa-bed:before {
+  content: "\f236";
+}
+.fa-viacoin:before {
+  content: "\f237";
+}
+.fa-train:before {
+  content: "\f238";
+}
+.fa-subway:before {
+  content: "\f239";
+}
+.fa-medium:before {
+  content: "\f23a";
+}
+.fa-yc:before,
+.fa-y-combinator:before {
+  content: "\f23b";
+}
+.fa-optin-monster:before {
+  content: "\f23c";
+}
+.fa-opencart:before {
+  content: "\f23d";
+}
+.fa-expeditedssl:before {
+  content: "\f23e";
+}
+.fa-battery-4:before,
+.fa-battery-full:before {
+  content: "\f240";
+}
+.fa-battery-3:before,
+.fa-battery-three-quarters:before {
+  content: "\f241";
+}
+.fa-battery-2:before,
+.fa-battery-half:before {
+  content: "\f242";
+}
+.fa-battery-1:before,
+.fa-battery-quarter:before {
+  content: "\f243";
+}
+.fa-battery-0:before,
+.fa-battery-empty:before {
+  content: "\f244";
+}
+.fa-mouse-pointer:before {
+  content: "\f245";
+}
+.fa-i-cursor:before {
+  content: "\f246";
+}
+.fa-object-group:before {
+  content: "\f247";
+}
+.fa-object-ungroup:before {
+  content: "\f248";
+}
+.fa-sticky-note:before {
+  content: "\f249";
+}
+.fa-sticky-note-o:before {
+  content: "\f24a";
+}
+.fa-cc-jcb:before {
+  content: "\f24b";
+}
+.fa-cc-diners-club:before {
+  content: "\f24c";
+}
+.fa-clone:before {
+  content: "\f24d";
+}
+.fa-balance-scale:before {
+  content: "\f24e";
+}
+.fa-hourglass-o:before {
+  content: "\f250";
+}
+.fa-hourglass-1:before,
+.fa-hourglass-start:before {
+  content: "\f251";
+}
+.fa-hourglass-2:before,
+.fa-hourglass-half:before {
+  content: "\f252";
+}
+.fa-hourglass-3:before,
+.fa-hourglass-end:before {
+  content: "\f253";
+}
+.fa-hourglass:before {
+  content: "\f254";
+}
+.fa-hand-grab-o:before,
+.fa-hand-rock-o:before {
+  content: "\f255";
+}
+.fa-hand-stop-o:before,
+.fa-hand-paper-o:before {
+  content: "\f256";
+}
+.fa-hand-scissors-o:before {
+  content: "\f257";
+}
+.fa-hand-lizard-o:before {
+  content: "\f258";
+}
+.fa-hand-spock-o:before {
+  content: "\f259";
+}
+.fa-hand-pointer-o:before {
+  content: "\f25a";
+}
+.fa-hand-peace-o:before {
+  content: "\f25b";
+}
+.fa-trademark:before {
+  content: "\f25c";
+}
+.fa-registered:before {
+  content: "\f25d";
+}
+.fa-creative-commons:before {
+  content: "\f25e";
+}
+.fa-gg:before {
+  content: "\f260";
+}
+.fa-gg-circle:before {
+  content: "\f261";
+}
+.fa-tripadvisor:before {
+  content: "\f262";
+}
+.fa-odnoklassniki:before {
+  content: "\f263";
+}
+.fa-odnoklassniki-square:before {
+  content: "\f264";
+}
+.fa-get-pocket:before {
+  content: "\f265";
+}
+.fa-wikipedia-w:before {
+  content: "\f266";
+}
+.fa-safari:before {
+  content: "\f267";
+}
+.fa-chrome:before {
+  content: "\f268";
+}
+.fa-firefox:before {
+  content: "\f269";
+}
+.fa-opera:before {
+  content: "\f26a";
+}
+.fa-internet-explorer:before {
+  content: "\f26b";
+}
+.fa-tv:before,
+.fa-television:before {
+  content: "\f26c";
+}
+.fa-contao:before {
+  content: "\f26d";
+}
+.fa-500px:before {
+  content: "\f26e";
+}
+.fa-amazon:before {
+  content: "\f270";
+}
+.fa-calendar-plus-o:before {
+  content: "\f271";
+}
+.fa-calendar-minus-o:before {
+  content: "\f272";
+}
+.fa-calendar-times-o:before {
+  content: "\f273";
+}
+.fa-calendar-check-o:before {
+  content: "\f274";
+}
+.fa-industry:before {
+  content: "\f275";
+}
+.fa-map-pin:before {
+  content: "\f276";
+}
+.fa-map-signs:before {
+  content: "\f277";
+}
+.fa-map-o:before {
+  content: "\f278";
+}
+.fa-map:before {
+  content: "\f279";
+}
+.fa-commenting:before {
+  content: "\f27a";
+}
+.fa-commenting-o:before {
+  content: "\f27b";
+}
+.fa-houzz:before {
+  content: "\f27c";
+}
+.fa-vimeo:before {
+  content: "\f27d";
+}
+.fa-black-tie:before {
+  content: "\f27e";
+}
+.fa-fonticons:before {
+  content: "\f280";
+}
+.fa-reddit-alien:before {
+  content: "\f281";
+}
+.fa-edge:before {
+  content: "\f282";
+}
+.fa-credit-card-alt:before {
+  content: "\f283";
+}
+.fa-codiepie:before {
+  content: "\f284";
+}
+.fa-modx:before {
+  content: "\f285";
+}
+.fa-fort-awesome:before {
+  content: "\f286";
+}
+.fa-usb:before {
+  content: "\f287";
+}
+.fa-product-hunt:before {
+  content: "\f288";
+}
+.fa-mixcloud:before {
+  content: "\f289";
+}
+.fa-scribd:before {
+  content: "\f28a";
+}
+.fa-pause-circle:before {
+  content: "\f28b";
+}
+.fa-pause-circle-o:before {
+  content: "\f28c";
+}
+.fa-stop-circle:before {
+  content: "\f28d";
+}
+.fa-stop-circle-o:before {
+  content: "\f28e";
+}
+.fa-shopping-bag:before {
+  content: "\f290";
+}
+.fa-shopping-basket:before {
+  content: "\f291";
+}
+.fa-hashtag:before {
+  content: "\f292";
+}
+.fa-bluetooth:before {
+  content: "\f293";
+}
+.fa-bluetooth-b:before {
+  content: "\f294";
+}
+.fa-percent:before {
+  content: "\f295";
+}

File diff suppressed because it is too large
+ 3 - 0
kanban-console/src/main/webapp/resources/lib/fontawesome/css/font-awesome.min.css


BIN
kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/FontAwesome.otf


BIN
kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.eot


File diff suppressed because it is too large
+ 196 - 0
kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.svg


BIN
kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.ttf


BIN
kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.woff


BIN
kanban-console/src/main/webapp/resources/lib/fontawesome/fonts/fontawesome-webfont.woff2


File diff suppressed because it is too large
+ 1 - 0
kanban-console/src/main/webapp/resources/lib/jquery/jquery.min.js


File diff suppressed because it is too large
+ 1 - 0
kanban-console/src/main/webapp/resources/lib/spin/spin.min.js


+ 115 - 0
pom.xml

@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-parent</artifactId>
+		<version>1.4.1.RELEASE</version>
+	</parent>
+	<groupId>com.uas.kanban</groupId>
+	<artifactId>kanban-parent</artifactId>
+	<version>0.0.1-SNAPSHOT</version>
+	<packaging>pom</packaging>
+	<description>看板后台</description>
+
+	<modules>
+		<module>kanban-console</module>
+		<module>kanban-auth</module>
+		<module>kanban-common</module>
+	</modules>
+
+	<properties>
+		<project.version>0.0.1-SNAPSHOT</project.version>
+		<kanban.auth.version>${project.version}</kanban.auth.version>
+		<kanban.common.version>${project.version}</kanban.common.version>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<springframeword.cloud.version>1.2.1.RELEASE</springframeword.cloud.version>
+		<spring.version>4.3.3.RELEASE</spring.version>
+		<fastjson.version>1.2.15</fastjson.version>
+		<slf4j.version>1.7.21</slf4j.version>
+		<servlet.version>3.1.0</servlet.version>
+		<mongo.java.version>3.5.0</mongo.java.version>
+		<morphia.version>1.3.2</morphia.version>
+	</properties>
+
+	<dependencyManagement>
+		<dependencies>
+			<dependency>
+				<groupId>org.springframework.cloud</groupId>
+				<artifactId>spring-cloud-starter-config</artifactId>
+				<version>${springframeword.cloud.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-beans</artifactId>
+				<version>${spring.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-context</artifactId>
+				<version>${spring.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-web</artifactId>
+				<version>${spring.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>com.alibaba</groupId>
+				<artifactId>fastjson</artifactId>
+				<version>${fastjson.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.slf4j</groupId>
+				<artifactId>log4j-over-slf4j</artifactId>
+				<version>${slf4j.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>javax.servlet</groupId>
+				<artifactId>javax.servlet-api</artifactId>
+				<version>${servlet.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.mongodb</groupId>
+				<artifactId>mongo-java-driver</artifactId>
+				<version>${mongo.java.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.mongodb.morphia</groupId>
+				<artifactId>morphia</artifactId>
+				<version>${morphia.version}</version>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
+	<build>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-compiler-plugin</artifactId>
+					<configuration>
+						<source>1.7</source>
+						<target>1.7</target>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
+
+	<distributionManagement>
+		<!-- 发布release仓库 -->
+		<repository>
+			<id>platform-release</id>
+			<name>platform-release</name>
+			<url>http://maven.ubtob.com/artifactory/libs-release-local</url>
+		</repository>
+		<!-- 发布快照版本 -->
+		<snapshotRepository>
+			<id>platform-snapshots</id>
+			<name>platform-snapshots</name>
+			<url>http://maven.ubtob.com/artifactory/libs-snapshot-local</url>
+		</snapshotRepository>
+	</distributionManagement>
+</project>

Some files were not shown because too many files changed in this diff