1. 程式人生 > 實用技巧 >Day06_頁面釋出與課程管理

Day06_頁面釋出與課程管理

頁面釋出 課程管理

1 頁面釋出

1.1 技術方案

本專案使用MQ實現頁面釋出的技術方案如下:

技術方案說明:

1、平臺包括多個站點,頁面歸屬不同的站點。
2、釋出一個頁面應將該頁面釋出到所屬站點的伺服器上。
3、每個站點服務部署cms client程式,並與交換機繫結,繫結時指定站點Id為routingKey。
指定站點id為routingKey就可以實現cms client只能接收到所屬站點的頁面釋出訊息。
4、頁面釋出程式向MQ釋出訊息時指定頁面所屬站點Id為routingKey,將該頁面釋出到它所在伺服器上的cms client。

路由模式分析如下:
釋出一個頁面,需釋出到該頁面所屬的每個站點伺服器,其它站點伺服器不釋出。
比如:釋出一個門戶的頁面,需要釋出到每個門戶伺服器上,而使用者中心伺服器則不需要釋出。
所以本專案採用routing模式,用站點id作為routingKey,這樣就可以匹配頁面只發布到所屬的站點伺服器上。

頁面釋出流程圖如下:

1、前端請求cms執行頁面釋出。
2、cms執行靜態化程式生成html檔案。
3、cms將html檔案儲存到GridFS中。
4、cms向MQ傳送頁面釋出訊息。
5、MQ將頁面釋出訊息通知給Cms Client。
6、Cms Client從GridFS中下載html檔案。
7、Cms Client將html儲存到所在伺服器指定目錄。

1.2 頁面釋出消費方

1.2.1 需求分析

功能分析:

建立Cms Client工程作為頁面釋出消費方,將Cms Client部署在多個伺服器上,它負責接收到頁面釋出的訊息後從 GridFS中下載檔案在本地儲存。

需求如下:

1、將cms Client部署在伺服器,配置佇列名稱和站點ID。
2、cms Client連線RabbitMQ並監聽各自的“頁面釋出佇列”。
3、cms Client接收頁面釋出佇列的訊息。
4、根據訊息中的頁面id從mongodb資料庫下載頁面到本地。

呼叫dao查詢頁面資訊,獲取到頁面的物理路徑,呼叫dao查詢站點資訊,得到站點的物理路徑。

頁面物理路徑=站點物理路徑+頁面物理路徑+頁面名稱。

從GridFS查詢靜態檔案內容,將靜態檔案內容儲存到頁面物理路徑下。

1.2.2 建立Cms Client工程

包結構:

1、建立maven工程

pom檔案:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>xc-framework-parent</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../xc-framework-parent/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>xc-service-manage-cms-client</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-framework-model</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>
</project>

2、配置檔案

在resources下配置application.yml和logback-spring.xml。

application.yml的內容如下:

server:
  port: 31000
spring:
  application:
    name: xc-service-manage-cms-client
  data:
    mongodb:
      uri: mongodb://localhost:27017
      database: xc_cms
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /
xuecheng:
  mq:
    #cms客戶端監控的佇列名稱(不同的客戶端監控的佇列不能重複)
    queue: queue_cms_postpage_01
    routingKey: 5a751fab6abb5044e0d19ea1	#此routingKey為門戶站點ID

說明:在配置檔案中配置佇列的名稱,每個cms client在部署時注意佇列名稱不要重複

3、啟動類

package com.xuecheng.cms_manage_client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;

/**
 * @author HackerStar
 * @create 2020-08-08 10:59
 */
@SpringBootApplication
@EntityScan("com.xuecheng.framework.domain.cms")//掃描實體類
@ComponentScan(basePackages = "com.xuecheng.cms_manage_client")//掃描本專案下的所有類
@ComponentScan(basePackages = "com.xuecheng.framework")//掃描common工程下的類
public class ManageCmsClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ManageCmsClientApplication.class);
    }
}

1.2.3 RabbitmqConfig配置類

訊息佇列設定如下:

1、建立“ex_cms_postpage”交換機
2、每個Cms Client建立一個佇列與交換機繫結
3、每個Cms Client程式配置佇列名稱和routingKey,將站點ID作為routingKey。
package com.xuecheng.cms_manage_client.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class RabbitmqConfig {

    //佇列bean的名稱
    public static final String QUEUE_CMS_POSTPAGE = "queue_cms_postpage";
    //交換機的名稱
    public static final String EX_ROUTING_CMS_POSTPAGE="ex_routing_cms_postpage";
    //佇列的名稱
    @Value("${xuecheng.mq.queue}")
    public  String queue_cms_postpage_name;
    //routingKey 即站點Id
    @Value("${xuecheng.mq.routingKey}")
    public  String routingKey;
    /**
     * 交換機配置使用direct型別
     * @return the exchange
     */
    @Bean(EX_ROUTING_CMS_POSTPAGE)
    public Exchange EXCHANGE_TOPICS_INFORM() {
        return ExchangeBuilder.directExchange(EX_ROUTING_CMS_POSTPAGE).durable(true).build();
    }
    //宣告佇列
    @Bean(QUEUE_CMS_POSTPAGE)
    public Queue QUEUE_CMS_POSTPAGE() {
        Queue queue = new Queue(queue_cms_postpage_name);
        return queue;
    }

    /**
     * 繫結佇列到交換機
     *
     * @param queue    the queue
     * @param exchange the exchange
     * @return the binding
     */
    @Bean
    public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_CMS_POSTPAGE) Queue queue, @Qualifier(EX_ROUTING_CMS_POSTPAGE) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(routingKey).noargs();
    }

}

1.2.4 定義訊息格式

訊息內容採用json格式儲存資料,如下:

頁面id:釋出頁面的id

{
	"pageId":""
}

1.2.5 PageDao

1、使用CmsPageRepository 查詢頁面資訊

package com.xuecheng.cms_manage_client.dao;

import com.xuecheng.framework.domain.cms.CmsPage;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface CmsPageRepository extends MongoRepository<CmsPage,String> {
    //根據頁面名稱查詢
    CmsPage findByPageName(String pageName);
    //根據頁面名稱、站點Id、頁面webpath查詢
    CmsPage findByPageNameAndSiteIdAndPageWebPath(String pageName, String siteId, String pageWebPath);
}

2、使用CmsSiteRepository查詢站點資訊,主要獲取站點物理路徑

package com.xuecheng.cms_manage_client.dao;


import com.xuecheng.framework.domain.cms.CmsSite;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface CmsSiteRepository extends MongoRepository<CmsSite,String> {
}

1.2.6 PageService

在Service中定義儲存頁面靜態檔案到伺服器物理路徑方法:

package com.xuecheng.cms_manage_client.service;

import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.model.GridFSFile;
import com.xuecheng.cms_manage_client.dao.CmsPageRepository;
import com.xuecheng.cms_manage_client.dao.CmsSiteRepository;
import com.xuecheng.framework.domain.cms.CmsPage;
import com.xuecheng.framework.domain.cms.CmsSite;
import com.xuecheng.framework.domain.cms.response.CmsCode;
import com.xuecheng.framework.exception.ExceptionCast;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * @author HackerStar
 * @create 2020-08-08 11:23
 */
public class PageService {

    @Autowired
    CmsPageRepository cmsPageRepository;
    @Autowired
    CmsSiteRepository cmsSiteRepository;

    @Autowired
    GridFsTemplate gridFsTemplate;
    @Autowired
    GridFSBucket gridFSBucket;

    //將頁面html儲存到頁面物理路徑
    public void savePageToServerPath(String pageId) {
        Optional<CmsPage> optional = cmsPageRepository.findById(pageId);
        if(!optional.isPresent()) {
            ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);
        }
        //取出頁面物理路徑
        CmsPage cmsPage = optional.get();
        //頁面所屬站點
        CmsSite cmsSite = this.getCmsSiteById(cmsPage.getSiteId());
        //頁面物理路徑
        String pagePath = cmsSite.getSitePhysicalPath() + cmsPage.getPagePhysicalPath() + cmsPage.getPageName();
        //查詢頁面靜態檔案
        String htmlFileId = cmsPage.getHtmlFileId();
        InputStream inputStream = this.getFileById(htmlFileId);
        if(inputStream == null) {
            ExceptionCast.cast(CmsCode.CMS_GENERATEHTML_HTMLISNULL);
        }
        FileOutputStream fileOutputStream = null;

        try {
            fileOutputStream = new FileOutputStream(new File(pagePath));
            //將檔案內容儲存到服務物理路徑
            IOUtils.copy(inputStream,fileOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    //根據檔案id獲取檔案內容
    public CmsSite getCmsSiteById(String siteId) {
        Optional<CmsSite> optional = cmsSiteRepository.findById(siteId);
        if(optional.isPresent()) {
            return optional.get();
        }
        return null;
    }

    //根據檔案id獲取檔案內容
    public InputStream getFileById(String fileId) {
        try {
            GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
            GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
            GridFsResource gridFsResource = new GridFsResource(gridFSFile,gridFSDownloadStream);
            return gridFsResource.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

1.2.7 ConsumerPostPage

在cms client工程的mq包下建立ConsumerPostPage類,ConsumerPostPage作為釋出頁面的消費客戶端,監聽 頁面釋出佇列的訊息,收到訊息後從mongodb下載檔案,儲存在本地。

package com.xuecheng.cms_manage_client.mq;

import com.alibaba.fastjson.JSON;
import com.xuecheng.cms_manage_client.dao.CmsPageRepository;
import com.xuecheng.cms_manage_client.service.PageService;
import com.xuecheng.framework.domain.cms.CmsPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Map;
import java.util.Optional;

/**
 * @author HackerStar
 * @create 2020-08-08 13:12
 */
public class ConsumerPostPage {
    private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerPostPage.class);
    @Autowired
    CmsPageRepository cmsPageRepository;
    @Autowired
    PageService pageService;

    @RabbitListener(queues = {"${xuecheng.mq.queue}"})
    public void postPage(String msg) {
        //解析訊息
        Map map = JSON.parseObject(msg, Map.class);
        LOGGER.info("receive cms post page:{}", msg.toString());
        //取出頁面id
        String pageId = (String) map.get("pageId");
        //查詢頁面資訊
        Optional<CmsPage> optional = cmsPageRepository.findById(pageId);
        if (!optional.isPresent()) {
            LOGGER.error("receive cms post page,cmsPage is null:{}", msg.toString());
            return;
        }
        //將頁面儲存到伺服器物理路徑
        pageService.savePageToServerPath(pageId);
    }
}

1.3 頁面釋出生產方

1.3.1 需求分析

管理員通過cms系統釋出“頁面釋出”的消費,cms系統作為頁面釋出的生產方。

需求如下:

1、管理員進入管理介面點選“頁面釋出”,前端請求cms頁面釋出介面。
2、cms頁面釋出介面執行頁面靜態化,並將靜態化頁面儲存至GridFS中。
3、靜態化成功後,向訊息佇列傳送頁面釋出的訊息。
1) 獲取頁面的資訊及頁面所屬站點ID。
2) 設定訊息內容為頁面ID。(採用json格式,方便日後擴充套件)
3) 傳送訊息給ex_cms_postpage交換機,並將站點ID作為routingKey。

1.3.2 RabbitMQ配置

1、配置Rabbitmq的連線引數

在application.yml新增如下配置:

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /

2、在pom.xml新增依賴

<dependency> 
	<groupId>org.springframework.boot</groupId> 
	<artifactId>spring‐boot‐starter‐amqp</artifactId>
</dependency>

3、RabbitMQConfig配置

由於cms作為頁面釋出方要面對很多不同站點的伺服器,面對很多頁面釋出佇列,所以這裡不再配置佇列,只需要 配置交換機即可。

在cms工程只配置交換機名稱即可。

package com.xuecheng.manage_cms.config;

import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class RabbitmqConfig {

    //交換機的名稱
    public static final String EX_ROUTING_CMS_POSTPAGE="ex_routing_cms_postpage";
    /**
     * 交換機配置使用direct型別
     * @return the exchange
     */
    @Bean(EX_ROUTING_CMS_POSTPAGE)
    public Exchange EXCHANGE_TOPICS_INFORM() {
        return ExchangeBuilder.directExchange(EX_ROUTING_CMS_POSTPAGE).durable(true).build();
    }

}

1.3.3 Api 介面

在api工程定義頁面釋出介面:

@ApiOperation("釋出頁面")
public ResponseResult post(String pageId);

1.3.4 PageService

在PageService中定義頁面釋出方法,程式碼如下:

/**
     * 頁面釋出
     */
    public ResponseResult postPage(String pageId) {
        //執行靜態化
        String pageHtml = this.getPageHtml(pageId);
        if (StringUtils.isEmpty(pageHtml)) {
            ExceptionCast.cast(CmsCode.CMS_GENERATEHTML_HTMLISNULL);
        }
        //儲存靜態化檔案
        CmsPage cmsPage = saveHtml(pageId, pageHtml);
        //傳送訊息
        sentPostPage(pageId);
        return new ResponseResult(CommonCode.SUCCESS);
    }

    //儲存靜態頁面內容
    private CmsPage saveHtml(String pageId, String content) {
        //查詢頁面
        Optional<CmsPage> optional = cmsPageRepository.findById(pageId);
        if (!optional.isPresent()) {
            ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);
        }
        CmsPage cmsPage = optional.get();
        //儲存之前先刪除
        String htmlFileId = cmsPage.getHtmlFileId();
        if (StringUtils.isNotEmpty(htmlFileId)) {
            gridFsTemplate.delete(Query.query(Criteria.where("_id").is(htmlFileId)));
        }
        //儲存html檔案到GridFS
        InputStream inputStream = IOUtils.toInputStream(content);
        ObjectId objectId = gridFsTemplate.store(inputStream, cmsPage.getPageName());
        //檔案id
        String fileId = objectId.toString();
        //將檔案id儲存到cmspage中
        cmsPage.setHtmlFileId(fileId);
        cmsPageRepository.save(cmsPage);
        return cmsPage;
    }

    //傳送頁面釋出訊息
    private void sentPostPage(String pageId) {
        CmsPage cmsPage = this.getById(pageId);
        if (cmsPage == null) {
            ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);
        }
        Map<String, String> msgMap = new HashMap<>();
        msgMap.put("pageId", pageId);
        //訊息內容
        String msg = JSON.toJSONString(msgMap);
        //獲取站點id作為routingKey
        String siteId = cmsPage.getSiteId();
        //釋出訊息
        this.rabbitTemplate.convertAndSend(RabbitmqConfig.EX_ROUTING_CMS_POSTPAGE, siteId, msg);
    }

1.3.5 CmsPageController

編寫Controller實現api介面,接收頁面請求,呼叫service執行頁面釋出。

 @Override
    @PostMapping("/postPage/{pageId}")
    public ResponseResult post(@PathVariable("pageId") String pageId) {
        return pageService.postPage(pageId);
    }

1.4 頁面釋出前端

使用者操作流程:

1、使用者進入cms頁面列表。
2、點選“釋出”請求服務端介面,釋出頁面。 
3、提示“釋出成功”,或釋出失敗。

1.4.1 API方法

在 cms前端新增 api方法。

/*釋出頁面*/
export const page_postPage = id => {
  return http.requestPost(apiUrl + '/cms/page/postPage/' + id)
}

1.4.2 頁面

修改page_list.vue,添加發布按鈕。

<el-button size="small" type="primary" plain @click="postPage(page.row.pageId)">釋出</el-button>

新增頁面釋出事件:

postPage(id) {
        this.$confirm('確認釋出該頁面嗎?', '提示', {}).then(() => {
          cmsApi.page_postPage(id).then((res) => {
            if (res.success) {
              console.log('釋出頁面id=' + id);
              this.$message.success('釋出成功,請稍後檢視結果');
            } else {
              this.$message.error('釋出失敗');
            }
          })
        }).catch(() => {
        })
      }

1.5 測試

這裡測試輪播圖頁面修改、釋出的流程:

1、修改輪播圖頁面模板或修改輪播圖地址
注意:先修改頁面原型,頁面原型除錯正常後再修改頁面模板。
2、執行頁面預覽
3、執行頁面釋出,檢視頁面是否寫到網站目錄
4、重新整理門戶首頁並觀察輪播圖是否變化

1.6 思考

1、如果釋出到伺服器的頁面內容不正確怎麼辦?

2、一個頁面需要釋出很多伺服器,點選“釋出”後如何知道詳細的釋出結果?

3、一個頁面釋出到多個伺服器,其中有一個伺服器釋出失敗時怎麼辦?

2 課程管理

2.1 需求分析

線上教育平臺的課程資訊相當於電商平臺的商品。課程管理是後臺管理功能中最重要的模組。本專案為教學機構提 供課程管理功能,教學機構可以新增屬於自己的課程,供學生線上學習。

課程管理包括如下功能需求:

1、分類管理
2、新增課程
3、修改課程
4、預覽課程
5、釋出課程

使用者的操作流程如下:

1、進入我的課程
2、點選“新增課程”,進入新增課程介面
3、輸入課程基本資訊,點選提交
4、課程基本資訊提交成功,自動進入“管理課程”介面,點選“管理課程”也可以進入“管理課程”介面
5、編輯圖片。上傳課程圖片。
6、編輯課程營銷資訊。營銷資訊主要是設定課程的收費方式及價格。
7、編輯課程計劃,新增課程計劃。

2.2 教學方法

本模組對課程資訊管理功能的教學方法採用實戰教學方法,旨在通過實戰提高介面編寫的能力,具體教學方法如 下:

1、前後端工程匯入
教學管理前端工程採用與系統管理工程相同的技術,直接匯入後在此基礎上開發。
課程管理服務端工程採用Spring Boot技術構建,技術層技術使用Spring data Jpa(與Spring data Mongodb類 似)、Mybatis,直接匯入後在此基礎上開發。
2、課程計劃功能
課程計劃功能採用全程教學。
3、我的課程、新增課程、修改課程、課程營銷
我的課程、新增課程、修改課程、課程營銷四個功能採用實戰方式,課堂上會講解每個功能的需求及技術點,講解完成學生開始實戰,由導師進行技術指導。
4、參考文件
實戰結束提供每個功能的開發文件,學生參考文件並修正功能缺陷。

2.3 環境搭建

2.3.1 搭建資料庫環境

  1. 建立資料庫

課程管理使用MySQL資料庫,建立課程管理資料庫:xc_course。

匯入xc_course.sql指令碼

  1. 資料表介紹

課程資訊內容繁多,將課程資訊分類儲存在如下表中:

2.3.2 匯入課程管理服務工程

1)持久層技術介紹:

課程管理服務使用MySQL資料庫儲存課程資訊,持久層技術如下:

1、spring data jpa:用於表的基本CRUD。
2、mybatis:用於複雜的查詢操作。
3、druid:使用阿里巴巴提供的spring boot整合druid包druid-spring-boot-starter管理連線池。

druid-spring-boot-starter地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

2)匯入工程

匯入資料下的“xc-service-manage-course.zip”。

2.3.3 匯入課程管理前端工程

課程管理屬於教學管理子系統的功能,使用使用者為教學機構的管理人員和老師,為保證系統的可維護性,單獨建立 一個教學管理前端工程。 教學管理前端工程與系統管理前端的工程結構一樣,也採用vue.js框架來實現。

從課程資料目錄拷貝xc-ui-pc-teach.zip到工程,使用webstorm開啟,啟動teach工程:

如果報錯,可以參考連結:https://www.cnblogs.com/artwalker/p/13368397.html

效果圖如下:

3 課程計劃

將資料中的兩個包,分別拷貝到WebStorm和IDEA中

3.1 需求分析

什麼是課程計劃?

課程計劃定義了課程的章節內容,學生通過課程計劃進行線上學習,下圖中右側顯示的就是課程計劃。

課程計劃包括兩級,第一級是課程的大章節、第二級是大章節下屬的小章節,每個小章節通常是一段視訊,學生點 擊小章節線上學習。

教學管理人員對課程計劃如何管理?

功能包括:新增課程計劃、刪除課程計劃、修改課程計劃等。

3.2 課程計劃查詢

3.2.1需求分析

課程計劃查詢是將某個課程的課程計劃內容完整的顯示出來,如下圖所示:

左側顯示的就是課程計劃,課程計劃是一個樹型結構,方便擴充套件課程計劃的級別。

在上邊頁面中,點選“新增課程計劃”即可對課程計劃進行新增操作。

點選修改可對某個章節內容進行修改。

點選刪除可刪除某個章節。

3.2.2 頁面原型

3.2.2.1 tree元件介紹

本功能使用element-ui 的tree元件來完成。

3.2.2.2 webstorm配置JSX

本元件用到了JSX語法,如下所示:

JSX 是Javascript和XML結合的一種格式,它是React的核心組成部分,JSX和XML語法類似,可以定義屬性以及子元素。唯一特殊的是可以用大括號來加入JavaScript表示式。遇到 HTML 標籤(以 < 開頭),就用 HTML 規則解析; 遇到程式碼塊(以 { 開頭),就用 JavaScript 規則解析。

下面是官方的一個例子:

設定方法如下:

1 、Javascript version 選擇 React JSX (如果沒有就選擇JSX Harmony)

2、HTML 型別檔案中增加vue

如果已經在vuetemplate中已存在.vue則把它改為.vue2(因為要在Html中新增.vue)

3.2.3 API介面

3.2.3.1 資料模型

1、表結構

2、模型類

課程計劃為樹型結構,由樹根(課程)和樹枝(章節)組成,為了保證系統的可擴充套件性,在系統設計時將課程計劃 設定為樹型結構。

package com.xuecheng.framework.domain.course;

import lombok.Data;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.io.Serializable;

/**
 * Created by admin on 2018/2/7.
 */
@Data
@ToString
@Entity
@Table(name="teachplan")
@GenericGenerator(name = "jpa-uuid", strategy = "uuid")
public class Teachplan implements Serializable {
    private static final long serialVersionUID = -916357110051689485L;
    @Id
    @GeneratedValue(generator = "jpa-uuid")
    @Column(length = 32)
    private String id;
    private String pname;
    private String parentid;
    private String grade;
    private String ptype;
    private String description;
    private String courseid;
    private String status;
    private Integer orderby;
    private Double timelength;
    private String trylearn;

}
3.2.3.2 自定義模型類

前端頁面需要樹型結構的資料來展示Tree元件,如下:

[{
	id: 1, label: '一級 1', children: 
  [{
		id: 4,
		label: '二級 1‐1'
	}] 
}]

自定義課程計劃結點類如下:

package com.xuecheng.framework.domain.course.ext;

import com.xuecheng.framework.domain.course.Teachplan;
import lombok.Data;
import lombok.ToString;

import java.util.List;

/**
 * Created by admin on 2018/2/7.
 */
@Data
@ToString
public class TeachplanNode extends Teachplan {
    List<TeachplanNode> children;
}
3.2.3.3 介面定義

根據課程id查詢課程的計劃介面如下,在api工程建立course包,建立CourseControllerApi介面類並定義介面方法 如下:

package com.xuecheng.api.course;

import com.xuecheng.framework.domain.course.ext.TeachplanNode;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

/**
 * @author HackerStar
 * @create 2020-08-08 19:20
 */
@Api(value = "課程", description = "課程")
public interface CourseControllerApi {
    @ApiOperation("課程計劃查詢")
    public TeachplanNode findTeachplanList(String courseId);
}

3.2.3 課程管理服務

3.2.3.1 Sql

課程計劃是樹型結構,採用表的自連線方式進行查詢,sql語句如下:

SELECT a.id one_id, a.pname one_pname, b.id two_id, b.pname two_pname, c.id three_id, c.pname three_pname
FROM teachplan a
         LEFT JOIN teachplan b ON a.id = b.parentid
         LEFT JOIN teachplan c ON b.id = c.parentid
WHERE a.parentid = '0'
  AND a.courseid = '402885816243d2dd016243f24c030002'
ORDER BY a.orderby, b.orderby, c.orderby
3.2.3.2 Dao
  1. mapper介面
package com.xuecheng.manage_course.dao;

import com.xuecheng.framework.domain.course.ext.TeachplanNode;
import org.apache.ibatis.annotations.Mapper;

/**
 * 課程計劃mapper
 * Created by Administrator.
 */
@Mapper
public interface TeachplanMapper {
    //課程計劃查詢
    public TeachplanNode selectList(String courseId);
}

2)mapper對映檔案

<?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.xuecheng.manage_course.dao.TeachplanMapper">

    <resultMap id="teachplanMap" type="com.xuecheng.framework.domain.course.ext.TeachplanNode">
        <id column="one_id" property="id"></id>
        <result column="one_pname" property="pname"></result>
        <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
            <id column="two_id" property="id"></id>
            <result column="two_pname" property="pname"></result>
            <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
                <id column="three_id" property="id"></id>
                <result column="three_pname" property="pname"></result>
            </collection>
        </collection>
    </resultMap>

    <select id="selectList" parameterType="java.lang.String"
            resultMap="teachplanMap">
        SELECT
        a.id one_id,
        a.pname one_pname,
        b.id two_id,
        b.pname two_pname,
        c.id three_id,
        c.pname three_pname
        FROM
        teachplan a
        LEFT JOIN teachplan b
        ON b.parentid = a.id
        LEFT JOIN teachplan c
        ON c.parentid = b.id
        WHERE a.parentid = '0'
        <if test="_parameter !=null and _parameter!=''">
            AND a.courseid = #{courseId}
        </if>

        ORDER BY a.orderby,
        b.orderby,
        c.orderby
    </select>
</mapper>

說明:針對輸入引數為簡單型別#{}中可以是任意型別,判斷引數是否為空要用 _parameter(它屬於mybatis的內建引數)

3.4.3.3 Service

建立CourseService類,定義查詢課程計劃方法。

@Service
public class CourseService {
    @Autowired
    TeachplanMapper teachplanMapper;

    @Autowired
    CourseBaseRepository courseBaseRepository;
    //查詢課程計劃
    public TeachplanNode findTeachplanList(String courseId){
        return teachplanMapper.selectList(courseId);
    }
}
3.4.3.4 Controller
@RestController
@RequestMapping("/course")
public class CourseController implements CourseControllerApi {

    @Autowired
    CourseService courseService;

    //查詢課程計劃 
    @Override
    @GetMapping("/teachplan/list/{courseId}")
    public TeachplanNode findTeachplanList(@PathVariable String courseId) {
        return courseService.findTeachplanList(courseId);
    }
}
3.4.3.5 測試

使用postman或swagger-ui測試查詢介面。

Get 請求:http://localhost:31200/course/teachplan/list/402885816243d2dd016243f24c030002

3.2.4 前端頁面

3.2.4.1 Api方法

定義課程計劃查詢的api方法:

/*查詢課程計劃*/ 
export const findTeachplanList = courseid => { 
	return http.requestQuickGet(apiUrl+'/course/teachplan/list/'+courseid) 
}
3.2.4.2 Api呼叫

1、在mounted鉤子方法中查詢課程計劃

定義查詢課程計劃的方法,賦值給資料物件teachplanList(course_plan.vue)

findTeachplan(){
	courseApi.findTeachplanList(this.courseid).then((res) => { 
		this.teachplanList = [];//清空樹 
		if(res.children){
				this.teachplanList = res.children; }
});

2、在mounted鉤子中查詢課程計劃(course_plan.vue)

mounted(){ 
	//課程id 
	this.courseid = this.$route.params.courseid; 
	//課程計劃 
	this.findTeachplan(); 
}

3、修改樹結點的標籤屬性(course_plan.vue)

課程計劃資訊中pname為結點的名稱,需要修改樹結點的標籤屬性方可正常顯示課程計劃名稱,如下:

defaultProps: { 
  children: 'children', 
  label: 'pname'
}

3.2.4.3 測試

3.3 新增課程計劃

使用者操作流程:

1、進入課程計劃頁面,點選“新增課程計劃”
2、開啟新增課程計劃頁面,輸入課程計劃資訊
3、點選提交

上級結點說明:

不選擇上級結點表示當前課程計劃為該課程的一級結點。

當新增該課程在課程計劃中還沒有節點時要自動新增課程的根結點。

3.3.1 頁面原型說明

新增課程計劃採用彈出視窗元件Dialog。

1、檢視部分

在course_plan.vue頁面新增新增課程計劃的彈出視窗程式碼:

 <el-dialog title="新增課程計劃" :visible.sync="teachplayFormVisible" >
      <el-form ref="teachplanForm"  :model="teachplanActive" label-width="140px" style="width:600px;" :rules="teachplanRules" >
        <el-form-item label="上級結點" >
          <el-select v-model="teachplanActive.parentid" placeholder="不填表示根結點">
            <el-option
              v-for="item in teachplanList"
              :key="item.id"
              :label="item.pname"
              :value="item.id">
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="章節/課時名稱" prop="pname">
          <el-input v-model="teachplanActive.pname" auto-complete="off"></el-input>
        </el-form-item>
        <el-form-item label="課程型別" >
          <el-radio-group v-model="teachplanActive.ptype">
            <el-radio class="radio" label='1'>視訊</el-radio>
            <el-radio class="radio" label='2'>文件</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="學習時長(分鐘)  請輸入數字" >
          <el-input type="number" v-model="teachplanActive.timelength" auto-complete="off" ></el-input>
        </el-form-item>
        <el-form-item label="排序欄位" >
          <el-input v-model="teachplanActive.orderby" auto-complete="off" ></el-input>
        </el-form-item>
        <el-form-item label="章節/課時介紹" prop="description">
          <el-input type="textarea" v-model="teachplanActive.description" ></el-input>
        </el-form-item>

        <el-form-item label="狀態" prop="status">
          <el-radio-group v-model="teachplanActive.status" >
            <el-radio class="radio" label="0" >未釋出</el-radio>
            <el-radio class="radio" label='1'>已釋出</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item  >
          <el-button type="primary" v-on:click="addTeachplan">提交</el-button>
          <el-button type="primary" v-on:click="resetForm">重置</el-button>
        </el-form-item>

      </el-form>
    </el-dialog>

2、資料模型

在資料模型中新增如下變數:

teachplayFormVisible:false,//控制新增視窗是否顯示
teachplanRules: {
          pname: [
            {required: true, message: '請輸入課程計劃名稱', trigger: 'blur'}
          ],
          status: [
            {required: true, message: '請選擇狀態', trigger: 'blur'}
          ]
        },
teachplanActive:{},

3、 新增按鈕

<el‐button type="primary" @click="teachplayFormVisible = true"> 新增課程計劃</el‐button>

4、定義表單提交方法和重置方法

 //提交課程計劃
      addTeachplan(){
        //校驗表單
        this.$refs.teachplanForm.validate((valid) => {
            if (valid) {
                //呼叫api方法
              //將課程id設定到teachplanActive
              this.teachplanActive.courseid = this.courseid
              courseApi.addTeachplan(this.teachplanActive).then(res=>{
                if(res.success){
                    this.$message.success("新增成功")
                    //重新整理樹
                    this.findTeachplan()
                }else{
                  this.$message.error(res.message)
                }

              })
            }
        })
      },
//重置表單
      resetForm(){
        this.teachplanActive = {}
      },

3.3.2 API介面

新增課程計劃

@Api(value="課程管理介面",description = "課程管理介面,提供課程的增、刪、改、查")
public interface CourseControllerApi {
    @ApiOperation("課程計劃查詢")
    public TeachplanNode findTeachplanList(String courseId);

    @ApiOperation("新增課程計劃")
    public ResponseResult addTeachplan(Teachplan teachplan);
}

3.3.3 課程管理服務

3.3.3.1 Dao
package com.xuecheng.manage_course.dao;

import com.xuecheng.framework.domain.course.Teachplan;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * @author HackerStar
 * @create 2020-08-08 20:33
 */
public interface TeachplanRepository extends JpaRepository<Teachplan, String> {
    //定義方法根據課程id和父結點id查詢出結點列表,可以使用此方法實現查詢根結點
    public List<Teachplan> findByCourseidAndParentid(String courseId, String parentId);
}
3.3.3.2 Service
package com.xuecheng.manage_course.service;

import com.xuecheng.framework.domain.course.CourseBase;
import com.xuecheng.framework.domain.course.Teachplan;
import com.xuecheng.framework.domain.course.ext.TeachplanNode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_course.dao.CourseBaseRepository;
import com.xuecheng.manage_course.dao.TeachplanMapper;
import com.xuecheng.manage_course.dao.TeachplanRepository;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

/**
 * @author Administrator
 * @version 1.0
 **/
@Service
public class CourseService {
    @Autowired
    TeachplanMapper teachplanMapper;

    @Autowired
    TeachplanRepository teachplanRepository;

    @Autowired
    CourseBaseRepository courseBaseRepository;
    //查詢課程計劃
    public TeachplanNode findTeachplanList(String courseId){
        return teachplanMapper.selectList(courseId);
    }

    @Transactional
    public ResponseResult addTeachplan(Teachplan teachplan) {

        if(teachplan == null ||
                StringUtils.isEmpty(teachplan.getPname()) ||
                StringUtils.isEmpty(teachplan.getCourseid())){
            ExceptionCast.cast(CommonCode.INVALID_PARAM);
        }
        //課程id
        String courseid = teachplan.getCourseid();
        //父結點的id
        String parentid = teachplan.getParentid();
        if(StringUtils.isEmpty(parentid)){
            //獲取課程的根結點
            parentid = getTeachplanRoot(courseid);
        }
        //查詢根結點資訊
        Optional<Teachplan> optional = teachplanRepository.findById(parentid);
        Teachplan teachplan1 = optional.get();
        //父結點的級別
        String parent_grade = teachplan1.getGrade();
        //建立一個新結點準備新增
        Teachplan teachplanNew = new Teachplan();
        //將teachplan的屬性拷貝到teachplanNew中
        BeanUtils.copyProperties(teachplan,teachplanNew);
        //要設定必要的屬性
        teachplanNew.setParentid(parentid);
        if(parent_grade.equals("1")){
            teachplanNew.setGrade("2");
        }else{
            teachplanNew.setGrade("3");
        }
        teachplanNew.setStatus("0");//未釋出
        teachplanRepository.save(teachplanNew);
        return new ResponseResult(CommonCode.SUCCESS);
    }

    //獲取課程的根結點
    public String getTeachplanRoot(String courseId){
        Optional<CourseBase> optional = courseBaseRepository.findById(courseId);
        if(!optional.isPresent()){
            return null;
        }
        CourseBase courseBase = optional.get();
        //呼叫dao查詢teachplan表得到該課程的根結點(一級結點)
        List<Teachplan> teachplanList = teachplanRepository.findByCourseidAndParentid(courseId, "0");
        if(teachplanList == null || teachplanList.size()<=0){
            //新新增一個課程的根結點
            Teachplan teachplan = new Teachplan();
            teachplan.setCourseid(courseId);
            teachplan.setParentid("0");
            teachplan.setGrade("1");//一級結點
            teachplan.setStatus("0");
            teachplan.setPname(courseBase.getName());
            teachplanRepository.save(teachplan);
            return teachplan.getId();

        }
        //返回根結點的id
        return teachplanList.get(0).getId();
    }
}
3.3.3.3 controller
package com.xuecheng.manage_course.controller;

import com.xuecheng.api.course.CourseControllerApi;
import com.xuecheng.framework.domain.course.Teachplan;
import com.xuecheng.framework.domain.course.ext.TeachplanNode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_course.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/course")
public class CourseController implements CourseControllerApi {

    @Autowired

    CourseService courseService;

    //查詢課程計劃
    @Override
    @GetMapping("/teachplan/list/{courseId}")
    public TeachplanNode findTeachplanList(@PathVariable String courseId) {
        return courseService.findTeachplanList(courseId);
    }

    //新增課程計劃
    @Override
    @PostMapping("/teachplan/add")
    public ResponseResult addTeachplan(Teachplan teachplan) {
        return courseService.addTeachplan(teachplan);
    }
}
3.3.3.4 測試

複雜一些的業務邏輯建議寫完服務端程式碼就進行單元測試。

使用swagger-ui或postman測試上邊的課程計劃新增介面。

3.3.5 前端

3.3.5.1 Api呼叫

1、定義 api方法(course.js)

/*新增課程計劃*/
export const addTeachplan = teachplah => {
  return http.requestPost(apiUrl+'/course/teachplan/add',teachplah)
}

2、呼叫api(course_plan.vue)

//提交課程計劃
      addTeachplan(){
        //校驗表單
        this.$refs.teachplanForm.validate((valid) => {
            if (valid) {
                //呼叫api方法
              //將課程id設定到teachplanActive
              this.teachplanActive.courseid = this.courseid
              courseApi.addTeachplan(this.teachplanActive).then(res=>{
                if(res.success){
                    this.$message.success("新增成功")
                    //重新整理樹
                    this.findTeachplan()
                }else{
                  this.$message.error(res.message)
                }

              })
            }
        })
      },	

3.3.5 測試

測試流程:

1、新建一個課程
2、向新建課程中新增課程計劃
新增一級結點
新增二級結點

未實現新建課程,所以直接在原有課程中新增課程計劃。