第二章 讲义-内容管理-课程新增修改和删除
学习目标
学习目标
1.熟悉新增课程需求
2.能够完成新增课程基本信息的功能实现
3.能够根据文档定义课程修改信息接口
4.能够完成修改课程基本信息的功能实现
5.能够根据文档定义删除课程基本信息接口
6.能够完成删除课程基本信息的功能实现熟悉新增课程需求
1.课程基本信息创建
学成在线需要教育机构入住到本平台后,通过教育机构中的老师来录入课程的相关信息。信息录入后由平台来管理,针对数据录入的需求,我们下面要开发课程基本信息创建的功能实现。
1.1 信息创建接口业务需求
课程基本数据的创建需要用户在页面中将课程的基本信息填写并提交给后端微服务,微服务将数据保存到数据库表中。
课程基本信息添加流程:
内容管理的课程业务流程
1.1.1 接口业务需求
具体需要如下:
1.机构老师点击“新增课程”,选择添加课程类型:直播课程或录播课程。
2.选择完课程类型后,填写基本内容:课程名称、课程适用人群、课程介绍等信息。
3.课程中的课程分类信息、课程等级、教学模式系统数据,需要前端查询系统管理服务后端接口。
4.填写完毕后,保存课程基本信息,并标识一个机构下的数据。
操作界面展示如下:
课程添加按钮示例图
选择直播或录播课程
前端课程新增页面效果
上图解释:
注释①:课程分类—数据来资源系统管理服务中的课程分类信息
注释②:课程等级—数据来资源系统管理服务中的数据字典信息
注释③:课程类型—数据来资源系统管理服务中的数据字典信息
课程基本信息创建前后端调用过程:
前后端调用示例图
上图解释:
1.前端会调用系统管理微服务获得页面常量信息并在页面显示。
2.用户在页面中填写完数据后向内容管理微服务提交课程基本信息数据并保存。
需求结论:
1.课程基本信息创建,前端需要两个微服务来完成
2.前端调用 ‘系统管理微服务’ 完成页面中常量信息的页面显示
3.前端调用 ‘内容管理系统微服务’ 完成课程基本信息的保存
1.1.2 课程基础信息数据模型
下面我来分析下这张表的主要字段。
课程基本信息(course_base)表结构
课程基本信息表分析
机构相关数据
课程基本信息是附属于一个教育架构下,学成在线主要是提供在线教学的平台。教学机构入驻后,在平台创建课程数据。
课程自身信息
对课程数据基本信息的描述,说明课程的教学模式、课程名称等。
课程数据操作数据
课程数据的操作会在相关字段进行记录,例如:课程数据的创建时间、课程数据的修改时间、课程数据的创建者等。
课程审核信息
教育结构创建出课程后,学成在线运营商需要对其进行数据审核,审核的操作也会进行记录,例如:审核人、审核时间等。
1.1.3 课程的状态说明
课程基础信息在内容管理中会有状态的显示,课程状态为 5 个状态,分别为:
1.未提交
2.已提交
3.审核通过
4.审核未通过
5.已发布
课程状态示意图
在添加对新增加的课程,应给予 “未提交” 的状态。
1.2 内容管理系统业务实现
下面将会实现课程基本信息保存功能,课程基本信息的保存是在内容管理系统服务,所以此接口的实现将在内容管理微服务中开发,接口信息依然定义在 Api 工程中。
1.2.1 信息创建接口定义
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
接口传入传出列表
上图解释:
两个属性源于课程营销信息,需要在 CourseBaseDTO中定义。
2.内容管理课程新增操作
前端传入的数据中包含课程基本信息和课程营销信息,所有在实现课程新增功能的同时,还需要对课程营销数据保存。
课程新增保存数据示意图
3. 课程数据封装类
在 ‘项目开发规范文档.md — 接口开发规范’ 文档声明,传入的参数使用 VO 实体类来封装传入的参数。 传出参数使用 DTO 已经声明,但根据参数列表,我们需要在 DTO 中将课程营销 DTO 作为属性。
保存和修改操作数据流转图
VO (Value Object)为值对象,通常是前端传输过来的数据封装对象。 VO 中的属性一般和前端所需的数据或表单中的数据一致。
VO在本项目中的定义:接受前端新增或修改的数据内容。
QO在本项目中的定义:接受前端查询条件数据内容。
PO在本项目中的定义:定义表结构数据内容。
DTO在本项目中的定义:返回前端所需要的结果数据内容。
课程基本信息 VO 数据(数据传入封装类)
package com.xuecheng.api.content.model.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
* <p></p>
*
* @Description:
*/
@Data
@ApiModel(value="CourseBaseVO", description="课程基本信息视图类,用于页面对课程基本信息添加和修改,操作的属性比较有限,不开放的属性不让操作")
public class CourseBaseVO {
@ApiModelProperty(value = "课程Id")
private Long courseBaseId;
@ApiModelProperty(value = "课程名称", required = true)
private String name;
@ApiModelProperty(value = "适用人群", required = true)
private String users;
@ApiModelProperty(value = "课程标签")
private String tags;
@ApiModelProperty(value = "大分类", required = true)
private String mt;
@ApiModelProperty(value = "小分类", required = true)
private String st;
@ApiModelProperty(value = "课程等级", required = true)
private String grade;
@ApiModelProperty(value = "教学模式(普通,录播,直播等)", required = true)
private String teachmode;
@ApiModelProperty(value = "课程介绍")
private String description;
@ApiModelProperty(value = "课程图片", required = true)
private String pic;
@ApiModelProperty(value = "收费规则,对应数据字典", required = true)
private String charge;
@ApiModelProperty(value = "价格")
private BigDecimal price;
}
课程基本信息 DTO 数据(数据传出封装类—项目已经创建)
@Data
@ApiModel(value="CourseBaseDTO", description="课程基本信息DTO")
public class CourseBaseDTO implements Serializable {
//其他代码省略
//添加营销信息的属性
@ApiModelProperty(value = "收费规则,对应数据字典", required = true)
private String charge;
@ApiModelProperty(value = "价格")
private BigDecimal price;
}
4. 使用代码生成器生成课程营销代码
● PO 数据 — CourseMarket
● DAO 持久层代码 — CourseMarketMapper和映射文件
● Service 业务层代码 — CourseMarketService接口和实现类
生成的代码后拷贝到 xc-content-service 基础包下。
5. 接口编写
在 xc-api 接口工程中的 ContentBaseApi 接口定义方法:
@Api(tags = "课程基本信息服务接口",description = "对课程基本信息业务操作")
public interface CourseBaseApi {
//其他代码省略
@ApiOperation(value = "保存课程基本信息")
@ApiImplicitParam(name = "courseBaseVO",
value = "课程基本视图信息", required = true,
dataType = "CourseBaseVO", paramType = "body")
CourseBaseDTO createCourseBase(CourseBaseVO courseBaseVO);
}
此接口编写后会在内容管理服务工程编写Controller类实现此接口。
1.2.2 信息创建接口开发
在之前的代码基础上开发课程基本信息添加功能,已经对数据源、持久层框架的配置信息都已配置,下面我们直接开始在应用三层(controller、service、dao)开发。
1.DAO编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2. service编写
接口
public interface CourseBaseService extends IService<CourseBase> {
//其他代码省略
/**
* 新增课程基本信息
* @param courseBaseDTO {@link CourseBaseDTO}
* @return boolean 操作的结果
*/
CourseBaseDTO createCourseBase(CourseBaseDTO courseBaseDTO);
}
实现类
@Service
public class CourseBaseServiceImpl extends ServiceImpl<CourseBaseMapper, CourseBase> implements CourseBaseService {
//其他代码省略
/*
* 业务分析:
* 1.是否要开启事务(查询不需要开启,增删改时需要开启事务)
* 2.判断关键数据
* 关键数据:数据来源是前端
* 添加时的必要数据:
* 1.从数据库结构-非空字段
* 2.前端的必要数据(红色*)
* 3.接口文档Yapi
* 数据内容:companyId、name、mt、st、grade、teachmode、users、pic、charge、price(收费课程需要判断)
* 如果必要数据没有给,告诉前端数据必填:抛出异常(终止程序、传递错误信息)
* 3.将dto数据转为po数据
* po数据都是全都传过来的信息
* po中对于新增数据一些字段需要在业务层进行赋值
* auditStatus
* status(数据库会默认赋值)
* createdate(mp自动填充)
* changedate(mp自动填充)
* 4.保持数据-判断保持的结果
* PS:要先保持课程基础信息表,课程营销表要记录coursebaseid
* 课程基础信息表
* 课程营销表
* PS:如果保持两张表时,其中有一张表保存失败,需要让全部的数据进行回滚:抛出异常
* 5.返回数据库最新的数据DTO
* */
@Override
@Transactional
public CourseBaseDTO createCourseBase(CourseBaseDTO dto) {
// 2.判断关键数据
// 关键数据:数据来源是前端
// 添加时的必要数据:
// 1.从数据库结构-非空字段
// 2.前端的必要数据(红色*)
// 3.接口文档Yapi
// 数据内容:companyId、name、mt、st、grade、teachmode、users、pic、charge、price(收费课程需要判断)
// 如果必要数据没有给,告诉前端数据必填:抛出异常(终止程序、传递错误信息)
if (ObjectUtils.isEmpty(dto.getCompanyId())) {
// 业务异常:程序员在做业务判断时,数据有问题。
// 异常:
// 1.终止程序
// 2.传递错误信息
// 3.使用运行时异常来抛出
// 运行时异常不需在编译期间处理
throw new RuntimeException("公司id不能为空");
}
if (StringUtil.isBlank(dto.getName())) {
throw new RuntimeException("课程名称不能为空");
}
if (StringUtil.isBlank(dto.getMt())) {
throw new RuntimeException("课程大分类不能为空");
}
if (StringUtil.isBlank(dto.getSt())) {
throw new RuntimeException("课程小分类不能为空");
}
if (StringUtil.isBlank(dto.getGrade())) {
throw new RuntimeException("课程等级不能为空");
}
if (StringUtil.isBlank(dto.getTeachmode())) {
throw new RuntimeException("课程教学模式不能为空");
}
if (StringUtil.isBlank(dto.getUsers())) {
throw new RuntimeException("使用人群不能为空");
}
if (StringUtil.isBlank(dto.getCharge())) {
throw new RuntimeException("课程收费不能为空");
}
// 判断收费课程,价格不能为空,必须要大于0
if (CourseChargeEnum.CHARGE_YES.getCode().equals(dto.getCharge())) {
if (ObjectUtils.isEmpty(dto.getPrice()) || dto.getPrice().floatValue() <= 0) {
throw new RuntimeException("收费课程价格非法,请填入合法的价格");
}
} else {
// 课程为免费课程,价格为0
dto.setPrice(new BigDecimal("0"));
}
// 3.将dto数据转为po数据
// po数据都是全都传过来的信息
// po中对于新增数据一些字段需要在业务层进行赋值
// auditStatus
// status(数据库会默认赋值)
// createdate(mp自动填充)
// changedate(mp自动填充)
CourseBase courseBase = CourseBaseConvert.INSTANCE.dto2entity(dto);
// auditStatus:新增课程的状态为-->未提交
courseBase.setAuditStatus(CourseAuditEnum.AUDIT_UNPAST_STATUS.getCode());
// 4.保持数据-判断保持的结果
// PS:要先保持课程基础信息表,课程营销表要记录coursebaseid
// 课程基础信息表
boolean baseResult = this.save(courseBase);
if (!baseResult) {
throw new RuntimeException("课程基础信息保存失败");
}
// 课程营销表
// PS:如果保持两张表时,其中有一张表保存失败,需要让全部的数据进行回滚:抛出异常
CourseMarket courseMarket = new CourseMarket();
courseMarket.setCourseId(courseBase.getId());
courseMarket.setCharge(dto.getCharge());
courseMarket.setPrice(dto.getPrice().floatValue());
boolean marketResult = courseMarketService.save(courseMarket);
if (!marketResult) {
throw new RuntimeException("课程营销保存失败");
}
// 5.返回数据库最新的数据DTO
CourseBase po = this.getById(courseBase.getId());
CourseBaseDTO resultDTO = CourseBaseConvert.INSTANCE.entity2dto(po);
resultDTO.setCharge(dto.getCharge());
resultDTO.setPrice(dto.getPrice());
return resultDTO;
}
}
业务层对数据的判断,不合理数据以 RuntimeException 进行抛出。使用异常的好处有:
1.RuntimeException 运行异常在编码阶段不需要使用 try/catch 进行处理。
2.异常的抛出可以是的当前业务代码终止,并返回错误数据。
2.Controller 编写
在 ContentBaseController 中新增方法,如下:
@RestController
public class CourseBaseController implements CourseBaseApi {
@Autowired
private CourseBaseService courseBaseService;
//其他代码省略
@PostMapping("course")
public CourseBaseDTO createCourseBase(@RequestBody CourseBaseVO courseBaseVO) {
CourseBaseDTO courseBaseDTO = CourseBaseConvert.INSTANCE.vo2dto(courseBaseVO);
//通过工具类获得公司和用户信息
Long companyId = SecurityUtil.getCompanyId();
//设置公司和登录用户信息
courseBaseDTO.setCompanyId(companyId);
return courseBaseService.createCourseBase(courseBaseDTO);
}
}
课程基础信息下赋值机构 Id 值的原因:
1.一门课程必须要在一个教育机构下。
2.课程中必须要存储教育机构 Id 值。
3.教育机构是在登录状态下,通过 SecurityUtil 来获得教育机构 Id 值, 后面的认证课程中会来讲解其原因。
1.2.3 信息创建接口测试
测试环境需要启动的微服务有:
1.注册中心 Nacos
2.服务网关 xc-gateway-service (端口:63010)
3.内容管理 xc-content-service (端口:63040)
1. 使用 postman 在请求信息中添加请求参数
请求头(访问令牌)
请求头的 key 值: authorization
请求头的 value 值:
Bearer ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9
请求体信息(课程添加测试数据)
{
"name" : "测试数据",
"mt" : "1-3",
"st" : "1-3-1",
"grade" : "204002",
"teachmode" : "200001",
"charge" : "201001"
}
1.3 系统管理业务实现(已实现)
课程基本信息保存功能中,前端需要在页面中显示课程分类和常量信息(课程等级、学习模式…), 这些数据在 ‘系统给管理微服务’ 中,对此需要构建 ‘系统管理微服务’ 并进行开发。
项目系统管理功能示例图
要对 ‘系统给管理微服务’ 进行开发,开发功能大致是分为两类:
1.系统数据信息获取
● 根据 Code来获得指定系统数据信息
●查询所有系统数据信息(便于前端用于存储)
2.课程分类信息获取
● 查询所有课程分类树形结构信息
针对上面两个功能我们需要分别开发三个接口,下面先来开发 ‘系统常量信息获取’ 。
1.3.1 系统数据信息业务实现
1.3.1.1 数据模型(表结构)
系统管理单独使用一个数据库(xc_system)中,下面我们来创建数据库和数据库表结构,并了解表中结构。
1. 导入数据库数据
在今天下发资料里的数据脚本导入到本地 MySQL 数据中,资料位置在 ‘资料/数据库脚本/xc_system.sql’ 。
导入后的数据库内容
2. 数据字典名称解释
在开发中我们会遇到两种情况:
1.开发中有些变量信息,其内容固定,并在多个地方使用。面对多变的需求,这些变量可能会有所调整。
2.在用户界面中显示下拉框菜单里的数据,一般不会写死,需要进行统一管理。
固定变量值示例图
面对上面的两个问题中的变量信息,通常我们使用数据库的表来进行管理,而这张表的名称为:数据字典。
简而言之,数据字典是描述系统数据的信息集合。
3. 数据字典表说明
name :标识数据字典的名称
code:数据字典的编号
item_values:数据字典项集合数据(json格式)
1.3.1.2 环境配置
1.在 xc-parent 父工程下导入模块 xc-system-service
将今天下发资料中的 xc-system-service 系统管理微服务工程解压并导入的 IDEA 工程项目中。
2. 工程结构
●创建包结构本工程为内容管理系统微服务,其包的结构为:
○基础包结构为:com.xuecheng.system
○控制层包:com.xuecheng.system.controller
○服务层包:com.xuecheng.system.service
○持久层:com.xuecheng.system.mapper
○配置:com.xuecheng.system.config
○实体:com.xuecheng.system.entity
●启动类
在基础包结构下创建 Spring Boot 启动类
package com.xuecheng.system;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* <p>
* 系统管理启动类
* </p>
*
* @Description:
*/
@SpringBootApplication
@EnableSwagger2Doc
@EnableDiscoveryClient
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class,args);
}
}
3. 配置文件
●启动配置文件 bootstrap.yml
在 maven 的结构中的 src/main/resources 里创建。
#微服务启动参数
spring:
application:
name: system-service
cloud:
nacos:
discovery: #配置注册中心
server-addr: 192.168.94.129:8848
namespace: 5c0b093c-4084-46b5-bf33-899321cb7ef5
group: ${group.name}
config: #配置中心
server-addr: 192.168.94.129:8848
namespace: 5c0b093c-4084-46b5-bf33-899321cb7ef5
group: ${group.name}
file-extension: properties
shared-configs:
- dataId: mp-config.properites
group: ${group.name}
profiles: # 激活配置环境
active: dev
# 组名称
group:
name: xc-group
# 日志文件配置路径
logging:
config: classpath:./log4j2-dev.xml
# swagger 文档配置
swagger:
title: "学成在线系统管理系统"
description: "系统管理对整个系统数据进行业务管理"
base-package: com.xuecheng.system
enabled: true
version: 1.0.0
上面的配置文件主要信息包括四类:
1.微服务的基本信息
2.日志配置路径
3.swagger配置信息
4.nacos配置信息
4.配置中心配置参数
在 nacos 的 dev环境下创建系统管理微服务的配置 system-service-dev.properties,配置如下:
#srpingboot http 配置信息
server.servlet.context-path = /system
server.port=63110
#srping druid 配置信息
spring.datasource.url = jdbc:mysql://192.168.94.129:3306/xc_system?userUnicode=true&useSSL=false&characterEncoding=utf8
5.公共配置信息
直接应用 mp-config.properties、spring-http-config.properties 、spring-druid-config.properties 公共配置即可
1.3.2 课程分类业务实现
1.3.2.1 数据模型(表结构)
之前我们已经将系统服务的数据库脚本导入到本次仓库中,无需再次导入。
1. 课程分类表说明
自身信息描述:id,name,label
父信息描述:parentId
树形结构排序:orderby
2.PO实体类定义
package com.xuecheng.system.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* <p>
* 课程分类
* </p>
*/
@Data
@TableName("course_category")
public class CourseCategory implements Serializable {
/**
* 主键
*/
private String id;
/**
* 分类名称
*/
private String name;
/**
* 分类标签默认和名称一样
*/
private String label;
/**
* 父结点id(第一级的父节点是0,自关联字段id)
*/
private String parentid;
/**
* 是否显示
*/
private Integer isShow;
/**
* 排序字段
*/
private Integer orderby;
/**
* 是否叶子
*/
private Integer isLeaf;
}
2.课程基本信息修改-实战
教育机构中的老师对录入课程基本信息,平台来管理应提供对其信息就行修改的业务操作,针对课程基本信息数据修改的需求,我们下面要开发课程基本信息创建的功能实现。
2.1 课程信息修改接口业务需求
在课程基本数据的添加之后,通过课程基本信息列表中的修改按钮对单个信息进行修改操作。在点击后修改按钮后跳入修改页面,并将之前的信息回显到修改页面中的表单当中。将信息修改完毕之后,通过提交按钮向后端微服务提交数据进行修改操作。
2.1.1 接口业务需求
操作界面展示如下:
课程列表编辑按钮
编辑页面回显内容数据
具体需要如下:
1.机构老师在课程基本信息列表中点击“修改课程”,进入修改课程页面。
2.页面跳入到课程基本信息编辑页面,并将之前课程基本数据进行回显。
3.课程基本信息修改完信息,点击提交按钮提交数据到后端微服务中。
4.课程基本信息的状态不能为’已提交’ 、’审核通过’、’课程已发布’ 状态。
5.教学机构只能修改自己机构下的数据。
2.1.2 数据模型(表结构)和实体类定义
数据库表所涉及到为 xc_content 中的 course_base 表。之前在信息查询中有所介绍,在此对表结构和实体类PO类不再介绍。
课程基础信息在内容管理中会有状态的显示,课程状态为 5 个状态,分别为:
1.未提交
2.已提交
3.审核通过
4.审核未通过
5.已发布
其中,课程状态为 ‘未提交’、‘审核未通过’状态才可以修改数据。
课程状态示意图
2.2 课程信息修改功能业务实现
下面将会实现课程基本信息修改功能,课程基本信息的修改是在内容管理系统服务,所以此接口的实现将在内容管理微服务中开发,接口信息依然定义在 Api 工程中。
根据课程信息修改功能业务现在需要定义两个接口:
1.根据课程 Id 查询课程基本信息接口—用于前端数据回显。
2.修改课程信息接口—用于前端保存修改后的课程基本信息数据。
1.判断关键数据
2.判断课程的审核状态
3.判断课程是否删除
2.2.1 信息修改接口定义
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
传入传出参数—更新课程信息
传入传出参数—根据Id查询课程基本信息
2. 课程数据封装类
在 ‘项目开发规范文档.md — 接口开发规范’ 文档声明,传入的参数使用 VO 实体类来封装传入的参数。 传出参数使用 DTO 已经声明,但根据参数列表,我们需要在 DTO 中将课程营销 DTO 作为属性。
VO 和 DTO 在课程基本信息创建已经声明好,无需再次声明。
2. 接口编写
在 xc-api 接口工程中的 ContentBaseApi 接口定义方法:
package com.xuecheng.api.content;
import com.xuecheng.api.content.model.qo.QueryCourseBaseModel;
import com.xuecheng.common.domain.page.PageVO;
/**
* <p>
* 课程基本信息 Api 接口路径定义
* </p>
*
* @Description: 课程基本信息 Api 接口路径定义
*/
@Api(tags = "课程基本信息服务接口",description = "对课程基本信息业务操作")
public interface CourseBaseApi {
//其他代码省略
@ApiOperation(value = "根据Id获取课程基本信息")
@ApiImplicitParam(name = "courseBaseId", value = "课程基本信息ID", required = true, dataType = "Long", paramType = "path", example = "1")
CourseBaseDTO getCourseBase(Long courseBaseId);
@ApiOperation("更新课程基本信息")
@ApiImplicitParam(name = "courseBaseVO", value = "课程基本信息VO", required = true, dataType = "CourseBaseVO", paramType = "body")
CourseBaseDTO modifyCourseBase(CourseBaseVO courseBaseVO);
}
2.2.2 信息修改接口开发
在之前的代码基础上开发课程基本信息添加功能,已经对数据源、持久层框架的配置信息都已配置,下面我们直接开始应用三层开发。
1.DAO编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2. service编写
接口
public interface CourseBaseService extends IService<CourseBase> {
//其他代码省略
/**
* 根据Id查询课程基础信息
* @param courseBaseId 课程的Id值
* @param companyId 公司的Id值
* @return CourseBaseDTO
*/
CourseBaseDTO getCourseBaseById(Long courseBaseId,Long companyId);
/**
* 修改课程基础信息
* @param dto CourseBaseDTO
* @return
*/
CourseBaseDTO modifyCourseBase(CourseBaseDTO dto);
}
实现类
@Service
public class CourseBaseServiceImpl extends ServiceImpl<CourseBaseMapper, CourseBase> implements CourseBaseService {
//其他代码省略
/*
* 业务分析:
* 1.是否要开启事务
* 2.判断关键数据
* 前端传递的数据为关键数据
* courseBaseId
* companyId
* 3.判断业务数据
* 操作做的后端数据或关联的数据--后端的数据
* 判断该课程是否存在
* 4.将查询出的数据转为dto返回
CourseBase
CourseMarket
* */
public CourseBaseDTO getCourseBaseById(Long courseBaseId, Long companyId) {
//2.判断关键数据
// 前端传递的数据为关键数据
// courseBaseId
// companyId
if (ObjectUtils.isEmpty(courseBaseId)||
ObjectUtils.isEmpty(companyId)
) {
throw new RuntimeException("传入参数和接口不匹配");
}
// 3.判断业务数据
// 课程基础信息
// 判断是否存在
// 判断是否是同一家教学机构
// 判断是否删除
CourseBase courseBase = getCourseBaseByBaseId(courseBaseId, companyId);
// 4.查询数据并转为dto返回
CourseBaseDTO courseBaseDTO = CourseBaseConvert.INSTANCE.entity2dto(courseBase);
// 课程营销
CourseMarket courseMarket = getCourseMarketByCourseId(courseBaseId);
courseBaseDTO.setCharge(courseMarket.getCharge());
courseBaseDTO.setPrice(new BigDecimal(courseMarket.getPrice().toString()));
return courseBaseDTO;
}
private CourseBase getCourseBaseByBaseId(Long courseBaseId, Long companyId) {
// select * from course_base where id = ? and companyId = ?
LambdaQueryWrapper<CourseBase> baseQueryWrapper = new LambdaQueryWrapper<>();
baseQueryWrapper.eq(CourseBase::getId, courseBaseId);
baseQueryWrapper.eq(CourseBase::getCompanyId, companyId);
// baseQueryWrapper.eq(CourseBase::getStatus, CommonEnum.USING_FLAG.getCodeInt());
CourseBase courseBase = this.getOne(baseQueryWrapper);
if (ObjectUtils.isEmpty(courseBase)) {
throw new RuntimeException("课程不存在");
}
Integer status = courseBase.getStatus();
if (!(CommonEnum.USING_FLAG.getCodeInt().equals(status))) {
throw new RuntimeException("课程信息已经被删除");
}
return courseBase;
}
/*
* 业务分析:
* 1.是否要开启事务
* 增删改需要开启事务
* 方式:在方法上添加@Transactional
* 2.判断关键数据
* 前端传递过来的数据
* 需要比新增时要多判断一个数据:courseBaseId
* 修改课程基础信息是必须携带id值的
* 3.判断业务数据
* 判断是否存在
* 判断是否是同一家机构
* 判断是否删除
* 判断审核状态
* 未提交--新创建出的课程
* 审核未通过--运营审核没有通过
* 只有这两个状态才可以修改课程基础信息
* 4.将dto转为po
* 5.保存数据
CourseBase
CourseMarket
6.将修改后的最新数据返回给前端
* */
@Transactional
public CourseBaseDTO modifyCourseBase(CourseBaseDTO dto) {
//2.判断关键数据
// 前端传递过来的数据
// 需要比新增时要多判断一个数据:courseBaseId
// 修改课程基础信息是必须携带id值的
verifyCourseMsg(dto);
Long courseBaseId = dto.getCourseBaseId();
if (ObjectUtils.isEmpty(courseBaseId)) {
throw new RuntimeException("修改课程id不能为空");
}
// 3.判断业务数据
// 判断是否存在
// 判断是否是同一家机构
// 判断是否删除
// 判断审核状态
// 未提交--新创建出的课程
// 审核未通过--运营审核没有通过
// 只有这两个状态才可以修改课程基础信息
getCourseByLogic(dto.getCompanyId(), courseBaseId);
// 课程营销
CourseMarket courseMarket = getCourseMarketByCourseId(courseBaseId);
// 4.将dto转为po
// 课程基础信息表
// 有些内容是不容修改的:companyid、auditstatus、status
CourseBase po = CourseBaseConvert.INSTANCE.dto2entity(dto);
//5.保存数据
// CourseBase
// CourseMarket
// 为了防止前端乱意修改内容
po.setCompanyId(null);
po.setAuditStatus(null);
po.setStatus(null);
boolean baseResult = this.updateById(po);
if (!baseResult) {
throw new RuntimeException("课程信息修改失败");
}
// 课程营销数据表
// charge price
// 如果使用mq修改一张表中的极个别数据,UpdateWrapper
// update course_market set charge=xxx,price=xxx where courseid=xx
LambdaUpdateWrapper<CourseMarket> marketUpdateWrapper = new LambdaUpdateWrapper<>();
marketUpdateWrapper.set(CourseMarket::getCharge,dto.getCharge());
String charge = dto.getCharge();
if (CourseChargeEnum.CHARGE_YES.getCode().equals(charge)) {
BigDecimal price = dto.getPrice();
if (ObjectUtils.isEmpty(price)) {
throw new RuntimeException("收费课程价格不能为空");
}
marketUpdateWrapper.set(CourseMarket::getPrice, dto.getPrice().floatValue());
} else {
// 如果课程为免费,需要价格赋值为0
marketUpdateWrapper.set(CourseMarket::getPrice, 0F);
}
marketUpdateWrapper.eq(CourseMarket::getCourseId, courseBaseId);
boolean marketResult = courseMarketService.update(marketUpdateWrapper);
// 修改数据时要判断修改后结果
if (!marketResult) {
throw new RuntimeException("修改课程营销数据失败");
}
// 6.将修改后的最新数据返回给前端
CourseBaseDTO resultDTO = getLastCourseBaseDTO(dto, courseBaseId);
return resultDTO;
}
private void verifyCourseMsg(CourseBaseDTO dto) {
if (ObjectUtils.isEmpty(dto.getCompanyId())) {
// 业务异常:程序员在做业务判断时,数据有问题。
// 异常:
// 1.终止程序
// 2.传递错误信息
// 3.使用运行时异常来抛出
// 运行时异常不需在编译期间处理
ExceptionCast.cast(ContentErrorCode.E_120018);
}
if (StringUtil.isBlank(dto.getName())) {
ExceptionCast.cast(ContentErrorCode.E_120004);
}
if (StringUtil.isBlank(dto.getMt())) {
ExceptionCast.cast(ContentErrorCode.E_120002);
}
if (StringUtil.isBlank(dto.getSt())) {
ExceptionCast.cast(ContentErrorCode.E_120003);
}
if (StringUtil.isBlank(dto.getGrade())) {
ExceptionCast.cast(ContentErrorCode.E_120007);
}
if (StringUtil.isBlank(dto.getTeachmode())) {
ExceptionCast.cast(ContentErrorCode.E_120006);
}
if (StringUtil.isBlank(dto.getUsers())) {
ExceptionCast.cast(ContentErrorCode.E_120019);
}
if (StringUtil.isBlank(dto.getCharge())) {
ExceptionCast.cast(ContentErrorCode.E_120020);
}
}
private void getCourseByLogic(Long companyId, Long courseBaseId) {
CourseBase courseBase = getCourseBaseByBaseId(courseBaseId, 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)
) {
throw new RuntimeException("课程审核状态异常");
}
}
private CourseMarket getCourseMarketByCourseId(Long courseBaseId) {
LambdaQueryWrapper<CourseMarket> marketQueryWrapper = new LambdaQueryWrapper<>();
marketQueryWrapper.eq(CourseMarket::getCourseId, courseBaseId);
CourseMarket courseMarket = courseMarketService.getOne(marketQueryWrapper);
if (ObjectUtils.isEmpty(courseMarket)) {
throw new RuntimeException("课程营销数据不存在");
}
return courseMarket;
}
private CourseBaseDTO getLastCourseBaseDTO(CourseBaseDTO dto, Long id) {
CourseBase po = this.getById(id);
CourseBaseDTO resultDTO = CourseBaseConvert.INSTANCE.entity2dto(po);
resultDTO.setCharge(dto.getCharge());
resultDTO.setPrice(dto.getPrice());
return resultDTO;
}
}
3. controller编写
@RestController
public class CourseBaseController implements CourseBaseApi {
@Autowired
private CourseBaseService courseBaseService;
//其他代码省略
@GetMapping(value = "course/{courseBaseId}")
public CourseBaseDTO getCourseBase(@PathVariable Long courseBaseId) {
//1.获得公司Id值
Long companyId = SecurityUtil.getCompanyId();
//2.获得课程基本信息数据
CourseBaseDTO courseBase = courseBaseService.getCourseBaseById(courseBaseId,companyId);
return courseBase;
}
@PutMapping("course")
public CourseBaseDTO modifyCourseBase(@RequestBody CourseBaseVO courseBaseVO) {
//1.将VO数据转为DTO数据
CourseBaseDTO courseBaseDTO = CourseBaseConvert.INSTANCE.vo2dto(courseBaseVO);
//2.通过工具类获得公司和用户信息
Long companyId = SecurityUtil.getCompanyId();
courseBaseDTO.setCompanyId(companyId);
return courseBaseService.modifyCourseBase(courseBaseDTO);
}
}
2.2.3 信息修改接口测试
测试环境需要启动的微服务有:
1.注册中心 Nacos
2.服务网关 xc-gateway-service (端口:63010)
3.内容管理 xc-content-service (端口:63040)
1. 使用 postman 在请求信息中添加请求参数
请求头(访问令牌)
请求头的 key 值: authorization
请求头的 value 值:
Bearer ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9
请求体信息(课程修改测试数据
{
"courseBaseId": 39,
"name" : "SpringBoot核心1111",
"mt" : "1-3",
"st" : "1-3-2",
"grade": "204003",
"teachmode": "200002",
"charge": "201000"
}
3.课程基本信息删除-实战
教育机构中的老师对录入课程基本信息,平台来管理应提供对其信息就行删除的业务操作,针对课程基本信息数据删除的需求,我们下面要开发课程基本信息删除的功能实现。
3.1 信息删除接口业务需求
通过课程基本信息列表中的删除按钮对单个信息进行删除操作。在点击后删除按钮后提示删除操作信息,需要用户点击 ‘确定’ 按钮后,方可将数据在数据库中逻辑删除 (并非物理上传)。删除成功后,需要返回信息列表界面。
3.1.1 接口业务需求
操作界面展示如下:
课程列表编辑按钮
具体需要如下:
1.机构老师在课程基本信息列表中点击“删除” 按钮。
2.页面弹出确认模态窗口,需要用户进行确认删除消息。
3.用户在确认模块窗口中点击 ‘确认’ 按钮,对数据进行删除操作。
4.删除成功后,提示删除结果操作。并返回信息列表界面中。
5.基本信息删除操作为逻辑删除,并非物理删除。
6.课程基本信息的状态不能为’已提交’ 、’审核通过’、’课程已发布’ 状态。
7.教学机构只能删除自己机构下的数据。
逻辑删除数据修改:
数据模型状态字段示意图
上图解释:
所谓逻辑删除,是对数据库数据不进行直接删除。而是对数据库表的状态字段进行修改操作。
3.2 课程删除功能业务实现
下面将会实现课程基本信息删除功能,课程基本信息的删除是在内容管理系统服务,所以此接口的实现将在内容管理微服务中开发,接口信息依然定义在 Api 工程中。
3.2.1 信息删除接口定义
在 ‘项目开发规范文档.md — 接口开发规范’ 文档声明,参入的参数为删除数据的 ID 集合数据,接口响应为Boolean类型数据。
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
传入传出参数—更新课程信息
1. 接口编写
在 xc-api 接口工程中的 ContentBaseApi 接口定义方法:
package com.xuecheng.api.content;
import com.xuecheng.api.content.model.qo.QueryCourseBaseModel;
import com.xuecheng.common.domain.page.PageVO;
/**
* <p>
* 课程基本信息 Api 接口路径定义
* </p>
*
* @Description: 课程基本信息 Api 接口路径定义
*/
@Api(tags = "课程基本信息服务接口",description = "对课程基本信息业务操作")
public interface CourseBaseApi {
//其他代码省略
@ApiOperation("根据Id删除课程信息")
@ApiImplicitParam(name = "courseBaseId", value = "课程id值", required = true, paramType = "path")
void removeCourseBaseById(Long courseBaseId)
}
3.2.2 信息删除接口开发
1.DAO编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2. service编写
接口
public interface CourseBaseService extends IService<CourseBase> {
//其他代码省略
/**
* 根据课程Id进行删除操作
*/
void removeCourseBase(Long courseBaseId,Long companyId);
}
实现类
@Service
public class CourseBaseServiceImpl extends ServiceImpl<CourseBaseMapper, CourseBase> implements CourseBaseService {
//其他代码省略
/*
* 业务分析:
1.是否要开启事务
需要开启
2.判断关键数据
courseBaseId
companyid
3.判断业务数据
判断课程是否存在
判断是否是同一家机构
判断课程是否已经删除
判断课程的审核状态
未提交
审核未通过
只有这些状态才可以被删除
4.删除课程基础信息(根据id删除)
修改数据库的数据status
* */
@Transactional
public void removeCourseBase(Long courseBaseId, Long companyId) {
// 2.判断关键数据
// courseBaseId
// companyid
if (ObjectUtils.isEmpty(courseBaseId) ||
ObjectUtils.isEmpty(companyId)
) {
ExceptionCast.cast(CommonErrorCode.E_100101);
}
//3.判断业务数据
// 判断课程是否存在
// 判断是否是同一家机构
// 判断课程是否已经删除
// 判断课程的审核状态
// 未提交
// 审核未通过
// 只有这些状态才可以被删除
getCourseByLogic(companyId, courseBaseId);
// 4.删除课程基础信息(根据id删除)
// 修改数据库的数据status
// update course_base set status = ? where course_id = ?
LambdaUpdateWrapper<CourseBase> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(CourseBase::getStatus,CommonEnum.DELETE_FLAG.getCodeInt());
updateWrapper.set(CourseBase::getChangeDate, LocalDateTime.now());
updateWrapper.eq(CourseBase::getId, courseBaseId);
boolean result = update(updateWrapper);
if (!result) {
ExceptionCast.cast(ContentErrorCode.E_120017);
}
}
private void getCourseByLogic(Long companyId, Long courseBaseId) {
CourseBase courseBase = getCourseBaseByBaseId(courseBaseId, 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)
) {
throw new RuntimeException("课程审核状态异常");
}
}
}
3. controller编写
@RestController
@RequestMapping("coursebase")
public class CourseBaseController implements CourseBaseApi {
@Autowired
private CourseBaseService courseBaseService;
@DeleteMapping("course/{courseBaseId}")
public void removeCourseBaseById(@PathVariable Long courseBaseId) {
// 1.获得公司Id
Long companyId = SecurityUtil.getCompanyId();
courseBaseService.removeCourseBase(courseBaseId, companyId);
}
//其他代码省略
}
3.2.3 信息删除接口测试
测试环境需要启动的微服务有:
1.注册中心 xc-discover-service (端口:63000)
2.服务网关 xc-gateway-service (端口:63010)
3.内容管理 xc-content-service (端口:63040)
1. 使用 postman 在请求信息中添加请求参数
DELETE http://localhost:63010/content/course/39
请求头(访问令牌)
请求头的 key 值: authorization
请求头的 value 值:
Bearer ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9