社交網站後端項目開發日記(二)

本項目目標是開發一個社區網站,擁有發帖、討論、搜索、登陸等一個正常社區擁有的功能。涉及到的版本參數爲: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

1. 開發網站首頁

開發流程實質上就是一次請求的執行過程。

image-20210715231657983

Controlloer(視圖層)依賴於Service(表現層)依賴於DAO(數據訪問層),因此開發過程當中能夠從DAO開始,依次進行開發。

首頁會有許多個功能,首先咱們須要實現一個簡單的demo,以後對功能進行豐富便可。

首先計劃開發頁面顯示10個帖子,進行分頁。

數據庫中的TABLE以下所示:

image-20210715234333573

其中,comment_count意義爲評論數量。

1.1 DAO層開發

首先在項目.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層部分至此結束。

1.2 Service層開發

@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層至此結束。

1.3 Controller層開發

@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等均已給出,本篇不對前端知識進行總結描述。

1.4 index.html相關

其中,在首頁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>

呈現效果以下:(此項目的前端部分都是根據已有的,仿牛客網設計)

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 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改變以後再次請求,頁面發生改變。注意理解一下程序流程。

至此,部分分頁組件(直接點擊頁碼尚未完成)開發完成,效果以下:

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做如下配置:

# 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");
}

效果如圖:

image-20210723031536094

發送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。在這裏的做用是聲明瞭動態模板中的變量。

image-20210725013326701

image-20210725013104826

2.2 開發註冊功能

首先思考註冊功能的具體流程:

image-20210725015253222

開發日記(一)中公佈的源碼已有前端代碼,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進行註冊

image-20210725035708443

成功狀況:

image-20210725035820472

自動跳轉回首頁:

image-20210725041321209

郵箱已接收到郵件:

image-20210725040220034


可是在到目前爲止,激活連接是無效的,由於咱們還沒進行這一步驟,接下來進行激活連接相關設計:

首先須要考慮的是激活的時候可能會有三種狀況:

  • 激活成功
  • 已經激活過了,再次激活重複操做無效
  • 激活失敗,激活路徑錯誤

首先在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已給出前端源碼,目前只完成了註冊功能,暫時只響應一下界面,下個博客再繼續開發登陸功能。

點擊郵件中的連接,效果以下:

image-20210725225513808

成功以後跳轉到登陸頁面。

image-20210725225442362

3. 總結

本篇博客關注於社交網站的首頁實現和註冊功能實現。須要理解MVC這三層概念,以及軟件設計的三層架構。一般來講,首先實現數據層,再設計服務層,最終實現視圖層,可是有些是不須要數據層的,好比註冊功能,咱們已經在設計首頁的時候創建了用戶實體,因此在開發註冊功能時,直接添加UserService便可。另外,發送郵件調用了Springmail,以及註冊過程當中處理字符串調用了Commonslang。總的來講,在開發過程當中,須要藉助成熟的包,熟悉它們的API,參考官方文檔,這是很是重要的。

相關文章
相關標籤/搜索