微服务[学成在线] day06:页面发布以及课程管理

SpringCloud微服务之集成thymeleaf访问html页面/静态页面&热部署 以spring boot框架为基础的app项目配置thymeleaf后,则可以在app项目中编写静态文件然后使用controller访问静态页面。除了最基本的html+css+js的语法使用,thymeleaf模版还提供了很多强大的语法帮助我们更好地编写静态页面。 1、修改app项目的pom.xml,在dependencies里添加新的dependency <dependency>... 阅读详情

😎 知识点概览

为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。

本章节为【学成在线】项目的 day06 的内容

  • 使用 Spring boot 集成 RabbitMQ 和 GridFS 实现基于生产者和消费者模型的页面静态化发布的流程。

    在本章节的知识点中,再次复习了基于 GridFS 和 RabbitMQ 的分布式静态页面发布的知识点,深化了记忆。

  • 使用三级菜单实现课程计划的查询和添加

    这里的技术点不是很多,用到了 Mysql 的表内自连接查询,以及在添加课程的时候,需要考虑一些意外情况的发生,例如再添加课程时,如果该课程的根节点(一级菜单)不存在,则需要为该课程添加一个根节点后再进行该二级节点的添加。

目录

内容会比较多,小伙伴们可以根据目录进行按需查阅。

一、页面发布

0x01 技术方案

本项目使用MQ实现页面发布的技术方案如下:

image-20200401211416315

**技术方案说明 **

1、平台包括多个站点,页面归属不同的站点。

2、发布一个页面应将该页面发布到所属站点的服务器上。

3、每个站点服务部署 cms client 程序,并与交换机绑定,绑定时指定站点Id为routingKey。

指定站点id为 routingKey 就可以实现 cms client 只能接收到所属站点的页面发布消息。

4、页面发布程序向MQ发布消息时指定页面所属站点 Id 为 routingKey,将该页面发布到它所在服务器上的cms client。

路由模式分析如下

发布一个页面,需发布到该页面所属的每个站点服务器,其它站点服务器不发布。

比如 发布一个门户的页面,需要发布到每个门户服务器上,而用户中心服务器则不需要发布。

所以本项目采用 routing 模式,用站点 id 作为 routingKey,这样就可以匹配页面只发布到所属的站点服务器上。

页面发布流程图如下

image-20200401211714847

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 保存到所在服务器指定目录

0x02 页面发布消费方

需求分析

功能分析

创建 Cms Client 工程作为页面发布消费方,将 Cms Client 部署在多个服务器上,它负责接收到页面发布 的消息后从 GridFS 中下载文件在本地保存。

需求如下

1、将 cms Client 部署在服务器,配置队列名称和站点 ID。

2、cms Client 连接 RabbitMQ 并监听各自的“页面发布队列”

3、cms Client 接收页面发布队列的消息

4、根据消息中的页面 id 从 mongodb 数据库下载页面到本地

创建Cms Client工程

1、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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xc-framework-parent</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../xc-framework-parent/pom.xml</relativePath>
    </parent>

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

    <!--项目依赖-->
    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-framework-model</artifactId>
            <version>1.0-SNAPSHOT</version>
        </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。

server:
  port: 31000
spring:
  application:
    name: xc-service-manage-cms-client
  data:
    mongodb:
      uri: mongodb://root:123123@localhost:27017
      database: xc_cms
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
  freemarker:
    cache: false #关闭模板缓存,方便测试
    settings:
      template_update_delay: 0
xuecheng:
  mq:
  #cms客户端监控的队列名称(不同的客户端监控的队列不能重复)
    queue: queue_cms_postpage_01
    routingKey: 5a751fab6abb5044e0d19ea1 #此routingKey为门户站点ID3

说明 在配置文件中配置队列的名称,每个 cms client在部署时注意队列名称不要重复

3、启动类
package com.xuecheng.manage_cms_client;

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

@SpringBootApplication
@ComponentScan(basePackages={"com.xuecheng.framework"})//扫描common下的所有类
@ComponentScan(basePackages={"com.xuecheng.manage_cms_client"})
public class ManageCmsClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ManageCmsClientApplication.class,args);
    }
}

RabbitmqConfig 配置类

消息队列设置如下

1、创建 ex_cms_postpage 交换机

2、每个 Cms Client 创建一个队列与交换机绑定

3、每个 Cms Client 程序配置队列名称和 routingKey,将站点ID作为routingKey。

package com.xuecheng.manage_cms_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;

@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;
    //站点id作为routing key
    @Value("${xuecheng.mq.routingKey}")
    public String routingKey;

    /**
     * 配置direct交换机
     * @return
     */
    @Bean(EX_ROUTING_CMS_POSTPAGE)
    public Exchange EXCHANGE_DIRECT_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
     * @param exchange
     * @return
     */
    @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();
    }

    
}

配置 MongoConfig

后续的代码中需要将GridFSBucket注入成bean

package com.xuecheng.manage_cms_client.config;


import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MongoConfig {
    @Value("${spring.data.mongodb.database}")
    String db;

    @Bean
    public GridFSBucket getGridFSBucket(MongoClient mongoClient){
        MongoDatabase database = mongoClient.getDatabase(db);
        GridFSBucket gridFSBucket = GridFSBuckets.create(database);
        return gridFSBucket;
    }
}

定义消息格式

消息内容采用 json 格式存储数据,如下

页面id 发布页面的id

{
    "pageId":""
}

PageDao

1、使用 CmsPageRepository 查询页面信息

public interface CmsPageRepository extends MongoRepository<CmsPage,String> {}

2、使用 CmsSiteRepository 查询站点信息,主要获取站点物理路径

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

PageService

package com.xuecheng.manage_cms_client.service;

import com.mongodb.client.gridfs.GridFSBucket;

import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.model.GridFSFile;
import com.xuecheng.framework.domain.cms.CmsPage;
import com.xuecheng.framework.domain.cms.CmsSite;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CmsCode;
import com.xuecheng.manage_cms_client.dao.CmsPageRepository;
import com.xuecheng.manage_cms_client.dao.CmsSiteRepository;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
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 org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;

@Service
public class PageService {
    @Autowired
    CmsPageRepository cmsPageRepository;

    @Autowired
    CmsSiteRepository cmsSiteRepository;

    @Autowired
    GridFsTemplate gridFsTemplate;

    @Autowired
    GridFSBucket gridFSBucket;

    /**
     * 将页面html保存到页面物理路径
     * @param pageId
     */
    public void savePageToServerPath(String pageId){
        Optional<CmsPage> optional = cmsPageRepository.findById(pageId);
        //页面不存在则抛出异常
        if(!optional.isPresent()){
            ExceptionCast.cast(CmsCode.CMS_PAGE_NOT_EXISTS);
        }

        CmsPage cmsPage = optional.get();
        CmsSite cmsSite = this.getCmsSiteById(cmsPage.getSiteId());
        if(cmsSite == null){
            ExceptionCast.cast(CmsCode.CMS_SIZE_NOT_EXISTS);
        }

        //原讲义和视频中使用的是sizePathysicaiPath,但是SizePage中没有这个字段,使用PageCms中的PathysicaiPath代替
        String pagePath = cmsPage.getPagePhysicalPath() + cmsPage.getPageWebPath() + cmsPage.getPageName();

        //获取页面文件id
        String htmlFileId = cmsPage.getHtmlFileId();
        if(StringUtils.isEmpty(htmlFileId)){
            ExceptionCast.cast(CmsCode.CMS_PAGE_FILEID_NOT_EXISTS);
        }
        //获取文件流
        InputStream inputStream = this.getFileInputStreamFileById(htmlFileId);
        if(inputStream == null){
            ExceptionCast.cast(CmsCode.CMS_GENRATEHTML_HTML_IS_NULL);
        }

        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(new File(pagePath));
            //将文件保存至物理路径
            IOUtils.copy(inputStream,fileOutputStream);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭流
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            } try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //根据文件id获取文件的gridFS输入流
    private InputStream getFileInputStreamFileById(String fileId){
        GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
        //获取gridFs下载流
        GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
        try {
            //获取文件输入流
            GridFsResource gridFsResource = new GridFsResource(gridFSFile, gridFSDownloadStream);
            return gridFsResource.getInputStream();
        }catch (IOException e){
            e.printStackTrace();
        }
        return null;
    }

    //根据站点id得到站点信息
    private CmsSite getCmsSiteById(String siteId){
        Optional<CmsSite> optional = cmsSiteRepository.findById(siteId);
        if(!optional.isPresent()){
            return null;
        }
        CmsSite cmsSite = optional.get();
        return cmsSite;
    }
}

ConsumerPostPage

在 cms client 工程的 mq 包下创建 ConsumerPostPage 类,ConsumerPostPage 作为发布页面的消费客户端,监听页面发布队列的消息,收到消息后从 mongodb 下载文件,保存在本地。

package com.xuecheng.manage_cms_client.mq;


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

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

@Component
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());
        }

        //将页面保存到服务器物理路径
        pageService.savePageToServerPath(pageId);
    }
}

0x03 页面发布生产方

需求分析

管理员通过 cms 系统发布 “页面发布” 的消费,cms 系统作为页面发布的生产方。
需求如下

1、管理员进入管理界面点击“页面发布”,前端请求cms页面发布接口。

2、cms页面发布接口执行页面静态化,并将静态化页面存储至GridFS中。

3、静态化成功后,向消息队列发送页面发布的消息。

​ 1) 获取页面的信息及页面所属站点ID。

​ 2) 设置消息内容为页面ID。(采用json格式,方便日后扩展)

​ 3) 发送消息给 ex_cms_postpage 交换机,并将站点ID作为 routingKey。

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 作为页面发布方要面对很多不同站点的服务器,面对很多页面发布队列,所以这里不再配置队列,只需要配置交换机名称即可。

package com.xuecheng.manage_cms.config;

import com.xuecheng.framework.exception.ExceptionCast;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitmqConfig {
    //交换机的名称
    public static final String EX_ROUTING_CMS_POSTPAGE="ex_routing_cms_postpage";

    /**
     * 交换配置使用direct类型
     */
    @Bean(EX_ROUTING_CMS_POSTPAGE)
    public Exchange EXCHANGE_TOPICS_INFORM(){
        return ExchangeBuilder.directExchange(EX_ROUTING_CMS_POSTPAGE).durable(true).build();
    }
}

Api 接口

在api工程定义页面发布接口

@ApiOperation("发布页面")
public ResponseResult post(String pageId);

PageService

package com.xuecheng.manage_cms.service;

import com.alibaba.fastjson.JSON;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.model.GridFSFile;
import com.xuecheng.framework.domain.cms.CmsPage;
import com.xuecheng.framework.domain.cms.CmsTemplate;
import com.xuecheng.framework.domain.cms.request.QueryPageRequest;
import com.xuecheng.framework.domain.cms.response.CmsPageResult;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.*;
import com.xuecheng.manage_cms.config.RabbitmqConfig;
import com.xuecheng.manage_cms.dao.CmsPageRepository;
import com.xuecheng.manage_cms.dao.CmsTemplateRepository;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import io.netty.util.internal.StringUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.bson.types.ObjectId;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
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 org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Service
public class PageService {

    @Autowired
    CmsPageRepository cmsPageRepository;

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    CmsTemplateRepository templateRepository;

    @Autowired
    GridFsTemplate gridFsTemplate;

    @Autowired
    GridFSBucket gridFSBucket;

    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * 页面发布
     * @param pageId
     * @return
     */
    public ResponseResult postPage(String pageId) {
        //执行静态化
        String pageHtml = null;
        try {
            pageHtml = this.getPageHtml(pageId);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TemplateException e) {
            e.printStackTrace();
        }
        if(StringUtils.isEmpty(pageHtml)){
            ExceptionCast.cast(CmsCode.CMS_GENRATEHTML_HTML_IS_NULL);
        }

        //保存静态化文件
        CmsPage cmsPage = this.saveHtml(pageId, pageHtml);
        //发送消息
        this.sendPostPage(pageId);
        return new ResponseResult(CommonCode.SUCCESS);
    }

    //发送页面发布消息
    private void sendPostPage(String pageId){
        CmsPageResult cmsPageResult = this.cmsPageQueryById(pageId);
        CmsPage cmsPage = cmsPageResult.getCmsPage();
        if(cmsPage == null){
            ExceptionCast.cast(CmsCode.CMS_PAGE_NOT_EXISTS);
        }
        Map<String, String> msgMap = new HashMap<>();
        msgMap.put("pageId",pageId);
        //消息内容
        String msg = JSON.toJSONString(msgMap);
        //获取站点id作为routing key
        String siteId = cmsPage.getSiteId();
        //发送消息到指定交换机、routingKey、以及要发送的消息
        rabbitTemplate.convertAndSend(RabbitmqConfig.EX_ROUTING_CMS_POSTPAGE,siteId,msg);
    }

    //保存静态化页面内容
    private CmsPage saveHtml(String pageId,String content){
        //查询页面是否存在
        Optional<CmsPage> optional = cmsPageRepository.findById(pageId);
        if(!optional.isPresent()){
            ExceptionCast.cast(CmsCode.CMS_PAGE_NOT_EXISTS);
        }

        CmsPage cmsPage = optional.get();
        //储存之前先删除
        String htmlFileId = cmsPage.getHtmlFileId();
        if(StringUtils.isNotEmpty(htmlFileId)){
            gridFsTemplate.delete(Query.query(Criteria.where("_id").is(htmlFileId)));
        }
        //保存html文件到GridFS
        try {
            InputStream inputStream = IOUtils.toInputStream(content,"utf-8");
            ObjectId objectId = gridFsTemplate.store(inputStream, cmsPage.getPageName());
            //文件id
            String fileId = objectId.toString();
            //将文件id储存到cmspage中
            cmsPage.setHtmlFileId(fileId);
            cmsPageRepository.save(cmsPage);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return cmsPage;
    }


    /**
     * 页面静态化
     * @param pageId
     * @return
     * @throws IOException
     * @throws TemplateException
     */
    public String getPageHtml(String pageId) throws IOException, TemplateException {
        //获取页面模型数据
        Map modelByPageId = this.getModelByPageId(pageId);
        if(modelByPageId == null){
            //获取页面模型数据为空
            ExceptionCast.cast(CmsCode.CMS_GENRATEHTML_DATA_IS_NULL);
        }
        //获取页面模板
        String templateContent = getTemplateByPageId(pageId);
        if(StringUtils.isEmpty(templateContent)){
            //页面模板为空
            ExceptionCast.cast(CmsCode.CMS_GENRATEHTML_TEMPLATE_IS_NULL);
        }
        //构建页面静态化数据
        String html = generateHtml(templateContent, modelByPageId);
        if(StringUtils.isEmpty(html)){
            ExceptionCast.cast(CmsCode.CMS_GENRATEHTML_HTML_IS_NULL);
        }
        return html;
    }

    //构建静态化页面数据
    private String generateHtml(String template,Map model) throws IOException, TemplateException {
        //生成配置类
        Configuration configuration = new Configuration(Configuration.getVersion());
        //模板加载器
        StringTemplateLoader stringTemplateLoader = new StringTemplateLoader();
        stringTemplateLoader.putTemplate("template",template);
        //配置模板加载器
        configuration.setTemplateLoader(stringTemplateLoader);

        //获取模板
        Template template1 = configuration.getTemplate("template");
        String html = FreeMarkerTemplateUtils.processTemplateIntoString(template1, model);
        return html;
    }

    //获取页面模板文件数据
    private String getTemplateByPageId(String pageId){
        //查询页面信息
        CmsPageResult cmsPageResult = this.cmsPageQueryById(pageId);
        CmsPage cmsPage = cmsPageResult.getCmsPage();
        //页面不存在
        if(cmsPage == null){
            ExceptionCast.cast(CmsCode.CMS_PAGE_NOT_EXISTS);
        }
        //获取页面模板数据
        String templateId = cmsPage.getTemplateId();
        if(StringUtils.isEmpty(templateId)){
            ExceptionCast.cast(CmsCode.CMS_GENRATEHTML_TEMPLATE_IS_NULL);
        }
        Optional<CmsTemplate> optional = templateRepository.findById(templateId);
        if(optional.isPresent()){
            CmsTemplate cmsTemplate = optional.get();
            //获取模板文件id
            String templateFileId = cmsTemplate.getTemplateFileId();
            //取出模板文件内容
            GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(templateFileId)));
            //打开下载流对象
            GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
            //创建GridResource
            GridFsResource gridFsResource = new GridFsResource(gridFSFile, gridFSDownloadStream);

            try {
                String content = IOUtils.toString(gridFsResource.getInputStream(), "utf-8");
                return content;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    //从dataUrl中获取页面模型数据
    private Map getModelByPageId(String pageId){
        //查询页面信息
        CmsPageResult cmsPageResult = this.cmsPageQueryById(pageId);
        CmsPage cmsPage = cmsPageResult.getCmsPage();
        //页面不存在
        if(cmsPage == null){
            ExceptionCast.cast(CmsCode.CMS_PAGE_NOT_EXISTS);
        }
        //取出dataUrl
        String dataUrl = cmsPage.getDataUrl();
        if(StringUtils.isEmpty(dataUrl)){
            ExceptionCast.cast(CmsCode.CMS_GENRATEHTML_DATAURL_IS_NULL);
        }
        //发送请求获取模型数据
        ResponseEntity<Map> forEntity = restTemplate.getForEntity(dataUrl,Map.class);
        Map body = forEntity.getBody();
        return body;
    }


    /**
     * 分页查询、页面CURL 部分代码略过,需要请参考前面的章节
     */
 	
    /**
     * 根据id获取页面数据
     */
    public CmsPageResult cmsPageQueryById(String id) {
        Optional<CmsPage> optional = cmsPageRepository.findById(id);
        if (optional.isPresent()) {
            CmsPage cmsPage = optional.get();
            return new CmsPageResult(CommonCode.SUCCESS, cmsPage);
        }
        return new CmsPageResult(CommonCode.FAIL, null);
    }
	
}

0x04 页面发布前端

用户操作流程

1、用户进入 cms 页面列表。

2、点击 “发布” 请求服务端接口,发布页面。

3、提示 “发布成功”,或发布失败。

API方法

在 cms前端添加 api方法。

/*发布页面*/
export const page_postPage= id => {
  return http.requestPost(apiUrl+'/cms/page/postPage/'+id)
}

页面开发

修改 page_list.vue,添加发布按钮

<el‐table‐column label="发布" width="80">
  <template slot‐scope="scope">
    <el‐button
      size="small" type="primary" plain @click="postPage(scope.row.pageId)">发布
    </el‐button>
  </template>
</el‐table‐column>

添加页面发布事件

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(() => {
  });
},

0x05 测试

这里测试轮播图页面修改、发布的流程

1、修改轮播图页面模板或修改轮播图地址

注意 先修改页面原型,页面原型调试正常后再修改页面模板。

2、执行页面预览

3、执行页面发布,查看页面是否写到网站目录

4、刷新门户首页并观察轮播图是否变化。

0x06 思考

1、如果发布到服务器的页面内容不正确怎么办?

2、一个页面需要发布很多服务器,点击“发布”后如何知道详细的发布结果?

3、一个页面发布到多个服务器,其中有一个服务器发布失败时怎么办?

二、课程管理

0x01 需求分析

在线教育平台的课程信息相当于电商平台的商品。课程管理是后台管理功能中最重要的模块。本项目为教学机构提供课程管理功能,教学机构可以添加属于自己的课程,供学生在线学习。

课程管理包括如下功能需求

1、分类管理
2、新增课程
3、修改课程
4、预览课程
5、发布课程

用户的操作流程如下

1、进入我的课程

image-20200401214528987

2、点击“添加课程”,进入添加课程界面

image-20200401214540896

3、输入课程基本信息,点击提交

4、课程基本信息提交成功,自动进入“管理课程”界面,点击“管理课程”也可以进入“管理课程”界面

image-20200401214605318

5、编辑图片上传课程图片。

image-20200401214657046

6、编辑课程营销信息

营销信息主要是设置课程的收费方式及价格。

image-20200401214707765

7、编辑课程计划

image-20200401214718496

0x02 教学方法

本模块对课程信息管理功能的教学方法采用实战教学方法,旨在通过实战提高接口编写的能力,具体教学方法如下:

1、前后端工程导入

教学管理前端工程采用与系统管理工程相同的技术,直接导入后在此基础上开发。
课程管理服务端工程采用Spring Boot技术构建,技术层技术使用Spring data Jpa(与Spring data Mongodb类
似)、Mybatis,直接导入后在此基础上开发。

2、课程计划功能

课程计划功能采用全程教学。

3、我的课程、新增课程、修改课程、课程营销

我的课程、新增课程、修改课程、课程营销四个功能采用实战方式,课堂上会讲解每个功能的需求及技术点,讲解完成学生开始实战,由导师进行技术指导。

4、参考文档

实战结束提供每个功能的开发文档,学生参考文档并修正功能缺陷。

0x03 环境搭建

1、搭建数据库环境

  1. 创建数据库

课程管理使用 MySQL 数据库,创建课程管理数据库:xc_course。

导入 xc_course.sql 脚本

image-20200401215113957

  1. 数据表介绍

课程信息内容繁多,将课程信息分类保存在如下表中:

image-20200401215204860

分类储存既可以提高解耦合度,也可以保证数据的完整性。

数据表结构如下:

image-20200401215300618

image-20200401215305777

image-20200401215310490

image-20200401215313716

image-20200401215316397

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

为什么持久层用到了 JPA 还要用 Mybatis ?

因为 JPA 是面向对象进行开发的,对于一些复杂的sql操作,优化起来会比较麻烦:

而 Mybatis 是面向sql的,对一些复杂的多表操作比较友好。

导入资料下的 “xc-service-manage-course.zip”。

3、导入课程管理前端工程

课程管理属于教学管理子系统的功能,使用用户为教学机构的管理人员和老师,为保证系统的可维护性,单独创建一个教学管理前端工程。 教学管理前端工程与系统管理前端的工程结构一样,也采用 vue.js 框架来实现。

image-20200401215549740

从课程资料目录拷贝 xc-ui-pc-teach.zip 到工程,使用 webstorm 打开,启动工程

效果图如下:

image-20200401215618671

三、课程计划

0x01 需求分析

什么是课程计划?

课程计划定义了课程的章节内容,学生通过课程计划进行在线学习,下图中右侧显示的就是课程计划。

课程计划包括两级,第一级是课程的大章节、第二级是大章节下属的小章节,每个小章节通常是一段视频,学生点击小章节在线学习。

教学管理人员对课程计划如何管理?

功能包括:添加课程计划、删除课程计划、修改课程计划等。例如:

image-20200401215716660

0x02 课程计划查询

课程计划查询是将某个课程的课程计划内容完整的显示出来,如下图所示:

image-20200401215738194

左侧显示的就是课程计划,课程计划是一个树型结构,方便扩展课程计划的级别。

在上边页面中,点击“添加课程计划”即可对课程计划进行添加操作。

点击修改可对某个章节内容进行修改。

点击删除可删除某个章节。

页面原型

tree组件介绍

本功能使用 element-ui 的 tree 组件来完成

image-20200401215821019

在 course_plan.vue 文件中添加 tree 组件的代码,进行测试:

1、组件标签

<el‐tree
      :data="data"
      show‐checkbox
      node‐key="id"
      default‐expand‐all
      :expand‐on‐click‐node="false"
      :render‐content="renderContent">
</el‐tree>

2、数据对象

let id = 1000;
  export default {
    data() {
      return {
        data : [{
          id: 1,
          label: '一级 1',
          children: [{
            id: 4,
            label: '二级 1‐1',
            children: [{
              id: 9,
              label: '三级 1‐1‐1'
            }, {
              id: 10,
              label: '三级 1‐1‐2'
            }]
          }]
        }]
      }
     }
 }
webstorm 配置JSX

本组件用到了JSX语法,如下所示:

image-20200401215955630

JSX 是 Javascript 和 XML 结合的一种格式,它是 React 的核心组成部分,JSX和XML 语法类似,可以定义属性以及子元素。唯一特殊的是可以用大括号来加入JavaScript 表达式。遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;
遇到代码块(以 { 开头),就用 JavaScript 规则解析。

下面是官方的一个例子:

image-20200401220038034

设置方法 如下:

1 、Javascript version 选择 React JSX (如果没有就选择JSX Harmony)

image-20200401220056325

2、HTML 类型文件中增加vue

image-20200401220104955

preferences -> Editor -> File Types 中找到上边框中HTML 在下边加一个 *.vue

如果已经在 vue template 中已存在.vue 则把它改为.vue2(因为要在Html中添加.vue)

image-20200401220120452

API接口

数据模型

1、表结构

image-20200401220143626

2、模型类

课程计划为树型结构,由树根(课程)和树枝(章节)组成,为了保证系统的可扩展性,在系统设计时将课程计划设置为树型结构。

@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;
}
自定义模型类

前端页面需要树型结构的数据来展示Tree组件,如下:

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

自定义课程计划结点类如下:

@Data
@ToString
public class TeachplanNode extends Teachplan {
    List<TeachplanNode> children;
}
接口定义

根据课程id查询课程的计划接口如下,在api工程创建 course 包,创建CourseControllerApi 接口类并定义接口方法如下:

public interface CourseControllerApi {
    @ApiOperation("课程计划查询")
    public TeachplanNode findTeachplanList(String courseId);
}

课程管理服务

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
DAO
  1. mapper接口
@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 property="id" column="one_id"/>
        <result property="pname" column="one_pname"/>
        <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
            <!--二级节点-->
            <id property="id" column="two_id"/>
            <result property="pname" column="two_pname"/>
            <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
                <!--三级节点-->
                <id property="id" column="three_id"/>
                <result property="pname" column="three_pname"/>
            </collection>
        </collection>
    </resultMap>

    <!--三级菜单查询-->
    <select id="selectList" resultMap="teachplanMap" parameterType="java.lang.String">
        SELECT
            a.id one_id,
            a.pname one_pname,
            a.courseid one_course,
            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的内置参数)

Service
@Service
public class CourseService {
    @Autowired
    TeachplanMapper teachplanMapper;
    //查询课程计划
    public TeachplanNode findTeachplanList(String courseId){
        TeachplanNode teachplanNode = teachplanMapper.selectList(courseId);
        return teachplanNode;
    }
}
Controller
@RestController
@RequestMapping("/course")
public class CourseController implements CourseControllerApi {
    @Autowired
    CourseService courseService;
    //查询课程计划
    @Override
    @GetMapping("/teachplan/list/{courseId}")
    public TeachplanNode findTeachplanList(String courseId) {
        return courseService.findTeachplanList(courseId);
    }
}
测试

使用postman或swagger-ui测试查询接口。

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

前端页面

API方法

定义课程计划查询的api方法:

/*查询课程计划*/
export const findTeachplanList = courseid => {
  return http.requestQuickGet(apiUrl+'/course/teachplan/list/'+courseid)
}
Api调用
  1. 在 mounted 钩子方法 中查询 课程计划
    定义查询课程计划的方法,赋值给数据对象 teachplanList
 findTeachplan(){
     courseApi.findTeachplanList(this.courseid).then((res) =>{
     this.teachplanList = [];//清空树
     if(res.children){
     this.teachplanList = res.children;
     }
 });
 }

2)在mounted钩子中查询课程计划

mounted(){ 
  //课程id
  this.courseid = this.$route.params.courseid;
  //课程计划
  this.findTeachplan();
}

3)修改树结点的标签属性

课程计划信息中 pname 为结点的名称,需要修改树结点的标签属性方可正常显示课程计划名称,如下:

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

image-20200401221133565

0x03 添加课程计划

需求分析

用户操作流程

1 、进入课程计划页面,点击“添加课程计划”

2、打开添加课程计划页面,输入课程计划信息

image-20200401221202889

上级结点说明:

如果不选择上级节点的话,表示当前添加的课程计划的父节点作为该课程的根节点,也就是说,添加的是二级菜单,如果选择了上级节点,表示添加的是三级菜单,如果添加该课程时,在课程计划中没有该课程的根节点时,要自动将该课程设置为根节点。

页面原型说明

添加课程计划采用弹出窗口组件Dialog。

1、视图部分

在course_plan.vue页面添加添加课程计划的弹出窗口代码:

<el‐dialog title=" 添加课程计划" :visible.sync="teachplayFormVisible" >
      <el‐form ref="teachplayForm"  :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、 添加按钮

通过变量 teachplayFormVisible 控制弹出窗口是否显示。

<el‐button type="primary" @click="teachplayFormVisible = true"> 添加课程计划</el‐button>

4、定义表单提交方法和重置方法

// 提交课程计划
addTeachplan(){
    alert()
},
//重置表单
resetForm(){
  this.teachplanActive = {}
},

API接口

1)添加课程计划

ApiOperation("添加课程计划")
public ResponseResult addTeachplan(Teachplan teachplan);

课程管理服务

DAO层
public interface TeachplanRepository extends JpaRepository<Teachplan, String> { 
   //定义方法根据课程id和父结点id查询出结点列表,可以使用此方法实现查询根结点
   public List<Teachplan> findByCourseidAndParentid(String courseId,String parentId);
}
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;

@Service
public class CourseService {
    @Autowired
    TeachplanMapper teachplanMapper;
    @Autowired
    CourseBaseRepository courseBaseRepository;
    @Autowired
    TeachplanRepository teachplanRepository;

    /**
     * 添加课程计划
     * @param teachplan
     * @return
     */
    @Transactional  //增删改操作都需要加spring事务
    public ResponseResult addTeachplan(Teachplan teachplan) {
        //校验课程id和课程计划名称
        if(teachplan == null|| StringUtils.isEmpty(teachplan.getCourseid()) || StringUtils.isEmpty(teachplan.getPname())){
            ExceptionCast.cast(CommonCode.INVALID_PARAM);
        }

        //取出课程id
        String courseId = teachplan.getCourseid();
        //取出父节点id
        String parentId = teachplan.getParentid();
        String newTeachplanGrade = "3";  //设置新节点的菜单等级

        // 如果用户未在添加时候未选择根节点,表示需要添加的是一个二级菜单,并且默认添加到当前课程的根节点下
        if(StringUtils.isEmpty(parentId)){
            newTeachplanGrade = "2";
            //根据课程id取根节点的id
            parentId = this.getTeachplanRoot(courseId);
            if(StringUtils.isEmpty(parentId)){
                ExceptionCast.cast(CommonCode.INVALID_PARAM);
            }
        }

        //创建新节点
        Teachplan teachplanNew = new Teachplan();
        //将传入的节点信息赋值到新节点内
        BeanUtils.copyProperties(teachplan,teachplanNew);
        teachplanNew.setParentid(parentId);
        teachplanNew.setCourseid(courseId);

        /* 设置新节点的级别
            方法1:根据父节点的级别进行设置,父节点级别为1,当前则为2,为2则当前为3
            方法2:在判断前端传入的父节点是否为空时进行设置,如果为空,表示需求为添加二级节点,设置2
                  如果不为空,则表示要添加的是三级节点,设置为3,与方法1相比可以减少一次查询。
         */
        teachplanNew.setGrade(newTeachplanGrade); //节点级别,根据父节点的级别进行设置
        teachplanRepository.save(teachplanNew);
        return new ResponseResult(CommonCode.SUCCESS);
    }

    //根据课程id获取课程根节点id,如果根节点不存在 则创建一个根节点,并作为该课程的根节点
    private String getTeachplanRoot(String courseId){
        //校验课程id
        Optional<CourseBase> optional = courseBaseRepository.findById(courseId);
        if(!optional.isPresent()){
            return null;
        }
        CourseBase courseBase = optional.get();
        //取出一级根节点菜单
        List<Teachplan> teachplanList = teachplanRepository.findByCourseidAndParentid(courseId, "0");

        //如果根节点不存在,则新增一个节点,并且作为该课程的根节点
        if(teachplanList == null || teachplanList.size() == 0){
            //新增一个节点
            Teachplan teachplanNewRoot = new Teachplan();
            teachplanNewRoot.setCourseid(courseId);
            teachplanNewRoot.setPname(courseBase.getName());
            teachplanNewRoot.setCourseid(courseId);
            teachplanNewRoot.setGrade("1"); //1级菜单
            teachplanNewRoot.setStatus("0"); //未发布
            teachplanRepository.save(teachplanNewRoot);
            return teachplanNewRoot.getId();
        }
        Teachplan teachplan = teachplanList.get(0);
        return teachplan.getId();
    }

    /**
     * 查询课程计划
     * @param courseId
     * @return
     */
    public TeachplanNode findTeachplanList(String courseId){
        TeachplanNode teachplanNode = teachplanMapper.selectList(courseId);
        return teachplanNode;
    }
}
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.stereotype.Service;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/course")
public class CourseController implements CourseControllerApi {
    @Autowired
    CourseService courseService;

    @GetMapping("/teachplan/list/{courseId}")
    @Override
    public TeachplanNode findTeachplanList(@PathVariable("courseId") String courseId) {
        return courseService.findTeachplanList(courseId);
    }

    @Override
    @PostMapping("/teachplan/add")
    public ResponseResult addTeachplan(@RequestBody Teachplan teachplan) {
        return courseService.addTeachplan(teachplan);
    }
}
测试

复杂一些的业务逻辑建议写完服务端代码就进行单元测试。

使用swagger-ui 或postman测试上边的课程计划添加接口。

前端Api调用

1、定义 api方法

/* 添加课程计划*/
export const addTeachplan = teachplah => {
  return http.requestPost(apiUrl+'/course/teachplan/add',teachplah)
}

2、调用 api

addTeachplan(){
  this.$refs.teachplayForm.validate((valid) => {
    if (valid) {
      //添加课程计划时带上课程id
      this.teachplanActive.courseid = this.courseid;
      courseApi.addTeachplan(this.teachplanActive).then((res) => {
        if(res.success){
          this.$message.success('提交成功');
          //清空表单
          this.teachplanActive = {}
          //刷新整个树
          this.findTeachplan();
          }else{
          this.$message.error('提交失败');
        }
      });
    }
  })
},

测试

测试流程:

1、新建一个课程

2、向新建课程中添加课程计划

​ 添加一级结点

​ 添加二级结点

进度复盘

一些数据

开始时间:2020/03/16

当前时间:2020/04/01

时 长:17 天

当前进度:完成至day 06

调整

进度太慢,在这之前平均每天投入的时间只有3~4个小时,再加上专注率不高,导致 1day的进度平均需要学习2~3天,根据今天统计的数据

image-20200401230342074

投入的时间需要在10个小时左右,才能完成一个day的进度,但是专注率还有提升的空间。

期望在本周能完成至day10的进度。

😁 认识作者

作者:👦 LCyee ,全干型代码🐕

自建博客:https://www.codeyee.com

记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。

优快云 博客:https://blog.youkuaiyun.com/codeyee

记录和分享一些开发过程中遇到的问题以及解决的思路。

欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

5步搞定ABB机器人RobotStudio与S7-1500PLC的Profinet通讯(附PLCSIM调试技巧) 本文详细介绍了如何利用RobotStudio与TIA Portal软件,结合PLCSIM Advanced仿真器,在零硬件条件下实现ABB机器人与西门子S7-1500 PLC之间的Profinet通讯虚拟调试。通过五个核心步骤,从环境搭建、网络配置、信号映射到联合仿真与高级调试,帮助工程师在项目前期高效验证逻辑、压缩现场调试周期,并掌握实用的PLCSIM调试技巧。 阅读详情

相关推荐

IBIS模型中的Pullup和Pulldown曲线详解:如何避免信号完整性设计中的常见错误

本文深入解析IBIS模型中Pullup和Pulldown曲线的关键作用,揭示其在信号完整性设计中的黄金法则。通过详细分析曲线特性、常见误区及验证方法,帮助工程师避免设计错误,优化高速PCB设计。文章还展示了曲线数据在动态终端匹配、串扰预测和电源完整性分析中的高级应用,提升设计效率和可靠性。

weixin_30275415的博客 451

微服务[学成在线] day07:课程管理开发

???? 知识点概览 为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。 本章节为【学成在线】项目的 day07 的内容 对课程管理的CRUD 级联菜单 使用 mongoDB 储存数据字典 目录 内容会比较多,小伙伴们可以根据目录进行按需查阅。 文章目录???? 知识点概览目录一、课程查询0x01 需求分析0x02 定义API接口0x03 课程管理服务PageHelperDaoSer

通往体面生活的路上 710

ICCompiler II实战:从NDM生成到APR流程的完整避坑指南

本文是一份针对ICCompiler II(ICC II)后端物理实现流程的实战避坑指南。文章详细解析了从NDM库生成、设计初始化、布局规划到时钟树综合与布线的完整APR流程,重点分享了各环节的常见陷阱、精细化配置命令与调试经验,旨在帮助工程师高效达成PPA目标,确保设计一次成功。

p4q5r6s7t的博客 531

学成在线 第7天 讲义-课程管理实战 五

3课程信息修改3.1需求分析课程添加成功进入课程管理页面,通过课程管理页面修改课程的基本信息、编辑课程图片、编辑课程营销信息等。本小节实现修改课程。3.2课程管理页面说明3.2.1页面结构课程管理页面的结构如下:3.2.2课程管理导航页面1、定义course_manage.vue为课程管理导航页面。导航效果使用Element-UI的NavMenu组件实现。 &lt...

qq_40208605的博客 332

FPGA防抄板实战:如何利用Xilinx芯片的“身份证”(DNA)实现你的加密锁?

本文深入探讨了如何利用Xilinx FPGA芯片的DNA(Device Native Authentication)功能实现硬件级加密保护,防止设计被抄袭。文章详细解析了DNA的物理实现、获取方式,并提供了从DNA到加密密钥的转换方法,以及构建商业级加密防护系统的实战方案。通过JTAG和原语两种读取方式的对比,帮助开发者选择最适合的方案。

weixin_42548829的博客 384

AI 与新媒体

AI重构新媒体生态:从生产到分发的全链路变革 AI正成为新媒体领域的核心基础设施,通过五大能力重塑内容产业:自动化生产(AIGC写作/制图/剪辑效率提升5-10倍)、智能分发(算法精准匹配用户)、数据洞察(实时舆情分析)、风险管控(审核效率提升12倍)和运营优化(A/B测试自动化)。这推动行业从"人找内容"转向"内容找人",制作成本下降90%的同时,也带来同质化加剧、版权争议和职业重构等挑战。当前阶段,人机协同成为主流——AI处理标准化生产,人类专注创意与价值观把控。

eastyuxiao的专栏 380

学成在线——课程管理实战

我的课程 课程添加完成后可通过我的课程进入课程修改页面,此页面显示我的课程列表,如下图所示,可分页查询。 注意:由于课程图片服务器没有搭建,这里在测试时图片暂时无法显示。 上边的查询要实现分页、会存在多表关联查询,所以建议使用mybatis实现我的课程查询。 学成在线——课程管理实战我的课程前言一、 需求分析二、使用步骤1.2 PageHelper1.2.1 PageHelper介绍1.2.2 PageHelper配置2.3.3 PageHelper测试1.3 前端页面说明1.4 Api接口2 新增课程

敲机闯天涯 307

2023-12-26~2025-07-04备份

一、海康视觉与汇川机器人 服务器-客户端通信设置IP在同一网段名称IP端口类型海康相机工控机7930服务器机器人3000客户端海康VM服务器设置1通信管理 增添TCP服务端,设置本机IP:192.168.0.20,端口号79302 协议解析 通信双方约定通信格式(设置收发数据格式,分隔符)机器人客户端设置(不同类型机器人设置有差别)1 通讯服务设置 设置连接服务器IP、服务端端口号、机器人端口号,确保连接正常2 协议解析 使用指令代码构建通讯解析。

青铜小码农 3126

学成在线 第7天 讲义-课程管理实战 三

2新增课程2.1需求分析用户操作流程如下:1、用户进入“我的课程”页面,点击“新增课程”,进入新增课程页面2、填写课程信息,选择课程分类、课程等级、学习模式等。3、信息填写完毕,点击“提交”,课程添加成功或课程添加失败并提示失败原因。需要解决的是在新增页面上输入的信息:1、课程分类多级分类,需要方便用户去选择。2、课程等级、学习模式等这些选项建议是可以配置的。页面效果如下:...

qq_40208605的博客 249

树莓派CM4 PCIe扩展方案与ASM1184e芯片应用

PCIe(Peripheral Component Interconnect Express)是计算机硬件中重要的高速串行总线标准,广泛应用于存储、网络等设备连接。其核心原理是通过差分信号实现高带宽传输,PCIe Gen2 x1的理论带宽可达5Gbps。在嵌入式开发中,PCIe扩展技术能有效解决单板计算机接口不足的问题,特别适合树莓派等微型平台。ASMedia ASM1184e作为PCIe交换芯片,可将单个接口扩展为多个,支持热插拔且免驱动,极大提升了工程灵活性。通过PCIe交换机方案,开发者能在树莓派CM

weixin_42602368的博客 421

学Simulink--基础微电网场景实例:基于Simulink的微电网黑启动过程建模与仿真

黑启动(Black Start)是指在全系统停电(无电压、无频率)状态下,微电网依靠内部电源自主恢复供电的能力。🌩️典型场景主网长时间故障自然灾害导致区域停电计划检修后重启❗挑战无参考电压/频率 → 逆变器无法锁相负荷逐级投入 → 需避免过流/过压多电源协调 → 防止环流或失步✅黑启动成功标准在30 秒内恢复关键负荷供电电压/频率稳定在范围内无设备损坏或保护跳闸🎯本文目标:手把手教你使用 Simulink + Simscape + Stateflow 搭建。

amy_mhd的博客 201

微服务[学成在线] day20:项目总结

???? 写在前面 我为什么要花时间去写这个笔记? 由于在学习该项目的过程中发现了原讲义存在的一些问题,所以该笔记基于「学成在线」微服务项目 PDF 讲义进行编写,并且投入了时间去优化了笔记的格式、代码的高亮、重点的标记等。 以及一些原讲义中所描述的一些知识点使我无法理解的内容,我会对这些内容的表达方式进行修改或者提出一些问题,并且用我自己所理解的一些想法去重新的解释这个问题。 总结一下原 PDF 讲义中已知的一些问题: 从 PDF 中复制出来的代码,部分特殊符号的编码有问题,并且不易被发现,例如横杠

通往体面生活的路上 3261

Android 13 Settings蓝牙列表卡顿问题排查及优化过程

此时我们的卡顿优化基本完成了,我们应该要考虑内存是否会泄漏,如果大家不知道哪些需要回收可以用leakcanary和android profiler进行内存检测,看代码有没有内存泄漏。

龚礼鹏的博客 2547

微服务[学成在线] day10:课程发布、ElasticSearch

???? 知识点概览 为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。 本章节为【学成在线】项目的 day10 的内容 课程发布功能开发 ElasticsSearch 安装部署 ElasticsSearch 快速入门、IK 分词器、映射、索引。 目录 内容会比较多,小伙伴们可以根据目录进行按需查阅。 文章目录???? 知识点概览目录一、课程发布0x01 需求分析0x02 CMS一键

通往体面生活的路上 1205

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

黑马学成在线项目

记录黑马学成在线项目的学习

Electric Sheep 889

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

最新传智学成在线JAVAEE微服务项目实战视频教程

目录 ├─day01 搭建环境 CMS服务端开发 ├─day02 CMS前端开发 ├─day03 CMS页面管理开发 ├─day04 页面静态化 ├─day05 消息中间件RabbitMQ ├─day06 页面发布 课程管理 ├─day07 课程管理实战 ├─day08 课程图片管理 分布式文件系统 ├─day09 课程预览 Eureka Feign ├─day10 课程发布 ElasticSea...

u011576602的博客 992

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Java的新项目学成在线笔记-day18(十)

微服务之间认证 4.1 需求分析 前边章节已经实现了用户携带身份令牌和JWT令牌访问微服务,微服务获取jwt并完成授权。 当微服务访问微服务,此时如果没有携带JWT则微服务会在授权时报错。 测试课程预览: 1、将课程管理服务和CMS全部添加授权配置 2、用户登录教学管理前端,进入课程发布界面,点击课程发布,观察课程管理服务端报错如下: [AppleScript]纯文本查看复制代码 ?...

czbkzmj的博客 148

Java的新项目学成在线笔记-day16(六)

3.3.4 资源服务授权 3.3.4.1 资源服务授权流程 资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资 源,如下图: 上图的业务流程如下: 1、客户端请求认证服务申请令牌 2、认证服务生成令牌 认证服务采用非对称加密算法,使用私钥生成令牌。 3、客户端携带令牌访问资源服务 客户端在Http header 中添加: Authorizatio...

czbkzmj的博客 169

Java的新项目学成在线笔记-day17(十一)

.4.2 路由配置 在appcation.yml中配置: [AppleScript]纯文本查看复制代码 ? 1 2 3 4 5 6 zuul: routes: manage‐course: #路由名称,名称任意,保持所有路由名称唯一 path: /co...

czbkzmj的博客 274

2017黑加传智java_传智黑马 Java在职加薪课 学成在线

传智黑马 Java在职加薪课 学成在线|____【传智播客】学成在线|____day20 Devops 项目总结|____day19 分布式事务|____day18 用户授权|____day17 用户认证 Zuul|____day16 Spring Security Oauth2|____day15 媒资管理系统集成|____day14 媒资管理|____day13 在线学习 HLS|____da...

weixin_30927801的博客 195

Java的新项目学成在线笔记-day9(六)

2.2 Feign 2.2.1 Feign介绍 Feign是Netflix公司开源的轻量级rest客户端,使用Feign可以非常方便的实现Http 客户端。 Spring Cloud引入 Feign并且集成了Ribbon实现客户端负载均衡调用。 1、在客户端添加依赖 在课程管理服务添加下边的依赖: [AppleSc...

czbkzmj的博客 160

安排,全栈分布式微服务媒资管理系统(视频、代码)

来源:来自网络,如侵权请告知博主删除????。仅学习使用,请勿用于其他~最近有小伙伴管我要分布式这类的项目,还有问我还有没有前后端分离的项目,还有些问我有没有微服务的,今天找的资源一...

qq_14958051的博客 605

Java的新项目学成在线笔记-day16(七)

3.3.4.3 资源服务授权测试 这里我们测试课程图片查询 get http://localhost:31200/course/coursepic/list/4028e58161bd3b380161bd3bcd2f0000 请求时没有携带令牌则报错: [AppleScript]纯文本查看复制代码 ? ...

czbkzmj的博客 240

最新《java技术提升班教程》

微服务持续集成与容器管理讲义持续集成与容器管理.pdf资源CentOS7-2.zipapache-maven-3.5.4-bin.tar.gzjenkins-2.83-1.1.noarch.rpmjdk-8u171-linux-x64.rpm华为 云服务治理day01 微服务引擎CSEday02 云容器引擎CCEday03 学成在线项目部署day04 微服务治理day05 Mesher学成在线项目...

a13141213的博客 472

Java的新项目学成在线笔记-day18(九)

3.4 获取当前用户信息 3.4.1需求分析 要想实现只查询自己的课程信息则需要获取当前用户所属的企业id。 1、认证服务在用户认证通过将用户所属公司id等信息存储到jwt令牌中。 2、用户请求到达资源服务后,资源服务需要取出header中的jwt令牌,并解析出用户信息。 3.4.2 jwt令牌包括企业Id 资源服务在授权时需要用到用户所属企业ID,需要实现认证服务生成的JWT令牌中包括用户...

czbkzmj的博客 166

学成在线笔记五:页面发布及课程管理

页面发布 技术方案 本项目使用MQ实现页面发布的技术方案如下: 技术方案说明: 平台包括多个站点,页面归属不同的站点。 发布一个页面应将该页面发布到所属站点的服务器上。 每个站点服务部署cms client程序,并与交换机绑定,绑定时指定站点Id为routingKey。指定站点id为routingKey就可以实现cms client只能接收到所属站点的页面发布消息。 页面发布程序向MQ发布消息时指定页面所属站点Id为routingKey,将该页面发布到它所在服务器上的cms client。 页面发布流

Bbb的博客 827
上一篇: 微服务[学成在线] day05:消息中间件 RabbitMQ
下一篇: 微服务[学成在线] day07:课程管理开发
cory_lin
博客等级 码龄7年 22粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值