SpringBoot项目快速开发框架JeecgBoot——Web处理!
Web处理
Jeecg Boot框架主要用于Web开发领域。下面介绍Jeecg Boot在Web开发中的常用功能,如控制器、登录、系统菜单、权限模块的角色管理和用户管理。
首先启动后台项目,将其导入IDEA中,并利用Maven自动加载依赖。然后把数据库脚本jeecg-boot目录下的
db/jeecgboot-mysql-5.7.sql导入本地数据库中,并修改数据库的配置文件为本地配置。最后启动本地Redis服务,最后启动后台项目,请务必按顺序启动,否则会报错。
启动前端项目时,首先需要在本地安装并配置好Node.js,然后切换到前端代码目录ant-design-jeecg-vue下,在控制台使用npm install -y yarn命令安装yarn,并使用yarn install命令下载依赖,最后使用yarn run serve命令启动前端项目,或者可以先使用yarn run build命令编译项目后再启动。
启动前后端项目之后,在浏览器中访问http://localhost:3000/,可以看到登录页面,使用账号admin和密码123456登录,可以看到系统首页,如图8.4所示。访问
http://localhost:8080/jeecg-boot/,可以查看系统的所有接口文档。
图8.4 系统首页
控制器
Jeecg Boot的控制器全部保存在包
org.jeecg.modules.system.controller下,其中有一个是CommonController.java,它是文件上传下载的统一入口方法。在每个控制器上都有以下注解:
@Slf4j
@RestController
@RequestMapping("/sys/common")
以上3个注解的作用分别是记录日志、标记本类为控制器、设置请求的路径。控制器的返回值为固定格式,它返回Result类的实例对象。使用Result类能够非常快速地构建返回结果的对象。Result类的部分代码如下:
package org.jeecg.common.api.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.jeecg.common.constant.CommonConstant;
import java.io.Serializable;
/**
* 接口返回数据格式
* @author scott
* @email jeecgos@163.com
* @date 2019年1月19日
*/
@Data@ApiModel(value="接口返回对象", description="接口返回对象")
public class Result<T> implements Serializable {
/**
* 成功标志
*/
@ApiModelProperty(value = "成功标志")
private boolean success = true;
/**
* 返回处理消息
*/
@ApiModelProperty(value = "返回处理消息")
private String message = "操作成功!";
/**
* 返回代码
*/
@ApiModelProperty(value = "返回代码")
private Integer code = 0;
/**
* 返回数据对象data
*/
@ApiModelProperty(value = "返回数据对象")
private T result;
/**
* 时间戳
*/
@ApiModelProperty(value = "时间戳")
private long timestamp = System.currentTimeMillis();
public Result() {
}
public Result<T> success(String message) {
this.message = message;
this.code = CommonConstant.SC_OK_200;
this.success = true;
return this; }
@Deprecated
public static Result<Object> ok() {
Result<Object> r = new Result<Object>();
r.setSuccess(true);
r.setCode(CommonConstant.SC_OK_200);
r.setMessage("成功");
return r;
}
public static<T> Result<T> OK() {
Result<T> r = new Result<T>();
r.setSuccess(true);
r.setCode(CommonConstant.SC_OK_200);
r.setMessage("成功");
return r;
}
public static<T> Result<T> OK(T data) {
Result<T> r = new Result<T>();
r.setSuccess(true);
r.setCode(CommonConstant.SC_OK_200);
r.setResult(data);
return r;
}
public static<T> Result<T> OK(String msg, T data) {
Result<T> r = new Result<T>();
r.setSuccess(true);
r.setCode(CommonConstant.SC_OK_200);
r.setMessage(msg);
r.setResult(data);
return r;
}
public static<T> Result<T> error(String msg, T data) {
Result<T> r = new Result<T>();
r.setSuccess(false);
r.setCode(CommonConstant.SC_INTERNAL_SERVER_ERROR_500);
r.setMessage(msg); r.setResult(data);
return r;
}
public static Result<Object> error(String msg) {
return error(CommonConstant.SC_INTERNAL_SERVER_ERROR_500, msg);
}
public static Result<Object> error(int code, String msg) {
Result<Object> r = new Result<Object>();
r.setCode(code);
r.setMessage(msg);
r.setSuccess(false);
return r;
}
public Result<T> error500(String message) {
this.message = message;
this.code = CommonConstant.SC_INTERNAL_SERVER_ERROR_500;
this.success = false;
return this;
}
/**
* 无权限访问返回结果
*/
public static Result<Object> noauth(String msg) {
return error(CommonConstant.SC_JEECG_NO_AUTHZ, msg);
}
@JsonIgnore
private String onlTable;
}
返回结果全部都使用Result进行包装后再返回,前端有统一的返回格式对结果进行处理。
系统登录
在Jeecg Boot项目中,登录控制器的类是LoginController,该类包含所有的登录控制方法,包括登录、退出、获取访问量、获取登录人的信息、选择用户当前部门、短信API登录接口、手机号登录接口、获取加密字符串、后台生成图形验证码、App登录和图形验证码入口等。登录用到的相关数据库用户的表为sys_user。
下面详细说明登录方法的代码逻辑。其中,Controller登录方法的代码如下:
@ApiOperation("登录接口")
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Result<JSONObject> login(@RequestBody SysLoginModel
sysLoginModel){
Result<JSONObject> result = new Result<JSONObject>();
String username = sysLoginModel.getUsername();
String password = sysLoginModel.getPassword();
//update-begin--Author:scott Date:20190805 for:暂时注释掉密码加密逻辑,
目前存在一些问题
//前端进行密码加密,后端进行密码解密
//password = AesEncryptUtil.desEncrypt(sysLoginModel.getPassword().
replaceAll("%2B", "\\+")).trim(); //密码解密
//update-begin--Author:scott Date:20190805 for:暂时注释掉密码加密逻辑,
目前存在一些问题
//update-begin-author:taoyan date:20190828 for:校验验证码String captcha = sysLoginModel.getCaptcha();
if(captcha==null){
result.error500("验证码无效");
return result;
}
String lowerCaseCaptcha = captcha.toLowerCase();
String realKey =
MD5Util.MD5Encode(lowerCaseCaptcha+sysLoginModel.getCheckKey(), "utf-
8");
Object checkCode = redisUtil.get(realKey); //当进入登录页面时,有一定概率出现验证码错误
if(checkCode==null ||
!checkCode.toString().equals(lowerCaseCaptcha)) {
result.error500("验证码错误");
return result;
}
//update-end-author:taoyan date:20190828 for:校验验证码//1. 校验用户是否有效
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>
();
queryWrapper.eq(SysUser::getUsername,username);
SysUser sysUser = sysUserService.getOne(queryWrapper);
result = sysUserService.checkUserIsEffective(sysUser);
if(!result.isSuccess()) {
return result;
}
//2. 校验用户名和密码是否正确
String userpassword = PasswordUtil.encrypt(username, password,
sysUser.getSalt());
String syspassword = sysUser.getPassword();
if (!syspassword.equals(userpassword)) {
result.error500("用户名或密码错误");
return result;
}
//用户登录信息
userInfo(sysUser, result);
//update-begin--Author:liusq Date:20210126 for:登录成功,删除Redis中的验证码
redisUtil.del(realKey);
//update-begin--Author:liusq Date:20210126 for:登录成功,删除Redis中的验证码
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(sysUser, loginUser);
baseCommonService.addLog("用户名: " + username + ",登录成功!",
CommonConstant.LOG_TYPE_1, null,loginUser);
//update-end--Author:wangshuai Date:20200714 for:登录日志没有记录人员 return result;
}
调用sysUserService的checkUserIsEffective()方法,代码如下:
/**
* 校验用户是否有效
* @param sysUser
* @return
*/
@Override
public Result<?> checkUserIsEffective(SysUser sysUser) {
Result<?> result = new Result<Object>();
//情况1:根据用户信息查询,该用户不存在
if (sysUser == null) {
result.error500("该用户不存在,请注册");
baseCommonService.addLog("用户登录失败,用户不存在!",
CommonConstant.
LOG_TYPE_1, null);
return result;
}
//情况2:根据用户信息查询,该用户已注销
//update-begin---author:王帅 if条件永远为false
if (CommonConstant.DEL_FLAG_1.equals(sysUser.getDelFlag())) {
//update-end---author:王帅 if条件永远为false
baseCommonService.addLog("用户登录失败,用户名:" +
sysUser.getUsername()
+ "已注销!", CommonConstant.LOG_TYPE_1, null);
result.error500("该用户已注销");
return result;
}
//情况3:根据用户信息查询,该用户已冻结
if (CommonConstant.USER_FREEZE.equals(sysUser.getStatus())) {
baseCommonService.addLog("用户登录失败,用户名:" +
sysUser.getUsername()
+ "已冻结!", CommonConstant.LOG_TYPE_1, null);
result.error500("该用户已冻结");
return result; }
return result;
}
Controller的login()方法用于判断用户登录是否成功,其执行逻辑如下:
(1)判断验证码是否正确。
(2)调用sysUserService的checkUserIsEffective()方法查询当前用户是否存在,用户状态是否正常,确认用户没有注销和被冻结。
(3)校验用户名和密码是否正确。
(4)完善登录成功的用户信息。
(5)记录日志。
(6)返回登录成功的结果。
菜单管理
在网页上可以快速进行菜单的创建和查看,在“系统管理”|“菜单管理”菜单下,可以看到系统的所有菜单,并且可以新建一个菜单。新建菜单的页面如图8.5所示,系统菜单的控制器为SysPermissionController,数据库对应的表为sys_permission。
SysPermissionController类的部分方法如下:
/**
* <p>
* 菜单权限表的前端控制器
* </p>
*
* @Author scott
* @since 2018-12-21
*/
@Slf4j
@RestController
@RequestMapping("/sys/permission")
public class SysPermissionController { @Autowired
private ISysPermissionService sysPermissionService;
/**
* 加载数据节点
*
* @return
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Result<List<SysPermissionTree>> list() {
long start = System.currentTimeMillis();
Result<List<SysPermissionTree>> result = new Result<>();
try {
LambdaQueryWrapper<SysPermission> query = new
LambdaQueryWrapper<SysPermission>();
query.eq(SysPermission::getDelFlag,
CommonConstant.DEL_FLAG_0);
query.orderByAsc(SysPermission::getSortNo);
List<SysPermission> list = sysPermissionService.list(query);
List<SysPermissionTree> treeList = new ArrayList<>();
getTreeList(treeList, list, null);
result.setResult(treeList);
result.setSuccess(true);
log.info("======获取全部菜单数据=====耗时:" +
(System.currentTimeMillis() - start) + "毫秒");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return result;
}
/**
* 添加菜单
* @param permission
* @return
*/
//@RequiresRoles({ "admin" })
@RequestMapping(value = "/add", method = RequestMethod.POST)
public Result<SysPermission> add(@RequestBody SysPermission
permission) { Result<SysPermission> result = new Result<SysPermission>();
try {
permission =
PermissionDataUtil.intelligentProcessData(permission);
sysPermissionService.addPermission(permission);
result.success("添加成功!");
} catch (Exception e) {
log.error(e.getMessage(), e);
result.error500("操作失败");
}
return result;
}
}
以上为菜单列表和新建菜单的Controller,其对应新增菜单的service代码如下:
@Override
@CacheEvict(value =
CacheConstant.SYS_DATA_PERMISSIONS_CACHE,allEntries=
true)
public void addPermission(SysPermission sysPermission) throws
JeecgBoot
Exception {
//-----------------------------------------------------------------
-
//判断是否是一级菜单,如果是,则清空父菜单
if(CommonConstant.MENU_TYPE_0.equals(sysPermission.getMenuType()))
{
sysPermission.setParentId(null);
}
//-----------------------------------------------------------------
-
String pid = sysPermission.getParentId();
if(oConvertUtils.isNotEmpty(pid)) {
//设置父节点不为子节点
this.sysPermissionMapper.setMenuLeaf(pid, 0); }
sysPermission.setCreateTime(new Date());
sysPermission.setDelFlag(0);
sysPermission.setLeaf(true);
this.save(sysPermission);
}
对应Dao方法的代码如下:
/**
* 修改菜单状态字段: 是否子节点
*/
@Update("update sys_permission set is_leaf=#{leaf} where id = #{id}")
public int setMenuLeaf(@Param("id") String id,@Param("leaf") int
leaf);
保存一个新的菜单时应判断是否是一级菜单,如果是,则清空父菜单,否则直接将菜单拼接到父菜单之下。
角色管理
角色是系统权限管理的一部分,用来管理一部分权限的合集。JeecgBoot的角色管理功能是在“系统管理”|“角色管理”菜单下。创建一个新的角色,如图8.6所示,这里创建了一个临时工的角色。在角色管理中还可以查看有多少用户拥有该角色。
角色管理的入口控制器是SysRoleController,其部分源码如下:
/**
* <p>
* 角色表的前端控制器
* </p>
*
* @Author scott
* @since 2018-12-19
*/
@RestController
@RequestMapping("/sys/role")
@Slf4j
public class SysRoleController {
@Autowired
private ISysRoleService sysRoleService;
/**
* 分页列表查询
* @param role
* @param pageNo
* @param pageSize
* @param req
* @return */
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Result<IPage<SysRole>> queryPageList(SysRole role,
@RequestParam(name="pageNo", defaultValue="1")
Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10")
Integer
pageSize,
HttpServletRequest req) {
Result<IPage<SysRole>> result = new Result<IPage<SysRole>>();
QueryWrapper<SysRole> queryWrapper = QueryGenerator.initQuery
Wrapper(role, req.getParameterMap());
Page<SysRole> page = new Page<SysRole>(pageNo, pageSize);
IPage<SysRole> pageList = sysRoleService.page(page,
queryWrapper);
result.setSuccess(true);
result.setResult(pageList);
return result;
}
/**
* 添加
* @param role
* @return
*/
@RequestMapping(value = "/add", method = RequestMethod.POST)
//@RequiresRoles({"admin"})
public Result<SysRole> add(@RequestBody SysRole role) {
Result<SysRole> result = new Result<SysRole>();
try {
role.setCreateTime(new Date());
sysRoleService.save(role);
result.success("添加成功!");
} catch (Exception e) {
log.error(e.getMessage(), e);
result.error500("操作失败");
}
return result;
}
}
角色的列表查询和新建都使用MyBatisPlus的接口方法实现,在开发中不需要再次实现,从而更加快捷地完成功能开发。
用户管理
当前登录的用户是管理员,系统还内置了其他账户。在sys_user表中,使用“系统管理”|“用户管理”命令可以查看所有的用户。下面新建一个账号为cc的新用户,然后登录。新建用户的设置页面如图8.7所示。
退出当前用户账号,使用cc登录,登录成功后的页面如图8.8所示。可以看到,已经登录成功,但是因为当前用户未分配任何权限,所以是空白页。
用户管理的入口控制器Controller是SysUserController,其部分源码如下:
/**
* <p>
* 用户表的前端控制器
* </p>
*/
@Slf4j
@RestController
@RequestMapping("/sys/user")
public class SysUserController {
@Autowired
private ISysBaseAPI sysBaseAPI;
@Autowired
private ISysUserService sysUserService;
/**
* 获取用户列表数据
* @param user
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@PermissionData(pageComponent = "system/UserList") @RequestMapping(value = "/list", method = RequestMethod.GET)
public Result<IPage<SysUser>> queryPageList(SysUser
user,@RequestParam
(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer
pageSize,
HttpServletRequest req) {
Result<IPage<SysUser>> result = new Result<IPage<SysUser>>();
QueryWrapper<SysUser> queryWrapper =
QueryGenerator.initQueryWrapper
(user, req.getParameterMap());
// 外部模拟登录临时账号,不显示列表
queryWrapper.ne("username","_reserve_user_external");
Page<SysUser> page = new Page<SysUser>(pageNo, pageSize);
IPage<SysUser> pageList = sysUserService.page(page,
queryWrapper);
//批量查询用户的所属部门
//步骤1:先获取全部的userIds
//步骤2:通过userIds一次性查询用户所属部门的名称
List<String> userIds =
pageList.getRecords().stream().map(SysUser::
getId).collect(Collectors.toList());
if(userIds!=null && userIds.size()>0){
Map<String,String> useDepNames =
sysUserService.getDepNamesByUserIds(userIds);
pageList.getRecords().forEach(item->{
item.setOrgCodeTxt(useDepNames.get(item.getId()));
});
}
result.setSuccess(true);
result.setResult(pageList);
log.info(pageList.toString());
return result;
}
//@RequiresRoles({"admin"})
//@RequiresPermissions("user:add")
@RequestMapping(value = "/add", method = RequestMethod.POST)
public Result<SysUser> add(@RequestBody JSONObject jsonObject) {
Result<SysUser> result = new Result<SysUser>(); String selectedRoles = jsonObject.getString("selectedroles");
String selectedDeparts = jsonObject.getString("selecteddeparts");
try {
SysUser user = JSON.parseObject(jsonObject.toJSONString(),
SysUser.class);
user.setCreateTime(new Date()); //设置创建时间
String salt = oConvertUtils.randomGen(8);
user.setSalt(salt);
String passwordEncode =
PasswordUtil.encrypt(user.getUsername(),user.getPassword(), salt);
user.setPassword(passwordEncode);
user.setStatus(1);
user.setDelFlag(CommonConstant.DEL_FLAG_0);
// 在一个方法中使用事务保存用户信息
sysUserService.saveUser(user, selectedRoles, selectedDeparts);
result.success("添加成功!");
} catch (Exception e) {
log.error(e.getMessage(), e);
result.error500("操作失败");
}
return result;
}
}
这里节选了用户列表和新增用户入口的方法。查询用户列表的service实现代码节选如下:
@Override
public Map<String, String> getDepNamesByUserIds(List<String> userIds)
{
List<SysUserDepVo> list =
this.baseMapper.getDepNamesByUserIds(userIds);
Map<String, String> res = new HashMap<String, String>();
list.forEach(item -> {
if (res.get(item.getUserId()) == null) {
res.put(item.getUserId(), item.getDepartName()); } else {
res.put(item.getUserId(), res.get(item.getUserId()) +
"," +
item.getDepartName());
}
}
);
return res;
}
其中,调用Dao的代码如下:
/**
* 根据 userIds查询,查询用户所属部门的名称(多个部门名称用逗号隔开)
* @param
* @return
*/
public Map<String,String> getDepNamesByUserIds(List<String> userIds);
XML中的SQL语句如下:
<!-- 查询用户所属部门名称的信息 -->
<select id="getDepNamesByUserIds"
resultType="org.jeecg.modules.system.vo.SysUserDepVo">
select d.depart_name,ud.user_id from sys_user_depart ud,sys_depart
d
where d.id = ud.dep_id and ud.user_id in
<foreach collection="userIds" index="index" item="id" open=
"(" separator="," close=")">
#{id}
</foreach>
</select>新增用户的service代码如下:
@Override
@Transactional(rollbackFor = Exception.class)
public void saveUser(SysUser user, String selectedRoles, String
selected
Departs) {
//步骤1 保存用户
this.save(user);
//步骤2 保存角色
if(oConvertUtils.isNotEmpty(selectedRoles)) {
String[] arr = selectedRoles.split(",");
for (String roleId : arr) {
SysUserRole userRole = new SysUserRole(user.getId(), roleId);
sysUserRoleMapper.insert(userRole);
}
}
//步骤3 保存所属部门
if(oConvertUtils.isNotEmpty(selectedDeparts)) {
String[] arr = selectedDeparts.split(",");
for (String deaprtId : arr) {
SysUserDepart userDeaprt = new SysUserDepart(user.getId(),
deaprtId);
sysUserDepartMapper.insert(userDeaprt);
}
}
}
保存用户时附带保存用户角色和用户所属部门的数据。在方法中增加了事务,以确保数据保存的完整性。
异常处理
Jeecg Boot项目使用了全局异常捕获的异常处理方式,不同类型的异常有不同的输出,从而为用户提示友好的错误信息,而不是详细的错误代码。其异常处理类的部分源码如下:
package org.jeecg.common.exception;
/**
* 异常处理器
*
* @Author scott
* @Date 2019
*/
@RestControllerAdvice
@SLF4J
public class JeecgBootExceptionHandler {
/**
* 处理自定义异常
*/
@ExceptionHandler(JeecgBootException.class)
public Result<?> handleRRException(JeecgBootException e){
log.error(e.getMessage(), e);
return Result.error(e.getMessage());
}
@ExceptionHandler(NoHandlerFoundException.class)
public Result<?> handlerNoFoundException(Exception e) {
log.error(e.getMessage(), e);
return Result.error(404, "路径不存在,请检查路径是否正确");
}
@ExceptionHandler({UnauthorizedException.class,
AuthorizationException.class})
public Result<?> handleAuthorizationException(AuthorizationException
e){
log.error(e.getMessage(), e);
return Result.noauth("没有权限,请联系管理员授权");
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e){ log.error(e.getMessage(), e);
return Result.error("操作失败,"+e.getMessage());
}
/**
* Spring默认上传文件的大小为10MB,若超出则捕获异常MaxUploadSizeExceeded
Exception
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public Result<?> handleMaxUploadSizeExceededException(MaxUploadSize
ExceededException e) {
log.error(e.getMessage(), e);
return Result.error("文件超出10MB的限制,请压缩或降低文件质量! ");
}
@ExceptionHandler(PoolException.class)
public Result<?> handlePoolException(PoolException e) {
log.error(e.getMessage(), e);
return Result.error("Redis 连接异常!");
}
}
功能扩展
以上列举的只是Jeecg Boot的一部分功能,还有很多功能读者可以自行去挖掘。常见的功能如下:
统计报表功能:使用该功能后,现有的报表就不再需要重新构建报表页面,而只需要后端返回响应的数据就能看到与结果相符的报表。
在线开发功能:使用该功能可以非常快速地开发在线表单,并且可以设置参数校验的规则,从而对系统的数据源进行管理。
还有动态切换数据源和代码生成等功能读者可以自行演示。
说明:如果现有功能不能满足用户的业务需要,就需要用户自己完成开发。
如果在使用的过程中发现系统Bug,则可以向Jeecg Boot团队反馈,也可以提供建议,这样也为开源做出了自己的贡献。