Spring Boot - 個人博客 - 博客管理


在這裏插入圖片描述


1. 需求分析

首先根據設計的靜態HTML頁面分析下所有的需求,博客管理頁如上所示。整體上來看,博客管理頁分爲三大部分內容:

  • 切換欄:這裏指的是導航欄下的那部分,可以實現博客列表和博客新增。編輯頁之間的切換。由於並不涉及其他的操作,實現頁較簡單
  • 搜索欄:搜索欄主要依據三方面的信息:標題、分類和是否是推薦博客,當獲取到信息後,點擊搜索按鈕進行搜索工作,並在下面的部分展示搜索的結果
  • 博客列表管理欄:博客信息包含:標題、類別、是否推薦、狀態、更新時間和相關的操作,相關操作主要包含編輯原有博文、刪除和新增

經過分析可知,前端進行結果的展示,已經獲取到某些輸入信息;而後主要針對於搜索、編輯、刪除和新增進行處理。


2. 前端處理

2.1 切換欄

切換欄的工作就是進行頁面的切換,因此th:href來實現頁面的跳轉即可。

<!--博客管理頁的功能切換欄-->
<div class="ui attached pointing menu">
    <div class="ui container">
        <div class="right menu">
            <a href="#" th:href="@{/admin/blogs/input}" class=" item">發佈</a>
            <a href="#" th:href="@{/admin/blogs}" class="teal active item">列表</a>
        </div>
    </div>
</div>

所涉及的後端處理邏輯後續再講。

2.2 搜索欄

搜索欄的頁面設計如下所示:

<div class="ui secondary segment form">
    <input type="hidden" name="page">
    <div class="inline fields">
        <!--標題欄-->
        <div class="field">
            <input type="text" name="title" placeholder="標題">
        </div>
        <!--分類欄-->
        <div class="field">
            <div class="ui labeled action input">
                <div class="ui type selection dropdown">
                    <input type="hidden" name="typeId">
                    <i class="dropdown icon"></i>
                    <div class="default text">分類</div>
                    <div class="menu">
                        <div th:each="type : ${types}" class="item" data-value="1" th:data-value="${type.id}" th:text="${type.name}">錯誤日誌</div>
                    </div>
                </div>
                <!--清除按鈕-->
                <button id="clear-btn" class="ui compact button">clear</button>
            </div>
        </div>
        <!--推薦按鈕-->
        <div class="field">
            <div class="ui checkbox">
                <input type="checkbox" id="recommend" name="recommend">
                <label for="recommend">推薦</label>
            </div>
        </div>
        <!--搜索按鈕-->
        <div class="field">
            <button type="button" id="search-btn" class="ui mini teal basic button"><i class="search icon"></i>搜索
            </button>
        </div>
    </div>
</div>

標題部分就是一個文本輸入框,因此使用<input type="text">即可。分類框這裏設計爲一個下拉框,當點擊時會在下拉框中展示當前已有的類別。推薦是一個單選框,因此使用<input type="checkbox">顯示推薦這兩個字,而單選框使用的是Sementic UI中的checkbox組件。

2.3 博客列表管理欄

博客管理欄設計如下所示

<div id="table-container">
    <!--使用th:fragment進行局部刷新-->
    <table th:fragment="blogList" class="ui compact teal table">
        <thead>
            <tr>
                <th></th>
                <th>標題</th>
                <th>類型</th>
                <th>推薦</th>
                <th>狀態</th>
                <th>更新時間</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            <!--
				使用th:each來遍歷查詢到的博客列表
				博客列表信息使用${}從前端傳來的page對象的content字段獲取
				iterStat表示狀態變量
			-->
            <tr th:each="blog,iterStat : ${page.content}">
                <!--取iterStat中的count屬性,即當前迭代對象的索引,從1開始-->
                <td th:text="${iterStat.count}">1</td>
                <!--使用th:text獲取博客標題-->
                <td th:text="${blog.title}">刻意練習清單</td>
                <!--使用th:text獲取Blog中type對象的name字段,即標籤-->
                <td th:text="${blog.type.name}">認知升級</td>
                <!--使用th:text獲取推薦信息,recommend爲Boolean字段,使用三元表達式進行判斷輸出-->
                <td th:text="${blog.recommend} ? '':''"></td>
                <!--使用th:text獲取發佈信息,published爲Boolean字段,使用三元表達式進行判斷輸出-->
                <td th:text="${blog.published} ? '發佈':'草稿'">草稿</td>
                <!--使用th:text獲取更新時間,同時使用#date.format方法進行日期的格式化-->
                <td th:text="${#dates.format(blog.updateTime,'yyyy-MM-dd')}">2017-10-02 09:45</td>
                <td>
                    <!--使用 th:href進行頁面跳轉-->
                    <a href="#" th:href="@{/admin/blogs/{id}/input(id=${blog.id})}"
                       class="ui mini teal basic button">編輯</a>
                    <a href="#" th:href="@{/admin/blogs/{id}/delete(id=${blog.id})}"
                       class="ui mini red basic button">刪除</a>
                </td>
            </tr>
        </tbody>
        <tfoot>
            <tr>
                <th colspan="7">
                    <!--通過前面隱藏域中的page來獲取分頁查詢結果-->
                    <div class="ui mini pagination menu" th:if="${page.totalPages}>1">
                        <a onclick="page(this)" th:attr="data-page=${page.number}-1" class="item"
                           th:unless="${page.first}">上一頁</a>
                        <a onclick="page(this)" th:attr="data-page=${page.number}+1" class=" item"
                           th:unless="${page.last}">下一頁</a>
                    </div>
                    <a href="#" th:href="@{/admin/blogs/input}"
                       class="ui mini right floated teal basic button">新增</a>
                </th>
            </tr>
        </tfoot>
    </table>
</div>

博客列表展示部分仍然是一個表單,這裏使用了Sementic UI的table組件。首先在<thead>標籤內部定義所有的標題部分,然後在<tbody>內部展示具體的每篇博客。編輯和刪除按鈕使用th:href來根據具體博客的id進行相應的操作。<tfoot>標籤部分主要進行上一頁和下一頁功能的實現,根據分頁查詢中設定的閾值來選擇顯示上一頁和下一頁這兩個按鈕,從而實現分頁顯示。

前端頁面就是進行使用Thymleaf來進行動態的渲染,只要通過Thymleaf來拿從後端傳過來的數據即可。


3. 後端處理

由前端處理分析可知,後端處理主要處理如下的幾個鏈接請求:

  • /admin/blogs
  • /admin/blogs/input
  • /admin/blogs/{id}/input
  • /admin/blogs/{id}/delete
  • /admin/blogs/search

3.1 /admin/blogs - get

首先來處理 /admin/blogs,它指向的就是博客管理頁,因此處理該請求的方法應該向前端傳遞所需的類別列表和具體的分頁查詢結果

@Controller
@RequestMapping("/admin")
public class BlogController {

    //博客列表頁面鏈接
    private static final String LIST = "admin/blogs";

    @Autowired
    private BlogService blogService;

    @Autowired
    private TypeService typeService;

    @GetMapping("/blogs")
    public String blogs(@PageableDefault(size = 5, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
                        BlogQuery blog, Model model) {

        // 獲取所有已有的博客類別
        // types : 類別結果
        model.addAttribute("types", typeService.listType());

        // 分頁查詢
        // page : 分頁查詢結果
        model.addAttribute("page", blogService.listBlog(pageable, blog));
        return LIST;
    }
}

根據前端頁面設計的需求,blogs()中應向前端傳遞當前所有的類別和分頁查詢的結果。因此,應該在Type的持久層定義方法listType()用於獲取所有的類別,在Blog的持久層定義listBlog()來獲取分頁查詢的結果。另外,因爲此時分頁查詢是根據搜索欄輸入的標題、類別和是否推薦信息進行的複合查詢,所以還應該定義一個封裝查詢條件的類BlogQuery,如下所示:

@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class BlogQuery {
    @Getter
    @Setter
    private String title;

    @Getter
    @Setter
    private Long typeId;  // 類別通過id標識

    @Getter
    @Setter
    private boolean recommend;
}

接着看如何實現listType()listBlog()

  • listType:在業務層TypeService中定義方法:

    public interface TypeService {
    
        // 獲取所有的類別
        List<Type> listType();
    }
    

    接口的實現類只需要調用持久層TypeRepository的findAll()即可

    @Service
    public class TypeServiceImpl implements TypeService {
    
        @Autowired
        TypeRepository typeRepository;
        
          @Override
        public List<Type> listType() {
            return typeRepository.findAll();
        }
    }
    
  • listBlog:方法的參數有兩個:

    • Pageable pageable:實現分頁查詢需傳入的參數,另外使用@PageableDefault註解設置了分頁查詢的一些默認值,如每頁最多5條記錄,並更具updateTime倒序排列
    • QueryBlog blog:用於封住從前端獲取的查詢項,後端方法中根據該查詢項進行查詢

    首先在業務層BlogService中定義方法:

    public interface BlogService {
        // 根據複合查詢條件獲取博客列表,實現分頁查詢
        Page<Blog> listBlog(Pageable pageable, BlogQuery blog);
    }
    

    接口的實現類爲:

    @Service
    public class BlogServiceImpl implements BlogService {
    
    
        @Autowired
        private BlogRepository blogRepository;
         @Override
        public Page<Blog> listBlog(Pageable pageable, BlogQuery blog) {
            // 這裏調用了JpaSpecificationExecutor中的findAll方法,方法的參數爲Specification對象
            return blogRepository.findAll(new Specification<Blog>() {
                // 重寫toPredicate方法,添加查詢條件
                @Override
                public Predicate toPredicate(Root<Blog> root,
                                             CriteriaQuery<?> cq,
                                             CriteriaBuilder cb) {
                    List<Predicate> predicates  = new ArrayList<>();
                    // 如果輸入了標題信息,則根據標題構建查詢語句
                    // 這裏使用模糊查詢
                    if (!"".equals(blog.getTitle()) && blog.getTitle() != null){
                        predicates.add(cb.like(root.<String>get("title"), "%" + blog.getTitle() + "%"));
                    }
                    // 如果輸入了類型,則獲取輸入類型對應的類型id,將其作爲查詢條件
                    if (blog.getTypeId() != null) {
                        predicates.add(cb.equal(root.<Type>get("type").get("id"), blog.getTypeId()));
                    }
                    // 如果點了推薦,則同樣將其作爲查詢條件
                    if (blog.isRecommend()) {
                        predicates.add(cb.equal(root.<Boolean>get("recommend"), blog.isRecommend()));
                    }
                    // 構建複合查詢條件
                    cq.where(predicates.toArray(new Predicate[predicates.size()]));
                    return null;
                }
            }, pageable);
        }
    }
    

    由於使用了複合的查詢條件,對應的持久層BlogRepository還應繼承JpaSpecificationExecutor<Blog>接口。

    public interface BlogRepository extends JpaRepository<Blog, Long> , JpaSpecificationExecutor<Blog> {}
    

    經過業務層和持久層的處理,最終表現層的blogs()或獲取到相應的結果,並通過Model對象傳給前端。

3.2 /admin/blogs/search

它相應的search()用於根據用於輸入的查詢條件執行查詢,並刷新博客列表。它和前一部分使用listBlog(Pageable pageable, BlogQuery blog)不同之處在於,search()用於處理post請求。它通過JS動態的獲取輸入的信息,然後發送請求,實現搜索和刷新。

@PostMapping("/blogs/search")
    public String search(@PageableDefault(size = 5, sort = {"updateTime"}, direction = Sort.Direction.DESC) Pageable pageable,
                         BlogQuery blog, Model model) {

        model.addAttribute("page", blogService.listBlog(pageable, blog));
        // 這裏只刷新結果部分的segment,實現區域刷新
        // 前端使用 th:fragment="blogList"指定刷新的區域
        return "admin/blogs :: blogList";
    }

3.3 /admin/blogs/input

它相應的方法爲input(),用於博客的新增。

@GetMapping("/blogs/input")
public String input(Model model) {
    model.addAttribute("types", typeService.listType());
    model.addAttribute("tags", tagService.listTag());
    model.addAttribute("blog", new Blog());
    
    return INPUT;
}

博客新增頁面如下所示
在這裏插入圖片描述

由於在博客保存/發佈時需要選擇分類和標籤,因此後端需向前端傳遞類別列表和標籤列表。此外,爲了保存新增的博客,還需要傳遞一個Blog對象。其中listType()在前一部分已經實現,listTag()和它幾乎一樣,所以這裏不加解釋的直接給出方法實現。

public interface TagService {
  
    List<Tag> listTag();
}
@Service
public class TagServiceImpl implements TagService {

    @Autowired
    private TagRepository tagRepository;
    
    @Override
    public List<Tag> listTag() {
        return tagRepository.findAll();
    }
}

那麼前端如何來處理後端傳遞過來tags、types和blog呢?前端實現如下所示:

<div class="m-container m-padded-tb-big">
    <div class="ui container">
        <!--
			博客新增本質上也是提交一個表單
			通過th:object來獲取blog
		-->
        <form id="blog-form" action="#" th:object="${blog}" th:action="@{/admin/blogs}" method="post" class="ui form">
            <!--隱藏域,用於標識博文是草稿還是已發佈-->
            <input type="hidden" name="published" th:value="*{published}">
            <!--博客id-->
            <input type="hidden" name="id" th:value="*{id}">
            <div class="required field">
                <div class="ui left labeled input">
                    <div class="ui selection compact teal basic dropdown label">
                        <!--博客flag-->
                        <input type="hidden" value="原創" name="flag" th:value="*{flag}">
                        <i class="dropdown icon"></i>
                        <div class="text">原創</div>
                        <div class="menu">
                            <div class="item" data-value="原創">原創</div>
                            <div class="item" data-value="轉載">轉載</div>
                            <div class="item" data-value="翻譯">翻譯</div>
                        </div>
                    </div>
                    <!--博客標題-->
                    <input type="text" name="title" placeholder="標題" th:value="*{title}">
                </div>
            </div>
			<!--博客具體內容-->
            <div class="required field">
                <div id="md-content" style="z-index: 1 !important;">
                    <textarea placeholder="博客內容" name="content" style="display: none" th:text="*{content}"></textarea>
                </div>
            </div>

            <div class="two fields">
                <div class="required field">
                    <div class="ui left labeled action input">
                        <label class="ui compact teal basic label">分類</label>
                        <div class="ui fluid selection dropdown">
                            <!--博客類別-->
                            <input type="hidden" name="type.id" th:value="*{type}!=null ? *{type.id}">
                            <i class="dropdown icon"></i>
                            <div class="default text">分類</div>
                            <div class="menu">
                                <div th:each="type : ${types}" class="item" data-value="1" th:data-value="${type.id}"
                                     th:text="${type.name}">錯誤日誌
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class=" field">
                    <div class="ui left labeled action input">
                        <!--博客標籤-->
                        <label class="ui compact teal basic label">標籤</label>
                        <div class="ui fluid selection multiple search  dropdown">
                            <input type="hidden" name="tagIds" th:value="*{tagIds}">
                            <i class="dropdown icon"></i>
                            <div class="default text">標籤</div>
                            <div class="menu">
                                <div th:each="tag : ${tags}" class="item" data-value="1" th:data-value="${tag.id}"
                                     th:text="${tag.name}">java
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
			<!--博客首圖地址-->
            <div class="required field">
                <div class="ui left labeled input">
                    <label class="ui teal basic label">首圖</label>
                    <input type="text" name="firstPicture" th:value="*{firstPicture}" placeholder="首圖引用地址">
                </div>
            </div>
			<!--博客描述信息-->
            <div class="required field">
                <textarea name="description" th:text="*{description}" placeholder="博客描述..." maxlength="200"></textarea>
            </div>
			<!--推薦、轉載聲明、讚賞、評論-->
            <div class="inline fields">
                <div class="field">
                    <div class="ui checkbox">
                        <input type="checkbox" id="recommend" name="recommend" checked th:checked="*{recommend}"
                               class="hidden">
                        <label for="recommend">推薦</label>
                    </div>
                </div>
                <div class="field">
                    <div class="ui checkbox">
                        <input type="checkbox" id="shareStatement" name="shareStatement" th:checked="*{shareStatement}"
                               class="hidden">
                        <label for="shareStatement">轉載聲明</label>
                    </div>
                </div>
                <div class="field">
                    <div class="ui checkbox">
                        <input type="checkbox" id="appreciation" name="appreciation" th:checked="*{appreciation}"
                               class="hidden">
                        <label for="appreciation">讚賞</label>
                    </div>
                </div>
                <div class="field">
                    <div class="ui checkbox">
                        <input type="checkbox" id="commentable" name="commentable" th:checked="*{commentable}"
                               class="hidden">
                        <label for="commentable">評論</label>
                    </div>
                </div>
            </div>
            <div class="ui error message"></div>
            <div class="ui right aligned container">
                <button type="button" class="ui button" onclick="window.history.go(-1)">返回</button>
                <button type="button" id="save-btn" class="ui secondary button">保存</button>
                <button type="button" id="publish-btn" class="ui teal button">發佈</button>
            </div>
        </form>
    </div>
</div>

當點擊保存或發佈按鈕時,程序就會發送/admin/blogs鏈接的post請求。

3.4 /admin/blogs - post

表現層對應的處理方法如下所示:

@Controller
@RequestMapping("/admin")
public class BlogController {

    // 博客輸入頁面鏈接
    private static final String INPUT = "admin/blogs-input";

    //博客列表頁面鏈接
    private static final String LIST = "admin/blogs";

    // 重定向
    private static final String REDIRECT_LIST = "redirect:/admin/blogs";

    @Autowired
    private BlogService blogService;

    @Autowired
    private TypeService typeService;

    @Autowired
    private TagService tagService;

    @PostMapping("/blogs")
    public String post(Blog blog, RedirectAttributes attributes, HttpSession session) {
        blog.setUser((User) session.getAttribute("user"));

        // 獲取創建好的博客指定的類別和標籤
        blog.setType(typeService.getType(blog.getType().getId()));
        blog.setTags(tagService.listTag(blog.getTagIds()));
        Blog b;
        if (blog.getId() == null) {
            b =  blogService.saveBlog(blog);
        } else {
            b = blogService.updateBlog(blog.getId(), blog);
        }

        if (b == null ) {
            attributes.addFlashAttribute("message", "操作失敗");
        } else {
            attributes.addFlashAttribute("message", "操作成功");
        }
        return REDIRECT_LIST;
    }
}

參數列表中使用blog來接收前端傳來的blog,首先從session中獲取user,設置blog的user字段。接着從前端的blog中獲取type對應的id來設置type字段。對於標籤來說,由於可以爲一篇博客指定多個標籤,所以前端傳過來的是類似"1,2,3"這樣的字符串,所以就需要在TagService中有相應的方法轉換爲tag的列表,然後用於設置blog的tags字段。

如果前端blog的id爲空,那麼表示此時是博客新增,否則是博客編輯。如果是新增,則調用BlogService的save()進行保存。如果保存成功,則重定向或之前的博客管理頁。

TypeService

public interface TypeService {

    // 根據Id獲取類別
    Type getType(Long id);
}

TypeService接口實現類

@Service
public class TypeServiceImpl implements TypeService {

    @Autowired
    TypeRepository typeRepository;

    @Transactional
    @Override
    public Type getType(Long id) {
        return typeRepository.getOne(id);
    }
}

tagService

public interface TagService {

    List<Tag> listTag(String ids);
}

接口實現類

@Service
public class TagServiceImpl implements TagService {

    @Autowired
    private TagRepository tagRepository; 
    @Override
    public List<Tag> listTag(String ids) { 
        return tagRepository.findAllById(convertToList(ids));
    }

    private List<Long> convertToList(String ids) {
        List<Long> list = new ArrayList<>();
        if (!"".equals(ids) && ids != null) {
            String[] idarray = ids.split(",");
            for (int i=0; i < idarray.length;i++) {
                list.add(new Long(idarray[i]));
            }
        }
        return list;
    }
}

BlogService

public interface BlogService {

    // 保存博客
    Blog saveBlog(Blog blog);
}

接口實現類

@Service
public class BlogServiceImpl implements BlogService {

    @Autowired
    private BlogRepository blogRepository;
    
    @Transactional
    @Override
    public Blog saveBlog(Blog blog) {
        // 如果執行的是新增操作,則需設置CreateTime和UpdateTime爲相同值
        // 同時設置Views值爲0
        if (blog.getId() == null){
            blog.setCreateTime(new Date());
            blog.setUpdateTime(new Date());
            blog.setViews(0);
        } else{
            // 否則,說明執行的是更新操作,只需要設置UpdateTime
            blog.setUpdateTime(new Date());
        }

        // 最後調用save方法保存博客
        return blogRepository.save(blog);
    }
}

如果是更新,則需調用BlogService中的updateBlog():

public interface BlogService {

    // 更新博客
    Blog updateBlog(Long id,Blog blog);
}
@Transactional
@Override
public Blog updateBlog(Long id, Blog blog) {
    // 使用Optional避免空指針異常
    Optional<Blog> b = blogRepository.findById(id);
    // 如果根據id可以找到blog,則得到對應的blog對象
    Blog blog1 = b.orElse(blog);

    // 如果該id對應的blog爲空,則告知博客不存在
    if (blog1 == null){
        throw new NotFoundException("該博客不存在...");
    }

    // 否則將更新內容後的博客進行復制
    BeanUtils.copyProperties(blog, blog1);
    // 設置更新時間
    blog1.setUpdateTime(new Date());
    // 調用save方法進行保存
    return blogRepository.save(blog1);
}

3.5 /admin/blogs/{id}/input

它用於編輯已有的博客,表現層對應的方法爲:

@Controller
@RequestMapping("/admin")
public class BlogController {

    // 博客輸入頁面鏈接
    private static final String INPUT = "admin/blogs-input";
    //博客列表頁面鏈接
    private static final String LIST = "admin/blogs";
    // 重定向
    private static final String REDIRECT_LIST = "redirect:/admin/blogs";

    @Autowired
    private BlogService blogService;

    @Autowired
    private TypeService typeService;

    @Autowired
    private TagService tagService;


    @GetMapping("/blogs/{id}/input")
    public String editInput(@PathVariable Long id, Model model) {
        
        setTypeAndTag(model);
        // 獲取要編輯的博客
        Blog blog = blogService.getBlog(id);
        // 獲取該博客所有的標籤對應的id
        blog.init();
   		// 此時直接傳遞的是根據id獲取的Blog對象,而不是new Blog()
        model.addAttribute("blog",blog);
        // 最後跳轉到博客新增頁進行編輯,然後再保存
        return INPUT;
    }


    private void setTypeAndTag(Model model) {
        model.addAttribute("types", typeService.listType());
        model.addAttribute("tags", tagService.listTag());
    }
}

不同於博客新增的操作之處在於,博客編輯向前端傳遞的是根據指定的id獲取到的具體博客blog,而且還需要傳入它已有的類別和標籤。最終編輯部分類似於新增操作,當編輯結束後點擊保存或發佈按鈕來實現博客的重新保存。具體的效果如下所示:
在這裏插入圖片描述

3.6 /admin/blogs/{id}/delete

博客刪除相對較簡單,直接根據具體的id刪除數據庫中的記錄即可,同時向前端傳遞消息,最後重定向回博客管理頁。相應的代碼實現如下:

@GetMapping("/blogs/{id}/delete")
public String delete(@PathVariable Long id,RedirectAttributes attributes) {
    blogService.deleteBlog(id);
    attributes.addFlashAttribute("message", "刪除成功");
    return REDIRECT_LIST;
}
public interface BlogService {

    // 刪除博客
    void deleteBlog(Long id);
}
@Service
public class BlogServiceImpl implements BlogService {

    @Autowired
    private BlogRepository blogRepository;
    @Transactional
    @Override
    public void deleteBlog(Long id) {
        blogRepository.deleteById(id);
    }
}

至此,博客管理的增、刪、改、查就已經全部實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章