将CrudApiController,MapBeanUtils抽取到common项目
diff --git a/samples/common/pom.xml b/samples/common/pom.xml
index 864192d..4c60df4 100644
--- a/samples/common/pom.xml
+++ b/samples/common/pom.xml
@@ -16,6 +16,12 @@
   <dependencies>
 
     <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-web</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+    <dependency>
       <groupId>com.supwisdom.infras</groupId>
       <artifactId>infras-data-jpa</artifactId>
     </dependency>
diff --git a/samples/common/src/main/java/com/supwisdom/leaveschool/common/controller/api/CrudApiController.java b/samples/common/src/main/java/com/supwisdom/leaveschool/common/controller/api/CrudApiController.java
new file mode 100644
index 0000000..eb45af1
--- /dev/null
+++ b/samples/common/src/main/java/com/supwisdom/leaveschool/common/controller/api/CrudApiController.java
@@ -0,0 +1,337 @@
+package com.supwisdom.leaveschool.common.controller.api;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.collections.IteratorUtils;
+import org.springframework.data.domain.Page;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.MimeTypeUtils;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import com.supwisdom.leaveschool.common.domain.ABaseDomain;
+import com.supwisdom.leaveschool.common.model.PagerRequestModel;
+import com.supwisdom.leaveschool.common.model.PagerResponseModel;
+import com.supwisdom.leaveschool.common.repository.BaseJpaRepository;
+import com.supwisdom.leaveschool.common.util.DomainUtils;
+
+public abstract class CrudApiController<D extends ABaseDomain, R extends BaseJpaRepository<D>> {
+  
+  protected abstract R getRepository();
+  
+  /**
+   * 
+   * curl -i -s -X GET -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains' 
+   * curl -i -s -X GET -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains?pageIndex=2&pageSize=50' 
+   * curl -i -s -X GET -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains?pageIndex=0&pageSize=20&mapBean[code]=code&mapBean[name]=name&mapBean[status]=1' 
+   * curl -i -s -X GET -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains?pageIndex=0&pageSize=20&mapBean[code]=code&mapBean[name]=name&mapBean[status]=0' 
+   * 
+   * response success: 
+   * 
+   * {
+   *   "pageIndex":0,
+   *   "pageSize":20,
+   *   "mapBean":null,
+   *   "pageCount":1,
+   *   "recordCount":1,
+   *   "items":[
+   *     {
+   *       "id":"ff80808164feb8990164feba0de50000",
+   *       "companyId":"1",
+   *       "deleted":false,
+   *       "addAccount":"group","addTime":"2018-08-03T07:39:23.000+0000",
+   *       "editAccount":null,"editTime":null,
+   *       "deleteAccount":null,"deleteTime":null,
+   *       "code":"test001",
+   *       "name":"测试001",
+   *       "memo":"测试001备注",
+   *       "status":"1"
+   *     }
+   *   ]
+   * }
+   * 
+   * response error 401:
+   * 
+   * {
+   *   "timestamp":"2018-08-03T08:48:25.777+0000",
+   *   "status":401,
+   *   "error":"Http Status 401",
+   *   "message":"Unauthorized",
+   *   "path":"/${API_PATH_PREFIX}/domains"
+   * }
+   * 
+   * @param pagerRequestModel
+   * @return
+   */
+  @GetMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
+  @ResponseStatus(value = HttpStatus.OK)
+  @ResponseBody
+  public PagerResponseModel<D> list(PagerRequestModel pagerRequestModel) {
+    
+    Page<D> page = getRepository().selectPageList(pagerRequestModel.getPageIndex(), pagerRequestModel.getPageSize(), pagerRequestModel.getMapBean());
+    
+    @SuppressWarnings("unchecked")
+    List<D> list = IteratorUtils.toList(page.iterator());
+    
+    PagerResponseModel<D> pagerResponseModel = PagerResponseModel.of(pagerRequestModel);
+    pagerResponseModel.setPageCount(page.getTotalPages());
+    pagerResponseModel.setRecordCount(page.getTotalElements());
+    pagerResponseModel.setItems(list);
+    
+    return pagerResponseModel;
+  }
+  
+  /**
+   * 
+   * curl -i -s -X GET -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains/1' 
+   * 
+   * response success: 
+   * 
+   * {
+   *   "id":"ff80808164feb8990164feba0de50000",
+   *   "companyId":"1",
+   *   "deleted":false,
+   *   "addAccount":"group","addTime":"2018-08-03T07:39:23.000+0000",
+   *   "editAccount":null,"editTime":null,
+   *   "deleteAccount":null,"deleteTime":null,
+   *   "code":"test001",
+   *   "name":"测试001",
+   *   "memo":"测试001备注",
+   *   "status":"1"
+   * }
+   * 
+   * response error 401:
+   * 
+   * {
+   *   "timestamp":"2018-08-03T08:43:26.080+0000",
+   *   "status":401,
+   *   "error":"Http Status 401",
+   *   "message":"Unauthorized",
+   *   "path":"/${API_PATH_PREFIX}/domains/ff80808164fecf640164fed269480000"
+   * }
+   * 
+   * response error 500:
+   * 
+   * {
+   *   "timestamp":"2018-08-03T07:44:07.963+0000",
+   *   "status":500,
+   *   "error":"Internal Server Error",
+   *   "exception":"java.lang.RuntimeException",
+   *   "message":"exception.get.domain.not.exist",
+   *   "path":"/${API_PATH_PREFIX}/domains/1"
+   * }
+   * 
+   * @param id
+   * @return
+   */
+  @GetMapping(path = "/{id}", produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
+  @ResponseStatus(value = HttpStatus.OK)
+  @ResponseBody
+  public D get(@PathVariable("id") String id) {
+    
+    if (id == null || id.length() == 0) {
+      throw new RuntimeException("exception.get.id.must.not.empty");  // FIXME: RestException
+    }
+    
+    D d = getRepository().selectById(id);
+    
+    if (d == null) {
+      throw new RuntimeException("exception.get.domain.not.exist");  // FIXME: RestException
+    }
+    
+    return d;
+  }
+  
+  /**
+   * 
+   * curl -i -s -X POST -H 'Content-Type:application/json' -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains' \
+   * -d '{"code":"test001","name":"测试001","memo":"测试001备注","status":"1","addAccount":"admin"}'
+   * 
+   * response success: 
+   * 
+   * {
+   *   "success":"info.save.success"
+   * }
+   * 
+   * response error 401:
+   * 
+   * {
+   *   "timestamp":"2018-08-03T08:48:25.777+0000",
+   *   "status":401,
+   *   "error":"Http Status 401",
+   *   "message":"Unauthorized",
+   *   "path":"/${API_PATH_PREFIX}/domains"
+   * }
+   * 
+   * response error: // FIXME: save error
+   * 
+   * {
+   *   "timestamp":"2018-08-03T07:45:43.436+0000",
+   *   "status":500,
+   *   "error":"Internal Server Error",
+   *   "exception":"org.springframework.dao.DataIntegrityViolationException",
+   *   "message":"could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
+   *   "path":"/${API_PATH_PREFIX}/domains"
+   * }
+   * 
+   * @param group
+   * @return
+   */
+  @PostMapping(consumes = MimeTypeUtils.APPLICATION_JSON_VALUE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
+  @ResponseStatus(value = HttpStatus.OK)
+  @ResponseBody
+  public Map<String, Object> save(@RequestBody D d) {
+    
+    @SuppressWarnings("unused")
+    D ret = getRepository().insert(d);
+    
+    Map<String, Object> res = new HashMap<String, Object>();
+    res.put("success", "info.save.success");
+    
+    return res;
+  }
+  
+  /**
+   * 
+   * curl -i -s -X PUT -H 'Content-Type:application/json' -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains' \
+   * -d '{"id":"1","status":"0"}'
+   * 
+   * response success:
+   * 
+   * {
+   *   "success":"info.update.success"
+   * }
+   * 
+   * response error 401:
+   * 
+   * {
+   *   "timestamp":"2018-08-03T08:48:25.777+0000",
+   *   "status":401,
+   *   "error":"Http Status 401",
+   *   "message":"Unauthorized",
+   *   "path":"/${API_PATH_PREFIX}/domains"
+   * }
+   * 
+   * curl -i -s -X PUT -H 'Content-Type:application/json' -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains' \
+   * -d '{"status":"0"}'
+   * 
+   * response error:
+   * 
+   * {
+   *   "timestamp":"2018-08-03T07:50:52.327+0000",
+   *   "status":500,
+   *   "error":"Internal Server Error",
+   *   "exception":"java.lang.RuntimeException",
+   *   "message":"exception.update.id.must.not.empty",
+   *   "path":"/${API_PATH_PREFIX}/domains"
+   * }
+   * 
+   * curl -i -s -X PUT -H 'Content-Type:application/json' -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains' \
+   * -d '{"id":"1","status":"0"}'
+   * 
+   * response error:
+   * 
+   * {
+   *   "timestamp":"2018-08-03T07:48:24.774+0000",
+   *   "status":500,
+   *   "error":"Internal Server Error",
+   *   "exception":"java.lang.RuntimeException",
+   *   "message":"exception.update.domain.not.exist",
+   *   "path":"/${API_PATH_PREFIX}/domains"
+   * }
+   * 
+   * @param group
+   * @return
+   */
+  @PutMapping(consumes = MimeTypeUtils.APPLICATION_JSON_VALUE, produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
+  @ResponseStatus(value = HttpStatus.OK)
+  @ResponseBody
+  public Map<String, Object> update(@RequestBody D d) {
+    
+    if (d.getId() == null || d.getId().length() == 0) {
+      throw new RuntimeException("exception.update.id.must.not.empty");  // FIXME: RestException
+    }
+    
+    D tmp = getRepository().selectById(d.getId());
+    if (tmp == null) {
+      throw new RuntimeException("exception.update.domain.not.exist");  // FIXME: RestException
+    }
+    
+    tmp = DomainUtils.merge(d, tmp);
+    
+    @SuppressWarnings("unused")
+    D ret = getRepository().update(tmp);
+    getRepository().flush();
+    
+    Map<String, Object> res = new HashMap<String, Object>();
+    res.put("success", "info.update.success");
+    
+    return res;
+  }
+  
+  /**
+   * 
+   * curl -i -s -X DELETE -H 'Accept:application/json' 'http://localhost:10010/${API_PATH_PREFIX}/domains/1'
+   * 
+   * response success: 
+   * 
+   * {
+   *   "success":"info.delete.success"
+   * }
+   * 
+   * response error 401:
+   * 
+   * {
+   *   "timestamp":"2018-08-03T08:48:25.777+0000",
+   *   "status":401,
+   *   "error":"Http Status 401",
+   *   "message":"Unauthorized",
+   *   "path":"/${API_PATH_PREFIX}/domains/1"
+   * }
+   * 
+   * response error 500: 
+   * 
+   * {
+   *   "timestamp":"2018-08-03T08:03:16.364+0000",
+   *   "status":500,
+   *   "error":"Internal Server Error",
+   *   "exception":"java.lang.RuntimeException",
+   *   "message":"exception.delete.domain.not.exist",
+   *   "path":"/${API_PATH_PREFIX}/domains/1"
+   * }
+   * 
+   * @param id
+   * @return
+   */
+  @DeleteMapping(path = "/{id}", produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
+  @ResponseStatus(value = HttpStatus.OK)
+  @ResponseBody
+  public Map<String, Object> delete(@PathVariable("id") String id) {
+    
+    if (id == null || id.length() == 0) {
+      throw new RuntimeException("exception.delete.id.must.not.empty");  // FIXME: RestException
+    }
+    
+    D tmp = getRepository().selectById(id);
+    if (tmp == null) {
+      throw new RuntimeException("exception.delete.domain.not.exist");  // FIXME: RestException
+    }
+    
+    getRepository().delete(tmp);
+    
+    Map<String, Object> res = new HashMap<String, Object>();
+    res.put("success", "info.delete.success");
+    
+    return res;
+  }
+  
+
+}
diff --git a/samples/common/src/main/java/com/supwisdom/leaveschool/common/util/MapBeanUtils.java b/samples/common/src/main/java/com/supwisdom/leaveschool/common/util/MapBeanUtils.java
new file mode 100644
index 0000000..e788bac
--- /dev/null
+++ b/samples/common/src/main/java/com/supwisdom/leaveschool/common/util/MapBeanUtils.java
@@ -0,0 +1,150 @@
+package com.supwisdom.leaveschool.common.util;
+
+import java.util.Map;
+
+public class MapBeanUtils {
+
+  /**
+   * 判断 mapBean 中的 key 是否存在;若存在,则判断是否有值
+   * 
+   * @param mapBean
+   * @param key
+   * @return
+   */
+  public static boolean containsValue(Map<String, Object> mapBean, String key) {
+
+    if (!mapBean.containsKey(key)) {
+      return false;
+    }
+
+    if (mapBean.get(key) == null) {
+      return false;
+    }
+
+    if (String.valueOf(mapBean.get(key)).isEmpty()) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * 获取 mapBean 中 key 的 value,若不存在,则返回 null
+   * 
+   * @param mapBean
+   * @param key
+   * @return
+   */
+  public static String getString(Map<String, Object> mapBean, String key) {
+
+    return getString(mapBean, key, null);
+  }
+
+  /**
+   * 获取 mapBean 中 key 的 value,若不存在,则返回 defaultValue
+   * 
+   * @param mapBean
+   * @param key
+   * @param defaultValue
+   * @return
+   */
+  public static String getString(Map<String, Object> mapBean, String key, String defaultValue) {
+
+    if (containsValue(mapBean, key)) {
+      return String.valueOf(mapBean.get(key));
+    }
+
+    return defaultValue;
+  }
+
+  /**
+   * 获取 mapBean 中 key 的 value,若不存在,则返回 false
+   * 
+   * @param mapBean
+   * @param key
+   * @return
+   */
+  public static boolean getBoolean(Map<String, Object> mapBean, String key) {
+
+    return getBoolean(mapBean, key, false);
+  }
+
+  /**
+   * 获取 mapBean 中 key 的 value,若不存在,则返回 defaultValue
+   * 
+   * @param mapBean
+   * @param key
+   * @param defaultValue
+   * @return
+   */
+  public static boolean getBoolean(Map<String, Object> mapBean, String key, Boolean defaultValue) {
+
+    if (containsValue(mapBean, key)) {
+      Boolean b = Boolean.valueOf(String.valueOf(mapBean.get(key)));
+      return b == null ? defaultValue : b.booleanValue();
+    }
+
+    return defaultValue;
+  }
+
+  /**
+   * 获取 mapBean 中 key 的 value,若不存在,则返回 -1
+   * 
+   * @param mapBean
+   * @param key
+   * @return
+   */
+  public static int getInteger(Map<String, Object> mapBean, String key) {
+
+    return getInteger(mapBean, key, -1);
+  }
+
+  /**
+   * 获取 mapBean 中 key 的 value,若不存在,则返回 defaultValue
+   * 
+   * @param mapBean
+   * @param key
+   * @param defaultValue
+   * @return
+   */
+  public static int getInteger(Map<String, Object> mapBean, String key, Integer defaultValue) {
+
+    if (containsValue(mapBean, key)) {
+      Integer i = Integer.valueOf(String.valueOf(mapBean.get(key)));
+      return i == null ? defaultValue : i.intValue();
+    }
+
+    return defaultValue;
+  }
+
+  /**
+   * 获取 mapBean 中 key 的 value,若不存在,则返回 -1L
+   * 
+   * @param mapBean
+   * @param key
+   * @return
+   */
+  public static long getLong(Map<String, Object> mapBean, String key) {
+
+    return getLong(mapBean, key, -1L);
+  }
+
+  /**
+   * 获取 mapBean 中 key 的 value,若不存在,则返回 defaultValue
+   * 
+   * @param mapBean
+   * @param key
+   * @param defaultValue
+   * @return
+   */
+  public static long getLong(Map<String, Object> mapBean, String key, Long defaultValue) {
+
+    if (containsValue(mapBean, key)) {
+      Long l = Long.valueOf(String.valueOf(mapBean.get(key)));
+      return l == null ? defaultValue : l.longValue();
+    }
+
+    return defaultValue;
+  }
+
+}