个人博客搭建

  • 技术选型
    • 主要框架
  • 安装 RabbitMq,Elasticsearch
    • 安装RabbitMq
      • 安装环境
      • 下载安装包
      • 安装配置 erlang
      • 安装配置 rabbitMQ
      • 验证
    • 安装Elasticsearch
      • 下载地址
  • 创建Springboot 项目
    • pom.xml
    • 前端的框架用的是 layui 的 fly-3.0
    • 拆分首页 以及填充
    • 引入Mybatis-plus
    • 处理分页的问题
    • 处理显示的时间问题,渲染时间
      • 错误记录
    • 修改置顶部分
    • 详情页面修改
    • 修改提问的页面
    • 处理评论问题
    • 本周热议功能
    • 文章的浏览量功能
    • 集成Shiro完成登录注册功能
    • 修改登录之后导航栏显示自己信息以及退出登录
    • 基本设置功能
      • 个人主页问题
      • 基本设置功能
      • 上传头像的功能
      • 修改密码的功能
    • 用户中心发表的贴和收藏的贴
    • 消息中心
  • 下面的内容地址

技术选型

主要框架

核心框架 Springboot
安全框架 Apache Shiro
持久层框架 Mybatis + mybatis-plus
页面模板 Freemarker
缓存框架 Redis
数据库 mysql
消息队列 RabbitMq
分布式搜索 Elasticsearch
双工通讯协议:websocket
网络通讯框架:t-io
工具集合:hutool

安装 RabbitMq,Elasticsearch

安装RabbitMq

安装环境

系统:win10 64位专业版
erlang:otp_win64_24.0
rabbitMQ:rabbitmq-server-3.8.19
安装rabbitMQ需要依赖erlang语言环境,所以需要我们下载erlang的环境安装程序。

下载安装包

rabbitMQ安装程序下载路径:https://www.rabbitmq.com/install-windows-manual.html

![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/fcfdfb6e7ca013aca108e203e2d2f8b4.png

erlang环境安装程序下载路径: https://www.erlang.org/downloads

安装配置 erlang

对于安装路径没有特殊要求的话,就一路next直至安装成功即可,默认安装路径为:C:\Program Files\erl-23.0。

接下来配置环境变量,常规操作,新建系统变量-键入变量名ERLANG_HOME,键入变量值:erlang安装路径。如下图:

然后添加系统path路径中,添加 : %ERLANG_HOME%\bin


然后打开cmd,输入erl,看到我们的erlang版本号,就说明安装成功了

安装配置 rabbitMQ

双击我们刚才下载的rabbitmq-server-3.8.19程序,next,install即可,此处需要注意,如果要自定义安装路径的话,路径中最好不要存在中文,会出现错误。
安装完成之后,需要我们激活rabbitmq_management

打开cmd,进到sbin目录下,运行命令

rabbitmq-plugins enable rabbitmq_management

验证

上面的命令执行成功之后,我们就可以通过http://localhost:15672来访问web端的管理界面
初始可以通过用户名:guest 密码guest来登录


这就说明我们安装成功了。

net start RabbitMQ  #启动
net stop RabbitMQ  #停止
rabbitmqctl status  #查看状态

安装Elasticsearch

下载地址

https://www.elastic.co/cn/elasticsearch/ 官网


下载完成后

目录结构
bin :启动文件
config 配置文件
log4j2 :日志文件
jvm.options java 虚拟机相关的配置
elasticsearch.yml elasticsearch 的配置文件
lib 相关jar 包
logs 日志
modules 功能模块
plugins 插件 ik

在bin 里点击启动即可


安装head 插件
在 github 下载 https://github.com/mobz/elasticsearch-head/
简单的前端页面
安装 cnpm install


**此时还不能链接上我们的 es **

连接不上解决一下跨域的问题
在ElasticSearch.yml 加上这两句话
http.cors.enabled: true
http.cors.allow-origin: “*”

此时就可以连接上来了

创建Springboot 项目

这个操作比较简单

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.2.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>Springboot-blog</artifactId><version>0.0.1-SNAPSHOT</version><name>Springboot-blog</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><!--netty--></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><!--mp--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.2.0</version></dependency><!--代码生成器--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.2.0</version></dependency><!-- sql分析器 --><dependency><groupId>p6spy</groupId><artifactId>p6spy</artifactId><version>3.8.6</version></dependency><!-- commons-lang3 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>4.1.17</version></dependency><!--验证码--><dependency><groupId>com.github.axet</groupId><artifactId>kaptcha</artifactId><version>0.0.9</version></dependency><!--shiro权限框架--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version></dependency><dependency><groupId>net.mingsoft</groupId><artifactId>shiro-freemarker-tags</artifactId><version>0.1</version></dependency><!--websocket--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- es 6.4.3版本 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId><!--netty--><version>2.1.1.RELEASE</version></dependency><!--整合rabbitmq--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>org.modelmapper</groupId><artifactId>modelmapper</artifactId><version>1.1.0</version></dependency><!-- tio --><dependency><groupId>org.t-io</groupId><artifactId>tio-websocket-server</artifactId><version>3.2.5.v20190101-RELEASE</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

前端的框架用的是 layui 的 fly-3.0

我放在了 资源里面 可以下载到https://download.csdn.net/download/m0_46937429/20398937?spm=1001.2014.3001.5503

大概框架是这个样子.下载在我的资源里面

拆分首页 以及填充

先把 下载好的 fly 3.0 的首页引入到 templates
controller 层

package com.example.springbootblog.Controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
public class IndexController {@RequestMapping ({"","/","index"})public String index () {return "index";}
}

此时访问 http://localhost:8080/

我们可以把 头信息抽离出来 再用 freemarker 引入就可以了


然后在 index.ftl 里面引入就可以了

同样的道理把 导航的部分跟 尾部抽离出来就可以了
中间部分处理

可以先把右边的部分抽离出来

这个 right.ftl 里面就是 那个 md 4 的 同样的道理 把 左侧的可以 抽离出来

这个时候还不太够完善,有好多的地方可以在所有的地方会用到
**新建一个 layout.ftl **

用这个标签 暂时把 index.ftl 里面的东西 复制到 下面这个标签 里面
但是不能全部复制过来 比如 这几个 就不是公共的。就是首页自己的。可能别的页面就没有的

<#macro layout title></macro>

怎么引用我们写的layout.ftl 页面呢?

<#include "./inc/layout.ftl"/>
<#-- 导入 layout 的标签 -->
<@layout "首页" >
<#include "./inc/hrader-panel.ftl"/>
<div class="layui-container"><div class="layui-row layui-col-space15"><#include "./inc/left.ftl"/><#include "./inc/right.ftl"/></div>
</div>
</@layout>

运行访问 ,其实就是拼图 套娃。哈哈哈哈

还可以访问,证明我们的抽离没有问题
添加 页面


把这两个页面复制过来

先写Controller 测试一下能不能用

@Controller
public class PostController {// 指定值接收 数字类型@GetMapping("/category/{id:\\d*}")public String category(@PathVariable(name = "id") Long id) {return "post/category";}@GetMapping("/detail/{id:\\d*}")public String detail(@PathVariable(name = "id") Long id) {return "post/detail";}
}

效果


是没有问题的。但是不能用 Controller 形式 跳转。我们应该用 页面的形式 。接下来修改页面用模板进行跳转
category 页面


<#include "../inc/layout.ftl"/>
<#-- 导入 layout 的标签 -->
<@layout "博客分类" ><#include "../inc/hrader-panel.ftl"/><div class="layui-container"><div class="layui-row layui-col-space15"><div class="layui-col-md8"><div class="fly-panel" style="margin-bottom: 0;"><div class="fly-panel-title fly-filter"><a href="" class="layui-this">综合</a><span class="fly-mid"></span><a href="">未结</a><span class="fly-mid"></span><a href="">已结</a><span class="fly-mid"></span><a href="">精华</a><span class="fly-filter-right layui-hide-xs"><a href="" class="layui-this">按最新</a><span class="fly-mid"></span><a href="">按热议</a></span></div><ul class="fly-list"><li><a href="user/home.html" class="fly-avatar"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><h2><a class="layui-badge">分享</a><a href="detail.html">基于 layui 的极简社区页面模版</a></h2><div class="fly-list-info"><a href="user/home.html" link><cite>贤心</cite><!--<i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i>--></a><span>刚刚</span><span class="fly-list-kiss layui-hide-xs" title="悬赏飞吻"><i class="iconfont icon-kiss"></i> 60</span><!--<span class="layui-badge fly-badge-accept layui-hide-xs">已结</span>--><span class="fly-list-nums"><i class="iconfont icon-pinglun1" title="回答"></i> 66</span></div><div class="fly-list-badge"><span class="layui-badge layui-bg-black">置顶</span><!--<span class="layui-badge layui-bg-red">精帖</span>--></div></li><li><a href="user/home.html" class="fly-avatar"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><h2><a class="layui-badge">动态</a><a href="detail.html">基于 layui 的极简社区页面模版</a></h2><div class="fly-list-info"><a href="user/home.html" link><cite>贤心</cite><!--<i class="iconfont icon-renzheng" title="认证信息:XXX"></i>--><i class="layui-badge fly-badge-vip">VIP3</i></a><span>刚刚</span><span class="fly-list-kiss layui-hide-xs" title="悬赏飞吻"><i class="iconfont icon-kiss"></i> 60</span><span class="layui-badge fly-badge-accept layui-hide-xs">已结</span><span class="fly-list-nums"><i class="iconfont icon-pinglun1" title="回答"></i> 66</span></div><div class="fly-list-badge"><span class="layui-badge layui-bg-red">精帖</span></div></li><li><a href="user/home.html" class="fly-avatar"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><h2><a class="layui-badge">动态</a><a href="detail.html">基于 layui 的极简社区页面模版</a></h2><div class="fly-list-info"><a href="user/home.html" link><cite>贤心</cite><!--<i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i>--></a><span>刚刚</span><span class="fly-list-kiss layui-hide-xs" title="悬赏飞吻"><i class="iconfont icon-kiss"></i> 60</span><!--<span class="layui-badge fly-badge-accept layui-hide-xs">已结</span>--><span class="fly-list-nums"><i class="iconfont icon-pinglun1" title="回答"></i> 66</span></div><div class="fly-list-badge"><!--<span class="layui-badge layui-bg-red">精帖</span>--></div></li><li><a href="user/home.html" class="fly-avatar"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><h2><a class="layui-badge">动态</a><a href="detail.html">基于 layui 的极简社区页面模版</a></h2><div class="fly-list-info"><a href="user/home.html" link><cite>贤心</cite><!--<i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i>--></a><span>刚刚</span><span class="fly-list-kiss layui-hide-xs" title="悬赏飞吻"><i class="iconfont icon-kiss"></i> 60</span><!--<span class="layui-badge fly-badge-accept layui-hide-xs">已结</span>--><span class="fly-list-nums"><i class="iconfont icon-pinglun1" title="回答"></i> 66</span></div><div class="fly-list-badge"><!--<span class="layui-badge layui-bg-red">精帖</span>--></div></li><li><a href="user/home.html" class="fly-avatar"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><h2><a class="layui-badge">动态</a><a href="detail.html">基于 layui 的极简社区页面模版</a></h2><div class="fly-list-info"><a href="user/home.html" link><cite>贤心</cite><!--<i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i>--></a><span>刚刚</span><span class="fly-list-kiss layui-hide-xs" title="悬赏飞吻"><i class="iconfont icon-kiss"></i> 60</span><!--<span class="layui-badge fly-badge-accept layui-hide-xs">已结</span>--><span class="fly-list-nums"><i class="iconfont icon-pinglun1" title="回答"></i> 66</span></div><div class="fly-list-badge"><!--<span class="layui-badge layui-bg-red">精帖</span>--></div></li><li><a href="user/home.html" class="fly-avatar"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><h2><a class="layui-badge">动态</a><a href="detail.html">基于 layui 的极简社区页面模版</a></h2><div class="fly-list-info"><a href="user/home.html" link><cite>贤心</cite><!--<i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i>--></a><span>刚刚</span><span class="fly-list-kiss layui-hide-xs" title="悬赏飞吻"><i class="iconfont icon-kiss"></i> 60</span><!--<span class="layui-badge fly-badge-accept layui-hide-xs">已结</span>--><span class="fly-list-nums"><i class="iconfont icon-pinglun1" title="回答"></i> 66</span></div><div class="fly-list-badge"><!--<span class="layui-badge layui-bg-red">精帖</span>--></div></li><li><a href="user/home.html" class="fly-avatar"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><h2><a class="layui-badge">动态</a><a href="detail.html">基于 layui 的极简社区页面模版</a></h2><div class="fly-list-info"><a href="user/home.html" link><cite>贤心</cite><!--<i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i>--></a><span>刚刚</span><span class="fly-list-kiss layui-hide-xs" title="悬赏飞吻"><i class="iconfont icon-kiss"></i> 60</span><!--<span class="layui-badge fly-badge-accept layui-hide-xs">已结</span>--><span class="fly-list-nums"><i class="iconfont icon-pinglun1" title="回答"></i> 66</span></div><div class="fly-list-badge"><!--<span class="layui-badge layui-bg-red">精帖</span>--></div></li><li><a href="user/home.html" class="fly-avatar"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><h2><a class="layui-badge">动态</a><a href="detail.html">基于 layui 的极简社区页面模版</a></h2><div class="fly-list-info"><a href="user/home.html" link><cite>贤心</cite><!--<i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i>--></a><span>刚刚</span><span class="fly-list-kiss layui-hide-xs" title="悬赏飞吻"><i class="iconfont icon-kiss"></i> 60</span><!--<span class="layui-badge fly-badge-accept layui-hide-xs">已结</span>--><span class="fly-list-nums"><i class="iconfont icon-pinglun1" title="回答"></i> 66</span></div><div class="fly-list-badge"><!--<span class="layui-badge layui-bg-red">精帖</span>--></div></li></ul><!-- <div class="fly-none">没有相关数据</div> --><div style="text-align: center"><div class="laypage-main"><span class="laypage-curr">1</span><a href="/jie/page/2/">2</a><a href="/jie/page/3/">3</a><a href="/jie/page/4/">4</a><a href="/jie/page/5/">5</a><span>…</span><a href="/jie/page/148/" class="laypage-last" title="尾页">尾页</a><a href="/jie/page/2/" class="laypage-next">下一页</a></div></div></div></div><#include "../inc/right.ftl"></div>
</div>
</@layout>

同理把detail.ftl 页面修改一下

<#include "../inc/layout.ftl"/>
<#-- 导入 layout 的标签 -->
<@layout "博客分类" ><#include "../inc/hrader-panel.ftl"/><div class="layui-container"><div class="layui-row layui-col-space15"><div class="layui-col-md8 content detail"><div class="fly-panel detail-box"><h1>Fly Template v3.0,基于 layui 的极简社区页面模版</h1><div class="fly-detail-info"><!-- <span class="layui-badge">审核中</span> --><span class="layui-badge layui-bg-green fly-detail-column">动态</span><span class="layui-badge" style="background-color: #999;">未结</span><!-- <span class="layui-badge" style="background-color: #5FB878;">已结</span> --><span class="layui-badge layui-bg-black">置顶</span><span class="layui-badge layui-bg-red">精帖</span><div class="fly-admin-box" data-id="123"><span class="layui-btn layui-btn-xs jie-admin" type="del">删除</span><span class="layui-btn layui-btn-xs jie-admin" type="set" field="stick" rank="1">置顶</span><!-- <span class="layui-btn layui-btn-xs jie-admin" type="set" field="stick" rank="0" style="background-color:#ccc;">取消置顶</span> --><span class="layui-btn layui-btn-xs jie-admin" type="set" field="status" rank="1">加精</span><!-- <span class="layui-btn layui-btn-xs jie-admin" type="set" field="status" rank="0" style="background-color:#ccc;">取消加精</span> --></div><span class="fly-list-nums"><a href="#comment"><i class="iconfont" title="回答">&#xe60c;</i> 66</a><i class="iconfont" title="人气">&#xe60b;</i> 99999</span></div><div class="detail-about"><a class="fly-avatar" href="../user/home.html"><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt="贤心"></a><div class="fly-detail-user"><a href="../user/home.html" class="fly-link"><cite>贤心</cite><i class="iconfont icon-renzheng" title="认证信息:{{ rows.user.approve }}"></i><i class="layui-badge fly-badge-vip">VIP3</i></a><span>2017-11-30</span></div><div class="detail-hits" id="LAY_jieAdmin" data-id="123"><span style="padding-right: 10px; color: #FF7200">悬赏:60飞吻</span><span class="layui-btn layui-btn-xs jie-admin" type="edit"><a href="add.html">编辑此贴</a></span></div></div><div class="detail-body photos"><p>该模版由 layui官方社区(<a href="http://fly.layui.com/" target="_blank">fly.layui.com</a>)倾情提供,只为表明我们对 layui 执着的信念、以及对未来持续加强的承诺。该模版基于 layui 搭建而成,可作为极简通用型社区的页面支撑。</p><p>更新日志:</p><pre>
# v3.0 2017-11-30
* 采用 layui 2.2.3 作为 UI 支撑
* 全面同步最新的 Fly 社区风格,各种细节得到大幅优化
* 更友好的响应式适配能力
</pre>下载<hr><p>官网:<a href="http://www.layui.com/template/fly/" target="_blank">http://www.layui.com/template/fly/</a><br>码云:<a href="https://gitee.com/sentsin/fly/" target="_blank">https://gitee.com/sentsin/fly/</a><br>GitHub:<a href="https://github.com/layui/fly" target="_blank">https://github.com/layui/fly</a></p>封面<hr><p><img src="../../res/images/fly.jpg" alt="Fly社区"></p></div></div><div class="fly-panel detail-box" id="flyReply"><fieldset class="layui-elem-field layui-field-title" style="text-align: center;"><legend>回帖</legend></fieldset><ul class="jieda" id="jieda"><li data-id="111" class="jieda-daan"><a name="item-1111111111"></a><div class="detail-about detail-about-reply"><a class="fly-avatar" href=""><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt=" "></a><div class="fly-detail-user"><a href="" class="fly-link"><cite>贤心</cite><i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i></a><span>(楼主)</span><!--<span style="color:#5FB878">(管理员)</span><span style="color:#FF9E3F">(社区之光)</span><span style="color:#999">(该号已被封)</span>--></div><div class="detail-hits"><span>2017-11-30</span></div><i class="iconfont icon-caina" title="最佳答案"></i></div><div class="detail-body jieda-body photos"><p>香菇那个蓝瘦,这是一条被采纳的回帖</p></div><div class="jieda-reply"><span class="jieda-zan zanok" type="zan"><i class="iconfont icon-zan"></i><em>66</em></span><span type="reply"><i class="iconfont icon-svgmoban53"></i>回复</span><div class="jieda-admin"><span type="edit">编辑</span><span type="del">删除</span><!-- <span class="jieda-accept" type="accept">采纳</span> --></div></div></li><li data-id="111"><a name="item-1111111111"></a><div class="detail-about detail-about-reply"><a class="fly-avatar" href=""><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt=" "></a><div class="fly-detail-user"><a href="" class="fly-link"><cite>贤心</cite></a></div><div class="detail-hits"><span>2017-11-30</span></div></div><div class="detail-body jieda-body photos"><p>蓝瘦那个香菇,这是一条没被采纳的回帖</p></div><div class="jieda-reply"><span class="jieda-zan" type="zan"><i class="iconfont icon-zan"></i><em>0</em></span><span type="reply"><i class="iconfont icon-svgmoban53"></i>回复</span><div class="jieda-admin"><span type="edit">编辑</span><span type="del">删除</span><span class="jieda-accept" type="accept">采纳</span></div></div></li><!-- 无数据时 --><!-- <li class="fly-none">消灭零回复</li> --></ul><div class="layui-form layui-form-pane"><form action="/jie/reply/" method="post"><div class="layui-form-item layui-form-text"><a name="comment"></a><div class="layui-input-block"><textarea id="L_content" name="content" required lay-verify="required" placeholder="请输入内容"  class="layui-textarea fly-editor" style="height: 150px;"></textarea></div></div><div class="layui-form-item"><input type="hidden" name="jid" value="123"><button class="layui-btn" lay-filter="*" lay-submit>提交回复</button></div></form></div></div></div><#include "../inc/right.ftl"></div></div>
</@layout>

以上前端暂时开发完毕

引入Mybatis-plus

导入mp的依赖包

   <!--mp--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.2.0</version></dependency><!--代码生成器--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.2.0</version></dependency>

从官网把生成代码的代码复制下来改一下

public class 生成代码 {public static String scanner(String tip) {Scanner scanner = new Scanner(System.in);StringBuilder help = new StringBuilder();help.append("请输入" + tip + ":");System.out.println(help.toString());if (scanner.hasNext()) {String ipt = scanner.next();if (StringUtils.isNotEmpty(ipt)) {return ipt;}}throw new MybatisPlusException("请输入正确的" + tip + "!");}public static void main(String[] args) {// 代码生成器AutoGenerator mpg = new AutoGenerator();// 全局配置GlobalConfig gc = new GlobalConfig();String projectPath = System.getProperty("user.dir");gc.setOutputDir(projectPath + "/src/main/java");gc.setAuthor("fjj");gc.setOpen(false);// gc.setSwagger2(true); 实体属性 Swagger2 注解mpg.setGlobalConfig(gc);// 数据源配置DataSourceConfig dsc = new DataSourceConfig();dsc.setUrl("jdbc:mysql://localhost:3306/eblog?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT");// dsc.setSchemaName("public");dsc.setDriverName("com.mysql.cj.jdbc.Driver");dsc.setUsername("root");dsc.setPassword("123456");mpg.setDataSource(dsc);// 包配置PackageConfig pc = new PackageConfig();
//        pc.setModuleName(scanner("模块名"));pc.setParent("com.example.springbootblog");mpg.setPackageInfo(pc);// 自定义配置InjectionConfig cfg = new InjectionConfig() {@Overridepublic void initMap() {// to do nothing}};// 如果模板引擎是 freemarkerString templatePath = "/templates/mapper.xml.ftl";// 如果模板引擎是 velocity// String templatePath = "/templates/mapper.xml.vm";// 自定义输出配置List<FileOutConfig> focList = new ArrayList<>();// 自定义配置会被优先输出focList.add(new FileOutConfig(templatePath) {@Overridepublic String outputFile(TableInfo tableInfo) {// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;}});cfg.setFileOutConfigList(focList);mpg.setCfg(cfg);// 配置模板TemplateConfig templateConfig = new TemplateConfig();templateConfig.setXml(null);mpg.setTemplate(templateConfig);// 策略配置StrategyConfig strategy = new StrategyConfig();strategy.setNaming(NamingStrategy.underline_to_camel);strategy.setColumnNaming(NamingStrategy.underline_to_camel);strategy.setEntityLombokModel(true);strategy.setRestControllerStyle(true);// 公共父类// 写于父类中的公共字段strategy.setInclude(scanner("user").split(","));strategy.setControllerMappingHyphenStyle(true);mpg.setStrategy(strategy);mpg.setTemplateEngine(new FreemarkerTemplateEngine());mpg.execute();}}

点击之后生成我们的代码和实体类注意要修改链接数据库还有包的地方
此外在引入MP 之后一定要编写COnfig 类或者在主启动类上加上 MapperScan

这样启动的时候就不会报错了

说白了就是现在上面的那些导航栏是死的,我们应该从数据库里面去查询我们的数据。变成项目启动的时候动态出来我们的数据就可以了 。

创建ContextStartup

@Component
// 实现 启动类 ,还有 上下文的servlect
public class ContextStartup implements ApplicationRunner, ServletContextAware {// 注入 categoryService@AutowiredIMCategoryService categoryService;ServletContext servletContext;@Overridepublic void run(ApplicationArguments args) throws Exception {// 调用全查的方法List<MCategory> list = categoryService.list(new QueryWrapper<MCategory>().eq("status", 0));servletContext.setAttribute("List",list);}@Overridepublic void setServletContext(ServletContext servletContext) {this.servletContext =servletContext;}
}

修改



变为动态的了,但是还是有一个小的bug 就是当我们点击的时候首页那个绿色不会消失
创建一个公共的Controller 类,主要放一些公共需要继承的东西

修改我们的PostController 类 设置属性


效果这个时候我们点击分享的时候首页就不会是绿色的,只有分享是绿色的

处理分页的问题

引入Mp 的分页插件


编写首页的Controller
解释为啥用这个方法


源码

Ok 接着编写
Controller 类编写完毕

@Controller
public class IndexController extends BaseController {@RequestMapping({"", "/", "index"})public String index() {// 设置默认的开始页面// 这里为啥不用 request.Parameter 获取是因为他不可以设置默认的值 这个工具类会判断你有没有设置默认页面 如果设置了就先展示int start = ServletRequestUtils.getIntParameter(request, "start", 1);// 设置默认的展示多少页面int end = ServletRequestUtils.getIntParameter(request, "end", 2);
//         调用 Mp 的分页方法Page page = new Page(start, end);// 调用服务类 1,分页信息 2,分类 3,用户 ,4 置顶 ,5 精选 6排序 排序是通过时间进行一个排序IPage results = postService.paging(page,null,null,null,null,"created");request.setAttribute("CategoryId", 0);request.setAttribute("created", 0);return "index";}
}

Postserviceimpl 实现类

/*** <p>* 服务实现类* </p>** @author fjj* @since 2021-07-23*/
@Service
public class MPostServiceImpl extends ServiceImpl<MPostMapper, MPost> implements IMPostService {@AutowiredMPostMapper postMapper;@Overridepublic IPage paging(Page page, Long categoryId, Long userId, Integer level, Boolean recommend, String order) {// 判断 level 是否等于 空if (level == null) level = -1;// 获取wrapperQueryWrapper wrapper = new QueryWrapper<MPost>().eq(categoryId != null,"category_id",categoryId).eq(userId !=null ,"user_id",userId).eq(level == 0 ,"level",0).gt(level >0 ,"level",0).orderByDesc(order != null,order);return postMapper.selectPosts(page, wrapper);}
}

MPostMapper 的Mapper

/*** <p>*  Mapper 接口* </p>** @author fjj* @since 2021-07-23*/
@Mapper
public interface MPostMapper extends BaseMapper<MPost> {// 这里之所以要 @Param(Constants.WRAPPER) 是 MP 官网的要求IPage<PostVo> selectPosts( Page page ,@Param(Constants.WRAPPER) QueryWrapper wrapper);
}

MPostMapper.xml 的Spl 语句 应该是3个表连起来

<?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.example.springbootblog.mapper.MPostMapper">
<select id="selectPosts" resultType="com.example.springbootblog.vo.PostVo">SELECTp.*,u.id AS authorId,u.username AS authorName,u.avatar AS authorAvatar,c.id AS categoryId,c.name AS categoryNameFROMm_post pLEFT JOIN m_user u ON p.user_id = u.idLEFT JOIN m_category c ON p.category_id = c.id${ew.customSqlSegment}
</select>
</mapper>

现在我们的Sql 都还不能看到参考MP 配置一下Sql 分析打印

配置application.yml

mybatis-plus:mapper-locations: classpath*:/mapper/**Mapper.xml#用于mybatis在控制台打印sql日志configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

重启项目

这个SQL是因为在我们的Config 里面有个

前端页面获取

这里之所以是 pageData.records是因为返回的是我们写好的PostVo 因为是三个表查询,有的是通过id查询的别的表的字段,原来的实体类里面没有有些字段所以先建了一个PostVo.class 用来映射个别字段继承Post 的实体类这样就可以有所有的字段了

package com.example.springbootblog.vo;import com.example.springbootblog.entity.MPost;
import lombok.Data;@Data
public class PostVo extends MPost {private Long authorId;private String authorName;private String authorAvatar;
//    private Long categoryId;private String categoryName;
}

打印输出一下pageData.records可以看到就是映射的是我们的Postvo.class

这样的话就页面就可以访问到我们数据库的数据了



效果

虽然渲染出来了,但是不是我们想要的数据。我们应该是存我们的后台返回出来的数据进行分页

修改前端的地方


layui 官网对于分页的这几个对象属性的解释

虽然有了从数据库出来的分页的数据的效果但是当我们点击第二页的时候数据并不会变化,所以还需要在修改一下代码layui 官网上就有

复制过来


抽取出来分页,因为分页不可能只在这一个地方出现

修改index.ftl 页面

不要忘记在layout里面添加标签

至此分页功能结束

处理显示的时间问题,渲染时间

两种方法

  1. 用JS 进行渲染
  2. 用freemarker标签

编写freemarker的接口

DirectiveHandler

package com.example.springbootblog.common.templates;import freemarker.template.*;import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;import static org.apache.commons.lang3.StringUtils.*;/*** Freemarker 模型工具类** Created by langhsu on 2017/11/14.*/
public class TemplateModelUtils {public static final DateFormat FULL_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static final int FULL_DATE_LENGTH = 19;public static final DateFormat SHORT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");public static final int SHORT_DATE_LENGTH = 10;public static String converString(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateScalarModel) {return ((TemplateScalarModel) model).getAsString();} else if ((model instanceof TemplateNumberModel)) {return ((TemplateNumberModel) model).getAsNumber().toString();}}return null;}public static TemplateHashModel converMap(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateHashModelEx) {return (TemplateHashModelEx) model;} else if (model instanceof TemplateHashModel) {return (TemplateHashModel) model;}}return null;}public static Integer converInteger(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateNumberModel) {return ((TemplateNumberModel) model).getAsNumber().intValue();} else if (model instanceof TemplateScalarModel) {String s = ((TemplateScalarModel) model).getAsString();if (isNotBlank(s)) {try {return Integer.parseInt(s);} catch (NumberFormatException e) {}}}}return null;}public static Short converShort(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateNumberModel) {return ((TemplateNumberModel) model).getAsNumber().shortValue();} else if (model instanceof TemplateScalarModel) {String s = ((TemplateScalarModel) model).getAsString();if (isNotBlank(s)) {try {return Short.parseShort(s);} catch (NumberFormatException e) {}}}}return null;}public static Long converLong(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateNumberModel) {return ((TemplateNumberModel) model).getAsNumber().longValue();} else if (model instanceof TemplateScalarModel) {String s = ((TemplateScalarModel) model).getAsString();if (isNotBlank(s)) {try {return Long.parseLong(s);} catch (NumberFormatException e) {}}}}return null;}public static Double converDouble(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateNumberModel) {return ((TemplateNumberModel) model).getAsNumber().doubleValue();} else if (model instanceof TemplateScalarModel) {String s = ((TemplateScalarModel) model).getAsString();if (isNotBlank(s)) {try {return Double.parseDouble(s);} catch (NumberFormatException ignored) {}}}}return null;}public static String[] converStringArray(TemplateModel model) throws TemplateModelException {if (model instanceof TemplateSequenceModel) {TemplateSequenceModel smodel = (TemplateSequenceModel) model;String[] values = new String[smodel.size()];for (int i = 0; i < smodel.size(); i++) {values[i] = converString(smodel.get(i));}return values;} else {String str = converString(model);if (isNotBlank(str)) {return split(str,',');}}return null;}public static Boolean converBoolean(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateBooleanModel) {return ((TemplateBooleanModel) model).getAsBoolean();} else if (model instanceof TemplateNumberModel) {return !(0 == ((TemplateNumberModel) model).getAsNumber().intValue());} else if (model instanceof TemplateScalarModel) {String temp = ((TemplateScalarModel) model).getAsString();if (isNotBlank(temp)) {return Boolean.valueOf(temp);}}}return null;}public static Date converDate(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateDateModel) {return ((TemplateDateModel) model).getAsDate();} else if (model instanceof TemplateScalarModel) {String temp = trimToEmpty(((TemplateScalarModel) model).getAsString());return parseDate(temp);}}return null;}public static Date parseDate(String date) {Date ret = null;try {if (FULL_DATE_LENGTH == date.length()) {ret = FULL_DATE_FORMAT.parse(date);} else if (SHORT_DATE_LENGTH == date.length()) {ret = SHORT_DATE_FORMAT.parse(date);}} catch (ParseException e) {}return ret;}
}

TemplateDirective

package com.example.springbootblog.common.templates;import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;import java.io.IOException;
import java.util.Map;/*** Created by langhsu on 2017/11/14.*/
public abstract class TemplateDirective implements TemplateDirectiveModel {protected static String RESULT = "result";protected static String RESULTS = "results";@Overridepublic void execute(Environment env, Map parameters,TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException {try {execute(new DirectiveHandler(env, parameters, loopVars, body));} catch (IOException e) {throw e;} catch (Exception e) {throw new TemplateException(e, env);}}abstract public String getName();abstract public void execute(DirectiveHandler handler) throws Exception;}

TemplateModelUtils

package com.example.springbootblog.common.templates;import freemarker.template.*;import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;import static org.apache.commons.lang3.StringUtils.*;/*** Freemarker 模型工具类** Created by langhsu on 2017/11/14.*/
public class TemplateModelUtils {public static final DateFormat FULL_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static final int FULL_DATE_LENGTH = 19;public static final DateFormat SHORT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");public static final int SHORT_DATE_LENGTH = 10;public static String converString(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateScalarModel) {return ((TemplateScalarModel) model).getAsString();} else if ((model instanceof TemplateNumberModel)) {return ((TemplateNumberModel) model).getAsNumber().toString();}}return null;}public static TemplateHashModel converMap(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateHashModelEx) {return (TemplateHashModelEx) model;} else if (model instanceof TemplateHashModel) {return (TemplateHashModel) model;}}return null;}public static Integer converInteger(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateNumberModel) {return ((TemplateNumberModel) model).getAsNumber().intValue();} else if (model instanceof TemplateScalarModel) {String s = ((TemplateScalarModel) model).getAsString();if (isNotBlank(s)) {try {return Integer.parseInt(s);} catch (NumberFormatException e) {}}}}return null;}public static Short converShort(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateNumberModel) {return ((TemplateNumberModel) model).getAsNumber().shortValue();} else if (model instanceof TemplateScalarModel) {String s = ((TemplateScalarModel) model).getAsString();if (isNotBlank(s)) {try {return Short.parseShort(s);} catch (NumberFormatException e) {}}}}return null;}public static Long converLong(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateNumberModel) {return ((TemplateNumberModel) model).getAsNumber().longValue();} else if (model instanceof TemplateScalarModel) {String s = ((TemplateScalarModel) model).getAsString();if (isNotBlank(s)) {try {return Long.parseLong(s);} catch (NumberFormatException e) {}}}}return null;}public static Double converDouble(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateNumberModel) {return ((TemplateNumberModel) model).getAsNumber().doubleValue();} else if (model instanceof TemplateScalarModel) {String s = ((TemplateScalarModel) model).getAsString();if (isNotBlank(s)) {try {return Double.parseDouble(s);} catch (NumberFormatException ignored) {}}}}return null;}public static String[] converStringArray(TemplateModel model) throws TemplateModelException {if (model instanceof TemplateSequenceModel) {TemplateSequenceModel smodel = (TemplateSequenceModel) model;String[] values = new String[smodel.size()];for (int i = 0; i < smodel.size(); i++) {values[i] = converString(smodel.get(i));}return values;} else {String str = converString(model);if (isNotBlank(str)) {return split(str,',');}}return null;}public static Boolean converBoolean(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateBooleanModel) {return ((TemplateBooleanModel) model).getAsBoolean();} else if (model instanceof TemplateNumberModel) {return !(0 == ((TemplateNumberModel) model).getAsNumber().intValue());} else if (model instanceof TemplateScalarModel) {String temp = ((TemplateScalarModel) model).getAsString();if (isNotBlank(temp)) {return Boolean.valueOf(temp);}}}return null;}public static Date converDate(TemplateModel model) throws TemplateModelException {if (null != model) {if (model instanceof TemplateDateModel) {return ((TemplateDateModel) model).getAsDate();} else if (model instanceof TemplateScalarModel) {String temp = trimToEmpty(((TemplateScalarModel) model).getAsString());return parseDate(temp);}}return null;}public static Date parseDate(String date) {Date ret = null;try {if (FULL_DATE_LENGTH == date.length()) {ret = FULL_DATE_FORMAT.parse(date);} else if (SHORT_DATE_LENGTH == date.length()) {ret = SHORT_DATE_FORMAT.parse(date);}} catch (ParseException e) {}return ret;}
}

编写template继承刚的方法

package com.example.springbootblog.template;import com.example.springbootblog.common.templates.DirectiveHandler;
import freemarker.template.TemplateModelException;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.List;@Component
public class TimeAgoMethod extends DirectiveHandler.BaseMethod {private static final long ONE_MINUTE = 60000L;private static final long ONE_HOUR = 3600000L;private static final long ONE_DAY = 86400000L;private static final long ONE_WEEK = 604800000L;private static final String ONE_SECOND_AGO = "秒前";private static final String ONE_MINUTE_AGO = "分钟前";private static final String ONE_HOUR_AGO = "小时前";private static final String ONE_DAY_AGO = "天前";private static final String ONE_MONTH_AGO = "月前";private static final String ONE_YEAR_AGO = "年前";private static final String ONE_UNKNOWN = "未知";@Overridepublic Object exec(List arguments) throws TemplateModelException {Date time = getDate(arguments, 0);return format(time);}public static String format(Date date) {if (null == date) {return ONE_UNKNOWN;}long delta = new Date().getTime() - date.getTime();if (delta < 1L * ONE_MINUTE) {long seconds = toSeconds(delta);return (seconds <= 0 ? 1 : seconds) + ONE_SECOND_AGO;}if (delta < 45L * ONE_MINUTE) {long minutes = toMinutes(delta);return (minutes <= 0 ? 1 : minutes) + ONE_MINUTE_AGO;}if (delta < 24L * ONE_HOUR) {long hours = toHours(delta);return (hours <= 0 ? 1 : hours) + ONE_HOUR_AGO;}if (delta < 48L * ONE_HOUR) {return "昨天";}if (delta < 30L * ONE_DAY) {long days = toDays(delta);return (days <= 0 ? 1 : days) + ONE_DAY_AGO;}if (delta < 12L * 4L * ONE_WEEK) {long months = toMonths(delta);return (months <= 0 ? 1 : months) + ONE_MONTH_AGO;} else {long years = toYears(delta);return (years <= 0 ? 1 : years) + ONE_YEAR_AGO;}}private static long toSeconds(long date) {return date / 1000L;}private static long toMinutes(long date) {return toSeconds(date) / 60L;}private static long toHours(long date) {return toMinutes(date) / 60L;}private static long toDays(long date) {return toHours(date) / 24L;}private static long toMonths(long date) {return toDays(date) / 30L;}private static long toYears(long date) {return toMonths(date) / 365L;}
}

编写Config


@Configuration
public class FreemarkerConfig {@Autowiredprivate freemarker.template.Configuration configuration;@PostConstructpublic void setUp() {configuration.setSharedVariable("timeAgo", new TimeAgoMethod());}}

修改前端就可以了

效果

错误记录

刚开始的时候我的日期一直是未知 ,打断点发现前台传给后台的时候一直就是null 所以一直返回的未知

打断点调试

最后在实体类的发现用的日期格式化是

换成Date 之后
发现可以使用了
总结可能是因为我们继承的freemarker 的接口他们规定的 yyyy-MM-dd 的格式

修改置顶部分

创建自定义的标签

PostsTemplate

@Component
public class PostsTemplate extends TemplateDirective {// 注入服务类@AutowiredIMPostService postService;@Overridepublic String getName() {return "posts";}@Overridepublic void execute(DirectiveHandler handler) throws Exception {// 设置置顶Integer level = handler.getInteger("level");//设置分页Integer pn = handler.getInteger("pn", 1);Integer size = handler.getInteger("size", 2);// 设置creadeIDLong categoryId = handler.getLong("categoryId");IPage<PostVo> page = postService.paging(new Page(pn, size), categoryId, null, level, null, "created");handler.put(RESULTS,page).render();}
}

在FreemarkerConfig里面注入

@Configuration
public class FreemarkerConfig {@Autowiredprivate freemarker.template.Configuration configuration;@AutowiredPostsTemplate postsTemplate;@PostConstructpublic void setUp() {configuration.setSharedVariable("timeAgo", new TimeAgoMethod());configuration.setSharedVariable("posts", postsTemplate);}}

修改前端这里的页面


把原来的这部分代码抽离出来添加到common.ftl。方便调用

抽离出来之后

index.ftl 页面

效果
数据库中置顶的也只有一篇

详情页面修改

点击标题到详情页面功能

PostController

@Controller
public class PostController extends BaseController{// 指定值接收 数字类型@GetMapping("/category/{id:\\d*}")public String category(@PathVariable(name = "id") Long id) {// 给每一个都设置一个属性request.setAttribute("CategoryId",id);return "post/category";}@GetMapping("/detail/{id:\\d*}")public String detail(@PathVariable(name = "id") Long id) {// 查询数据 后面传得到底是根据那个id 进行查询。PostVo postVo =  postService.selectOnePost(new QueryWrapper<MPost>().eq("p.id",id));//断言判断是否被删除、如果时空的话就提示文章已经被删除了Assert.notNull(postVo,"文章已被删除");// 这个是为了设置 可以方便到底是那个 是分享还是提问之类request.setAttribute("CategoryId",postVo.getCategoryId());// 这个是为了设置到前台方便获取到数据request.setAttribute("post",postVo);return "post/detail";}
}

postServiceimpl

    @Overridepublic PostVo selectOnePost(QueryWrapper<MPost> wrapper) {return postMapper.selectOnePost(wrapper);}

PostMapper

// 这里要加上这个注解上面解释过为什么了PostVo selectOnePost(@Param(Constants.WRAPPER) QueryWrapper<MPost> wrapper);

PostMapper.xml

<!--这里跟上面一样,因为需要查询的数据也一样 --><select id="selectOnePost" resultType="com.example.springbootblog.vo.PostVo">SELECTp.*,u.id AS authorId,u.username AS authorName,u.avatar AS authorAvatar,c.id AS categoryId,c.name AS categoryNameFROMm_post pLEFT JOIN m_user u ON p.user_id = u.idLEFT JOIN m_category c ON p.category_id = c.id${ew.customSqlSegment}</select>

修改前端页面detail.ftl

// <!-- 这里是主要修改一下。然后获取到数据-->
<#include "../inc/layout.ftl"/>
<#-- 导入 layout 的标签 -->
<@layout "博客分类" ><#include "../inc/hrader-panel.ftl"/><div class="layui-container"><div class="layui-row layui-col-space15"><div class="layui-col-md8 content detail"><div class="fly-panel detail-box"><h1>${post.title}</h1><div class="fly-detail-info"><!-- <span class="layui-badge">审核中</span> --><span class="layui-badge layui-bg-green fly-detail-column">${post.categoryName}</span><#if  post.level gt 0 ><span class="layui-badge layui-bg-black">置顶</span></#if><#if  post.recommend><span class="layui-badge layui-bg-red">精帖</span> </#if><div class="fly-admin-box" data-id="${post.id}"><span class="layui-btn layui-btn-xs jie-admin" type="del">删除</span><span class="layui-btn layui-btn-xs jie-admin" type="set" field="stick" rank="1">置顶</span><!-- <span class="layui-btn layui-btn-xs jie-admin" type="set" field="stick" rank="0" style="background-color:#ccc;">取消置顶</span> --><span class="layui-btn layui-btn-xs jie-admin" type="set" field="status" rank="1">加精</span><!-- <span class="layui-btn layui-btn-xs jie-admin" type="set" field="status" rank="0" style="background-color:#ccc;">取消加精</span> --></div><span class="fly-list-nums"><a href="#comment"><i class="iconfont" title="回答">&#xe60c;</i> ${post.commentCount}</a><i class="iconfont" title="人气">&#xe60b;</i> ${post.viewCount}</span></div><div class="detail-about"><a class="fly-avatar" href="/user/${post.authorId}"><img src="${post.authorAvatar}" alt="${post.authorName}"></a><div class="fly-detail-user"><a href="/user/${post.authorId}" class="fly-link"><cite>${post.authorName}</cite></a><span>${timeAgo(post.created)}</span></div><div class="detail-hits" id="LAY_jieAdmin" data-id="${post.id}">
<#--              <#if profile.id == post.userId><span class="layui-btn layui-btn-xs jie-admin" type="edit"><a href="/post/edit?id=${post.id}">编辑此贴</a></span></#if>--></div></div><div class="detail-body photos">${post.content}</div></div><div class="fly-panel detail-box" id="flyReply"><fieldset class="layui-elem-field layui-field-title" style="text-align: center;"><legend>回帖</legend></fieldset><ul class="jieda" id="jieda"><li data-id="111" class="jieda-daan"><a name="item-1111111111"></a><div class="detail-about detail-about-reply"><a class="fly-avatar" href=""><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt=" "></a><div class="fly-detail-user"><a href="" class="fly-link"><cite>贤心</cite><i class="iconfont icon-renzheng" title="认证信息:XXX"></i><i class="layui-badge fly-badge-vip">VIP3</i></a><span>(楼主)</span></div><div class="detail-hits"><span>2017-11-30</span></div><i class="iconfont icon-caina" title="最佳答案"></i></div><div class="detail-body jieda-body photos"><p>香菇那个蓝瘦,这是一条被采纳的回帖</p></div><div class="jieda-reply"><span class="jieda-zan zanok" type="zan"><i class="iconfont icon-zan"></i><em>66</em></span><span type="reply"><i class="iconfont icon-svgmoban53"></i>回复</span><div class="jieda-admin"><span type="edit">编辑</span><span type="del">删除</span><!-- <span class="jieda-accept" type="accept">采纳</span> --></div></div></li><li data-id="111"><a name="item-1111111111"></a><div class="detail-about detail-about-reply"><a class="fly-avatar" href=""><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" alt=" "></a><div class="fly-detail-user"><a href="" class="fly-link"><cite>贤心</cite></a></div><div class="detail-hits"><span>2017-11-30</span></div></div><div class="detail-body jieda-body photos"><p>蓝瘦那个香菇,这是一条没被采纳的回帖</p></div><div class="jieda-reply"><span class="jieda-zan" type="zan"><i class="iconfont icon-zan"></i><em>0</em></span><span type="reply"><i class="iconfont icon-svgmoban53"></i>回复</span><div class="jieda-admin"><span type="edit">编辑</span><span type="del">删除</span><span class="jieda-accept" type="accept">采纳</span></div></div></li><!-- 无数据时 --><!-- <li class="fly-none">消灭零回复</li> --></ul><div class="layui-form layui-form-pane"><form action="/jie/reply/" method="post"><div class="layui-form-item layui-form-text"><a name="comment"></a><div class="layui-input-block"><textarea id="L_content" name="content" required lay-verify="required" placeholder="请输入内容"  class="layui-textarea fly-editor" style="height: 150px;"></textarea></div></div><div class="layui-form-item"><input type="hidden" name="jid" value="123"><button class="layui-btn" lay-filter="*" lay-submit>提交回复</button></div></form></div></div></div><#include "../inc/right.ftl"></div></div>
</@layout>

效果

修改提问的页面


修改前端的页面我们可以用我们自己自定义的标签

分页的信息要在PostController 里面设置一下

public class PostController extends BaseController{// 指定值接收 数字类型@GetMapping("/category/{id:\\d*}")public String category(@PathVariable(name = "id") Long id) {int pn = ServletRequestUtils.getIntParameter(request, "start", 1);request.setAttribute("CategoryId", id);request.setAttribute("start", pn);return "post/category";}

效果

处理评论问题


处理这里的评论
编写Postcontroller类

    @GetMapping("/detail/{id:\\d*}")public String detail(@PathVariable(name = "id") Long id) {// 查询数据PostVo postVo =  postService.selectOnePost(new QueryWrapper<MPost>().eq("p.id",id));//断言判断是否被删除Assert.notNull(postVo,"文章已被删除");// 调用 评论的方法 1, 分页 2,文章的id 3, 用户 id 4,排序IPage<CommentVo> results = imCommentService.paging(getPage(),postVo.getId(),null,"created");request.setAttribute("CategoryId",postVo.getCategoryId());request.setAttribute("post",postVo);request.setAttribute("pageData",results);return "post/detail";}

评论的服务类

/*** <p>* 服务实现类* </p>** @author fjj* @since 2021-07-23*/
@Service
public class MCommentServiceImpl extends ServiceImpl<MCommentMapper, MComment> implements IMCommentService {// 导入 Mapper@AutowiredMCommentMapper mCommentMapper;@Overridepublic IPage<CommentVo> paging(Page page, Long postId, Long userid, String order) {return mCommentMapper.selectComment(page, new QueryWrapper<MComment>().eq(postId != null, "post_id", postId).eq(userid != null, "user_id", userid).orderByDesc(order != null, order));}
}

评论的mapper

@Mapper
public interface MCommentMapper extends BaseMapper<MComment> {IPage<CommentVo> selectComment(Page page, @Param(Constants.WRAPPER) QueryWrapper<MComment> orderByDesc);
}

评论的mapper.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.example.springbootblog.mapper.MCommentMapper">
<select id="selectComment" resultType="com.example.springbootblog.vo.CommentVo">SELECTc.*,u.id AS authorId,u.username AS authorName,U.avatar AS authorAvatarFROMm_comment cLEFT JOIN m_user u ON c.user_id = u.id${ew.customSqlSegment}
</select>
</mapper>

CommentVo

@Data
public class CommentVo extends MComment {// 作者的IDprivate Long authorId;//名字private String authorName;private String authorAvatar;
}

前端修改

        <div class="fly-panel detail-box" id="flyReply"><fieldset class="layui-elem-field layui-field-title" style="text-align: center;"><legend>回帖</legend></fieldset><ul class="jieda" id="jieda"><#list pageData.records as comment><li data-id="${comment.id}" class="jieda-daan"><a name="${comment.id}"></a><div class="detail-about detail-about-reply"><a class="fly-avatar" href="/user/${post.authorId}"><img src="${post.authorAvatar}" alt="${post.authorName}"></a><div class="fly-detail-user"><a href="/user/${post.authorId}" class="fly-link"><cite>${post.authorName}</cite></a><#if comment.userId == post.userId><span>(楼主)</span></#if></div><div class="detail-hits"><span>${timeAgo(comment.created)}</span></div></div><div class="detail-body jieda-body photos"><p>${comment.content}</p></div><div class="jieda-reply"><span class="jieda-zan zanok" type="zan"><i class="iconfont icon-zan"></i><em>${comment.voteUp}</em></span><span type="reply"><i class="iconfont icon-svgmoban53"></i>回复</span><div class="jieda-admin"><span type="edit">编辑</span><span type="del">删除</span></div></div></li></#list></ul><@paging pageData></@paging><div class="layui-form layui-form-pane"><form action="/jie/reply/" method="post"><div class="layui-form-item layui-form-text"><a name="comment"></a><div class="layui-input-block"><textarea id="L_content" name="content" required lay-verify="required" placeholder="请输入内容"  class="layui-textarea fly-editor" style="height: 150px;"></textarea></div></div><div class="layui-form-item"><input type="hidden" name="jid" value="${post.id}"><button class="layui-btn" lay-filter="*" lay-submit>提交回复</button></div></form></div></div>

效果

本周热议功能

我写在了我的另一个博客里面,可以点击查看

文章的浏览量功能

当我们刷新页面浏览量应该加1,但是如果一直刷新一直差数据库的话会对数据库造成压力,所以可以先夹缓存里面然后定时添加到数据库
先写入缓存

    @Overridepublic void putViewCount(PostVo postVo) {// 获取到 keyString key = "rank:post:" +postVo.getId();// 先从缓存中拿到我们的viewCountInteger viewCount = (Integer) redisUtil.hget(key, "post:viewCount");// 判断存在不存在,如果存在的话就加1 不存在从实体 加1if (viewCount!=null) {postVo.setViewCount(viewCount +1);} else {postVo.setViewCount(postVo.getViewCount()+1);}// 同步到缓存中redisUtil.hset(key,"post:viewCount",postVo.getViewCount());}

现在可以存到缓存里面了,设置定时的功能

@Component
public class ViewCountSyncTask {@AutowiredRedisUtil redisUtil;@AutowiredIMPostService postService;@AutowiredRedisTemplate redisTemplate;// 设置定时器的时间设置@Scheduled(cron = "* 1-2 0/1 * * ? ")public void task() {// 获取到所有的keySet<String> keys = redisTemplate.keys("rank:post:*");// 创建用来存放的idsArrayList<String> ids = new ArrayList<>();// 遍历所有的key 找到 存放浏览量的 keyfor (String key : keys) {// 判断是否存在 redis 中if (redisUtil.hHasKey(key, "post:viewCount")) {ids.add(key.substring("rank:post:".length()));}}if (ids.isEmpty())return;// 需要更新的阅读量List<MPost> posts = postService.list(new QueryWrapper<MPost>().in("id", ids));// 遍历 并且从 redis 中获取出来posts.stream().forEach(post -> {Integer hget = (Integer) redisUtil.hget("rank:post:" + post.getId(), "post:viewCount");// 添加到 post 里post.setViewCount(hget);});if (posts.isEmpty())return;boolean inSucc = postService.updateBatchById(posts);if (inSucc) {ids.stream().forEach(id -> {redisUtil.hdel("rank:post:" + id, "post:viewCount");});}}
}

记得在主启动类里面添加

集成Shiro完成登录注册功能

注册功能的集成,先把我们的登录注册页面拿过来

login.ftl

<#include "../inc/layout.ftl"/>
<#-- 导入 layout 的标签 -->
<@layout "登录" ><div class="layui-container fly-marginTop"><div class="fly-panel fly-panel-user" pad20><div class="layui-tab layui-tab-brief" lay-filter="user"><ul class="layui-tab-title"><li class="/login">登入</li><li><a href="/register">注册</a></li></ul><div class="layui-form layui-tab-content" id="LAY_ucm" style="padding: 20px 0;"><div class="layui-tab-item layui-show"><div class="layui-form layui-form-pane"><form method="post"><div class="layui-form-item"><label for="L_email" class="layui-form-label">邮箱</label><div class="layui-input-inline"><input type="text" id="L_email" name="email" required lay-verify="required" autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><label for="L_pass" class="layui-form-label">密码</label><div class="layui-input-inline"><input type="password" id="L_pass" name="pass" required lay-verify="required" autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><label for="L_vercode" class="layui-form-label">人类验证</label><div class="layui-input-inline"><input type="text" id="L_vercode" name="vercode" required lay-verify="required" placeholder="请回答后面的问题" autocomplete="off" class="layui-input"></div><div class="layui-form-mid"><span style="color: #c00;">{{d.vercode}}</span></div></div><div class="layui-form-item"><button class="layui-btn" lay-filter="*" lay-submit>立即登录</button><span style="padding-left:20px;"><a href="forget.html">忘记密码?</a></span></div><div class="layui-form-item fly-form-app"><span>或者使用社交账号登入</span><a href="" onclick="layer.msg('正在通过QQ登入', {icon:16, shade: 0.1, time:0})" class="iconfont icon-qq" title="QQ登入"></a><a href="" onclick="layer.msg('正在通过微博登入', {icon:16, shade: 0.1, time:0})" class="iconfont icon-weibo" title="微博登入"></a></div></form></div></div></div></div></div>
</div><script src="../../res/layui/layui.js"></script>
<script>
layui.cache.page = 'user';
</script>
</@layout>

reg.ftl

<#include "../inc/layout.ftl"/>
<#-- 导入 layout 的标签 -->
<@layout "注册" ><div class="layui-container fly-marginTop"><div class="fly-panel fly-panel-user" pad20><div class="layui-tab layui-tab-brief" lay-filter="user"><ul class="layui-tab-title"><li><a href="/login">登入</a></li><li class="/register">注册</li></ul><div class="layui-form layui-tab-content" id="LAY_ucm" style="padding: 20px 0;"><div class="layui-tab-item layui-show"><div class="layui-form layui-form-pane"><form method="post"><div class="layui-form-item"><label for="L_email" class="layui-form-label">邮箱</label><div class="layui-input-inline"><input type="text" id="L_email" name="email"  autocomplete="off" class="layui-input"></div><div class="layui-form-mid layui-word-aux">将会成为您唯一的登入名</div></div><div class="layui-form-item"><label for="L_username" class="layui-form-label">昵称</label><div class="layui-input-inline"><input type="text" id="L_username" name="username"  autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><label for="L_pass" class="layui-form-label">密码</label><div class="layui-input-inline"><input type="password" id="L_pass" name="password"  autocomplete="off" class="layui-input"></div><div class="layui-form-mid layui-word-aux">6到16个字符</div></div><div class="layui-form-item"><label for="L_repass" class="layui-form-label">确认密码</label><div class="layui-input-inline"><input type="password" id="L_repass" name="repass"  autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><label for="L_vercode" class="layui-form-label">人类验证</label><div class="layui-input-inline"><input type="text" id="L_vercode" name="vercode"  placeholder="请填写后面的验证码" autocomplete="off" class="layui-input"></div><div class=""><img src="/captcha.jpg" id="captch"></img></div></div><div class="layui-form-item"><button class="layui-btn" lay-filter="*" lay-submit alert="true">立即注册</button></div><div class="layui-form-item fly-form-app"><span>或者直接使用社交账号快捷注册</span><a href="" onclick="layer.msg('正在通过QQ登入', {icon:16, shade: 0.1, time:0})" class="iconfont icon-qq" title="QQ登入"></a><a href="" onclick="layer.msg('正在通过微博登入', {icon:16, shade: 0.1, time:0})" class="iconfont icon-weibo" title="微博登入"></a></div></form></div></div></div></div></div></div><script src="../../res/layui/layui.js"></script>
<script>
layui.cache.page = 'user';
$("#captch").click(function () {this.src = "/captcha.jpg"
})
</script></@layout>

编写跳转的Controller可以进行页面的跳转

    @GetMapping("/login")public String login() {return "/auth/login";}@GetMapping("/register")public String register() {return "/auth/reg";}

现在可以进行简单的页面的跳转
编写注册功能

    //生成验证码@AutowiredProducer producer;@GetMapping("/captcha.jpg")public void kaptcha(HttpServletResponse response) throws IOException {// 生成验证String text = producer.createText();// 生成图片BufferedImage image = producer.createImage(text);// 设置到session 会话属性request.getSession().setAttribute(KAPTCHA_SESSION, text);// 设置基本的信息response.setHeader("Cache-Control", "no-store,no-cache");response.setContentType("image/jpeg");ServletOutputStream outputStream = response.getOutputStream();ImageIO.write(image, "jpg", outputStream);}

前端获取这个Get路径就可以

验证码成功后开始编写注册的功能。这里有一个需要注意的是如何保证输入的验证码和生成的验证码一样,上面我们先存到了session,等集成shiro 后在修改暂时放到了session里面

@PostMapping("/register")@ResponseBodypublic Result doreg(MUser user, String repass, String vercode) {// 判断检验实体类ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(user);if (validResult.hasErrors()) {return Result.fail(validResult.getErrors());}//判断两次输入的密码是否正确if (!user.getPassword().equals(repass)) {return Result.fail("两次密码不正确");}// 获取到 用户的输入的验证码// 获取到session 会话属性String captcha = (String) request.getSession().getAttribute(KAPTCHA_SESSION);// 判断验证码是否if (vercode == null || !vercode.equalsIgnoreCase(captcha)) {return Result.fail("验证码不一致");}// 注册的方法Result result =userService.register(user);return result.success().action("/login");}

这里使用到了两个工具类,结果集和实体校验的工具类,都是网上找的代码就不复制的
服务类MUserServiceImpl


/*** <p>* 服务实现类* </p>** @author fjj* @since 2021-07-23*/
@Service
public class MUserServiceImpl extends ServiceImpl<MUserMapper, MUser> implements IMUserService {// 注入 Mapper@AutowiredMUserMapper userMapper;@Overridepublic Result register(MUser user) {// 判断是不是唯一的int count = this.count(new QueryWrapper<MUser>().eq("email", user.getEmail()).or().eq("username", user.getUsername()));if (count > 0)Result.fail("用户名或者邮箱已经存在了,建议换个名字啦");// 创建实体类 只写我们需要注册的字段MUser temp = new MUser();temp.setUsername(user.getUsername());// 密码需要MD5 加密temp.setPassword(SecureUtil.md5(user.getPassword()));temp.setEmail(user.getEmail());temp.setCreated(new Date());temp.setPoint(0);temp.setVipLevel(0);temp.setCommentCount(0);temp.setPostCount(0);temp.setAvatar("/res/images/avatar/default.png");this.save(temp);return Result.success();}
}

注册流程结束

登录的流程
前面有登录的login的ftl就不粘贴过来了
Controller 类

    @PostMapping("/login")@ResponseBodypublic Result dologin(String email,String password,String vercode) {// 判断 用户名和密码 是不是为空if (StrUtil.isEmpty(email )|| StrUtil.isBlank(password)) {return Result.fail("不能为空");}// 获取到 tokenUsernamePasswordToken token = new UsernamePasswordToken(email, SecureUtil.md5(password));// 获取到 用户的输入的验证码// 获取到session 会话属性String captcha = (String) request.getSession().getAttribute(KAPTCHA_SESSION);// 判断验证码是否if (vercode == null || !vercode.equalsIgnoreCase(captcha)) {return Result.fail("验证码不一致");}try {SecurityUtils.getSubject().login(token);} catch (AuthenticationException e) {if (e instanceof UnknownAccountException) {return Result.fail("用户不存在");} else if (e instanceof LockedAccountException) {return Result.fail("用户被禁用");} else if (e instanceof IncorrectCredentialsException) {return Result.fail("密码错误");} else {return Result.fail("用户认证失败");}}return Result.success().action("/");}

这里用到了Shiro的所以要配置一下Shiro的配置文件securityManager

@Slf4j
@Configuration
public class ShiroConfig {@Beanpublic SecurityManager securityManager(AccountRealm accountRealm){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(accountRealm);log.info("------------------>securityManager注入成功");return securityManager;}@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();filterFactoryBean.setSecurityManager(securityManager);// 配置登录的url和登录成功的urlfilterFactoryBean.setLoginUrl("/login");filterFactoryBean.setSuccessUrl("/user/center");// 配置未授权跳转页面filterFactoryBean.setUnauthorizedUrl("/error/403");//        filterFactoryBean.setFilters(MapUtil.of("auth", authFilter()));Map<String, String> hashMap = new LinkedHashMap<>();hashMap.put("/res/**", "anon");hashMap.put("/user/home", "auth");hashMap.put("/user/set", "auth");hashMap.put("/user/upload", "auth");hashMap.put("/user/index", "auth");hashMap.put("/user/public", "auth");hashMap.put("/user/collection", "auth");hashMap.put("/user/mess", "auth");hashMap.put("/msg/remove/", "auth");hashMap.put("/message/nums/", "auth");hashMap.put("/collection/remove/", "auth");hashMap.put("/collection/find/", "auth");hashMap.put("/collection/add/", "auth");hashMap.put("/post/edit", "auth");hashMap.put("/post/submit", "auth");hashMap.put("/post/delete", "auth");hashMap.put("/post/reply/", "auth");hashMap.put("/websocket", "anon");hashMap.put("/login", "anon");return filterFactoryBean;}

重写AccountRealm

@Component
public class AccountRealm extends AuthorizingRealm {@AutowiredIMUserService userService;@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {return null;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;// 调用登录的逻辑AccountProfile profile = userService.login(usernamePasswordToken.getUsername(), String.valueOf(usernamePasswordToken.getPassword()));SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(profile, token.getCredentials(), getName());return info;}
}

重写AccountProfile实体类用来反射字段使用的

@Data
public class AccountProfile implements Serializable {private Long id;private String username;private String email;private String sign;private String avatar;private String gender;private Date created;public String getSex() {return "0".equals(gender) ? "女" : "男";}}

最后就是服务类了

 // 登录@Overridepublic AccountProfile login(String email, String password) {MUser user1 = this.getOne(new QueryWrapper<MUser>().eq("email", email));if (user1 == null) {if (user1 == null) {throw new UnknownAccountException();}if (!user1.getPassword().equals(user1.getPassword())) {throw new IncorrectCredentialsException();}user1.setLasted(new Date());this.updateById(user1);}AccountProfile profile = new AccountProfile();BeanUtil.copyProperties(user1,profile);return profile;}

之上所以的登录基本流程 over了

修改登录之后导航栏显示自己信息以及退出登录


上面是登录前的,理想的状态应该是登录后显示每个人的基本信息
这里要用到一个SpringBoot 跟Shiro的一个标签
主要的标签博客
修改我们的header.ftl的页面
未登录用这个标签

登录之后的


效果



Controller

    // 退出登录页面@RequestMapping("/logout")public String logout() {// 清楚 sessionSecurityUtils.getSubject().logout();return "redirect:/";}

基本设置功能

个人主页问题

先把写Controller 类

@Controller
public class UserController extends BaseController {@GetMapping("/user/home")public String home() {// 获取到当前的 userMUser user = userService.getById(getProfilrId());// 获取到 该用户的基本信息List<MPost> posts = postService.list(new QueryWrapper<MPost>().eq("user_id", getProfilrId())// 获取 30 天内的文章.gt("created", DateUtil.lastMonth()).orderByDesc("created"));request.setAttribute("user",user);request.setAttribute("posts",posts);// 设置到缓存return "/user/home";}
}

事先在BaseController 里面写入获取到Shiro id的方法

    // 获取到 shiro 里面的 用户 idpublic AccountProfile getProfile () {return (AccountProfile) SecurityUtils.getSubject().getPrincipal();}protected Long getProfilrId () {return getProfile().getId();}

前端页面

<#include "../inc/layout.ftl" /><@layout "我的主页"><div class="fly-home fly-panel" style="background-image: url();"><img src="${user.avatar}" alt="${user.username}"><i class="iconfont icon-renzheng" title="Fly社区认证"></i><h1>${user.username}<i class="iconfont icon-nan"></i><!-- <i class="iconfont icon-nv"></i>  --><i class="layui-badge fly-badge-vip">VIP3</i><!--<span style="color:#c00;">(管理员)</span><span style="color:#5FB878;">(社区之光)</span><span>(该号已被封)</span>--></h1><p class="fly-home-info">
<#--      <i class="iconfont icon-kiss" title="飞吻"></i><span style="color: #FF7200;">66666 飞吻</span>--><i class="iconfont icon-shijian"></i><span> ${timeAgo(user.created)}加入</span><i class="iconfont icon-chengshi"></i><span>${user.address}</span></p><p class="fly-home-sign">(${user.sign!'这个人好懒,什么都没留下!'})</p><#--    <div class="fly-sns" data-user="">-->
<#--      <a href="javascript:;" class="layui-btn layui-btn-primary fly-imActive" data-type="addFriend">加为好友</a>-->
<#--      <a href="javascript:;" class="layui-btn layui-btn-normal fly-imActive" data-type="chat">发起会话</a>-->
<#--    </div>--></div><div class="layui-container"><div class="layui-row layui-col-space15"><div class="layui-col-md6 fly-home-jie"><div class="fly-panel"><h3 class="fly-panel-title">${user.username} 最近的提问</h3><ul class="jie-row"><#list posts as post><li><#if post.recommend><span class="fly-jing">精</span></#if><a href="/post/${post.id}" class="jie-title"> ${post.title}</a><i>${timeAgo(post.created)}</i><em class="layui-hide-xs">${post.viewCount}阅/${post.commentCount}答</em></li></#list><#if posts??><div class="fly-none" style="min-height: 50px; padding:30px 0; height:auto;"><i style="font-size:14px;">没有发表任何求解</i></div></#if></ul></div></div><div class="layui-col-md6 fly-home-da"><div class="fly-panel"><h3 class="fly-panel-title">${user.username} 最近的回答</h3><ul class="home-jieda"><div class="fly-none" style="min-height: 50px; padding:30px 0; height:auto;"><span>没有回答任何问题</span></div></ul></div></div></div></div></@layout>

效果

基本设置功能

先修改我们的页面,把Set.ftl 页面复制过来

<#include "../inc/layout.ftl" /><@layout "基本设置"><div class="layui-container fly-marginTop fly-user-main"><ul class="layui-nav layui-nav-tree layui-inline" lay-filter="user"><li class="layui-nav-item"><a href="/user/home"><i class="layui-icon">&#xe609;</i>我的主页</a></li>
<#--      <li class="layui-nav-item">-->
<#--        <a href="index.html">-->
<#--          <i class="layui-icon">&#xe612;</i>-->
<#--          用户中心-->
<#--        </a>-->
<#--      </li>--><li class="layui-nav-item layui-this"><a href="/user/set"><i class="layui-icon">&#xe620;</i>基本设置</a></li><li class="layui-nav-item"><a href="/user/mess"><i class="layui-icon">&#xe611;</i>我的消息</a></li></ul><div class="site-tree-mobile layui-hide"><i class="layui-icon">&#xe602;</i></div><div class="site-mobile-shade"></div><div class="site-tree-mobile layui-hide"><i class="layui-icon">&#xe602;</i></div><div class="site-mobile-shade"></div><div class="fly-panel fly-panel-user" pad20><div class="layui-tab layui-tab-brief" lay-filter="user"><ul class="layui-tab-title" id="LAY_mine"><li class="layui-this" lay-id="info">我的资料</li><li lay-id="avatar">头像</li><li lay-id="pass">密码</li></ul><div class="layui-tab-content" style="padding: 20px 0;"><div class="layui-form layui-form-pane layui-tab-item layui-show"><form method="post"><div class="layui-form-item"><label for="L_email" class="layui-form-label">邮箱</label><div class="layui-input-inline"><input type="text" id="L_email" name="email" required lay-verify="email" autocomplete="off" value="${user.email}" class="layui-input" readonly></div><div class="layui-form-mid layui-word-aux">如果您在邮箱已激活的情况下,变更了邮箱,需<a href="activate.html" style="font-size: 12px; color: #4f99cf;">重新验证邮箱</a>。</div></div><div class="layui-form-item"><label for="L_username" class="layui-form-label">昵称</label><div class="layui-input-inline"><input type="text" id="L_username" name="username" required lay-verify="required" value="${user.username}" autocomplete="off" value="" class="layui-input"></div><div class="layui-inline"><div class="layui-input-inline"><input type="radio" name="sex" value="0" <#if user.gender =='0'>checked</#if> title="男"><input type="radio" name="sex" value="1" <#if user.gender =='1'>checked</#if> title="女"></div></div></div><div class="layui-form-item"><label for="L_city" class="layui-form-label">城市</label><div class="layui-input-inline"><input type="text" id="L_city" name="address" autocomplete="off" value="${user.address}" class="layui-input"></div></div><div class="layui-form-item layui-form-text"><label for="L_sign" class="layui-form-label">签名</label><div class="layui-input-block"><textarea placeholder="随便写些什么刷下存在感" id="L_sign"  name="sign" autocomplete="off" class="layui-textarea" style="height: 80px;">${user.sign}</textarea></div></div><div class="layui-form-item"><button class="layui-btn" key="set-mine" lay-filter="*" lay-submit alert="true" reload="true">确认修改</button></div></form></div><div class="layui-form layui-form-pane layui-tab-item"><div class="layui-form-item"><div class="avatar-add"><p>建议尺寸168*168,支持jpg、png、gif,最大不能超过50KB</p><button type="button" class="layui-btn upload-img"><i class="layui-icon">&#xe67c;</i>上传头像</button><img src="https://tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg"><span class="loading"></span></div></div></div><div class="layui-form layui-form-pane layui-tab-item"><form action="/user/repass" method="post"><div class="layui-form-item"><label for="L_nowpass" class="layui-form-label">当前密码</label><div class="layui-input-inline"><input type="password" id="L_nowpass" name="nowpass" required lay-verify="required" autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><label for="L_pass" class="layui-form-label">新密码</label><div class="layui-input-inline"><input type="password" id="L_pass" name="pass" required lay-verify="required" autocomplete="off" class="layui-input"></div><div class="layui-form-mid layui-word-aux">6到16个字符</div></div><div class="layui-form-item"><label for="L_repass" class="layui-form-label">确认密码</label><div class="layui-input-inline"><input type="password" id="L_repass" name="repass" required lay-verify="required" autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><button class="layui-btn" key="set-mine" lay-filter="*" lay-submit>确认修改</button></div></form></div></div></div></div></div><script>layui.cache.page = 'user';</script>
</@layout>

修改后端的Controller类

 // 基本设置@GetMapping ("/user/set")public String set () {// 获取到当前的 userMUser user1 = userService.getById(getProfilrId());request.setAttribute("user", user1);return "/user/set";}@PostMapping("/user/set")@ResponseBodypublic Result set(MUser user) {//判断名字是否为空if (StrUtil.isBlank(user.getUsername())) {return Result.fail("昵称为空了");}// 判断 是否名字被占用了int count = userService.count(new QueryWrapper<MUser>().eq("username", getProfile().getUsername()).ne("id", getProfilrId()));if (count >0) {return Result.fail("名字已经存在");}MUser temp = userService.getById(getProfilrId());temp.setUsername(user.getUsername());temp.setGender(user.getGender());temp.setSign(user.getSign());temp.setAddress(user.getAddress());userService.updateById(temp);AccountProfile profile = getProfile();profile.setAddress(temp.getAddress());profile.setUsername(temp.getUsername());profile.setAvatar(temp.getAvatar());profile.setGender(temp.getGender());profile.setSign(temp.getSign());return Result.success().action("/user/set#info");}

效果

上传头像的功能

前端页面

          <div class="layui-form layui-form-pane layui-tab-item"><div class="layui-form-item"><div class="avatar-add"><p>建议尺寸168*168,支持jpg、png、gif,最大不能超过50KB</p><button type="button" class="layui-btn upload-img"><i class="layui-icon">&#xe67c;</i>上传头像</button><img src="${user.avatar}"><span class="loading"></span></div></div></div>

需要用到上传工具类

@Slf4j
@Component
public class UploadUtil {@AutowiredConsts consts;public final static String type_avatar = "avatar";public Result upload(String type, MultipartFile file) throws IOException {if(StrUtil.isBlank(type) || file.isEmpty()) {return Result.fail("上传失败");}// 获取文件名String fileName = file.getOriginalFilename();log.info("上传的文件名为:" + fileName);// 获取文件的后缀名String suffixName = fileName.substring(fileName.lastIndexOf("."));log.info("上传的后缀名为:" + suffixName);// 文件上传后的路径String filePath = consts.getUploadDir();if ("avatar".equalsIgnoreCase(type)) {AccountProfile profile = (AccountProfile) SecurityUtils.getSubject().getPrincipal();fileName = "/avatar/avatar_" + profile.getId() + suffixName;} else if ("post".equalsIgnoreCase(type)) {fileName = "/post/post_" + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN) + suffixName;}File dest = new File(filePath + fileName);// 检测是否存在目录if (!dest.getParentFile().exists()) {dest.getParentFile().mkdirs();}try {file.transferTo(dest);log.info("上传成功后的文件路径未:" + filePath + fileName);String path = filePath + fileName;String url = "/upload" + fileName;log.info("url ---> {}", url);return Result.success(url);} catch (IllegalStateException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return Result.success(null);}}

控制层

 @PostMapping("/user/set")@ResponseBodypublic Result set(MUser user) {// 这里写上传头像的路径 由于layui 的js 封装的 /user/setif (StrUtil.isNotBlank(user.getAvatar())) {// 获取到当前用户MUser user1 = userService.getById(getProfilrId());user1.setAvatar(user.getAvatar());userService.updateById(user1);AccountProfile profile = getProfile();profile.setAvatar(user.getAvatar());return Result.success().action("/user/set#avatar");}//判断名字是否为空if (StrUtil.isBlank(user.getUsername())) {return Result.fail("昵称为空了");}// 判断 是否名字被占用了int count = userService.count(new QueryWrapper<MUser>().eq("username", getProfile().getUsername()).ne("id", getProfilrId()));if (count > 0) {return Result.fail("名字已经存在");}MUser temp = userService.getById(getProfilrId());temp.setUsername(user.getUsername());temp.setGender(user.getGender());temp.setSign(user.getSign());temp.setAddress(user.getAddress());userService.updateById(temp);AccountProfile profile = getProfile();profile.setAddress(temp.getAddress());profile.setUsername(temp.getUsername());profile.setAvatar(temp.getAvatar());profile.setGender(temp.getGender());profile.setSign(temp.getSign());return Result.success().action("/user/set#info");}// 上传头像@PostMapping("/user/upload")@ResponseBodypublic Result upload(@RequestParam(value = "file") MultipartFile file) throws Exception {return uploadUtil.upload(UploadUtil.type_avatar, file);}

js 是layui 封装好的

效果

修改密码的功能

前端页面

            <form action="/user/repass" method="post"><div class="layui-form-item"><label for="L_nowpass" class="layui-form-label">当前密码</label><div class="layui-input-inline"><input type="password" id="L_nowpass" name="nowpass" required lay-verify="required" autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><label for="L_pass" class="layui-form-label">新密码</label><div class="layui-input-inline"><input type="password" id="L_pass" name="pass" required lay-verify="required" autocomplete="off" class="layui-input"></div><div class="layui-form-mid layui-word-aux">6到16个字符</div></div><div class="layui-form-item"><label for="L_repass" class="layui-form-label">确认密码</label><div class="layui-input-inline"><input type="password" id="L_repass" name="repass" required lay-verify="required" autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><button class="layui-btn" key="set-mine" lay-filter="*" lay-submit alert="true" reload="true">确认修改</button></div></form>

控制层

   // 修改 密码@ResponseBody@PostMapping("/user/repass")public Result repass(String nowpass, String pass, String repass) {if(!pass.equals(repass)) {return Result.fail("两次密码不相同");}MUser user = userService.getById(getProfilrId());String nowPassMd5 = SecureUtil.md5(nowpass);if(!nowPassMd5.equals(user.getPassword())) {return Result.fail("密码不正确");}user.setPassword(SecureUtil.md5(pass));userService.updateById(user);return Result.success().action("/user/set#pass");}

用户中心发表的贴和收藏的贴

这里的分页用流的形式做,参考一下layui官网

前端的页面

<#include "../inc/layout.ftl" /><@layout "用户中心"><div class="layui-container fly-marginTop fly-user-main"><@centerLeft level=1></@centerLeft><div class="site-tree-mobile layui-hide"><i class="layui-icon">&#xe602;</i></div><div class="site-mobile-shade"></div><div class="site-tree-mobile layui-hide"><i class="layui-icon">&#xe602;</i></div><div class="site-mobile-shade"></div><div class="fly-panel fly-panel-user" pad20><!--<div class="fly-msg" style="margin-top: 15px;">您的邮箱尚未验证,这比较影响您的帐号安全,<a href="activate.html">立即去激活?</a></div>--><div class="layui-tab layui-tab-brief" lay-filter="user"><ul class="layui-tab-title" id="LAY_mine"><li data-type="mine-jie" lay-id="index" class="layui-this">我发的帖(<span>89</span>)</li><li data-type="collection" data-url="/collection/find/" lay-id="collection">我收藏的帖(<span>16</span>)</li></ul><div class="layui-tab-content" style="padding: 20px 0;"><div class="layui-tab-item layui-show"><ul class="mine-view jie-row" id="fabu"><script id="tpl-fabu" type="text/html"><li><a class="jie-title" href="/detail/{{d.id}}" target="_blank">{{ d.title }}</a><i>{{layui.util.toDateString(d.created, 'yyyy-MM-dd HH:mm:ss')}}</i><a class="mine-edit" href="/post/edit?id={{d.id}}">编辑</a><em>{{d.viewCount}}阅/{{d.commentCount}}答</em></li></script></ul><div id="LAY_page"></div></div><div class="layui-tab-item"><ul class="mine-view jie-row" id="collection"><script id="tpl-collection" type="text/html"><li><a class="jie-title" href="/detail/{{d.id}}" target="_blank">{{d.title}}</a><i>收藏于{{layui.util.timeAgo(d.created, true)}}</i></li></script></ul><div id="LAY_page1"></div></div></div></div></div></div><script>layui.cache.page = 'user';layui.use(['laytpl', 'flow', 'util'], function() {var $ = layui.jquery;var laytpl = layui.laytpl;var flow = layui.flow;var util = layui.util;flow.load({elem: '#fabu' //指定列表容器,isAuto: false,done: function(page, next){var lis = [];$.get('/user/public?start='+page, function(res){layui.each(res.data.records, function(index, item){var tpl = $("#tpl-fabu").html();laytpl(tpl).render(item, function (html) {$("#fabu .layui-flow-more").before(html);});});next(lis.join(''), page < res.data.pages);});}});flow.load({elem: '#collection',isAuto: false,done: function(page, next){var lis = [];$.get('/user/collection?pn='+page, function(res){layui.each(res.data.records, function(index, item){var tpl = $("#tpl-collection").html();laytpl(tpl).render(item, function (html) {$("#collection .layui-flow-more").before(html);});});next(lis.join(''), page < res.data.pages);});}});});</script>
</@layout>

后端的Controller

 // 发表的文章@GetMapping("/user/public")@ResponseBodypublic Result PublicFaBu() {// 获取到分页信息IPage page = postService.page(getPage(), new QueryWrapper<MPost>().eq("user_id", getProfilrId()).orderByDesc("created"));return Result.success(page);}// 收藏的帖子@GetMapping("/user/collection")@ResponseBodypublic Result Collection() {IPage page = postService.page(getPage(), new QueryWrapper<MPost>().inSql("id", "select post_id from m_user_collection where user_id = " + getProfilrId()));return Result.success(page);}

效果

消息中心

我的消息先导入前端的页面

<#include "../inc/layout.ftl" /><@layout "用户中心"><div class="layui-container fly-marginTop fly-user-main"><@centerLeft level=3></@centerLeft><div class="site-tree-mobile layui-hide"><i class="layui-icon">&#xe602;</i></div><div class="site-mobile-shade"></div><div class="site-tree-mobile layui-hide"><i class="layui-icon">&#xe602;</i></div><div class="site-mobile-shade"></div><div class="fly-panel fly-panel-user" pad20><div class="layui-tab layui-tab-brief" lay-filter="user" id="LAY_msg" style="margin-top: 15px;"><button class="layui-btn layui-btn-danger" id="LAY_delallmsg">清空全部消息</button><div  id="LAY_minemsg" style="margin-top: 10px;"><!--<div class="fly-none">您暂时没有最新消息</div>--><ul class="mine-msg"><#list pageData.records as mess><li data-id="${mess.id}"><blockquote class="layui-elem-quote"><#if mess.type == 0>系统消息:${mess.content}</#if><#if mess.type == 1>${mess.fromUserName} 评论了你的文章 <${mess.postTitle}>,内容是 (${mess.content})</#if><#if mess.type == 2>${mess.fromUserName} 回复了你的评论 (${mess.content}),文章是 <${mess.postTitle}></#if></blockquote><p><span>${timeAgo(mess.creted)}</span><a href="javascript:;" class="layui-btn layui-btn-small layui-btn-danger fly-delete">删除</a></p></li></#list></ul><@paging pageData></@paging></div></div></div></div><script>layui.cache.page = 'user';</script>
</@layout>

开始写Controller这里我们用的自己的方法,所以要自己写sql

// 基本设置消息@GetMapping("/user/mess")public String Mess1() {IPage page = userMessageService.pageing(getPage(), new QueryWrapper<MUserMessage>().eq("to_user_id", getProfilrId()).orderByAsc("created"));request.setAttribute("pageData", page);return "/user/mess";}

pageing方法

/*** <p>*  服务实现类* </p>** @author fjj* @since 2021-07-23*/
@Service
public class MUserMessageServiceImpl extends ServiceImpl<MUserMessageMapper, MUserMessage> implements IMUserMessageService {@AutowiredMUserMessageMapper mUserMessageMapper;@Overridepublic IPage pageing(Page page, QueryWrapper<MUserMessage> wrapper) {return mUserMessageMapper.selectMessages(page,wrapper);}
}

Mapperxml

<?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.example.springbootblog.mapper.MUserMessageMapper">
<select id="selectMessages" resultType="com.example.springbootblog.vo.UserMessageVo">SELECTm.*, (SELECTusernameFROM`m_user`WHEREid = m.from_user_id) AS fromUserName,(SELECTtitleFROM`m_post`WHEREid = m.post_id) AS postTitleFROM`m_user_message` m${ew.customSqlSegment}
</select>
</mapper>

一定要记得在Mapper 里面写上一个注解

/*** <p>*  Mapper 接口* </p>** @author fjj* @since 2021-07-23*/
@Mapper
public interface MUserMessageMapper extends BaseMapper<MUserMessage> {IPage<UserMessageVo> selectMessages(Page page, @Param(Constants.WRAPPER) QueryWrapper<MUserMessage> wrapper);
}

因为可能原来的实体类没有办法帮我们映射完所以新写了一个VO


@Data
public class UserMessageVo extends MUserMessage {private String toUserName;private String fromUserName;private String postTitle;private String commentContent;
}

效果

删除消息功能Controller

    // 删除消息功能@PostMapping("/msg/remove/")@ResponseBodypublic Result msgRemove(Long id, @RequestParam(defaultValue = "false") boolean all) {boolean remove = userMessageService.remove(new QueryWrapper<MUserMessage>().eq("to_user_id", getProfilrId()).eq(!all, "id", id));return remove ? Result.success(null) : Result.fail("删除失败了哦!");}

下面的内容地址

续篇

基于Springboot搭建个人博客 (学习笔记)相关推荐

  1. Hexo 搭建个人博客学习笔记(5):个性化主题Next配置站内搜索

    使用hexo-generator-searchdb插件,实现博客站内搜索 版本 Hexo 版本:5.0.x Next 版本:7.8.0 hexo-generator-searchdb 版本:1.3.2 ...

  2. 基于springboot+vue个人博客搭建

    目录 博客介绍 源码地址:springboot+vue个人博客系统: 基于springboot+vue个人博客系统 在线地址 目录结构 ​编辑 项目特点 技术介绍 开发环境 项目截图 注意事项: 项目 ...

  3. 传智博客学习笔记8--面向对象下

    传智博客学习笔记8--面向对象下 2009-3-12 15:41:42 继承 extends 接口 interface 如果一个抽象类中的所有方法都是抽象的,我们就可以将这个类用另外一种方式来定义,也 ...

  4. 【SpringBoot搭建个人博客】- 相册管理(九)

    博客地址:ONESTARの客栈 源码领取方式一: 扫一扫文末二维码,关注公众号[编程日刊],后台回复[博客],即可领取源码 源码领取方式二: 前端页面源码地址:https://github.com/o ...

  5. 【SpringBoot搭建个人博客】- 博客首页显示(十)

    博客地址:ONESTARの客栈 源码领取方式一: 扫一扫文末二维码,关注公众号[编程日刊],后台回复[博客],即可领取源码 源码领取方式二: 前端页面源码地址:https://github.com/o ...

  6. Springboot搭建个人博客系列

    前言 为什么想要搭建这个博客? 我还记得,在大二寒假的某天,同往常一样的在家解决这某个bug,不停地问度娘,很巧的碰到了一个同行在他的博客中完美的记录了我的bug的解决方案,随后我又看了看他写的其他博 ...

  7. 基于hexo搭建个人博客

    hexo搭建个人博客 百金买骏马,千金买美人:万金买高爵,何处买青春? -- 屈原 个人博客:Ronetver-Blog 概述 1. 安装git (因为git很早之前已安装,本文在这里就不说明了) 2 ...

  8. 基于springboot框架的博客系统

    1   概览 1.1  文档目的 利用该文档可以搭建一套博客的在线系统,其中包括博客首页以及博客管理等后台系统,可以实现博客预览,博客发布,博客管理以及其他管理功能,来满足工作或者学习上的需要. 1. ...

  9. 基于 Vuepress 搭建个人博客,文章详细,助你快速上手

    搭建博客 几年前在小白阶段使用过 WordPress + 阿里云服务器 搭建了自己的博客,也走了很多坑,从零摸坑最后也弄好了,也顺便使用了阿里云的一些服务.但当时没有记录笔记的习惯,过了好久很多操作也 ...

最新文章

  1. [转]23种经典设计模式的java实现_5_职责链模式
  2. 对于电单车同学们提出的改动建议
  3. OpenCV中SiftDescriptorExtractor
  4. 【Flask】Flask-RESTful 风格编程
  5. Pool多进程的加速
  6. 公共钥匙盒(排序CCF)
  7. SkyWalking学习笔记(CentOS环境)
  8. 在C#中实现托盘是多么简单
  9. vivo S10系列官方渲染图公布 外壳太好看了!
  10. php auth和rbac区别,THINKPHP中的AUTH权限管理介绍
  11. vue 对象继承_JS面向对象—对象的继承
  12. 绕过SQL注入限制的方法
  13. EasyUI treegrid 获取编辑状态中某字段的值 [getEditor方法获取不到editor]
  14. hbase数据库详解
  15. 内蒙古自治区及其盟市行政单位中英文名称对照表
  16. UnityShader 浮雕凹凸贴图BumpMap与法线贴图NormalMap的原理及其区别
  17. 面试官再问分布式事务,求你看完这份至尊级分布式笔记,给年轻的面试官上一课
  18. BUUCTF-[安洵杯 2019]easy_web1
  19. 《Loy解说SpringBoot的注解》
  20. pandas学习task05变形

热门文章

  1. mac framework
  2. IPv6大航海,风帆指向强应用
  3. McAfee评论:McAfee很好吗?
  4. 安卓沉浸式状态栏_《宫廷秘传》今日安卓计费删档开测
  5. 树莓派3卡片电脑,指尖上的智慧
  6. (转)视觉工程师笔试知识汇总
  7. java 时间戳验证_时间戳校验问题
  8. 【解救Ros】CLB的Ros小车imu校正,角速度校正,线速度校正过程
  9. 16Python文本数据分析:新闻分类任务 (贝叶斯算法应用实例)
  10. Flex入门基础——项目item的属性(order、flex-grow、flex-shrink、flex-basis、flex、align-self)