完成留言管理功能
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperRoleDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperRoleDao.java
deleted file mode 100644
index fa3dd5a..0000000
--- a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperRoleDao.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.supwisdom.dlpay.framework.dao;
-
-import com.supwisdom.dlpay.framework.domain.TOperRole;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.stereotype.Repository;
-
-import java.util.List;
-
-@Repository
-public interface OperRoleDao extends JpaRepository<TOperRole, String> {
-
-  @Query(value = "select distinct rolecode from TB_OPER_ROLE a,TB_ROLE b where a.roleid=b.roleid and a.operid=?1", nativeQuery = true)
-  List<String> getRolecodeByOperid(String operid);
-
-  void deleteByRoleId(String roleId);
-
-  List<TOperRole> findAllByOperid(String operid);
-
-  void deleteByOperid(String operid);
-}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperRole.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperRole.java
deleted file mode 100644
index 54df513..0000000
--- a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperRole.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.supwisdom.dlpay.framework.domain;
-
-import org.hibernate.annotations.GenericGenerator;
-
-import javax.persistence.*;
-import javax.validation.constraints.NotNull;
-
-@Entity
-@Table(name = "TB_OPER_ROLE",
-    indexes = {@Index(name = "operrole_operid_idx", columnList = "operid")})
-public class TOperRole {
-  @Id
-  @GenericGenerator(name = "idGenerator", strategy = "uuid")
-  @GeneratedValue(generator = "idGenerator")
-  @Column(name = "ID", nullable = false, length = 32)
-  private String id;
-
-  @Column(name = "ROLEID", length = 32)
-  @NotNull
-  private String roleId;
-
-  @Column(name = "OPERID", length = 32)
-  @NotNull
-  private String operid;
-
-  @Column(name = "tenantid", length = 20)
-  @NotNull
-  private String tenantId;
-
-  public String getId() {
-    return id;
-  }
-
-  public void setId(String id) {
-    this.id = id;
-  }
-
-  public String getRoleId() {
-    return roleId;
-  }
-
-  public void setRoleId(String roleId) {
-    this.roleId = roleId;
-  }
-
-  public String getOperid() {
-    return operid;
-  }
-
-  public void setOperid(String operid) {
-    this.operid = operid;
-  }
-
-  public String getTenantId() {
-    return tenantId;
-  }
-
-  public void setTenantId(String tenantId) {
-    this.tenantId = tenantId;
-  }
-}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperator.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperator.java
index 8e8f83b..be38fed 100644
--- a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperator.java
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperator.java
@@ -59,8 +59,9 @@
 
   @Column(name = "CLOSEDATE", length = 8)
   private String closedate;
-  @Transient
-  private String roleids;
+
+  @Column(name = "ROLEID", length = 32)
+  private String roleid;
 
   @Column(name = "tenantid", length = 20)
   @NotNull
@@ -70,6 +71,9 @@
   @NotNull
   private String thirdadmin;
 
+  @Column(name = "jti", length = 64)
+  private String jti;
+
   @Transient
   private Collection<? extends GrantedAuthority> authorities;  //权限
 
@@ -81,7 +85,7 @@
     this.opername = opername;
   }
 
-  public TOperator(String opercode, String opertype, String opername, String operpwd, String status, String sex, String mobile, String email, String opendate, String closedate, Collection<? extends GrantedAuthority> authorities) {
+  public TOperator(String opercode, String opertype, String opername, String operpwd, String status, String sex, String mobile, String email, String opendate, String closedate,String jti, Collection<? extends GrantedAuthority> authorities) {
     this.opercode = opercode;
     this.opertype = opertype;
     this.opername = opername;
@@ -92,6 +96,7 @@
     this.email = email;
     this.opendate = opendate;
     this.closedate = closedate;
+    this.jti = jti;
     this.authorities = authorities;
   }
 
@@ -183,6 +188,14 @@
     this.closedate = closedate;
   }
 
+  public String getJti() {
+    return jti;
+  }
+
+  public void setJti(String jti) {
+    this.jti = jti;
+  }
+
   public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
     this.authorities = authorities;
   }
@@ -222,12 +235,12 @@
     return !TradeDict.STATUS_CLOSED.equals(this.status);  //注销操作员不启用
   }
 
-  public String getRoleids() {
-    return roleids;
+  public String getRoleid() {
+    return roleid;
   }
 
-  public void setRoleids(String roleids) {
-    this.roleids = roleids;
+  public void setRoleid(String roleid) {
+    this.roleid = roleid;
   }
 
   public String getTenantId() {
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/BaseRepository.java b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/BaseRepository.java
new file mode 100644
index 0000000..34cecc8
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/BaseRepository.java
@@ -0,0 +1,113 @@
+package com.supwisdom.dlpay.framework.jpa;
+
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination;
+import org.hibernate.query.NativeQuery;
+import org.hibernate.transform.ResultTransformer;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.persistence.EntityManager;
+import javax.persistence.Query;
+import java.util.*;
+
+public class BaseRepository {
+    @Autowired
+    protected EntityManager entityManager;
+
+    /**
+     * 查询全部(不分页) HQL 写法
+     *
+     * */
+    protected List find(Finder finder) {
+        Query query = entityManager.createQuery(finder.getOrigHql());
+        finder.setParamsToQuery(query);
+        return query.getResultList();
+    }
+
+    /**
+     * 分页方法 HQL 写法
+     * 直接查询Entity对象
+     *
+     * */
+    protected Pagination find(Finder finder, int pageNo, int pageSize) {
+        int totalCount = countQueryResult(finder);
+        Pagination p = new Pagination(pageNo, pageSize, totalCount);
+        if (totalCount < 1) {
+            p.setList(new ArrayList());
+            return p;
+        }
+        Query query = entityManager.createQuery(finder.getOrigHql());
+        finder.setParamsToQuery(query);
+        query.setFirstResult(p.getFirstResult());
+        query.setMaxResults(p.getPageSize());
+        List list = query.getResultList();
+        p.setList(list);
+        return p;
+    }
+    /**
+     *
+     * 分页方法 Native SQL 写法
+     * 原生SQL
+     * 使用finder.addScalar()方法,添加字段映射
+     *
+     * */
+    protected Pagination findNative(Finder finder, ResultTransformer transformer,
+                                    int pageNo, int pageSize) {
+        int totalCount = countNativeQueryResult(finder);
+        Pagination p = new Pagination(pageNo, pageSize, totalCount);
+        if (totalCount < 1) {
+            p.setList(new ArrayList());
+            return p;
+        }
+        Query query = entityManager.createNativeQuery(finder.getOrigHql());
+        finder.setParamsToQuery(query);
+        NativeQuery nativeQuery = query.unwrap(NativeQuery.class);
+        if(finder.columAlias!=null){
+            Set<Map.Entry<String, org.hibernate.type.Type>> entrySet = finder.columAlias.entrySet();
+            Iterator<Map.Entry<String, org.hibernate.type.Type>> iter = entrySet.iterator();
+            while (iter.hasNext())
+            {
+                Map.Entry<String, org.hibernate.type.Type> entry = iter.next();
+                nativeQuery.addScalar(entry.getKey(),entry.getValue());
+            }
+        }
+        if(transformer!=null){
+            nativeQuery.setResultTransformer(transformer);
+        }
+        query.setFirstResult(p.getFirstResult());
+        query.setMaxResults(p.getPageSize());
+        List list = nativeQuery.getResultList();
+        p.setList(list);
+        return p;
+    }
+    protected int countNativeQueryResult(Finder finder) {
+        Query query = entityManager.createNativeQuery(finder.getRowCountHql());
+        finder.setParamsToQuery(query);
+        return ((Number) query.getSingleResult()).intValue();
+    }
+
+    protected int countQueryResult(Finder finder) {
+        Query query = entityManager.createQuery(finder.getRowCountHql());
+        finder.setParamsToQuery(query);
+        return ((Number) query.getSingleResult()).intValue();
+    }
+    /**
+     * 创建本地SQL语句的方法,不带addScalor等
+     * */
+    protected NativeQuery createNativeQuery(String sql){
+        return entityManager.createNativeQuery(sql).unwrap(NativeQuery.class);
+    }
+    /**
+     * 创建本地SQL语句的方法,与createNativeQuery类似,区别是不需要转换类型
+     * */
+    protected Query createNavQuery(String sql){
+        return entityManager.createNativeQuery(sql);
+    }
+
+    /**
+     * Hql 语句
+     * */
+    protected Query createQuery(String hql){
+        return entityManager.createQuery(hql);
+    }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/Finder.java b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/Finder.java
new file mode 100644
index 0000000..a344ce8
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/Finder.java
@@ -0,0 +1,287 @@
+package com.supwisdom.dlpay.framework.jpa;
+
+import com.google.common.collect.Maps;
+import org.hibernate.Session;
+import org.hibernate.type.Type;
+
+import javax.persistence.Query;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * HQL语句分页查询
+ */
+public class Finder {
+	protected Finder() {
+		hqlBuilder = new StringBuilder();
+	}
+	protected Map<String, Type> columAlias = Maps.newHashMap();
+	protected Finder(String hql) {
+		hqlBuilder = new StringBuilder(hql);
+		columAlias.clear();
+	}
+
+	public static Finder create() {
+		return new Finder();
+	}
+
+	public static Finder create(String hql) {
+
+		return new Finder(hql);
+	}
+	public  Finder addScalar(String columnAlias, Type type){
+		columAlias.put(columnAlias,type);
+		return this;
+	}
+
+	public Finder append(String hql) {
+		hqlBuilder.append(hql);
+		return this;
+	}
+
+	/**
+	 * 获得原始hql语句
+	 * 
+	 * @return
+	 */
+	public String getOrigHql() {
+		return hqlBuilder.toString();
+	}
+
+	/**
+	 * 获得查询数据库记录数的hql语句。
+	 * 
+	 * @return
+	 */
+	public String getRowCountHql() {
+		String hql = hqlBuilder.toString();
+
+		int fromIndex = hql.toLowerCase().indexOf(FROM);
+		String projectionHql = hql.substring(0, fromIndex);
+
+		hql = hql.substring(fromIndex);
+		String rowCountHql = hql.replace(HQL_FETCH, "");
+
+		int groupIndex = hql.toLowerCase().indexOf(GROUP_BY);
+		int orderIndex = hql.toLowerCase().indexOf(ORDER_BY);
+		if (orderIndex > 0) {
+			rowCountHql = rowCountHql.substring(0, orderIndex);
+		}
+		if (groupIndex > 0) {
+			return wrapProjectionWithGroupBy(projectionHql) + rowCountHql +") t ";
+		} else {
+			return wrapProjection(projectionHql) + rowCountHql;
+		}
+
+	}
+
+	public int getFirstResult() {
+		return firstResult;
+	}
+
+	public void setFirstResult(int firstResult) {
+		this.firstResult = firstResult;
+	}
+
+	public int getMaxResults() {
+		return maxResults;
+	}
+
+	public void setMaxResults(int maxResults) {
+		this.maxResults = maxResults;
+	}
+
+	/**
+	 * 是否使用查询缓存
+	 * 
+	 * @return
+	 */
+	public boolean isCacheable() {
+		return cacheable;
+	}
+
+	/**
+	 * 设置是否使用查询缓存
+	 * 
+	 * @param cacheable
+	 */
+	public void setCacheable(boolean cacheable) {
+		this.cacheable = cacheable;
+	}
+
+	/**
+	 * 设置参数
+	 *
+	 * @param param
+	 * @param value
+	 * @return
+	 * @see Query#setParameter(String, Object)
+	 */
+	public Finder setParameter(String param, Object value) {
+		return setParameter(param, value, null);
+	}
+
+	/**
+	 * 设置参数。与hibernate的Query接口一致。
+	 *
+	 * @param param
+	 * @param value
+	 * @param type
+	 * @return
+	 */
+	public Finder setParameter(String param, Object value, Type type) {
+		getParams().add(param);
+		getValues().add(value);
+		getTypes().add(type);
+		return this;
+	}
+
+	/**
+	 * 将finder中的参数设置到query中。
+	 *
+	 * @param query
+	 */
+	public Query setParamsToQuery(Query query) {
+		if (params != null) {
+			for (int i = 0; i < params.size(); i++) {
+				if (types.get(i) == null) {
+					query.setParameter(params.get(i), values.get(i));
+				}
+			}
+		}
+		if (paramsList != null) {
+			for (int i = 0; i < paramsList.size(); i++) {
+				if (typesList.get(i) == null) {
+					query.setParameter(paramsList.get(i), valuesList.get(i));
+				}
+			}
+		}
+		if (paramsArray != null) {
+			for (int i = 0; i < paramsArray.size(); i++) {
+				if (typesArray.get(i) == null) {
+					query.setParameter(paramsArray.get(i),
+							valuesArray.get(i));
+				}
+			}
+		}
+		return query;
+	}
+
+	public Query createQuery(Session s) {
+		Query query = setParamsToQuery(s.createQuery(getOrigHql()));
+		if (getFirstResult() > 0) {
+			query.setFirstResult(getFirstResult());
+		}
+		if (getMaxResults() > 0) {
+			query.setMaxResults(getMaxResults());
+		}
+		return query;
+	}
+
+	private String wrapProjection(String projection) {
+		return ROW_COUNT;
+	}
+
+	private String wrapProjectionWithGroupBy(String projection) {
+		return " select  count(*) from ("+projection;
+
+	}
+
+	private List<String> getParams() {
+		if (params == null) {
+			params = new ArrayList<String>();
+		}
+		return params;
+	}
+
+	private List<Object> getValues() {
+		if (values == null) {
+			values = new ArrayList<Object>();
+		}
+		return values;
+	}
+
+	private List<Type> getTypes() {
+		if (types == null) {
+			types = new ArrayList<Type>();
+		}
+		return types;
+	}
+
+	private List<String> getParamsList() {
+		if (paramsList == null) {
+			paramsList = new ArrayList<String>();
+		}
+		return paramsList;
+	}
+
+	private List<Collection<Object>> getValuesList() {
+		if (valuesList == null) {
+			valuesList = new ArrayList<Collection<Object>>();
+		}
+		return valuesList;
+	}
+
+	private List<Type> getTypesList() {
+		if (typesList == null) {
+			typesList = new ArrayList<Type>();
+		}
+		return typesList;
+	}
+
+	private List<String> getParamsArray() {
+		if (paramsArray == null) {
+			paramsArray = new ArrayList<String>();
+		}
+		return paramsArray;
+	}
+
+	private List<Object[]> getValuesArray() {
+		if (valuesArray == null) {
+			valuesArray = new ArrayList<Object[]>();
+		}
+		return valuesArray;
+	}
+
+	private List<Type> getTypesArray() {
+		if (typesArray == null) {
+			typesArray = new ArrayList<Type>();
+		}
+		return typesArray;
+	}
+
+	private StringBuilder hqlBuilder;
+
+	private List<String> params;
+	private List<Object> values;
+	private List<Type> types;
+
+	private List<String> paramsList;
+	private List<Collection<Object>> valuesList;
+	private List<Type> typesList;
+
+	private List<String> paramsArray;
+	private List<Object[]> valuesArray;
+	private List<Type> typesArray;
+
+	private int firstResult = 0;
+
+	private int maxResults = 0;
+
+	private boolean cacheable = false;
+
+	public static final String ROW_COUNT = "select count(*) ";
+	public static final String FROM = "from";
+	public static final String HQL_FETCH = "fetch";
+	public static final String ORDER_BY = "order by";
+	public static final String GROUP_BY = "group by";
+
+	public static void main(String[] args) {
+		Finder find = Finder
+				.create("select distinct p FROM BookType join fetch p");
+		System.out.println(find.getRowCountHql());
+		System.out.println(find.getOrigHql());
+	}
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/Paginable.java b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/Paginable.java
new file mode 100644
index 0000000..1687fde
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/Paginable.java
@@ -0,0 +1,58 @@
+package com.supwisdom.dlpay.framework.jpa.page;
+
+/**
+ * 分页接口
+ */
+public interface Paginable {
+	/**
+	 * 总记录数
+	 * 
+	 * @return
+	 */
+	public int getTotalCount();
+
+	/**
+	 * 总页数
+	 * 
+	 * @return
+	 */
+	public int getTotalPage();
+
+	/**
+	 * 每页记录数
+	 * 
+	 * @return
+	 */
+	public int getPageSize();
+
+	/**
+	 * 当前页号
+	 * 
+	 * @return
+	 */
+	public int getPageNo();
+
+	/**
+	 * 是否第一页
+	 * 
+	 * @return
+	 */
+	public boolean isFirstPage();
+
+	/**
+	 * 是否最后一页
+	 * 
+	 * @return
+	 */
+	public boolean isLastPage();
+
+	/**
+	 * 返回下页的页号
+	 */
+	public int getNextPage();
+
+	/**
+	 * 返回上页的页号
+	 */
+	public int getPrePage();
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/Pagination.java b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/Pagination.java
new file mode 100644
index 0000000..38919de
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/Pagination.java
@@ -0,0 +1,85 @@
+package com.supwisdom.dlpay.framework.jpa.page;
+
+import org.springframework.data.domain.Page;
+
+import java.util.List;
+
+/**
+ * 列表分页。包含list属性。
+ */
+@SuppressWarnings("serial")
+public class Pagination extends SimplePage implements java.io.Serializable,
+		Paginable {
+
+	public Pagination() {
+	}
+
+	/**
+	 * 构造器
+	 * 
+	 * @param pageNo
+	 *            页码
+	 * @param pageSize
+	 *            每页几条数据
+	 * @param totalCount
+	 *            总共几条数据
+	 */
+	public Pagination(int pageNo, int pageSize, int totalCount) {
+		super(pageNo, pageSize, totalCount);
+	}
+
+	/**
+	 * 构造器
+	 * 
+	 * @param pageNo
+	 *            页码
+	 * @param pageSize
+	 *            每页几条数据
+	 * @param totalCount
+	 *            总共几条数据
+	 * @param list
+	 *            分页内容
+	 */
+	public Pagination(int pageNo, int pageSize, int totalCount, List<?> list) {
+		super(pageNo, pageSize, totalCount);
+		this.list = list;
+	}
+
+	/**
+	 * 第一条数据位置
+	 * 
+	 * @return
+	 */
+	public int getFirstResult() {
+		return (pageNo - 1) * pageSize;
+	}
+
+	/**
+	 * 当前页的数据
+	 */
+	private List<?> list;
+
+	/**
+	 * 获得分页内容
+	 * 
+	 * @return
+	 */
+	public List<?> getList() {
+		return list;
+	}
+
+	/**
+	 * 设置分页内容
+	 * 
+	 * @param list
+	 */
+	@SuppressWarnings("unchecked")
+	public void setList(List list) {
+		this.list = list;
+	}
+
+	public Pagination(Page page){
+		super(page.getNumber(),page.getSize(),(int)page.getTotalElements());
+		this.list = page.getContent();
+	}
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/SimplePage.java b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/SimplePage.java
new file mode 100644
index 0000000..6386838
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/jpa/page/SimplePage.java
@@ -0,0 +1,171 @@
+package com.supwisdom.dlpay.framework.jpa.page;
+
+/**
+ * 简单分页类
+ */
+public class SimplePage implements Paginable {
+	private static final long serialVersionUID = 1L;
+	public static final int DEF_COUNT = 20;
+
+	/**
+	 * 检查页码 checkPageNo
+	 * 
+	 * @param pageNo
+	 * @return if pageNo==null or pageNo<1 then return 1 else return pageNo
+	 */
+	public static int cpn(Integer pageNo) {
+		return (pageNo == null || pageNo < 1) ? 1 : pageNo;
+	}
+
+	public SimplePage() {
+	}
+
+	/**
+	 * 构造器
+	 * 
+	 * @param pageNo
+	 *            页码
+	 * @param pageSize
+	 *            每页几条数据
+	 * @param totalCount
+	 *            总共几条数据
+	 */
+	public SimplePage(int pageNo, int pageSize, int totalCount) {
+		setTotalCount(totalCount);
+		setPageSize(pageSize);
+		setPageNo(pageNo);
+		adjustPageNo();
+	}
+
+	/**
+	 * 调整页码,使不超过最大页数
+	 */
+	public void adjustPageNo() {
+		if (pageNo == 1) {
+			return;
+		}
+		int tp = getTotalPage();
+		if (pageNo > tp) {
+			pageNo = tp;
+		}
+	}
+
+	/**
+	 * 获得页码
+	 */
+	@Override
+  public int getPageNo() {
+		return pageNo;
+	}
+
+	/**
+	 * 每页几条数据
+	 */
+	@Override
+  public int getPageSize() {
+		return pageSize;
+	}
+
+	/**
+	 * 总共几条数据
+	 */
+	@Override
+  public int getTotalCount() {
+		return totalCount;
+	}
+
+	/**
+	 * 总共几页
+	 */
+	@Override
+  public int getTotalPage() {
+		int totalPage = totalCount / pageSize;
+		if (totalPage == 0 || totalCount % pageSize != 0) {
+			totalPage++;
+		}
+		return totalPage;
+	}
+
+	/**
+	 * 是否第一页
+	 */
+	@Override
+  public boolean isFirstPage() {
+		return pageNo <= 1;
+	}
+
+	/**
+	 * 是否最后一页
+	 */
+	@Override
+  public boolean isLastPage() {
+		return pageNo >= getTotalPage();
+	}
+
+	/**
+	 * 下一页页码
+	 */
+	@Override
+  public int getNextPage() {
+		if (isLastPage()) {
+			return pageNo;
+		} else {
+			return pageNo + 1;
+		}
+	}
+
+	/**
+	 * 上一页页码
+	 */
+	@Override
+  public int getPrePage() {
+		if (isFirstPage()) {
+			return pageNo;
+		} else {
+			return pageNo - 1;
+		}
+	}
+
+	protected int totalCount = 0;
+	protected int pageSize = 20;
+	protected int pageNo = 1;
+
+	/**
+	 * if totalCount<0 then totalCount=0
+	 * 
+	 * @param totalCount
+	 */
+	public void setTotalCount(int totalCount) {
+		if (totalCount < 0) {
+			this.totalCount = 0;
+		} else {
+			this.totalCount = totalCount;
+		}
+	}
+
+	/**
+	 * if pageSize< 1 then pageSize=DEF_COUNT
+	 * 
+	 * @param pageSize
+	 */
+	public void setPageSize(int pageSize) {
+		if (pageSize < 1) {
+			this.pageSize = DEF_COUNT;
+		} else {
+			this.pageSize = pageSize;
+		}
+	}
+
+	/**
+	 * if pageNo < 1 then pageNo=1
+	 * 
+	 * @param pageNo
+	 */
+	public void setPageNo(int pageNo) {
+		if (pageNo < 1) {
+			this.pageNo = 1;
+		} else {
+			this.pageNo = pageNo;
+		}
+	}
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/service/OperatorDetailService.java b/backend/src/main/java/com/supwisdom/dlpay/framework/service/OperatorDetailService.java
index bf72bfe..4c31b55 100644
--- a/backend/src/main/java/com/supwisdom/dlpay/framework/service/OperatorDetailService.java
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/service/OperatorDetailService.java
@@ -1,6 +1,15 @@
 package com.supwisdom.dlpay.framework.service;
 
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import com.supwisdom.dlpay.portal.domain.TBResource;
 import org.springframework.security.core.userdetails.UserDetailsService;
 
+import java.util.List;
+
 public interface OperatorDetailService extends UserDetailsService {
+  TOperator findByOperid(String operid);
+
+  TOperator saveOper(TOperator operator);
+
+  List<TBResource> getResByRoleId(String roleId);
 }
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/OperatorDetailServiceImpl.java b/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/OperatorDetailServiceImpl.java
index 755828d..d2e1153 100644
--- a/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/OperatorDetailServiceImpl.java
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/OperatorDetailServiceImpl.java
@@ -1,13 +1,12 @@
 package com.supwisdom.dlpay.framework.service.impl;
 
-import com.supwisdom.dlpay.framework.dao.OperRoleDao;
 import com.supwisdom.dlpay.framework.dao.OperatorDao;
 import com.supwisdom.dlpay.framework.domain.TOperator;
 import com.supwisdom.dlpay.framework.service.OperatorDetailService;
-import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.portal.dao.ResourceDao;
+import com.supwisdom.dlpay.portal.domain.TBResource;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
@@ -21,7 +20,7 @@
   @Autowired
   private OperatorDao operatorDao;
   @Autowired
-  private OperRoleDao operRoleDao;
+  private ResourceDao resourceDao;
 
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
@@ -32,11 +31,27 @@
     Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>() {
     };
 
-    List<String> roles = operRoleDao.getRolecodeByOperid(oper.getOperid());
-    if (!StringUtil.isEmpty(roles)) {
-      authorities = AuthorityUtils.createAuthorityList(roles.toArray(new String[0]));
-    }
     oper.setAuthorities(authorities);
     return oper;
   }
+
+  @Override
+  public TOperator findByOperid(String operid) {
+    return operatorDao.findByOperid(operid);
+  }
+
+  @Override
+  public TOperator saveOper(TOperator operator) {
+    return operatorDao.save(operator);
+  }
+
+  @Override
+  public List<TBResource> getResByRoleId(String roleId) {
+    List<TBResource> rootResource = resourceDao.findRootListByRole(roleId);
+    for (TBResource resource : rootResource) {
+      List<TBResource> children = resourceDao.findChildrenByRoleAndParent(roleId, resource.getResid());
+      resource.setChildren(children);
+    }
+    return rootResource;
+  }
 }
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/FeedbackRepositoryImpl.java b/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/FeedbackRepositoryImpl.java
new file mode 100644
index 0000000..ef6c456
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/FeedbackRepositoryImpl.java
@@ -0,0 +1,59 @@
+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.FeedbackSearchBean;
+import com.supwisdom.dlpay.portal.dao.FeedbackRepository;
+import com.supwisdom.dlpay.portal.domain.TBFeedback;
+import org.hibernate.transform.Transformers;
+import org.jetbrains.annotations.NotNull;
+
+public class FeedbackRepositoryImpl extends BaseRepository implements FeedbackRepository {
+  @NotNull
+  @Override
+  public Pagination getFeedbackList(@NotNull FeedbackSearchBean bean) {
+    StringBuilder sql = new StringBuilder("select f.*,p.name username from tb_feedback f left join tb_person p on f.userid = p.userid where 1=1 ");
+    String username = bean.getUsername();
+    String content = bean.getContent();
+    String startdate = bean.getStartdate();
+    String enddate = bean.getEnddate();
+    String replystatus = bean.getReplystatus();
+    int pageno = bean.getPageno();
+    int pagesize = bean.getPagesize();
+    if (!StringUtil.isEmpty(username)) {
+      sql.append(" and p.name like :username");
+    }
+    if (!StringUtil.isEmpty(content)) {
+      sql.append(" and f.content like :content");
+    }
+    if (!StringUtil.isEmpty(startdate)) {
+      sql.append(" and f.fbtime >= :startdate");
+    }
+    if (!StringUtil.isEmpty(enddate)) {
+      sql.append(" and f.fbtime <= :enddate");
+    }
+    if (!StringUtil.isEmpty(replystatus)) {
+      sql.append(" and f.replystatus = :replystatus");
+    }
+    sql.append(" order by f.fbtime desc");
+    Finder f = Finder.create(sql.toString());
+    if (!StringUtil.isEmpty(username)) {
+      f.setParameter("username", "%" + username.trim() + "%");
+    }
+    if (!StringUtil.isEmpty(content)) {
+      f.setParameter("content", "%" + content.trim() + "%");
+    }
+    if (!StringUtil.isEmpty(startdate)) {
+      f.setParameter("startdate", startdate + "000000");
+    }
+    if (!StringUtil.isEmpty(enddate)) {
+      f.setParameter("enddate", enddate + "235959");
+    }
+    if (!StringUtil.isEmpty(replystatus)) {
+      f.setParameter("replystatus", replystatus);
+    }
+    return findNative(f, Transformers.aliasToBean(TBFeedback.class), pageno, pagesize);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBAnnex.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBAnnex.java
new file mode 100644
index 0000000..2679855
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBAnnex.java
@@ -0,0 +1,53 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "tb_annex")
+public class TBAnnex {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "annexid", nullable = false, length = 32)
+  private String annexid;
+  @Column(name = "fbid", length = 32)
+  private String fbid;
+  @Column(name = "picid", length = 32)
+  private String picid;
+  @Column(name = "minpicid", length = 14)
+  private String minpicid;
+
+  public String getAnnexid() {
+    return annexid;
+  }
+
+  public void setAnnexid(String annexid) {
+    this.annexid = annexid;
+  }
+
+  public String getFbid() {
+    return fbid;
+  }
+
+  public void setFbid(String fbid) {
+    this.fbid = fbid;
+  }
+
+  public String getPicid() {
+    return picid;
+  }
+
+  public void setPicid(String picid) {
+    this.picid = picid;
+  }
+
+  public String getMinpicid() {
+    return minpicid;
+  }
+
+  public void setMinpicid(String minpicid) {
+    this.minpicid = minpicid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBFeedback.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBFeedback.java
new file mode 100644
index 0000000..951b947
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBFeedback.java
@@ -0,0 +1,96 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import java.util.List;
+
+@Entity
+@Table(name = "tb_feedback")
+public class TBFeedback {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "fbid", nullable = false, length = 32)
+  private String fbid;
+  @Column(name = "userid", nullable = false, length = 32)
+  private String userid;
+  @Column(name = "content", nullable = false, length = 200)
+  private String content;
+  @Column(name = "fbtime", length = 14)
+  private String fbtime;
+  @Column(name = "fbip", length = 32)
+  private String fbip;
+  @Column(name = "replystatus", length = 1)
+  private String replystatus;
+  @Transient
+  private String username;
+  @Transient
+  private List<TBAnnex> pictures;
+
+  public String getFbid() {
+    return fbid;
+  }
+
+  public void setFbid(String fbid) {
+    this.fbid = fbid;
+  }
+
+  public String getUserid() {
+    return userid;
+  }
+
+  public void setUserid(String userid) {
+    this.userid = userid;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  public String getFbtime() {
+    return fbtime;
+  }
+
+  public void setFbtime(String fbtime) {
+    this.fbtime = fbtime;
+  }
+
+  public String getFbip() {
+    return fbip;
+  }
+
+  public void setFbip(String fbip) {
+    this.fbip = fbip;
+  }
+
+
+  public String getReplystatus() {
+    return replystatus;
+  }
+
+  public void setReplystatus(String replystatus) {
+    this.replystatus = replystatus;
+  }
+
+
+  public String getUsername() {
+    return username;
+  }
+
+  public void setUsername(String username) {
+    this.username = username;
+  }
+
+  public List<TBAnnex> getPictures() {
+    return pictures;
+  }
+
+  public void setPictures(List<TBAnnex> pictures) {
+    this.pictures = pictures;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBReply.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBReply.java
new file mode 100644
index 0000000..4a65054
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBReply.java
@@ -0,0 +1,63 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "tb_reply")
+public class TBReply {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "replyid", nullable = false, length = 32)
+  private String replyid;
+  @Column(name = "replycontent", length = 200)
+  private String replycontent;
+  @Column(name = "operid", length = 32)
+  private String operid;
+  @Column(name = "updatetime", length = 14)
+  private String updatetime;
+  @Column(name = "fbid", length = 32)
+  private String fbid;
+
+  public String getReplyid() {
+    return replyid;
+  }
+
+  public void setReplyid(String replyid) {
+    this.replyid = replyid;
+  }
+
+  public String getReplycontent() {
+    return replycontent;
+  }
+
+  public void setReplycontent(String replycontent) {
+    this.replycontent = replycontent;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public String getUpdatetime() {
+    return updatetime;
+  }
+
+  public void setUpdatetime(String updatetime) {
+    this.updatetime = updatetime;
+  }
+
+  public String getFbid() {
+    return fbid;
+  }
+
+  public void setFbid(String fbid) {
+    this.fbid = fbid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBResource.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBResource.java
new file mode 100644
index 0000000..031e189
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBResource.java
@@ -0,0 +1,112 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import java.util.List;
+
+@Entity
+@Table(name = "tb_resource")
+public class TBResource {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "resid", nullable = false, length = 32)
+  private String resid;
+
+  @Column(name = "respath", length = 100)
+  private String respath;
+
+  @Column(name = "resname", length = 50)
+  private String resname;
+
+  @Column(name = "parentid", length = 32)
+  private String parentid;
+
+  @Column(name = "isleaf", length = 1)
+  private String isleaf;
+
+  @Column(name = "showflag", length = 1)
+  private String showflag;
+
+  @Column(name = "ordernum", precision = 3)
+  private int ordernum;
+
+  @Column(name = "icon",length = 50)
+  private String icon;
+
+  @Transient
+  private List<TBResource> children;
+
+  public String getResid() {
+    return resid;
+  }
+
+  public void setResid(String resid) {
+    this.resid = resid;
+  }
+
+  public String getRespath() {
+    return respath;
+  }
+
+  public void setRespath(String respath) {
+    this.respath = respath;
+  }
+
+  public String getResname() {
+    return resname;
+  }
+
+  public void setResname(String resname) {
+    this.resname = resname;
+  }
+
+  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 getShowflag() {
+    return showflag;
+  }
+
+  public void setShowflag(String showflag) {
+    this.showflag = showflag;
+  }
+
+  public int getOrdernum() {
+    return ordernum;
+  }
+
+  public void setOrdernum(int ordernum) {
+    this.ordernum = ordernum;
+  }
+
+  public String getIcon() {
+    return icon;
+  }
+
+  public void setIcon(String icon) {
+    this.icon = icon;
+  }
+
+  public List<TBResource> getChildren() {
+    return children;
+  }
+
+  public void setChildren(List<TBResource> children) {
+    this.children = children;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBRole.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBRole.java
new file mode 100644
index 0000000..9bb1a5d
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBRole.java
@@ -0,0 +1,68 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+import javax.persistence.*;
+
+@Entity
+@Table(name = "tb_role")
+public class TBRole {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "roleid", nullable = false, length = 32)
+  private String roleid;
+
+  @Column(name = "rolename", nullable = false, length = 32)
+  private String rolename;
+
+  @Column(name = "rolecode", nullable = false, length = 1)
+  private String rolecode;
+
+  @Column(name = "detail", length = 200)
+  private String detail;
+
+  @Column(name = "moditime", nullable = false, length = 14)
+  private String moditime;
+
+  public String getRoleid() {
+    return roleid;
+  }
+
+  public void setRoleid(String roleid) {
+    this.roleid = roleid;
+  }
+
+  public String getRolename() {
+    return rolename;
+  }
+
+  public void setRolename(String rolename) {
+    this.rolename = rolename;
+  }
+
+  public String getRolecode() {
+    return rolecode;
+  }
+
+  public void setRolecode(String rolecode) {
+    this.rolecode = rolecode;
+  }
+
+  public String getDetail() {
+    return detail;
+  }
+
+  public void setDetail(String detail) {
+    this.detail = detail;
+  }
+
+  public String getModitime() {
+    return moditime;
+  }
+
+  public void setModitime(String moditime) {
+    this.moditime = moditime;
+  }
+
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TRoleResource.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TRoleResource.java
new file mode 100644
index 0000000..08a3b6a
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TRoleResource.java
@@ -0,0 +1,55 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+import javax.persistence.*;
+
+@Entity
+@Table(name = "tb_role_resource")
+public class TRoleResource {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "id", nullable = false, length = 32)
+  private String id;
+
+  @Column(name = "roleid", nullable = false, length = 32)
+  private String roleid;
+
+  @Column(name = "resid", nullable = false, length = 32)
+  private String resid;
+
+  @Column(name = "addtime", length = 16)
+  private String addtime;
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getRoleid() {
+    return roleid;
+  }
+
+  public void setRoleid(String roleid) {
+    this.roleid = roleid;
+  }
+
+  public String getResid() {
+    return resid;
+  }
+
+  public void setResid(String resid) {
+    this.resid = resid;
+  }
+
+  public String getAddtime() {
+    return addtime;
+  }
+
+  public void setAddtime(String addtime) {
+    this.addtime = addtime;
+  }
+}
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
new file mode 100644
index 0000000..4de5a35
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/util/PortalConstant.java
@@ -0,0 +1,8 @@
+package com.supwisdom.dlpay.portal.util;
+
+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";
+}
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/OperLoginHandler.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/OperLoginHandler.kt
new file mode 100644
index 0000000..7d4d4e6
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/OperLoginHandler.kt
@@ -0,0 +1,102 @@
+package com.supwisdom.dlpay.portal
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.supwisdom.dlpay.api.bean.JsonResult
+import com.supwisdom.dlpay.framework.core.JwtConfig
+import com.supwisdom.dlpay.framework.core.JwtTokenUtil
+import com.supwisdom.dlpay.framework.domain.JwtRedis
+import com.supwisdom.dlpay.framework.domain.TOperator
+import com.supwisdom.dlpay.framework.redisrepo.ApiJwtRepository
+import com.supwisdom.dlpay.framework.service.OperatorDetailService
+import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.framework.util.Constants
+import com.supwisdom.dlpay.framework.util.SysparaUtil
+import com.supwisdom.dlpay.framework.util.TradeDict
+import com.supwisdom.dlpay.mobile.exception.UserLoginFailException
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.http.HttpStatus
+import org.springframework.security.authentication.BadCredentialsException
+import org.springframework.security.authentication.LockedException
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.AuthenticationException
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
+import org.springframework.stereotype.Component
+import java.io.IOException
+import javax.servlet.ServletException
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+
+@Component("operLoginSuccessHandler")
+class OperLoginSuccessHandler : SimpleUrlAuthenticationSuccessHandler() {
+    @Autowired
+    lateinit var operatorDetailService: OperatorDetailService
+    @Autowired
+    lateinit var objectMapper: ObjectMapper
+    @Autowired
+    lateinit var jwtConfig: JwtConfig
+    @Autowired
+    lateinit var apiJwtRepository: ApiJwtRepository
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+
+    override fun onAuthenticationSuccess(request: HttpServletRequest, response: HttpServletResponse, authentication: Authentication) {
+        val temp = authentication.principal as TOperator
+        val operator = operatorDetailService.findByOperid(temp.operid)
+        val exp = systemUtilService.getSysparaValueAsInt(SysparaUtil.MOBILE_LOGIN_EXPIRE_IN_SECONDS,60*60*24*3)
+        jwtConfig.expiration = exp.toLong()
+        if (operator != null) {
+            //TODO 从数据取jwtConfig.expiration
+            val token = JwtTokenUtil(jwtConfig).generateToken(
+                    mapOf("uid" to operator.operid, "issuer" to "portal",
+                            "audience" to operator.opername,
+                            Constants.JWT_CLAIM_TENANTID to "portal",
+                            Constants.JWT_CLAIM_AUTHORITIES to temp.authorities))
+            val jwt = JwtRedis().apply {
+                jti = token.jti
+                uid = operator.opername
+                status = TradeDict.JWT_STATUS_NORMAL
+                expiration = token.expiration.valueInMillis
+            }.apply {
+                //删除之前的token
+                if (!operator.jti.isNullOrEmpty()) {
+                    apiJwtRepository.deleteById(operator.jti!!)
+                }
+                apiJwtRepository.save(this)
+            }
+            operator.jti = jwt.jti
+            operatorDetailService.saveOper(operator)
+            response.status = HttpStatus.OK.value()
+            response.contentType = "application/json;charset=UTF-8"
+            response.writer.write(objectMapper.writeValueAsString(JsonResult.ok()
+                    .put("token", token.jwtToken)
+                    ?.put("expire",token.expiration.valueInMillis)
+                    ?.put("now",System.currentTimeMillis())
+                    ?.put("tenantid", "mobile")
+                    ?.put("uid", operator.operid)))
+        } else {
+            throw UserLoginFailException("登录错误")
+        }
+    }
+}
+
+
+@Component("operLoginFailHandler")
+class OperLoginFailHandler : SimpleUrlAuthenticationFailureHandler() {
+    @Autowired
+    lateinit var objectMapper: ObjectMapper
+
+    @Throws(IOException::class, ServletException::class)
+    override fun onAuthenticationFailure(request: HttpServletRequest,
+                                         response: HttpServletResponse, exception: AuthenticationException) {
+        logger.error("登录失败:" + exception.message + "|" + exception.javaClass)
+        val errmsg = when (exception) {
+            is BadCredentialsException -> "账号或密码错误"
+            is LockedException -> "账户被锁定"
+            else -> exception.message!!
+        }
+        response.status = HttpStatus.OK.value()
+        response.contentType = "application/json;charset=UTF-8"
+        response.writer.write(objectMapper.writeValueAsString(JsonResult.error(errmsg)))
+    }
+}
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 3ea8bbc..50f52a3 100644
--- a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
@@ -1,14 +1,131 @@
 package com.supwisdom.dlpay.portal
 
+import com.supwisdom.dlpay.api.bean.JsonResult
+import com.supwisdom.dlpay.framework.core.JwtConfig
+import com.supwisdom.dlpay.framework.core.JwtTokenUtil
+import com.supwisdom.dlpay.framework.redisrepo.ApiJwtRepository
+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.portal.bean.FeedbackSearchBean
+import com.supwisdom.dlpay.portal.domain.TBReply
+import com.supwisdom.dlpay.portal.service.FeedbackService
+import com.supwisdom.dlpay.portal.util.PortalConstant
+import mu.KotlinLogging
+import org.jose4j.jwt.ReservedClaimNames
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.http.HttpStatus
 import org.springframework.http.ResponseEntity
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RestController
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.web.bind.annotation.*
 
 @RestController
 @RequestMapping("/portalapi")
-class PortalApi{
+class PortalApi {
+    @Autowired
+    lateinit var operatorDetailService: OperatorDetailService
+    @Autowired
+    lateinit var feedbackService: FeedbackService
+    @Autowired
+    lateinit var jwtConfig: JwtConfig
+    @Autowired
+    lateinit var apiJwtRepository: ApiJwtRepository
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+    val logger = KotlinLogging.logger { }
+
     @RequestMapping("/test")
-    fun test(): ResponseEntity<Any>{
-        return ResponseEntity.ok("测试")
+    fun test(): JsonResult {
+        return JsonResult.ok("测试")
+    }
+
+    @RequestMapping("user/logout")
+    fun logout(@RequestHeader("Authorization") auth: String?): ResponseEntity<Any>{
+        if (auth == null) {
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+        }
+        val jwt = auth.substring(jwtConfig.tokenHeader.length)
+        val claims = JwtTokenUtil(jwtConfig).verifyToken(jwt)
+        SecurityContextHolder.clearContext()
+        apiJwtRepository.deleteById(claims[ReservedClaimNames.JWT_ID].toString())
+        return ResponseEntity.ok().body(JsonResult.ok())
+    }
+
+    @RequestMapping("/user/info")
+    fun getUserInfo(): JsonResult? {
+        return try {
+            val p = SecurityContextHolder.getContext().authentication
+            val oper = operatorDetailService.findByOperid(p.name)
+            val data = HashMap<String, String>()
+            val url = systemUtilService.getBusinessValue(PortalConstant.SYSPARA_IMAGESERVER_URL)
+            data["name"] = oper.opername
+            data["roles"] = "admin"
+            data["url"] = url
+            JsonResult.ok().put("data", data)
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("查询用户信息异常")
+        }
+
+    }
+
+    @RequestMapping("/user/resource")
+    fun getUserResource(): JsonResult? {
+        return try {
+            val p = SecurityContextHolder.getContext().authentication
+            val oper = operatorDetailService.findByOperid(p.name)
+            val resource = operatorDetailService.getResByRoleId(oper.roleid)
+            JsonResult.ok().put("resource", resource)
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("查询功能列表异常")
+        }
+
+    }
+
+    @RequestMapping("/feedback/list")
+    fun getFeedbackList(bean: FeedbackSearchBean): JsonResult? {
+        return try {
+            val page = feedbackService.getFeedbackList(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("/feedback/reply/{fbid}")
+    fun getFeedbackReply(@PathVariable("fbid") fbid: String): JsonResult? {
+        return try {
+            val list = feedbackService.getReplyListByFbId(fbid)
+            if (list.isEmpty()) {
+                return JsonResult.ok().put("msg", "无数据")
+            }
+            return JsonResult.ok().put("list", list)
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("查询用户留言回复异常")
+        }
+    }
+
+    @RequestMapping(value= ["/feedback/reply/save"],method = [RequestMethod.POST])
+    fun saveFeedbackReply(@RequestBody reply:TBReply): JsonResult? {
+        return try {
+            val p = SecurityContextHolder.getContext().authentication
+            val oper = operatorDetailService.findByOperid(p.name)
+            reply.operid = oper.operid
+            val msg = feedbackService.saveReplyListByFbId(reply)
+            return if (StringUtil.isEmpty(msg)) {
+                JsonResult.ok()
+            } else {
+                JsonResult.error(msg)
+            }
+        } 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/FeedbackSearchBean.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/FeedbackSearchBean.kt
new file mode 100644
index 0000000..3b15a48
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/FeedbackSearchBean.kt
@@ -0,0 +1,11 @@
+package com.supwisdom.dlpay.portal.bean
+
+class FeedbackSearchBean {
+    var username: String = ""
+    var content: String = ""
+    var startdate: String = ""
+    var enddate: String = ""
+    var replystatus: 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
new file mode 100644
index 0000000..59968d8
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/AnnexDao.kt
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.portal.domain.TBAnnex
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface AnnexDao : JpaRepository<TBAnnex, String>, FeedbackRepository {
+    fun getByFbid(fbid: String): List<TBAnnex>
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/FeedbackDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/FeedbackDao.kt
new file mode 100644
index 0000000..9d55724
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/FeedbackDao.kt
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.portal.domain.TBFeedback
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface FeedbackDao :JpaRepository<TBFeedback,String>,FeedbackRepository{
+
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/FeedbackRepository.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/FeedbackRepository.kt
new file mode 100644
index 0000000..6a91a73
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/FeedbackRepository.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.FeedbackSearchBean
+
+interface FeedbackRepository {
+    fun getFeedbackList(bean:FeedbackSearchBean):Pagination
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ReplyDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ReplyDao.kt
new file mode 100644
index 0000000..e0a71bf
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ReplyDao.kt
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.portal.domain.TBReply
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface ReplyDao :JpaRepository<TBReply,String>{
+    fun getAllByFbid(fbid:String):List<TBReply>
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ResourceDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ResourceDao.kt
new file mode 100644
index 0000000..4d79afe
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/ResourceDao.kt
@@ -0,0 +1,16 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.portal.domain.TBResource
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.data.jpa.repository.Query
+import org.springframework.stereotype.Repository
+
+@Repository
+interface ResourceDao : JpaRepository<TBResource, String> {
+
+    @Query("select t2 from TRoleResource t1,TBResource t2 where t1.resid = t2.resid and t2.isleaf = '0' and t1.roleid=?1 order by t2.ordernum")
+    fun findRootListByRole(roleId: String): List<TBResource>
+
+    @Query("select t2 from TRoleResource t1,TBResource t2 where t1.resid = t2.resid and t2.isleaf = '1' and t1.roleid=?1 and t2.parentid=?2 order by t2.ordernum")
+    fun findChildrenByRoleAndParent(roleId: String, parentId: String): List<TBResource>
+}
\ 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
new file mode 100644
index 0000000..005de34
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/FeedbackService.kt
@@ -0,0 +1,11 @@
+package com.supwisdom.dlpay.portal.service
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.portal.bean.FeedbackSearchBean
+import com.supwisdom.dlpay.portal.domain.TBReply
+
+interface FeedbackService {
+    fun getFeedbackList(bean: FeedbackSearchBean): Pagination
+    fun getReplyListByFbId(fbId: String): List<TBReply>
+    fun saveReplyListByFbId(reply: TBReply):String?
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/FeedbackServiceImpl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/FeedbackServiceImpl.kt
new file mode 100644
index 0000000..d690911
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/FeedbackServiceImpl.kt
@@ -0,0 +1,56 @@
+package com.supwisdom.dlpay.portal.service.Impl
+
+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.FeedbackSearchBean
+import com.supwisdom.dlpay.portal.dao.AnnexDao
+import com.supwisdom.dlpay.portal.dao.FeedbackDao
+import com.supwisdom.dlpay.portal.dao.ReplyDao
+import com.supwisdom.dlpay.portal.domain.TBFeedback
+import com.supwisdom.dlpay.portal.domain.TBReply
+import com.supwisdom.dlpay.portal.service.FeedbackService
+import com.supwisdom.dlpay.portal.util.PortalConstant
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+
+@Service
+class FeedbackServiceImpl : FeedbackService {
+    @Autowired
+    lateinit var feedbackDao: FeedbackDao
+    @Autowired
+    lateinit var replyDao: ReplyDao
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    lateinit var annexDao: AnnexDao
+
+    override fun getFeedbackList(bean: FeedbackSearchBean): Pagination {
+        val page = feedbackDao.getFeedbackList(bean)
+        val list = page.list as List<TBFeedback>
+        list.forEach { feedback ->
+            run {
+                feedback.pictures = annexDao.getByFbid(feedback.fbid)
+            }
+         }
+        return page
+    }
+
+    override fun getReplyListByFbId(fbId: String): List<TBReply> {
+        return replyDao.getAllByFbid(fbId)
+    }
+
+    override fun saveReplyListByFbId(reply: TBReply): String? {
+        val optional = feedbackDao.findById(reply.fbid)
+        return if (optional.isPresent) {
+            val feedback = optional.get()
+            reply.updatetime = systemUtilService.sysdatetime.hostdatetime
+            replyDao.save(reply)
+            feedback.replystatus = PortalConstant.YES
+            feedbackDao.save(feedback)
+            null
+        } else {
+            "回复的留言不存在"
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/security.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/security.kt
index e8464f1..e1d5a1f 100644
--- a/backend/src/main/kotlin/com/supwisdom/dlpay/security.kt
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/security.kt
@@ -4,8 +4,6 @@
 import com.supwisdom.dlpay.framework.core.JwtTokenUtil
 import com.supwisdom.dlpay.framework.core.PasswordBCryptConfig
 import com.supwisdom.dlpay.framework.redisrepo.ApiJwtRepository
-import com.supwisdom.dlpay.framework.security.MyAuthenticationFailureHandler
-import com.supwisdom.dlpay.framework.security.ValidateCodeSecurityConfig
 import com.supwisdom.dlpay.framework.service.OperatorDetailService
 import com.supwisdom.dlpay.framework.tenant.TenantContext
 import com.supwisdom.dlpay.framework.util.Constants
@@ -13,6 +11,8 @@
 import com.supwisdom.dlpay.mobile.AuthLoginFailHandler
 import com.supwisdom.dlpay.mobile.AuthLoginSuccessHandler
 import com.supwisdom.dlpay.mobile.service.MobileUserService
+import com.supwisdom.dlpay.portal.OperLoginFailHandler
+import com.supwisdom.dlpay.portal.OperLoginSuccessHandler
 import org.jose4j.jwt.ReservedClaimNames
 import org.jose4j.jwt.consumer.InvalidJwtException
 import org.jose4j.lang.JoseException
@@ -34,8 +34,6 @@
 import org.springframework.security.core.context.SecurityContextHolder
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
-import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl
-import org.springframework.security.web.util.matcher.AntPathRequestMatcher
 import org.springframework.stereotype.Component
 import org.springframework.web.cors.CorsConfiguration
 import org.springframework.web.cors.CorsConfigurationSource
@@ -46,8 +44,6 @@
 import javax.servlet.FilterChain
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
-import javax.sql.DataSource
-
 
 @Component
 class ApiJwtAuthenticationFilter : OncePerRequestFilter() {
@@ -160,10 +156,6 @@
             url = url.replace(context, "")
         }
         logger.info(url)
-        if (!url.startsWith("/mobileapi/v1/")) {
-            filterChain.doFilter(request, response)
-            return
-        }
         request.getHeader(jwtConfig.header)?.let { authHeader ->
             try {
                 val jwt = if (authHeader.startsWith(jwtConfig.tokenHeader)) {
@@ -326,6 +318,37 @@
 class WebSecurityConfig {
 
     companion object {
+        @Configuration
+        @Order(1)
+        class ApiWebSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() {
+            @Autowired
+            lateinit var apiJwtAuthenticationFilter: ApiJwtAuthenticationFilter
+            override fun configure(http: HttpSecurity) {
+                // 设置 API 访问权限管理
+                http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                        .and()
+                        .antMatcher("/api/**")
+                        .addFilterAfter(apiJwtAuthenticationFilter,
+                                UsernamePasswordAuthenticationFilter::class.java)
+                        .authorizeRequests()
+                        .antMatchers("/api/auth/**").permitAll()
+                        .antMatchers("/api/notify/**").permitAll()
+                        .antMatchers("/api/common/**").permitAll()
+                        .antMatchers("/api/consume/**").hasRole("THIRD_CONSUME")
+                        .antMatchers("/api/recharge/**").hasRole("THIRD_DEPOSIT")
+                        .antMatchers("/api/user/**").hasRole("THIRD_ADMIN")
+                        .antMatchers("/api/shop/**").hasRole("THIRD_SHOP")
+                        .anyRequest().hasRole("THIRD_COMMON")
+                        .and()
+                        .csrf().ignoringAntMatchers("/api/**", "oauth/**")
+
+            }
+
+            @Bean
+            override fun authenticationManager(): AuthenticationManager {
+                return super.authenticationManagerBean()
+            }
+        }
 
         @Configuration
         @Order(2)
@@ -348,11 +371,6 @@
             }
 
             @Bean
-            override fun authenticationManager(): AuthenticationManager {
-                return super.authenticationManagerBean()
-            }
-
-            @Bean
             fun userProvider(): DaoAuthenticationProvider {
                 return DaoAuthenticationProvider().apply {
                     setUserDetailsService(userDetailsService)
@@ -396,7 +414,7 @@
                 configuration.allowedMethods = listOf("GET", "POST")
                 configuration.allowedHeaders = listOf("*")
                 val source = UrlBasedCorsConfigurationSource()
-                source.registerCorsConfiguration("/mobileapi/**", configuration)
+                source.registerCorsConfiguration("/**", configuration)
                 return source
             }
         }
@@ -405,28 +423,32 @@
         @Order(3)
         class PortalApiSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() {
             @Autowired
+            lateinit var failureHandler: OperLoginFailHandler
+            @Autowired
+            lateinit var successHandler: OperLoginSuccessHandler
+            @Autowired
             lateinit var passwordBCryptConfig: PasswordBCryptConfig
 
             @Autowired
-            lateinit var userDetailsService: MobileUserService
+            lateinit var operatorDetailService: OperatorDetailService
             @Autowired
             lateinit var portalApiSecurityFilter: PortalApiSecurityFilter
 
 
             override fun configure(auth: AuthenticationManagerBuilder) {
-                auth.authenticationProvider(portalUserProvider())
+                auth.authenticationProvider(operatorUserProvider())
             }
 
             @Bean
-            fun portalUserProvider(): DaoAuthenticationProvider {
+            fun operatorUserProvider(): DaoAuthenticationProvider {
                 return DaoAuthenticationProvider().apply {
-                    setUserDetailsService(userDetailsService)
-                    setPasswordEncoder(portalUserPasswordEncoder())
+                    setUserDetailsService(operatorDetailService)
+                    setPasswordEncoder(operatorUserPasswordEncoder())
                 }
             }
 
             @Bean
-            fun portalUserPasswordEncoder(): BCryptPasswordEncoder {
+            fun operatorUserPasswordEncoder(): BCryptPasswordEncoder {
                 return if (passwordBCryptConfig.seed.isBlank()) {
                     BCryptPasswordEncoder()
                 } else {
@@ -443,11 +465,13 @@
                         .antMatcher("/portalapi/**")
                         .addFilterAfter(portalApiSecurityFilter,
                                 UsernamePasswordAuthenticationFilter::class.java)
-                        .authorizeRequests().antMatchers( "/mobileapi/login")
-                        .permitAll().anyRequest().authenticated()
+                        .authorizeRequests().antMatchers("/portalapi/login").permitAll()
+                        .anyRequest().authenticated()
                         .and()
                         .formLogin()
-                        .loginProcessingUrl("/mobileapi/login")
+                        .loginProcessingUrl("/portalapi/login")
+                        .failureHandler(failureHandler)
+                        .successHandler(successHandler)
                         .and().csrf().disable()
             }
         }
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index 091b5cf..ffe72fb 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -14,7 +14,8 @@
 #################### JSP PAGE ####################
 #spring.mvc.view.prefix=/pages/
 #spring.mvc.view.suffix=.jsp
-server.servlet.context-path=/payapi
+server.servlet.context-path=/portal
+server.port=8089
 #################### thymeleaf ####################
 spring.mvc.static-path-pattern=/static/**
 spring.thymeleaf.prefix=classpath:/templates/
diff --git a/backend/src/main/resources/data-postgresql.sql b/backend/src/main/resources/data-postgresql.sql
index 4298c90..562254a 100644
--- a/backend/src/main/resources/data-postgresql.sql
+++ b/backend/src/main/resources/data-postgresql.sql
@@ -1,16 +1,9 @@
 --pg--
-INSERT INTO "tb_period" ("id", "period_year", "period_month", "startdate", "enddate", "settleflag" , "tenantid")
-VALUES ('8a53b7826c65b925016c65bfa7c3001c',to_number(to_char(CURRENT_TIMESTAMP,'yyyy'),'9999'),to_number(to_char(CURRENT_TIMESTAMP,'MM'),'99'), to_char(CURRENT_TIMESTAMP,'yyyyMM')||'01', to_char((to_date(to_char(CURRENT_TIMESTAMP+'1 month','yyyyMM')||'01','yyyyMMdd')-1)::Timestamp,'yyyyMMdd'), 0, '{tenantid}');
-
-insert into TB_SETTLECTL(BOOKSETNO,PERIODYEAR,PERIODMONTH,STATDATE,SETTLEDATE,STATUS,updtime, "tenantid")
-values (1,to_number(to_char(CURRENT_TIMESTAMP,'yyyy'),'9999'),to_number(to_char(CURRENT_TIMESTAMP,'MM'),'99'),to_number(to_char(CURRENT_TIMESTAMP,'yyyyMMdd'),'99999999'),to_number(to_char(CURRENT_TIMESTAMP,'yyyyMMdd'),'99999999'),0,to_char(CURRENT_TIMESTAMP,'yyyyMMddhh24miss'), '{tenantid}');
-
-insert into TB_VOUCHERNOCTL(VOUCHERTYPE,PERIODMONTH,VOUCHERNO,"tenantid")
-values (1,to_number(to_char(CURRENT_TIMESTAMP,'MM'),'99'),0, , '{tenantid}');
-
-update TB_SUBJECT set opendate = to_number(to_char(CURRENT_TIMESTAMP,'yyyymmdd'),'99999999');
-
-CREATE SEQUENCE seq_refno;
+INSERT INTO "tb_businesspara"("parakey", "paraval", "tenantid") VALUES ('imageserver.url.assign', 'http://ykt.supwisdom.com:9333', '{tenentid}');
+INSERT INTO "tb_businesspara"("parakey", "paraval", "tenantid") VALUES ('imagemaxsize', '204800', '{tenentid}');
+INSERT INTO "tb_businesspara"("parakey", "paraval", "tenantid") VALUES ('minimagesize', '150,150', '{tenentid}');
+INSERT INTO "tb_businesspara"("parakey", "paraval", "tenantid") VALUES ('imageserver.url.image', 'http://ykt.supwisdom.com:9119/touchorder/dcpic', '{tenentid}');
+INSERT INTO "tb_businesspara"("parakey", "paraval", "tenantid") VALUES ('imageserver.url.push', 'http://ykt.supwisdom.com:8777', '{tenentid}');
 
 ---------  end of script
 commit;
diff --git a/config/application-devel-pg.properties b/config/application-devel-pg.properties
index 4346bdc..53d2f04 100644
--- a/config/application-devel-pg.properties
+++ b/config/application-devel-pg.properties
@@ -6,7 +6,7 @@
 # Postgresql settings
 spring.datasource.platform=postgresql
 #spring.datasource.url=jdbc:postgresql://ykt.supwisdom.com:15432/payapidev
-spring.datasource.url=jdbc:postgresql://172.28.201.70:15432/payapidev
+spring.datasource.url=jdbc:postgresql://172.28.201.70:15432/portal
 spring.datasource.username=payapi
 spring.datasource.password=123456
 spring.datasource.continue-on-error=true
diff --git a/frontend/.env.development b/frontend/.env.development
index de583d0..4dc439f 100644
--- a/frontend/.env.development
+++ b/frontend/.env.development
@@ -2,4 +2,5 @@
 ENV = 'development'
 
 # base api
-VUE_APP_BASE_API = '/dev-api'
+#VUE_APP_BASE_API = '/dev-api'
+VUE_APP_BASE_API = 'http://localhost:8089/portal/portalapi'
diff --git a/frontend/package.json b/frontend/package.json
index 02f68e2..f57cfa8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -28,6 +28,7 @@
     "js-cookie": "2.2.0",
     "jsonlint": "1.6.3",
     "jszip": "3.2.1",
+    "moment": "^2.27.0",
     "normalize.css": "7.0.0",
     "nprogress": "0.2.0",
     "path-to-regexp": "2.4.0",
diff --git a/frontend/src/api/feedback.js b/frontend/src/api/feedback.js
new file mode 100644
index 0000000..016794e
--- /dev/null
+++ b/frontend/src/api/feedback.js
@@ -0,0 +1,24 @@
+import request from '@/utils/request'
+
+export function getFeedbackList(query) {
+  return request({
+    url: '/feedback/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getFeedbackReply(fbid) {
+  return request({
+    url: '/feedback/reply/' + fbid,
+    method: 'get'
+  })
+}
+
+export function saveFeedbackReply(data) {
+  return request({
+    url: '/feedback/reply/save',
+    method: 'post',
+    data
+  })
+}
diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js
index b8b8741..aba990c 100644
--- a/frontend/src/api/user.js
+++ b/frontend/src/api/user.js
@@ -2,15 +2,25 @@
 
 export function login(data) {
   return request({
-    url: '/vue-element-admin/user/login',
+    url: '/login',
     method: 'post',
-    data
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded'
+    },
+    data,
+    transformRequest: [function(data) {
+      let ret = ''
+      for (const it in data) {
+        ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
+      }
+      return ret
+    }]
   })
 }
 
 export function getInfo(token) {
   return request({
-    url: '/vue-element-admin/user/info',
+    url: '/user/info',
     method: 'get',
     params: { token }
   })
@@ -18,7 +28,15 @@
 
 export function logout() {
   return request({
-    url: '/vue-element-admin/user/logout',
+    url: '/user/logout',
     method: 'post'
   })
 }
+
+export function getAuthMenu(token) {
+  return request({
+    url: '/user/resource',
+    method: 'get',
+    params: { token }
+  })
+}
diff --git a/frontend/src/components/Breadcrumb/index.vue b/frontend/src/components/Breadcrumb/index.vue
index e224ff7..8283946 100644
--- a/frontend/src/components/Breadcrumb/index.vue
+++ b/frontend/src/components/Breadcrumb/index.vue
@@ -2,7 +2,7 @@
   <el-breadcrumb class="app-breadcrumb" separator="/">
     <transition-group name="breadcrumb">
       <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
-        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1||index==levelList.length-2" class="no-redirect">{{ item.meta.title }}</span>
         <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
       </el-breadcrumb-item>
     </transition-group>
@@ -37,7 +37,7 @@
       const first = matched[0]
 
       if (!this.isDashboard(first)) {
-        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
+        matched = [{ path: '/', meta: { title: '首页' }}].concat(matched)
       }
 
       this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
diff --git a/frontend/src/components/HeaderSearch/index.vue b/frontend/src/components/HeaderSearch/index.vue
index 6026ebb..9a30d1a 100644
--- a/frontend/src/components/HeaderSearch/index.vue
+++ b/frontend/src/components/HeaderSearch/index.vue
@@ -8,7 +8,7 @@
       filterable
       default-first-option
       remote
-      placeholder="Search"
+      placeholder="搜索功能"
       class="header-search-select"
       @change="change"
     >
diff --git a/frontend/src/components/RightPanel/index.vue b/frontend/src/components/RightPanel/index.vue
index 55e8c1e..f437c0c 100644
--- a/frontend/src/components/RightPanel/index.vue
+++ b/frontend/src/components/RightPanel/index.vue
@@ -2,9 +2,6 @@
   <div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
     <div class="rightPanel-background" />
     <div class="rightPanel">
-      <div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
-        <i :class="show?'el-icon-close':'el-icon-setting'" />
-      </div>
       <div class="rightPanel-items">
         <slot />
       </div>
diff --git a/frontend/src/layout/components/Navbar.vue b/frontend/src/layout/components/Navbar.vue
index 37bc1e6..0b0ebeb 100644
--- a/frontend/src/layout/components/Navbar.vue
+++ b/frontend/src/layout/components/Navbar.vue
@@ -8,36 +8,20 @@
       <template v-if="device!=='mobile'">
         <search id="header-search" class="right-menu-item" />
 
-        <error-log class="errLog-container right-menu-item hover-effect" />
-
-        <screenfull id="screenfull" class="right-menu-item hover-effect" />
-
-        <el-tooltip content="Global Size" effect="dark" placement="bottom">
-          <size-select id="size-select" class="right-menu-item hover-effect" />
-        </el-tooltip>
-
       </template>
-
-      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
+      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
         <div class="avatar-wrapper">
-          <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
-          <i class="el-icon-caret-bottom" />
+          <span v-html="getOperName()" />
         </div>
         <el-dropdown-menu slot="dropdown">
-          <router-link to="/profile/index">
-            <el-dropdown-item>Profile</el-dropdown-item>
+          <router-link to="/">
+            <el-dropdown-item>个人信息</el-dropdown-item>
           </router-link>
           <router-link to="/">
-            <el-dropdown-item>Dashboard</el-dropdown-item>
+            <el-dropdown-item>修改密码</el-dropdown-item>
           </router-link>
-          <a target="_blank" href="https://github.com/PanJiaChen/vue-element-admin/">
-            <el-dropdown-item>Github</el-dropdown-item>
-          </a>
-          <a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
-            <el-dropdown-item>Docs</el-dropdown-item>
-          </a>
           <el-dropdown-item divided @click.native="logout">
-            <span style="display:block;">Log Out</span>
+            <span style="display:block;">退出</span>
           </el-dropdown-item>
         </el-dropdown-menu>
       </el-dropdown>
@@ -47,20 +31,15 @@
 
 <script>
 import { mapGetters } from 'vuex'
+import user from '@/store/modules/user'
 import Breadcrumb from '@/components/Breadcrumb'
 import Hamburger from '@/components/Hamburger'
-import ErrorLog from '@/components/ErrorLog'
-import Screenfull from '@/components/Screenfull'
-import SizeSelect from '@/components/SizeSelect'
 import Search from '@/components/HeaderSearch'
 
 export default {
   components: {
     Breadcrumb,
     Hamburger,
-    ErrorLog,
-    Screenfull,
-    SizeSelect,
     Search
   },
   computed: {
@@ -75,8 +54,14 @@
       this.$store.dispatch('app/toggleSideBar')
     },
     async logout() {
-      await this.$store.dispatch('user/logout')
-      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+      this.$store.dispatch('user/logout').then(() => {
+        this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+      }).catch((response) => {
+        console.log(response)
+      })
+    },
+    getOperName() {
+      return user.state.name
     }
   }
 }
diff --git a/frontend/src/layout/components/Sidebar/Logo.vue b/frontend/src/layout/components/Sidebar/Logo.vue
index ac0c8d8..4979dc2 100644
--- a/frontend/src/layout/components/Sidebar/Logo.vue
+++ b/frontend/src/layout/components/Sidebar/Logo.vue
@@ -24,7 +24,7 @@
   },
   data() {
     return {
-      title: 'Vue Element Admin',
+      title: '门户系统',
       logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
     }
   }
diff --git a/frontend/src/main.js b/frontend/src/main.js
index b5fa135..e750af9 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -6,7 +6,7 @@
 
 import Element from 'element-ui'
 import './styles/element-variables.scss'
-import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖
+// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖
 
 import '@/styles/index.scss' // global css
 
@@ -34,8 +34,7 @@
 }
 
 Vue.use(Element, {
-  size: Cookies.get('size') || 'medium', // set element-ui default size
-  locale: enLang // 如果使用中文,无需设置,请删除
+  size: Cookies.get('size') || 'medium' // set element-ui default size
 })
 
 // register global utility filters
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 2be959d..bce74c5 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -7,10 +7,6 @@
 import Layout from '@/layout'
 
 /* Router Modules */
-import componentsRouter from './modules/components'
-import chartsRouter from './modules/charts'
-import tableRouter from './modules/table'
-import nestedRouter from './modules/nested'
 
 /**
  * Note: sub-menu only appear when route children.length >= 1
@@ -72,55 +68,7 @@
   },
   {
     path: '/',
-    component: Layout,
-    redirect: '/dashboard',
-    children: [
-      {
-        path: 'dashboard',
-        component: () => import('@/views/dashboard/index'),
-        name: 'Dashboard',
-        meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
-      }
-    ]
-  },
-  {
-    path: '/documentation',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/documentation/index'),
-        name: 'Documentation',
-        meta: { title: 'Documentation', icon: 'documentation', affix: true }
-      }
-    ]
-  },
-  {
-    path: '/guide',
-    component: Layout,
-    redirect: '/guide/index',
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/guide/index'),
-        name: 'Guide',
-        meta: { title: 'Guide', icon: 'guide', noCache: true }
-      }
-    ]
-  },
-  {
-    path: '/profile',
-    component: Layout,
-    redirect: '/profile/index',
-    hidden: true,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/profile/index'),
-        name: 'Profile',
-        meta: { title: 'Profile', icon: 'user', noCache: true }
-      }
-    ]
+    component: Layout
   }
 ]
 
@@ -128,261 +76,7 @@
  * asyncRoutes
  * the routes that need to be dynamically loaded based on user roles
  */
-export const asyncRoutes = [
-  {
-    path: '/permission',
-    component: Layout,
-    redirect: '/permission/page',
-    alwaysShow: true, // will always show the root menu
-    name: 'Permission',
-    meta: {
-      title: 'Permission',
-      icon: 'lock',
-      roles: ['admin', 'editor'] // you can set roles in root nav
-    },
-    children: [
-      {
-        path: 'page',
-        component: () => import('@/views/permission/page'),
-        name: 'PagePermission',
-        meta: {
-          title: 'Page Permission',
-          roles: ['admin'] // or you can only set roles in sub nav
-        }
-      },
-      {
-        path: 'directive',
-        component: () => import('@/views/permission/directive'),
-        name: 'DirectivePermission',
-        meta: {
-          title: 'Directive Permission'
-          // if do not set roles, means: this page does not require permission
-        }
-      },
-      {
-        path: 'role',
-        component: () => import('@/views/permission/role'),
-        name: 'RolePermission',
-        meta: {
-          title: 'Role Permission',
-          roles: ['admin']
-        }
-      }
-    ]
-  },
-
-  {
-    path: '/icon',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/icons/index'),
-        name: 'Icons',
-        meta: { title: 'Icons', icon: 'icon', noCache: true }
-      }
-    ]
-  },
-
-  /** when your routing map is too long, you can split it into small modules **/
-  componentsRouter,
-  chartsRouter,
-  nestedRouter,
-  tableRouter,
-
-  {
-    path: '/example',
-    component: Layout,
-    redirect: '/example/list',
-    name: 'Example',
-    meta: {
-      title: 'Example',
-      icon: 'el-icon-s-help'
-    },
-    children: [
-      {
-        path: 'create',
-        component: () => import('@/views/example/create'),
-        name: 'CreateArticle',
-        meta: { title: 'Create Article', icon: 'edit' }
-      },
-      {
-        path: 'edit/:id(\\d+)',
-        component: () => import('@/views/example/edit'),
-        name: 'EditArticle',
-        meta: { title: 'Edit Article', noCache: true, activeMenu: '/example/list' },
-        hidden: true
-      },
-      {
-        path: 'list',
-        component: () => import('@/views/example/list'),
-        name: 'ArticleList',
-        meta: { title: 'Article List', icon: 'list' }
-      }
-    ]
-  },
-
-  {
-    path: '/tab',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/tab/index'),
-        name: 'Tab',
-        meta: { title: 'Tab', icon: 'tab' }
-      }
-    ]
-  },
-
-  {
-    path: '/error',
-    component: Layout,
-    redirect: 'noRedirect',
-    name: 'ErrorPages',
-    meta: {
-      title: 'Error Pages',
-      icon: '404'
-    },
-    children: [
-      {
-        path: '401',
-        component: () => import('@/views/error-page/401'),
-        name: 'Page401',
-        meta: { title: '401', noCache: true }
-      },
-      {
-        path: '404',
-        component: () => import('@/views/error-page/404'),
-        name: 'Page404',
-        meta: { title: '404', noCache: true }
-      }
-    ]
-  },
-
-  {
-    path: '/error-log',
-    component: Layout,
-    children: [
-      {
-        path: 'log',
-        component: () => import('@/views/error-log/index'),
-        name: 'ErrorLog',
-        meta: { title: 'Error Log', icon: 'bug' }
-      }
-    ]
-  },
-
-  {
-    path: '/excel',
-    component: Layout,
-    redirect: '/excel/export-excel',
-    name: 'Excel',
-    meta: {
-      title: 'Excel',
-      icon: 'excel'
-    },
-    children: [
-      {
-        path: 'export-excel',
-        component: () => import('@/views/excel/export-excel'),
-        name: 'ExportExcel',
-        meta: { title: 'Export Excel' }
-      },
-      {
-        path: 'export-selected-excel',
-        component: () => import('@/views/excel/select-excel'),
-        name: 'SelectExcel',
-        meta: { title: 'Export Selected' }
-      },
-      {
-        path: 'export-merge-header',
-        component: () => import('@/views/excel/merge-header'),
-        name: 'MergeHeader',
-        meta: { title: 'Merge Header' }
-      },
-      {
-        path: 'upload-excel',
-        component: () => import('@/views/excel/upload-excel'),
-        name: 'UploadExcel',
-        meta: { title: 'Upload Excel' }
-      }
-    ]
-  },
-
-  {
-    path: '/zip',
-    component: Layout,
-    redirect: '/zip/download',
-    alwaysShow: true,
-    name: 'Zip',
-    meta: { title: 'Zip', icon: 'zip' },
-    children: [
-      {
-        path: 'download',
-        component: () => import('@/views/zip/index'),
-        name: 'ExportZip',
-        meta: { title: 'Export Zip' }
-      }
-    ]
-  },
-
-  {
-    path: '/pdf',
-    component: Layout,
-    redirect: '/pdf/index',
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/pdf/index'),
-        name: 'PDF',
-        meta: { title: 'PDF', icon: 'pdf' }
-      }
-    ]
-  },
-  {
-    path: '/pdf/download',
-    component: () => import('@/views/pdf/download'),
-    hidden: true
-  },
-
-  {
-    path: '/theme',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/theme/index'),
-        name: 'Theme',
-        meta: { title: 'Theme', icon: 'theme' }
-      }
-    ]
-  },
-
-  {
-    path: '/clipboard',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/clipboard/index'),
-        name: 'ClipboardDemo',
-        meta: { title: 'Clipboard', icon: 'clipboard' }
-      }
-    ]
-  },
-
-  {
-    path: 'external-link',
-    component: Layout,
-    children: [
-      {
-        path: 'https://github.com/PanJiaChen/vue-element-admin',
-        meta: { title: 'External Link', icon: 'link' }
-      }
-    ]
-  },
-
+export let asyncRoutes = [
   // 404 page must be placed at the end !!!
   { path: '*', redirect: '/404', hidden: true }
 ]
@@ -398,6 +92,7 @@
 // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
 export function resetRouter() {
   const newRouter = createRouter()
+  asyncRoutes = []
   router.matcher = newRouter.matcher // reset router
 }
 
diff --git a/frontend/src/settings.js b/frontend/src/settings.js
index 1ebc7f2..0f32b7f 100644
--- a/frontend/src/settings.js
+++ b/frontend/src/settings.js
@@ -1,5 +1,5 @@
 module.exports = {
-  title: 'Vue Element Admin',
+  title: '门户系统',
 
   /**
    * @type {boolean} true | false
@@ -23,7 +23,7 @@
    * @type {boolean} true | false
    * @description Whether show the logo in sidebar
    */
-  sidebarLogo: false,
+  sidebarLogo: true,
 
   /**
    * @type {string | array} 'production' | ['production', 'development']
diff --git a/frontend/src/store/modules/permission.js b/frontend/src/store/modules/permission.js
index aeb5ee5..f1d5583 100644
--- a/frontend/src/store/modules/permission.js
+++ b/frontend/src/store/modules/permission.js
@@ -1,4 +1,6 @@
 import { asyncRoutes, constantRoutes } from '@/router'
+import { getAuthMenu } from '@/api/user'
+import Layout from '@/layout'
 
 /**
  * Use meta.role to determine if the current user has permission
@@ -14,6 +16,28 @@
 }
 
 /**
+ * 后台查询的菜单数据拼装成路由格式的数据
+ * @param routes
+ */
+export function generaMenu(routes, data) {
+  data.forEach(item => {
+    // alert(JSON.stringify(item))
+    const menu = {
+      path: item.respath === '#' ? item.resid + '_key' : item.respath,
+      component: item.respath === '#' ? Layout : (resolve) => require([`@/views${item.respath}/index`], resolve),
+      // hidden: true,
+      children: [],
+      name: 'menu_' + item.resid,
+      meta: { title: item.resname, id: item.resid, roles: ['admin'], icon: item.icon }
+    }
+    if (item.children) {
+      generaMenu(menu.children, item.children)
+    }
+    routes.push(menu)
+  })
+}
+
+/**
  * Filter asynchronous routing tables by recursion
  * @param routes asyncRoutes
  * @param roles
@@ -49,14 +73,26 @@
 const actions = {
   generateRoutes({ commit }, roles) {
     return new Promise(resolve => {
-      let accessedRoutes
-      if (roles.includes('admin')) {
-        accessedRoutes = asyncRoutes || []
-      } else {
-        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
-      }
-      commit('SET_ROUTES', accessedRoutes)
-      resolve(accessedRoutes)
+      const loadMenuData = []
+      // 先查询后台并返回左侧菜单数据并把数据添加到路由
+      getAuthMenu(state.token).then(response => {
+        const data = response.resource
+        Object.assign(loadMenuData, data)
+        generaMenu(asyncRoutes, loadMenuData)
+        let accessedRoutes
+        if (roles.includes('admin')) {
+          // alert(JSON.stringify(asyncRoutes))
+          accessedRoutes = asyncRoutes || []
+        } else {
+          accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
+        }
+        commit('SET_ROUTES', accessedRoutes)
+        resolve(accessedRoutes)
+
+        // generaMenu(asyncRoutes, data)
+      }).catch(error => {
+        console.log(error)
+      })
     })
   }
 }
diff --git a/frontend/src/store/modules/user.js b/frontend/src/store/modules/user.js
index 7800941..b8348bf 100644
--- a/frontend/src/store/modules/user.js
+++ b/frontend/src/store/modules/user.js
@@ -7,7 +7,8 @@
   name: '',
   avatar: '',
   introduction: '',
-  roles: []
+  roles: [],
+  url: ''
 }
 
 const mutations = {
@@ -25,6 +26,9 @@
   },
   SET_ROLES: (state, roles) => {
     state.roles = roles
+  },
+  SET_URL: (state, url) => {
+    state.url = url
   }
 }
 
@@ -33,8 +37,7 @@
   login({ commit }, userInfo) {
     const { username, password } = userInfo
     return new Promise((resolve, reject) => {
-      login({ username: username.trim(), password: password }).then(response => {
-        const { data } = response
+      login({ username: username.trim(), password: password }).then(data => {
         commit('SET_TOKEN', data.token)
         setToken(data.token)
         resolve()
@@ -51,20 +54,13 @@
         const { data } = response
 
         if (!data) {
-          reject('Verification failed, please Login again.')
+          reject('认证失败,请稍后重试')
         }
 
-        const { roles, name, avatar, introduction } = data
-
-        // roles must be a non-empty array
-        if (!roles || roles.length <= 0) {
-          reject('getInfo: roles must be a non-null array!')
-        }
-
+        const { name, roles, url } = data
         commit('SET_ROLES', roles)
         commit('SET_NAME', name)
-        commit('SET_AVATAR', avatar)
-        commit('SET_INTRODUCTION', introduction)
+        commit('SET_URL', url + '/')
         resolve(data)
       }).catch(error => {
         reject(error)
@@ -78,6 +74,7 @@
       logout(state.token).then(() => {
         commit('SET_TOKEN', '')
         commit('SET_ROLES', [])
+        commit('SET_URL', '')
         removeToken()
         resetRouter()
 
@@ -97,6 +94,7 @@
     return new Promise(resolve => {
       commit('SET_TOKEN', '')
       commit('SET_ROLES', [])
+      commit('SET_URL', '')
       removeToken()
       resolve()
     })
diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js
index 2fb95ac..7288f82 100644
--- a/frontend/src/utils/request.js
+++ b/frontend/src/utils/request.js
@@ -1,5 +1,6 @@
 import axios from 'axios'
-import { MessageBox, Message } from 'element-ui'
+import { Message } from 'element-ui'
+import router from '@/router'
 import store from '@/store'
 import { getToken } from '@/utils/auth'
 
@@ -19,7 +20,7 @@
       // let each request carry token
       // ['X-Token'] is a custom headers key
       // please modify it according to the actual situation
-      config.headers['X-Token'] = getToken()
+      config.headers['Authorization'] = 'Bearer ' + getToken()
     }
     return config
   },
@@ -44,41 +45,24 @@
    */
   response => {
     const res = response.data
-
     // if the custom code is not 20000, it is judged as an error.
-    if (res.code !== 20000) {
-      Message({
-        message: res.message || 'Error',
-        type: 'error',
-        duration: 5 * 1000
-      })
-
-      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
-      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
-        // to re-login
-        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
-          confirmButtonText: 'Re-Login',
-          cancelButtonText: 'Cancel',
-          type: 'warning'
-        }).then(() => {
-          store.dispatch('user/resetToken').then(() => {
-            location.reload()
-          })
-        })
-      }
-      return Promise.reject(new Error(res.message || 'Error'))
-    } else {
+    if (res.code === 200) {
       return res
+    } else {
+      return Promise.reject(res || 'Error')
     }
   },
   error => {
-    console.log('err' + error) // for debug
-    Message({
-      message: error.message,
-      type: 'error',
-      duration: 5 * 1000
-    })
-    return Promise.reject(error)
+    if (error.response.status === 401) {
+      Message({
+        message: '当前登录信息已过期,请重新登录',
+        type: 'error',
+        duration: 5 * 1000
+      })
+      router.push(`/login`)
+    } else {
+      return Promise.reject(error)
+    }
   }
 )
 
diff --git a/frontend/src/views/feedback/index.vue b/frontend/src/views/feedback/index.vue
new file mode 100644
index 0000000..125e87f
--- /dev/null
+++ b/frontend/src/views/feedback/index.vue
@@ -0,0 +1,451 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-item" style="margin-right:15px">留言用户</div>
+      <el-input
+        v-model="formData.username"
+        placeholder="用户名"
+        style="width: 350px;margin-right:50px"
+        class="filter-item"
+      />
+      <div class="filter-item" style="margin-right:15px">留言内容</div>
+      <el-input
+        v-model="formData.content"
+        placeholder="留言关键字"
+        style="width: 300px;margin-right:50px"
+        class="filter-item"
+      />
+    </div>
+    <div class="filter-container">
+      <div class="filter-item" style="margin-right:15px">留言日期</div>
+      <el-date-picker
+        v-model="queryDate"
+        type="daterange"
+        align="left"
+        style="width:350px;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.replystatus"
+        style="width:200px;margin-right:100px"
+      >
+        <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
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;margin-top:10px"
+    >
+      <el-table-column label="留言用户" width="150">
+        <template slot-scope="{row}">
+          <span>{{ row.username }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言内容" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.content }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" width="100">
+        <template slot-scope="{row}">
+          <el-tag v-if="row.replystatus==='0'" size="medium">待回复</el-tag>
+          <el-tag v-else type="success" size="medium">已回复</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言时间" align="center" width="160">
+        <template slot-scope="{row}">
+          <span>{{ dateFormat(row.fbtime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="100">
+        <template slot-scope="{row}">
+          <el-tooltip class="item" effect="dark" content="查看详情" placement="bottom">
+            <el-button icon="el-icon-search" circle size="mini" @click="openDetailDialog(row)" />
+          </el-tooltip>
+          <el-tooltip class="item" effect="dark" content="回复" placement="bottom">
+            <el-button type="primary" icon="el-icon-edit" circle size="mini" @click="openReplyDialog(row)" />
+          </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="getFeedbackList"
+    />
+
+    <el-dialog
+      title="留言详情"
+      :visible.sync="detailDialogVisible"
+      width="60%"
+      top="5vh"
+    >
+      <el-table
+        :data="detailContent"
+        border
+        fit
+        :span-method="mergeCells"
+        style="width: 100%"
+        :header-cell-style="{background:'white'}"
+      >
+        <el-table-column align="center">
+          <template slot="header" slot-scope="{}">
+            <span>当前留言状态:
+              <span v-if="currentFeedback.replystatus === '0'" style="color:#409EFF">待回复</span>
+              <span v-else style="color:#67C23A">已回复</span>
+            </span>
+          </template>
+          <el-table-column label="留言用户" align="center" width="120px">
+            <template slot-scope="{row}">
+              <span style="font-weight:bold;color:#909399">{{ row.title }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column>
+            <template slot="header" slot-scope="{}">
+              <span style="font-weight: normal;color:#606266">{{ currentFeedback.username }}</span>
+            </template>
+            <template slot-scope="{row}">
+              <div style="position:relative">
+                <div style="height:120px">{{ row.content }}</div>
+                <el-divider
+                  v-if="row.pictures && row.pictures.length!==0"
+                  style="margin:10px 0"
+                />
+                <div>
+                  <el-image
+                    v-for="(picture) in row.pictures"
+                    :key="picture.annexid"
+                    style="width: 100px; height: 100px;margin-left:10px"
+                    :src="picture.path"
+                    :preview-src-list="picture.previewList"
+                    fit="cover"
+                  >
+                    <div
+                      slot="error"
+                      style="text-align:center;
+                    vertical-align:middle;
+                    font-size:20px;
+                    padding-top:37px;
+                    width:100%;height:100%;background-color:#f4f7fa"
+                    >
+                      <i size="medium" class="el-icon-picture-outline" />
+                    </div>
+                  </el-image>
+                </div>
+                <div style="color:#909399;position:absolute;right:0;bottom:0;">{{ row.time }}</div>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="提交IP" align="center" />
+          <el-table-column>
+            <template slot="header" slot-scope="{}">
+              <span style="font-weight: normal;color:#606266">{{ currentFeedback.fbip }}</span>
+            </template>
+
+          </el-table-column>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+
+    <el-dialog
+      title="留言回复"
+      :visible.sync="replyDialogVisible"
+      width="45%"
+    >
+      <div>
+        <div class="filter-container">
+          <div class="filter-item" style="margin:10px 15px 0 0;vertical-align:top">回复内容</div>
+          <el-input
+            v-model="currentreply.replycontent"
+            :placeholder="'回复'+currentFeedback.username+':'"
+            :rows="7"
+            class="filter-item"
+            style="width:80%;"
+            type="textarea"
+            maxlength="180"
+            show-word-limit
+          />
+        </div>
+        <div style="text-align:center">
+          <el-button type="primary" @click="saveReply">保存</el-button>
+          <el-button @click="replyDialogVisible=false">取消</el-button>
+        </div>
+
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import {
+  getFeedbackList,
+  getFeedbackReply,
+  saveFeedbackReply
+} from '@/api/feedback'
+import moment from 'moment'
+import user from '@/store/modules/user'
+import Pagination from '@/components/Pagination'
+
+export default {
+  name: 'Feedback',
+  components: {
+    Pagination
+  },
+  data() {
+    return {
+      imageUrl: '',
+      imageList: [],
+      textarea: '',
+      listLoading: false,
+      tableKey: 0,
+      list: null,
+      currentFeedback: { username: '' },
+      detailContent: [
+        {
+          title: '留言内容',
+          content: '',
+          time: '',
+          pictures: []
+        },
+        {
+          title: '回复内容',
+          content: '',
+          time: ''
+        }
+      ],
+      total: 0,
+      detailDialogVisible: false,
+      replyDialogVisible: false,
+      queryDate: null,
+      formData: {
+        username: '',
+        content: '',
+        replystatus: '',
+        startdate: '',
+        enddate: '',
+        pageno: 1,
+        pagesize: 10
+      },
+      currentreply: {
+        replycontent: ''
+      },
+      statusOptions: [{
+        value: '',
+        label: '所有'
+      },
+      {
+        value: '0',
+        label: '待回复'
+      }, {
+        value: '1',
+        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.imageUrl = user.state.url
+    this.getFeedbackList()
+  },
+  methods: {
+    getFeedbackList() {
+      this.listLoading = true
+      const date = this.queryDate
+      if (date != null) {
+        this.formData.startdate = date[0]
+        this.formData.enddate = date[1]
+      } else {
+        this.formData.startdate = ''
+        this.formData.enddate = ''
+      }
+      getFeedbackList(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.getFeedbackList()
+    },
+    clearFilter() {
+      this.formData.username = ''
+      this.formData.content = ''
+      this.formData.replystatus = ''
+      this.queryDate = null
+    },
+    dateFormat(date) {
+      if (date === null) {
+        return ''
+      }
+      return moment(date, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss')
+    },
+    openDetailDialog(row) {
+      this.currentFeedback = row
+      const list = []
+      const previewList = []
+      for (let i = 0; i < row.pictures.length; i++) {
+        const item = {}
+        item.path = this.imageUrl + row.pictures[i].minpicid
+        list.push(item)
+        previewList.push(this.imageUrl + row.pictures[i].picid)
+      }
+      // 根据图片顺序(index)更改每张图片绑定的list的图片顺序
+      for (let i = 0; i < row.pictures.length; i++) {
+        const container = previewList
+        const frontArr = container.slice(0, i)
+        const behindArr = container.slice(i, row.pictures.length)
+        const concatList = behindArr.concat(frontArr)
+        list[i].previewList = concatList
+      }
+      const feedbackContent = {
+        title: '留言内容',
+        content: row.content,
+        time: row.fbtime === null ? '' : moment(row.fbtime, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss'),
+        pictures: list
+      }
+
+      this.$set(this.detailContent, 0, feedbackContent)
+
+      getFeedbackReply(row.fbid).then(response => {
+        let reply = {
+          replycontent: '',
+          updatetime: null
+        }
+        if (response.list) {
+          reply = response.list[0]
+        }
+        const replyContent = {
+          title: '回复内容',
+          content: reply.replycontent,
+          time: reply.updatetime === null ? '' : moment(reply.updatetime, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss')
+        }
+        this.$set(this.detailContent, 1, replyContent)
+        this.detailDialogVisible = true
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+      })
+    },
+    openReplyDialog(row) {
+      this.currentFeedback = row
+      this.currentreply = {
+        replycontent: '',
+        fbid: row.fbid,
+        replyid: null
+      }
+      getFeedbackReply(row.fbid).then(response => {
+        if (response.list) {
+          this.currentreply = response.list[0]
+        }
+        this.replyDialogVisible = true
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+      })
+    },
+    saveReply() {
+      if (this.currentreply.replycontent === '') {
+        this.$message('请输入回复内容')
+        return
+      }
+      saveFeedbackReply(this.currentreply).then(response => {
+        this.$notify({
+          title: '成功',
+          message: '回复留言成功!',
+          type: 'success',
+          duration: 2000
+        })
+        this.replyDialogVisible = false
+        this.getFeedbackList()
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+      })
+    },
+    mergeCells({ row, column, rowIndex, columnIndex }) {
+      if (columnIndex === 1) {
+        if (rowIndex === 0) {
+          return [1, 3]
+        } else {
+          return [2, 3]
+        }
+      }
+    }
+  }
+}
+
+</script>
+
diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue
index 2590640..2abcff9 100644
--- a/frontend/src/views/login/index.vue
+++ b/frontend/src/views/login/index.vue
@@ -45,7 +45,7 @@
         </el-form-item>
       </el-tooltip>
 
-      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
+      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
 
       <div style="position:relative">
         <div class="tips">
@@ -74,7 +74,6 @@
 </template>
 
 <script>
-import { validUsername } from '@/utils/validate'
 import SocialSign from './components/SocialSignin'
 
 export default {
@@ -82,11 +81,7 @@
   components: { SocialSign },
   data() {
     const validateUsername = (rule, value, callback) => {
-      if (!validUsername(value)) {
-        callback(new Error('Please enter the correct user name'))
-      } else {
-        callback()
-      }
+      callback()
     }
     const validatePassword = (rule, value, callback) => {
       if (value.length < 6) {
@@ -97,8 +92,8 @@
     }
     return {
       loginForm: {
-        username: 'admin',
-        password: '111111'
+        username: 'system',
+        password: '123456'
       },
       loginRules: {
         username: [{ required: true, trigger: 'blur', validator: validateUsername }],
@@ -156,12 +151,16 @@
       this.$refs.loginForm.validate(valid => {
         if (valid) {
           this.loading = true
-          this.$store.dispatch('user/login', this.loginForm)
+          this.$store.dispatch('user/login', { 'username': this.loginForm.username, 'password': this.loginForm.password })
             .then(() => {
               this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
               this.loading = false
             })
-            .catch(() => {
+            .catch((response) => {
+              this.$message({
+                message: response.msg || '请求异常',
+                type: 'error'
+              })
               this.loading = false
             })
         } else {
diff --git a/frontend/src/views/operator/index.vue b/frontend/src/views/operator/index.vue
new file mode 100644
index 0000000..5956c3f
--- /dev/null
+++ b/frontend/src/views/operator/index.vue
@@ -0,0 +1,9 @@
+
+<template>
+  <div class="app-container" /></template>
+<script>
+export default {
+  name: 'Operator'
+}
+</script>
+
diff --git a/frontend/src/views/pushmsg/index.vue b/frontend/src/views/pushmsg/index.vue
new file mode 100644
index 0000000..cd8e058
--- /dev/null
+++ b/frontend/src/views/pushmsg/index.vue
@@ -0,0 +1,8 @@
+<template>
+  <div>消息推送</div>
+</template>
+<script>
+export default {
+  name: 'PushMsg'
+}
+</script>
diff --git a/frontend/vue.config.js b/frontend/vue.config.js
index 33a6348..dfe2483 100644
--- a/frontend/vue.config.js
+++ b/frontend/vue.config.js
@@ -24,7 +24,7 @@
    * In most cases please use '/' !!!
    * Detail: https://cli.vuejs.org/config/#publicpath
    */
-  publicPath: '/',
+  publicPath: '/portal',
   outputDir: 'dist',
   assetsDir: 'static',
   lintOnSave: process.env.NODE_ENV === 'development',
@@ -36,7 +36,15 @@
       warnings: false,
       errors: true
     },
-    before: require('./mock/mock-server.js')
+    proxy: {
+      [process.env.VUE_APP_BASE_API]: {
+        target: process.env.VUE_APP_BASE_API,
+        changeOrigin: true, // 配置跨域
+        pathRewrite: {
+          ['^' + process.env.VUE_APP_BASE_API]: ''
+        }
+      }
+    }
   },
   configureWebpack: {
     // provide the app's title in webpack's name field, so that