用户对象新增部门角色

This commit is contained in:
RuoYi 2018-02-24 11:19:35 +08:00
parent 09d140b9c8
commit e468d48c2f
22 changed files with 322 additions and 114 deletions

View File

@ -5,13 +5,19 @@ package com.ruoyi.project.shiro.common;
*
* @author yangzz
*/
public class UserConstants
public class Constants
{
/** 正常状态 */
public static final String NORMAL = "0";
/** 用户正常状态 */
public static final String USER_NORMAL = "0";
/** 封禁状态 */
public static final String BLOCKED = "1";
/** 用户封禁状态 */
public static final String USER_BLOCKED = "1";
/** 角色正常状态 */
public static final String ROLE_NORMAL = "0";
/** 角色封禁状态 */
public static final String ROLE_BLOCKED = "1";
/** 异常状态 */
public static final String EXCEPTION = "1";

View File

@ -1,5 +1,7 @@
package com.ruoyi.project.shiro.exception;
import com.ruoyi.project.shiro.exception.user.UserException;
/**
* 验证码错误异常类
*

View File

@ -1,4 +1,4 @@
package com.ruoyi.project.shiro.exception;
package com.ruoyi.project.shiro.exception.user;
/**
* 角色锁定异常类

View File

@ -1,4 +1,4 @@
package com.ruoyi.project.shiro.exception;
package com.ruoyi.project.shiro.exception.user;
/**
* 用户锁定异常类

View File

@ -1,4 +1,4 @@
package com.ruoyi.project.shiro.exception;
package com.ruoyi.project.shiro.exception.user;
import com.ruoyi.project.shiro.exception.base.BaseException;

View File

@ -1,4 +1,4 @@
package com.ruoyi.project.shiro.exception;
package com.ruoyi.project.shiro.exception.user;
/**
* 用户不存在异常类

View File

@ -1,4 +1,4 @@
package com.ruoyi.project.shiro.exception;
package com.ruoyi.project.shiro.exception.user;
/**
* 用户密码不正确或不符合规范异常类

View File

@ -0,0 +1,16 @@
package com.ruoyi.project.shiro.exception.user;
/**
* 用户错误记数异常类
*
* @author yangzz
*/
public class UserPasswordRetryLimitCountException extends UserException
{
private static final long serialVersionUID = 1L;
public UserPasswordRetryLimitCountException(int retryLimitCount, String password)
{
super("user.password.retry.limit.count", new Object[] { retryLimitCount, password });
}
}

View File

@ -1,7 +1,7 @@
package com.ruoyi.project.shiro.exception;
package com.ruoyi.project.shiro.exception.user;
/**
* 用户错误次数异常类
* 用户错误最大次数异常类
*
* @author yangzz
*/

View File

@ -3,12 +3,14 @@ package com.ruoyi.project.shiro.realm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.ruoyi.framework.constant.CommonConstant;
import com.ruoyi.project.shiro.common.UserConstants;
import com.ruoyi.project.shiro.common.Constants;
import com.ruoyi.project.shiro.common.utils.MessageUtils;
import com.ruoyi.project.shiro.exception.UserBlockedException;
import com.ruoyi.project.shiro.exception.UserNotExistsException;
import com.ruoyi.project.shiro.exception.UserPasswordNotMatchException;
import com.ruoyi.project.shiro.exception.user.RoleBlockedException;
import com.ruoyi.project.shiro.exception.user.UserBlockedException;
import com.ruoyi.project.shiro.exception.user.UserNotExistsException;
import com.ruoyi.project.shiro.exception.user.UserPasswordNotMatchException;
import com.ruoyi.project.shiro.service.PasswordService;
import com.ruoyi.project.system.user.domain.User;
import com.ruoyi.project.system.user.service.IUserService;
@ -36,20 +38,20 @@ public class LoginService
// 用户名或密码为空 错误
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
{
SystemLogUtils.log(username, CommonConstant.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
SystemLogUtils.log(username, CommonConstant.LOGIN_FAIL, MessageUtils.message("not.null"));
throw new UserNotExistsException();
}
// 密码如果不在指定范围内 错误
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
if (password.length() < Constants.PASSWORD_MIN_LENGTH
|| password.length() > Constants.PASSWORD_MAX_LENGTH)
{
SystemLogUtils.log(username, CommonConstant.LOGIN_FAIL, MessageUtils.message("user.password.not.match"));
throw new UserPasswordNotMatchException();
}
// 用户名不在指定范围内 错误
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
if (username.length() < Constants.USERNAME_MIN_LENGTH
|| username.length() > Constants.USERNAME_MAX_LENGTH)
{
SystemLogUtils.log(username, CommonConstant.LOGIN_FAIL, MessageUtils.message("user.password.not.match"));
throw new UserPasswordNotMatchException();
@ -66,12 +68,18 @@ public class LoginService
passwordService.validate(user, password);
if (UserConstants.BLOCKED.equals(user.getStatus()))
if (Constants.USER_BLOCKED.equals(user.getStatus()))
{
SystemLogUtils.log(username, CommonConstant.LOGIN_FAIL, MessageUtils.message("user.blocked", user.getRefuseDes()));
throw new UserBlockedException(user.getRefuseDes());
}
if (Constants.ROLE_BLOCKED.equals(user.getRole().getStatus()))
{
SystemLogUtils.log(username, CommonConstant.LOGIN_FAIL, MessageUtils.message("role.blocked", user.getRole().getRemark()));
throw new RoleBlockedException(user.getRole().getRemark());
}
SystemLogUtils.log(username, CommonConstant.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
return user;
}

View File

@ -20,11 +20,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import com.ruoyi.common.tools.StringTools;
import com.ruoyi.common.utils.security.ShiroUtils;
import com.ruoyi.project.shiro.exception.JCaptchaException;
import com.ruoyi.project.shiro.exception.RoleBlockedException;
import com.ruoyi.project.shiro.exception.UserBlockedException;
import com.ruoyi.project.shiro.exception.UserNotExistsException;
import com.ruoyi.project.shiro.exception.UserPasswordNotMatchException;
import com.ruoyi.project.shiro.exception.UserPasswordRetryLimitExceedException;
import com.ruoyi.project.shiro.exception.user.RoleBlockedException;
import com.ruoyi.project.shiro.exception.user.UserBlockedException;
import com.ruoyi.project.shiro.exception.user.UserNotExistsException;
import com.ruoyi.project.shiro.exception.user.UserPasswordNotMatchException;
import com.ruoyi.project.shiro.exception.user.UserPasswordRetryLimitExceedException;
import com.ruoyi.project.system.menu.service.IMenuService;
import com.ruoyi.project.system.user.domain.User;

View File

@ -9,8 +9,9 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.ruoyi.framework.constant.CommonConstant;
import com.ruoyi.project.shiro.common.utils.Md5Utils;
import com.ruoyi.project.shiro.exception.UserPasswordNotMatchException;
import com.ruoyi.project.shiro.exception.UserPasswordRetryLimitExceedException;
import com.ruoyi.project.shiro.common.utils.MessageUtils;
import com.ruoyi.project.shiro.exception.user.UserPasswordNotMatchException;
import com.ruoyi.project.shiro.exception.user.UserPasswordRetryLimitExceedException;
import com.ruoyi.project.system.user.domain.User;
import com.ruoyi.project.util.SystemLogUtils;
@ -50,13 +51,13 @@ public class PasswordService
}
if (retryCount.incrementAndGet() > Integer.valueOf(maxRetryCount).intValue())
{
SystemLogUtils.log(loginName, CommonConstant.LOGIN_FAIL, "密码输入错误次数太多禁止登录,最大{}次!", maxRetryCount);
SystemLogUtils.log(loginName, CommonConstant.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount));
throw new UserPasswordRetryLimitExceedException(Integer.valueOf(maxRetryCount).intValue());
}
if (!matches(user, password))
{
SystemLogUtils.log(loginName, CommonConstant.LOGIN_FAIL, "密码错误!密码:{},重试计数:{}", password, retryCount);
SystemLogUtils.log(loginName, CommonConstant.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.count", retryCount, password));
loginRecordCache.put(loginName, retryCount);
throw new UserPasswordNotMatchException();
}

View File

@ -0,0 +1,82 @@
package com.ruoyi.project.system.dept.domain;
import java.io.Serializable;
/**
* 部门对象 sys_dept
*
* @author yangzz
*/
public class Dept implements Serializable
{
private static final long serialVersionUID = 1L;
// 部门ID
private Long deptId;
// 父部门ID
private Long parentId;
// 部门名称
private String deptName;
// 显示顺序
private String orderNum;
// 部门状态:0正常,1停用
private String status;
public Long getDeptId()
{
return deptId;
}
public void setDeptId(Long deptId)
{
this.deptId = deptId;
}
public Long getParentId()
{
return parentId;
}
public void setParentId(Long parentId)
{
this.parentId = parentId;
}
public String getDeptName()
{
return deptName;
}
public void setDeptName(String deptName)
{
this.deptName = deptName;
}
public String getOrderNum()
{
return orderNum;
}
public void setOrderNum(String orderNum)
{
this.orderNum = orderNum;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
@Override
public String toString()
{
return "{\"deptId\":\"" + deptId + "\",\"parentId\":\"" + parentId + "\",\"deptName\":\"" + deptName
+ "\",\"orderNum\":\"" + orderNum + "\",\"status\":\"" + status + "\"} ";
}
}

View File

@ -0,0 +1,107 @@
package com.ruoyi.project.system.role.domain;
import java.io.Serializable;
/**
* 角色对象 sys_role
*
* @author yangzz
*/
public class Role implements Serializable
{
private static final long serialVersionUID = 1L;
// 角色ID
private Long roleId;
// 角色名
private String roleName;
// 角色状态:0正常,1禁用
private String status;
// 创建时间
private String createTime;
// 更新时间
private String updateTime;
// 更新者
private String updateBy;
// 备注
private String remark;
public Long getRoleId()
{
return roleId;
}
public void setRoleId(Long roleId)
{
this.roleId = roleId;
}
public String getRoleName()
{
return roleName;
}
public void setRoleName(String roleName)
{
this.roleName = roleName;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public String getCreateTime()
{
return createTime;
}
public void setCreateTime(String createTime)
{
this.createTime = createTime;
}
public String getUpdateTime()
{
return updateTime;
}
public void setUpdateTime(String updateTime)
{
this.updateTime = updateTime;
}
public String getUpdateBy()
{
return updateBy;
}
public void setUpdateBy(String updateBy)
{
this.updateBy = updateBy;
}
public String getRemark()
{
return remark;
}
public void setRemark(String remark)
{
this.remark = remark;
}
@Override
public String toString()
{
return "{\"roleId\":\"" + roleId + "\",\"roleName\":\"" + roleName + "\",\"status\":\"" + status
+ "\",\"createTime\":\"" + createTime + "\",\"updateTime\":\"" + updateTime + "\",\"updateBy\":\""
+ updateBy + "\",\"remark\":\"" + remark + "\"} ";
}
}

View File

@ -1,6 +1,8 @@
package com.ruoyi.project.system.user.domain;
import java.io.Serializable;
import com.ruoyi.project.system.dept.domain.Dept;
import com.ruoyi.project.system.role.domain.Role;
/**
* 用户对象 sys_user
@ -33,8 +35,10 @@ public class User implements Serializable
private String refuseDes;
// 创建时间
private String createTime;
// 角色临时字段
private String roleName;
// 部门对象
private Dept dept;
// 角色对象
private Role role;
public User()
{
@ -158,14 +162,24 @@ public class User implements Serializable
this.createTime = createTime;
}
public String getRoleName()
public Dept getDept()
{
return roleName;
return dept;
}
public void setRoleName(String roleName)
public void setDept(Dept dept)
{
this.roleName = roleName;
this.dept = dept;
}
public Role getRole()
{
return role;
}
public void setRole(Role role)
{
this.role = role;
}
@Override
@ -174,8 +188,7 @@ public class User implements Serializable
return "{\"userId\":\"" + userId + "\",\"deptId\":\"" + deptId + "\",\"loginName\":\"" + loginName
+ "\",\"userName\":\"" + userName + "\",\"email\":\"" + email + "\",\"phonenumber\":\"" + phonenumber
+ "\",\"password\":\"" + password + "\",\"salt\":\"" + salt + "\",\"status\":\"" + status
+ "\",\"refuseDes\":\"" + refuseDes + "\",\"createTime\":\"" + createTime + "\",\"roleName\":\""
+ roleName + "\"} ";
+ "\",\"refuseDes\":\"" + refuseDes + "\",\"createTime\":\"" + createTime + "\"} ";
}
}

View File

@ -1,18 +1,18 @@
#错误消息
not.null=* 必须填写
user.not.exists=用户不存在/密码错误
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.exceed=密码输入错误次数太多帐户锁定10分钟
user.password.retry.limit.count=密码输入错误{0}次,{1}
user.password.retry.limit.exceed=密码输入错误{0}次帐户锁定10分钟
user.blocked=用户已封禁,原因:{0}
role.blocked=角色已封禁,原因:{0}
not.null=* 必须填写
length.not.valid=长度必须在{min}到{max}个字符之间
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成且必须以非数字开头
user.password.not.valid=* 5-50个字符
user.jcaptcha.error=验证码错误
user.blocked=用户已封禁,原因:{0}
role.blocked=角色已封禁,原因:{0}
user.email.not.valid=邮箱格式错误
user.mobile.phone.number.not.valid=手机号格式错误

View File

@ -5,8 +5,8 @@ PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
<configuration>
<settings>
<setting name="cacheEnabled" value="true" /> <!-- 全局映射器启用缓存 -->
<setting name="useGeneratedKeys" value="true" />
<setting name="defaultExecutorType" value="REUSE" />
<setting name="logImpl" value="SLF4J"/>
<setting name="useGeneratedKeys" value="true" /> <!-- 允许 JDBC 支持自动生成主键 -->
<setting name="defaultExecutorType" value="REUSE" /> <!-- 配置默认的执行器 -->
<setting name="logImpl" value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
</settings>
</configuration>

View File

@ -4,8 +4,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="SystemLogininforMapper">
<resultMap type="logininfor" id="LogininforResult">
<result property="infoId" column="info_id" />
<resultMap type="Logininfor" id="LogininforResult">
<id property="infoId" column="info_id" />
<result property="loginName" column="login_name" />
<result property="status" column="status" />
<result property="ipaddr" column="ipaddr" />
@ -15,7 +15,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="logondate" column="logondate" />
</resultMap>
<insert id="insertLogininfor" parameterType="logininfor">
<insert id="insertLogininfor" parameterType="Logininfor">
insert into sys_logininfor (login_name, status, ipaddr, browser, os, msg)
values (#{loginName}, #{status}, #{ipaddr}, #{browser}, #{os}, #{msg})
</insert>

View File

@ -4,8 +4,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="SystemMenuMapper">
<resultMap type="menu" id="MenuResult">
<result property="menuId" column="menu_id" />
<resultMap type="Menu" id="MenuResult">
<id property="menuId" column="menu_id" />
<result property="menuName" column="menu_name" />
<result property="parentId" column="parent_id" />
<result property="orderNum" column="order_num" />

View File

@ -4,8 +4,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="SystemUserMapper">
<resultMap type="user" id="UserResult">
<result property="userId" column="user_id" />
<resultMap type="User" id="UserResult">
<id property="userId" column="user_id" />
<result property="deptId" column="dept_id" />
<result property="loginName" column="login_name" />
<result property="userName" column="user_name" />
@ -16,14 +16,43 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="status" column="status" />
<result property="refuseDes" column="refuse_des" />
<result property="createTime" column="create_time" />
<association property="dept" column="dept_id" javaType="Dept" resultMap="deptResult"/>
<association property="role" column="role_id" javaType="Role" resultMap="roleResult"/>
</resultMap>
<select id="queryUserListByCond" parameterType="user" resultMap="UserResult">
<resultMap id="deptResult" type="Dept">
<id property="deptId" column="dept_id" />
<result property="parentId" column="parent_id" />
<result property="deptName" column="dept_name" />
<result property="orderNum" column="order_num" />
<result property="status" column="dept_status" />
</resultMap>
<resultMap id="roleResult" type="Role">
<id property="roleId" column="role_id" />
<result property="roleName" column="role_name" />
<result property="status" column="role_status" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="updateBy" column="update_by" />
<result property="remark" column="remark" />
</resultMap>
<select id="queryUserListByCond" parameterType="User" resultMap="UserResult">
select * from sys_user
</select>
<select id="findByUserName" parameterType="String" resultMap="UserResult">
select * from sys_user where login_name = #{userName}
SELECT u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.phonenumber, u.password, u.salt, u.status, u.refuse_des, u.create_time,
d.dept_id, d.parent_id, d.dept_name, d.order_num, d.status as dept_status,
r.role_id, r.role_name, r.status as role_status, r.remark
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
LEFT JOIN sys_user_role ur ON u.user_id = ur.user_id
LEFT JOIN sys_role r ON ur.role_id = r.role_id
WHERE u.login_name = #{userName}
</select>
</mapper>

View File

@ -1,56 +0,0 @@
resultType 返回值类型为对象不做映射
resultMap 返回值类型为对象根据resultMap定义映射
注解别名 @Alias("User")
等同<typeAlias type="com.ruoyi.project.system.user.domain.User" alias="User"/>
需配置
<!-- 搜索指定包名下面搜索需要的 Java Bean -->
<typeAliases>
<package name="com.ruoyi.project.system"/>
</typeAliases>
第二种方式 在配置文件中新增
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.project.system
在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名
select 节点
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
resultType 从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。使用 resultType 或 resultMap但不能同时使用。
resultMap 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,对其有一个很好的理解的话,许多复杂映射的情形都能迎刃而解。使用 resultMap 或 resultType但不能同时使用。
flushCache 将其设置为 true任何时候只要语句被调用都会导致本地缓存和二级缓存都会被清空默认值false。
useCache 将其设置为 true将会导致本条语句的结果被二级缓存默认值对 select 元素为 true。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset依赖驱动
fetchSize 这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset依赖驱动
statementType STATEMENTPREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 StatementPreparedStatement 或 CallableStatement默认值PREPARED。
resultSetType FORWARD_ONLYSCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)。
databaseId 如果配置了 databaseIdProviderMyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。
resultOrdered 这个设置仅针对嵌套结果 select 语句适用:如果为 true就是假设包含了嵌套结果集或是分组了这样的话当返回一个主结果行的时候就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值false。
resultSets 个设置仅对多结果集的情况适用,它将列出语句执行后返回的结果集并每个结果集给一个名称,名称是逗号分隔的。
insert, update 和 delete 节点
id 命名空间中的唯一标识符,可被用来代表这条语句。
parameterType 将要传入语句的参数的完全限定类名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
flushCache 将其设置为 true任何时候只要语句被调用都会导致本地缓存和二级缓存都会被清空默认值true对应插入、更新和删除语句
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset依赖驱动
statementType STATEMENTPREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 StatementPreparedStatement 或 CallableStatement默认值PREPARED。
useGeneratedKeys (仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段默认值false。
keyProperty (仅对 insert 和 update 有用唯一标记一个属性MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值默认unset。如果希望得到多个生成的列也可以是逗号分隔的属性名称列表。
keyColumn (仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL是必须的当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列也可以是逗号分隔的属性名称列表。
databaseId 如果配置了 databaseIdProviderMyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">

View File

@ -32,7 +32,7 @@
<img src="img/profile_small.jpg" alt="image" class="img-circle" height="60" width="60"/></span>
<a data-toggle="dropdown" class="dropdown-toggle" href="#">
<span class="clear"><span class="block m-t-xs"><strong class="font-bold" th:text="${user.userName}">RuoYi</strong></span>
<span class="text-muted text-xs block">超级管理员<b class="caret"></b></span> </span> </a>
<span class="text-muted text-xs block" th:text="${user.dept.deptName} + '.' + ${user.role.roleName}">超级管理员<b class="caret"></b></span> </span> </a>
<ul class="dropdown-menu animated fadeInRight m-t-xs">
<li><a href="profile.html">个人信息</a></li>
<li class="divider"></li>