本项目目标是开发一个社区网站,拥有发帖、讨论、搜索、登陆等一个正常社区拥有的功能。涉及到的版本参数为:css
- JDK1.8
- Maven3.8.1(直接集成到IDEA)
- Springboot 2.5.1
- tomcat 9.0.45
- Mybatis
- Mysql 8.0.15
参考网站(在使用框架过程当中可能会看的开发文档):html
https://mvnrepository.com/ 查找maven依赖前端
https://mybatis.org/mybatis-3/zh/index.html mybatis的官方文档,配置等都有说明java
项目代码已发布到github https://github.com/GaoYuan-1/web-projectmysql
关于数据库文件,该篇博客中已有提到,可去文中github获取数据 MySQL基础篇(一)git
本文介绍如何实现注册,发送激活邮件等内容。本系列下一篇博客将会开发登陆功能或发布帖子功能等,最终将会把完整项目经历发布出来。github
本系列主要介绍的是实战内容,对于理论知识介绍较少,适合有必定基础的人。web
接上次开发日记(一)说明:面试
spring.datasource.url=jdbc:mysql://localhost:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong&allowPublicKeyRetrieval=true
在项目application.properties中添加一句allowPublicKeyRetrieval=true。不然每次打开项目须要将数据库启动,否则的话会出现公钥不识别的错误。redis
开发流程实质上就是一次请求的执行过程。
Controlloer(视图层)依赖于Service(表现层)依赖于DAO(数据访问层),因此开发过程当中能够从DAO开始,依次进行开发。
首页会有许多个功能,首先咱们须要实现一个简单的demo,以后对功能进行丰富便可。
首先计划开发页面显示10个帖子,进行分页。
数据库中的TABLE以下所示:
其中,comment_count意义为评论数量。
首先在项目.entity文件中,创建DisscussPost实体(帖子信息),而后创建DiscussPostMapper。
@Mapper public interface DiscussPostMapper { //userId传参是为了未来显示我的首页,能够认为userId==0时为网站首页 List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit); //由于首页要分页显示,每页十条,因此直接使用集合 //若是在<if>里使用时,好比显示首页时不须要判断userId,而显示我的首页须要,若是只有一个参数,须要加上@Param,不然会报错 int selectDiscussPostRows(@Param("userId") int userId); //该注解能够给参数取别名 }
这个接口只须要写两个方法。第一个负责返回一个集合,好比咱们要分页显示,每页10条,返回这10条记录的集合
第二个方法,负责返回总的行数。
接下来写Mybatis的.xml文件
<mapper namespace="com.nowcoder.community.dao.DiscussPostMapper"> <!-- 这里写服务接口的全限定名 --> <sql id="selectFields"> id, user_id, title, content, type, status, create_time, comment_count, score </sql> <select id="selectDiscussPosts" resultType="DiscussPost"> select <include refid="selectFields"></include> from discuss_post where status != 2 <if test="userId != 0"> and user_id = #{userId} </if> order by type desc, create_time desc limit #{offset}, #{limit} </select> <select id="selectDiscussPostRows" resultType="int"> select count(id) from discuss_post where status != 2 <if test="userId != 0"> and user_id = #{userId} </if> </select> </mapper>
配置和以前的user-mapper配置相同,只是namespace须要更改成当前的。注意这个<if>语句是为了判断是显示首页,仍是显示用户我的首页(这个功能未来实现),配置完成以后进行测试。
若是测试对数据库的操做无误,DAO层部分至此结束。
@Service public class DiscussPostService { @Autowired private DiscussPostMapper discussPostMapper; public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit){ return discussPostMapper.selectDiscussPosts(userId, offset, limit); } public int findDiscussPostRows(int userId) { return discussPostMapper.selectDiscussPostRows(userId); } }
首先在Service层对上述两个功能进行实现,这时候须要考虑一个问题,DisscusPost 对象中的userId意味着用户的ID,可是在之后调取信息时候确定不能直接使用这个数字而是使用用户名,因此这时候有两种实现方式:一是在SQL查询时直接关联查询,二是针对每个DisscusPost查询相应的用户。这里采用第二种方式,是为了未来采用redis缓存数据时候有必定好处。
这个功能是User相关的(用户相关),因此在UserService中添加方法:
@Service public class UserService { @Autowired private UserMapper userMapper; public User findUserById(int id) { return userMapper.selectById(id); } }
这两个功能相对简单,Service层至此结束。
@Controller public class HomeController { @Autowired private DiscussPostService discussPostService; @Autowired private UserService userService; @RequestMapping(path = "/index", method = RequestMethod.GET) public String getIndexPage(Model model) { List<DiscussPost> list = discussPostService.findDiscussPosts(0,0,10); List<Map<String, Object>> discussPosts = new ArrayList<>(); if(list != null) { //list中每一个元素装的是一个map,map中含有两个元素,一个帖子信息,一个用户,方便thymeleaf操做 for(DiscussPost post : list) { Map<String, Object> map = new HashMap<>(); map.put("post", post); User user = userService.findUserById(post.getId()); map.put("user", user); discussPosts.add(map); } } model.addAttribute("discussPosts",discussPosts); return "/index"; } }
这里没有写@ResponseBody由于咱们返回的是一个html。有两种实现方式,可回顾上篇博客。
其中前端文件html,css,js等均已给出,本篇不对前端知识进行总结描述。
其中,在首页index.html中,咱们利用thymeleaf引擎对帖子列表进行循环,后面须要加上th:,这是和静态页面不一样的地方。
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
<!-- 帖子列表 --> <ul class="list-unstyled"> <!-- th:each="" 循环方式,这里引用map对象,即list中的map --> <li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}"> <a href="site/profile.html"> <!-- 用户头像是动态的,map.user实际上是map.get("user"),后面也是get操做,会自动识别 --> <img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width:50px;height:50px;"> </a> <div class="media-body"> <h6 class="mt-0 mb-3"> <!-- 帖子标题动态,其中utext能够直接将转义字符呈现出来,text则不能够 --> <a href="#" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a> <!-- if标签 --> <span class="badge badge-secondary bg-primary" th:if="${map.post.type==1}">置顶</span> <span class="badge badge-secondary bg-danger" th:if="${map.post.status==1}">精华</span> </h6> <div class="text-muted font-size-12"> <!-- 时间转义 --> <u class="mr-3" th:utext="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b> <ul class="d-inline float-right"> <!-- 目前暂不处理 --> <li class="d-inline ml-2">赞 11</li> <li class="d-inline ml-2">|</li> <li class="d-inline ml-2">回帖 7</li> </ul> </div> </div> </li> </ul>
呈现效果以下:(此项目的前端部分都是根据已有的,仿牛客网设计)
第一页共10条帖子,固然此时第二页尚未设计。
注意:可能出现的bug有:引入的bootstrap和jQuery失效,这样会形成页面显示有问题。若是遇到这种问题,可在html中更换连接。
demo完成以后,须要思考的是:这时候点击帖子其实是没有信息返回的,包括页码,都没有返回信息,咱们接下来须要作的就是这一步。
接下来咱们须要实现的是分页,真正把页码部分给利用起来。首先在entity文件中,创建Page对象。创建一系列须要的方法,方便在index.html中使用。
//封装分页相关信息 public class Page { //当前页码 private int current = 1; //显示上限 private int limit = 10; //记录数量(计算总页数) private int rows; //查询路径 private String path; //get和set省略了,注意判断set,好比setRows,rows要大于等于0,current要大于等于1 /* 获取当前页的起始行 */ public int getOffset() { return (current-1) * limit; } //获取总页数 public int getTotal() { if(rows % limit == 0) return rows/limit; else return rows/limit + 1; } //获取起始页码以及结束页码 public int getFrom() { int from = current - 2; return from < 1 ? 1 : from; } public int getTo() { int to = current + 2; int total = getTotal(); return to > total ? total : to; } }
Controller中只须要添加两行代码:
//方法调用前,SpringMVC会自动实例化model和page,并将page注入model //因此在thymeleaf中能够直接访问page对象中的数据 page.setRows(discussPostService.findDiscussPostRows(0)); page.setPath("/index");
接下来介绍index.html中关于分页部分的代码,其中有些thymeleaf相关代码须要注意,已添加注释。
<!-- 分页 --> <nav class="mt-5" th:if="${page.rows>0}"> <ul class="pagination justify-content-center"> <li class="page-item"> <!-- 小括号的意义 /index?current=1 --> <a class="page-link" th:href="@{${page.path}(current=1)}">首页</a> </li> <!-- disabled指点击无效,好比第一页点上一页无效 --> <li th:class="|page-item ${page.current==1?'disabled':''}|"> <a class="page-link" th:href="@{${page.path}(current=${page.current-1})}">上一页</a> </li> <!-- 这里是调用numbers中创建两个数为起点和终点的数组 --> <!-- active这里是点亮 --> <li th:class="|page-item ${i==page.current?'active':''}|" th:each="i:${#numbers.sequence(page.from,page.to)}"> <a class="page-link" href="#" th:text="${i}">1</a> </li> <li th:class="|page-item ${page.current==page.total?'disabled':''}|"> <a class="page-link" th:href="@{${page.path}(current=${page.current+1})}">下一页</a> </li> <li class="page-item"> <a class="page-link" th:href="@{${page.path}(current=${page.total})}">末页</a> </li> </ul> </nav>
这里的跳转连接:/index?current=x,这个current实际是根据请求改变的,进而current改变以后再次请求,页面发生改变。注意理解一下程序流程。
至此,部分分页组件(直接点击页码尚未完成)开发完成,效果以下:
注册功能首先须要服务器向用户发送激活邮件进行验证。
Spring Email参考文档:https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#spring-integration
maven仓库中找到依赖进行声明:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>2.5.2</version> </dependency>
大体思路:
过程:
首先须要在application.properties做如下配置:
# MailProperties spring.mail.host=smtp.qq.com spring.mail.port=465 spring.mail.username=422374979@qq.com spring.mail.password=QQ邮箱的话须要激活码,其余邮箱的话须要密码 #表示启用的安全的协议 spring.mail.protocol=smtps #采用SSL安全链接 spring.mail.properties.mail.smtp.ssl.enable=true
这时候邮件发送类放在DAO,Service等以上提到的包中显然不合适,创建util工具包,创建以下类:
@Component public class MailClient { private static final Logger logger = LoggerFactory.getLogger(MailClient.class); @Autowired private JavaMailSender mailSender; @Value("${spring.mail.username}") private String from; public void sendMail(String to, String subject, String content) { try { MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage); helper.setFrom(from); helper.setTo(to); helper.setSubject(subject); helper.setText(content,true); //加true,会认为内容支持html文本 mailSender.send(helper.getMimeMessage()); } catch (MessagingException e) { logger.error("发送邮件失败" + e.getMessage()); } } }
由于这个不属于controller,dao,service这三层框架中任何一层,因此用的注解为@Component,声明Bean
以上的接口等若是是自学,且想深刻了解,能够查找博客,不过最全的仍是官方文档,上文已给出连接。
测试类进行测试:
@Test public void testTextMail() { mailClient.sendMail("gaoyuan206@gmail.com","TEST","Welcome"); }
效果如图:
发送HTML邮件,利用thymeleaf创建动态模板,如下进行示例:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>邮件示例</title> </head> <body> <p>hello,world.<span style="color:red;" th:text="${username}"></span></p> </body> </html>
测试代码以下:
@Test public void testHtmlMail() { Context context = new Context(); context.setVariable("username","gaoyuan"); String content = templateEngine.process("/mail/demo",context); //templates文件下的路径 System.out.println(content); mailClient.sendMail("gaoyuan206@gmail.com","TEST1",content); }
这里面调用了TemplateEngine类(SpringMVC中的核心类),会将HTML邮件的内容转为字符串。
此外,context是org.thymeleaf.context.Context。在这里的做用是声明了动态模板中的变量。
首先思考注册功能的具体流程:
开发日记(一)中公布的源码已有前端代码,templates/site/register.html
对该代码进行thymeleaf声明,以及相对路径更改。
对Index.html进行必定更改:
<header class="bg-dark sticky-top" th:fragment="header">
这里的意思是对header代码块进行声明,同时在register.html进行声明:
<header class="bg-dark sticky-top" th:replace="index::header">
这样的话,/register页面会复用/index页面的header。
建议读者对thymeleaf的相关知识进行必定的了解,本篇博客注重于实战。
首先对访问注册页面进行实现,很是简单,创建LoginController.class:
@RequestMapping(path = "/register", method = RequestMethod.GET) public String getRegisterPage() { return "/site/register"; }
在提交注册数据的过程当中,须要对字符串进行必定的处理,接下来插入一个新的包:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency>
在application.properties中配置域名:
# community community.path.domain=http://localhost:8080
目前项目没上线,直接配置为tomcat主机名。
在工具类目录下新建CommunityUtil.class,创建项目中须要用到的一些方法。
public class CommunityUtil { //生成随机字符串(用于激活码) public static String generateUUID() { return UUID.randomUUID().toString().replaceAll("-",""); } //MD5加密 public static String md5(String key) { if(StringUtils.isBlank(key)) { return null; //即便是空格,也会认为空 } return DigestUtils.md5DigestAsHex(key.getBytes()); //将传入结果加密成一个十六进制的字符串返回,要求参数为byte } }
以上为注册功能中涉及到的字符串处理方法。
密码咱们采用MD5加密,该类加密方式只能加密,不能解密:
假如说 hello加密为avblkjafdlkja,是不能有后者解密为前者的。可是只有这样还不够安全,由于简单字符串的加密结果都是固定的。
所以咱们对密码采用 password + salt(加一个随机字符串),这样的话即便密码设置为简单字符串,也会较为安全。
这是涉及到的字符串处理逻辑。
接下来介绍Service层如何编码,进行注册用户,发送激活邮件:
这个属于用户服务,在UserSevice中进行添加:
public Map<String, Object> register(User user) { Map<String, Object> map = new HashMap<>(); //空值处理 if(user==null) { throw new IllegalArgumentException("参数不能为空!"); } if(StringUtils.isBlank(user.getUsername())) { map.put("usernameMsg", "帐号不能为空!"); return map; } if(StringUtils.isBlank(user.getPassword())) { map.put("passwordMsg", "密码不能为空!"); return map; } if(StringUtils.isBlank(user.getEmail())) { map.put("emailMsg", "邮箱不能为空!"); return map; } //验证帐号 User u = userMapper.selectByName(user.getUsername()); if(u != null) { map.put("usernameMsg", "该帐号已存在"); return map; } //验证邮箱 u = userMapper.selectByEmail(user.getEmail()); if(u != null) { map.put("emailMsg", "该邮箱已被注册"); return map; } //注册用户 user.setSalt(CommunityUtil.generateUUID().substring(0,5)); //设置5位salt user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt())); //对密码进行加密 user.setType(0); user.setStatus(0); user.setActivationCode(CommunityUtil.generateUUID()); //激活码 user.setHeaderUrl(String.format("https://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000))); //设置随机头像,该Url对应的0到1000均为头像文件 user.setCreateTime(new Date()); userMapper.insertUser(user); //激活邮件 Context context = new Context(); //利用该对象携带变量 context.setVariable("email",user.getEmail()); // http://localhost:8080/community/activation/101(user_id)/code(ActivationCode) 激活路径 String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode(); context.setVariable("url", url); String content = templateEngine.process("/mail/activation", context); mailClient.sendMail(user.getEmail(), "激活帐号", content); return map; //若是map为空,说明没有问题 }
注意:该代码块只是部分代码,省略了注入对象等简单代码。
激活邮件的动态模板为:templates/site/activation.html,改成thymeleaf适用便可
<!doctype html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/> <title>牛客网-激活帐号</title> </head> <body> <div> <p> <b th:text="${email}">xxx@xxx.com</b>, 您好! </p> <p> 您正在注册牛客网, 这是一封激活邮件, 请点击 <!-- 这里相似于markdown的 []() --> <a th:href="${url}">此连接</a>, 激活您的牛客帐号! </p> </div> </body> </html>
接下来,处理Controller层逻辑,LoginController.class:
@RequestMapping(path = "/register", method = RequestMethod.POST) public String register(Model model, User user) { Map<String, Object> map = userService.register(user); if(map == null || map.isEmpty()) { model.addAttribute("msg","注册成功,咱们已经向您的邮箱发送了一封激活邮件,请尽快激活"); model.addAttribute("target", "/community/index"); return "/site/operate-result"; }else { model.addAttribute("usernameMsg", map.get("usernameMsg")); model.addAttribute("passwordMsg", map.get("passwordMsg")); model.addAttribute("emailMsg", map.get("emailMsg")); return "/site/register"; } }
该请求为POST请求,由于要向服务器提交注册信息。/site/operate-result地址为注册成功的html文件,公布源码中能够查看。
与此同时,咱们须要考虑,若是注册过程当中,发生错误信息了,继续返回register,前端部分须要做如下处理(部分代码):
<div class="form-group row"> <label for="username" class="col-sm-2 col-form-label text-right">帐号:</label> <div class="col-sm-10"> <input type="text" class="form-control" th:value="${user!=null?user.username:''}" id="username" name="username" placeholder="请输入您的帐号!" required> <div class="invalid-feedback"> 该帐号已存在! </div> </div> </div>
user!=null?user.username:'' 这句话是进行赋默认值,若是错误以后返回该页面,保存上次输入的信息,if判断上次是否输入user信息
接下来对代码进行测试,开启debug模式:
查找了一个数据库中已存在的username进行注册
成功状况:
自动跳转回首页:
邮箱已接收到邮件:
可是在到目前为止,激活连接是无效的,由于咱们还没进行这一步骤,接下来进行激活连接相关设计:
首先须要考虑的是激活的时候可能会有三种状况:
首先在util目录下创建一个常量接口:
//定义常量 public interface CommunityConstant { //激活成功 int ACTIVATION_SUCCESS = 0; //重复激活 int ACTIVATION_REPEAT = 1; //激活失败 int ACTIVATION_FAILURE = 2; }
实际上,激活连接只须要咱们向数据库进行访问,当HTTP请求路径中的激活码部分和数据库中相等,将数据库中用户的状态改成已激活便可。
在UserService中添加该方法。
// http://localhost:8080/community/activation/101(user_id)/code(ActivationCode) @RequestMapping(path = "/activation/{userId}/{code}", method = RequestMethod.GET) public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) { int result = userService.activation(userId, code); if(result == ACTIVATION_SUCCESS){ model.addAttribute("msg","激活成功,您的帐号已经能够正常使用!"); model.addAttribute("target", "/community/login"); }else if(result == ACTIVATION_REPEAT){ model.addAttribute("msg","无效操做,该帐号已经激活过了!"); model.addAttribute("target", "/community/index"); }else{ model.addAttribute("msg","激活失败,您提供的激活码不正确!"); model.addAttribute("target", "/community/index"); } return "/site/operate-result"; }
这里咱们不须要提交数据,采用GET请求便可,可是咱们须要复用operate-result动态模板,因此须要利用model添加变量。
至于/login.html已给出前端源码,目前只完成了注册功能,暂时只响应一下界面,下个博客再继续开发登陆功能。
点击邮件中的连接,效果以下:
成功以后跳转到登陆页面。
本篇博客关注于社交网站的首页实现和注册功能实现。须要理解MVC这三层概念,以及软件设计的三层架构。一般来讲,首先实现数据层,再设计服务层,最终实现视图层,可是有些是不须要数据层的,好比注册功能,咱们已经在设计首页的时候创建了用户实体,因此在开发注册功能时,直接添加UserService便可。另外,发送邮件调用了Springmail,以及注册过程当中处理字符串调用了Commonslang。总的来讲,在开发过程当中,须要借助成熟的包,熟悉它们的API,参考官方文档,这是很是重要的。