51,52,53--DB旅游生态系统-日志模块设计

DB-日志管理模块设计

日志管理设计说明

记录用户操作的日志功能, 可以对用户的操作习惯进行分析

主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。

数据库表设计:

表明根据需求, 这里使用sys_logs作为表明

字段 类型 注释
id bigint(20) 主键自增
username varchar(50) 记录用户名
operation varchar(50) 记录用户操作
method varchar(200) 请求方法
params varchar(5000) 请求参数
time bigint(20) 执行时长
ip varcher(64) 用户ip
createdTime datetime 创建时间

API设计说明

说明:分层目的主要将复杂问题简单化,实现各司其职,各尽所能。

日志管理列表页面呈现

业务时序分析

1594544130905

服务端实现

Controller实现

  • 业务描述与设计实现

    基于日志管理的请求业务,在PageController中添加doLogUI方法,doPageUI方法分别用于返回日志列表页面,日志分页页面。

  • 关键代码设计与实现

    • 第一步:在PageController中定义返回日志列表的方法。代码如下:
    @RequestMapping("log/log_list")
    public String doLogUI() {
    	return "sys/log_list";
    }
    
    • 第二步:在PageController中定义用于返回分页页面的方法。代码如下:@RequestMapping("doPageUI")
    public String doPageUI() {
    	return "common/page";
    }
    

客户端实现

日志菜单事件处理

  • 业务描述与设计

    ​ 首先准备日志列表页面(/templates/pages/sys/log_list.html),然后在starter.html页面中点击日志管理菜单时异步加载日志列表页面。

  • 关键代码设计与实现

    ​ 找到项目中的starter.html 页面,页面加载完成以后,注册日志管理菜单项的点击事件,当点击日志管理时,执行事件处理函数。关键代码如下:

    <script type="text/javascript">
       //此函数是在页面加载完成以后执行
       (function(){
    	 doLoadUI("load-log-id","log/log_list");
       })()
       //基于不同对象的点击事件执行资源加载操作
       function doLoadUI(id,url){
    	   $("#"+id).click(function(){//click事件处理函数
               //jquery中的load函数为一个异步加载的ajax函数。
               //此函数用于在指定位置异步加载资源(并将返回的资源填充到这个指定位置)
    	       $("#mainContentId").load(url);
    	   })
       }
    </script>
    

日志列表页面事件处理

  • 业务描述与设计实现

    当日志列表页面加载完成以后异步加载分页页面(page.html)。

  • 关键代码设计与实现:

    ​ 在log_list.html页面中异步加载page页面,这样可以实现分页页面重用,哪里需要分页页面,哪里就进行页面加载即可。

    $(function(){
    	$("#pageId").load("doPageUI");
    });
    

    说明:数据加载通常是一个相对比较耗时操作,为了改善用户体验,可以先为用户呈现一个页面,数据加载时,显示数据正在加载中,数据加载完成以后再呈现数据。这样也可满足现阶段不同类型客户端需求.

日志管理列表数据呈现

数据架构分析

日志查询服务端数据基本架构

服务端API架构及业务时序图分析

  • 服务端日志分页查询代码基本架构

1594544946669

  • 服务端日志列表数据查询时序图

1594544961136

服务端关键业务及代码实现

POJO类实现

此对象主要用于封装数据库提取的数据, 或者向输入框写入的数据, 此对象中的属性建议和表中字段有对应的映射关系(名字, 类型)

建议: 所有用于封装数据的对象都建议实现序列化接口 (Serializable), 将对象通过网络进行传输

  1. 序列化: 将对象转为字节的过程称之为对象序列化
  2. 反序列化: 将字节转为对象的过程称之为反序列化
  3. 应用场景: 对对象进行缓存, 将对象进行钝化(写入文件)
package com.cy.pj.sys.pojo;
@Data
public class SysLog implements Serializable{
	//对象在序列化和反序列化时会基于此id进行数据处理。
	//将对象序列化时会将这个id作为版本写入到字节中。
	//将字节反序列化会从字节中提取这个版本id然后和类中的版本id进行比对,一致则进行反序列化。
	private static final long serialVersionUID = -1592163223057343412L;
	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;

}

说明:通过此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据,实现层与层之间数据的传递。

Dao接口实现

  • 业务描述及设计实现

    ​ 通过数据层对象,基于业务层参数数据查询日志记录总数以及当前页要呈现的用户行为日志信息。

  • 关键代码分析及实现:

    ​ 1 定义数据层接口对象,通过将此对象保证给业务层以提供日志数据访问

    ​ 2 在SysLogDao接口中添加getRowCount方法用于按条件统计记录总数

    ​ 3 在SysLogDao接口中添加findPageObjects方法,基于此方法实现当前页记录的数据查询操作。

package com.cy.pj.sys.dao;
@Mapper
public interface SysLogDao {
    /**
	   * 	基于条件查询用户行为日志记录总数
	   * @param username 查询条件
	   * @return 查询到的记录总数
	   */
    int getRowCount(@Param("username")String username);
    /**
	   * 	基于条件查询当前页记录
	   * @param username 查询条件
	   * @param startIndex 当前页数据的起始位置(用于limit 子句)
	   * @param pageSize 当前页面大小(每页最多显示多少条记录)
	   * @return 查询到的记录
	   */
    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)描述要执行的数据操作

  • 关键代码设计及实现

    在映射文件的设计目录(mapper/sys)中添加SysLogMapper.xml映射文件

<?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.pj.sys.dao.SysLogDao">
       <!-- 通过sql标签提取sql中共性 -->
       <sql id="queryWhereId">
              from sys_logs
              <if test="username!=null and username!=''">
                 <where>
                   username like concat("%",#{username},"%")
                 </where>
             </if>
       </sql> 
       <!-- 查询当前页记录总数 -->
       <select id="findPageObjects" 
               resultType="com.cy.pj.sys.pojo.SysLog">
               select *
               <!-- 包含id为 queryWhereId的sql元素-->
               <include refid="queryWhereId"/>
               order by createdTime
               limit #{startIndex},#{pageSize}
       </select>
       <!-- 按条件查询总记录数 -->
       <select id="getRowCount" resultType="int">
          select count(*)
          <include refid="queryWhereId"/>
       </select>
  </mapper>

注意:

提取sql语句的共性内容使用sql标签进行提取,并且赋予id属性然后再所需的sql标签内部通过 <include refid="id"/>标签直接引入,减少代码的编写.

bug分析:

1594546067320

1594546908533

测试数据层数据:

package com.cy.pj.sys.dao;//测试包下
@SpringBootTest
public class SysLogDaoTests {
	@Autowired
	private SysLogDao sysLogDao;
	@Test
	public void testGetRowCount() {
		System.out.println(sysLogDao.getRowCount("admin"));
	}
	@Test
	public void testFindPageObjects() {
		List<SysLog> list = sysLogDao.findPageObjects("admin", 0, 3);
//		for (SysLog sysLog : list) {
//			System.out.println(sysLog);
//		}
		list.forEach(System.out::println);//遍历方式
	}
}

Service接口及实现类

  • 业务描述与设计实现

    ​ 业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通过业务方法中的参数接收控制层数据(例如username,pageCurrent)并校验。然后基于用户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。

  • 关键代码设计及实现

    业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息

    基于此对象封装业务执行结果

    在Java语言,可以简单将内存中的对象分为两大类:

    1)存储数据的对象(设计的关键在属性上)-典型的POJO对象(VO,BO,DO)

    2)执行业务的对象(设计的关键在方法上)-典型的controller,service,dao

package com.cy.pj.common.pojo;
@Data
@NoArgsConstructor
//@AllArgsConstructor
public class PageObject<T> implements Serializable {//pojo中的bo对象
    private static final long serialVersionUID = -3130527491950235344L;	
    //总记录数 查出来的
    private Integer rowCount;
    //总页数 计算出来的 基于rowCount 和 pageSize
    private Integer pageCount;
    //页面大小(每页最多可以呈现的记录总数)
    private Integer pageSize;
    //当前页码值 客户端传过来的
    private Integer pageCurrent;
    //当前页记录 list集合中的T由PageObject类上定义的泛型决定
    private List<T> records;
    public PageObject(Integer rowCount, Integer pageSize, Integer pageCurrent, List<T> records) {
        this.rowCount = rowCount;
        this.pageSize = pageSize;
        this.pageCurrent = pageCurrent;
        this.records = records;
        //计算总页数的方法一:
        //		this.pageCount =this.rowCount/this.pageSize;
        //		if (this.rowCount%this.pageSize!=0) {
        //			this.pageCount++;
        //		}
        //计算总页数的方法二:
        this.pageCount =(rowCount-1)/pageSize+1;
    }
}

定义日志业务接口及方法,暴露外界对日志业务数据的访问

package com.cy.pj.sys.service;
public interface SysLogService {
	     /**
      * @param name 基于条件查询时的参数名
      * @param pageCurrent 当前的页码值
      * @return 当前页记录+分页信息
      */
	 PageObject<SysLog> findPageObjects(
			 String username,
			 Integer pageCurrent);
}

日志业务接口及实现类,用于具体执行日志业务数据的分页查询操作

package com.cy.pj.sys.service.impl;
@Service
public class SysLogServiceImpl implements SysLogService {
	@Autowired
	private SysLogDao sysLogDao;
	@Override
	public PageObject<SysLog> findPageObjects(String username, Integer pageCurrent) {
		//1.参数校验。
		if(pageCurrent==null||pageCurrent<1)
			throw new IllegalArgumentException("页码值无效");
		//2.基于用户名查询总记录数并校验。
		int rowCount=sysLogDao.getRowCount(username);
		if(rowCount==0)
			//为了对业务中的信息进行更好的反馈和定位,通常会在项目中自定义异常
			//throw new RuntimeException("记录不存在");//尽量避免抛出RuntimeException
			throw new ServiceException("记录不存在");
		//3.基于pageCurrent查询当前页记录。
		int pageSize=3;//每页最多显示多少条记录
		int startIndex=(pageCurrent-1)*pageSize;
		List<SysLog> records=
		sysLogDao.findPageObjects(username, startIndex, pageSize);
		//4.对查询结果进行封装并返回。
		//封装方式1
//		PageObject<SysLog> pageObject=new PageObject<>();
//		pageObject.setRowCount(rowCount);
//		pageObject.setPageSize(pageSize);
//		pageObject.setPageCurrent(pageCurrent);
//		int pageCount=rowCount/pageSize;
//		if(rowCount%pageSize!=0)pageCount++;
//		pageObject.setPageCount(pageCount);
//		pageObject.setRecords(records);
//		return pageObject;
		//封装方式2
		//return new PageObject<>(rowCount, pageCount, pageSize, pageCurrent, records);
	    //封装方式3
		return new PageObject<>(rowCount, pageSize, pageCurrent, records);
	}
}

在当前方法中需要的ServiceException是一个自己定义的异常, 通过自定义异常可更好的实现对业务问题的描述,同时可以更好的提高用户体验

此类中的构造方法参考父类构造方法进行实现,几乎在所有的框架中都提供了自定义异常,例如MyBatis中的BindingException等

package com.cy.pj.common.exception;
/**
   * 自定义非检查异常
 *   目的:对对业务中的信息进行更好的反馈和定位
 * @author qilei
 *   说明:此类中的构造方法参考父类构造方法进行实现
 */
public class ServiceException extends RuntimeException {
	private static final long serialVersionUID = -9085326160255400760L;
	public ServiceException(String message, Throwable cause) {
		super(message, cause);
	}

	public ServiceException(String message) {
		super(message);
	}

	public ServiceException(Throwable cause) {
		super(cause);
	}	
}

业务层对象测试

package com.cy.pj.sys.service;
import org.junit.jupiter.api.Test;
@SpringBootTest
public class SysLogServiceTests {
	@Resource
	private SysLogService serviceLogService;		
	@Test
	public void testFindPageObjects() {
		PageObject<SysLog> pageObject = serviceLogService.findPageObjects("admin", 1);
		System.out.println(pageObject);
		
	}	
}

bug分析

1594546928992

1594546941441

Controller类实现

  • 业务描述与设计实现

    ​ 控制层对象主要负责请求和响应数据的处理,本模块首先要通过控制层对象处理请求参数,然后通过业务层对象执行业务逻辑,再通过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。

  • 关键代码设计与实现

    定义控制层值对象:基于此对象封装服务端要响应到客户端的数据, 这个数据包含:

    1. 状态码 (表示这个响应结果是正确的还是错误的)
    2. 状态信息 (状态码对应的消息)
    3. 正常的响应数据(例如一个查询结果)

    POJO: (VO-View Object封装了表现层要呈现的数据)

package com.cy.pj.common.pojo;
@Data
@NoArgsConstructor
public class JsonResult implements Serializable{
	 private static final long serialVersionUID = 5110901796917551720L;
	/**状态吗*/
	 private Integer state=1;
	 /**状态信息*/
	 private String message="ok";
	 /**正常的响应数据*/
	 private Object data;
	 
	 public JsonResult(String message){// 构造方法, 设置其他状态信息
		 this.message=message;
	 }
	 public JsonResult(Object data){// 构造方法, 用于封装数据
		 this.data=data;
	 }
	 public JsonResult(Throwable e){//Throwable为所有异常类的父类
		 this.state=0;
		 this.message=e.getMessage();
	 }
	 //.....
}

定义Controller类,并将此类对象使用Spring框架中的@Controller注解进行标识,表示此类对象要交给Spring管理。然后基于@RequestMapping注解为此类定义根路径映射

然后在Controller类中添加分页请求处理方法

package com.cy.pj.sys.controller;
@Controller
@RequestMapping("/log/")
public class SysLogController {
	@Autowired
	private SysLogService sysLogService;
	
	@RequestMapping("doFindPageObjects")
	@ResponseBody
	public JsonResult doFindPageObjects(String username,Integer pageCurrent) {
		PageObject<SysLog> pageObject=sysLogService.findPageObjects(username, pageCurrent);
		//控制层对业务数据再次封装
		//方法1
		//JsonResult jr=new JsonResult();
		//jr.setData(pageObject);
		//方法2
		return new JsonResult(pageObject);//此位置封装为业务的正常数据
	}
}

统一异常处理

定义全局异常处理类,对控制层可能出现的异常,进行统一异常处理

package com.cy.pj.common.web;
/**
 * @ControllerAdvice 注解描述的类,spring mvc会认为它是一个控制层全局异常处理对象。
 */
@ControllerAdvice
public class GlobalExceptionHandler {
      /**
       * @ExceptionHandler 注解描述的方法为异常处理方法(不是我们定的),
               *   此注解中定义的异常类型,为这个方法可以处理的异常类型,它可以处理
               *   此异常以及这个异常类型的子类类型的异常。
       * @param e 此参数用于接收具体异常对象,其类型一般与@ExceptionHandler
               *   注解中定义异常类型相同或者为其父类类型。
       * @return 封装了异常信息的对象
       */
	  @ExceptionHandler(RuntimeException.class)
	  @ResponseBody //告诉spring mvc最终将响应结果转换为json格式进行输出。
	  public JsonResult doHandleRuntimeException(RuntimeException e) {
		  //方法内部实现异常处理
		  //例如:输出异常,封装异常
		  //输出或打印异常
		  e.printStackTrace();
		  //封装异常信息
		  return new JsonResult(e);
	  }
	  //。。。。。。。。。。。	  
}

控制层响应数据处理分析

1594549084169

客户端关键业务及代码实现

客户端页面事件分析

​ 当用户点击首页日志管理时,其页面流转分析如图

1594549304009

数据分析

1594549360149

1594549372145

日志列表信息呈现

  • 业务描述与设计实现

    ​ 日志分页页面加载完成以后,向服务端发起异步请求加载日志信息,当日志信息加载完成需要将日志信息、分页信息呈现到列表页面上。

  • 关键代码设计与实现

    第一步:分页页面加载完成,向服务端发起异步请求

    $(function(){
        //debugger
        //当日志列表页面加载完成以后,加载分页页面。
        // $("#pageId").load("doPageUI",function(){//资源加载完成以后执行
        //异步加载用户行为日志信息
        // doGetObjects();
        // });
        //上面写法的简化形式
        $("#pageId").load("doPageUI",doGetObjects);//不要写成doGetObjects()
    });
    

    第二步:定义异步请求处理函数

    //开启ajax异步任务,按条件加载用户行为日志信息
    function doGetObjects(){
        //1.定义请求参数
        //1.1定义参数的方式1
        //var params="pageCurrent=1";
        //1.2定义参数的方式2
        var params={"pageCurrent":1};
        //2.定义请求url
        var url="log/doFindPageObjects";
        //3.发送异步请求处理响应结果
        //$.getJSON(url,params,function(result){
        //doHandleQueryResponseResult(result);
        //});
        //如上方法的简写形式
        $.getJSON(url,params,doHandleQueryResponseResult);
    
        //getJSON函数内部的实现是如下形式
        /*$.ajax({
        		url:url,
        		dataType:"json",
        		data:params,
        		success:doHandleQueryResponseResult
        	 });*/
    
        //$.ajax底层实现
        /*
        	var xhr=new XMLHttpRequest();
        	xhr.onreadystatechange=function(){
        	   if(xhr.readyState==4&&xhr.status==200){
        		   doHandleQueryResponseResult(JSON.parse(xhr.responseText));
        	   }
        	};
        	xhr.open("Get","log/doFindPageObjects?pageCurrent=1",true);
        	xhr.send(null);
        	*/
    }
    

    第三步:定义回调函数,处理服务端的响应结果

    //处理查询结果
    function doHandleQueryResponseResult(result){//result为json格式的js对象
        //console.log("result",result);
        //console.log("result.state",result.state);
        //console.log("result.data",result.data);//对应服务端的pageObject
        //console.log("result.data.records",result.data.records);//对应服务端查询到的用户日志
        if(result.state==1){//state==1表示是正常的响应数据
            //将result.data.records呈现在log_list.html的tbody位置
            doSetTableBodyRows(result.data.records);
            //将result.data中的分页信息呈现在page.html指定位置
            //作业:参考文档去实现,并尝试实现分页事件处理
        }else{
            alert(result.message);
        }
    }
    

    第四步:更新table对象的tbody位置的内容

    function doSetTableBodyRows(records){
        //1.获得tbody对象并清空原有内容
        var tBody=$("#tbodyId");
        tBody.empty();
        //2.迭代records记录并将其追加到tbody中
        //方法1
        /*
        	for(var i=0;i<records.legnth;i++){
        		tBody.append(doCreateRows(records[i]));
        	}
        	*/
        //方法2:
        records.forEach((item)=>tBody.append(doCreateRows(item)));
    }
    

    第五步:基于记录创建行(tr)对象

    function doCreateRows(item){
        //debugger
        return `<tr>
                    <td><input type='checkbox' value=${item.id}></td>
                    <td>${item.username}</td>
                    <td>${item.operation}</td>
                    <td>${item.method}</td>
                    <td>${item.params}</td>
                    <td>${item.ip}</td>
                    <td>${item.time}</td>
                </tr>`;
    }
    

分页数据信息呈现

  • 业务描述与设计实现

    ​ 日志信息列表初始化完成以后初始化分页数据(调用doSetPagination函数),然后再点击上一页,下一页等操作时,更新页码值,执行基于当前页码值的查询。

  • 关键代码设计与实现:

    ​ 第一步:在page.html页面中定义doSetPagination方法(实现分页数据初始化)

    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);
        }
    

    ​ 第二步:分页页面page.html中注册点击事件。

    $(function(){
        	//事件注册
        	 $("#pageId").on("click",".first,.pre,.next,.last",doJumpToPage);
    });
    

    ​ 第三步:定义doJumpToPage方法(通过此方法实现当前数据查询)

    function doJumpToPage(){
        //1.获取点击对象的class值
        var cls=$(this).prop("class");//Property
        //2.基于点击的对象执行pageCurrent值的修改
        //2.1获取pageCurrent,pageCount的当前值
        var pageCurrent=$("#pageId").data("pageCurrent");
        var 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();
    }
    

    ​ 第四步:修改分页查询方法:

    function doGetObjects(){
        //debugger;//断点调试
        //1.定义url和参数
        var url="log/doFindPageObjects"
        //? 请问data函数的含义是什么?(从指定元素上获取绑定的数据)
        //此数据会在何时进行绑定?(setPagination,doQueryObjects)
        var pageCurrent=$("#pageId").data("pageCurrent");
        //为什么要执行如下语句的判定,然后初始化pageCurrent的值为1
        //pageCurrent参数在没有赋值的情况下,默认初始值应该为1.
        if(!pageCurrent) pageCurrent=1;
        var params={"pageCurrent":pageCurrent};
        //2.发起异步请求
        //请问如下ajax请求的回调函数参数名可以是任意吗?可以,必须符合标识符的规范
        $.getJSON(url,params,function(result){
            //请问result是一个字符串还是json格式的js对象?对象
            doH
            andleResponseResult(result);
        });//特殊的ajax函数 }
    

列表页面信息查询实现

  • 业务描述及设计

    ​ 当用户点击日志列表的查询按钮时,基于用户输入的用户名进行有条件的分页查询,并将查询结果呈现在页面。

  • 关键代码设计与实现

    ​ 第一步:日志列表页面加载完成,在查询按钮上进行事件注册。

     $(".input-group-btn")
        	 .on("click",".btn-search",doQueryObjects);
    

    说明:on函数为jquery中的一个事件注册函数(推荐)

    ​ on函数语法:$("选择器").on("事件","子元素选择器"[,数据],事件处理函数)

    ​ 第二步:定义查询按钮对应的点击事件处理函数。

    //处理查询按钮事件
    function doQueryObjects(){
        //1.定义参数
        //数据查询时页码的初始位置也应该是第一页
        $("#pageId").data("pageCurrent",1);//设置当前页码值
        //2.定义请求url
        //3.发送异步请求,重用js代码,简化jS代码编写。
        doGetObjects();
    }
    

    ​ 第三步:在分页查询函数中追加name参数定义

     //开启ajax异步任务,按条件加载用户行为日志信息
        function doGetObjects(){
        	debugger
        	//1.定义请求参数
        	var pageCurrent=$("#pageId").data("pageCurrent");
        	if(!pageCurrent)pageCurrent=1;//pageCurrent没有值时,设置默认值为1
        	//1.1定义参数的方式1
        	//var params="pageCurrent=1";
        	//1.2定义参数的方式2
        	var params={"pageCurrent":pageCurrent};
        	//获取用户输入的用户名
        	var uname=$("#searchNameId").val();
        	//向json格式的js对象中动态添加key,value;
        	if(uname)params.username=uname;//这里的key需要与服务端接收参数时使用的名字相同。
        	console.log("params",params);
        	//2.定义请求url
        	var url="log/doFindPageObjects";
        	//3.发送异步请求处理响应结果
        	//$.getJSON(url,params,function(result){
        		//doHandleQueryResponseResult(result);
        	//});
        	//如上方法的简写形式
        	$.getJSON(url,params,doHandleQueryResponseResult);	
        }
    

    说明:getJSON函数内部的实现是如下形式,$.ajax底层实现

    //getJSON函数内部的实现是如下形式
    $.ajax({
        url:url,
        dataType:"json",
        data:params,
        success:doHandleQueryResponseResult
    });
    //$.ajax底层实现
    var xhr=new XMLHttpRequest();
    xhr.onreadystatechange=function(){
        if(xhr.readyState==4&&xhr.status==200){
            doHandleQueryResponseResult(JSON.parse(xhr.responseText));
        }
    };
    xhr.open("Get","log/doFindPageObjects?pageCurrent=1",true);
    xhr.send(null);
    

日志管理删除操作实现

数据架构分析

​ 当用户执行日志删除操作时,客户端与务端交互时的基本数据架构

1594709332420

删除业务时序分析

​ 客户端提交删除请求,服务端对象的工作时序分析

1594709352388

服务端关键业务及代码实现

Dao接口实现

  • 业务描述及设计实现

    ​ 数据层基于业务层提交的日志记录id,进行日志删除操作。

  • 关键代码设计及实现

    在SysLogDao中添加基于id执行日志删除的方法。

    /**
    * 基于多个记录id执行数据删除操作
    * @param ids 记录id(可变参数)
    * @return
    */
    int deleteObjects(Integer ...ids);// int deleteObjects(Integer[] ids)
    

Mapper文件实现

  • 业务描述及设计实现

    ​ 在SysLogDao接口对应的映射文件中添加用于执行删除业务的delete元素,此元素内部定义具体的SQL实现。

  • 关键代码设计及实现

    ​ 在SysLogMapper.xml文件添加delete元素

    <delete id="deleteObjects">
        delete from sys_logs
        <where>
            <if test="ids!=null and ids.length>0">
                id in
                <foreach collection="ids"
                         open="(" close=")" separator="," item="id">
                    #{id}
                </foreach>
            </if>
            or 1=2
        </where>
    </delete>
    

    ​ 从SQL执行性能角度分析,可以参考如下代码进行实现,使用or性能会更差(重点是forearch中or运算符的应用):

delete from sys_logs id in #{id} 1=2 ```

说明:这里的choose元素也为一种选择结构,when元素相当于if,otherwise相当于else的语法。

Service接口及实现类

  • 业务描述及设计实现

    ​ 在日志业务层定义用于执行删除业务的方法,首先通过方法参数接收控制层传递的多个记录的id,并对参数id进行校验。然后基于日志记录id执行删除业务实现。最后返回业务执行结果。

  • 关键代码设计及实现

    第一步:在SysLogService接口中,添加基于多个id进行日志删除的方法。

    /**
    *   基于记录id删除日志信息
    * @param ids
    * @return 删除的行数
    */
    int deleteObjects(Integer... ids);
    

    第二步:在SysLogServiceImpl实现类中添加删除业务的具体实现

    @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中添加用于执行删除业务的方法。

    @RequestMapping("doDeleteObjects")
    @ResponseBody
    public JsonResult doDeleteObjects(Integer… ids){
        sysLogService.deleteObjects(ids);
        return new JsonResult("delete ok");
    }
    

    第二步:启动tomcat进行访问测试,打开浏览器输入如下网址:

    http://localhost/log/doDeleteObjects?ids=20,21,22
    
    

客户端关键业务及代码实现

日志列表页面事件处理

  • 业务描述及设计实现

    ​ 用户在页面上首先选择要删除的元素,然后点击删除按钮,将用户选择的记录id异步提交到服务端,最后在服务端执行日志的删除动作。

  • 关键代码设计及实现

    第一步:页面加载完成以后,在删除按钮上进行点击事件注册。

    $(function(){
        //....
        $(".input-group-btn")
            .on("click",".btn-delete",doDeleteObjects);
        //...
    });
    

    第二步:定义删除操作对应的事件处理函数。

    //向服务端发送异步请求执行删除操作
    function doDeleteObjects(){
        //1.定义请求参数
        //1.1获取用户选中记录的id值并将其存储到数组
        var idArray=doGetCheckedIds();//[1,2,3,4,5]
        console.log("idArray",idArray);
        //1.2判定是否选中
        if(!idArray||idArray.length==0){
            alert("请先选择");
            return;
        }
        //1.3给出提示确定删除吗
        if(!confirm("确定删除吗"))return;
        //1.4构建请求参数
        var params={"ids":idArray.toString()};//{"ids":"1,2,3,4,5"}
        console.log("delete.params",params);
        //2.定义请求url
        var url="log/doDeleteObjects";
        //3.发送请求执行删除操作
        $.post(url,params,function(result){
            if(result.state==1){
                alert(result.message);
                //doGetObjects();//刷新,先这么写没有优化
                doRefreshAfterDeleteOK();//优化之后的写法
            }else{
                alert(result.message);
            }
        })
    }
    

    第三步:定义获取用户选中的记录id的函数。

    //获取选中记录的id值并将其存储到数组
    function doGetCheckedIds(){
        var idArray=[];//new Array();
        $("#tbodyId input:checkbox")
            .each(function(){//each函数表示迭代
            //$(this)代表input 类型为checkbox的对象
            if($(this).prop("checked")){//选中则布尔表达式为true
                idArray.push($(this).val())//将选中checkbox对象的值写入数组
            }
        });
        return idArray;
    }
    

    第四步:全选 Thead中全选元素的状态影响tbody中checkbox对象状态。

    一 注册全选事件

     //全选checkbox对象上的事件注册(一般与状态相关的事件会用change)
    $("#checkAll").change(doChangeTableBodyCheckboxState);
    

    二 定义处理函数

    //修改tbody中checkbox对象的状态
    function doChangeTableBodyCheckboxState(){
        //1.获取全选checkbox对象的checked属性值
        //var flag=$(this).prop("checked")
        var flag=$("#checkAll").prop("checked")
        //2.设置tbody中checkbox对象的checked属性的值
        $("#tbodyId input:checkbox").prop("checked",flag);
    }
    

    第五步: 反选 Tbody中checkbox的状态影响thead中全选元素的状态。(保证状态统一)

    一 注册反选事件

    //tbody中checkbox对象的事件注册
        	 $("#tbodyId")
        	 .on("change","input:checkbox",doChangeTableHeadCheckboxState)
    

    二 定义处理函数

    //基于tbody中checkbox对象的状态修改thead中checkbox对象状态
    function doChangeTableHeadCheckboxState(){
        //1.获取tbody中checkbox对象的checked属性的值并进行逻辑与操作。
        var flag=true;
        $("#tbodyId input:checkbox")
            .each(function(){
            flag=flag&&$(this).prop("checked");
        })
        //2.修改thead中checkbox对象的值。
        $("#checkAll").prop("checked",flag);
    }
    

    第六步:完善业务刷新方法,当在最后一页执行删除操作时,基于全选按钮状态及当前页码值,刷新页面。该方法写在 doDeleteObjects()方法中的处理响应位置.

    //删除成功以后的刷新
    function doRefreshAfterDeleteOK(){
        //页码值减减操作(回到上一页)的需要满足的条件
        //1)thead中checkbox的状态为全选
        var allChecked=$("#checkAll").prop("checked");
        //2)当前页码值是最后一页并且不是第一页
        var pageCurrent=$("#pageId").data("pageCurrent");
        var pageCount=$("#pageId").data("pageCount");
        if(allChecked&&pageCurrent==pageCount&&pageCurrent>1){
            pageCurrent--;
            $("#pageId").data("pageCurrent",pageCurrent);
        }
        doGetObjects();
    }
    

    第七步:优化全选按钮(点击上一页下一页刷新全选按钮)

    思路:在查询下一页的时候,在处理响应数据的时候,将全选框刷新

     $("#checkAll").prop("checked",false);
    

    整体处理代码

    //处理查询结果
    function doHandleQueryResponseResult(result){//result为json格式的js对象
        if(result.state==1){//state==1表示是正常的响应数据
            //将result.data.records呈现在log_list.html的tbody位置
            doSetTableBodyRows(result.data.records);
            //将result.data中的分页信息呈现在page.html指定位置
            doSetPagination(result.data);//result.data对应服务端业务层的pageObject
            //更新thead中checkbox的值(目的在处理分页查询事件时假如有全选状态也随之更新)
            $("#checkAll").prop("checked",false);
        }else{
            //alert(result.message);
            doHandleQueryError(result.message);
        }
    }
    

    第八步:当数据就剩余一页时,删除完当前页数据,再次刷新页面的时候应该在tbody中给予对应的提示信息,并且要刷新page页面的一系列页码数据

    //处理查询失败的情况
    function doHandleQueryError(msg){
        //设置tbody中的内容
        $("#tbodyId").html(`<tr><td colspan=7>${msg}</td></tr>`);
        //设置全选状态
        $("#checkAll").prop("checked",false);
        //1.初始化总记录数
        $(".rowCount").html("总记录数(0)");
        //2.初始化总页数
        $(".pageCount").html("总页数(0)");
        //3.初始化当前页
        //基于Jquery框架实现
        $(".pageCurrent").html("当前页(1)");
    }
    

日志管理数据添加实现

服务端关键业务及代码实现

Dao接口实现

  • 业务描述与设计实现

    ​ 数据层基于业务层的持久化请求,将业务层提交的用户行为日志信息写入到数据库。

  • 关键代码设计与实现

    ​ 在SysLogDao接口中添加用于实现日志信息持久化的方法。

    int insertObject(SysLog sysLog);
    

Mapper映射文件

  • 业务描述与设计实现

    ​ 基于SysLogDao中方法的定义,编写用于数据持久化的SQL元素。

  • 关键代码设计与实现

    ​ 在SysLogMapper.xml中添加insertObject元素,用于向日志表写入用户行为日志。

    <insert id="insertObject">
        insert into sys_logs
        (username,operation,method,params,time,ip,createdTime)
        values
        (#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
    </insert>
    

Service接口及实现类

  • 业务描述与设计实现

    ​ 将日志切面中抓取到的用户行为日志信息,通过业务层对象方法持久化到数据库。

  • 关键代码设计与实现

    第一步:在SysLogService接口中,添加保存日志信息的方法。

    //新增日志信息
    void saveObject(SysLog sysLog);
    

    第二步:在SysLogServiceImpl类中添加,保存日志的方法实现。

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    //Propagation.REQUIRES_NEW 表示写日志的操作始终会运行在一个独立的事务中
    @Async //使用此注解的描述的方法会运行在由spring框架提供的一个线程中
    @Override
    public void saveObject(SysLog sysLog) {
        sysLogDao.insertObject(sysLog);
    
    }
    

日志切面Aspect实现

  • 业务描述与设计实现

    ​ 在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业务层对象对日志日志信息做进一步处理。

  • 关键代码设计与实现

    定义日志切面类对象,通过环绕通知处理日志记录操作。

    package com.cy.pj.common.aspect;
    /**
     * @Aspect 注解描述的类型为Spring AOP中的切面对象类型。此对象中可以封装:
     * 1)切入点(定义在哪些目标对象的哪些方法上进行功能扩展)
     * 2)通知(封装功能扩展的业务逻辑)
     */
    //@Slf4j
    //@Order(1) //优先级比较高
    @Aspect
    @Component
    public class SysLogAspect {
        //假如使用了lombok注解@Slf4j描述类,默认会在类的内部生成如下语句。
        private static final Logger log = 
            LoggerFactory.getLogger(SysLogAspect.class); 
        /**
            * PointCut注解用于定义切入点,具体方式可以基于特定表达式进行实现。例如
            * 1)bean为一种切入点表达式类型
            * 2)sysUserServiceImpl 为spring容器中的一个bean的名字
            * 	这里的涵义是当sysUserServiceImpl对象中的任意方法执行时,都由本切面
            * 	对象的通知方法做功能增强。
            */
        //@Pointcut("bean(sysUserServiceImpl)")
        //注解方式的切入点表达式
        @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
        public void doLogPointCut() {}//此方法中不需要写任何代码
        /**由@Around注解描述的方法为一个环绕通知方法,我们可以在此方法内部
    	          * 手动调用目标方法(通过连接点对象ProceedingJoinPoint的proceed方法进行调用)
    	          * 环绕通知:此环绕通知使用的切入点为bean(sysUserServiceImpl)
    	          * 环绕通知特点:
    	     	1)编写:
    	           a)方法的返回值为Object.
    	           b)方法参数为ProceedingJoinPoint类型.
    	           c)方法抛出异常为throwable.
    	        2)应用:
    	           a)目标方法执行之前或之后都可以进行功能拓展
    	           b)相对于其它通知优先级最高。
    	     @param jp 为一个连接对象(封装了正在要执行的目标方法信息)
    	     @return 目标方法的执行结果
    	     */
        @Around(value="doLogPointCut()")
        public Object around(ProceedingJoinPoint jp)throws Throwable{
            long start=System.currentTimeMillis();
            log.info("method start {}",start);
            try {
                Object result=jp.proceed();//本类@Before,下一个切面,目标方法
                long end=System.currentTimeMillis();
                log.info("method end {}",end);
                //将用户的正常的行为信息写入到数据库
                saveUserLog(jp,(end-start));
                return result;
            }catch(Throwable e) {
                log.error("method error {},error msg is {}",
                    System.currentTimeMillis(),e.getMessage());
                throw e;
            }
        }
        @Autowired
        private SysLogService sysLogService;
        private void saveUserLog(ProceedingJoinPoint jp,long time)throws Exception {
            //1.获取用户行为日志信息
            //获取目标对象(要执行的那个目标业务对象)类
            Class<?> targetCls=jp.getTarget().getClass();
            //获取方法签名对象(此对象中封装了要执行的目标方法信息)
            MethodSignature ms=(MethodSignature) jp.getSignature();
            //获取目标方法对象,基于此对象获取方法上的RequiredLog注解,进而取到操作名
            //Method method=ms.getMethod();//假如是jdk代码默认取到的是接口中的方法对象
            Method targetMethod=
                targetCls.getMethod(ms.getName(),ms.getParameterTypes());
            RequiredLog required=targetMethod.getAnnotation(RequiredLog.class);
            //获取操作名
            String operation="operation";
            if(required!=null) {//注解方式的切入点无须做此判断
                operation=required.value();
            }
            //获取目标方法类全名
            String targetMethodName=targetCls.getName()+"."+ms.getName();
            //2.构建用户行为日志对象封装用户行为信息
            SysLog log=new SysLog();
            log.setIp(IPUtils.getIpAddr());
            log.setUsername(ShiroUtil.getUsername());//登陆用户
            log.setOperation(operation);//
            log.setMethod(targetMethodName);//类全名.方法名
            //log.setParams(Arrays.toString(jp.getArgs()));//将参数对象转换为普通串
            //将参数对象尽量转换为json格式字符串。
            log.setParams(new ObjectMapper().writeValueAsString(jp.getArgs()));
            log.setTime(time);
            log.setCreatedTime(new java.util.Date());
            //3.用户行为日志写入到数据库
            sysLogService.saveObject(log);
    
            //		   new Thread() { //高并发环境下可能很快就会出现内存溢出
            //			   public void run() {
            //				   sysLogService.saveObject(log);
            //			   };
            //		   }.start();
        }
    }
    

IPUtils 工具类

package com.cy.pj.common.util;
public class IPUtils {
    private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
    public static String getIpAddr() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = null;
        try {
            ip = request.getHeader("x-forwarded-for");
            if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        } catch (Exception e) {
            logger.error("IPUtils ERROR ", e);
        }
        return ip;
    }
}

ShiroUtils工具类

package com.cy.pj.common.util;

import org.apache.shiro.SecurityUtils;

import com.cy.pj.sys.pojo.SysUser;

public class ShiroUtil {

    public static String getUsername() {
        return getUser().getUsername();
    }
    public static SysUser getUser() {
        //从session中获取登陆用户
        return (SysUser)SecurityUtils.getSubject().getPrincipal();
    }
}

自定义日志注解

package com.cy.pj.common.annotation;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {

	  String value() default "operation";
}

AssertUtil工具类

package com.cy.pj.common.util;
/**
 * 通过此工具类封装对参数,执行过程,结果等业务的校验。
 *
 */
public class AssertUtil {
	 /**验证参数的有效性*/
	 public static void isArgumentValid(boolean statement,String msg) {
		 if(statement)
			 throw new IllegalArgumentException(msg);
	 }
	 public static void isResultValid(boolean statement,String msg) {
		 if(statement)
			 throw new ServiceException(msg);
	 }
}

至此日志模块完毕

posted on 2020-07-12 19:21  liqiangbk  阅读(566)  评论(0编辑  收藏  举报

导航