赞
踩
1. 了解博客系统的系统功能和文件组织结构
2. 掌握前后台管理功能的实现
3. 掌握用户登录、定时邮件发送的功能实现
4. 熟悉博客系统数据库相关表及字段的设计、环境搭建的步骤及相关配置
1、数据库实现
2、系统环境搭建
3、文章分页展示
4、文章详情查看
5、文章评论管理
6、数据展示
7、文章发布
8、文章修改
9、文章删除
10、用户登录控制
11、定时邮件发送
1、准备项目环境
(1)创建项目,引入依赖文件
创建一个名称为blog_system的Spring Boot项目,选择Web模块
在pom.xml中添加
- <dependencies>
- <!-- 阿里巴巴的Druid数据源依赖启动器 -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>1.1.10</version>
- </dependency>
- <!-- MyBatis依赖启动器 -->
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- <version>2.0.0</version>
- </dependency>
- <!-- MySQL数据库连接驱动 -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-j</artifactId>
- <scope>runtime</scope>
- </dependency>
- <!-- Redis服务启动器 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- <!-- mail邮件服务启动器 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-mail</artifactId>
- </dependency>
- <!-- thymeleaf模板整合security控制页面安全访问依赖 -->
- <dependency>
- <groupId>org.thymeleaf.extras</groupId>
- <artifactId>thymeleaf-extras-springsecurity5</artifactId>
- </dependency>
- <!-- Spring Security依赖启动器 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
- <!-- Thymeleaf模板引擎启动器 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
- <!-- Web服务启动器 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!-- MyBatis分页插件 -->
- <dependency>
- <groupId>com.github.pagehelper</groupId>
- <artifactId>pagehelper-spring-boot-starter</artifactId>
- <version>1.2.8</version>
- </dependency>
- <!-- String工具类包-->
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
- <version>3.5</version>
- </dependency>
- <!-- Markdown处理html -->
- <dependency>
- <groupId>com.atlassian.commonmark</groupId>
- <artifactId>commonmark</artifactId>
- <version>0.11.0</version>
- </dependency>
- <!-- Markdown处理表格 -->
- <dependency>
- <groupId>com.atlassian.commonmark</groupId>
- <artifactId>commonmark-ext-gfm-tables</artifactId>
- <version>0.11.0</version>
- </dependency>
- <!-- 过滤emoji表情字符 -->
- <dependency>
- <groupId>com.vdurmont</groupId>
- <artifactId>emoji-java</artifactId>
- <version>4.0.0</version>
- </dependency>
- <!-- devtools热部署工具 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-devtools</artifactId>
- <scope>runtime</scope>
- </dependency>
- <!-- Spring Boot测试服务启动器 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
(2)编写配置文件
将application.properties全局配置文件更名为application.yml
- server:
- port: 80
- spring:
- profiles:
- # 外置jdbc、redis和mail配置文件
- active: jdbc,redis,mail
- # 关闭thymeleaf页面缓存
- thymeleaf:
- cache: false
- main:
- allow-circular-references: true
- # 配置国际化资源文件
- messages:
- basename: i18n.logo
- # MyBatis配置
- mybatis:
- configuration:
- #开启驼峰命名匹配映射
- map-underscore-to-camel-case: true
- #配置MyBatis的xml映射文件路径
- mapper-locations: classpath:mapper/*.xml
- #配置XML映射文件中指定的实体类别名路径
- type-aliases-package: com.lg.ch10.model.domain
- #pagehelper分页设置
- pagehelper:
- helper-dialect: mysql
- reasonable: true
- support-methods-arguments: true
- params: count=countSql
- #浏览器cookie相关设置
- COOKIE:
- # 设置cookie默认时长为30分钟
- VALIDITY: 1800
(3)前端资源引入
(4)后端基础代码导入
(5)在resources文件夹下创建application-jdbc.properties配置文件
- #添加并配置第三方数据库连接池druid
- spring.datasource.type = com.alibaba.druid.pool.DruidDataSource
- spring.datasource.initialSize=20
- spring.datasource.minIdle=10
- spring.datasource.maxActive=100
-
- #数据源连接配置
- spring.datasource.url = jdbc:mysql://localhost:3306/blog_system?serverTimezone=UTC&useSSL=false
- spring.datasource.username = root
- spring.datasource.password = 1234
(6)编写redis配置文件
在resources文件夹下application-redis.properties
- # Redis服务器地址,另外注意要开启Redis服务
- spring.redis.host=127.0.0.1
- # Redis服务器连接端口
- spring.redis.port=6379
- # Redis服务器连接密码(默认为空)
- spring.redis.password=
- # 连接池最大连接数(使用负值表示没有限制)
- spring.redis.jedis.pool.max-active=8
- # 连接池最大阻塞等待时间(使用负值表示没有限制)
- spring.redis.jedis.pool.max-wait=-1
- # 连接池中的最大空闲连接
- spring.redis.jedis.pool.max-idle=8
(7)编写mail配置文件
- 在resources文件夹下application-mail.properties
- #QQ邮箱邮件发送服务配置
- spring.mail.host=smtp.qq.com
- spring.mail.port=587
- # 配置个人QQ账户和密码(密码是加密后的授权码)
- spring.mail.username=1847147914@qq.com
- spring.mail.password=xwfsocuuhxqhiaib
(8)数据资源引入
在MySQL数据库中新建blog_system数据库并将数据库文件导入
2.文章分页展示
(1)数据访问层实现
① 在java\com.lg.ch10中创建dao包并在其中创建Dao层接口文件
ArticleMApper.java和StatisticMapper.java
ArticleMApper.java
- import com.lg.ch10.model.domain.Article;
- import org.apache.ibatis.annotations.*;
- import java.util.List;
-
- @Mapper
- public interface ArticleMApper {
- // 根据id查询文章信息
- @Select("SELECT * FROM t_article WHERE id=#{id}")
- public Article selectArticleWithId(Integer id);
-
- // 发表文章,同时使用@Options注解获取自动生成的主键id
- @Insert("INSERT INTO t_article (title,created,modified,tags,categories," +
- " allow_comment, thumbnail, content)" +
- " VALUES (#{title},#{created}, #{modified}, #{tags}, #{categories}," +
- " #{allowComment}, #{thumbnail}, #{content})")
- @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
- public Integer publishArticle(Article article);
-
- // 文章发分页查询
- @Select("SELECT * FROM t_article ORDER BY id DESC")
- public List<Article> selectArticleWithPage();
-
- // 通过id删除文章
- @Delete("DELETE FROM t_article WHERE id=#{id}")
- public void deleteArticleWithId(int id);
-
- // 站点服务统计,统计文章数量
- @Select("SELECT COUNT(1) FROM t_article")
- public Integer countArticle();
-
- // 通过id更新文章
- public Integer updateArticleWithId(Article article);
- }
StatisticMapper.java
- import com.lg.ch10.model.domain.Article;
- import com.lg.ch10.model.domain.Statistic;
- import org.apache.ibatis.annotations.*;
- import java.util.List;
- @Mapper
- public interface StatisticMapper {
- // 新增文章对应的统计信息
- @Insert("INSERT INTO t_statistic(article_id,hits,comments_num) values (#{id},0,0)")
- public void addStatistic(Article article);
-
- // 根据文章id查询点击量和评论量相关信息
- @Select("SELECT * FROM t_statistic WHERE article_id=#{articleId}")
- public Statistic selectStatisticWithArticleId(Integer articleId);
-
- // 通过文章id更新点击量
- @Update("UPDATE t_statistic SET hits=#{hits} " +
- "WHERE article_id=#{articleId}")
- public void updateArticleHitsWithId(Statistic statistic);
-
- // 通过文章id更新评论量
- @Update("UPDATE t_statistic SET comments_num=#{commentsNum} " +
- "WHERE article_id=#{articleId}")
- public void updateArticleCommentsWithId(Statistic statistic);
-
- // 根据文章id删除统计数据
- @Delete("DELETE FROM t_statistic WHERE article_id=#{aid}")
- public void deleteStatisticWithId(int aid);
-
- // 统计文章热度信息
- @Select("SELECT * FROM t_statistic WHERE hits !='0' " +
- "ORDER BY hits DESC, comments_num DESC")
- public List<Statistic> getStatistic();
-
- // 统计博客文章总访问量
- @Select("SELECT SUM(hits) FROM t_statistic")
- public long getTotalVisit();
-
- // 统计博客文章总评论量
- @Select("SELECT SUM(comments_num) FROM t_statistic")
- public long getTotalComment();
- }
② 在resources中创建名为mapper的包并在该包中创建ArticleMapper.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.lg.ch10.dao.ArticleMapper">
- <update id="updateArticleWithId" parameterType="Article">
- update t_article
- <set>
- <if test="title != null">
- title = #{title},
- </if>
- <if test="created != null">
- created = #{created},
- </if>
- <if test="modified != null">
- modified = #{modified},
- </if>
- <if test="tags != null">
- tags = #{tags},
- </if>
- <if test="categories != null">
- categories = #{categories},
- </if>
- <if test="hits != null">
- hits = #{hits},
- </if>
- <if test="commentsNum != null">
- comments_num = #{commentsNum},
- </if>
- <if test="allowComment != null">
- allow_comment = #{allowComment},
- </if>
- <if test="thumbnail != null">
- thumbnail = #{thumbnail},
- </if>
- <if test="content != null">
- content = #{content},
- </if>
- </set>
- where id = #{id}
- </update>
- </mapper>
(2)业务处理层实现
① 创建Service层接口文件
在com.lg.ch10中创建service包并在其中创建IArticleService.java
- import com.github.pagehelper.PageInfo;
-
- import java.util.List;
-
- public interface IArticleService {
- // 分页查询文章列表
- public PageInfo<com.lg.ch10.model.domain.Article> selectArticleWithPage(Integer page, Integer count);
-
- // 统计前10的热度文章信息
- public List<com.lg.ch10.model.domain.Article> getHeatArticles();
- }
② 创建Service下impl包层接口实现类文件ArticleServiceImpl.java
- import com.github.pagehelper.PageHelper;
- import com.github.pagehelper.PageInfo;
- import com.lg.ch10.dao.ArticleMapper;
- import com.lg.ch10.dao.StatisticMapper;
- import com.lg.ch10.model.domain.Article;
- import com.lg.ch10.model.domain.Statistic;
- import com.lg.ch10.service.IArticleService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.util.ArrayList;
- import java.util.List;
-
- @Service
- @Transactional
- public class ArticleServiceImpl implements IArticleService {
- @Autowired
- private ArticleMapper articleMapper;
- @Autowired
- private StatisticMapper statisticMapper;
- // 分页查询文章列表
- @Override
- public PageInfo<Article> selectArticleWithPage(Integer page, Integer count) {
- PageHelper.startPage(page, count);
- List<Article> articleList = articleMapper.selectArticleWithPage();
- // 封装文章统计数据
- for (int i = 0; i < articleList.size(); i++) {
- Article article = articleList.get(i);
- Statistic statistic = statisticMapper.selectStatisticWithArticleId(article.getId());
- article.setHits(statistic.getHits());
- article.setCommentsNum(statistic.getCommentsNum());
- }
- PageInfo<Article> pageInfo=new PageInfo<>(articleList);
- return pageInfo;
- }
-
- // 统计前10的热度文章信息
- @Override
- public List<Article> getHeatArticles( ) {
- List<Statistic> list = statisticMapper.getStatistic();
- List<Article> articlelist=new ArrayList<>();
- for (int i = 0; i < list.size(); i++) {
- Article article = articleMapper.selectArticleWithId(list.get(i).getArticleId());
- article.setHits(list.get(i).getHits());
- article.setCommentsNum(list.get(i).getCommentsNum());
- articlelist.add(article);
- if(i>=9){
- break;
- }
- }
- return articlelist;
- }
- }
(3)请求处理层实现
① 在com.lg.ch10文件夹下创建web文件夹并在下面创建client文件夹后创建IndexController.java
- import com.github.pagehelper.PageInfo;
- import com.lg.ch10.model.domain.Article;
- import com.lg.ch10.service.IArticleService;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RequestParam;
-
- import javax.servlet.http.HttpServletRequest;
- import java.util.List;
-
- @Controller
- public class IndexController {
- private static final Logger logger = LoggerFactory.getLogger(IndexController.class);
- @Autowired
- private IArticleService articleServiceImpl;
- // 博客首页,会自动跳转到文章页
- @GetMapping(value = "/")
- private String index(HttpServletRequest request) {
- return this.index(request, 1, 5);
- }
- // 文章页
- @GetMapping(value = "/page/{p}")
- public String index(HttpServletRequest request, @PathVariable("p") int page, @RequestParam(value = "count", defaultValue = "5") int count) {
- PageInfo<Article> articles = articleServiceImpl.selectArticleWithPage(page, count);
- // 获取文章热度统计信息
- List<Article> articleList = articleServiceImpl.getHeatArticles();
- request.setAttribute("articles", articles);
- request.setAttribute("articleList", articleList);
- logger.info("分页获取文章信息: 页码 "+page+",条数 "+count);
- return "client/index";
- }
- }
② web文件夹并在下面创建interceptor文件夹后创建BaseInterceptor.java
- import com.lg.ch10.utils.Commons;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.HandlerInterceptor;
- import org.springframework.web.servlet.ModelAndView;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
-
- @Configuration
- public class BaseInterceptor implements HandlerInterceptor {
- @Autowired
- private Commons commons;
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- return true;
- }
- @Override
- public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
- // 用户将封装的Commons工具返回页面
- request.setAttribute("commons",commons);
- }
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- }
- }
③ 在interceptor文件夹中创建WebMvcConfig.java
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
- @Configuration
- public class WebMvcConfig implements WebMvcConfigurer {
- @Autowired
- private BaseInterceptor baseInterceptor;
-
- @Override
- // 重写addInterceptors()方法,注册自定义拦截器
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(baseInterceptor);
- }
- }
(4)实现前端页面功能
在resources\templates\client下创建index.html文件
- <!DOCTYPE html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org">
- <!-- 载入文章头部页面,位置在/client文件夹下的header模板页面,模板名称th:fragment为header -->
- <div th:replace="/client/header::header(null,null)" />
- <body>
- <div class="am-g am-g-fixed blog-fixed index-page">
- <div class="am-u-md-8 am-u-sm-12">
- <!-- 文章遍历并分页展示 -->
- <div th:each="article: ${articles.list}">
- <article class="am-g blog-entry-article">
- <div class="am-u-lg-6 am-u-md-12 am-u-sm-12 blog-entry-img">
- <img width="100%" class="am-u-sm-12" th:src="@{${commons.show_thumb(article)}}"/>
- </div>
- <div class="am-u-lg-6 am-u-md-12 am-u-sm-12 blog-entry-text">
- <!-- 文章分类 -->
- <span class="blog-color"style="font-size: 15px;"><a>默认分类</a></span>
- <span> </span>
- <!-- 发布时间 -->
- <span style="font-size: 15px;" th:text="'发布于 '+ ${commons.dateFormat(article.created)}" />
- <h2>
- <div><a style="color: #0f9ae0;font-size: 20px;" th:href="${commons.permalink(article.id)}" th:text="${article.title}" />
- </div>
- </h2>
- <!-- 文章摘要-->
- <div style="font-size: 16px;" th:utext="${commons.intro(article,75)}" />
- </div>
- </article>
- </div>
- <!-- 文章分页信息 -->
- <div class="am-pagination">
- <div th:replace="/comm/paging::pageNav(${articles},'上一页','下一页','page')" />
- </div>
- </div>
- <!-- 博主信息描述 -->
- <div class="am-u-md-4 am-u-sm-12 blog-sidebar">
- <div class="blog-sidebar-widget blog-bor">
- <h2 class="blog-text-center blog-title"><span>CrazyStone</span></h2>
- <img th:src="@{/assets/img/me.jpg}" alt="about me" class="blog-entry-img"/>
- <p>
- Java后台开发
- </p>
- <p>个人博客小站,主要发表关于Java、Spring、Docker等相关文章</p>
- </div>
- <div class="blog-sidebar-widget blog-bor">
- <h2 class="blog-text-center blog-title"><span>联系我</span></h2>
- <p>
- <a><span class="am-icon-github am-icon-fw blog-icon"></span></a>
- <a><span class="am-icon-weibo am-icon-fw blog-icon"></span></a>
- </p>
- </div>
- </div>
- <!-- 阅读排行榜 -->
- <div class="am-u-md-4 am-u-sm-12 blog-sidebar">
- <div class="blog-sidebar-widget blog-bor">
- <h2 class="blog-text-center blog-title"><span>阅读排行榜</span></h2>
- <div style="text-align: left">
- <th:block th:each="article :${articleList}">
- <a style="font-size: 15px;" th:href="@{'/article/'+${article.id}}"
- th:text="${articleStat.index+1}+'、'+${article.title}+'('+${article.hits}+')'">
- </a>
- <hr style="margin-top: 0.6rem;margin-bottom: 0.4rem" />
- </th:block>
- </div>
- </div>
- </div>
- </div>
- </body>
- <!-- 载入文章尾部页面,位置在/client文件夹下的footer模板页面,模板名称th:fragment为footer -->
- <div th:replace="/client/footer::footer" />
- </html>
(5)效果展示
在浏览器输入:localhost/login
输入账号:user和控制台随机生成的密码
3.文章分页展示
(1)数据访问层实现
在dao文件夹下创建CommentMapper.java
- import com.lg.ch10.model.domain.Comment;
- import org.apache.ibatis.annotations.*;
- import java.util.List;
-
- @Mapper
- public interface CommentMapper {
- // 分页展示某个文章的评论
- @Select("SELECT * FROM t_comment WHERE article_id=#{aid} ORDER BY id DESC")
- public List<Comment> selectCommentWithPage(Integer aid);
-
- // 后台查询最新几条评论
- @Select("SELECT * FROM t_comment ORDER BY id DESC")
- public List<Comment> selectNewComment();
-
- // 发表评论
- @Insert("INSERT INTO t_comment (article_id,created,author,ip,content)" +
- " VALUES (#{articleId}, #{created},#{author},#{ip},#{content})")
- public void pushComment(Comment comment);
-
- // 站点服务统计,统计评论数量
- @Select("SELECT COUNT(1) FROM t_comment")
- public Integer countComment();
-
- // 通过文章id删除评论信息
- @Delete("DELETE FROM t_comment WHERE article_id=#{aid}")
- public void deleteCommentWithId(Integer aid);
- }
(2)业务处理层实现
① 创建Service层接口文件
在service文件夹下的IArticleService中编写id接口方法
- // 根据文章id查询单个文章详情
- public Article selectArticleWithId(Integer id);
- 在service中创建ICommentService.java
- import com.github.pagehelper.PageInfo;
- import com.lg.ch10.model.domain.Comment;
-
- public interface ICommentService {
- // 获取文章下的评论
- public PageInfo<Comment> getComments(Integer aid, int page, int count);
- }
在service中创建ISiteService.java
- import com.lg.ch10.model.ResponseData.StaticticsBo;
- import com.lg.ch10.model.domain.Article;
- import com.lg.ch10.model.domain.Comment;
- import java.util.List;
-
- public interface ISiteService {
- // 最新收到的评论
- public List<Comment> recentComments(int count);
-
- // 最新发表的文章
- public List<Article> recentArticles(int count);
-
- // 获取后台统计数据
- public StaticticsBo getStatistics();
-
- // 更新某个文章的统计数据
- public void updateStatistics(Article article);
- }
② 创建Service层接口实现类文件
在service\impl文件夹下创建ArticleServiceImpl中添加
- @Autowired
- private RedisTemplate redisTemplate;
- // 根据id查询单个文章详情,并使用Redis进行缓存管理
- public Article selectArticleWithId(Integer id){
- Article article = null;
- Object o = redisTemplate.opsForValue().get("article_" + id);
- if(o!=null){
- article=(Article)o;
- }else{
- article = articleMapper.selectArticleWithId(id);
- if(article!=null){
- redisTemplate.opsForValue().set("article_" + id,article);
- }
- }
- return article;
- }
在service\impl文件夹下创建CommentServiceImpl.java
- import com.github.pagehelper.PageHelper;
- import com.github.pagehelper.PageInfo;
- import com.lg.ch10.dao.CommentMapper;
- import com.lg.ch10.model.domain.Comment;
- import com.lg.ch10.service.ICommentService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.util.List;
-
- @Service
- @Transactional
- public class CommentServiceImpl implements ICommentService {
- @Autowired
- private CommentMapper commentMapper;
- // 根据文章id分页查询评论
- @Override
- public PageInfo<Comment> getComments(Integer aid, int page, int count) {
- PageHelper.startPage(page,count);
- List<Comment> commentList = commentMapper.selectCommentWithPage(aid);
- PageInfo<Comment> commentInfo = new PageInfo<>(commentList);
- return commentInfo;
- }
- }
在service\impl文件夹下创建SiteServiceImpl.java
- import com.github.pagehelper.PageHelper;
- import com.lg.ch10.dao.ArticleMapper;
- import com.lg.ch10.dao.CommentMapper;
- import com.lg.ch10.dao.StatisticMapper;
- import com.lg.ch10.model.ResponseData.StaticticsBo;
- import com.lg.ch10.model.domain.Article;
- import com.lg.ch10.model.domain.Comment;
- import com.lg.ch10.model.domain.Statistic;
- import com.lg.ch10.service.ISiteService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.util.List;
-
- @Service
- @Transactional
- public class SiteServiceImpl implements ISiteService {
- @Autowired
- private CommentMapper commentMapper;
- @Autowired
- private ArticleMapper articleMapper;
- @Autowired
- private StatisticMapper statisticMapper;
-
- @Override
- public void updateStatistics(Article article) {
- Statistic statistic = statisticMapper.selectStatisticWithArticleId(article.getId());
- statistic.setHits(statistic.getHits()+1);
- statisticMapper.updateArticleHitsWithId(statistic);
- }
-
- @Override
- public List<Comment> recentComments(int limit) {
- PageHelper.startPage(1, limit>10 || limit<1 ? 10:limit);
- List<Comment> byPage = commentMapper.selectNewComment();
- return byPage;
- }
-
- @Override
- public List<Article> recentArticles(int limit) {
- PageHelper.startPage(1, limit>10 || limit<1 ? 10:limit);
- List<Article> list = articleMapper.selectArticleWithPage();
- // 封装文章统计数据
- for (int i = 0; i < list.size(); i++) {
- Article article = list.get(i);
- Statistic statistic = statisticMapper.selectStatisticWithArticleId(article.getId());
- article.setHits(statistic.getHits());
- article.setCommentsNum(statistic.getCommentsNum());
- }
- return list;
- }
-
- @Override
- public StaticticsBo getStatistics() {
- StaticticsBo staticticsBo = new StaticticsBo();
- Integer articles = articleMapper.countArticle();
- Integer comments = commentMapper.countComment();
- staticticsBo.setArticles(articles);
- staticticsBo.setComments(comments);
- return staticticsBo;
- }
- }
(3)请求处理层实现
在web\client文件夹下的IndexController.java中添加
- @Autowired
- private ICommentService commentServiceImpl;
- @Autowired
- private ISiteService siteServiceImpl;
- // 文章详情查询
- @GetMapping(value = "/article/{id}")
- public String getArticleById(@PathVariable("id") Integer id, HttpServletRequest request){
- Article article = articleServiceImpl.selectArticleWithId(id);
- if(article!=null){
- // 查询封装评论相关数据
- getArticleComments(request, article);
- // 更新文章点击量
- siteServiceImpl.updateStatistics(article);
- request.setAttribute("article",article);
- return "client/articleDetails";
- }else {
- logger.warn("查询文章详情结果为空,查询文章id: "+id);
- // 未找到对应文章页面,跳转到提示页
- return "comm/error_404";
- }
- }
-
- // 查询文章的评论信息,并补充到文章详情里面
- private void getArticleComments(HttpServletRequest request, Article article) {
- if (article.getAllowComment()) {
- // cp表示评论页码,commentPage
- String cp = request.getParameter("cp");
- cp = StringUtils.isBlank(cp) ? "1" : cp;
- request.setAttribute("cp", cp);
- PageInfo<Comment> comments = commentServiceImpl.getComments(article.getId(),Integer.parseInt(cp),3);
- request.setAttribute("cp", cp);
- request.setAttribute("comments", comments);
- }
- }
(4)实现前端页面功能
在templates\client中创建acticleDetails.html
- <!DOCTYPE html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org">
- <head>
- <script th:src="@{/assets/js/jquery.min.js}"></script>
- <script th:src="@{/assets/js/layer.js}"></script>
- </head>
- <div th:replace="client/header::header(${article.title},null)"></div>
- <body>
- <article class="main-content post-page">
- <div class="post-header">
- <h1 class="post-title" itemprop="name headline" th:text="${article.title}"></h1>
- <div class="post-data">
- <time th:datetime="${commons.dateFormat(article.created)}" itemprop="datePublished" th:text="'发布于 '+ ${commons.dateFormat(article.created)}"></time>
- </div>
- </div>
- <br />
- <div id="post-content" class="post-content content" th:utext="${commons.article(article.content)}"></div>
- </article>
- <div th:replace="client/comments::comments"></div>
- <div th:replace="client/footer::footer"></div>
- <!-- 使用layer.js实现图片缩放功能 -->
- <script type="text/JavaScript">
- $('.post-content img').on('click', function(){
- var imgurl=$(this).attr('src');
- var w=this.width*2;
- var h=this.height*2+50;
- layer.open({
- type: 1,
- title: false, //不显示标题栏
- area: [w+"px", h+"px"],
- shadeClose: true, //点击遮罩关闭
- content: '\<\div style="padding:20px;">' +
- '\<\img style="width:'+(w-50)+'px;" src='+imgurl+'\>\<\/div>'
- });
- });
- </script>
- </body>
- </html>
(5)Redis服务启动与配置
(6)效果展示
4.文章评论管理
(1)业务处理层实现
① 编写service层接口方法
在ICommentService中编写发布文章评论的方法
- // 用户发表评论
- public void pushComment(Comment comment);
② 编写service层接口实现类方法
在CommentServiceImpl中增加评论发布方法
- @Autowired
- private StatisticMapper statisticMapper;
-
- @Override
- public void pushComment(Comment comment){
- commentMapper.pushComment(comment);
- // 更新文章评论数据量
- Statistic statistic = statisticMapper.selectStatisticWithArticleId(comment.getArticleId());
- statistic.setCommentsNum(statistic.getCommentsNum()+1);
- statisticMapper.updateArticleCommentsWithId(statistic);
- }
(2)请求处理层实现
在com.lg.ch10/web/client下创建用户评论管理的控制类CommentController.java文件
- import com.lg.ch10.model.ResponseData.ArticleResponseData;
- import com.lg.ch10.model.domain.Comment;
- import com.lg.ch10.service.ICommentService;
- import com.lg.ch10.utils.MyUtils;
- import com.vdurmont.emoji.EmojiParser;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.core.context.SecurityContextHolder;
- import org.springframework.security.core.userdetails.User;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.ResponseBody;
-
- import javax.servlet.http.HttpServletRequest;
- import java.util.Date;
-
- @Controller
- @RequestMapping("/comments")
- public class CommentController {
- private static final Logger logger = LoggerFactory.getLogger(CommentController.class);
-
- @Autowired
- private ICommentService commentServcieImpl;
-
- // 发表评论操作
- @PostMapping(value = "/publish")
- @ResponseBody
- public ArticleResponseData publishComment(HttpServletRequest request, @RequestParam Integer aid, @RequestParam String text) {
- // 去除js脚本
- text = MyUtils.cleanXSS(text);
- text = EmojiParser.parseToAliases(text);
- // 获取当前登录用户
- User user=(User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
- // 封装评论信息
- Comment comments = new Comment();
- comments.setArticleId(aid);
- comments.setIp(request.getRemoteAddr());
- comments.setCreated(new Date());
- comments.setAuthor(user.getUsername());
- comments.setContent(text);
- try {
- commentServcieImpl.pushComment(comments);
- logger.info("发布评论成功,对应文章id: "+aid);
- return ArticleResponseData.ok();
- } catch (Exception e) {
- logger.error("发布评论失败,对应文章id: "+aid +";错误描述: "+e.getMessage());
- return ArticleResponseData.fail();
- }
- }
- }
(3)实现前端页面功能
打开client文件夹中的comments.html,实现前端页面评论展示的实现。
- <!DOCTYPE html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org" th:fragment="comments"
- xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
- <body>
- <div th:if="${article}!=null">
- <div th:id="${article.id ?: 0}" class="comment-container">
- <div id="comments" class="clearfix">
- <div th:if="${article.allowComment}">
- <span class="response">
- <form name="logoutform" th:action="@{/logout}" method="post"></form>
- <th:block sec:authorize="isAuthenticated()">
- Hello,<a data-no-instant="" sec:authentication="name"></a>
- 如果你想 <a href="javascript:document.logoutform.submit();">注销</a> ?
- </th:block>
- <th:block sec:authorize="isAnonymous()">
- 用户想要评论,请先<a th:href="@{/login}" title="登录" data-no-instant="">登录</a>!
- </th:block>
- </span>
- <div sec:authorize="isAuthenticated()">
- <form id="comment-form" class="comment-form" role="form" onsubmit="return TaleComment.subComment();">
- <input type="hidden" name="aid" id="aid" th:value="${article.id}"/>
- <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
- <textarea name="text" id="textarea" class="form-control" placeholder="以上信息可以为空,评论不能为空哦!"
- required="required" minlength="5" maxlength="2000"></textarea>
- <button type="submit" class="submit" id="misubmit">提交</button>
- </form>
- </div>
- </div>
- <!-- 分页显示其他评论内容 -->
- <div th:if="${comments}">
- <ol class="comment-list">
- <th:block th:each="comment :${comments.list}">
- <li th:id="'li-comment-'+${comment.id}" class="comment-body comment-parent comment-odd">
- <div th:id="'comment-'+${comment.id}">
- <div class="comment-view" onclick="">
- <div class="comment-header">
- <!--设置人物头像和名称-->
- <img class="avatar" th:src="@{/assets/img/avatars.jpg}" height="50"/>
- <a class="comment-author" rel="external nofollow" th:text="${comment.author}" />
- </div>
- <!-- 评论内容 -->
- <div class="comment-content">
- <span class="comment-author-at"></span>
- <p th:utext="${commons.article(comment.content)}"></p>
- </div>
- <!-- 评论日期 -->
- <div class="comment-meta">
- <time class="comment-time" th:text="${commons.dateFormat(comment.created)}"></time>
- <a sec:authorize="isAuthenticated()" th:if="${comment.author}!= ${session.SPRING_SECURITY_CONTEXT.authentication.principal.username}" href="javascript:void(0)" style="color: #1b961b">
- 回复
- </a>
- </div>
- </div>
- </div>
- </li>
- </th:block>
- </ol>
- <!-- 进行评论分页 -->
- <div class="lists-navigator clearfix">
- <ol class="page-navigator">
- <!-- 判断并展示上一页 -->
- <th:block th:if="${comments.hasPreviousPage}">
- <li class="prev"><a th:href="'?cp='+${comments.prePage}+'#comments'">上一页</a></li>
- </th:block>
- <!-- 判断并展示中间页 -->
- <th:block th:each="navIndex : ${comments.navigatepageNums}">
- <th:block th:if="${comments.pages} <= 5">
- <li th:class="${comments.pageNum}==${navIndex}?'current':''">
- <a th:href="'?cp='+${navIndex}+'#comments'" th:text="${navIndex}"></a>
- </li>
- </th:block>
- <th:block th:if="${comments.pages} > 5">
- <li th:if="${comments.pageNum <=3 && navIndex <= 5}" th:class="${comments.pageNum}==${navIndex}?'current':''">
- <a th:href="'?cp='+${navIndex}+'#comments'" th:text="${navIndex}"></a>
- </li>
- <li th:if="${comments.pageNum >= comments.pages-2 && navIndex > comments.pages-5}" th:class="${comments.pageNum}==${navIndex}?'current':''">
- <a th:href="'?cp='+${navIndex}+'#comments'" th:text="${navIndex}"></a>
- </li>
- <li th:if="${comments.pageNum >=4 && comments.pageNum <= comments.pages-3 && navIndex >= comments.pageNum-2 && navIndex <= comments.pageNum+2}" th:class="${comments.pageNum}==${navIndex}?'current':''">
- <a th:href="'?cp='+${navIndex}+'#comments'" th:text="${navIndex}"></a>
- </li>
- </th:block>
- </th:block>
- <!-- 判断并展示下一页 -->
- <th:block th:if="${comments.hasNextPage}">
- <li class="next"><a th:href="'?cp='+${comments.nextPage}+'#comments'">下一页</a></li>
- </th:block>
- </ol>
- </div>
- </div>
- </div>
- </div>
- </div>
- </body>
- <div th:replace="comm/tale_comment::tale_comment"></div>
- </html>
打开comm文件夹下的table_comment.html进行查看
- <!DOCTYPE html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org" th:fragment="tale_comment" >
- <body>
- <script type="text/javascript">
- /*<![CDATA[*/
- (function () {
- window.TaleComment = {
- subComment: function () {
- $.ajax({
- type: 'post',
- url: '/comments/publish',
- data: $('#comment-form').serialize(),
- async: false,
- dataType: 'json',
- success: function (result) {
- if (result && result.success) {
- window.alert("评论提交成功!");
- window.location.reload();
- } else {
- window.alert("发送失败")
- if (result.msg) {
- alert(result.msg);
- }
- }
- }
- });
- return false;
- }
- };
- })();
- </script>
- </body>
- </html>
(4)效果展示
5.数据展示
(1)请求处理层实现
在com.lg.ch10/web文件夹下创建admin文件夹,并在其中创建后台管理控制类AdminController
- import com.lg.ch10.model.ResponseData.StaticticsBo;
- import com.lg.ch10.model.domain.Article;
- import com.lg.ch10.model.domain.Comment;
- import com.lg.ch10.service.ISiteService;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
-
- import javax.servlet.http.HttpServletRequest;
- import java.util.List;
-
- @Controller
- @RequestMapping("/admin")
- public class AdminController {
- private static final Logger logger = LoggerFactory.getLogger(AdminController.class);
-
- @Autowired
- private ISiteService siteServiceImpl;
-
- // 管理中心起始页
- @GetMapping(value = {"", "/index"})
- public String index(HttpServletRequest request) {
- // 获取最新的5篇博客、评论以及统计数据
- List<Article> articles = siteServiceImpl.recentArticles(5);
- List<Comment> comments = siteServiceImpl.recentComments(5);
- StaticticsBo staticticsBo = siteServiceImpl.getStatistics();
- // 向Request域中存储数据
- request.setAttribute("comments", comments);
- request.setAttribute("articles", articles);
- request.setAttribute("statistics", staticticsBo);
- return "back/index";
- }
- }
(2)实现后台前端页面功能
back文件夹中index.html是后台系统的首页
- <!DOCTYPE html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org" th:with="title='管理中心',active='home'">
- <header th:replace="back/header::headerFragment(${title},${active})"></header>
- <body class="fixed-left">
- <div id="wrapper">
- <div th:replace="back/header::header-body"></div>
- <div class="content-page">
- <div class="content">
- <div class="container">
- <div class="row">
- <div class="col-sm-12">
- <h4 class="page-title">仪表盘</h4>
- </div>
-
- <div class="row">
- <div class="col-sm-6 col-lg-3">
- <div class="mini-stat clearfix bx-shadow bg-info">
- <span class="mini-stat-icon"><i class="fa fa-quote-right" aria-hidden="true"></i></span>
- <div class="mini-stat-info text-right">
- 发表了<span class="counter" th:text="${statistics.articles}"></span>篇文章
- </div>
- </div>
- </div>
- <div class="col-sm-6 col-lg-3">
- <div class="mini-stat clearfix bg-purple bx-shadow">
- <span class="mini-stat-icon"><i class="fa fa-comments-o" aria-hidden="true"></i></span>
- <div class="mini-stat-info text-right">
- 收到了<span class="counter" th:text="${statistics.comments}"></span>条留言
- </div>
- </div>
- </div>
- </div>
-
- <div class="row">
- <div class="col-md-4">
- <div class="panel panel-default">
- <div class="panel-heading">
- <h4 class="panel-title">最新文章</h4>
- </div>
- <div class="panel-body">
- <ul class="list-group">
- <li class="list-group-item" th:each="article : ${articles}">
- <span class="badge badge-primary" th:text="${article.commentsNum}">
- </span>
- <a target="_blank" th:href="${commons.site_url('/article/')}+${article.id}"
- th:text="${article.title}"></a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- <div class="col-md-4">
- <div class="panel panel-default">
- <div class="panel-heading">
- <h4 class="panel-title">最新留言</h4>
- </div>
- <div class="panel-body">
- <div th:if="${comments.size()}==0">
- <div class="alert alert-warning">
- 还没有收到留言.
- </div>
- </div>
- <ul class="list-group" th:unless="${comments}==null and ${comments.size()}==0">
- <li class="list-group-item" th:each="comment : ${comments}">
- <th:block th:text="${comment.author}"/>于 <th:block th:text="${commons.dateFormat(comment.created)}"/>:
- <a th:href="${commons.site_url('/article/')}+${comment.articleId}+'#comments'"
- target="_blank" th:utext="${commons.article(comment.content)}"></a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div th:replace="back/footer :: footer-content"></div>
- </div>
- </div>
- </div>
- </div>
- <div th:replace="back/footer :: footer"></div>
- </body>
- </html>
(3)效果展示
6.文章发布
(1)业务处理层实现
① 编写service层接口方法
在IArticleService中添加
- // 发布文章
- public void publish(Article article);
② 编写service层接口实现类方法
在ArticleServiceImpl中添加
- // 发布文章
- @Override
- public void publish(Article article) {
- // 去除表情
- article.setContent(EmojiParser.parseToAliases(article.getContent()));
- article.setCreated(new Date());
- article.setHits(0);
- article.setCommentsNum(0);
- // 插入文章,同时插入文章统计数据
- articleMapper.publishArticle(article);
- statisticMapper.addStatistic(article);
- }
(2)请求处理层实现
在后台管理控制类AdminController中添加页面跳转请求的方法
- @Autowired
- private IArticleService articleServiceImpl;
-
- // 向文章发表页面跳转
- @GetMapping(value = "/article/toEditPage")
- public String newArticle( ) {
- return "back/article_edit";
- }
- // 发表文章
- @PostMapping(value = "/article/publish")
- @ResponseBody
- public ArticleResponseData publishArticle(Article article) {
- if (StringUtils.isBlank(article.getCategories())) {
- article.setCategories("默认分类");
- }
- try {
- articleServiceImpl.publish(article);
- logger.info("文章发布成功");
- return ArticleResponseData.ok();
- } catch (Exception e) {
- logger.error("文章发布失败,错误信息: "+e.getMessage());
- return ArticleResponseData.fail();
- }
- }
- // 跳转到后台文章列表页面
- @GetMapping(value = "/article")
- public String index(@RequestParam(value = "page", defaultValue = "1") int page,
- @RequestParam(value = "count", defaultValue = "10") int count,
- HttpServletRequest request) {
- PageInfo<Article> pageInfo = articleServiceImpl.selectArticleWithPage(page, count);
- request.setAttribute("articles", pageInfo);
- return "back/article_list";
- }
(3)实现前端页面功能
打开back文件夹中的article_edit.html文件,该文件用于实现文章编辑和发布
- <!DOCTYPE html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org" th:with="title='发布文章',active='publish'">
- <header th:replace="back/header::headerFragment(${title},${active})"></header>
- <link th:href="@{/back/plugins/tagsinput/jquery.tagsinput.css}" rel="stylesheet"/>
- <link th:href="@{/back/plugins/select2.dist.css/select2-bootstrap.css}" rel="stylesheet"/>
- <link th:href="@{/back/plugins/toggles/toggles.css}" rel="stylesheet"/>
-
- <link th:href="@{/back/plugins/mditor/css/mditor.min.css}" rel="stylesheet"/>
- <link th:href="@{/back/plugins/dropzone/4.3.0/min/dropzone.min.css}" rel="stylesheet">
- <style>
- #tags_tagsinput {
- background-color: #fafafa;
- border: 1px solid #eeeeee;
- }
-
- #tags_addTag input {
- width: 100%;
- }
-
- #tags_addTag {
- margin-top: -5px;
- }
-
- .mditor .editor{
- font-size: 14px;
- font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
- }
- .mditor .backdrop, .mditor .textarea, .mditor .viewer{
- font-size: 14px;
- }
- .markdown-body{
- font-size: 14px;
- }
- .note-toolbar {
- text-align: center;
- }
-
- .note-editor.note-frame {
- border: none;
- }
-
- .note-editor .note-toolbar {
- background-color: #f5f5f5;
- padding-bottom: 10px;
- }
-
- .note-toolbar .note-btn-group {
- margin: 0;
- }
-
- .note-toolbar .note-btn {
- border: none;
- }
-
- #articleForm #dropzone {
- min-height: 200px;
- background-color: #dbdde0;
- line-height:200px;
- margin-bottom: 10px;
- }
- #articleForm .dropzone {
- border: 1px dashed #8662c6;
- border-radius: 5px;
- background: white;
- }
- #articleForm .dropzone .dz-message {
- font-weight: 400;
- }
- #articleForm .dropzone .dz-message .note {
- font-size: 0.8em;
- font-weight: 200;
- display: block;
- margin-top: 1.4rem;
- }
- </style>
- <body class="fixed-left">
- <div id="wrapper">
- <div th:replace="back/header::header-body"></div>
- <div class="content-page">
- <div class="content">
- <div class="container">
- <div class="row">
- <div class="col-sm-12">
- <h4 class="page-title">
- <th:block th:if="${null != contents}">
- 编辑文章
- </th:block>
- <th:block th:unless="${null != contents}">
- 发布文章
- </th:block>
- </h4>
- </div>
- <div class="col-md-12">
- <form id="articleForm">
- <input type="hidden" name="id"
- th:value="${contents!=null and contents.id!=null}?${contents.id}: ''" id="id"/>
- <input type="hidden" name="allowComment"
- th:value="${contents!=null and contents.allowComment !=null}
- ?${contents.allowComment}: true" id="allow_comment"/>
- <input type="hidden" name="content" id="content-editor"/>
- <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
- <div class="form-group col-md-6" style="padding: 0 10px 0 0;">
- <th:block th:if="${null != contents}">
- <input type="text" class="form-control" name="title"
- required="required" aria-required="true" th:value="${contents.title}"/>
- </th:block>
- <th:block th:unless="${null != contents}">
- <input type="text" class="form-control" placeholder="请输入文章标题(必须)" name="title"
- required="required" aria-required="true"/>
- </th:block>
- </div>
- <div class="form-group col-md-6" style="padding: 0 10px 0 0;">
- <th:block th:if="${null != contents}">
- <input name="tags" id="tags" type="text" class="form-control" th:value="${contents.tags}" />
- </th:block>
- <th:block th:unless="${null != contents}">
- <input name="tags" id="tags" type="text" class="form-control" placeholder="请输入文章标签" />
- </th:block>
- </div>
- <div class="clearfix"></div>
- <div id="md-container" class="form-group">
- <textarea id="md-editor" th:utext="${contents!=null and contents.content !=null}?${contents.content}: ''"></textarea>
- </div>
- <div class="clearfix"></div>
-
- <div class="text-right">
- <a class="btn btn-default waves-effect waves-light" th:href="@{/admin/article}">返回列表</a>
- <button type="button" class="btn btn-primary waves-effect waves-light" onclick="subArticle('publish');">
- 保存文章
- </button>
- </div>
- </form>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </body>
- </html>
- 当点击保存文章按钮后会触发执行提交方法
- function subArticle(status) {
- var title = $('#articleForm input[name=title]').val();
- var content = mditor.value;
- if (title == '') {
- tale.alertWarn('请输入文章标题');
- return;
- }
- if (title .length>25) {
- tale.alertWarn('文章标题不能超过25个字符!');
- return;
- }
- if (content == '') {
- tale.alertWarn('请输入文章内容');
- return;
- }
- $('#content-editor').val(content);
- $("#articleForm #status").val(status);
- $("#articleForm #categories").val($('#multiple-sel').val());
- var params = $("#articleForm").serialize();
- var url = $('#articleForm #id').val() != '' ? '/admin/article/modify' : '/admin/article/publish';
- tale.post({
- url:url,
- data:params,
- success: function (result) {
- if (result && result.success) {
- tale.alertOk({
- text:'文章保存成功',
- then: function () {
- setTimeout(function () {
- window.location.href = '/admin/article';
- }, 500);
- }
- });
- } else {
- tale.alertError(result.msg || '保存文章失败!');
- }
- }
- });
- }
(4)效果展示
7.文章修改
(1)业务处理层实现
① 编写service层接口方法
- // 根据主键更新文章
- public void updateArticleWithId(Article article);
② 编写service层接口实现类方法
在ArticleServiceImpl中添加
- // 更新文章
- @Override
- public void updateArticleWithId(Article article) {
- article.setModified(new Date());
- articleMapper.updateArticleWithId(article);
- redisTemplate.delete("article_" + article.getId());
- }
(2)请求处理层实现
在后台管理控制类AdminController中定义分别用于处理跳转到文章修改页面和保存修改文章的操作
- // 向文章修改页面跳转
- @GetMapping(value = "/article/{id}")
- public String editArticle(@PathVariable("id") String id, HttpServletRequest request) {
- Article article = articleServiceImpl.selectArticleWithId(Integer.parseInt(id));
- request.setAttribute("contents", article);
- request.setAttribute("categories", article.getCategories());
- return "back/article_edit";
- }
-
- // 文章修改处理
- @PostMapping(value = "/article/modify")
- @ResponseBody
- public ArticleResponseData modifyArticle(Article article) {
- try {
- articleServiceImpl.updateArticleWithId(article);
- logger.info("文章更新成功");
- return ArticleResponseData.ok();
- } catch (Exception e) {
- logger.error("文章更新失败,错误信息: "+e.getMessage());
- return ArticleResponseData.fail();
- }
- }
(3)效果展示
8.文章删除
(1)业务处理层实现
① 编写service层接口方法
在IArticleService中添加
- // 根据主键删除文章
- public void deleteArticleWithId(int id);
② 编写service层接口实现类方法
在ArticleServiceImpl中添加
- @Autowired
- private CommentMapper commentMapper;
- // 删除文章
- @Override
- public void deleteArticleWithId(int id) {
- // 删除文章的同时,删除对应的缓存
- articleMapper.deleteArticleWithId(id);
- redisTemplate.delete("article_" + id);
- // 同时删除对应文章的统计数据
- statisticMapper.deleteStatisticWithId(id);
- // 同时删除对应文章的评论数据
- commentMapper.deleteCommentWithId(id);
- }
(2)请求处理层实现
- // 文章删除
- @PostMapping(value = "/article/delete")
- @ResponseBody
- public ArticleResponseData delete(@RequestParam int id) {
- try {
- articleServiceImpl.deleteArticleWithId(id);
- logger.info("文章删除成功");
- return ArticleResponseData.ok();
- } catch (Exception e) {
- logger.error("文章删除失败,错误信息: "+e.getMessage());
- return ArticleResponseData.fail();
- }
- }
(3)实现前端页面功能
在back文件夹下的artice_list.html文件中处理展示文章列表和删除文章请求的前端页面功能
- <!DOCTYPE html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org" th:with="title='文章管理',active='article'">
- <header th:replace="back/header::headerFragment(${title},${active})"></header>
- <head>
- <meta th:name="_csrf" th:content="${_csrf.token}"/>
- <!-- 默认的header name是X-CSRF-TOKEN -->
- <meta th:name="_csrf_header" th:content="${_csrf.headerName}"/>
- </head>
- <body class="fixed-left">
- <div id="wrapper">
- <div th:replace="back/header::header-body"></div>
- <div class="content-page">
- <div class="content">
- <div class="container">
- <div class="row">
- <div class="col-sm-12">
- <h4 class="page-title">文章管理</h4>
- </div>
- <div class="col-md-12">
- <table class="table table-striped table-bordered">
- <thead>
- <tr>
- <th width="35%">文章标题</th>
- <th width="15%">发布时间</th>
- <th>浏览量</th>
- <th>所属分类</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
- <th:block th:each="article : ${articles.list}">
- <tr th:id="${article.id}">
- <td>
- <a th:href="@{'/article/'+${article.id}}" th:text="${article.title}" target="_blank"></a>
- </td>
- <td><th:block th:text="${commons.dateFormat(article.created)}"/></td>
- <td><th:block th:text="${article.hits}"/></td>
- <td><th:block th:text="${article.categories}"/></td>
- <td>
- <a th:href="@{'/admin/article/'+${article.id}}"
- class="btn btn-primary btn-sm waves-effect waves-light m-b-5">
- <i class="fa fa-edit"></i> <span>编辑</span></a>
- <a href="javascript:void(0)" th:onclick="'delArticle('+${article.id}+');'"
- class="btn btn-danger btn-sm waves-effect waves-light m-b-5">
- <i class="fa fa-trash-o"></i> <span>删除</span></a>
- <a class="btn btn-warning btn-sm waves-effect waves-light m-b-5" href="javascript:void(0)">
- <i class="fa fa-rocket"></i> <span>预览</span></a>
- </td>
- </tr>
- </th:block>
-
- </tbody>
- </table>
- <div th:replace="comm/paging :: pageAdminNav(${articles})"></div>
- </div>
- </div>
- <div th:replace="back/footer :: footer-content"></div>
- </div>
- </div>
- </div>
- </div>
- <div th:replace="back/footer :: footer"></div>
- <script type="text/javascript">
- function delArticle(id) {
- // 获取<meta>标签中封装的_csrf信息
- var token = $("meta[name='_csrf']").attr("content");
- var header = $("meta[name='_csrf_header']").attr("content");
- if(confirm('确定删除该文章吗?')){
- $.ajax({
- type:'post',
- url : '/admin/article/delete',
- data: {id:id},
- dataType: 'json',
- beforeSend : function(xhr) {
- xhr.setRequestHeader(header, token);
- },
- success: function (result) {
- if (result && result.success) {
- window.alert("文章删除成功");
- window.location.reload();
- } else {
- window.alert(result.msg || '文章删除失败')
- }
- }
- });
- }
- }
- </script>
- </body>
- </html>
(4)效果展示
9.用户登录控制
(1)请求处理层实现
在com.lg.ch10/web/client文件夹下创建用户登录管理的控制类文件LoginController
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
- import javax.servlet.http.HttpServletRequest;
- import java.util.Map;
-
- @Controller
- public class LoginController {
- // 向登录页面跳转,同时封装原始页面地址
- @GetMapping(value = "/login")
- public String login(HttpServletRequest request, Map map) {
- // 分别获取请求头和参数url中的原始访问路径
- String referer = request.getHeader("Referer");
- String url = request.getParameter("url");
- System.out.println("referer= "+referer);
- System.out.println("url= "+url);
-
- // 如果参数url中已经封装了原始页面路径,直接返回该路径
- if (url!=null && !url.equals("")){
- map.put("url",url);
- // 如果请求头本身包含登录,将重定向url设为空,让后台通过用户角色进行选择跳转
- }else if (referer!=null && referer.contains("/login")){
- map.put("url", "");
- }else {
- // 否则的话,就记住请求头中的原始访问路径
- map.put("url", referer);
- }
- return "comm/login";
- }
- // 对Security拦截的无权限访问异常处理路径映射
- @GetMapping(value = "/errorPage/{page}/{code}")
- public String AccessExecptionHandler(@PathVariable("page") String page, @PathVariable("code") String code) {
- return page+"/"+code;
- }
- }
(2)实现前端页面功能
打开comm文件下的自定义用户登录页面login.html进行自定义用户登录功能的查看和实现
- <!DOCTYPE html>
- <html xmlns:th="http://www.thymeleaf.org">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport"
- content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
- <title>登录博客后台</title>
-
- <meta http-equiv="Cache-Control" content="no-siteapp"/>
- <link rel="shortcut icon" th:href="@{/user/img/bloglogo.jpg}"/>
-
- <script th:src="@{/assets/js/jquery.min.js}"></script>
- <script th:src="@{/assets/js/amazeui.min.js}"></script>
- <link rel="stylesheet" th:href="@{/assets/css/amazeui.min.css}"/>
- <link rel="stylesheet" th:href="@{/assets/css/app.css}"/>
- </head>
-
- <body>
-
- <div class="log">
- <div class="am-g">
- <div class="am-u-lg-3 am-u-md-6 am-u-sm-8 am-u-sm-centered log-content">
- <h1 class="log-title am-animation-slide-top" style="color: black;" th:text="#{login.welcomeTitle}">~欢迎登录博客~</h1>
- <br>
- <div th:if="${param.error}" style="color: red" th:text="#{login.error}">用户名或密码错误!</div>
- <form class="am-form" id="loginForm" th:action="@{/login}" method="post">
- <div>
- <input type="hidden" name="url" th:value="${url}">
- </div>
- <div class="am-input-group am-radius am-animation-slide-left">
- <input type="text" class="am-radius" th:placeholder="#{login.username}" name="username" />
- <span class="am-input-group-label log-icon am-radius">
- <i class="am-icon-user am-icon-sm am-icon-fw"></i>
- </span>
- </div>
- <br>
- <div class="am-input-group am-animation-slide-left log-animation-delay">
- <input type="password" class="am-form-field am-radius log-input" th:placeholder="#{login.password}" name="password" />
- <span class="am-input-group-label log-icon am-radius">
- <i class="am-icon-lock am-icon-sm am-icon-fw"></i>
- </span>
- </div>
- <div style="padding-top: 10px;">
- <input type="submit" th:value="#{login.sub}"
- class="am-btn am-btn-primary am-btn-block am-btn-lg am-radius am-animation-slide-bottom log-animation-delay" />
- </div>
- </form>
- </div>
- </div>
-
- <footer class="log-footer">
- <p style="margin: 30px; color: #2E2D3C"><time class="comment-time" th:text="${#dates.format(new java.util.Date().getTime(), 'yyyy')}"></time> © Powered By <a style="color: #0e90d2" rel="nofollow">CrazyStone</a></p>
- </footer>
- </div>
- </body>
- </html>
(3)编写Security认证授权配置类
在com.lg.ch10/web/config文件夹下创建SecurityConfig
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.security.access.AccessDeniedException;
- import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
- import org.springframework.security.config.annotation.web.builders.HttpSecurity;
- import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
- import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.core.AuthenticationException;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.authority.SimpleGrantedAuthority;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.web.access.AccessDeniedHandler;
- import org.springframework.security.web.authentication.AuthenticationFailureHandler;
- import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
- import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
- import org.springframework.security.web.savedrequest.RequestCache;
- import org.springframework.security.web.savedrequest.SavedRequest;
-
- import javax.servlet.RequestDispatcher;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.sql.DataSource;
- import java.io.IOException;
- import java.net.URL;
- import java.util.Collection;
-
- @EnableWebSecurity // 开启MVC security安全支持
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- @Autowired
- private DataSource dataSource;
- @Value("${COOKIE.VALIDITY}")
- private Integer COOKIE_VALIDITY;
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- // 1、自定义用户访问控制
- http.authorizeRequests()
- .antMatchers("/","/page/**","/article/**","/login").permitAll()
- .antMatchers("/back/**","/assets/**","/user/**","/article_img/**").permitAll()
- .antMatchers("/admin/**").hasRole("admin")
- .anyRequest().authenticated();
- // 2、自定义用户登录控制
- http.formLogin()
- .loginPage("/login")
- .usernameParameter("username").passwordParameter("password")
- .successHandler(new AuthenticationSuccessHandler() {
- @Override
- public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
- String url = httpServletRequest.getParameter("url");
- // 获取被拦截的原始访问路径
- RequestCache requestCache = new HttpSessionRequestCache();
- SavedRequest savedRequest = requestCache.getRequest(httpServletRequest,httpServletResponse);
- if(savedRequest !=null){
- // 如果存在原始拦截路径,登录成功后重定向到原始访问路径
- httpServletResponse.sendRedirect(savedRequest.getRedirectUrl());
- } else if(url != null && !url.equals("")){
- // 跳转到之前所在页面
- URL fullURL = new URL(url);
- httpServletResponse.sendRedirect(fullURL.getPath());
- }else {
- // 直接登录的用户,根据用户角色分别重定向到后台首页和前台首页
- Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
- boolean isAdmin = authorities.contains(new SimpleGrantedAuthority("ROLE_admin"));
- if(isAdmin){
- httpServletResponse.sendRedirect("/admin");
- }else {
- httpServletResponse.sendRedirect("/");
- }
- }
- }
- })
- // 用户登录失败处理
- .failureHandler(new AuthenticationFailureHandler() {
- @Override
- public void onAuthenticationFailure(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
- // 登录失败后,取出原始页面url并追加在重定向路径上
- String url = httpServletRequest.getParameter("url");
- httpServletResponse.sendRedirect("/login?error&url="+url);
- }
- });
- // 3、设置用户登录后cookie有效期,默认值
- http.rememberMe().alwaysRemember(true).tokenValiditySeconds(COOKIE_VALIDITY);
- // 4、自定义用户退出控制
- http.logout().logoutUrl("/logout").logoutSuccessUrl("/");
- // 5、针对访问无权限页面出现的403页面进行定制处理
- http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
- @Override
- public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
- // 如果是权限访问异常,则进行拦截到指定错误页面
- RequestDispatcher dispatcher = httpServletRequest.getRequestDispatcher("/errorPage/comm/error_403");
- dispatcher.forward(httpServletRequest, httpServletResponse);
- }
- });
- }
- }
(4)效果展示
在浏览器输入http://localhost
10.定时邮件发送
(1)邮件发送工具类实现
在com.lg.ch10/utils包下创建工具类MailUtils.java
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.mail.SimpleMailMessage;
- import org.springframework.mail.javamail.JavaMailSenderImpl;
- import org.springframework.stereotype.Component;
-
- @Component
- public class MailUtils {
- @Autowired
- private JavaMailSenderImpl mailSender;
-
- @Value("${spring.mail.username}")
- private String mailfrom;
-
- // 发送简单邮件
- public void sendSimpleEmail(String mailto, String title, String content) {
- // 定制邮件发送内容
- SimpleMailMessage message = new SimpleMailMessage();
- message.setFrom(mailfrom);
- message.setTo(mailto);
- message.setSubject(title);
- message.setText(content);
- // 发送邮件
- mailSender.send(message);
- }
- }
(2)邮件定时发送调度实现
在com.lg.ch10/web包下创建scheduletask包,并在该包内创建一个管理类ScheduleTask
- import com.lg.ch10.dao.StatisticMapper;
- import com.lg.ch10.utils.MailUtils;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.scheduling.annotation.Scheduled;
- import org.springframework.stereotype.Component;
-
- @Component
- public class ScheduleTask {
- @Autowired
- private StatisticMapper statisticMapper;
- @Autowired
- private MailUtils mailUtils;
- @Value("${spring.mail.username}")
- private String mailto;
-
- @Scheduled(cron = "0 */3 * * * ? ")
- public void sendEmail(){
- // 定制邮件内容
- long totalvisit = statisticMapper.getTotalVisit();
- long totalComment = statisticMapper.getTotalComment();
- StringBuffer content = new StringBuffer();
- content.append("博客系统总访问量为:"+totalvisit+"人次").append("\n");
- content.append("博客系统总评论量为:"+totalComment+"人次").append("\n");
- mailUtils.sendSimpleEmail(mailto,"个人博客系统流量统计情况",content.toString());
- }
- }
(3)开启基于注解的定时任务
在启动类中添加
@EnableScheduling // 开启定时任务注解功能支持
(4)效果展示
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。