整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

我的工作学习笔记

的工作学习笔记

计算机(computer)俗称电脑,是现代一种用于高速计算的电子计算机器,可以进行数值计算,又可以进行逻辑计算,还具有存储记忆功能。是能够按照程序运行,自动、高速处理海量数据的现代化智能电子设备。

60天带领小白学习计算机和编程,下面分享我的第二课

Excel:电子表格
功能:统计数据、制作图表、数据分析
电子表格是现代办公最常用的软件没有之一

如果office文件是97-2003版本,那么可以在之后所有的office版本中打开,但是如果office是2007之后的版本无法在97-2003版本中打开。office不支持向上兼容

保存97-2003版本:文件-另存为-选择97-2003版本

输入身份证号:需要将单元格数字属性设置为文本
单元格坐标:由纵坐标和横坐标表示,当需要表示一个区域时这个区域的斜对角线坐标可以表示这个区域例如:(C6:F11)
注:标点符号必须为英文标点符号

每一个工作表生成一个表格,不允许出现一张工作表中设置两个表格的情况
创建表格:选择需要创建表格的区域-右键-设置单元格格式-边框标签-选择表格边框的样式外观-单击外边框和内边框

插入:右键插入,插入内容会出现在当前表格的左边
数字递增:选中当前表格-光标移动到右下角-按住ctrl-点击向下拖拽
复制内容:选择当前表格-光标移动到右下角-点击向下拖拽

回车:光标向下移动
tab:光标向右移动

条件格式:
a、设置条件:设置大于-10分
b、根据设置条件设置突出颜色
表格样式:在样式选项中office给了默认选项,但是所有的表格需要严肃类型,颜色尽量使用冷色调

筛选排序:
a、选中需要排序的行或者列
b、选中筛选-点击选中列或行的漏斗图标
c、设置筛选条件或者排序条件-确定即可

背景色和前景色
a、背景色就是表格的底色,图标为油桶
b、前景色就是字体颜色,图标是大写A

放大缩小表格:按ctrl+鼠标滚轮

冻结窗格:可以冻结首列或首行,方便在表格项太多时不清楚内容是什么
a、视图标签-冻结窗格-选择目标单元格-冻结拆分窗格(以目标单元格为基准,向上和向左所有单元格全部冻结)

设置打印线:
选择视图-页面布局-普通页面-在表格中会出现虚线-这个虚线就是打印线
注:如果需要微调打印线位置可以通过页面布局标签-设置页边距

数据的有效性:用于设置选项
选择数据-数据验证-数据验证-在允许中选择序列-在来源中添加数据来源,数据来源1和数据来源2使用逗号隔开(必须是英文逗号)

求和:
=单元格坐标1+单元格左边2
=F3+F5+F7

=sum(坐标1:坐标2)
=SUM(F3:F7)
=SUM(E4,F5,F8)代表求三个单元格相加(按住ctrl鼠标点击)

平均值:average
=average(坐标1:坐标2)
=AVERAGE(E3:E7)

Execl:电子表格

引用:
a、表内引用:
在单元格内输入=坐标,例如
=b3
b、表见引用:不同表格之间的引用
在单元格中输入=表名称!单元格坐标,例如
=表2!F5
(=手动鼠标切换表,点击准备引用的单元格、回车)

递增:
a、在单元格内输入文本-鼠标点击右下角-按住ctrl-向下拖拽
b、连续在两个单元格内输入文本-鼠标点击右下角-向下拖拽
求和:
a、=坐标1+坐标2+坐标3
=C3+D3+E3
b、=sum(坐标1:坐标2)
=sum(C3:E3)
平均分:=average(坐标1:坐标2)
=average(D3:D10)
最大值最小值:
=max(坐标1:坐标2)
=max(D3:D10)
=min(坐标1:坐标2)
=min(D3:D10)

条件函数:
=if(条件,"结果1","结果2")
=IF(D3>=60,"及格","不及格")
注:if函数只有是或者否,没有第三种情况,必须符号为英文输入法

在生成图表后-插入-图表-选择适当的图表对应表格-调整图表的样式

随机:=RANDBETWEEN(1,100)
between:在。。。。之间
在表格任意位置发生数据变化时就会重新产生一个随机数

引用:=vlookup(引用值,引用数据范文,输出列)
=VLOOKUP(B3,表3!A1:B18,2)
引用值:为B3
引用范围是:表3的A1到B18之间的区域
输出一个结果:输出的结果为引用范围的第2列

注意:引用范围必须为成列的数据,输出的结果必须是数字,每一个数字代表一列。

查看本机IP地址:
a、选择桌面网络快捷方式-右键-属性-更改适配器设置-选择网卡-右键-详细信息-IPv4地址
b、windows+R-cmd回车-ipconfig-找到正在使用的网卡-查看ipv4地址


config
configure 配置
configuration

测试连通性:
ping+对端的IP地址,可以测试与对端的联通是否正常

C:\Users\Administrator>ping 192.168.60.12

正在 Ping 192.168.60.12 具有 32 字节的数据:
来自 192.168.60.12 的回复: 字节=32 时间=1ms TTL=128
来自 192.168.60.12 的回复: 字节=32 时间=1ms TTL=128
来自 192.168.60.12 的回复: 字节=32 时间=1ms TTL=128
来自 192.168.60.12 的回复: 字节=32 时间=1ms TTL=128

TTL:生存时间,用于解决数据无线循环
Request timed out \请求超时

设置边框:对角线设置,选择单元格-右键-设置单元格-边框-选择样式-点击预览图周边的对角线

国家数据:http://data.stats.gov.cn/
从国家数据导出数据-复制数据粘贴到本地建立的excel电子表格中-插入图表-点击画笔可以修改图表中的样式(背景色和前景色必须形成反差)-点击加号工具可以添加图表的其他内容(例如各种网格和趋势线)-点击漏斗完成数据筛选

注:可以使用ctrl+鼠标的点击拖拽完成数据的独立选择

镜像文件:一种文件后缀为.iso的安装包,需要使用虚拟光驱软件运行才能使用

解决office激活:
a、安装.NET ,要先于激活工具安装
b、关闭防火墙安装或者下载KMS软件
1、对于win10操作系统,必须关闭windows defender防火墙-windows键-设置-更新和安全-windows安全-关闭windows defender防火墙
2、如果关闭windows defender防火墙没有效果,那么需要下载其他防火墙覆盖windows 防火墙权限

条形图:当左侧项目较多时,使用条形图
饼图:对单一项目进行详细数据分析时使用饼图

对总表中每一个数据添加单独表项:选择名字-右键-超链接-本文档中位置-选择对应的独立表
save 保存 edit 编辑 user 用户

如果在excel电子表格中出现超链接失败,给出的提示时您的组织策略阻止。。。需要修改注册表:

a、打开注册表:windows+R-regedit
b、寻找路径:HKEY_CURRENT_USER\Software\Classes\.html
c、修改值:双击.html-修改值为htmlfile
d、重启计算机

设置excel电子表格密码:
a、文件-另存为-设置保存路径和文件名称-工具-常规选项
b、添加只读密码和修改密码

设置PC的用户名密码
我的电脑-右键-管理-本地用户和组-选择当前账户名称-右键-设置密码

数据透视图:
a、选择插入数据透视图的表格页面
b、插入-数据透视图-同时选择数据透视图和数据透视表
c、勾选右边筛选项(销售员和总利润)

注:在设置数据透视图时,必须保证第一行各个表项名称完整,不可以有空白表格

每年中国会发布互联网报告:http://www.cnnic.net.cn
NIC:网卡
rule 规则
permit 允许
source 源、源头
setup/install 安装

1、两口温饱家庭收支系统

函数求和:=SUM(B4:M4)

数据透视图:用于数据统计
在表格中-插入标签-数据透视图-数据透视图和数据透视表-在新标签中打开-勾选支出类型和汇总-根据数据透视表生成-收支表

求平均数函数=average(B4:M4)

Excel表格插件
安装插件:
a、必须关闭所有office软件
b、打开execl电子表格在视图右边出现2个新的标签(选项卡,方方格子、DIY工具箱)
c、选择身份-随机身份证号-设置人数

在word中调用execl电子表格
a、负载excel电子表格-站提到word中,但是这种办法不推荐(因为对表格的调整不方便)
b、在word中-插入-对象-由文件创建-浏览-找到excel电子表格-双击(进入在word中添加了excel表格功能)-修改表格


=randbetween(1,20):随机生成一个1到20的随机数
between:范围

=VLOOKUP(B2,名单!A1:B20,2)
引用值 引用范围 输出第二列

网络工程的最基本工作:完成通信
信号是什么?规则的电流、规则的电磁波
计算机发送信息依赖于二进制(0 1)->规则的电流就是以特定频率波动电压的电流

互联网通信的几大要素:终端、传输介质、IP地址、协议
a、终端设备:电脑、手机、pad、服务器等
b、传输介质:串口线、双绞线(网线)、光纤、无线电
ADSL:下载速度远大于上传速度(10:1)
c、IP地址:IPv4、IPv6
d、协议:http 超文本传输协议
power point:PPT 幻灯片

point:点

幻灯片课件的内容必须为大纲,不可以为详细说明书

新建幻灯片:在导航栏中鼠标右键-新建幻灯片-选择建好的幻灯片-右键-版式

播放幻灯片:F5播放幻灯片,从当前页播放:shift+F5

母版:制作统一样式课件
a、打开母版:视图-幻灯片母版-可以看到所有的幻灯片版式
b、分辨率问题:主流PC分辨率(1920*1080),将幻灯片设置为4:3-母版-幻灯片大小-4:3
c、隐藏背景:选择幻灯片版式-勾选隐藏背景图形,(新建背景:选择背景-在背景对话框中-图片和纹理填充-文件-添加背景图片)
d、PPT目录:插入-smart图形-层次结构-创建图形-添加形状(添加新的文本框-升级-下移)-更改颜色和主题
e、标题降级和升级:选择标题内容-tab(降级)-shift+tab(升级)


幻灯片切换动画:切换-选择细微性动画效果
播放幻灯片画笔:右键-指针选项-选择画笔样式
独立动画效果:
a、进入:具体素材进入课件的动画效果
b、强调:对素材重点显示的动画效果
c、退出:对素材退出当前课件页面的动画效果
d、路径:设置素材自定义的移动路线
选择素材-动画-添加动画-点击动画效果从上一项开始可以让多个动画的开始时间统一

导出演示文稿:在没有office软件时,课件依然可以使用
1、文件-导出-将演示文稿打包成CD(注:如果是演示文稿的PPT无法修改)
2、将文件后缀修改为ppt即可修改,不可以修改为pptx
注:ppt文件后缀为97-03版本office幻灯片文件后缀,pptx是2007之后版本幻灯片文件后缀


如何做一个好的PPT:因为现在很多公司将做PPT放入了入职要求中
a、布局:课件的逻辑必须清晰
b、图片:所有添加的图片必须像素足够高(不可以使用手机照片)
c、字体:对非美术人员来说,所有字体全部使用微软雅黑
d、图标:如果会PS可以自己制作简单矢量图标,不会的话网站下载

ileUpload 对象

在网页上传文件,最核心元素就是这个HTML DOM的FileUpload对象了。什么鬼?好像不太熟啊~别急,看到真人就熟了:

<input type="file">

就是他啊!其实在 HTML 文档中该标签每出现一次,一个 FileUpload 对象就会被创建。该标签包含一个按钮,用来打开文件选择对话框,以及一段文字显示选中的文件名或提示没有文件被选中。

把这个标签放在<form>标签内,设置form的action为服务器目标上传地址,并点击submit按钮或通过JS调用form的submit()方法就可以实现最简单的文件上传了。

<form id="uploadForm" method="POST" action="upload" enctype="multipart/form-data">

<input type="file" id="myFile" name="file"></input>

<input type="submit" value="提交"></input>

</form>

这样就完成功能啦?没错。但是你要是敢提交这样的代码,估计脸要被打肿

都什么年代了,我们要的是页面无刷新上传!

更优雅的上传

现代网页通过什么来实现用户与服务器的无刷新交互?

——XMLHttpRequest

对,就是这个你很熟悉的家伙。如果你开发的产品支持的浏览器是现代浏览器,那么恭喜你,文件上传就是这么easy!特别强调强调现代浏览器是因为我们接下来讨论的XMLHttpRequest指的是XMLHttpRequest Level 2。

那什么是Level 1?为什么不行?因为它有如下限制:

  • 仅支持文本数据传输, 无法传输二进制数据.
  • 传输数据时, 没有进度信息提示, 只能提示是否完成.
  • 受浏览器 同源策略 限制, 只能请求同域资源.
  • 没有超时机制, 不方便掌控ajax请求节奏.

而XMLHttpRequest Level 2针对这些缺陷做出了改进:

  • 支持二进制数据, 可以上传文件, 可以使用FormData对象管理表单.
  • 提供进度提示, 可通过 xhr.upload.onprogress 事件回调方法获取传输进度.
  • 依然受 同源策略 限制, 这个安全机制不会变. XHR2新提供 Access-Control-Allow-Origin 等headers, 设置为 * 时表示允许任何域名请求, 从而实现跨域CORS访问(有关CORS详细介绍请耐心往下读).
  • 可以设置timeout 及 ontimeout, 方便设置超时时长和超时后续处理.

关于XMLHttpRequest的细节就不在这里赘述了,有兴趣可以移步这篇博客。目前, 主流浏览器基本上都支持XHR2, 除了IE系列需要IE10及更高版本. 因此IE10以下是不支持XHR2的.

上面提到的FormData就是我们最常用的一种方式。通过在脚本里新建FormData对象,把File对象设置到表单项中,然后利用XMLHttpRequest异步上传到服务器:

<form id="uploadForm" method="POST" action="upload" enctype="multipart/form-data">

<input type="file" id="myFile" name="file"></input>

<input type="submit" value="提交"></input>

</form>

完成最基本的需求无法满足我们对用户体验的追求,所以我们还想要支持上传进度显示和上传图片预览。

上传进度

因为是XMLHttpRequest Level 2, 所以很容易就可以支持对上传进度的监听。细心地小伙伴会发现在chrome的developer tools的console里new一个XHR对象,调用点运算符就可以看到智能提示出来一个onprogress事件监听器,那是不是我们只要绑定XHR对象的progress事件就可以了呢?

很接近了,但是XHR对象的直属progress事件并不是用来监听上传资源的进度的。XHR对象还有一个属性upload, 它返回一个XMLHttpRequestUpload 对象,这个对象拥有下列下列方法:

  • onloadstart
  • onprogress
  • onabort
  • onerror
  • onload
  • ontimeout
  • onloadend

这些方法在XHR对象中都存在同名版本,区别是后者是用于加载资源时,而前者用于资源上传时。其中onprogress 事件回调方法可用于跟踪资源上传的进度,它的event参数对象包含两个重要的属性loaded和total。分别代表当前已上传的字节数(number of bytes)和文件的总字节数。比如我们可以这样计算进度百分比:

xhr.upload.onprogress = function(event) {

if (event.lengthComputable) {

var percentComplete = (event.loaded / event.total) * 100;

// 对进度进行处理 }

}

其中事件的lengthComputable属性代表文件总大小是否可知。如果 lengthComputable 属性的值是 false,那么意味着总字节数是未知并且 total 的值为零。

如果是现代浏览器,可以直接配合HTML5提供的

<progress id="myProgress" value="50" max="100">

</progress>

其value属性绑定上面代码中的percentComplete的值即可。再进一步我们还可以对<progress>的样式统一调整,实现优雅降级方案,具体参见这篇文章。

再说说我在测试这个progress事件时遇到的一个问题。一开始我设在onprogress事件回调里的断点总是只能走到一次,并且loaded值始终等于total。觉得有点诡异,改用console.log打印loaded值不见效,于是直接加大上传文件的大小到50MB,终于看到了5个不同的百分比值。

因为xhr.upload.onprogress在上传阶段(即xhr.send()之后,xhr.readystate=2之前)触发,每50ms触发一次。所以文件太小网络环境好的时候是直接到100%的。

图片预览

普通青年的图片预览方式是待文件上传成功后,后台返回上传文件的url,然后把预览图片的img元素的src指向该url。这其实达不到预览的效果和目的。

属于文艺青年的现代浏览器又登场了:“使用HTML5的FileReader API吧!” 让我们直接上代码,直奔主题:

function handleImageFile(file) {

var previewArea = document.getElementById('previewArea');

var img = document.createElement('img');

var fileInput = document.getElementById("myFile");

var file = fileInput.files[0];

img.file = file;

previewArea.appendChild(img);

var reader = new FileReader();

reader.onload = (function(aImg) {

return function(e) {

aImg.src = e.target.result;

}

})(img);

reader.readAsDataURL(file);

}

这里我们使用FileReader来处理图片的异步加载。在创建新的FileReader对象之后,我们建立了onload函数,然后调用readAsDataURL()开始在后台进行读取操作。当图像文件加载后,转换成一个 data: URL,并传递到onload回调函数中设置给img的src。

另外我们还可以通过使用对象URL来实现预览:

var img = document.createElement("img");

img.src = window.URL.createObjectURL(file);;

img.onload = function() {

// 明确地通过调用释放

window.URL.revokeObjectURL(this.src);

}

previewArea.appendChild(img);

多文件支持

什么?一个一个添加文件太烦?别急,打开一个开关就好了。别忘了我们文章一开头就登场的FileUpload对象,它有一个multiple属性。只要这样

<input id="myFile" type="file" multiple>

我们就能在打开的文件选择对话框中选中多个文件了。然后你在代码里拿到的FileUpload对象的files属性就是一个选中的多文件的数组了。

var fileInput = document.getElementById("myFile");

var files = fileInput.files;

var formData = new FormData();

for(var i = 0; i < files.length; i++) {

var file = files[i];

formData.append('files[]', file, file.name);

}

FormData的append方法提供第三个可选参数用于指定文件名,这样就可以使用同一个表单项名,然后用文件名区分上传的多个文件。这样也方便前后台的循环操作。

二进制上传

有了FileReader,其实我们还有一种上传的途径,读取文件内容后直接以二进制格式上传。

var reader = new FileReader();

reader.onload = function(){

xhr.sendAsBinary(this.result);

}

// 把从input里读取的文件内容,放到fileReader的result字段里

reader.readAsBinaryString(file);

不过chrome已经把XMLHttpRequest的sendAsBinary方法移除了。所以可能得自行实现一个

XMLHttpRequest.prototype.sendAsBinary = function(text){

var data = new ArrayBuffer(text.length);

var ui8a = new Uint8Array(data, 0);

for (var i = 0; i < text.length; i++){

ui8a[i] = (text.charCodeAt(i) & 0xff);

}

this.send(ui8a);

}

这段代码将字符串转成8位无符号整型,然后存放到一个8位无符号整型数组里面,再把整个数组发送出去。

到这里,我们应该可以结合业务需求实现一个比较优雅的文件上传组件了。等等,哪里优雅了?都不支持拖拽!

拖拽的支持

利用HTML5的drag & drop事件,我们可以很快实现对拖拽的支持。首先我们可能需要确定一个允许拖放的区域,然后绑定相应的事件进行处理。看代码

var dropArea;

dropArea = document.getElementById("dropArea");

dropArea.addEventListener("dragenter", handleDragenter, false);

dropArea.addEventListener("dragover", handleDragover, false);

dropArea.addEventListener("drop", handleDrop, false);

// 阻止dragenter和dragover的默认行为,这样才能使drop事件被触发function handleDragenter(e) {

e.stopPropagation();

e.preventDefault();

}

function handleDragover(e) {

e.stopPropagation();

e.preventDefault();

}

function handleDrop(e) {

e.stopPropagation();

e.preventDefault();

var dt = e.dataTransfer;

var files = dt.files;

// handle files ...

}

这里可以把通过事件对象的dataTransfer拿到的files数组和之前相同处理,以实现预览上传等功能。有了这些事件回调,我们也可以在不同的事件给我们UI元素添加不同的class来实现更好交互效果。

好了,一个比较优雅的上传组件可以进入生产模式了。什么?还要支持IE9?好吧,让我们来看看IE10以下的浏览器如何实现无刷新上传。

借用iframe

之前说了要实现文件上传使用FileUpload对象()即可。这在低版本的IE里也是适用的。那我们为什么还要用iframe呢?

因为在现代浏览器中我们可以用XMLHttpRequest Level 2来支持二进制数据,异步文件上传,并且动态创建FormData。而低版本的IE里的XMLHttpRequest是Level 1。所以我们通过XHR异步向服务器发上传请求的路走不通了。只能老老实实的用form的submit。

而form的submit会导致页面的刷新。原因分析好了,那么答案就近在咫尺了。我们能不能让form的submit不刷新整个页面呢?答案就是利用iframe。把form的target指定到一个看不见的iframe,那么返回的数据就会被这个iframe接受,于是乎就只有这个iframe会刷新。而它又是看不见的,用户自然就感知不到了。

window.__iframeCount = 0;

var hiddenframe = document.createElement("iframe");

var frameName = "upload-iframe" + ++window.__iframeCount;

hiddenframe.name = frameName;

hiddenframe.id = frameName;

hiddenframe.setAttribute("style", "width:0;height:0;display:none");

document.body.appendChild(hiddenframe);

var form = document.getElementById("myForm");

form.target = frameName;

然后响应iframe的onload事件,获取response

hiddenframe.onload = function(){

// 获取iframe的内容,即服务返回的数据

var resData = this.contentDocument.body.textContent || this.contentWindow.document.body.textContent;

// 处理数据 。。。

//删除iframe setTimeout(function(){

var _frame = document.getElementById(frameName);

_frame.parentNode.removeChild(_frame);

}, 100);

}

iframe的实现大致如此,但是如果文件上传的地址与当前页面不在同一个域下就会出现跨域问题。导致iframe的onload回调里的访问服务返回的数据失败。

这时我们再祭出JSONP这把利剑,来解决跨域问题。首先在上传之前注册一个全局的函数,把函数名发给服务器。服务器需要配合在response里让浏览器直接调用这个函数。

// 生成全局函数名,避免冲突var CALLBACK_NAME = 'CALLBACK_NAME';

var genCallbackName = (function () {

var i = 0;

return function () {

return CALLBACK_NAME + ++i;

};

})();

var curCallbackName = genCallbackName();

window[curCallbackName] = function(res) {

// 处理response 。。。

// 删除iframe

var _frame = document.getElementById(frameName);

_frame.parentNode.removeChild(_frame);

// 删除全局函数本身

window[curCallbackName] = undefined;

}

// 如果已有其他参数,这里需要判断一下,改为拼接 &callback=

form.action = form.action + '?callback=' + curCallbackName;

好了,实现一个文件上传组件的基本知识点大致总结了一下。

页面发布

1.1技术方案

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

技术方案说明:

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

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

3、每个站点服务部署cms client程序,并与交换机绑定,绑定时指定站点Id为routingKey。指定站点id为routingKey就可以实现cms client只能接收到所属站点的页面发布消息。

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

路由模式分析如下:

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

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

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

页面发布流程图如下:

1、前端请求cms执行页面发布。

2、cms执行静态化程序生成html文件。3、cms将html文件存储到GridFS中。

4、cms向MQ发送页面发布消息

5、MQ将页面发布消息通知给Cms Client 6、Cms Client从GridFS中下载html文件

7、Cms Client将html保存到所在服务器指定目录

1.2页面发布消费方

1.2.1需求分析

功能分析:

创建Cms Client工程作为页面发布消费方,将Cms Client部署在多个服务器上,它负责接收到页面发布 的消息后从

GridFS中下载文件在本地保存。

需求如下:

1、将cms Client部署在服务器,配置队列名称和站点ID。2、cms Client连接RabbitMQ并监听各自的“页面发布队列” 3、cms Client接收页面发布队列的消息

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

调用dao查询页面信息,获取到页面的物理路径,调用dao查询站点信息,得到站点的物理路径 页面物理路径=站点物理路径+页面物理路径+页面名称。

从GridFS查询静态文件内容,将静态文件内容保存到页面物理路径下。

1.2.2创建Cms Client工程

1、创建maven工程pom.xml

<?xml version="1.0" encoding="UTF‐8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven‐4.0.0.xsd">
<parent>
<artifactId>xc‐framework‐parent</artifactId>
<groupId>com.xuecheng</groupId>
<version>1.0‐SNAPSHOT</version>
<relativePath>../xc‐framework‐parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>xc‐service‐manage‐cms‐client</artifactId>
<dependencies>
<dependency>
	<groupId>com.xuecheng</groupId>
	<artifactId>xc‐framework‐model</artifactId>
<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和logback-spring.xml。

application.yml的内容如下:

server:
	port: 31000 
spring:
	application:
		name: xc‐service‐manage‐cms‐client 
	data:
		mongodb:
			uri: mongodb://root:123@localhost:27017
		 database: xc_cms
		rabbitmq:
			host: 127.0.0.1
			port: 5672
		 username: guest
		 password: guest
		 virtualHost: /
	xuecheng:
		mq:
		#cms客户端监控的队列名称(不同的客户端监控的队列不能重复)
			 queue: 			queue_cms_postpage_01
			routingKey: 5a751fab6abb5044e0d19ea1	#此routingKey为门户站点ID

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

@SpringBootApplication @EntityScan("com.xuecheng.framework.domain.cms")//扫描实体类
@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);
}
}

1.2.3RabbitmqConfig配置类

消息队列设置如下:

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;
/**
*@author Administrator
*@version 1.0
**/ @Configuration
public class RabbitmqConfig {
//队列bean的名称
public static final String QUEUE_CMS_POSTPAGE = "queue_cms_postpage";
//交换机的名称
public static final String EX_ROUTING_CMS_POSTPAGE="ex_routing_cms_postpage";
//队列的名称@Value("${xuecheng.mq.queue}")
public String queue_cms_postpage_name;
//routingKey 即 站 点 Id @Value("${xuecheng.mq.routingKey}") public String routingKey;
/**
*交换机配置使用direct类型
*@return the exchange
*/ @Bean(EX_ROUTING_CMS_POSTPAGE)
public Exchange EXCHANGE_TOPICS_INFORM() {
	return ExchangeBuilder.directExchange(EX_ROUTING_CMS_POSTPAGE).durable(true).build();
}
//声明队列@Bean(QUEUE_CMS_POSTPAGE)
public Queue QUEUE_CMS_POSTPAGE() {
	Queue queue = new Queue(queue_cms_postpage_name); return queue;
}
/**
*绑定队列到交换机
*
*@param queue	the queue
*@param exchange the exchange
* @return the binding
*/ @Bean
public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_CMS_POSTPAGE) Queue queue, @Qualifier(EX_ROUTING_CMS_POSTPAGE) Exchange exchange) {
	return BindingBuilder.bind(queue).to(exchange).with(routingKey).noargs();
	}
}

1.2.4定义消息格式

消息内容采用json格式存储数据,如下: 页面id:发布页面的id

{
	"pageId":""
}

1.2.5PageDao

1、使用CmsPageRepository 查询页面信息

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

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

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

1.2.6PageService

在Service中定义保存页面静态文件到服务器物理路径方法:

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.domain.cms.response.CmsCode; import com.xuecheng.framework.exception.ExceptionCast; import com.xuecheng.manage_cms_client.dao.CmsPageRepository;

import com.xuecheng.manage_cms_client.dao.CmsSiteRepository;

import org.apache.commons.io.IOUtils;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.gridfs.GridFsResource; import org.springframework.data.mongodb.gridfs.GridFsTemplate; import org.springframework.stereotype.Service;

import java.io.*;

import java.util.Optional;

/**

*@author Administrator

*@version 1.0

**/ @Service

public class PageService {

@Autowired

CmsPageRepository cmsPageRepository; @Autowired

CmsSiteRepository cmsSiteRepository;

@Autowired

GridFsTemplate gridFsTemplate; @Autowired

GridFSBucket gridFSBucket;

//将页面html保存到页面物理路径

public void savePageToServerPath(String pageId){

Optional<CmsPage> optional = cmsPageRepository.findById(pageId); if(!optional.isPresent()){

ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);

}

//取出页面物理路径

CmsPage cmsPage = optional.get();

//页面所属站点

CmsSite cmsSite = this.getCmsSiteById(cmsPage.getSiteId());

//页面物理路径

String pagePath = cmsSite.getSitePhysicalPath() + cmsPage.getPagePhysicalPath() + cmsPage.getPageName();

//查询页面静态文件

String htmlFileId = cmsPage.getHtmlFileId(); InputStream inputStream = this.getFileById(htmlFileId); if(inputStream == null){

ExceptionCast.cast(CmsCode.CMS_GENERATEHTML_HTMLISNULL);

}

FileOutputStream fileOutputStream = null; try {

fileOutputStream = new FileOutputStream(new File(pagePath));

//将文件内容保存到服务物理路径IOUtils.copy(inputStream,fileOutputStream);

} catch (Exception e) {

e.printStackTrace();

}finally {

try {

inputStream.close();

} catch (IOException e) { e.printStackTrace();

}

try {

fileOutputStream.close();

} catch (IOException e) { e.printStackTrace();

}

}

}

//根据文件id获取文件内容

public InputStream getFileById(String fileId){ try {

GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));

GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());

GridFsResource gridFsResource = new GridFsResource(gridFSFile,gridFSDownloadStream); return gridFsResource.getInputStream();

} catch (IOException e) {

e.printStackTrace();

}

return null;

}

//根据站点id得到站点

public CmsSite getCmsSiteById(String siteId){

Optional<CmsSite> optional = cmsSiteRepository.findById(siteId); if(optional.isPresent()){

CmsSite cmsSite = optional.get(); return cmsSite;

}

return null;

}

}

1.2.6ConsumerPostPage

在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;
/**
*@author Administrator
*@version 1.0
**/ @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()); return ;
	}
//将页面保存到服务器物理路径pageService.savePageToServerPath(pageId);
}
}

1.3页面发布生产方

1.3.1需求分析

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

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

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

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

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

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

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

1.3.2RabbitMQ配置

1、配置Rabbitmq的连接参数

在application.yml添加如下配置:

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

2、在pom.xml添加依赖

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

3、RabbitMQConfig配置

由于cms作为页面发布方要面对很多不同站点的服务器,面对很多页面发布队列,所以这里不再配置队列,只需要 配置交换机即可。

在cms工程只配置交换机名称即可。

package com.xuecheng.manage_cms.config;
import org.springframework.amqp.core.*;
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;
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 {
//交换机的名称
public static final String EX_ROUTING_CMS_POSTPAGE="ex_routing_cms_postpage";
/**
*交换机配置使用direct类型
*@return the exchange
*/
 @Bean(EX_ROUTING_CMS_POSTPAGE)
public Exchange EXCHANGE_TOPICS_INFORM() {
	return ExchangeBuilder.directExchange(EX_ROUTING_CMS_POSTPAGE).durable(true).build();
}
}

1.3.3Api接口

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

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

1.3.4PageService

在PageService中定义页面发布方法,代码如下:

//页面发布
public ResponseResult postPage(String pageId){
//执行静态化
	String pageHtml = this.getPageHtml(pageId); if(StringUtils.isEmpty(pageHtml)){
ExceptionCast.cast(CmsCode.CMS_GENERATEHTML_HTMLISNULL);
}
//保存静态化文件
CmsPage cmsPage = saveHtml(pageId, pageHtml);
//发送消息sendPostPage(pageId);
return new ResponseResult(CommonCode.SUCCESS);
}
//发送页面发布消息
private void sendPostPage(String pageId){
	CmsPage cmsPage = this.getById(pageId); if(cmsPage == null){
ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);
}
Map<String,String> msgMap = new HashMap<>(); msgMap.put("pageId",pageId);
//消息内容
String msg = JSON.toJSONString(msgMap);
//获取站点id作为routingKey
String siteId = cmsPage.getSiteId();
//发布消息this.rabbitTemplate.convertAndSend(RabbitmqConfig.EX_ROUTING_CMS_POSTPAGE,siteId, msg);
}
//保存静态页面内容
private CmsPage saveHtml(String pageId,String content){
//查询页面
	Optional<CmsPage> optional = cmsPageRepository.findById(pageId); if(!optional.isPresent()){
ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);
}
	CmsPage cmsPage = optional.get();
//存储之前先删除
	String htmlFileId = cmsPage.getHtmlFileId(); if(StringUtils.isNotEmpty(htmlFileId)){
	gridFsTemplate.delete(Query.query(Criteria.where("_id").is(htmlFileId)));
}
//保存html文件到GridFS
InputStream inputStream = IOUtils.toInputStream(content);
ObjectId objectId = gridFsTemplate.store(inputStream, cmsPage.getPageName());
//文件id
String fileId = objectId.toString();
//将文件id存储到cmspage中cmsPage.setHtmlFileId(fileId); cmsPageRepository.save(cmsPage); return cmsPage;
}

1.3.5CmsPageController

编写Controller实现api接口,接收页面请求,调用service执行页面发布。

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

1.4页面发布前端

用户操作流程:

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

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

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

1.4.1API方法

在 cms前端添加 api方法。

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

1.4.2页面

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

1.5测试

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

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

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

2、执行页面预览

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

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

1.6思考

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

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

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

2课程管理

2.1需求分析

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

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

1、分类管理

2、新增课程

3、修改课程

4、预览课程

5、发布课程

用户的操作流程如下:

1、进入我的课程

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

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

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

5、编辑图片

上传课程图片。

5、编辑图片

上传课程图片。

7、编辑课程计划

添加课程计划:

2.2教学方法

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

1、前后端工程导入

教学管理前端工程采用与系统管理工程相同的技术,直接导入后在此基础上开发。

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

2、课程计划功能

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

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

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

4、参考文档

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

2.3环境搭建

2.3.1搭建数据库环境

1)创建数据库

课程管理使用MySQL数据库,创建课程管理数据库:xc_course。导入xc_course.sql脚本

2)数据表介绍

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

数据表结构如下:

2.3.2导入课程管理服务工程

1)持久层技术介绍:

课程管理服务使用MySQL数据库存储课程信息,持久层技术如下:

1、spring data jpa:用于表的基本CRUD。2、mybatis:用于复杂的查询操作。

3、druid:使用阿里巴巴提供的spring boot 整合druid包druid-spring-boot-starter管理连接池。

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

2)导入工程

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

2.3.3导入课程管理前端工程

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

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

3课程计划

3.1需求分析

什么是课程计划?

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

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

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

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

3.2课程计划查询

3.2.1需求分析

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

左侧显示的就是课程计划,课程计划是一个树型结构,方便扩展课程计划的级别。 在上边页面中,点击“添加课程计划”即可对课程计划进行添加操作。

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

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

3.2.2页面原型

3.2.2.1tree组件介绍

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

在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'
}]
}]
}]
}
}
}

3.2.2.2webstorm配置JSX

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

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

下面是官方的一个例子:

设置方法 如下:

设置方法 如下:

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

3.2.3API接口

3.2.3.1数据模型

1、表结构

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;
}

3.2.3.2自定义模型类

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

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

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

@Data @ToString
public class TeachplanNode extends Teachplan { List<TeachplanNode> children;
}

3.2.3.3接口定义

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

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

3.2.3课程管理服务

3.2.3.1Sql

课程计划是树型结构,采用表的自连接方式进行查询,sql语句如下:

SELECT
a.id one_id, a.pname one_pname, b.id two_id, b.pname two_pname, c.id three_id, c.pname three_pname
FROM
teachplan a
LEFT JOIN teachplan b ON a.id = b.parentid
LEFT JOIN teachplan c ON b.id = c.parentid
WHERE a.parentid = '0'
AND a.courseid = '402885816243d2dd016243f24c030002'
ORDER BY a.orderby, b.orderby, c.orderby

3.2.3.2Dao

  1. mapper接口
@Mapper
public interface TeachplanMapper {
public TeachplanNode selectList(String courseId);
}

2)mapper映射文件

<resultMap type="com.xuecheng.framework.domain.course.ext.TeachplanNode" id="teachplanMap" >
<id property="id" column="one_id"/>
<result property="pname" column="one_name"/>
<collection property="children"
ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
<id property="id" column="two_id"/>
<result property="pname" column="two_name"/>
<collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
<id property="id" column="three_id"/>
<result property="pname" column="three_name"/>
</collection>
</collection>
</resultMap>
<select id="selectList" resultMap="teachplanMap" parameterType="java.lang.String" > SELECT
a.id one_id, a.pname one_name, b.id two_id, b.pname two_name, c.id three_id, c.pname three_name
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'
<if test="_parameter!=null and _parameter!=''"> and a.courseid=#{courseId}
</if>
ORDER BY a.orderby, b.orderby, c.orderby
</select>

说明:针对输入参数为简单类型#{}中可以是任意类型,判断参数是否为空要用 _parameter(它属于mybatis的内置参数)

3.4.3.3Service

创建CourseService类,定义查询课程计划方法。

@Service
public class CourseService { @Autowired
TeachplanMapper teachplanMapper;
//查询课程计划
public TeachplanNode findTeachplanList(String courseId){
	TeachplanNode teachplanNode = teachplanMapper.selectList(courseId); 
	return teachplanNode;
}
}

3.4.3.4Controller

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

3.4.3.5测试

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

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

3.2.4前端页面

3.2.4.1Api方法

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

/*查询课程计划*/
export const findTeachplanList = courseid => {
	return http.requestQuickGet(apiUrl+'/course/teachplan/list/'+courseid)
}

3.2.4.2Api调用

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'
}

3.2.4.3测试

3.3添加课程计划

3.3.1需求分析

用户操作流程:

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

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

上级结点说明:

不选择上级结点表示当前课程计划为该课程的一级结点。

当添加该课程在课程计划中还没有节点时要自动添加课程的根结点。

3、点击提交。

3.3.1.1页面原型说明

添加课程计划采用弹出窗口组件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‐
</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 = {}
},

3.3.3API接口

1)添加课程计划

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

3.3.4课程管理服务

3.3.3.1Dao

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

3.3.3.2Service

//获取课程根结点,如果没有则添加根结点
public 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,
if(teachplanList == null || teachplanList.size()==0){
//新增一个根结点
Teachplan teachplanRoot = new Teachplan(); teachplanRoot.setCourseid(courseId); teachplanRoot.setPname(courseBase.getName()); teachplanRoot.setParentid("0");
teachplanRoot.setGrade("1");//1级
teachplanRoot.setStatus("0");//未发布
teachplanRepository.save(teachplanRoot); return teachplanRoot.getId();
}
Teachplan teachplan = teachplanList.get(0); return teachplan.getId();
}
//添加课程计划@Transactional
public ResponseResult addTeachplan(Teachplan teachplan){
//校验课程id和课程计划名称if(teachplan == null ||
StringUtils.isEmpty(teachplan.getCourseid()) || StringUtils.isEmpty(teachplan.getPname())){
ExceptionCast.cast(CommonCode.INVALIDPARAM);
}
//取出课程id
String courseid = teachplan.getCourseid();
//取出父结点id
String parentid = teachplan.getParentid(); if(StringUtils.isEmpty(parentid)){
//如果父结点为空则获取根结点
parentid= getTeachplanRoot(courseid);
}
//取出父结点信息
Optional<Teachplan> teachplanOptional = teachplanRepository.findById(parentid); if(!teachplanOptional.isPresent()){
ExceptionCast.cast(CommonCode.INVALIDPARAM);
}
//父结点
Teachplan teachplanParent = teachplanOptional.get();
//父结点级别
String parentGrade = teachplanParent.getGrade();
//设置父结点teachplan.setParentid(parentid); teachplan.setStatus("0");//未发布
//子结点的级别,根据父结点来判断
if(parentGrade.equals("1")){ teachplan.setGrade("2");
}else if(parentGrade.equals("2")){ teachplan.setGrade("3");
}
// 设 置 课 程 id teachplan.setCourseid(teachplanParent.getCourseid()); teachplanRepository.save(teachplan);
return new ResponseResult(CommonCode.SUCCESS);
}

3.3.3.3controller

//添加课程计划@Override
@PostMapping("/teachplan/add")
public ResponseResult addTeachplan(@RequestBody Teachplan teachplan) { return courseService.addTeachplan(teachplan);
}

3.3.3.4测试

复杂一些的业务逻辑建议写完服务端代码就进行单元测试。使用swagger-ui或postman测试上边的课程计划添加接口。

3.3.5前端

3.3.5.1Api调用

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('提交失败');
}
});
}
})
},

3.3.5 测试

测试流程:

1、新建一个课程

2、向新建课程中添加课程计划添加一级结点

添加二级结点