今日重点
课程发布&课程预览课程预览: (校验数据的正确性)教育机构提交审核前可使用平台运营在提交之后进行审核操作时可以查看整体功能需要4张表的内容才能全部展示,所以把这些前端需要展示的热点内容,放置到单独的一张表中 实现业务隔离!发布信息表的诞生意义:课程相关信息的四张表,由于业务操作存在增删改的时候会导致锁表操作 这会大降低查询效率 所以course_pub表只负责查询(业务角度实现读写分离)course_pub表 查询改表中的数据前端展示,并发量是会很大的! 可以将此单表的数据放置到ES中构建索引!按照业务维度对数据进行读写分 为了后面和索引库进行数据同步 在业务维度进行读写分离 方便 查询 ES 同步freemaeker介绍 :模板引擎:一种基于模板和要改变的数据。生产输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具, 不是面向最终用户,而是一个java类库(FTL 简单 专用的语言)模板中专注于如何展示数据,模板之外可以专注要展示什么数据!核心点:模板 + 数据模型 = 输出template +datamodel =htmlFreemaker 生成静态化的方式:1.SpringMVC+FreemarkerSpringMVC将Freemarker作为视图解析器生成的文件位置:在内存中项目中使用的场景:课程预览整个页面解析是在后端生成,扛不住大并发!2.Freemarker的原生API生成的文件位置:制定文件生成的位置API生成的文件形式:1.模板文件+数据模型2.模板字符串+数据模型项目中使用的场景:课程发布Controller层的数据添加 最终前端页面访问${插值表达式}会变为被引用的key值所对应的value值!(前端访问数据 是直接对接Controller层而不是直接访问类路径下的静态资源页面)访问流程: 前端访问C控制层,控制层 封装组成一个model 然后交给View (模板) 柔和成一个完整的html返回给前端前后端开发SpringMVC: Model(javaBean对象) + view(展示) + controller(控制层)SpringMVC的结果试图器将Model和view结合 生成完整的html页面返回前端课程发布流程:课程预览交互流程如下:后端实现业务:常用的java模板引擎还有哪些?(面试)Jsp、Freemarker、Thymeleaf 、Velocity 等。1.Jsp 为 Servlet 专用,不能单独进行使用。2.Thymeleaf 为新技术,功能较为强大,但是执行的效率比较低。3.Velocity从2010年更新完 2.0 版本后,便没有在更新。Spring Boot 官方在 1.4 版本后对此也不在支持,虽然 Velocity 在 2017 年版本得到迭代,但为时已晚。功能足够项目使用,功能较为稳定freemarker并不关心数据的来源,只是根据模板的内容,将数据模型在模板中显示并输出文件Freemarker基本语法:注释 <#-- xxxxxxx -->插值表达式 hello ${name}FLT指令: <# >FTL指令</#>普通文本: <b>普通文本 String 展示:</b>遍历集合获得序列编号 变量名_indes +1空值处理: ??用在标签,!用在插值表达式内建函数: 变量名? + 内嵌函数名称集合长度 ?size 数值?c静态化测试:使用freemarker的原生API将页面生产HTML文件:1、根据模板文件生成html文件模板文件 + 数据模型 = 静态文件2、根据模板字符串生成html文件模板字符串 + 数据模型 = 静态文件课程预览:审核状态要求: 正常业务逻辑教学机构再未发布之前可以预览运营平台在机构提交后可以进行预览使用SpringMVC+Freemarker生成返回完整页面给前端 也就是说前端是不需要操作的 所有成型都是后端实现!SpringMVC 中controller 多结果视图器并存* CourseBaseController :* JackSonResolver (默认)--controller使用的是RestController* FreemarkerViewResolver* 1.添加依赖:spring-boot-starter-Freemarker* 2.添加配置:spring.freemarker.enabled = true* 3.方法的返回:controller的方法返回为ModelAndView对象前端预览使用 :Windows.open方法 而不是异步请求axios 点击预览此课程后会打开新界面展示数据,请求头中是没有Token令牌的!
学习目标
1理解课程发布和预览的功能性需求
2理解课程发布和预览的数据模型
3能够根据文档编写Freemarker入门案例
4掌握FreeMarker基础语法
5掌握FreeMarker原生Api生成静态页面的两种方式
6熟悉课程预览的系统交互
7能够将Freemarker集成到SpringBoot微服务中
8能够实现课程发布查询课程分类信息的Feign接口实现
9能够根据文档定义课程预览接口
10能够完成课程预览的功能实现
11能够根据文档搭建课程预览功能展示环境
1. 课程发布需求概述
课程发布是一个课程从设计、实现、审核、调整后的最终确定的操作,被发布的课程将公布于众,课程发布后,学生将能够通过门户查看该课程的详细内容。
课程发布功能在整个学成在线功能架构中依然处于教学管理模块中的课程管理里,如下图:

1.1 课程发布概述
课程发布的功能是还是在课程管理中,而课程管理的相关信息通过调用内容管理微服务来完成业务实现,而内容管理主要需求如下:
●课程基础信息管理 对课程的基本信息进行管理,其中包括课程基础信息、课程图片、课程分类信息、课程等级、学习模式课程介绍等内容。
●课程营销信息管理 一门课程的除了基本信息之外,还有课程的营销信息,营销信息主要对课程是否收费、课程价格、课程有效期。
●课程计划信息管理 课程所关联的课程计划信息,其中包括客户才能计划关联媒资信息管理,课程计划为课程内容的大纲,主要方便学员学习和视频播放列表的展示。
●课程教师信息管理 每一门课程都需要相应的讲师信息,在课程信息操作的时候,需要填写教师信息,并保存到数据库中。由于功能上的重复,课上不会安排此功能实现。
●课程发布信息管理 课程信息从开始的创建、课程审批、课程预览,最后到课程发布整个流程。课程发布后,学员才可以在门户网站中找到对应课程内容,否则学员是找不到课程信息的。所有内容管理中需要对课程发布后的信息进行统一管理。
1.2 业务介绍
本章,我们来完善课程审核过程并实现课程发布功能。
1教育机构在课程提交审核前,往往需要看一下课程最终会被发布成什么样子,是否符合所预期,因此需要提供 教学机构课程预览功能来满足该需求,若符合预期,则进行课程提交审核操作。
2平台运营在课程的审核过程中,也需要预览功能来更直观的看到课程内容,因此需要提供平台运营课程预览功能来满足该需求。
3教育机构在课程审核通过后,可进行课程发布操作,这是将是本章的功能实现重点。
4门户能够浏览已发布课程的详细内容
课程发布流程

其中,课程提交审核、课程审核 功能在前面的课程中已经介绍过,本章需要完成的需求列表如下:
1、课程预览:包括教育机构提交审核前的课程预览功能、平台运营审核过程中所需的课程预览功能。
2、课程发布:课程发布时形成门户可访问的课程详情页面。
我们经常浏览电商网站,电商网站浏览量最大的页面是什么?商品检索列表、商品详情。对于在线教育网站,课程就是商品。因此,课程详情页的访问流量是巨大的。因此,除了上面的功能性需求,本章我们还需要满足一个非功能性需求:
●高性能课程详情页访问。
1.3 业务流程
1.3.1 课程预览
教学机构课程预览流程如下:
1.教育机构用户在课程管理中可对该机构内所管理的课程进行检索。

2.点击某课程数据后的预览链接,即可对该课程进行预览,可以看到发布后的详情页面效果。


平台运营课程预览流程如下:
1.平台运营用户在课程管理中可对所有机构的课程进行检索。
2.点击某课程数据中的预览链接,即可对该课程进行预览,可以看到发布后的详情页面效果。


1.3.2 课程发布
教学机构课程发布流程如下:
1.教育机构用户在课程管理中可对机构内课程进行检索。

2.点击某课程数据后的 发布 链接(审核状态为通过),即可对该课程进行发布。

3.通过门户首页,点击课程连接,即可浏览已发布课程的详情页面。

4.课程详情页面如下。


1.4 课程发布的数据模型
课程发布的数据会调用内容管理微服务,这里会使用内容管理 xc_content 数据库。
学成在线数据结构图

针对课程发布的业务需求,本次将使用 course_pub 数据库表来存储课程发布的业务数据,数据库表示意图:
内容管理数据模型(表结构)

通过之前的学习,我们已经已经了解到课程相关信息表包含了课程基本信息、营销信息、师资信息以及该课程具体课程计划信息,而**课程发布**的过程会将这些信息表进行汇总,形成课程发布信息。

为什么要形成课程发布信息呢?
课程发布信息代表着一次课程输出的最终确定。后续的业务,如课程购买、学习将围绕着该课程发布信息,从业务角度该信息不可轻易修改,只读(若修改,需要重新走审核流程)。
CoursePub表存在的原因:
课程业务表与发布内容相分离,课程业务信息是经常发生变动的,而课程发布信息不会轻易修改,因此形成业务级别的读写分离,两者互不影响。
课程发布的数据最终要存放到索引库中,如果没有CoursePub表,需要查询多表数据向es存放数据,比较麻烦。如果有CoursePub表就可以查询单表数据就可以将es索引数据保存到索引库中。
2. Freemarker 研究
在课程管理中业务 课程发布或 课程预览,都是将课程的信息输出到具体的页面中来进行展示,最终生成 Html 页面返回给前端,这里是需要技术来做支撑,就是模板引擎技术。学成在线模板引擎技术就采用的是 Freemarker。
Freemarker是模板技术,课程预览 与后续的 课程发布
生成都采用此技术实现 ,其中:
●课程预览,使用Freemarker来页面动态渲染课程预览详情页中的内容,并返回给前端。
●课程详情页,使用Freemarker来用于课程详情页静态页面生成。
具体请学习课程资料中的 学成网-第4章-freemarker。
3. 课程预览
课程管理中,教学机构和运营平台都是需要对课程生成的详情页进行预览操作,项目中的课程预览会使用 Freemarker 模板技术来做业务实现,之前已经将 Freemarker 的相关技术学习完后,下面将进行课程预览的相关需求开发。
3.1 需求分析
此模块主要针对教育机构及平台运营对课程信息的预览,主要功能包括:
●教育机构提交审核前预览
●平台运营审核过程中预览
3.2 业务流程描述
1教育机构在课程提交审核前,往往需要看一下课程最终会被发布成什么样子,是否符合所预期,因此需要提供 教学机构课程预览功能来满足该需求,若符合预期,则进行课程提交审核操作。
2平台运营在课程的审核过程中,也需要预览功能来更直观的看到课程内容,因此需要提供平台运营课程预览功能来满足该需求。
教学机构课程预览流程如下:
1.教育机构用户在课程管理中可对该机构内所管理的课程进行检索。

2.点击某课程数据后的预览链接,即可对该课程进行预览,可以看到发布后的详情页面效果。


平台运营课程预览流程如下:
1.平台运营用户在课程管理中可对所有机构的课程进行检索。

2.点击某课程数据中的课程名称链接,即可对该课程进行预览,可以看到发布后的详情页面效果。


3.3 系统交互流程
课程预览交互流程如下:

步骤描述:
1.前端对某审核通过的课程执行课程发布操作
2.查询课程基本信息、课程营销、课程计划、教师信息并保存到 课程发布中。
3.远程调用系统管理查询课程分类信息
4.保存课程发布信息
5.获得页面的数据模型(课程发布、课程营销、课程计划、教师信息、课程分类等)
6.使用Spring MVC 结果视图器 Freemarker 选择页面模板,生成静态页面
7.将生成的静态页面返回给前端
上面设计到两个前端系统,这两个前端系统都会调用后端的统一接口 课程预览 ,由内容管理微服务提供。所有本次将开发一个接口来完成两个前端功能需求。
内容管理开发中,需要调用系统管理服务来完成数据的获取。所以本次开发中,需要在两个微服务中,分别进行开发,功能如下:
系统管理服务:开发根据可曾分类 ID 获得课程分类信息的接口。
内容管理服务:开发课程预览的接口。
3.4 项目环境搭建
学成在线的课程预览会在 xc-content-service 微服务工程中,进行开发。在开发前,我们需要将其运行环境先构建出来。
1.数据库环境
2.Freemarker 环境集成
3.4.1 数据库环境
对于内容管理中课程发布数据表已经在之前导入到项目中,所以无需进行导入SQL脚本操作。
数据库内容

3.4.2 Freemarker 环境集成
1.xc-content-service引入maven依赖(已经集成,无需引入)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency>
2.新增公共配置 freemarker-config.properties
#开启 freemarker 功能spring.freemarker.enabled = true#关闭模板缓存,方便测试spring.freemarker.cache = falsespring.freemarker.settings.template_update_delay = 0#页面模板后缀名spring.freemarker.suffix = .ftlspring.freemarker.charset = UTF-8#页面模板位置(默认为 classpath:/templates/)spring.freemarker.template-loader-path = classpath:/templates/#关闭项目中的静态资源映射(static、resources文件夹下的资源)spring.resources.add-mappings = false
3.在 内容管理微服务 xc-content-service 工程的 bootstrap. 配置文件添加依赖配置
#其他配置省略spring:application:name: content-servicejackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8cloud:nacos:discovery: #配置注册中心server-addr: 192.168.94.129:8848namespace: 5c0b093c-4084-46b5-bf33-899321cb7ef5group: ${group.name}config: #配置中心server-addr: 192.168.94.129:8848namespace: 5c0b093c-4084-46b5-bf33-899321cb7ef5group: ${group.name}file-extension: propertiesshared-configs:- dataId: mp-config.properitesgroup: ${group.name}- dataId: feign-config.propertiesgroup: ${group.name}- dataId: ribbon-config.propertiesgroup: ${group.name}- dataId: freemarker-config.propertiesgroup: ${group.name}
3.将今天下发的 资料/详情页模板/learing_article.ftl 复制到工程中src\main\resources\templates中,此文件为课程详情页freemark模板。

PS:由于该模板中需要使用修改的内容太多,会占用课上大量的时间,所有为了节省课上时间,该模板中已经将相应的内容添加上了 Freemarker 的插值表达式,在项目中直接使用。
3.5 课程分类信息业务实现(已实现,无需实现)
在课程预览中,内容管理微服务 远程调用 系统管理服务接口来获得课程分类名称,如下:

下面将在系统管理微服务 xc-system-service 中开发此接口。
3.5.1 课程分类接口定义
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址

接口传入传出列表

2. 接口编写
在 xc-api 工程的 com.xuecheng.api.media 包下定义如下:
●根据 ID 课程分类信息查询
/*** <p>* 课程分类服务 Api* </p>** @Description:*/@Api(value = "课程分类服务Api" ,tags = "系统-课程分类服务" ,description = "对课程分类信息业务操作")public interface CourseCategoryApi {//其他代码省略@ApiOperation("根据id查询课程分类")RestResponse<CourseCategoryDTO> getCourseCategoryById(String id);}
3.5 课程预览业务实现
3.5.1 数据模型(表结构)
内容管理服务的数据库 xc_content 中定义了课程发布表 course_pub。
1.课程发布表说明
课程发布表结构

在上图中的表结构中,主要字段为:
1.课程基本信息描述:course_id,company_id,name,users,mt,st 等
2.课程营销信息描述:market (json 数据)
3.课程计划信息描述:teachplan(课程计划树形结构 json 数据)
4.教师信息描述:teachers( json 数据)
3.5.2 远程获得课程分类开发
由于在 CoursePub 中需要将课程分类(大分类、小分类)的名称,这是需要 内容管理微服务 远程调用 系统管理服务接口,来完成课程分类名称的获取,如下图:

下面需要在内容管理微服务中创建一下内容:
1.Feign远程调用接口
在 xc-content-service 中创建远程调用的接口,如下:
package com.xuecheng.content.agent;import com.xuecheng.api.system.model.dto.CourseCategoryDTO;import com.xuecheng.common.constant.XcFeignServiceNameList;import com.xuecheng.common.domain.response.RestResponse;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;/*** <p>* 内容管理对系统管理的 Feign 的远程调用* </p>** @Description:*/@FeignClient(value = XcFeignServiceNameList.XC_SYSTEM_SERVICE)public interface SystemApiAgent {@GetMapping("/system/l/course-category/{id}")RestResponse<CourseCategoryDTO> getById(@PathVariable String id);}
@_FeignClient 需要赋值属性:
value:指定系统管理在注册中心的名称,这里使用 XcFeignServiceNameList 类来获得
@_GetMapping 赋值属性:
由于是微服务的远程调用,需要在系统管理服务根路径下加上 /l 标识。
2.Feign接口服务降级
在内容管理的 bootstrap.yml 中配置 sentinel
# 配置sentinelspring:cloud:sentinel:transport:dashboard: 192.168.94.129:8858 #sentinel控制台地址#开启feign的Sentinel降级feign:sentinel:enabled: true
编写 Sentinel 降级类
package com.xuecheng.content.agent.sentinel;import com.xuecheng.api.system.model.dto.CourseCategoryDTO;import com.xuecheng.common.domain.code.CommonErrorCode;import com.xuecheng.common.domain.response.RestResponse;import com.xuecheng.content.agent.SystemApiAgent;import org.springframework.stereotype.Component;/*** <p></p>** @Description:*/@Componentpublic class SystemApiAgentFallBack implements SystemApiAgent {@Overridepublic RestResponse<CourseCategoryDTO> getById(String id) {return RestResponse.validfail(CommonErrorCode.E_999981);}}
在Feign 接口中配置fallback
package com.xuecheng.content.agent;import com.xuecheng.api.system.model.dto.CourseCategoryDTO;import com.xuecheng.common.constant.XcFeignServiceNameList;import com.xuecheng.common.domain.response.RestResponse;import com.xuecheng.content.agent.sentinel.SystemApiAgentFallBack;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;/*** <p></p>** @Description:*/@FeignClient(value = XcFeignServiceNameList.XC_SYSTEM_SERVICE,fallback = SystemApiAgentFallBack.class)public interface SystemApiAgent {@GetMapping("/system/l/course-category/{id}")RestResponse<CourseCategoryDTO> getById(@PathVariable String id);}
3.5.3 模板中表达式解析
在 FreeMarker模板文件中,存在下面表达式,其 key 值如下:
1.课程发布key
${coursePub.xxxx}
2.课程营销key
${courseMarket.xxxx}
3.课程模式key值
${courseTeachModeEnums}
参考项目枚举类 CourseModeEnum 中的数据。
4.课程计划key值
${teachplanNode}
上面的四个 key 为数据模型中制定的主键值。
3.5.4 课程预览接口定义
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
GET /content/course/preview/{courseBaseId}/{companyId}
接口传入传出列表


2. 接口编写
在 xc-api 工程的 com.xuecheng.api.content 包下创建接口类接口定义如下:
●课程预览
@Api(value = "课程基本信息管理Api",tags = "内容-课程基本信息管理",description = "课程基本信息业务管理")public interface CourseBaseApi {//其他代码省略@ApiOperation(value = "课程预览")Object previewCourse(Long courseId,Long companyId);}
3.5.5 课程预览接口开发
1.DAO编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2.service 编写
●对象属性转换器
@Mapperpublic interface CoursePubConvert {CoursePubConvert INSTANCE = Mappers.getMapper(CoursePubConvert.class);CoursePub courseBase2coursePub(CourseBaseDTO courseBase);CoursePubDTO entity2dto(CoursePub coursePub);}
●接口
/*** <p>* 课程发布 服务类* </p>** @author itcast*/public interface CourseBaseService extends IService<CourseBase> {/*** 课程预览* @param courseId 课程Id* @param companyId 公司Id* @return Map 数据模型 Map*/Map<String, Object> previewCourse(Long courseBaseId, Long companId);}
●实现类
/*** <p>* 课程 服务实现类* </p>** @author itcast*/@Servicepublic class CourseBaseServiceImpl extends ServiceImpl<CourseBaseMapper, CourseBase> implements CourseBaseService {@Autowiredprivate CourseBaseService courseBaseService;@Autowiredprivate CourseMarketService courseMarketService;@Autowiredprivate TeachplanService teachplanService;@Autowiredprivate SystemApiAgent systemApiAgent;/** 主方法业务:* 1.构建CoursePub数据并保存** 2.根据CoursePub内容构架DataMap数据** 3.将DataMap进行返回* */@Transactionalpublic Map<String, Object> preview(Long courseId, Long companyId) {//1.构建CoursePub数据并保存CoursePub coursePub = generateCoursePub(courseId,companyId);// 2.根据CoursePub内容构架DataMap数据Map<String,Object> dataMap = generateDataMap(coursePub);// 3.将DataMap进行返回return dataMap;}private Map<String, Object> generateDataMap(CoursePub coursePub) {// 1.构建数据模型对象HashMap<String, Object> dataMap = new HashMap<>();// 2.构建coursePubdataMap.put(CoursePubTemplateKey.COURSEPUB, coursePub);// 3.构建courseMarketString marketJsonString = coursePub.getMarket();CourseMarket courseMarket = JsonUtil.jsonToObject(marketJsonString, CourseMarket.class);dataMap.put(CoursePubTemplateKey.COURSEMARKET,courseMarket );// 4.构建课程计划String teachplanJsonString = coursePub.getTeachplan();TeachplanDTO teachplanDTO = JsonUtil.jsonToObject(teachplanJsonString, TeachplanDTO.class);dataMap.put(CoursePubTemplateKey.TEACHPLANNODE,teachplanDTO );// 5.构建课程模式CourseModeEnum[] values = CourseModeEnum.values();dataMap.put(CoursePubTemplateKey.COURSETEACHMODEENUMS,values );return dataMap;}/** 构建CoursePub数据并保存* 业务分析:* 1.判断关键数据* courseId companyId* 2.判断业务数据* 课程基础信息* 判断是否存在* 判断是否是同一家机构* 判断是否删除* 判断审核状态:教学机构课程预览--未提交、审核未通过* 课程营销* 判断是否存在:根据courseid* 课程计划* 获得课程计划:根据courseId和companyId(树形结构)* 课程教师* 判断教师信息是否存在:一定要确保课程最少有一个教师信息** 课程分类数据并完善Coursepub数据* 调用system服务获得课程分类的名称** 3.保存课程发布数据* CoursePub数据保存数据库中* 课程基础信息 课程营销 课程计划 课程教师** 4.将coursePub数据返回** */private CoursePub generateCoursePub(Long courseId, Long companyId) {//1.判断关键数据// courseId companyId// 2.判断业务数据// 课程基础信息// 判断是否存在// 判断是否是同一家机构// 判断是否删除// 判断审核状态:教学机构课程预览--未提交、审核未通过CourseBaseDTO courseBase = courseBaseService.getCourseBaseById(courseId, companyId);String auditStatus = courseBase.getAuditStatus();if (CourseAuditEnum.AUDIT_PASTED_STATUS.getCode().equals(auditStatus)||CourseAuditEnum.AUDIT_COMMIT_STATUS.getCode().equals(auditStatus)||CourseAuditEnum.AUDIT_PUBLISHED_STATUS.getCode().equals(auditStatus)) {ExceptionCast.cast(ContentErrorCode.E_120015);}// 课程营销// 判断是否存在:根据courseid (courseBaseService.getCourseBaseById 里已经对其进行判断是否存在)LambdaQueryWrapper<CourseMarket> marketQueryWrapper = new LambdaQueryWrapper<>();marketQueryWrapper.eq(CourseMarket::getCourseId, courseId);CourseMarket courseMarket = courseMarketService.getOne(marketQueryWrapper);// 课程计划// 获得课程计划:根据courseId和companyId(树形结构)TeachplanDTO teachplanTreeNodes = teachplanService.getTreeNodes(courseId, companyId);// 课程教师// 判断教师信息是否存在:一定要确保课程最少有一个教师信息// TODO: 2021/12/17 学员完成// 课程分类数据并完善Coursepub数据// 调用system服务获得课程分类的名称String mt = courseBase.getMt();String st = courseBase.getSt();RestResponse<CourseCategoryDTO> mtResponse = systemApiAgent.getCourseCategoryById4s(mt);if (!(mtResponse.isSuccessful())) {ExceptionCast.castWithCodeAndDesc(mtResponse.getCode(),mtResponse.getMsg());}RestResponse<CourseCategoryDTO> stResponse = systemApiAgent.getCourseCategoryById4s(st);if (!(stResponse.isSuccessful())) {ExceptionCast.castWithCodeAndDesc(stResponse.getCode(),stResponse.getMsg());}CourseCategoryDTO mtEntity = mtResponse.getResult();CourseCategoryDTO stEntity = stResponse.getResult();// 构建coursePub数据并执行保存操作// 课程基础数据CoursePub coursePub = CoursePubConvert.INSTANCE.courseBase2coursePub(courseBase);// 课程营销数据String marketJsonString = JsonUtil.objectTojson(courseMarket);coursePub.setMarket(marketJsonString);coursePub.setPrice(courseMarket.getPrice());coursePub.setCharge(courseMarket.getCharge());// 课程计划数据String teachplanJsonString = JsonUtil.objectTojson(teachplanTreeNodes);coursePub.setTeachplan(teachplanJsonString);// 课程教师// TODO: 2021/12/17 学员完成// 课程分类数据coursePub.setMtName(mtEntity.getName());coursePub.setStName(stEntity.getName());// 3.保存课程发布数据// CoursePub数据保存数据库中// 课程基础信息 课程营销 课程计划 课程教师// 一个courseBase数据对应一个coursePub数据LambdaQueryWrapper<CoursePub> pubQueryWrapper = new LambdaQueryWrapper<>();pubQueryWrapper.eq(CoursePub::getCourseId, courseId);pubQueryWrapper.eq(CoursePub::getCompanyId, companyId);CoursePub po = this.getOne(pubQueryWrapper);boolean result = false;// 判断课程发布数据是否存在if (ObjectUtils.isEmpty(po)) {// 如果不存在// 创建课程发布数据// 创建课程发布数据时,要将coursePub和courseBase数据进行关联coursePub.setCourseId(courseBase.getCourseBaseId());result = this.save(coursePub);} else {// 如果存在// 修改课程发布数据内容coursePub.setId(po.getId());result = this.updateById(coursePub);}if (!result) {ExceptionCast.cast(ContentErrorCode.E_120205);}// 4.将coursePub数据返回return coursePub;}}
常量类:
package com.xuecheng.content.common.constant;/*** <p>* 课程发布模板页面中的所有数据模型key值* </p>** @Description:*/public interface CoursePubTemplateKey {/*** 课程发布的数据模型key值* */String COURSEPUB="coursePub";/*** 课程营销的数据模型key值* */String COURSEMARKET="courseMarket";/*** 课程模式的数据模型key值* */String COURSETEACHMODEENUMS="courseTeachModeEnums";/*** 课程计划的数据模型key值* */String TEACHPLANNODE="teachplanNode";}
3.controller编写
@RestControllerpublic class CourseBaseController implements CourseBaseApi {/** SpringMVC 中controller 多结果视图器并存* CourseBaseController :* JackSonResolver (默认)--controller使用的是RestController* FreemarkerViewResolver* 1.添加依赖:spring-boot-starter-Freemarker* 2.添加配置:spring.freemarker.enabled = true* 3.方法的返回:controller的方法返回为ModelAndView对象* */@GetMapping("course/preview/{courseId}/{companyId}")public Object previewCourse(@PathVariable Long courseId,@PathVariable Long companyId) {Map<String, Object> dataMap = coursePubService.previewCourse(courseId, companyId);// 1.使用视图和数据模型对象ModelAndView modelAndView = new ModelAndView("learing_article",dataMap);return modelAndView;}}
3.5.6 信息接口测试
测试环境需要启动的微服务有:
1.注册中心 Nacos
2.服务网关 xc-gateway-service (端口:63010)
3.媒资管理 xc-media-service (端口:63050)
4.内容管理 xc-content-service (端口:63040)
1. 使用 postman 在请求信息中添加请求参数
GET http://127.0.0.1:63010/content/course/preview/43/1232141425

返回内容为渲染后的html
3.5.7 课程预览显示优化
在上面课程预览后,我们只是看到返回静态化后的html代码,并不能看到课程预览的最终效果,如下图:

对于高性能课程详情页实现,我们使用 CDN+Nginx缓存实现,这里需要做下面事项:
1.七牛云服务存储相关静态资源
2.Nginx对七牛云的代理
3.5.7.1 静态资源上传
教学机构在将课程发布后,会生成课程详情页。而课程详情页以及页面中所使用的静态资源都需要将其内容提前上传到七牛云存储中,下面我们开始上传静态资源。
1.创建静态页面存储空间
创建存储空间

在七牛云中创建存储空间:xc-static-pages
2.上传页面等静态资源
将今天下发资料中的 资料/详情页模板/课程详情页 下的所有资源上传到七牛服务上。

针对不同的资源,都会在一定的目录下,所有大家在上传时,一定要添加上传资源的路径,如下:
●CSS 资源路径 CSS资源路径为:css/
路径示意图

●img 资源路径 img资源路径为:img/
路径示意图

●js资源路径 js资源路径为:js/
路径示意图

●pages资源路径 pages资源路径为:pages/
路径示意图

●plugins资源路径 plugins资源路径比较多,分别为:
plugins/bootstrap/dist/css/
plugins/bootstrap/dist/js/
plugins/jquery/dist/
plugins/jscookie/
plugins/normalize-css/
plugins/vue/
路径示意图

3.访问上传的静态资源页面
在将所有静态资源上传后,我们通过浏览器可以访问器静态资源,例如访问静态资源页面:
复制页面访问连接

对静态页的访问

如果能看到上面的页面内容,说明静态资源上传成功。
3.5.7.2 Nginx 反向代理配置
在之前对七牛云上的访问静态资源路径,不难发现。其路径为:
http://自己七牛cdn域名/pages/learing_article.html

上面的域名为七牛云的 CDN 加速域名,每一个人所创建的存储空间给的域名都不一样。不管生成怎样的域名,都与学成在线网站的标识没有关系。我们现在需要做的就是将上面的路径改为:
http://www.xuecheng.com/cdn/pages/learing_article.html
这是我们就需要使用 Nginx 的反向代理来解决此问题,反向代理示意图如下:

●下载Nginx
下载地址:http://nginx.org/en/download.html
下载稳定版本,以nginx/Windows-1.14.2为例,直接下载 nginx-1.14.2.zip
我们也可以直接使用学成在线 \项目前置资料\软件\nginx-1.14.2.zip,复制到合法路径中并解压:
PS:路径中不要有中文和特殊字符

●启动Nginx
1.直接双击nginx.exe,双击后一个黑色的弹窗一闪而过

2.查看任务管理中,是否有 nginx 程序

nginx 的运行程序一共是两个,不能多也不能少。
3.访问 nginx 首页
直接在浏览器地址栏输入网址 http://localhost:80,回车,出现以下页面说明启动成功

●配置本地Host文件
windows系统下的 host 文件所在的目录为:C:\Windows\System32\drivers\etc

在host文件中,添加下面的域名:
127.0.0.1 www.xuecheng.com
示意图

●配置Nginx
打开nginx配置目录,在配置目录中找到nginx.conf配置文件并打开修改 :
nginx配置文件

修改其中的配置内容
#学成门户虚拟主机server {listen 80;server_name www.xuecheng.com;#对七牛云page资源的配置location ^~ /cdn/pages/ {proxy_pass http://自己cdn域名/pages/;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header REMOTE-HOST $remote_addr;}#对七牛云css资源的配置location ^~ /cdn/css/ {proxy_pass http://自己cdn域名/css/;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header REMOTE-HOST $remote_addr;}#对七牛云img资源的配置location ^~ /cdn/img/ {proxy_pass http://自己cdn域名/img/;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header REMOTE-HOST $remote_addr;}#对七牛云js资源的配置location ^~ /cdn/js/ {proxy_pass http://自己cdn域名/js/;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header REMOTE-HOST $remote_addr;}#对七牛云plugin资源的配置location ^~ /cdn/plugins/ {proxy_pass http://自己cdn域名/plugins/;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header REMOTE-HOST $remote_addr;}}
proxy_set_header:即允许重新定义或添加字段传递给代理服务器的请求头。该值可以包含文本、变量和它们的组合。
1)proxy_set_header X-Real-IP $remote_addr;
将$remote_addr的值放进变量X-Real-IP中,此变量名可变,$remote_addr的值为客户端的ip
2)proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
被代理服务可以获得真实客户端的 IP 地址
3)proxy_set_header REMOTE-HOST $remote_addr;
将$remote_addr的值放进变量 REMOTE-HOST 中,可以获得客户端域名地址
●访问测试页面
当我们修改了nginx的配置文件nginx.conf 时,不需要关闭nginx后重新启动nginx,
只需要执行命令相关命令即可让改动生效,命令为:
nginx -s reload
PS:此命令要进入到 nginx 的根目录下来执行,如下图:

在此访问 http://www.xuecheng.com/cdn/pages/learing_article.html 地址,得到的内容为:
●使用前端工程进行测试


