Public account: MarkerHub (pay attention to get more project resources)

Eblog codebase: github.com/markerhub/e…

Eblog project video: www.bilibili.com/video/BV1ri…


Development Document Catalog:

(Eblog) 1. Set up the project architecture and initialized the home page

(EBLOG) 2. Integrate Redis and project elegant exception handling and return result encapsulation

(eblog) 3, using Redis zset ordered set to achieve a hot feature of the week

(EBLOG) 4, customize Freemaker label to achieve blog homepage data filling

(Eblog) 5, blog classification filling, login registration logic

(EBLOG) 6. Set up blog publishing collection and user center

(eblog) 7, message asynchronous notification, details adjustment

8. Blog search engine development and background selection

(Eblog) 9. Instant group chat development, chat history, etc


Front and back end separation project vueblog please click here: super detailed! 4 hours to develop a SpringBoot+ Vue front and back separated blog project!!


This time we will improve the content of the home page, such as our home page article list, home navigation classification, classification list, article details.

At the same time, I wrote a lot of bugs in the last homework, and then I secretly changed a lot of them, they are relatively detailed, I may not write all of them, if you don’t know what I changed, there are two ways:

  • 1. Check git’s submission record. Click on the file to see the comparison

  • 2, run my project and your project, link the unified database, judge whether the content display and function of the page is consistent, inconsistent means I have secretly changed some unknown bugs.

1. Home page content filling

The list of pages

Table pagination here refers to the content of the home page list, you can see that the content of each line of the list is actually the same as the top list, so the original SQL we can apply again. The list of navigation categories that we click on is also consistent. If the content is consistent, we can think of the first front end of the list we can separate out as a template, so that everything can only be changed once, can control everything.

Then there are two ways to handle the backend:

  • 1. Use our freemarker tag

  • 2. Use the controller to transfer data to the front end

We’ve already learned how to tag, so let’s submit data to the front end again in the Controller.

First look at the controller on the home page

@RequestMapping({""."/"."/index"})
public String index () {
    IPage results = postService.paging(getPage(), null, null, null, null, "created");
    req.setAttribute("pageData", results);
    return "index";
}

Copy the code

Postservice.paging is what we wrote earlier, but with a few more parameters, I secretly changed it. Let me show you the latest version

@Override
@Cacheable(cacheNames = "cache_post", key = "'page_' + #page.current + '_' + #page.size " +
        "+ '_query_' +#userId + '_' + #categoryId + '_' + #level + '_' + #recommend + '_' + #order")
public IPage paging(Page page, Long userId, Long categoryId, Integer level, Boolean recommend, String order) {
    if(level == null) level = -1; QueryWrapper wrapper = new QueryWrapper<Post>() .eq(userId ! = null,"user_id", userId) .eq(categoryId ! = null && categoryId ! = 0,"category_id", categoryId)
            .gt(level > 0, "level", 0)
            .eq(level == 0, "level", 0) .eq(recommend ! = null,"recommend", recommend)
            .orderByDesc(order);
    IPage<PostVo> pageData = postMapper.selectPosts(page, wrapper);
    return pageData;
}

Copy the code

In fact, a few more parameters are added to cope with more scenarios. Going back to the index method, there’s a getPage() that I wrote in the BaseController, which is a wrapper to get paging data, get the front-end paging information, and wrap it into a POGE object. Then give the default values for the parameters.

public Page getPage() {
    int pn =  ServletRequestUtils.getIntParameter(req, "pn", 1);
    int size =  ServletRequestUtils.getIntParameter(req, "size", 10);
    Page page = new Page(pn, size);
    return page;
}

Copy the code

And then you can see in index, I passed a pageData object to the front end, so let’s look at the front end.

<ul class="fly-list">
    <#list pageData.records as post>
        <@listing post></@listing>
    </#list>
</ul>

Copy the code

Look for the content section in the middle and write it like this because I’m encapsulating the record list as a macro.

That’s what it is

<#macro listing post>
<li>
    <a href="${base}/user/${post.authorId}" class="fly-avatar">
        <img src="${post.authorAvatar}" alt="${post.authorName}">
    </a>
    <h2>
        <a class="layui-badge">${post.categoryName}</a>
        <a href="${base}/post/${post.id}">${post.title}</a>
    </h2>
    <div class="fly-list-info">
        <a href="${base}/user/${post.authorId}" link>
            <cite>${post.authorName}</cite>
            <i class="layui-badge fly-badge-vip">VIP${post.authorVip}</i>
        </a>
        <span>${post.created? string('yyyy-MM-dd')}</span>
        <span class="fly-list-nums">
                <i class="iconfont icon-pinglun1" title="Answer"></i> ${post.commentCount}
              </span>
    </div>
    <div class="fly-list-badge">
         < 
      
         < 
      
    </div>
</li>
</#macro>

Copy the code

Freemarker “macro” is a freemarker tag. Forget ~

<#macro listing post>... 
      
Copy the code

Listing defines a macro term called listing. The argument is post, and the contents of the tag are the contents of the macro. Where you need to call this macro you just use the tag, so you see what I just did.

Okay, so the list is looping out, but there’s one problem we haven’t solved, and that’s paging, and in the front end we need a paging navigation that gives us the number of pages to click on. In phase 2 we used a different plugin, but this time we used layui’s pagination plugin directly, which was quite simple. Because of this page or pages, many page need to use, so I make the content of the page again a macro, then consider layui paging write www.layui.com/demo/laypag…

<#-- Pagination template -->
<#macro page data>
<div id="laypage-main"></div>
<script type="application/javascript">
    $(function () {
        layui.use(['laypage'.'layer'].function(){ var laypage = layui.laypage ,layer = layui.layer; Laypage. Render ({elem:'laypage-main'
                ,count: ${data.total}// Total number of data,curr:${data.current}
                ,limit: ${data.size}
                ,jump: function(obj, first){console.log(obj) // This is not executed for the first timeif(! first){ var url = window.location.href; location.href ="? pn="+ obj.curr; }}}); }); }); </script> </#macro>

Copy the code

The js above is relatively simple, just call a layui layPage. render can be the page number to render out, we need to call the code where

<div style="text-align: center">
    <@page pageData></@page>
</div>

Copy the code

PageData is the data transmitted by controller, and the rendering effect is as follows:

It’s perfect. I’m a genius. Everyone loves me

Navigation classification

Next we will improve the navigation classification information, this is quite simple, we have a table dedicated to store classification information, just need to get out the list (ID, name) does not need to associate other tables, then Mybatis Plus can help me directly, do not need me to write service, Where should I send the data to? Home index? Navigation categories are used everywhere and are not appropriate, so defining a Freemarker tag is a good idea.

But instead of using tags here, I put the data in the global application Context, so that when we initialize the project we load the data, a little bit like we initialized this week, so we add our code directly to that startup class

Ok, 2 lines of code, never write too much, some of you might have a status control category display, you can make a condition.

CurrentCategoryId is used to display the currently selected category, default is 0 (home page)

Then the front end, which shows the data:

<li class="${(0 == currentCategoryId)? string('layui-hide-xs layui-this', '')}">
    <a href="/"</a> </li> <#list categorys as category>
    <li class="${(category.id == currentCategoryId)? string('layui-hide-xs layui-this', '')}">
        <a href="${base}/category/${category.id}">${category.name}</a>
    </li>
</#list>

Copy the code

Emm ~, note the binary notation of freemarker, otherwise simple ~

Classification details

Click on the navigation classification, we jump to http://localhost:8080/category/1, the content again and our list is a bit like home page, com. Example. Controller. The PostController

@RequestMapping("/category/{id:\\d*}")
public String category(@PathVariable Long id) {
    Page page = getPage();
    IPage<PostVo> pageData = postService.paging(page, null, id, null, null, "created");
    req.setAttribute("pageData", pageData);
    req.setAttribute("currentCategoryId", id);
    return "post/category";
}

Copy the code

CurrentCategoryId is to echo the column I am currently selecting.

  • templates/post/category.ftl
<ul class="fly-list">
  <#list pageData.records as post>
    <@listing post></@listing>
  </#list></ul> <! -- <div class="fly-none"<div style= "box-sizing: border-box! Important; word-wrap: break-word! Important;"text-align: center">
  <@page pageData></@page>
</div>

Copy the code

Details on the blog

Ok, now let’s look at the blog details. Click on the list to go to the page that shows the blog content and the comment list.

  • com.example.controller.PostController
@RequestMapping("/post/{id:\\d*}") public String view(@PathVariable Long id) { QueryWrapper wrapper = new QueryWrapper<Post>() .eq(id ! = null,"p.id", id);
    PostVo vo = postService.selectOne(wrapper);
    IPage commentPage = commentService.paging(getPage(), null, id, "id");
    req.setAttribute("post", vo);
    req.setAttribute("pageData", commentPage);
    return "post/view";
}

Copy the code

I wrote two service methods above

  • postService.selectOne

The SQL element of selectOne’s method is the same as that of the original selectPosts, except that it returns a page, a bean, and no page object.

<select id="selectOne" resultType="com.example.vo.PostVo">
    select p.*
    , c.id as categoryId, c.name as categoryName
    , u.id as authorId, u.username as authorName, u.avatar as authorAvatar, u.vip_level as authorVip
    from post p
    left join user u on p.user_id = u.id
    left join category c on p.category_id = c.id
    ${ew.customSqlSegment}

Copy the code

commentService.paging

For this method, I wrote commentVo for transferring data, adding associated information like user names, etc

<select id="selectComments" resultType="com.example.vo.CommentVo">
    select c.*
    , u.id as authorId, u.username as authorName, u.avatar as authorAvatar, u.vip_level as authorVip
    from comment c
    left join user u on c.user_id = u.id
    ${ew.customSqlSegment}
</select>

Copy the code

The front end is simple, just list to loop through the data

<#list pageData.records as comment>. </#list><! <div style= "box-sizing: border-box! Important"text-align: center">
    <@page pageData></@page>
</div>

Copy the code

See our code, there is no need to post here. Well, the data show here ~ first

2. User status

Above, we have completed the data display. Data editing requires the permission of the login user, so before editing, we will first do the login authentication of the user. Here, we will use Shiro framework to complete.

For the login module, let’s first sort out the logic, first copy the login registration page, then change it to a template form (header and tail, sidebar, etc.), then integrate shiro framework, write the login registration interface, Login -> Realm -> Write login registration logic -> shiro TAB of the page -> Related configuration of distributed Session, then

The login logic

  • com.example.controller.IndexController
@GetMapping("/login")
public String login() {
    return "auth/login";
}
@ResponseBody
@PostMapping("/login")
public Result doLogin(String email, String password) {
    if(StringUtils.isEmpty(email) || StringUtils.isEmpty(password)) {
        return Result.fail("User name or password cannot be empty!"); } AuthenticationToken token = new UsernamePasswordToken(email, SecureUtil.md5(password)); Realm securityutils.getSubject ().login(token); }catch (AuthenticationException e) {if (e instanceof UnknownAccountException) {
            return Result.fail("User does not exist");
        } else if (e instanceof LockedAccountException) {
            return Result.fail("User is disabled");
        } else if (e instanceof IncorrectCredentialsException) {
            return Result.fail("Password error");
        } else {
            return Result.fail("User authentication failed"); }}return Result.succ("Login successful", null, "/");
}
@GetMapping("/register")
public String register() {
    return "auth/register";
}
@ResponseBody
@PostMapping("/register")
public Result doRegister(User user, String captcha, String repass) {
    String kaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute(KAPTCHA_SESSION_KEY);
    if(! kaptcha.equalsIgnoreCase(captcha)) {return Result.fail("Verification code is not correct");
    }
    if(repass == null || ! repass.equals(user.getPassword())) {return Result.fail("Inconsistent passwords entered twice.");
    }
    Result result = userService.register(user);
    result.setAction("/login"); // Jump to the page after successful registrationreturn result;
}
@GetMapping("/logout")
public String logout() throws IOException {
    SecurityUtils.getSubject().logout();
    return "redirect:/";
}

Copy the code

The first is to jump to login, and then we submit the form data through the asynchronous POST method. The main logic of login is very simple, mainly one line of code:

SecurityUtils.getSubject().login(token); Login will then delegate to a Realm to authenticate the logon logic.doGetAuthenticationInfo) @slf4j @Component Public Class AccountRealm extends AuthorizingRealm {@autoWired UserService userService; @Override protected AuthorizationInfodoGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken; // note that token.getUsername() is an email!! AccountProfile profile = userService.login(token.getUsername(), String.valueOf(token.getPassword())); log.info("----------------> Enter the authentication step");
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(profile, token.getCredentials(), getName());
        returninfo; }}Copy the code

DoGetAuthenticationInfo is our authentication method, and authenticationToken is our passed UsernamePasswordToken, which contains the email address and password. Userservice. login verifies that the account is valid, throws an exception, and returns the encapsulated AccountProfile

@Override
public AccountProfile login(String username, String password) {
    log.info("------------> Enter user login judgment and obtain user information steps");
    User user = this.getOne(new QueryWrapper<User>().eq("email", username));
    if(user == null) {
        throw new UnknownAccountException("Account does not exist");
    }
    if(! user.getPassword().equals(password)) { throw new IncorrectCredentialsException("Password error"); } // Update the last login time user.setDATE (new Date()); this.updateById(user); AccountProfile profile = new AccountProfile(); BeanUtil.copyProperties(user, profile);return profile;
}

Copy the code

Ok, login logic has been sorted out, we will do the page later, and then do the registration logic.

Registration logic

The registration process is designed to be a captcha plug-in. Here we use Kaptcha, Google’s captcha generator.

To consolidate, import the JAR package first

<! --> <dependency> <groupId>com.github. Axet </groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency>Copy the code

Then configure the captcha image generation rules :(border, color, font size, length, height)

@Configuration
public class WebMvcConfig {
    @Bean
    public DefaultKaptcha producer () {
        Properties propertis = new Properties();
        propertis.put("kaptcha.border"."no");
        propertis.put("kaptcha.image.height"."38");
        propertis.put("kaptcha.image.width"."150");
        propertis.put("kaptcha.textproducer.font.color"."black");
        propertis.put("kaptcha.textproducer.font.size"."32");
        Config config = new Config(propertis);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        returndefaultKaptcha; }}Copy the code

Ok, now that we have the plug-in integrated, let’s provide an access interface to generate captcha images first to inject the plug-in

@Autowired private Producer producer; Then com. Example. Controller. The IndexController @ GetMapping ("/capthca.jpg")
public void captcha(HttpServletResponse response) throws IOException {
    response.setHeader("Cache-Control"."no-store, no-cache");
    response.setContentType("image/jpeg"); String text = producer.createText(); BufferedImage image = producer.createImage(text); Securityutils.getsubject ().getSession().setAttribute(KAPTCHA_SESSION_KEY, text); ServletOutputStream outputStream = response.getOutputStream(); ImageIO.write(image,"jpg", outputStream);
}

Copy the code

So access this interface to get captCHA image stream, page:

<img
 
id
=
"capthca"
 
src
=
"/capthca.jpg"
>
Copy the code

Therefore, the stream is connected to the front end and back end, and the correctness of the verification code needs to be verified at the back end. Therefore, when generating the verification code, we need to store the verification code in the session first, register the interface, and then obtain the verification code from the session and compare whether it is correct.

@ResponseBody
@PostMapping("/register")
public Result doRegister(User user, String captcha, String repass) {
    String kaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute(KAPTCHA_SESSION_KEY);
    if(! kaptcha.equalsIgnoreCase(captcha)) {return Result.fail("Verification code is not correct"); }...return result;
}

Copy the code

So the first thing to do when registering an interface is to verify that the captcha is correct.

Result
 result 
=
 userService
.
register
(
user
);
Copy the code

Let’s look at the logic

@Override
public Result register(User user) {
    if(StringUtils.isEmpty(user.getEmail()) || StringUtils.isEmpty(user.getPassword())
            || StringUtils.isEmpty(user.getUsername())) {
        return Result.fail("Required fields cannot be empty.");
    }
    User po = this.getOne(new QueryWrapper<User>().eq("email", user.getEmail()));
    if(po ! = null) {return Result.fail("Mailbox registered");
    }
    String passMd5 = SecureUtil.md5(user.getPassword());
    po = new User();
    po.setEmail(user.getEmail());
    po.setPassword(passMd5);
    po.setCreated(new Date());
    po.setUsername(user.getUsername());
    po.setAvatar("/res/images/avatar/default.png");
    po.setPoint(0);
    return this.save(po)? Result.succ("") : Result.fail("Registration failed");
}

Copy the code

In fact, it is to check whether the user has registered, and insert a record if not registered. I only used MD5 encryption for the password here. If you think the password encryption is not strict enough, you can add salt or change other encryption methods. Ok, now that we’ve done the registration logic on the back end, let’s look at the front end. Layui has already wrapped the form submission logic for us

So the return value should have properties such as action, status, MSG, etc. Therefore, the Result class we encapsulated before needs to be modified. Previously, we only had code, data and MSG for Result, but now we add an action and status.

  • com.example.common.lang.Result
@Data public class Result implements Serializable { private Integer code; private Integer status; private String msg; private Object data; private String action; . }Copy the code

Above is our latest return to the wrapper class, there are some specific encapsulation method to see the specific code ha. So the return value of the registration method ends up like this

Result result = userService.register(user);
result.setAction("/login"); // Jump to the page after successful registrationreturn result;

Copy the code

Action represents the link to jump to after the form has been successfully processed.

As you can see above, I clicked the “confirm” button and went to the login page, which is what I set in this action.

Now that we’ve done the business-level logic, let’s look at the page side. The original LayUI background interface has already done the page logic for us. There is no logic to this. After the fields are mapped to the form form, we know that js already has a submit button that monitors all forms, which triggers the following method:

  • static/res/mods/index.js
// Submit form.on('submit(*)'.function(data){
  var action = $(data.form).attr('action'), button = $(data.elem);
  fly.json(action, data.field, function(res){
    var end = function() {if(res.action){
        location.href = res.action;
      } else {
        fly.form[action||button.attr('key')](data.field, data.form); }};if(res.status == 0){
      button.attr('alert')? layer.alert(res.msg, { icon: 1, time: 10*1000, end: end }) : end(); }; });return false;
});

Copy the code

So, in the registration page, we don’t need to write js, we can give the image captcha a click event, because sometimes it is not clear can click to change another

  • templates/auth/register.ftl
<script>
    $(function() {$("#capthca").click(function () {
            this.src="/capthca.jpg";
        });
    });
</script>

Copy the code

We don’t need js in the login page. Done! So that’s our registration logic.

Shiro page TAB

Let’s use some of Shiro’s tags on the front so that we can control button permissions, user login status, user information, and so on on the page. Since our page uses Freemarker, we use a Freemarker-Shiro JAR package

< the dependency > < groupId > net. Mingsoft < / groupId > < artifactId > shiro freemarker - tags < / artifactId > < version > 0.1 < / version > </dependency>Copy the code

Second, shiro’s tag needs to be injected into freemarker’s tag configuration:

  • com.example.config.FreemarkerConfig

Third, we display the user’s login information in the upper right corner of the page

Dimly remember that our head content is placed on

  • templates/inc/header.ftl

So how do Shiro’s tags work? The specific usage, we look at this article popular science

  • www.cnblogs.com/Jimc/p/1003…

The < @shiro.guest>

The < @shiro.user>

*<@shiro.principal property=“username” */>

So once we learn Shiro’s tags, then we can use them

<ul class="layui-nav fly-nav-user"> <@shiro.guest> <! --> <li class="layui-nav-item">
        <a class="iconfont icon-touxiang layui-hide-xs" href="/login"></a>
    </li>
    <li class="layui-nav-item">
        <a href="/login"> Login </a> </li> <li class="layui-nav-item">
        <a href="/register"> Register </a> </li> </ @shro.guest > < @shro.user > <! --> <li class="layui-nav-item">
      <a class="fly-nav-avatar" href="javascript:;">
        <cite class="layui-hide-xs"><@shiro.principal property="username" /></cite>
        <i class="iconfont icon-renzheng layui-hide-xs" title="Authenticated information: Layui author"></i>
        <i class="layui-badge fly-badge-vip layui-hide-xs">VIP<@shiro.principal property="vipLevel" /></i>
        <img src="<@shiro.principal property="avatar" />">
      </a>
      <dl class="layui-nav-child">
        <dd><a href="user/set.html"><i class="layui-icon"></ I > Basic Settings </a></dd> <dd><a href="user/message.html"><i class="iconfont icon-tongzhi" style="top: 4px;"></ I > My message </a></dd> <dd><a href="user/home.html"><i class="layui-icon" style="margin-left: 2px; font-size: 22px;"></ I > my home </a></dd> <hr style="margin: 5px 0;">
        <dd><a href="/logout" style="text-align: center;"> Exit </a></dd> </dl> </li> </ @shro.user ></ ul>Copy the code

The < @shro. guest> and < @shro. user > tags are used to identify whether the user has logged in. In this way, before login, we see the login registration button, after login, we see the user name, avatar, etc. ~

Ok, we have finished shiro’s label above

That’s all for today’s homework. Let’s log in and register first.