完成栏目功能
diff --git a/backend/build.gradle b/backend/build.gradle
index f922424..d9047cc 100644
--- a/backend/build.gradle
+++ b/backend/build.gradle
@@ -58,6 +58,11 @@
     implementation 'com.github.penggle:kaptcha:2.3.2'
 
     testCompile group: 'junit', name: 'junit', version: '4.12'
+
+    compile group: 'net.coobird', name: 'thumbnailator', version: '0.4.8'
+    compile group: 'com.sun.jersey.contribs', name: 'jersey-multipart', version: '1.19.3'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.9.1'
+    compile group: 'log4j', name: 'log4j', version: '1.2.17'
 }
 
 dependencyManagement {
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/ArticleRepositoryImpl.java b/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/ArticleRepositoryImpl.java
new file mode 100644
index 0000000..a4bafe4
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/ArticleRepositoryImpl.java
@@ -0,0 +1,75 @@
+package com.supwisdom.dlpay.portal.dao.impl;
+
+import com.supwisdom.dlpay.framework.jpa.BaseRepository;
+import com.supwisdom.dlpay.framework.jpa.Finder;
+import com.supwisdom.dlpay.framework.jpa.page.Pagination;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.portal.bean.ArticleSearchBean;
+import com.supwisdom.dlpay.portal.dao.ArticleRepository;
+import com.supwisdom.dlpay.portal.domain.TBArticle;
+import org.hibernate.transform.Transformers;
+import org.jetbrains.annotations.NotNull;
+
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+public class ArticleRepositoryImpl  extends BaseRepository implements ArticleRepository {
+  @NotNull
+  @Override
+  public Pagination getArticleList(@NotNull ArticleSearchBean bean) throws ParseException {
+    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddhhmmss");
+    StringBuilder sql = new StringBuilder("select a.*,c.name columnname from tb_article a left join tb_column c on a.columnid = c.columnid where 1=1 ");
+    String title = bean.getTitle();
+    String status = bean.getStatus();
+    String savestartdate = bean.getSavestartdate();
+    String saveenddate = bean.getSaveenddate();
+    String releasestartdate = bean.getReleasestartdate();
+    String releaseenddate = bean.getReleaseenddate();
+    int pageno = bean.getPageno();
+    int pagesize = bean.getPagesize();
+    if (!StringUtil.isEmpty(title)) {
+      sql.append(" and a.title like :title");
+    }
+    if (!StringUtil.isEmpty(status)) {
+      sql.append(" and a.status =:status");
+    }
+    if (!StringUtil.isEmpty(releasestartdate)) {
+      sql.append(" and a.releasetime >=:releasestartdate");
+    }
+    if (!StringUtil.isEmpty(releaseenddate)) {
+      sql.append(" and a.releasetime <=:releaseenddate");
+    }
+    if (!StringUtil.isEmpty(savestartdate)) {
+      sql.append(" and a.createtime >=:savestartdate");
+    }
+    if (!StringUtil.isEmpty(saveenddate)) {
+      sql.append(" and a.createtime <=:saveenddate");
+    }
+    sql.append(" order by a.releasetime");
+    Finder f = Finder.create(sql.toString());
+    if (!StringUtil.isEmpty(title)) {
+      f.setParameter("title", "%" + title.trim() + "%");
+    }
+    if (!StringUtil.isEmpty(status)) {
+      f.setParameter("status", status);
+    }
+    if (!StringUtil.isEmpty(releasestartdate)) {
+      Timestamp timestamp = new Timestamp(sdf.parse(releasestartdate+"000000").getTime());
+      f.setParameter("releasestartdate", timestamp);
+    }
+    if (!StringUtil.isEmpty(releaseenddate)) {
+      Timestamp timestamp = new Timestamp(sdf.parse(releaseenddate+"235959").getTime());
+      f.setParameter("releaseenddate", timestamp);
+    }
+    if (!StringUtil.isEmpty(savestartdate)) {
+      Timestamp timestamp = new Timestamp(sdf.parse(savestartdate+"000000").getTime());
+      f.setParameter("savestartdate", timestamp);
+    }
+    if (!StringUtil.isEmpty(saveenddate)) {
+      Timestamp timestamp = new Timestamp(sdf.parse(saveenddate+"235959").getTime());
+      f.setParameter("saveenddate", timestamp);
+    }
+    return findNative(f, Transformers.aliasToBean(TBArticle.class), pageno, pagesize);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/ColumnRepositoryImpl.java b/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/ColumnRepositoryImpl.java
new file mode 100644
index 0000000..54661d7
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/ColumnRepositoryImpl.java
@@ -0,0 +1,38 @@
+package com.supwisdom.dlpay.portal.dao.impl;
+
+import com.supwisdom.dlpay.framework.jpa.BaseRepository;
+import com.supwisdom.dlpay.framework.jpa.Finder;
+import com.supwisdom.dlpay.framework.jpa.page.Pagination;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.portal.bean.ColumnSearchBean;
+import com.supwisdom.dlpay.portal.dao.ColumnRepository;
+import com.supwisdom.dlpay.portal.domain.TBColumn;
+import org.hibernate.transform.Transformers;
+import org.jetbrains.annotations.NotNull;
+
+public class ColumnRepositoryImpl extends BaseRepository implements ColumnRepository {
+  @NotNull
+  @Override
+  public Pagination getColumnList(@NotNull ColumnSearchBean bean) {
+    StringBuilder sql = new StringBuilder("select s.*,p.name parentname from tb_column s left join tb_column p on s.parentid = p.columnid where 1=1 ");
+    String name = bean.getName();
+    String code = bean.getCode();
+    int pageno = bean.getPageno();
+    int pagesize = bean.getPagesize();
+    if (!StringUtil.isEmpty(name)) {
+      sql.append(" and s.name like :name");
+    }
+    if (!StringUtil.isEmpty(code)) {
+      sql.append(" and s.code like :code");
+    }
+    sql.append(" order by s.isleaf,s.ordernum");
+    Finder f = Finder.create(sql.toString());
+    if (!StringUtil.isEmpty(name)) {
+      f.setParameter("name", "%" + name.trim() + "%");
+    }
+    if (!StringUtil.isEmpty(code)) {
+      f.setParameter("code", "%" + code.trim() + "%");
+    }
+    return findNative(f, Transformers.aliasToBean(TBColumn.class), pageno, pagesize);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBArticle.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBArticle.java
new file mode 100644
index 0000000..e03bf41
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBArticle.java
@@ -0,0 +1,185 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import java.sql.Timestamp;
+
+
+@Entity
+@Table(name = "tb_article", indexes = {
+    @Index(name = "article_idx1", columnList = "articleno", unique = true),
+    @Index(name = "article_idx2", columnList = "title"),
+    @Index(name = "article_idx3", columnList = "releasetime,createtime,status")
+})
+public class TBArticle {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "articleid", nullable = false, length = 32)
+  private String articleid;
+
+  @Column(name = "columnid", nullable = false, length = 32)
+  private String columnid;
+
+  @Column(name = "articleno", unique = true, nullable = false, length = 20)
+  private String articleno;
+
+  @Column(name = "title", nullable = false, length = 30)
+  private String title;
+
+  @Lob
+  @Column(name = "content", columnDefinition = "TEXT")
+  private String content;
+
+  @Column(name = "createtime")
+  private Timestamp createtime;
+
+  @Column(name = "updatetime")
+  private Timestamp updatetime;
+
+  @Column(name = "releasetime")
+  private Timestamp releasetime;
+
+  @Column(name = "operid", length = 32)
+  private String operid;
+
+  @Column(name = "hits")
+  private Integer hits;
+
+  @Column(name = "islink", length = 1)
+  private String islink;
+
+  @Column(name = "isdisplay", length = 1)
+  private String isdisplay;
+
+  @Column(name = "isdelete", length = 1)
+  private String isdelete;
+
+  @Column(name = "status", length = 20)
+  private String status;
+
+  @Transient
+  private String columnname;
+
+
+  public String getArticleid() {
+    return articleid;
+  }
+
+  public void setArticleid(String articleid) {
+    this.articleid = articleid;
+  }
+
+  public String getColumnid() {
+    return columnid;
+  }
+
+  public void setColumnid(String columnid) {
+    this.columnid = columnid;
+  }
+
+  public String getArticleno() {
+    return articleno;
+  }
+
+  public void setArticleno(String articleno) {
+    this.articleno = articleno;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  public Timestamp getCreatetime() {
+    return createtime;
+  }
+
+  public void setCreatetime(Timestamp createtime) {
+    this.createtime = createtime;
+  }
+
+  public Timestamp getUpdatetime() {
+    return updatetime;
+  }
+
+  public void setUpdatetime(Timestamp updatetime) {
+    this.updatetime = updatetime;
+  }
+
+  public Timestamp getReleasetime() {
+    return releasetime;
+  }
+
+  public void setReleasetime(Timestamp releasetime) {
+    this.releasetime = releasetime;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public Integer getHits() {
+    return hits;
+  }
+
+  public void setHits(Integer hits) {
+    this.hits = hits;
+  }
+
+  public String getIslink() {
+    return islink;
+  }
+
+  public void setIslink(String islink) {
+    this.islink = islink;
+  }
+
+  public String getIsdisplay() {
+    return isdisplay;
+  }
+
+  public void setIsdisplay(String isdisplay) {
+    this.isdisplay = isdisplay;
+  }
+
+  public String getIsdelete() {
+    return isdelete;
+  }
+
+  public void setIsdelete(String isdelete) {
+    this.isdelete = isdelete;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getColumnname() {
+    return columnname;
+  }
+
+  public void setColumnname(String columnname) {
+    this.columnname = columnname;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBColumn.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBColumn.java
new file mode 100644
index 0000000..500ac27
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBColumn.java
@@ -0,0 +1,131 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "tb_column")
+public class TBColumn {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "columnid", nullable = false, length = 32)
+  private String columnid;
+
+  @Column(name = "parentid", length = 32)
+  private String parentid;
+
+  @Column(name = "isleaf", length = 1,nullable = false)
+  private String isleaf;
+
+  @Column(name = "name", length = 30,unique = true,nullable = false)
+  private String name;
+
+  /**
+   * 是否需要审核后才能发布
+   */
+  @Column(name = "needreview", length = 1,nullable = false)
+  private String needreview;
+
+  /**
+   * 是否公开:游客登录只能查看公开栏目下的文章
+   */
+  @Column(name = "ispublic", length = 1,nullable = false)
+  private String ispublic;
+
+  /**
+   * 是否能发布:该栏目下的文章是否能发布
+   */
+  @Column(name = "publishable", length = 1,nullable = false)
+  private String publishable;
+
+  @Column(name = "code", length = 30,unique = true,nullable = false)
+  private String code;
+
+  @Column(name = "ordernum")
+  private Integer ordernum;
+
+  @Transient
+  private String parentname;
+
+  public String getColumnid() {
+    return columnid;
+  }
+
+  public void setColumnid(String columnid) {
+    this.columnid = columnid;
+  }
+
+  public String getParentid() {
+    return parentid;
+  }
+
+  public void setParentid(String parentid) {
+    this.parentid = parentid;
+  }
+
+  public String getIsleaf() {
+    return isleaf;
+  }
+
+  public void setIsleaf(String isleaf) {
+    this.isleaf = isleaf;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getNeedreview() {
+    return needreview;
+  }
+
+  public void setNeedreview(String needreview) {
+    this.needreview = needreview;
+  }
+
+  public String getIspublic() {
+    return ispublic;
+  }
+
+  public void setIspublic(String ispublic) {
+    this.ispublic = ispublic;
+  }
+
+  public String getPublishable() {
+    return publishable;
+  }
+
+  public void setPublishable(String publishable) {
+    this.publishable = publishable;
+  }
+
+  public String getCode() {
+    return code;
+  }
+
+  public void setCode(String code) {
+    this.code = code;
+  }
+
+  public String getParentname() {
+    return parentname;
+  }
+
+  public void setParentname(String parentname) {
+    this.parentname = parentname;
+  }
+
+  public Integer getOrdernum() {
+    return ordernum;
+  }
+
+  public void setOrdernum(Integer ordernum) {
+    this.ordernum = ordernum;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/util/PortalConstant.java b/backend/src/main/java/com/supwisdom/dlpay/portal/util/PortalConstant.java
index 4de5a35..fe195fd 100644
--- a/backend/src/main/java/com/supwisdom/dlpay/portal/util/PortalConstant.java
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/util/PortalConstant.java
@@ -1,8 +1,17 @@
 package com.supwisdom.dlpay.portal.util;
 
-public class PortalConstant{
+public class PortalConstant {
   public static final String YES = "1";
   public static final String NO = "0";
 
   public static final String SYSPARA_IMAGESERVER_URL = "imageserver.url.image";
+  public static final String SYSPARA_IMAGE_MAXSIZE = "imagemaxsize";
+  public static final String SYSPARA_IMAGE_MINISIZE = "minimagesize";
+  public static final String SYSPARA_IMAGE_URLASSIGN = "imageserver.url.assign";
+  public static final String SYSPARA_IMAGE_URLPUSH = "imageserver.url.push";
+  public static final String SYSPARA_ARTICLE_CURRENTNO = "article.currentno";
+
+  public static final String ARTICLE_STATUS_SAVE = "save";
+  public static final String ARTICLE_STATUS_RELEASED = "released";
+  public static final String ARTICLE_STATUS_REVIEW = "review";
 }
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/util/PutImageToServer.java b/backend/src/main/java/com/supwisdom/dlpay/portal/util/PutImageToServer.java
new file mode 100644
index 0000000..c43b076
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/util/PutImageToServer.java
@@ -0,0 +1,161 @@
+package com.supwisdom.dlpay.portal.util;
+
+import com.google.gson.Gson;
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.WebResource;
+import com.sun.jersey.api.client.config.ClientConfig;
+import com.sun.jersey.api.client.config.DefaultClientConfig;
+import com.sun.jersey.multipart.FormDataBodyPart;
+import com.sun.jersey.multipart.FormDataMultiPart;
+import com.sun.jersey.multipart.file.StreamDataBodyPart;
+import com.sun.jersey.multipart.impl.MultiPartWriter;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import org.apache.log4j.Logger;
+
+import javax.ws.rs.core.MediaType;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * Created by jov on 2017/1/10.
+ */
+public class PutImageToServer {
+    public static Gson gson = new Gson();
+    private static Logger logger = Logger.getLogger(PutImageToServer.class);
+
+    public static boolean postImage(String url, InputStream inputStream) {
+        try {
+            FormDataMultiPart part = new FormDataMultiPart();
+            part.bodyPart(new StreamDataBodyPart("files", inputStream));
+            part.bodyPart(new FormDataBodyPart("cm","true"));
+            ClientConfig cc = new DefaultClientConfig();
+            cc.getClasses().add(MultiPartWriter.class);
+            Client c = Client.create(cc);
+            c.setConnectTimeout(10 * 1000);
+            WebResource resource = c.resource(url);
+            ClientResponse response = resource.type(MediaType.MULTIPART_FORM_DATA_TYPE).post(ClientResponse.class, part);
+            logger.error("code=" + response.getStatus() + ",ret=" + response.getEntity(String.class));
+            if (response.getStatus() / 100 == 2) {
+                return true;
+            } else {
+                return false;
+            }
+        } catch (Exception e) {
+            logger.error("error:",e);
+            return false;
+        }
+    }
+
+    public static ImageBean postBefore(String url) {
+        try {
+            Client c = Client.create();
+            c.setConnectTimeout(10 * 1000);
+            WebResource resource = c.resource(url);
+            ClientResponse response = resource.get(ClientResponse.class);
+            if (response.getStatus() == 200) {
+                String ret = response.getEntity(String.class);
+                ImageBean imageBean = gson.fromJson(ret, ImageBean.class);
+                return imageBean;
+            } else {
+                logger.error("code=" + response.getStatus() + ",ret=" + response.getEntity(String.class));
+                return null;
+            }
+        } catch (Exception e) {
+            logger.error("error:",e);
+            return null;
+        }
+    }
+
+    public static boolean downloadFileFromUrl(String img, File file) {
+        if (StringUtil.isEmpty(img) || file == null) {
+            return false;
+        }
+        if (!file.exists()) {
+            file.mkdir();
+        }
+        try {
+            URL url = new URL(img);
+            DataInputStream dataInputStream = new DataInputStream(url.openStream());
+            FileOutputStream fileOutputStream = new FileOutputStream(file);
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = dataInputStream.read(buffer)) > 0) {
+                fileOutputStream.write(buffer, 0, length);
+            }
+            dataInputStream.close();
+            fileOutputStream.close();
+            return true;
+        } catch (MalformedURLException e) {
+            logger.error("error:",e);
+        } catch (IOException e) {
+            logger.error("error:",e);
+        }
+        return false;
+    }
+
+    @XmlRootElement
+    public class ImageBean {
+        private String fid;
+        private String url;
+        private String publicUrl;
+        private int count;
+        private List<LocalBean> locations;
+
+        public String getFid() {
+            return fid;
+        }
+
+        public void setFid(String fid) {
+            this.fid = fid;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+
+        public void setUrl(String url) {
+            this.url = url;
+        }
+
+        public String getPublicUrl() {
+            return publicUrl;
+        }
+
+        public void setPublicUrl(String publicUrl) {
+            this.publicUrl = publicUrl;
+        }
+
+        public int getCount() {
+            return count;
+        }
+
+        public void setCount(int count) {
+            this.count = count;
+        }
+
+        public List<LocalBean> getLocations() {
+            return locations;
+        }
+
+        public void setLocations(List<LocalBean> locations) {
+            this.locations = locations;
+        }
+    }
+
+    @XmlRootElement
+    public class LocalBean {
+        private String publicUrl;
+
+        public String getPublicUrl() {
+            return publicUrl;
+        }
+
+        public void setPublicUrl(String publicUrl) {
+            this.publicUrl = publicUrl;
+        }
+    }
+}
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
index 089c1a5..43bc662 100644
--- a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
@@ -7,12 +7,13 @@
 import com.supwisdom.dlpay.framework.service.OperatorDetailService
 import com.supwisdom.dlpay.framework.service.SystemUtilService
 import com.supwisdom.dlpay.framework.util.StringUtil
-import com.supwisdom.dlpay.mobile.domain.TBMsg
-import com.supwisdom.dlpay.portal.bean.FeedbackSearchBean
-import com.supwisdom.dlpay.portal.bean.MsgTemplateSearchBean
-import com.supwisdom.dlpay.portal.bean.SendMsgBean
+import com.supwisdom.dlpay.portal.bean.*
+import com.supwisdom.dlpay.portal.domain.TBArticle
+import com.supwisdom.dlpay.portal.domain.TBColumn
 import com.supwisdom.dlpay.portal.domain.TBMsgTemplate
 import com.supwisdom.dlpay.portal.domain.TBReply
+import com.supwisdom.dlpay.portal.service.ArticleService
+import com.supwisdom.dlpay.portal.service.ColumnService
 import com.supwisdom.dlpay.portal.service.FeedbackService
 import com.supwisdom.dlpay.portal.service.MsgService
 import com.supwisdom.dlpay.portal.util.PortalConstant
@@ -23,6 +24,8 @@
 import org.springframework.http.ResponseEntity
 import org.springframework.security.core.context.SecurityContextHolder
 import org.springframework.web.bind.annotation.*
+import org.springframework.web.multipart.MultipartHttpServletRequest
+import java.util.regex.Pattern
 
 @RestController
 @RequestMapping("/portalapi")
@@ -39,6 +42,10 @@
     lateinit var systemUtilService: SystemUtilService
     @Autowired
     lateinit var msgService: MsgService
+    @Autowired
+    lateinit var articleService: ArticleService
+    @Autowired
+    lateinit var columnService: ColumnService
     val logger = KotlinLogging.logger { }
 
     @RequestMapping("/test")
@@ -150,8 +157,8 @@
         }
     }
 
-    @RequestMapping(value = ["/template/save"],method = [RequestMethod.POST])
-    fun saveMsgTemplate(@RequestBody template:TBMsgTemplate):JsonResult?{
+    @RequestMapping(value = ["/template/save"], method = [RequestMethod.POST])
+    fun saveMsgTemplate(@RequestBody template: TBMsgTemplate): JsonResult? {
         return try {
             val p = SecurityContextHolder.getContext().authentication
             val oper = operatorDetailService.findByOperid(p.name)
@@ -164,8 +171,8 @@
         }
     }
 
-    @RequestMapping(value = ["/template/sendmsg"],method = [RequestMethod.POST])
-    fun sendMsg(@RequestBody bean: SendMsgBean):JsonResult?{
+    @RequestMapping(value = ["/template/sendmsg"], method = [RequestMethod.POST])
+    fun sendMsg(@RequestBody bean: SendMsgBean): JsonResult? {
         return try {
             msgService.sendMsg(bean)
             return JsonResult.ok()
@@ -174,4 +181,86 @@
             JsonResult.error(e.message)
         }
     }
+
+    @RequestMapping(value = ["/article/uploadpic"], method = [RequestMethod.POST])
+    fun uploadPic(request: MultipartHttpServletRequest): JsonResult? {
+        val map = articleService.uploadPic(request)
+        return JsonResult.ok()
+    }
+
+    @RequestMapping("/column/all")
+    fun getAllColumn(): JsonResult? {
+        return try {
+            val list = columnService.getAllColumnList()
+            if (list == null || list.isEmpty()) {
+                return JsonResult.ok().put("msg", "无数据")
+            }
+            return JsonResult.ok().put("list", list)
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("查询所有栏目列表异常")
+        }
+    }
+
+    @RequestMapping("/column/list")
+    fun getColumnList(bean: ColumnSearchBean): JsonResult? {
+        return try {
+            val page = columnService.getColumnList(bean)
+            if (page.list == null || page.list.size == 0) {
+                return JsonResult.ok().put("msg", "无数据")
+            }
+            return JsonResult.ok().put("page", page)
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("查询栏目列表异常")
+        }
+    }
+
+    @RequestMapping(value = ["/column/save"], method = [RequestMethod.POST])
+    fun saveColumn(@RequestBody column: TBColumn): JsonResult? {
+        return try {
+            val matches = Pattern.matches("^[a-zA-Z0-9]{1,30}$", column.code)
+            if (!matches) {
+                return JsonResult.error("请输入合法的栏目CODE!")
+            }
+            val msg = columnService.saveColumn(column)
+            return if (StringUtil.isEmpty(msg)) {
+                JsonResult.ok()
+            } else {
+                JsonResult.error(msg)
+            }
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("保存栏目异常")
+        }
+    }
+
+    @RequestMapping(value = ["/article/save"], method = [RequestMethod.POST])
+    fun saveArticle(@RequestBody article: TBArticle): JsonResult? {
+        return try {
+            val p = SecurityContextHolder.getContext().authentication
+            val oper = operatorDetailService.findByOperid(p.name)
+            article.operid = oper.operid
+            articleService.saveArticle(article)
+            JsonResult.ok()
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("保存文章异常")
+        }
+    }
+
+    @RequestMapping("/article/list")
+    fun getArticleList(bean: ArticleSearchBean): JsonResult? {
+        return try {
+            val page = articleService.getArticleList(bean)
+            if (page.list == null || page.list.size == 0) {
+                return JsonResult.ok().put("msg", "无数据")
+            }
+            return JsonResult.ok().put("page", page)
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("查询文章列表异常")
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/ArticleSearchBean.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/ArticleSearchBean.kt
new file mode 100644
index 0000000..322bb0e
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/ArticleSearchBean.kt
@@ -0,0 +1,12 @@
+package com.supwisdom.dlpay.portal.bean
+
+class ArticleSearchBean {
+    var title: String = ""
+    var status: String = ""
+    var savestartdate: String = ""
+    var saveenddate: String = ""
+    var releasestartdate: String = ""
+    var releaseenddate: String = ""
+    var pageno: Int = 0
+    var pagesize: Int = 10
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/ColumnSearchBean.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/ColumnSearchBean.kt
new file mode 100644
index 0000000..c4cbfbf
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/ColumnSearchBean.kt
@@ -0,0 +1,8 @@
+package com.supwisdom.dlpay.portal.bean
+
+class ColumnSearchBean {
+    var name: String = ""
+    var code: String = ""
+    var pageno: Int = 0
+    var pagesize: Int = 10
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/AnnexDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/AnnexDao.kt
index 59968d8..eb91d8f 100644
--- a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/AnnexDao.kt
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/AnnexDao.kt
@@ -5,6 +5,6 @@
 import org.springframework.stereotype.Repository
 
 @Repository
-interface AnnexDao : JpaRepository<TBAnnex, String>, FeedbackRepository {
+interface AnnexDao : JpaRepository<TBAnnex, String> {
     fun getByFbid(fbid: String): List<TBAnnex>
 }
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ArticleDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ArticleDao.kt
new file mode 100644
index 0000000..6f89cc4
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ArticleDao.kt
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.portal.domain.TBArticle
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface ArticleDao:JpaRepository<TBArticle,String>,ArticleRepository{
+
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ArticleRepository.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ArticleRepository.kt
new file mode 100644
index 0000000..9c27094
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ArticleRepository.kt
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.portal.bean.ArticleSearchBean
+import java.text.ParseException
+
+interface ArticleRepository {
+    @Throws(ParseException::class)
+    fun getArticleList(bean:ArticleSearchBean): Pagination
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ColumnDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ColumnDao.kt
new file mode 100644
index 0000000..fa53b3c
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ColumnDao.kt
@@ -0,0 +1,11 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.portal.domain.TBColumn
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface ColumnDao: JpaRepository<TBColumn, String>,ColumnRepository {
+    fun findByNameAndColumnidNot(name: String,columnid:String): TBColumn?
+    fun findByCodeAndColumnidNot(code:String,columnid:String): TBColumn?
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ColumnRepository.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ColumnRepository.kt
new file mode 100644
index 0000000..4a3c44f
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ColumnRepository.kt
@@ -0,0 +1,8 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.portal.bean.ColumnSearchBean
+
+interface ColumnRepository {
+    fun getColumnList(bean: ColumnSearchBean): Pagination
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/ArticleService.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/ArticleService.kt
new file mode 100644
index 0000000..3488b2f
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/ArticleService.kt
@@ -0,0 +1,16 @@
+package com.supwisdom.dlpay.portal.service
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.portal.bean.ArticleSearchBean
+import com.supwisdom.dlpay.portal.domain.TBArticle
+import org.springframework.transaction.annotation.Transactional
+import org.springframework.web.multipart.MultipartHttpServletRequest
+
+interface ArticleService {
+    @Transactional
+    fun uploadPic(request: MultipartHttpServletRequest): Map<String, String>
+    @Transactional
+    fun saveArticle(article: TBArticle)
+    @Transactional
+    fun getArticleList(bean:ArticleSearchBean): Pagination
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/ColumnService.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/ColumnService.kt
new file mode 100644
index 0000000..ea3db3e
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/ColumnService.kt
@@ -0,0 +1,15 @@
+package com.supwisdom.dlpay.portal.service
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.portal.bean.ColumnSearchBean
+import com.supwisdom.dlpay.portal.domain.TBColumn
+import org.springframework.transaction.annotation.Transactional
+
+interface ColumnService {
+    @Transactional
+    fun saveColumn(column:TBColumn):String?
+    @Transactional
+    fun getColumnList(bean: ColumnSearchBean): Pagination
+    @Transactional
+    fun getAllColumnList():List<TBColumn>?
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/FeedbackService.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/FeedbackService.kt
index 005de34..2b8e221 100644
--- a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/FeedbackService.kt
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/FeedbackService.kt
@@ -3,9 +3,13 @@
 import com.supwisdom.dlpay.framework.jpa.page.Pagination
 import com.supwisdom.dlpay.portal.bean.FeedbackSearchBean
 import com.supwisdom.dlpay.portal.domain.TBReply
+import org.springframework.transaction.annotation.Transactional
 
 interface FeedbackService {
+    @Transactional
     fun getFeedbackList(bean: FeedbackSearchBean): Pagination
+    @Transactional
     fun getReplyListByFbId(fbId: String): List<TBReply>
+    @Transactional
     fun saveReplyListByFbId(reply: TBReply):String?
 }
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/ArticleServiceImpl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/ArticleServiceImpl.kt
new file mode 100644
index 0000000..986639b
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/ArticleServiceImpl.kt
@@ -0,0 +1,237 @@
+package com.supwisdom.dlpay.portal.service.Impl
+
+import com.supwisdom.dlpay.framework.dao.BusinessparaDao
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.framework.util.StringUtil
+import com.supwisdom.dlpay.portal.bean.ArticleSearchBean
+import com.supwisdom.dlpay.portal.dao.ArticleDao
+import com.supwisdom.dlpay.portal.domain.TBArticle
+import com.supwisdom.dlpay.portal.service.ArticleService
+import com.supwisdom.dlpay.portal.util.PortalConstant
+import com.supwisdom.dlpay.portal.util.PutImageToServer
+import mu.KotlinLogging
+import net.coobird.thumbnailator.Thumbnails
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+import org.springframework.web.multipart.MultipartFile
+import org.springframework.web.multipart.MultipartHttpServletRequest
+import java.io.File
+import java.io.FileInputStream
+import java.io.IOException
+import java.lang.RuntimeException
+import java.util.*
+import kotlin.math.sqrt
+
+@Service
+class ArticleServiceImpl : ArticleService {
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    lateinit var articleDao: ArticleDao
+    @Autowired
+    lateinit var businessparaDao: BusinessparaDao
+
+    val logger = KotlinLogging.logger { }
+
+    override fun saveArticle(article: TBArticle) {
+        val timestamp = systemUtilService.sysdatetime.sysdate
+        val currentNoBusiness = businessparaDao.findByParakey(PortalConstant.SYSPARA_ARTICLE_CURRENTNO)
+                ?: throw RuntimeException("文章编号参数未配置")
+        val currentNo = currentNoBusiness.paraval.toInt()
+        currentNoBusiness.paraval = (currentNo + 1).toString()
+        businessparaDao.save(currentNoBusiness)
+        if (StringUtil.isEmpty(article.articleid)) {
+            article.createtime = timestamp
+            article.articleno = String.format("%08d", currentNo)
+            article.hits = 0
+            article.isdisplay = PortalConstant.YES
+            article.isdelete = PortalConstant.NO
+            article.status = PortalConstant.ARTICLE_STATUS_SAVE
+        }else{
+            article.updatetime = timestamp
+        }
+        articleDao.save(article)
+    }
+
+    override fun getArticleList(bean: ArticleSearchBean): Pagination {
+        return articleDao.getArticleList(bean)
+    }
+
+    override fun uploadPic(request: MultipartHttpServletRequest): Map<String, String> {
+        val map: MutableMap<String, String> = HashMap()
+        try {
+            logger.error("===put up pic")
+            val pathDir = "/files/"
+            /** 得到图片保存目录的真实路径  */
+            val logoRealPathDir = request.session.servletContext
+                    .getRealPath(pathDir)
+            /** 根据真实路径创建目录  */
+            val logoSaveFile = File(logoRealPathDir)
+            if (!logoSaveFile.exists()) logoSaveFile.mkdirs()
+            /** 页面控件的文件流  */
+            val multipartFile: MultipartFile
+            multipartFile = request.getFile("file")
+            //System.out.println(multipartFile);
+            /* 获取文件的后缀 */
+            val suffix = multipartFile.originalFilename.substring(
+                    multipartFile.originalFilename.lastIndexOf("."))
+            //* 拼成完整的文件保存路径加文件 *//*
+            val name = System.currentTimeMillis().toString() + suffix
+            val fileName = logoRealPathDir + File.separator + name
+            val file = File(fileName)
+            // String data = file.getPath();
+            try {
+                multipartFile.transferTo(file)
+            } catch (e: IllegalStateException) {
+                logger.error("error:", e)
+            } catch (e: IOException) {
+                logger.error("error:", e)
+            }
+            //savePicToDb(fileName, goodsidString);*/
+            //String filepath = logoRealPathDir + File.separator + multipartFile.getOriginalFilename();
+            val size = multipartFile.size
+            var maxSize = 200 * 1024.toLong()
+            val imageMaxConf = systemUtilService.getBusinessValue(PortalConstant.SYSPARA_IMAGE_MAXSIZE)
+            if (imageMaxConf != null && !StringUtil.isEmpty(imageMaxConf)) {
+                try {
+                    maxSize = imageMaxConf.toLong()
+                } catch (e: Exception) {
+                }
+            }
+            var minwidth = 150
+            var minheight = 150
+            val imageMinConf = systemUtilService.getBusinessValue(PortalConstant.SYSPARA_IMAGE_MINISIZE)
+            if (imageMinConf != null && !StringUtil.isEmpty(imageMinConf)) {
+                try {
+                    val temps = imageMinConf.split(",")
+                    if (temps.size == 2) {
+                        minwidth = temps[0].toInt()
+                        minheight = temps[1].toInt()
+                    }
+                } catch (e: Exception) {
+                    logger.error("error:", e)
+                }
+            }
+            logger.error("===put up pic 1")
+            var baseURL = systemUtilService.getBusinessValue(PortalConstant.SYSPARA_IMAGE_URLASSIGN)
+            if (baseURL == null || StringUtil.isEmpty(baseURL)) {
+                map["retmsg"] = "系统未配置图片服务器地址,无法保存图片!"
+                map["retcode"] = "1"
+                return map
+            }
+            if (!baseURL.startsWith("http")) {
+                baseURL = "http://$baseURL"
+            }
+            if (baseURL.endsWith("/")) {
+                baseURL = baseURL.substring(0, baseURL.length - 1)
+            }
+            logger.error("===put up pic 2")
+            var minImageURL: String
+            val picid: String
+            val minpicid: String
+            var imageURL = systemUtilService.getBusinessValue(PortalConstant.SYSPARA_IMAGE_URLPUSH)
+            if (imageURL == null || StringUtil.isEmpty(imageURL)) {
+                map["retmsg"] = "系统未配置图片服务器地址,无法保存图片!"
+                map["retcode"] = "1"
+                return map
+            }
+            logger.error("===put up pic 4")
+            if (!imageURL.startsWith("http")) {
+                imageURL = "http://$imageURL"
+            }
+            if (!imageURL.endsWith("/")) {
+                imageURL += "/"
+            }
+            minImageURL = imageURL
+            if (!minImageURL.startsWith("http")) {
+                minImageURL = "http://$minImageURL"
+            }
+            if (!minImageURL.endsWith("/")) {
+                minImageURL += "/"
+            }
+            val assign = "$baseURL/dir/assign?count=2"
+            val bean = PutImageToServer.postBefore(assign)
+            if (bean == null || StringUtil.isEmpty(bean.getFid())) {
+                map["retmsg"] = "图片上传失败,无法连接图片服务器!"
+                map["retcode"] = "1"
+                return map
+            }
+            imageURL += bean.fid
+            picid = bean.fid
+            val minImgBean = PutImageToServer.postBefore(assign)
+            if (minImgBean == null || StringUtil.isEmpty(minImgBean.getFid())) {
+                map["retmsg"] = "图片上传失败,无法连接图片服务器!"
+                map["retcode"] = "1"
+                return map
+            }
+            minpicid = minImgBean.fid
+            minImageURL += minImgBean.fid
+            //}
+            val processToFile = logoRealPathDir + File.separator + picid + ".jpg"
+            var scale = 1.0
+            if (size > 0 && size > maxSize) {
+                scale = maxSize * 1.0 / size
+            }
+            try {
+                if (scale < 0.7) {
+                    val s = sqrt(maxSize.toDouble()).toInt()
+                    Thumbnails.of(fileName).size(s, s).outputFormat("jpg").toFile(processToFile)
+                } else {
+                    Thumbnails.of(fileName).scale(1.0).outputQuality(scale).outputFormat("jpg").toFile(processToFile)
+                }
+                logger.error("===put up pic 5")
+            } catch (e: Exception) {
+                map["retmsg"] = "图片上传失败,压缩失败"
+                map["retcode"] = "1"
+                file.delete()
+                return map
+            }
+            val file1 = File(processToFile)
+            val processToMinFile = logoRealPathDir + File.separator + minpicid + ".jpg"
+            try {
+                Thumbnails.of(fileName).size(minwidth, minheight).outputFormat("jpg").toFile(processToMinFile)
+            } catch (e: Exception) {
+                map["retmsg"] = "图片上传失败,压缩失败"
+                map["retcode"] = "1"
+                file.delete()
+                file1.delete()
+                return map
+            }
+            val file2 = File(processToMinFile)
+            logger.error("==========imageurl=$imageURL")
+            if (!PutImageToServer.postImage(imageURL, FileInputStream(file1))) {
+                map["retmsg"] = "图片上传失败,请稍后再试"
+                map["retcode"] = "1"
+                file1.delete()
+                file2.delete()
+                file.delete()
+                logger.error("=step1 fail=")
+                return map
+            }
+            if (!PutImageToServer.postImage(minImageURL, FileInputStream(file2))) {
+                map["retmsg"] = "图片上传失败,请稍后再试"
+                map["retcode"] = "1"
+                file1.delete()
+                file2.delete()
+                file.delete()
+                logger.error("=step2 fail=")
+            } else {
+                map["retmsg"] = "上传成功"
+                map["retcode"] = "0"
+                file1.delete()
+                file2.delete()
+                file.delete()
+                map["picid"] = picid
+                map["minpicid"] = minpicid
+                logger.error("=step3 success=")
+            }
+        } catch (e: Exception) {
+            map["retmsg"] = "图片上传失败--" + e.message
+            map["retcode"] = "1"
+            // return map;
+            logger.error("=step fail=", e)
+        }
+        return map
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/ColumnServiceImpl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/ColumnServiceImpl.kt
new file mode 100644
index 0000000..f83e441
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/ColumnServiceImpl.kt
@@ -0,0 +1,48 @@
+package com.supwisdom.dlpay.portal.service.Impl
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.framework.util.StringUtil
+import com.supwisdom.dlpay.portal.bean.ColumnSearchBean
+import com.supwisdom.dlpay.portal.dao.ColumnDao
+import com.supwisdom.dlpay.portal.domain.TBColumn
+import com.supwisdom.dlpay.portal.service.ColumnService
+import com.supwisdom.dlpay.portal.util.PortalConstant
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+
+@Service
+class ColumnServiceImpl : ColumnService {
+    @Autowired
+    lateinit var columnDao: ColumnDao
+
+    override fun getColumnList(bean: ColumnSearchBean): Pagination {
+        return columnDao.getColumnList(bean)
+    }
+
+    override fun getAllColumnList(): List<TBColumn>? {
+        return columnDao.findAll()
+    }
+
+    override fun saveColumn(column: TBColumn):String? {
+        var columnid = "####"
+        if (!StringUtil.isEmpty(column.columnid)) {
+            columnid = column.columnid
+        }else{
+            if (StringUtil.isEmpty(column.parentid)) {
+                column.isleaf = PortalConstant.NO
+            } else {
+                column.isleaf = PortalConstant.YES
+            }
+        }
+        val nameColumn = columnDao.findByNameAndColumnidNot(column.name,columnid)
+        if (nameColumn!=null){
+            return "栏目名称已存在,请更换!"
+        }
+        val codeColumn = columnDao.findByCodeAndColumnidNot(column.code,columnid)
+        if (codeColumn!=null){
+            return "栏目CODE已存在,请更换!"
+        }
+        columnDao.save(column)
+        return null
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/MsgService.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/MsgService.kt
index ebec659..f874828 100644
--- a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/MsgService.kt
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/MsgService.kt
@@ -4,9 +4,13 @@
 import com.supwisdom.dlpay.portal.bean.MsgTemplateSearchBean
 import com.supwisdom.dlpay.portal.bean.SendMsgBean
 import com.supwisdom.dlpay.portal.domain.TBMsgTemplate
+import org.springframework.transaction.annotation.Transactional
 
 interface MsgService {
+    @Transactional
     fun getMsgTemplateList(bean:MsgTemplateSearchBean):Pagination
+    @Transactional
     fun saveMsgTemplate(template:TBMsgTemplate)
+    @Transactional
     fun sendMsg(bean: SendMsgBean)
 }
diff --git a/backend/src/test/java/test.java b/backend/src/test/java/test.java
index 1659da6..e7b34d4 100644
--- a/backend/src/test/java/test.java
+++ b/backend/src/test/java/test.java
@@ -1,3 +1,5 @@
+import org.junit.Test;
+
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -29,4 +31,9 @@
     }
     System.out.println(sqlFragment);
   }
+
+  @Test
+  public void test() {
+    System.out.println(String.format("%08d",2));
+  }
 }
diff --git a/frontend/src/api/article.js b/frontend/src/api/article.js
index 407bda1..a05932e 100644
--- a/frontend/src/api/article.js
+++ b/frontend/src/api/article.js
@@ -1,5 +1,32 @@
 import request from '@/utils/request'
 
+export function uploadImage(data) {
+  return request({
+    url: '/article/uploadpic',
+    method: 'post',
+    headers: {
+      'content-type': 'multipart/form-data'
+    },
+    data
+  })
+}
+
+export function saveArticle(data) {
+  return request({
+    url: '/article/save',
+    method: 'post',
+    data
+  })
+}
+
+export function getArticleList(query) {
+  return request({
+    url: '/article/list',
+    method: 'get',
+    params: query
+  })
+}
+
 export function fetchList(query) {
   return request({
     url: '/vue-element-admin/article/list',
diff --git a/frontend/src/api/column.js b/frontend/src/api/column.js
new file mode 100644
index 0000000..32616b6
--- /dev/null
+++ b/frontend/src/api/column.js
@@ -0,0 +1,24 @@
+import request from '@/utils/request'
+
+export function getColumnList(query) {
+  return request({
+    url: '/column/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getAllColumn() {
+  return request({
+    url: '/column/all',
+    method: 'get'
+  })
+}
+
+export function saveColumnList(data) {
+  return request({
+    url: '/column/save',
+    method: 'post',
+    data
+  })
+}
diff --git a/frontend/src/components/Tinymce/components/EditorImage.vue b/frontend/src/components/Tinymce/components/EditorImage.vue
index 07d48e6..366bb4f 100644
--- a/frontend/src/components/Tinymce/components/EditorImage.vue
+++ b/frontend/src/components/Tinymce/components/EditorImage.vue
@@ -1,7 +1,7 @@
 <template>
   <div class="upload-container">
     <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
-      upload
+      上传本地图片
     </el-button>
     <el-dialog :visible.sync="dialogVisible">
       <el-upload
@@ -12,18 +12,19 @@
         :on-success="handleSuccess"
         :before-upload="beforeUpload"
         class="editor-slide-upload"
-        action="https://httpbin.org/post"
+        action="#"
+        :http-request="uploadImage"
         list-type="picture-card"
       >
         <el-button size="small" type="primary">
-          Click upload
+          点击上传
         </el-button>
       </el-upload>
       <el-button @click="dialogVisible = false">
-        Cancel
+        取消
       </el-button>
       <el-button type="primary" @click="handleSubmit">
-        Confirm
+        确认
       </el-button>
     </el-dialog>
   </div>
@@ -31,6 +32,7 @@
 
 <script>
 // import { getToken } from 'api/qiniu'
+import { uploadImage } from '@/api/article'
 
 export default {
   name: 'EditorSlideUpload',
@@ -44,17 +46,34 @@
     return {
       dialogVisible: false,
       listObj: {},
-      fileList: []
+      fileList: [],
+      url: ''
     }
   },
+  created() {
+    this.url = process.env.VUE_APP_BASE_API
+  },
   methods: {
     checkAllSuccess() {
       return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
     },
+    uploadImage(file) {
+      const params = new FormData()
+      params.append('file', file.file)
+      uploadImage(params).then(response => {
+        console.log(response)
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '图片上传异常',
+          type: 'error'
+        })
+        this.listLoading = false
+      })
+    },
     handleSubmit() {
       const arr = Object.keys(this.listObj).map(v => this.listObj[v])
       if (!this.checkAllSuccess()) {
-        this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
+        this.$message('请稍候,图片正在上传。如果网络出现问题, 请刷新页面重新上传!')
         return
       }
       this.$emit('successCBK', arr)
@@ -63,6 +82,7 @@
       this.dialogVisible = false
     },
     handleSuccess(response, file) {
+      console.log(response)
       const uid = file.uid
       const objKeyArr = Object.keys(this.listObj)
       for (let i = 0, len = objKeyArr.length; i < len; i++) {
diff --git a/frontend/src/components/Tinymce/index.vue b/frontend/src/components/Tinymce/index.vue
index cb6b91c..7a9877f 100644
--- a/frontend/src/components/Tinymce/index.vue
+++ b/frontend/src/components/Tinymce/index.vue
@@ -116,7 +116,7 @@
       const _this = this
       window.tinymce.init({
         selector: `#${this.tinymceId}`,
-        language: this.languageTypeList['en'],
+        language: this.languageTypeList['zh'],
         height: this.height,
         body_class: 'panel-body ',
         object_resizing: false,
diff --git a/frontend/src/store/modules/permission.js b/frontend/src/store/modules/permission.js
index f1d5583..9e2aa4e 100644
--- a/frontend/src/store/modules/permission.js
+++ b/frontend/src/store/modules/permission.js
@@ -22,9 +22,19 @@
 export function generaMenu(routes, data) {
   data.forEach(item => {
     // alert(JSON.stringify(item))
+    if (item.respath === '/article/list') {
+      const editArticle = {
+        path: '/article/edit/:id(\\d+)',
+        component: (resolve) => require([`@/views/article/edit`], resolve),
+        name: 'EditArticle',
+        meta: { title: '编辑文章', id: '1c039c677ded4f1dab63a4fa9c0a27ca', noCache: true, roles: ['admin'] },
+        hidden: true
+      }
+      routes.push(editArticle)
+    }
     const menu = {
       path: item.respath === '#' ? item.resid + '_key' : item.respath,
-      component: item.respath === '#' ? Layout : (resolve) => require([`@/views${item.respath}/index`], resolve),
+      component: item.respath === '#' ? Layout : (resolve) => require([`@/views${item.respath}`], resolve),
       // hidden: true,
       children: [],
       name: 'menu_' + item.resid,
diff --git a/frontend/src/views/article/components/ArticleDetail.vue b/frontend/src/views/article/components/ArticleDetail.vue
new file mode 100644
index 0000000..f634220
--- /dev/null
+++ b/frontend/src/views/article/components/ArticleDetail.vue
@@ -0,0 +1,248 @@
+<template>
+  <div class="createPost-container">
+    <el-form ref="postForm" :model="postForm" :rules="rules" class="form-container">
+      <sticky :z-index="10" :class-name="'sub-navbar '+postForm.status">
+        <el-button v-loading="loading" style="margin-left: 10px;" type="primary" @click="submitForm">
+          发布
+        </el-button>
+        <el-button v-loading="loading" type="primary" @click="draftForm">
+          保存
+        </el-button>
+      </sticky>
+
+      <div class="createPost-main-container">
+        <el-row>
+
+          <el-col :span="24">
+            <el-form-item style="margin-bottom: 40px;" prop="title">
+              <MDinput v-model="postForm.title" :maxlength="30" name="name" required>
+                标题
+              </MDinput>
+            </el-form-item>
+
+            <div class="postInfo-container">
+              <el-row>
+                <el-col :span="8">
+                  <el-form-item label-width="100px" prop="columnid" label="所属栏目:" class="postInfo-container-item">
+                    <el-select
+                      v-model="postForm.columnid"
+                      no-data-text="当前无栏目,请先新建栏目"
+                      filterable
+                      default-first-option
+                      placeholder="搜索栏目"
+                    >
+                      <el-option
+                        v-for="(item,index) in columnList"
+                        :key="item+index"
+                        :label="item.name"
+                        :value="item.columnid"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </div>
+          </el-col>
+        </el-row>
+
+        <el-form-item prop="content" style="margin-bottom: 30px;">
+          <Tinymce ref="editor" v-model="postForm.content" :height="400" />
+        </el-form-item>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import Tinymce from '@/components/Tinymce'
+import MDinput from '@/components/MDinput'
+import Sticky from '@/components/Sticky' // 粘性header组件
+import { saveArticle } from '@/api/article'
+import { getAllColumn } from '@/api/column'
+
+const defaultForm = {
+  status: 'draft',
+  title: '', // 文章题目
+  content: '', // 文章内容
+  columnid: '',
+  display_time: undefined, // 前台展示时间
+  articleid: undefined,
+  islink: ''
+}
+
+export default {
+  name: 'ArticleDetail',
+  components: { Tinymce, MDinput, Sticky },
+  props: {
+    isEdit: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      postForm: Object.assign({}, defaultForm),
+      loading: false,
+      columnList: [],
+      rules: {
+        title: [{ required: true, message: '请输入文章标题', trigger: 'blur' }],
+        columnid: [{ required: true, message: '请选择文章栏目', trigger: 'blur' }],
+        content: [{ required: true, message: '文章内容不能为空', trigger: 'blur' }]
+      },
+      tempRoute: {}
+    }
+  },
+  computed: {
+    contentShortLength() {
+      return this.postForm.content_short.length
+    },
+    lang() {
+      return this.$store.getters.language
+    },
+    displayTime: {
+      // set and get is useful when the data
+      // returned by the back end api is different from the front end
+      // back end return => "2013-06-25 06:59:25"
+      // front end need timestamp => 1372114765000
+      get() {
+        return (+new Date(this.postForm.display_time))
+      },
+      set(val) {
+        this.postForm.display_time = new Date(val)
+      }
+    }
+  },
+  created() {
+    if (this.isEdit) {
+      const id = this.$route.params && this.$route.params.id
+      this.fetchData(id)
+    }
+
+    // Why need to make a copy of this.$route here?
+    // Because if you enter this page and quickly switch tag, may be in the execution of the setTagsViewTitle function, this.$route is no longer pointing to the current page
+    // https://github.com/PanJiaChen/vue-element-admin/issues/1221
+    this.tempRoute = Object.assign({}, this.$route)
+    getAllColumn().then(response => {
+      if (response.list) {
+        this.columnList = response.list
+      } else {
+        this.columnList = []
+      }
+    }).catch(error => {
+      this.$message({
+        message: error.msg || '请求异常',
+        type: 'error'
+      })
+    })
+  },
+  methods: {
+    fetchData(id) {
+      // fetchArticle(id).then(response => {
+      //   this.postForm = response.data
+
+      //   // just for test
+      //   this.postForm.title += `   Article Id:${this.postForm.id}`
+      //   this.postForm.content_short += `   Article Id:${this.postForm.id}`
+
+      //   // set tagsview title
+      //   this.setTagsViewTitle()
+
+      //   // set page title
+      //   this.setPageTitle()
+      // }).catch(err => {
+      //   console.log(err)
+      // })
+    },
+    setTagsViewTitle() {
+      const title = this.lang === 'zh' ? '编辑文章' : 'Edit Article'
+      const route = Object.assign({}, this.tempRoute, { title: `${title}-${this.postForm.id}` })
+      this.$store.dispatch('tagsView/updateVisitedView', route)
+    },
+    setPageTitle() {
+      const title = 'Edit Article'
+      document.title = `${title} - ${this.postForm.id}`
+    },
+    submitForm() {
+      this.$refs.postForm.validate(valid => {
+        if (valid) {
+          this.loading = true
+          this.$notify({
+            title: '成功',
+            message: '发布文章成功',
+            type: 'success',
+            duration: 2000
+          })
+          this.postForm.status = 'published'
+          this.loading = false
+        } else {
+          console.log('error submit!!')
+          return false
+        }
+      })
+    },
+    draftForm() {
+      if (this.postForm.content.length === 0 || this.postForm.title.length === 0) {
+        this.$message({
+          message: '请填写必要的标题和内容',
+          type: 'warning'
+        })
+        return
+      }
+      this.postForm.islink = '0'
+      saveArticle(this.postForm).then(response => {
+        this.$message({
+          message: '保存成功',
+          type: 'success',
+          showClose: true,
+          duration: 1000
+        })
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+      })
+      this.postForm.status = 'draft'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "~@/styles/mixin.scss";
+
+.createPost-container {
+  position: relative;
+
+  .createPost-main-container {
+    padding: 40px 45px 20px 50px;
+
+    .postInfo-container {
+      position: relative;
+      @include clearfix;
+      margin-bottom: 10px;
+
+      .postInfo-container-item {
+        float: left;
+      }
+    }
+  }
+
+  .word-counter {
+    width: 40px;
+    position: absolute;
+    right: 10px;
+    top: 0px;
+  }
+}
+
+.article-textarea ::v-deep {
+  textarea {
+    padding-right: 40px;
+    resize: none;
+    border: none;
+    border-radius: 0px;
+    border-bottom: 1px solid #bfcbd9;
+  }
+}
+</style>
diff --git a/frontend/src/views/article/components/Dropdown/Comment.vue b/frontend/src/views/article/components/Dropdown/Comment.vue
new file mode 100644
index 0000000..d34b2b9
--- /dev/null
+++ b/frontend/src/views/article/components/Dropdown/Comment.vue
@@ -0,0 +1,41 @@
+<template>
+  <el-dropdown :show-timeout="100" trigger="click">
+    <el-button plain>
+      {{ !comment_disabled?'Comment: opened':'Comment: closed' }}
+      <i class="el-icon-caret-bottom el-icon--right" />
+    </el-button>
+    <el-dropdown-menu slot="dropdown" class="no-padding">
+      <el-dropdown-item>
+        <el-radio-group v-model="comment_disabled" style="padding: 10px;">
+          <el-radio :label="true">
+            Close comment
+          </el-radio>
+          <el-radio :label="false">
+            Open comment
+          </el-radio>
+        </el-radio-group>
+      </el-dropdown-item>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    comment_disabled: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/article/components/Dropdown/Platform.vue b/frontend/src/views/article/components/Dropdown/Platform.vue
new file mode 100644
index 0000000..0a52726
--- /dev/null
+++ b/frontend/src/views/article/components/Dropdown/Platform.vue
@@ -0,0 +1,46 @@
+<template>
+  <el-dropdown :hide-on-click="false" :show-timeout="100" trigger="click">
+    <el-button plain>
+      Platfroms({{ platforms.length }})
+      <i class="el-icon-caret-bottom el-icon--right" />
+    </el-button>
+    <el-dropdown-menu slot="dropdown" class="no-border">
+      <el-checkbox-group v-model="platforms" style="padding: 5px 15px;">
+        <el-checkbox v-for="item in platformsOptions" :key="item.key" :label="item.key">
+          {{ item.name }}
+        </el-checkbox>
+      </el-checkbox-group>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      required: true,
+      default: () => [],
+      type: Array
+    }
+  },
+  data() {
+    return {
+      platformsOptions: [
+        { key: 'a-platform', name: 'a-platform' },
+        { key: 'b-platform', name: 'b-platform' },
+        { key: 'c-platform', name: 'c-platform' }
+      ]
+    }
+  },
+  computed: {
+    platforms: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/article/components/Dropdown/SourceUrl.vue b/frontend/src/views/article/components/Dropdown/SourceUrl.vue
new file mode 100644
index 0000000..8f47485
--- /dev/null
+++ b/frontend/src/views/article/components/Dropdown/SourceUrl.vue
@@ -0,0 +1,38 @@
+<template>
+  <el-dropdown :show-timeout="100" trigger="click">
+    <el-button plain>
+      Link
+      <i class="el-icon-caret-bottom el-icon--right" />
+    </el-button>
+    <el-dropdown-menu slot="dropdown" class="no-padding no-border" style="width:400px">
+      <el-form-item label-width="0px" style="margin-bottom: 0px" prop="source_uri">
+        <el-input v-model="source_uri" placeholder="Please enter the content">
+          <template slot="prepend">
+            URL
+          </template>
+        </el-input>
+      </el-form-item>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    source_uri: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/article/components/Dropdown/index.js b/frontend/src/views/article/components/Dropdown/index.js
new file mode 100644
index 0000000..bc0c171
--- /dev/null
+++ b/frontend/src/views/article/components/Dropdown/index.js
@@ -0,0 +1,3 @@
+export { default as CommentDropdown } from './Comment'
+export { default as PlatformDropdown } from './Platform'
+export { default as SourceUrlDropdown } from './SourceUrl'
diff --git a/frontend/src/views/article/create.vue b/frontend/src/views/article/create.vue
new file mode 100644
index 0000000..f28ce28
--- /dev/null
+++ b/frontend/src/views/article/create.vue
@@ -0,0 +1,13 @@
+<template>
+  <article-detail :is-edit="false" />
+</template>
+
+<script>
+import ArticleDetail from './components/ArticleDetail'
+
+export default {
+  name: 'CreateArticle',
+  components: { ArticleDetail }
+}
+</script>
+
diff --git a/frontend/src/views/article/edit.vue b/frontend/src/views/article/edit.vue
new file mode 100644
index 0000000..87b6126
--- /dev/null
+++ b/frontend/src/views/article/edit.vue
@@ -0,0 +1,13 @@
+<template>
+  <article-detail :is-edit="true" />
+</template>
+
+<script>
+import ArticleDetail from './components/ArticleDetail'
+
+export default {
+  name: 'EditForm',
+  components: { ArticleDetail }
+}
+</script>
+
diff --git a/frontend/src/views/article/list.vue b/frontend/src/views/article/list.vue
new file mode 100644
index 0000000..1ae114d
--- /dev/null
+++ b/frontend/src/views/article/list.vue
@@ -0,0 +1,261 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-item" style="margin-right:15px">文章标题</div>
+      <el-input
+        v-model="formData.title"
+        placeholder="输入文章标题"
+        style="width: 320px;margin-right:50px"
+        class="filter-item"
+      />
+      <div class="filter-item" style="margin-right:15px">发布日期</div>
+      <el-date-picker
+        v-model="releaseDate"
+        type="daterange"
+        align="left"
+        style="width:320px;margin-right:50px"
+        unlink-panels
+        range-separator="至"
+        start-placeholder="开始日期"
+        end-placeholder="结束日期"
+        value-format="yyyyMMdd"
+        :picker-options="pickerOptions"
+      />
+    </div>
+    <div class="filter-container">
+      <div class="filter-item" style="margin-right:15px">保存日期</div>
+      <el-date-picker
+        v-model="saveDate"
+        type="daterange"
+        align="left"
+        style="width:320px;margin-right:50px"
+        unlink-panels
+        range-separator="至"
+        start-placeholder="开始日期"
+        end-placeholder="结束日期"
+        value-format="yyyyMMdd"
+        :picker-options="pickerOptions"
+      />
+      <div class="filter-item" style="margin-right:15px">文章状态</div>
+      <el-select
+        v-model="formData.status"
+        style="width:200px;margin-right:120px"
+      >
+        <el-option
+          v-for="item in statusOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+
+      <el-button
+        class="filter-item"
+        type="primary"
+        icon="el-icon-search"
+        @click="handleFilter()"
+      >
+        搜索
+      </el-button>
+      <el-button class="filter-item" type="info" @click="clearFilter">
+        清空
+      </el-button>
+    </div>
+    <el-table v-loading="listLoading" :data="list" border fit highlight-current-row style="width: 100%">
+      <el-table-column align="center" label="编号" width="100">
+        <template slot-scope="{row}">
+          <span>{{ row.articleno }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="标题">
+        <template slot-scope="{row}">
+          <span>{{ row.title }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="120px" align="center" label="栏目">
+        <template slot-scope="{row}">
+          <span>{{ row.columnname }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="100px" label="状态" align="center">
+        <template slot-scope="{row}">
+          <el-tag v-if="row.status==='save'" effect="dark" size="medium">已保存</el-tag>
+          <el-tag v-if="row.status==='released'" effect="dark" type="success" size="medium">已发布</el-tag>
+          <el-tag v-if="row.status==='review'" effect="dark" type="warning" size="medium">审核中</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column width="100px" label="保存时间">
+        <template slot-scope="{row}">
+          <span>{{ dateFormat(row.createtime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column width="100px" label="发布时间">
+        <template slot-scope="{row}">
+          <span>{{ dateFormat(row.releasetime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column width="100px" label="是否展示">
+        <template slot-scope="{row}">
+          <span>{{ row.isdisplay }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column width="100px" label="操作">
+        <template slot-scope="{}">
+          <span>删除</span>
+        </template>
+      </el-table-column>
+
+      <pagination v-show="total>0" :total="total" :page.sync="formData.page" :limit.sync="formData.limit" @pagination="getArticleList" />
+    </el-table></div>
+</template>
+
+<script>
+import moment from 'moment'
+import { getArticleList } from '@/api/article'
+import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
+
+export default {
+  name: 'ArticleList',
+  components: { Pagination },
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        published: 'success',
+        draft: 'info',
+        deleted: 'danger'
+      }
+      return statusMap[status]
+    }
+  },
+  data() {
+    return {
+      list: null,
+      total: 0,
+      listLoading: false,
+      formData: {
+        title: '',
+        status: '',
+        savestartdate: '',
+        saveenddate: '',
+        releasestartdate: '',
+        releaseenddate: '',
+        pageno: 1,
+        pagesize: 20
+      },
+      saveDate: null,
+      releaseDate: null,
+      statusOptions: [{
+        value: '',
+        label: '所有'
+      },
+      {
+        value: 'save',
+        label: '已保存'
+      }, {
+        value: 'released',
+        label: '已发布'
+      }, {
+        value: 'review',
+        label: '审核中'
+      }],
+      pickerOptions: {
+        shortcuts: [{
+          text: '今天',
+          onClick(picker) {
+            const end = new Date()
+            const start = new Date()
+            picker.$emit('pick', [start, end])
+          }
+        }, {
+          text: '最近三天',
+          onClick(picker) {
+            const end = new Date()
+            const start = new Date()
+            start.setTime(start.getTime() - 3600 * 1000 * 24 * 3)
+            picker.$emit('pick', [start, end])
+          }
+        }, {
+          text: '最近一周',
+          onClick(picker) {
+            const end = new Date()
+            const start = new Date()
+            start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
+            picker.$emit('pick', [start, end])
+          }
+        }]
+      }
+    }
+  },
+  created() {
+    this.getArticleList()
+  },
+  methods: {
+    getArticleList() {
+      this.listLoading = true
+      const saveDate = this.saveDate
+      const releaseDate = this.releaseDate
+      if (saveDate != null) {
+        this.formData.savestartdate = saveDate[0]
+        this.formData.saveenddate = saveDate[1]
+      } else {
+        this.formData.savestartdate = ''
+        this.formData.saveenddate = ''
+      }
+      if (releaseDate != null) {
+        this.formData.releasestartdate = releaseDate[0]
+        this.formData.releaseenddate = releaseDate[1]
+      } else {
+        this.formData.releasestartdate = ''
+        this.formData.releaseenddate = ''
+      }
+      getArticleList(this.formData).then(response => {
+        console.log(response)
+        if (response.page) {
+          this.list = response.page.list
+          this.total = response.page.totalCount
+        } else {
+          this.list = null
+          this.total = 0
+        }
+        this.listLoading = false
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+        this.listLoading = false
+      })
+    },
+    handleFilter() {
+      this.formData.pageno = 1
+      this.getArticleList()
+    },
+    clearFilter() {
+      this.formData.title = ''
+      this.formData.status = ''
+      this.saveDate = null
+      this.releaseDate = null
+    },
+    dateFormat(date) {
+      if (date === null) {
+        return ''
+      }
+      return moment(date).format('YYYY-MM-DD HH:mm:ss')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.edit-input {
+  padding-right: 100px;
+}
+.cancel-btn {
+  position: absolute;
+  right: 15px;
+  top: 10px;
+}
+</style>
diff --git a/frontend/src/views/column/index.vue b/frontend/src/views/column/index.vue
new file mode 100644
index 0000000..42e1e7d
--- /dev/null
+++ b/frontend/src/views/column/index.vue
@@ -0,0 +1,340 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-item" style="margin-right:15px">栏目名称</div>
+      <el-input
+        v-model="formData.name"
+        placeholder="输入栏目名称"
+        style="width: 350px;margin-right:50px"
+        class="filter-item"
+      />
+      <div class="filter-item" style="margin-right:15px">栏目CODE</div>
+      <el-input
+        v-model="formData.code"
+        placeholder="输入栏目CODE"
+        style="width: 350px;margin-right:50px"
+        class="filter-item"
+      />
+      <el-button
+        class="filter-item"
+        type="primary"
+        icon="el-icon-search"
+        @click="handleFilter()"
+      >
+        搜索
+      </el-button>
+    </div>
+    <el-button type="primary" icon="el-icon-circle-plus-outline" @click="addColumn()">
+      新增栏目
+    </el-button>
+    <el-table
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;margin-top:10px"
+    >
+      <el-table-column label="上级栏目">
+        <template slot-scope="{row}">
+          <span>{{ row.parentname }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="栏目CODE" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.code }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="栏目名称" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="序号" align="center" width="70">
+        <template slot-scope="{row}">
+          <span>{{ row.ordernum }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否公开" width="100" align="center">
+        <template slot-scope="{row}">
+          <i
+            v-if="row.ispublic==='1'"
+            style="color:#67C23A;font-size:20px"
+            class="el-icon-circle-check"
+          />
+          <i
+            v-else
+            style="color:#F56C6C;font-size:20px"
+            class="el-icon-circle-close"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否要审核" width="100" align="center">
+        <template slot-scope="{row}">
+          <i
+            v-if="row.needreview==='1'"
+            style="color:#67C23A;font-size:20px"
+            class="el-icon-circle-check"
+          />
+          <i
+            v-else
+            style="color:#F56C6C;font-size:20px"
+            class="el-icon-circle-close"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否能发布" width="100" align="center">
+        <template slot-scope="{row}">
+          <i
+            v-if="row.publishable==='1'"
+            style="color:#67C23A;font-size:20px"
+            class="el-icon-circle-check"
+          />
+          <i
+            v-else
+            style="color:#F56C6C;font-size:20px"
+            class="el-icon-circle-close"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="180">
+        <template slot-scope="{row}">
+          <el-tooltip class="item" effect="dark" content="修改" placement="bottom">
+            <el-button type="primary" icon="el-icon-edit" circle size="mini" @click="updateColumn(row)" />
+          </el-tooltip>
+          <el-tooltip v-if="row.isleaf==='0'" class="item" effect="dark" content="添加子栏目" placement="bottom">
+            <el-button type="primary" size="mini" @click="addSubColumn(row)">添加子栏目</el-button>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="formData.pageno"
+      :limit.sync="formData.pagesize"
+      style="margin-top:0;"
+      @pagination="getColumnList"
+    />
+    <el-dialog
+      :title="title"
+      :visible.sync="columnDialogVisible"
+      width="30%"
+    >
+      <div>
+        <el-form ref="columnForm" :model="columnForm" :rules="rules" label-width="100px">
+          <el-form-item label="栏目名称" prop="name" class="form-input-item">
+            <el-input
+              v-model="columnForm.name"
+              maxlength="30"
+              show-word-limit
+              style="width:80%"
+            />
+          </el-form-item>
+          <el-form-item label="栏目CODE" prop="code" class="form-input-item">
+            <el-input
+              v-model="columnForm.code"
+              maxlength="30"
+              show-word-limit
+              style="width:80%"
+              placeholder="只能由字母或数字组成"
+            />
+          </el-form-item>
+          <el-form-item label="是否公开" prop="ispublic">
+            <el-select
+              v-model="columnForm.ispublic"
+              style="width:80%"
+              placeholder="游客只能浏览公开内容"
+            >
+              <el-option label="是" value="1" />
+              <el-option label="否" value="0" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="是否要审核" prop="needreview">
+            <el-select
+              v-model="columnForm.needreview"
+              style="width:80%"
+              placeholder="栏目下文章审核后才能发布"
+            >
+              <el-option label="是" value="1" />
+              <el-option label="否" value="0" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="是否能发布" prop="publishable">
+            <el-select
+              v-model="columnForm.publishable"
+              style="width:80%"
+              placeholder="该栏目下不能发布文章"
+            >
+              <el-option label="是" value="1" />
+              <el-option label="否" value="0" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="栏目排序" prop="ordernum">
+            <el-input v-model="columnForm.ordernum" style="width:80%" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div style="text-align:center">
+        <el-button
+          type="primary"
+          @click="saveColumn('columnForm')"
+        >保存
+        </el-button>
+        <el-button @click="columnDialogVisible = false">取消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import {
+  getColumnList,
+  saveColumnList
+} from '@/api/column'
+import Pagination from '@/components/Pagination'
+export default {
+  name: 'Column',
+  components: {
+    Pagination
+  },
+  data() {
+    return {
+      formData: {
+        name: '',
+        code: '',
+        pageno: 1,
+        pagesize: 10
+      },
+      listLoading: false,
+      tableKey: 0,
+      list: null,
+      total: 0,
+      title: '',
+      columnForm: {
+        columnid: '',
+        name: '',
+        code: '',
+        ispublic: '',
+        needreview: '',
+        publishable: '',
+        parentid: '',
+        ordernum: null
+      },
+      columnDialogVisible: false,
+      rules: {
+        name: [
+          { required: true, message: '请输入栏目名称', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入栏目CODE', trigger: 'blur' },
+          { pattern: /^[0-9a-zA-Z]{1,30}$/, message: '只能由字母或数字组成', trigger: 'blur' }
+        ],
+        ispublic: [
+          { required: true, message: '请选择是否公开', trigger: 'change' }
+        ],
+        needreview: [
+          { required: true, message: '请选择是否需要审核', trigger: 'change' }
+        ],
+        publishable: [
+          { required: true, message: '请选择是否能发布', trigger: 'change' }
+        ],
+        ordernum: [
+          { pattern: /^[0-9]{1,10}$/, message: '只能输入数字', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  created() {
+    this.getColumnList()
+  },
+  methods: {
+    getColumnList() {
+      this.listLoading = true
+      getColumnList(this.formData).then(response => {
+        if (response.page) {
+          this.list = response.page.list
+          this.total = response.page.totalCount
+        } else {
+          this.list = null
+          this.total = 0
+        }
+        this.listLoading = false
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+        this.listLoading = false
+      })
+    },
+    handleFilter() {
+      this.formData.pageno = 1
+      this.getColumnList()
+    },
+    addColumn() {
+      this.title = '新增栏目'
+      this.resetForm('columnForm')
+      this.columnDialogVisible = true
+    },
+    updateColumn(row) {
+      this.title = '修改栏目'
+      this.resetForm('columnForm')
+      this.columnForm = Object.assign({}, row)
+      this.columnDialogVisible = true
+    },
+    addSubColumn(row) {
+      this.title = '[' + row.name + ']添加子栏目'
+      this.resetForm('columnForm')
+      this.columnForm.parentid = row.columnid
+      this.columnDialogVisible = true
+    },
+    saveColumn(formName) {
+      this.$refs[formName].validate((valid) => {
+        if (valid) {
+          saveColumnList(this.columnForm).then(response => {
+            this.$notify({
+              title: '成功',
+              message: '保存成功!',
+              type: 'success',
+              duration: 2000
+            })
+            this.columnDialogVisible = false
+            this.getColumnList()
+          }).catch(error => {
+            this.$message({
+              message: error.msg || '请求异常',
+              type: 'error'
+            })
+          })
+        } else {
+          return false
+        }
+      })
+    },
+    resetForm(formName) {
+      this.columnForm = {
+        columnid: '',
+        name: '',
+        code: '',
+        ispublic: '',
+        needreview: '',
+        publishable: '',
+        parentid: '',
+        ordernum: null
+      }
+      this.$nextTick(() => {
+        this.$refs[formName].clearValidate()
+      })
+    }
+  }
+}
+</script>
+<style>
+.form-input-item
+  .el-input__inner{
+    padding-right: 50px;
+  }
+
+</style>
diff --git a/frontend/src/views/example/components/ArticleDetail.vue b/frontend/src/views/example/components/ArticleDetail.vue
index 157497b..1c8fcbe 100644
--- a/frontend/src/views/example/components/ArticleDetail.vue
+++ b/frontend/src/views/example/components/ArticleDetail.vue
@@ -16,7 +16,6 @@
 
       <div class="createPost-main-container">
         <el-row>
-          <Warning />
 
           <el-col :span="24">
             <el-form-item style="margin-bottom: 40px;" prop="title">
@@ -83,7 +82,6 @@
 import { validURL } from '@/utils/validate'
 import { fetchArticle } from '@/api/article'
 import { searchUser } from '@/api/remote-search'
-import Warning from './Warning'
 import { CommentDropdown, PlatformDropdown, SourceUrlDropdown } from './Dropdown'
 
 const defaultForm = {
@@ -102,7 +100,7 @@
 
 export default {
   name: 'ArticleDetail',
-  components: { Tinymce, MDinput, Upload, Sticky, Warning, CommentDropdown, PlatformDropdown, SourceUrlDropdown },
+  components: { Tinymce, MDinput, Upload, Sticky, CommentDropdown, PlatformDropdown, SourceUrlDropdown },
   props: {
     isEdit: {
       type: Boolean,
@@ -153,6 +151,9 @@
     contentShortLength() {
       return this.postForm.content_short.length
     },
+    lang() {
+      return this.$store.getters.language
+    },
     displayTime: {
       // set and get is useful when the data
       // returned by the back end api is different from the front end
@@ -196,7 +197,7 @@
       })
     },
     setTagsViewTitle() {
-      const title = 'Edit Article'
+      const title = this.lang === 'zh' ? '编辑文章' : 'Edit Article'
       const route = Object.assign({}, this.tempRoute, { title: `${title}-${this.postForm.id}` })
       this.$store.dispatch('tagsView/updateVisitedView', route)
     },
diff --git a/frontend/src/views/example/components/Warning.vue b/frontend/src/views/example/components/Warning.vue
index 8d2a7e5..d24fe29 100644
--- a/frontend/src/views/example/components/Warning.vue
+++ b/frontend/src/views/example/components/Warning.vue
@@ -1,9 +1,6 @@
 <template>
   <aside>
-    Creating and editing pages cannot be cached by keep-alive because keep-alive include does not currently support
-    caching based on routes, so it is currently cached based on component name. If you want to achieve a similar caching
-    effect, you can use a browser caching scheme such as localStorage. Or do not use keep-alive include to cache all
-    pages directly. See details
+    {{ $t('example.warning') }}
     <a
       href="https://panjiachen.github.io/vue-element-admin-site/guide/essentials/tags-view.html"
       target="_blank"