2021-07-25

社交网站后端项目开发日记(二)

本项目目标是开发一个社区网站,拥有发帖、讨论、搜索、登录等一个正常社区拥有的功能。涉及到的版本参数为:

  • JDK1.8
  • Maven3.8.1(直接集成到IDEA)
  • Springboot 2.5.1
  • tomcat 9.0.45
  • Mybatis
  • Mysql 8.0.15

参考网站(在使用框架过程中可能会看的开发文档):

https://mvnrepository.com/ 查找maven依赖

https://mybatis.org/mybatis-3/zh/index.html mybatis的官方文档,配置等都有说明

项目代码已发布到github https://github.com/GaoYuan-1/web-project

关于数据库文件,该篇博客中已有提到,可去文中github获取数据 MySQL基础篇(一)

本文介绍如何实现注册,发送激活邮件等内容。本系列下一篇博客将会开发登录功能或发布帖子功能等,最终将会把完整项目经历发布出来。

本系列主要介绍的是实战内容,对于理论知识介绍较少,适合有一定基础的人。

接上次开发日记(一)说明:

spring.datasource.url=jdbc:mysql://localhost:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong&allowPublicKeyRetrieval=true

在项目application.properties中添加一句allowPublicKeyRetrieval=true。否则每次打开项目需要将数据库启动,不然的话会出现公钥不识别的错误。

1. 开发网站首页

开发流程实质上就是一次请求的执行过程。

image-20210715231657983

Controlloer(视图层)依赖于Service(表现层)依赖于DAO(数据访问层),所以开发过程中可以从DAO开始,依次进行开发。

首页会有许多个功能,首先我们需要实现一个简单的demo,之后对功能进行丰富即可。

首先计划开发页面显示10个帖子,进行分页。

数据库中的TABLE如下所示:

image-20210715234333573

其中,comment_count意义为评论数量。

1.1 DAO层开发

首先在项目.entity文件中,建立DisscussPost实体(帖子信息),然后建立DiscussPostMapper。

@Mapperpublic 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的.

<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层部分至此结束。

1.2 Service层开发

@Servicepublic 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中添加方法:

@Servicepublic class UserService { @Autowired private UserMapper userMapper; public User findUserById(int id) {  return userMapper.selectById(id); }}

这两个功能相对简单,Service层至此结束。

1.3 Controller层开发

@Controllerpublic 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等均已给出,本篇不对前端知识进行总结描述。

1.4 index.html相关

其中,在首页index.html中,我们利用thymeleaf引擎对帖子列表进行循环,后面需要加上th:,这是和静态页面不同的地方。

<li th:each="map:${discussPosts}">
<!-- 帖子列表 --><ul > <!-- th:each="" 循环方式,这里引用map对象,即list中的map --> <li th:each="map:${discussPosts}">  <a href="site/profile.html">   <!-- 用户头像是动态的,map.user其实是map.get("user"),后面也是get操作,会自动识别 -->   <img th:src="${map.user.headerUrl}" alt="用户头像" >  </a>  <div >   <h6 >   <!-- 帖子标题动态,其中utext可以直接将转义字符呈现出来,text则不可以 -->   <a href="#" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>   <!-- if标签 -->   <span th:if="${map.post.type==1}">置顶</span>   <span th:if="${map.post.status==1}">精华</span>   </h6>   <div >   <!-- 时间转义 -->   <u 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 >    <!-- 目前暂不处理 -->    <li >赞 11</li>    <li >|</li>    <li >回帖 7</li>   </ul>   </div>  </div>     </li></ul>

呈现效果如下:(此项目的前端部分都是根据已有的,仿牛客网设计)

image-20210717084225057

image-20210717084239579

第一页共10条帖子,当然此时第二页还没有设计。

注意:可能出现的bug有:引入的bootstrap和jQuery失效,这样会造成页面显示有问题。如果遇到这种问题,可在html中更换链接。

demo完成之后,需要思考的是:这时候点击帖子实际上是没有信息返回的,包括页码,都没有返回信息,我们接下来需要做的就是这一步。

1.5 分页

接下来我们需要实现的是分页,真正把页码部分给利用起来。首先在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 th:if="${page.rows>0}"> <ul >  <li >   <!-- 小括号的意义 /index?current=1 -->   <a th:href="@{${page.path}(current=1)}">首页</a>  </li>  <!-- disabled指点击无效,比如第一页点上一页无效 -->  <li th:>   <a th:href="@{${page.path}(current=${page.current-1})}">上一页</a>  </li>  <!-- 这里是调用numbers中建立两个数为起点和终点的数组 -->  <!-- active这里是点亮 -->  <li th: th:each="i:${#numbers.sequence(page.from,page.to)}">   <a href="#" th:text="${i}">1</a>  </li>  <li th:>   <a th:href="@{${page.path}(current=${page.current+1})}">下一页</a>  </li>  <li >   <a th:href="@{${page.path}(current=${page.total})}">末页</a>  </li> </ul></nav>

这里的跳转链接:/index?current=x,这个current实际是根据请求改变的,进而current改变之后再次请求,页面发生改变。注意理解一下程序流程。

至此,部分分页组件(直接点击页码还没有完成)开发完成,效果如下:

image-20210717101700077

image-20210717101852153

2. 登录注册功能

注册功能首先需要服务器向用户发送激活邮件进行验证。

2.1 发送邮件

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>

大致思路:

image-20210723021235201

过程:

首先需要在application.properties作以下配置:

# MailPropertiesspring.mail.host=smtp.qq.comspring.mail.port=465spring.mail.username=422374979@qq.comspring.mail.password=QQ邮箱的话需要激活码,其他邮箱的话需要密码#表示启用的安全的协议spring.mail.protocol=smtps#采用SSL安全连接spring.mail.properties.mail.smtp.ssl.enable=true

这时候邮件发送类放在DAO,Service等以上提到的包中显然不合适,建立util工具包,建立如下类:

@Componentpublic 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

以上的接口等如果是自学,且想深入了解,可以查找博客,不过最全的还是官方文档,上文已给出链接。

测试类进行测试:

@Testpublic void testTextMail() { mailClient.sendMail("gaoyuan206@gmail.com","TEST","Welcome");}

效果如图:

image-20210723031536094

发送HTML邮件,利用thymeleaf建立动态模板,以下进行示例:

<!DOCTYPE html><html lang="en" 

测试代码如下:

@Testpublic 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。在这里的作用是声明了动态模板中的变量。

image-20210725013326701

image-20210725013104826

2.2 开发注册功能

首先思考注册功能的具体流程:

image-20210725015253222

开发日记(一)中公布的源码已有前端代码,templates/site/register.html

对该代码进行thymeleaf声明,以及相对路径更改。

对Index.html进行一定更改:

<header th:fragment="header">

这里的意思是对header代码块进行声明,同时在register.html进行声明:

<header 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中配置域名:

# communitycommunity.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.......

原文转载:http://www.shaoqun.com/a/895361.html

跨境电商:https://www.ikjzd.com/

笨鸟:https://www.ikjzd.com/w/2713

c88:https://www.ikjzd.com/w/1017.html

unsplash:https://www.ikjzd.com/w/756.html


本项目目标是开发一个社区网站,拥有发帖、讨论、搜索、登录等一个正常社区拥有的功能。涉及到的版本参数为:JDK1.8Maven3.8.1(直接集成到IDEA)Springboot2.5.1tomcat9.0.45MybatisMysql8.0.15参考网站(在使用框架过程中可能会看的开发文档):https://mvnrepository.com/查找maven依赖https://mybatis.or
dmm杂志:https://www.ikjzd.com/w/2026
西递宏村举办列入世界文化遗产20周年活动:http://www.30bags.com/a/223927.html
西方国家有哪些传统节日?:http://www.30bags.com/a/430854.html
西方人,你为什么不唱K?:http://www.30bags.com/a/245400.html
西贡、会安体验越南style :http://www.30bags.com/a/414066.html
好湿好紧好浪好大好爽 老师在教室猛烈要了我:http://lady.shaoqun.com/m/a/247591.html
将她双腿向左右开的更大 把美腿扛在肩上疯狂输出:http://lady.shaoqun.com/m/a/248361.html
口述:我和良家妇女火热的婚外关系(4/4):http://lady.shaoqun.com/m/a/38731.html
外出工作的年轻女性想用"夫妻生活"做什么?听听这些人的声音!:http://lady.shaoqun.com/a/431470.html
小说:不,我要去洗澡:http://lady.shaoqun.com/a/431471.html
女人"性冷淡"。吃什么可以帮助他们?提高性功能?:http://lady.shaoqun.com/a/431472.html
女人希望被你的睡眠所暗示。女人明白你有趣的潜台词吗:http://lady.shaoqun.com/a/431473.html

No comments:

Post a Comment