当前位置: 首页 > news >正文

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团队反馈,也可以提供建议,这样也为开源做出了自己的贡献。

http://www.lqws.cn/news/538345.html

相关文章:

  • linux cp与mv那个更可靠
  • MySQL5.7和8.0 破解root密码
  • 快速傅里叶变换(FFT)是什么?
  • python中学物理实验模拟:斜面受力分析
  • 圆周期性显示和消失——瞬态实现(CAD c#二次开发、插件定制)
  • Nordic nRF54L15 SoC对包含电池监测、中断处理和电源轨控制的定制 nPM1300 示例
  • springcloud 尚硅谷 看到9开头
  • 华为云鸿蒙应用入门级开发者认证 实验(HCCDA-HarmonyOS Cloud Apps)
  • 玄机抽奖Spring Web项目
  • Maven Javadoc 插件使用详解
  • [论文阅读]RaFe: Ranking Feedback Improves Query Rewriting for RAG
  • 解决uniapp vue3版本封装组件后:deep()样式穿透不生效的问题
  • react-嵌套路由 二级路由
  • 事件循环(Event Loop)机制对比:Node.js vs 浏览器​
  • python+requests接口自动化测试
  • 大脑感官:视觉系统中将感观信息转换为神经信号
  • @Autowired 和 @Resource 有什么区别?
  • Java常用设计模式详解
  • linux网络编程socket套接字
  • 【论文阅读】--Instruction Backdoor Attacks Against Customized LLMs
  • Neo4j2.0.1桌面端使用教程(简化版)
  • MySQL 锁的分类
  • WinAppDriver 自动化测试:C#篇
  • EMQ X Broker 配置HTTP 的外部鉴权接口
  • 生物化学 PCR(聚合酶链式反应)引物 制造(固相磷酰胺化学法) 购买 存储
  • 如何在x86_64 Linux上部署Android Cuttlefish模拟器运行环境
  • MySQL事物隔离级别详解
  • 笔记04:层叠的定义及添加
  • 链表基本功(相交链表)
  • Ubuntu通过防火墙管控Docker容器