一.日志管理设计说明
1. 业务设计说明
本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。
其表设计语句如下:
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL COMMENT '用户名',
`operation` varchar(50) DEFAULT NULL COMMENT '用户操作',
`method` varchar(200) DEFAULT NULL COMMENT '请求方法',
`params` varchar(5000) DEFAULT NULL COMMENT '请求参数',
`time` bigint(20) NOT NULL COMMENT '执行时长(毫秒)',
`ip` varchar(64) DEFAULT NULL COMMENT 'IP地址',
`createdTime` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系统日志';
2. 原型设计说明
基于用户需求,实现静态页面(html/css/js),通过静态页面为用户呈现基本需求实现,如图-1所示。
说明:假如客户对此原型进行了确认,后续则可以基于此原型进行研发。
API设计说明
日志业务后台API分层架构及调用关系如图-2所示:
说明:分层目的主要将复杂问题简单化,实现各司其职,各尽所能。
二.日志管理列表页面呈现
1. 业务时序分析
当点击首页左侧的"日志管理"菜单时,其总体时序分析如图-3所示:
2. 服务端实现
2.1 Controller实现
业务描述与设计实现
基于日志菜单管理的请求业务,在PageController中添加doLogUI方法,
doPageUI方法分别用于返回日志列表页面,日志分页页面。
关键代码设计与实现
第一步:在PageController中定义返回日志列表页面 log_list.html的方法。代码如下:
@RequestMapping("log/log_list")
public String doLogUI() {
return "sys/log_list";
}
第二步:在PageController中定义用于返回分页页面 page.html的方法。代码如下:
@RequestMapping("doPageUI")
public String doPageUI() {
return "common/page";
}
2.2 客户端呈现
2.2.1 显示日志管理页面
在starter.html 添加js代码
在body标签内添加js代码
<script type="text/javascript">
//注册按钮(日志管理)的点击事件
$("#load-log-id").click(function (){
//在主页面显示日志管理页面 ajax异步请求
var url="log/log_list";
$("#mainContentId").load(url);//发送异步请求
})
</script>
js代码进阶 优化js代码
<script type="text/javascript">
$(function (){ //页面初始化
doPageUI("load-log-id", "log/log_list");//打开日志管理页面
})
// doPageUI 在mainContentId处打开某个页面
function doPageUI(id, url) {
//注册按钮(日志管理...)的点击事件
$("#" + id).click(function () {
//在主页面显示日志管理页面 load方法是ajax的异步请求
$("#mainContentId").load(url);
})
}
</script>
2.2.2 日志管理页面显示分页页面
分析
在log_list.html页面添加分页页面
js代码
<script type="text/javascript">
// 注册初始化函数
$(function () {
//发送打开分页页面
$("#pageId").load("doPageUI")
})
</script>
三.日志管理列表数据呈现
数据架构分析
日志查询服务端数据基本架构,如图-4所示。
服务端API架构及业务时序图分析
服务端日志分页查询代码基本架构,如图-5所示
服务端日志列表数据查询时序图,如图-6所示:
服务端关键业务及代码实现
Entity类实现
- 业务描述及设计实现
构建实体对象(POJO)封装从数据库查询到的记录,一行记录映射为内存中一个的这样的对象。对象属性定义时尽量与表中字段有一定的映射关系,并添加对应的set/get/toString等方法,便于对数据进行更好的操作。
- 关键代码分析及实现
package com.cy.sys.entity;
import java.io.Serializable;
import java.util.Date;
/**
* 日志管理对象
*/
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
//用户名
private String username;
//用户操作
private String operation;
//请求方法
private String method;
//请求参数
private String params;
//执行时长(毫秒)
private Long time;
//IP地址
private String ip;
//创建时间
private Date createdTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getParams() {
return params;
}
public void setParams(String params) {
this.params = params;
}
public Long getTime() {
return time;
}
public void setTime(Long time) {
this.time = time;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Date getCreatedTime() {
return createdTime;
}
public void setCreatedTime(Date createdTime) {
this.createdTime = createdTime;
}
@Override
public String toString() {
return "SysLog{" +
"id=" + id +
", username='" + username + '\'' +
", operation='" + operation + '\'' +
", method='" + method + '\'' +
", params='" + params + '\'' +
", time=" + time +
", ip='" + ip + '\'' +
", createdTime=" + createdTime +
'}';
}
}
说明:通过此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据,实现层与层之间数据的传递。
思考:这个对象的set方法,get方法可能会在什么场景用到?
Dao接口实现
-
业务描述及设计实现
通过数据层对象,基于业务层参数数据查询日志记录总数以及当前页要呈现的用户行为日志信息。 -
关键代码分析及实现:
第一步:定义数据层接口对象,通过将此对象保证给业务层以提供日志数据访问。代码如下:
第二步:在SysLogDao接口中添加getRowCount方法用于按条件统计记录总数。代码如下:
第三步:在SysLogDao接口中添加findPageObjects方法,基于此方法实现当前页记录的数据查询操作。代码如下:
package com.cy.sys.dao;
import com.cy.sys.entity.SysLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 日志管理Dao
*/
@Mapper
public interface SysLogDao {
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @return 总记录数(基于这个结果可以计算总页数)
*/
int getRowCount(@Param("username") String username);
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @param startIndex 当前页的起始位置
* @param pageSize 当前页的页面大小
* @return 当前页的日志记录信息
* <p>
* 数据库中每条日志信息封装到一个SysLog对象中
*/
List<SysLog> findPageObjects(
@Param("username") String username,
@Param("startIndex") Integer startIndex,
@Param("pageSize") Integer pageSize);
}
说明:
当DAO中方法参数多余一个时尽量使用**@Param注解**进行修饰并指定名字,然后再Mapper文件中便可以通过类似
**#{username}**方式进行获取,否则只能通过#{arg0},#{arg1}或者#{param1},#{param2}等方式进行获取。
当DAO方法中的参数应用在动态SQL中时无论多少个参数,尽量使用@Param注解进行修饰并定义。
Mapper文件实现
- 业务描述及设计实现
基于Dao接口创建映射文件,在此文件中通过相关元素(例如select)描述要执行的数据操作。
配置文件application.yml
#mybatis
mybatis:
configuration:
default-statement-timeout: 30
map-underscore-to-camel-case: true
mapper-locations:
- classpath:/mapper/*/*.xml
- 关键代码设计及实现
第一步:在映射文件的设计目录(mapper/sys)中添加SysLogMapper.xml映射文件,代码如下:
第二步:在映射文件中添加sql元素实现,SQL中的共性操作,代码如下:
第三步:在映射文件中添加id为getRowCount元素,按条件统计记录总数,
第四步:在映射文件中添加id为findPageObjects元素,实现分页查询。
代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cy.sys.dao.SysLogDao">
<sql id="queryWhereId">
from sys_Logs
<where>
<if test="username!=null and username!=''">
username like concat("%",#{username},"%")
</if>
</where>
</sql>
<!-- 查询日志总记录数 -->
<select id="getRowCount" resultType="int">
select count(*)
<include refid="queryWhereId"/>
</select>
<!--查询当前页面记录详情-->
<select id="findPageObjects" resultType="com.cy.sys.entity.SysLog">
select *
<include refid="queryWhereId"/>
order by createdTime desc
limit #{startIndex},#{pageSize}
</select>
</mapper>
Service接口及实现类
-
业务描述与设计实现
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通过业务方法中的参数接收控制层数据(例如username,pageCurrent)并校验。然后基于用户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。 -
关键代码设计及实现
业务层对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码参考如下:
业务层对象
封装数据层查询的返回值
-
业务描述与设计实现
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通过业务方法中的参数接收控制层数据(例如username,pageCurrent)并校验。然后基于用户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。 -
关键代码设计及实现
业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码参考如下:
package com.cy.common.bo;
import java.io.Serializable;
import java.util.List;
/**
* 通用:业务层bo对象_封装数据层查询的数据,并实现分页功能
*
* @param <T>
*/
public class PageObject<T> implements Serializable {
private static final long serialVersionUID = 7160293007556675158L;
/**
* 当前页的页码值
*/
private Integer pageCurrent = 1;
/**
* 页面大小 手动设置
*/
private Integer pageSize = 10;
/**
* 总行数(通过查询获得)
*/
private Integer rowCount = 0;
/**
* 总页数(通过计算获得)
*/
private Integer pageCount = 0;
/**
* 当前页记录
*/
private List<T> records;
public PageObject() {
}
/**
* 实现分页
*
* @param pageCurrent 页码
* @param pageSize 页面大小
* @param rowCount 总记录数
* @param records 页记录
*/
public PageObject(Integer pageCurrent, Integer pageSize, Integer rowCount, List<T> records) {
super();
this.pageCurrent = pageCurrent;
this.pageSize = pageSize;
this.rowCount = rowCount;
this.records = records;
this.pageCount = (rowCount - 1) / pageSize + 1;
}
public Integer getPageCurrent() {
return pageCurrent;
}
public void setPageCurrent(Integer pageCurrent) {
this.pageCurrent = pageCurrent;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
public Integer getRowCount() {
return rowCount;
}
public void setRowCount(Integer rowCount) {
this.rowCount = rowCount;
}
public Integer getPageCount() {
return pageCount;
}
public void setPageCount(Integer pageCount) {
this.pageCount = pageCount;
}
public List<T> getRecords() {
return records;
}
public void setRecords(List<T> records) {
this.records = records;
}
@Override
public String toString() {
return "PageObject{" +
"pageCurrent=" + pageCurrent +
", pageSize=" + pageSize +
", rowCount=" + rowCount +
", pageCount=" + pageCount +
", records=" + records +
'}';
}
}
SysLogService接口
查询日志记录
package com.cy.sys.service;
import com.cy.common.bo.PageObject;
import com.cy.sys.entity.SysLog;
public interface SysLogService {
/**
* 查询日志
* @param name 基于条件查询时的参数名
* @param pageCurrent 当前的页码值
* @return 当前页记录+分页信息
*/
PageObject<SysLog> findPageObjects(String username, Integer pageCurrent);
}
SysLogServiceImpl实现类
查询日志记录
package com.cy.sys.service.impl;
import com.cy.common.bo.PageObject;
import com.cy.common.exception.ServiceException;
import com.cy.sys.dao.SysLogDao;
import com.cy.sys.entity.SysLog;
import com.cy.sys.service.SysLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SysLogServiceImpl implements SysLogService {
@Autowired
private SysLogDao sysLogDao;
/**
* 查询日志
*
* @param username
* @param pageCurrent 当前的页码值
* @return 当前页记录+分页信息
*/
@Override
public PageObject<SysLog> findPageObjects(String username, Integer pageCurrent) {
//1.验证参数合法性
//1.1验证pageCurrent的合法性,
//不合法抛出IllegalArgumentException异常
if (pageCurrent == null || pageCurrent < 1)
throw new IllegalArgumentException("当前页码不正确");
//2.基于条件查询总记录数
//2.1) 执行查询
int rowCount = sysLogDao.getRowCount(username);
//2.2) 验证查询结果,假如结果为0不再执行如下操作
if (rowCount == 0)
throw new ServiceException("系统没有查到对应记录");//自定义:业务层异常
//3.基于条件查询当前页记录(pageSize定义为2)
//3.1)定义pageSize
int pageSize = 10;
//3.2)计算startIndex
int startIndex = (pageCurrent - 1) * pageSize;
//3.3)执行当前数据的查询操作
List<SysLog> records =
sysLogDao.findPageObjects(username, startIndex, pageSize);
//5.返回封装结果。
return new PageObject<>(pageCurrent, pageSize, rowCount, records);
}
}
业务层异常 ServiceException
通过自定义异常可更好的实现对业务问题的描述,同时可以更好的提高用户体验。
参考代码如下:
package com.cy.common.exception;
/**
* 业务层异常
*/
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 3170203650661076982L;
/**
* Constructs a new runtime exception with {@code null} as its
* detail message. The cause is not initialized, and may subsequently be
* initialized by a call to {@link #initCause}.
*/
public ServiceException() {
super();
}
/**
* Constructs a new runtime exception with the specified detail message.
* The cause is not initialized, and may subsequently be initialized by a
* call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public ServiceException(String message) {
super(message);
}
/**
* Constructs a new runtime exception with the specified detail message and
* cause. <p>Note that the detail message associated with
* {@code cause} is <i>not</i> automatically incorporated in
* this runtime exception's detail message.
*
* @param message the detail message (which is saved for later retrieval
* by the {@link #getMessage()} method).
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new runtime exception with the specified cause and a
* detail message of <tt>(cause==null ? null : cause.toString())</tt>
* (which typically contains the class and detail message of
* <tt>cause</tt>). This constructor is useful for runtime exceptions
* that are little more than wrappers for other throwables.
*
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public ServiceException(Throwable cause) {
super(cause);
}
/**
* Constructs a new runtime exception with the specified detail
* message, cause, suppression enabled or disabled, and writable
* stack trace enabled or disabled.
*
* @param message the detail message.
* @param cause the cause. (A {@code null} value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @param enableSuppression whether or not suppression is enabled
* or disabled
* @param writableStackTrace whether or not the stack trace should
* be writable
* @since 1.7
*/
protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
测试
package com.cy.sys.service;
import com.cy.common.bo.PageObject;
import com.cy.sys.entity.SysLog;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class SysLogServiceTest {
@Autowired
private SysLogService sysLogService;
@Test
void testFindPageObjects() {
PageObject<SysLog> pageObject=
sysLogService.findPageObjects("admin", 2);
System.out.println("测试:"+pageObject);
}
}
Controller类实现
- 业务描述与设计实现
控制层对象主要负责请求和响应数据的处理,例如,本模块首先要通过控制层对象处理请求参数,然后通过业务层对象执行业务逻辑,再通过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。 - 关键代码设计与实现
定义控制层值对象(VO),目的是基于此对象封装控制层响应结果(在此对象中主要是为业务层执行结果添加状态信息)。
Spring MVC框架在响应时可以调用相关API(例如jackson)将其对象转换为
JSON格式字符串
代码如下:
package com.cy.common.vo;
import java.io.Serializable;
/**
* 控制层返回视图层的JSON格式字符串
*/
public class JsonResult implements Serializable {
private static final long serialVersionUID = -8389587237488952536L;
/**
* 状态码
*/
private int state = 1;//1表示SUCCESS,0表示ERROR
/**
* 状态信息
*/
private String message = "ok";
/**
* 正确数据
*/
private Object data;
public JsonResult() {
}
/**
* 返回提示信息
* @param message
*/
public JsonResult(String message) {
this.message = message;
}
/**
* 一般查询时调用,封装查询结果
* @param object
*/
public JsonResult(Object object) {
this.data = object;
}
/**
* 出现异常时时调用
* @param t
*/
public JsonResult(Throwable t) {
this.state = 0;
this.message = t.getMessage();
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
Controller
并将此类对象使用Spring框架中的**@Controller注解进行标识,表示此类对象要交给Spring管理。
然后基于@RequestMapping注解**为此类定义根路径映射。代码参考如下:
package com.cy.sys.controller;
import com.cy.common.bo.PageObject;
import com.cy.common.vo.JsonResult;
import com.cy.sys.entity.SysLog;
import com.cy.sys.service.SysLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 日志管理
*/
@Controller
@RequestMapping("/log/")
public class SysLogController {
@Autowired
private SysLogService sysLogService;
}
查询日志页面
/**
* 查询日志页面,返回当前页面数据,以json字符串形式传递
*
* @param username 查询的用户名
* @param pageCurrent 页码值
* @return
*/
@RequestMapping("doFindPageObjects")
@ResponseBody
public JsonResult doFindPageObjects(String username, Integer pageCurrent) {
PageObject<SysLog> pageObject = sysLogService.findPageObjects(username, pageCurrent);
return new JsonResult(pageObject);
}
全局异常处理类
当后端报错时,前端展示错误信息,页面杂乱,给客户不好的体验,
于是全局异常处理类孕育而生。
作用: 对控制层可能出现的异常,进行统一异常处理
package com.cy.common.web;
import com.cy.common.vo.JsonResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 全局异常处理类,对控制层可能出现的异常,进行统一异常处理
*/
@ControllerAdvice
public class GlobalExceptionHandler {
//JDK中的自带的日志API
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public JsonResult doHandleRuntimeException(RuntimeException e) {
e.printStackTrace();//也可以写日志异常信息
return new JsonResult(e);//封装
}
}
@ResponseBody
将返回值转成json字符串给前端页面
测试
正常访问:
http://localhost:8091/log/doFindPageObjects?pageCurrent=1
请求参数错误访问:
http://localhost:8091/log/doFindPageObjects?pageCurrent=0
测试成功
控制层响应数据处理分析
客户端关键业务及代码实现
客户端页面事件分析
当用户点击首页日志管理时,其页面流转分析如图-8所示:
日志列表信息呈现
-
业务描述与设计实现
日志分页页面加载完成以后,向服务端发起异步请求加载日志信息,当日志信息加载完成需要将日志信息、分页信息呈现到列表页面上。 -
关键代码设计与实现 – log_list.html
第一步:分页页面(page.html)加载完成时,(log_list.html)向服务端发起异步请求,代码参考如下:
/**
* 注册初始化函数,加载分页插件及注册函数加载数据
*/
$(function () {
//加载分页页面,并注册doGetObjects函数
$("#pageId").load("doPageUI", doGetObjects);
})
第二步:定义异步请求处理函数,代码参考如下:
/**
* 第二步:定义异步请求处理函数,代码参考如下:
*/
function doGetObjects() {
//debugger;//断点调试
//1.定义url和参数
const url = "/log/doFindPageObjects";
//动态实现分页功能? 请问data函数的含义是什么?(从指定元素上获取绑定的数据)
//此数据会在何时进行绑定?(setPagination,doQueryObjects)
let pageCurrent = $("#pageId").data("pageCurrent");
//为什么要执行如下语句的判定,然后初始化pageCurrent的值为1
//pageCurrent参数在没有赋值的情况下,默认初始值应该为1.
if(!pageCurrent) pageCurrent=1;
const params = {"pageCurrent": pageCurrent};//pageCurrent=2
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?//可以,必须符合标识符的规范
$.getJSON(url, params, function (result) {
//请问result是一个字符串还是json格式的js对象?json格式的js对象
doHandleQueryResponseResult(result);
}
);
}
result 结果对象分析,如图-9所示:
第三步:定义回调函数,处理服务端的响应结果。代码如下
/**
* 第三步:定义回调函数,处理服务端的响应结果。代码如下
* @param result
*/
function doHandleQueryResponseResult(result) { //JsonResult
if (result.state == 1) {//ok
//更新table中tbody内部的数据
doSetTableBodyRows(result.data.records);//将数据呈现在页面上
//更新页面page.html分页数据
//doSetPagination(result.data); //此方法写到page.html中
} else {
// alert(result.message);//弹窗:错误信息
doSetQueryErrors(result.message);
}
}
function doSetQueryErrors(message) {
$("#tbodyId").html(`<tr><td colspan='7'>${message}</td></tr>`);
}
第四步:将异步响应结果呈现在table的tbody位置。代码参考如下
/**
* 第四步:将异步响应结果呈现在table的tbody位置。代码参考如下
* @param records
*/
function doSetTableBodyRows(records) {
//1.获取tbody对象,并清空对象
const tBody = $("#tbodyId");
tBody.empty();
//2.迭代records记录,并将其内容追加到tbody
for (const i in records) {
//2.1 构建tr对象
const tr = $("<tr></tr>");
//2.2 构建tds对象
const tds = doCreateTds(records[i]);
//2.3 将tds追加到tr中
tr.append(tds);
//2.4 将tr追加到tbody中
tBody.append(tr);
}
}
第五步:创建每行中的td元素,并填充具体业务数据。代码参考如下:
/**
* 第五步:创建每行中的td元素,并填充具体业务数据。代码参考如下:
* @param data
* @returns {string}
*/
function doCreateTds(data) {
const tds = "<td><input type='checkbox' class='cBox' name='cItem' value='" + data.id + "'></td>" +
"<td>" + data.username + "</td>" +
"<td>" + data.operation + "</td>" +
"<td>" + data.method + "</td>" +
"<td>" + data.params + "</td>" +
"<td>" + data.ip + "</td>" +
"<td>" + data.time + "</td>";
return tds;
}
页面效果
加载分页插件数据
-
业务描述与设计实现
日志信息列表初始化完成以后初始化分页数据(调用setPagination函数),然后再点击上一页,下一页等操作时,更新页码值,执行基于当前页码值的查询。 -
关键代码设计与实现:
第一步:在page.html页面中定义doSetPagination方法(实现分页数据初始化),代码如下:
/**
* 定义分页数据处理函数
* @param page
*/
function doSetPagination(page){
//1.始化数据
$(".rowCount").html("总记录数("+page.rowCount+")");
$(".pageCount").html("总页数("+page.pageCount+")");
$(".pageCurrent").html("当前页("+page.pageCurrent+")");
//2.绑定数据(为后续对此数据的使用提供服务)
$("#pageId").data("pageCurrent",page.pageCurrent);
$("#pageId").data("pageCount",page.pageCount);
}
第二步 : 调用doSetPagination函数
/**
* 第三步:定义回调函数,处理服务端的响应结果。代码如下
* @param result
*/
function doHandleQueryResponseResult(result) { //JsonResult
if (result.state == 1) {//ok
//更新table中tbody内部的数据
doSetTableBodyRows(result.data.records);//将数据呈现在页面上
//更新页面page.html分页数据
doSetPagination(result.data); //调用分页加载函数
} else {
alert(result.message);//弹窗:错误信息
}
}
页面效果:
第三步:分页页面page.html中注册点击事件。实现分页按钮的功能,代码如下:
/**
* 实现跳转到首页,上一页,下一页...等按钮功能
* 注册按钮点击事件
*/
$(function () {
$("#pageId").on("click", ".first,.pre,.next,.last", doJumpToPage);
})
第四步:定义doJumpToPage方法(通过此方法实现当前数据查询)
/**
* 定义跳转函数
*/
function doJumpToPage() {
//1.获取点击对象的class值
const cls = $(this).prop("class");//Property
//2.基于点击的对象执行pageCurrent值的修改
//2.1获取pageCurrent,pageCount的当前值
let pageCurrent = $("#pageId").data("pageCurrent");
const pageCount = $("#pageId").data("pageCount");
//2.2修改pageCurrent的值
if (cls == "first") {//首页
pageCurrent = 1;
} else if (cls == "pre" && pageCurrent > 1) {//上一页
pageCurrent--;
} else if (cls == "next" && pageCurrent < pageCount) {//下一页
pageCurrent++;
} else if (cls == "last") {//最后一页
pageCurrent = pageCount;
} else {
return;
}
//3.对pageCurrent值进行重新绑定
$("#pageId").data("pageCurrent", pageCurrent);
//4.基于新的pageCurrent的值进行当前页数据查询
doGetObjects();
}
实现用户名查询按钮功能
- 业务描述及设计
当用户点击日志列表的查询按钮时,基于用户输入的用户名进行有条件的分页查询,并将查询结果呈现在页面。 - 关键代码设计与实现:
第一步:日志列表页面加载完成,log_list.html在查询按钮上进行事件注册doQueryObjects。代码如下:
/**
* 注册初始化函数,加载分页插件及注册函数加载数据
*/
$(function () {
//加载分页页面,并注册doGetObjects函数
$("#pageId").load("doPageUI", doGetObjects);
//注册用户查询按钮事件
$(".input-group-btn").on("click",".btn-search",doQueryObjects)
})
第二步:定义查询按钮对应的点击事件处理函数。代码如下:
/**
* 用户查询按钮事件
*/
function doQueryObjects() {
//为什么要在此位置初始化pageCurrent的值为1?
//数据查询时页码的初始位置也应该是第一页
$("#pageId").data("pageCurrent", 1);
//为什么要调用doGetObjects函数?
//重用js代码,简化jS代码编写。
doGetObjects();
}
第三步:在分页查询函数中追加name参数定义,代码如下:
/**
* 第二步:定义异步请求处理函数,代码参考如下:
*/
function doGetObjects() {
//debugger;//断点调试
//1.定义url和参数
const url = "/log/doFindPageObjects";
//? 请问data函数的含义是什么?(从指定元素上获取绑定的数据)
//此数据会在何时进行绑定?(setPagination,doQueryObjects)
let pageCurrent = $("#pageId").data("pageCurrent");
//为什么要执行如下语句的判定,然后初始化pageCurrent的值为1
//pageCurrent参数在没有赋值的情况下,默认初始值应该为1.
if (!pageCurrent) pageCurrent = 1;
const params = {"pageCurrent": pageCurrent};//pageCurrent=2
//为什么此位置要获取查询参数的值?
//一种冗余的应用方法,目的时让此函数在查询时可以重用。
const username = $("#searchNameId").val();
//如下语句的含义是什么?动态在json格式的js对象中添加key/value,
if(username) params.username=username;//查询时需要,当username不为空时,在params追加username参数
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?//可以,必须符合标识符的规范
$.getJSON(url, params, function (result) {
//请问result是一个字符串还是json格式的js对象?json格式的js对象
doHandleQueryResponseResult(result);
}
);
}
页面效果:
四. 日志管理删除操作实现
数据架构分析
当用户执行日志删除操作时,客户端与务端交互时的基本数据架构,如图-10所示。
删除业务时序分析
客户端提交删除请求,服务端对象的工作时序分析,如图-11所示。
服务端关键业务及代码实现
Dao接口实现
-
业务描述及设计实现
数据层基于业务层提交的日志记录id,进行日志删除操作。 -
关键代码设计及实现:
在SysLogDao中添加基于id执行日志删除的方法。代码参考如下:
/**
* 批量删除日志
* @param ids
* @return
*/
int deleteObjects(@Param("ids") Integer... ids);
Mapper文件实现
注: 当ids为空或长度<=0时,数据库应不执行删除操作
<delete id="deleteObjects">
delete from sys_logs
<choose>
<when test="ids!=null and ids.length>0">
<where>
<foreach collection="ids"
item="id"
separator="or">
id=#{id}
</foreach>
</where>
</when>
<otherwise>
where 1==2
</otherwise>
</choose>
</delete>
说明:这里的
choose元素也为一种选择结构,when元素相当于if,otherwise相当于else的语法。
foreach元素为拼接,其中collection指向数组,item指向数组元素,separator表示元素之间如何拼接.
Service接口及实现类
-
业务描述与设计实现
在日志业务层定义用于执行删除业务的方法,首先通过方法参数接收控制层传递的多个记录的id,并对参数id进行校验。然后基于日志记录id执行删除业务实现。最后返回业务执行结果。 -
关键代码设计与实现
第一步:在SysLogService接口中,添加基于多个id进行日志删除的方法。关键代码如下:
int deleteObjects(Integer... ids);
第二步:在SysLogServiceImpl实现类中添加删除业务的具体实现。关键代码如下:
/**
* 批量删除日志
* @param ids
* @return
*/
@Override
public int deleteObjects(Integer... ids) {
//1.判定参数合法性
if (ids == null || ids.length == 0)
throw new IllegalArgumentException("请选择一个");
//2.执行删除操作
int rows;
try {
rows = sysLogDao.deleteObjects(ids);
} catch (Throwable e) {
e.printStackTrace();
//发出报警信息(例如给运维人员发短信)
throw new ServiceException("系统故障,正在恢复中...");
}
//4.对结果进行验证
if (rows == 0)
throw new ServiceException("记录可能已经不存在");
//5.返回结果
return rows;
}
Controller类实现
-
业务描述与设计实现
在日志控制层对象中,添加用于处理日志删除请求的方法。首先在此方法中通过形参接收客户端提交的数据,然后调用业务层对象执行删除操作,最后封装执行结果,并在运行时将响应对象转换为JSON格式的字符串,响应到客户端。 -
关键代码设计与实现
第一步:在SysLogController中添加用于执行删除业务的方法。代码如下:
/**
* 按id批量删除日志
*
* @param ids
* @return
*/
@RequestMapping("doDeleteObjects")
@ResponseBody
public JsonResult doDeleteObjects(Integer... ids) {
sysLogService.deleteObjects(ids);
return new JsonResult("delete ok");
}
第二步:启动tomcat进行访问测试,打开浏览器输入如下网址:
http://localhost:8091/log/doDeleteObjects?ids=9,10,11
客户端关键业务及代码实现
日志列表页面事件处理
-
业务描述及设计实现
用户在页面上首先选择要删除的元素,然后点击删除按钮,将用户选择的记录id异步提交到服务端,最后在服务端执行日志的删除动作。 -
关键代码设计与实现
第一步:页面加载完成以后,在删除按钮上进行点击事件注册。关键代码如下:
$(".input-group-btn").on("click",".btn-delete",doDeleteObjects);
第二步:定义删除操作对应的事件处理函数。关键代码如下:
/**
* 删除点击事件
*/
function doDeleteObjects() {
//1.获取选中的id值
const ids = doGetCheckedIds();
if (ids.length == 0) {
alert("至少选择一个");
return;
}
//2.发异步请求执行删除操作
const url = "log/doDeleteObjects";
const params = {"ids": ids.toString()};
console.log(params);
$.post(url, params, function (result) {
if (result.state == 1) {
alert(result.message);
doRefreshAfterDeleteOK();//删除后,页面状态更新
} else {
alert(result.message);
}
});
}
当在最后一页执行删除操作时,基于全选按钮状态及当前页码值,刷新页面。关键代码如下:
* 当在最后一页执行删除操作时,基于全选按钮状态及当前页码值,刷新页面
*/
function doRefreshAfterDeleteOK() {
const pageCount = $("#pageId").data("pageCount");
let pageCurrent = $("#pageId").data("pageCurrent");
const checked = $("#checkAll").prop("checked");
if (pageCurrent == pageCount && checked && pageCurrent > 1) {
pageCurrent--;
$("#pageId").data("pageCurrent", pageCurrent);
}
doGetObjects();
}
第三步:定义获取用户选中的记录id的函数。关键代码如下:
/**
* 获取用户选中的记录id
*/
function doGetCheckedIds() {
//定义一个数组,用于存储选中的checkbox的id值
const array = [];//new Array();
//获取tbody中所有类型为checkbox的input元素
$("#tbodyId input[type=checkbox]").
//迭代这些元素,每发现一个元素都会执行如下回调函数
each(function () {
//假如此元素的checked属性的值为true
if ($(this).prop("checked")) {
//调用数组对象的push方法将选中对象的值存储到数组
array.push($(this).val());
}
});
return array;
}
实现全选功能
第四步:实现全选,thead中全选元素的状态影响tbody中checkbox对象状态。代码如下:
注册全选事件
$(".table-responsive").on("click", "#checkAll", doChangeTBodyCheckBoxState);
处理函数
/**
* 当点击全选按钮时,改变各行按钮状态
*/
function doChangeTBodyCheckBoxState() {
//1.获取当前点击对象的checked属性的值
const flag = $(this).prop("checked");//true or false
//2.将tbody中所有checkbox元素的值都修改为flag对应的值。
$("#tbodyId input[type='checkbox']")
.prop("checked", flag);
}
第五步:tbody中checkbox的状态影响thead中全选元素的状态。当tbody全选时修改全选按钮状态,代码如下:
/**
* 检测各行选中按钮状态,确认全选按钮状态
*/
function doChangeTHeadCheckBoxState() {
//1.设定默认状态值
let flag = true;
//2.迭代所有tbody中的checkbox值并进行与操作
$("#tbodyId input[type='checkbox']")
.each(function () {
flag = flag & $(this).prop("checked")
});
//3.修改全选元素checkbox的值为flag
$("#checkAll").prop("checked", flag);
}
第六步: 页面加载时,确定全选框状态
在数据返回处理上加载doChangeTHeadCheckBoxState函数
function doGetObjects() {
//debugger;//断点调试
//1.定义url和参数
const url = "/log/doFindPageObjects";
//? 请问data函数的含义是什么?(从指定元素上获取绑定的数据)
//此数据会在何时进行绑定?(setPagination,doQueryObjects)
let pageCurrent = $("#pageId").data("pageCurrent");
//为什么要执行如下语句的判定,然后初始化pageCurrent的值为1
//pageCurrent参数在没有赋值的情况下,默认初始值应该为1.
if (!pageCurrent) pageCurrent = 1;
const params = {"pageCurrent": pageCurrent};//pageCurrent=2
//为什么此位置要获取查询参数的值?
//一种冗余的应用方法,目的时让此函数在查询时可以重用。
const username = $("#searchNameId").val();
//如下语句的含义是什么?动态在json格式的js对象中添加key/value,
if (username) params.username = username;//查询时需要,当username不为空时,在params追加username参数
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?//可以,必须符合标识符的规范
$.getJSON(url, params, function (result) {
//请问result是一个字符串还是json格式的js对象?json格式的js对象
doHandleQueryResponseResult(result);
doChangeTHeadCheckBoxState();//加载全选框状态
}
);
}
自定义异常
业务层异常 ServiceException
通过自定义异常可更好的实现对业务问题的描述,同时可以更好的提高用户体验。
参考代码如下:
package com.cy.common.exception;
/**
* 业务层异常
*/
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 3170203650661076982L;
/**
* Constructs a new runtime exception with {@code null} as its
* detail message. The cause is not initialized, and may subsequently be
* initialized by a call to {@link #initCause}.
*/
public ServiceException() {
super();
}
/**
* Constructs a new runtime exception with the specified detail message.
* The cause is not initialized, and may subsequently be initialized by a
* call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public ServiceException(String message) {
super(message);
}
/**
* Constructs a new runtime exception with the specified detail message and
* cause. <p>Note that the detail message associated with
* {@code cause} is <i>not</i> automatically incorporated in
* this runtime exception's detail message.
*
* @param message the detail message (which is saved for later retrieval
* by the {@link #getMessage()} method).
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new runtime exception with the specified cause and a
* detail message of <tt>(cause==null ? null : cause.toString())</tt>
* (which typically contains the class and detail message of
* <tt>cause</tt>). This constructor is useful for runtime exceptions
* that are little more than wrappers for other throwables.
*
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public ServiceException(Throwable cause) {
super(cause);
}
/**
* Constructs a new runtime exception with the specified detail
* message, cause, suppression enabled or disabled, and writable
* stack trace enabled or disabled.
*
* @param message the detail message.
* @param cause the cause. (A {@code null} value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @param enableSuppression whether or not suppression is enabled
* or disabled
* @param writableStackTrace whether or not the stack trace should
* be writable
* @since 1.7
*/
protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
全局异常处理类
没有全局异常处理类时,前端展示页面遇到后端异常时:
有全局异常处理类时,前端展示页面遇到后端异常时:
作用: 对控制层可能出现的异常,进行统一异常处理
package com.cy.common.web;
import com.cy.common.vo.JsonResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 全局异常处理类,对控制层可能出现的异常,进行统一异常处理
*/
@ControllerAdvice
public class GlobalExceptionHandler {
//JDK中的自带的日志API
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public JsonResult doHandleRuntimeException(RuntimeException e) {
e.printStackTrace();//也可以写日志异常信息
return new JsonResult(e);//封装
}
}
JS 问题如何解决?
console.log(),debugger,排除法