# 行为日志

本小节建立在你已经阅读和熟悉了单元测试小节的基础上。

行为日志与日志系统不是一种东西,日志系统是对于程序运行的日志打点记录,而行为日志是对用户行为的记录。

# 搜索图书

在正式的行为日志介绍前,我们还是得先完善一下我们的图书应用。

在 BookController 中,我们新增一个searchBook方法,并以search路径对外暴露 API,考虑到 MySQL 的模糊搜索,因此我们给前端传入的 keyword 两边都新增%

public class BookController {
    @GetMapping("/search")
    public List<BookDO> searchBook(@RequestParam(value = "q", required = false, defaultValue = "") String q) {
        List<BookDO> books = bookService.getBookByKeyword("%" + q + "%");
        return books;
    }
}

在 BookService 中,我们也添加上对应的getBookByKeyword方法,并 在BookServiceImpl中实现。

public interface BookService {
    List<BookDO> getBookByKeyword(String s);
}
public class BookServiceImpl implements BookService {
    @Override
    public List<BookDO> getBookByKeyword(String q) {
        List<BookDO> books = bookMapper.selectByTitleLikeKeyword(q);
        return books;
    }
}

BookServiceImpl 随之调用BookMapper#selectByTitleLikeKeyword方法来搜索数据库中 的图书。然后 BookMapper 其实没有selectByTitleLikeKeyword方法,我们需要自己来实 现。如下:

package io.github.talelin.latticy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import io.github.talelin.latticy.model.BookDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface BookMapper extends BaseMapper<BookDO> {
    List<BookDO> selectByTitleLikeKeyword(@Param("q") String q);
}

这样还不够,因为我们这是定义了代理接口,但并未书写相应的 SQL,因此我们还完 善BookMapper.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="io.github.talelin.latticy.mapper.BookMapper">

    <resultMap id="BaseResultMap" type="io.github.talelin.latticy.model.BookDO">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="title" jdbcType="VARCHAR" property="title"/>
        <result column="author" jdbcType="VARCHAR" property="author"/>
        <result column="summary" jdbcType="VARCHAR" property="summary"/>
        <result column="image" jdbcType="VARCHAR" property="image"/>
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
        <result column="delete_time" jdbcType="TIMESTAMP" property="deleteTime"/>
    </resultMap>

    <select id="selectByTitleLikeKeyword" resultMap="BaseResultMap">
        SELECT *
        FROM book b
        WHERE
        b.title LIKE #{q}
        AND
        b.delete_time IS NULL
    </select>
</mapper>

这样,我们从分别从控制层、业务层、DAO 层完善了图书应用,接下来我们测试一下。

curl http://localhost:5000/v1/book/search\?q\=c

结果如下:

[
  {
    "id": 2,
    "title": "C程序设计语言",
    "author": "(美)Brian W. Kernighan",
    "summary": "在计算机发展的历史上,没有哪一种程序设计语言像C语言这样应用广泛。本书原著即为C语言的设计者之一Dennis M.Ritchie和著名计算机科学家Brian W.Kernighan合著的一本介绍C语言的权威经典著作。",
    "image": "https://img3.doubanio.com/lpic/s1106934.jpg"
  }
]

很好,我们以c作为关键字,顺利地从数据库中搜索出了C程序设计语言这本书。

我们再来尝试一个关键词java

curl http://localhost:5000/v1/book/search\?q\=java
[]

不知道你是否注意到了,在前面的小节中,如果查询图书无果,我们会抛出一个NotFoundException异常,但是在这里我们却未抛出异常,而是不作为,即使结果为[]

这是 lin-cms 的一种标准,或者说一种妥协,如果返回结果为多个(即列表 List 或者分页 Page),那么即使最后结果是空的,我们也不抛出异常,而是给出一个空的结果。

# 记录日志

行为日志的使用十分简单,我们再次优化一下 BookController 代码。

public class BookController {
    @GetMapping("/search")
    @Logger(template = "{user.nickname}搜索的一本书")
    public List<BookDO> searchBook(@RequestParam(value = "q", required = false, defaultValue = "") String q) {
        List<BookDO> books = bookService.getBookByKeyword("%" + q + "%");
        return books;
    }
}

我们新增了一行代 码@Logger(template = "{user.nickname}搜索的一本书")
Logger是 lin-cms 提供的注解,有了它你就可以方便地使用行为日志。

注意在 Loggertemplate参数中有{}占位符,它会帮助我们做一些很酷的事,你可以猜测一下,我们将在后面揭晓。

我们再次运行程序,并且请求该 API。

curl http://localhost:5000/v1/book/search\?q\=c

很不幸,没有任何日志被数据库记录。Logger 的使用依赖于权限系统,因为记录日志是针对用户而言的,如果连用户都没有,那记录的日志也没有意义。

因此我们还得为 searchBook 添加上相应的 @PermissionMeta 和 @LoginRequired 注解。如下:

public class BookController {
    @GetMapping("/search")
    @LoginRequired
    @PermissionMeta(permission = "搜索图书", module = "图书", mount = true)
    @Logger(template = "{user.nickname}搜索的一本书")
    public List<BookDO> searchBook(@RequestParam(value = "q", required = false, defaultValue = "") String q) {
        List<BookDO> books = bookService.getBookByKeyword("%" + q + "%");
        return books;
    }
}

而后,我们运行并请求,但是我们得先登录获得令牌,并将其加入到请求头中,可参考上一小节

curl -XPOST -H 'Content-Type:application/json' -d '{"username":"root","password":"123456"}'  localhost:5000/cms/user/login
curl -H 'Authorization:Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6MSwic2NvcGUiOiJsaW4iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTgwMjg5NDQ4fQ._vWFoX04EPT4ubVVtASbBx7rAeGkVZfO52KhOry6z94' http://localhost:5000/v1/book/search\?q\=c

结果就不贴上了,我们又顺利得到了图书数据,并且在数据库lin-log表中,我们可以看 到新增的行为日志:

+----+------------------+---------+----------+-------------+--------+-----------------+--------------+
| id | message          | user_id | username | status_code | method | path            | permission   |
+----+------------------+---------+----------+-------------+--------+-----------------+--------------+
| 2  | 范闲搜索的一本书    | 1       | 范闲     | 200         | GET     | /v1/book/search| 搜索图书      |
+----+------------------+---------+----------+-------------+--------+-----------------+--------------+

我们截取了大部分数据,细致的你一定能够看懂大部分数据。有一处有些奇怪,message 字段范闲搜索的一本书究竟是怎么来的呢?

在 Logger 的 template 中我们明明填入的是{user.nickname}搜索的一本书,没 错{}占位符会被默认解析。

其中的user是当前登录的用户,如果用户未登录,那么你便不能使用{}占位符,因此无法知道具体的数据,而user对应的数据模型其实 是io.github.talelin.latticy.model.UserDO,它的定义如下:

public class UserDO implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户名,唯一
     */
    private String username;

    /**
     * 用户昵称
     */
    private String nickname;

    /**
     * 头像url
     */
    private String avatar;

    /**
     * 邮箱
     */
    private String email;

    @JsonIgnore
    private Date createTime;

    @JsonIgnore
    private Date updateTime;

    @JsonIgnore
    @TableLogic
    private Date deleteTime;
}

意味着你也可以在{}中使用其它的属性,利于{user.email}

# 已登录用户

在上面我们谈到,用户通过令牌携带信息,lin-cms 解析令牌后便可知道当前登录的用户信息。在 lin-cms 中,我们可以很便捷的拿到登录用户,如下(伪代码):

import io.github.talelin.latticy.common.LocalUser;

public UserPermissionsVO getPermissions() {
    UserDO user = LocalUser.getLocalUser();
    // ***
}

注意:如果你需要通过LocalUser来获得当前登录用户,那么请一定要保证有登录的用户 ,所以应该如下两个条件:

  1. 请求必须携带有效的令牌,令牌损坏或令牌过期均不行。
  2. 你的接口方法,如searchBook必须被LoginRequired修饰,或者GroupRequiredAdminRequired修饰。

# 总结

在本小节中,我们带你使用了 lin-cms 的行为日志,详细的说明了登录用户的作用与有效 范围。

最后,我们附上一些参考资料供你阅读。

好用的 orm 框架mybatis

好用的 mybatis 增强框架mybatis-plus