CMS (Content Management System)即内容管理系统,不同的项目对CMS的定位不同。
每个公司对每个项目的CMS定位不同,CMS基本上分为:针对后台数据内容的管理、针对前端页面的管理、针对样式风格的管理等 。
比如:一个给企业做网站的公司,其CMS系统主要是网站页面管理及样式风格的管理。
流程
(1)创建站点
一个网站有很多子站点,比如:学成在线有主门户、学习中心、问答系统等子站点。具体的哪个页面是归属于具体的站点,所以要管理页面,先要管理页面所属的站点。
(2)创建模板
如电商网站的商品详情页面,每个页面的内容布局、板式是相同的,不同的只是内容,这个页面的布局、板式就是页面模板,模板+数据就组成一个完整的页面,最终要创建一个页面文件需要先定义此页面的模板,最终拿到页面的数据再结合模板就拼装成一个完整的页面。
(3)创建页面
指填写页面的基本信息,如:页面的名称、页面的url地址等。
(4)页面预览
页面发布前的一项工作,页面预览使用静态化技术根据页面模板和数据生成页面内容,并通过浏览器预览页面。页面发布前进行页面预览的目是为了保证页面发布后的正确性。
(5)页面发布
将页面发送到页面所在站点的服务器,页面发布成功就可以通过浏览器来访问了。
功能
1)页面管理
管理员在后台添加、修改、删除页面信息
2)页面预览
管理员通过页面预览功能预览页面发布后的效果。
3)页面发布
管理员通过页面发布功能将页面发布到远程门户服务器。
页面发布成功,用户即可在浏览器浏览到最新发布的页面,整个页面添加、发布的过程由于软件自动执行,无需人工登录服务器操作。
技术
数据库:网站管理对事物要求不是很严格,故使用nosql数据库mongodb,提升操作效率。
消息队列:RabbitMQ
框架:SpringBoot
工程结构
基础结构
parent工程:父工程,提供依赖管理,如下所有工程必须依赖该工程
model工程:模型工程,提供统一的模型类管理
utils工程:工具类工程,提供本项目所使用的工具类
api工程:接口工程,统一管理本项目的服务接口swagger,依赖model
face工程:服务接口工程,统一管理本项目的服务接口(可选,拓展dubbo技术),依赖model
common工程:通用工程,提供各层封装,依赖utils
cms工程:业务功能工程,提供网站、页面和模板crud功能实现,依赖api、common
cms_client工程:业务功能工程,提供物理服务器页面文件更新功能,依赖api、common
api工程
swagger2接口编写示例,该接口需要被对应业务工程controller实现
@Api(value="cms页面管理接口",description = "cms页面管理接口,提供页面的增、删、改、查")
public interface CmsPageControllerApi {
//页面查询
@ApiOperation("分页查询页面列表")
@ApiImplicitParams({
@ApiImplicitParam(name="page",value = "页码",required=true,paramType="path",dataType="int"),
@ApiImplicitParam(name="size",value = "每页记录数",required=true,paramType="path",dataType="int")
})
public QueryResponseResult findList(int page, int size, QueryPageRequest queryPageRequest);
//新增页面
@ApiOperation("新增页面")
public CmsPageResult add(CmsPage cmsPage);
//根据页面id查询页面信息
@ApiOperation("根据页面id查询页面信息")
public CmsPage findById(String id);
//修改页面
@ApiOperation("修改页面")
public CmsPageResult edit(String id,CmsPage cmsPage);
//删除页面
@ApiOperation("删除页面")
public ResponseResult delete(String id);
//页面发布
@ApiOperation("页面发布")
public ResponseResult post(String pageId);
@ApiOperation("保存页面")
public CmsPageResult save(CmsPage cmsPage);
@ApiOperation("一键发布页面")
public CmsPostPageResult postPageQuick(CmsPage cmsPage);
}
业务工程目录基本结构
一、代码文件
(1)controller
(2)service
(3)dao
(4)config
二、配置文件
(1)application.yml
(2)logback-spring.xml
示例:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property resource="application.properties" />
<!--定义日志文件的存储地址,使用绝对路径-->
<property name="LOG_HOME" value="${log.logDirectory}"/>
<!-- Console 输出设置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<fileNamePattern>${LOG_HOME}/vander.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 异步输出 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="FILE"/>
</appender>
<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<logger name="org.springframework.boot" level="DEBUG"/>
<root level="info">
<!--<appender-ref ref="ASYNC"/>-->
<appender-ref ref="FILE"/>
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
业务模型
页面信息
1、定义一个页面需要指定页面所属站点
一个站点包括多个页面,比如:学成在线的门户站点(网站)包括了多个页面。
2、定义一个页面需要指定页面使用的模板
多个页面可以使用相同的模板,比如:商品信息模板,每个商品就是一个页面,所有商品使用同一个商品信息模板
唯一索引:页面名称pageName、站点siteId、页面pageWebPath
拼接页面Url:cmsSite.siteDomain+cmsSite.siteWebPath+ cmsPage.pageWebPath + cmsPage.pageName
@Data
@ToString
@Document(collection = "cms_page")
public class CmsPage {
//站点ID
private String siteId;
//页面ID
@Id
private String pageId;
//页面名称,preview_4028e58161bd3b380161bd3bcd2f0000.html
private String pageName;
//别名,课程预览页面
private String pageAliase;
//访问地址,/coursepre/
private String pageWebPath;
//参数
private String pageParameter;
//物理路径,F:\\\\develop\\\\xc_portal_static\\\\course\\\\preview\\\\
private String pagePhysicalPath;
//类型(静态/动态),1
private String pageType;
//页面模版
private String pageTemplate;
//页面静态化内容
private String pageHtml;
//状态
private String pageStatus;
//创建时间
private Date pageCreateTime;
//模版id
private String templateId;
//参数列表
private List<CmsPageParam> pageParams;
//模版文件Id
// private String templateFileId;
//静态文件Id
private String htmlFileId;
//数据Url,http://localhost:40200/portalview/course/getpre/4028e58161bd3b380161bd3bcd2f0000
private String dataUrl;
}
@Data
@ToString
public class CmsPageParam {
//参数名称
private String pageParamName;
//参数值
private String pageParamValue;
}
站点信息
@Data
@ToString
@Document(collection = "cms_site")
public class CmsSite {
//站点ID
@Id
private String siteId;
//站点名称,门户主站
private String siteName;
//站点域名,http://localhost
private String siteDomain;
//站点端口,80
private String sitePort;
//站点访问地址,/
private String siteWebPath;
//创建时间
private Date siteCreateTime;
//站点物理路径
private String sitePhysicalPath;
}
@Data
@ToString
@Document(collection = "cms_site_server")
public class CmsSiteServer {
//站点id
private String siteId;
//服务器ID
@Id
private String serverId;
//服务器IP,127.0.0.1
private String ip;
//端口,80
private String port;
//访问地址,/
private String webPath;
//服务器名称(代理、静态、动态、CDN),门户服务器
private String serverName;
//资源发布地址(完整的HTTP接口),/upload
private String uploadPath;
//使用类型(测试、生产)
private String useType;
}
模板信息
@Data
@ToString
@Document(collection = "cms_template")
public class CmsTemplate {
//站点ID
private String siteId;
//模版ID
@Id
private String templateId;
//模版名称,轮播图
private String templateName;
//模版参数
private String templateParameter;
//模版文件Id,存在到GridFs唯一ID
private String templateFileId;
}
功能实现
需明确先后顺序,如下:
(1)模板crud实现(上传模板到GridFs获取templateFileId)
(2)站点crud实现
(3)页面crud实现
页面发布
流程
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)通过页面pageId获取:数据模型Map
//获取数据模型
private Map getModelByPageId(String pageId){
//取出页面的信息
CmsPage cmsPage = this.getById(pageId);
if(cmsPage == null){
//页面不存在
ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);
}
//取出页面的dataUrl
String dataUrl = cmsPage.getDataUrl();
if(StringUtils.isEmpty(dataUrl)){
//页面dataUrl为空
ExceptionCast.cast(CmsCode.CMS_GENERATEHTML_DATAURLISNULL);
}
//通过restTemplate请求dataUrl获取数据
ResponseEntity<Map> forEntity = restTemplate.getForEntity(dataUrl, Map.class);
Map body = forEntity.getBody();
return body;
}
(2)通过页面pageId获取:模板文件字符串
//获取页面的模板信息
private String getTemplateByPageId(String pageId){
//取出页面的信息
CmsPage cmsPage = this.getById(pageId);
if(cmsPage == null){
//页面不存在
ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);
}
//获取页面的模板id
String templateId = cmsPage.getTemplateId();
if(StringUtils.isEmpty(templateId)){
ExceptionCast.cast(CmsCode.CMS_GENERATEHTML_TEMPLATEISNULL);
}
//查询模板信息
Optional<CmsTemplate> optional = cmsTemplateRepository.findById(templateId);
if(optional.isPresent()){
CmsTemplate cmsTemplate = optional.get();
//获取模板文件id
String templateFileId = cmsTemplate.getTemplateFileId();
//从GridFS中取模板文件内容
//根据文件id查询文件
GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(templateFileId)));
//打开一个下载流对象
GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
//创建GridFsResource对象,获取流
GridFsResource gridFsResource = new GridFsResource(gridFSFile,gridFSDownloadStream);
//从流中取数据
try {
String content = IOUtils.toString(gridFsResource.getInputStream(), "utf-8");
return content;
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
(3)执行静态化获取:最终页面文件字符串
//执行静态化
private String generateHtml(String templateContent,Map model ){
//创建配置对象
Configuration configuration = new Configuration(Configuration.getVersion());
//创建模板加载器
StringTemplateLoader stringTemplateLoader = new StringTemplateLoader();
stringTemplateLoader.putTemplate("template",templateContent);
//向configuration配置模板加载器
configuration.setTemplateLoader(stringTemplateLoader);
//获取模板
try {
Template template = configuration.getTemplate("template");
//调用api进行静态化
String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
return content;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
(4)保存最终页面文件到GridFs,并更新数据库页面信息
//保存html到GridFS
private CmsPage saveHtml(String pageId,String htmlContent){
//先得到页面信息
CmsPage cmsPage = this.getById(pageId);
if(cmsPage == null){
ExceptionCast.cast(CommonCode.INVALID_PARAM);
}
ObjectId objectId = null;
try {
//将htmlContent内容转成输入流
InputStream inputStream = IOUtils.toInputStream(htmlContent, "utf-8");
//将html文件内容保存到GridFS
objectId = gridFsTemplate.store(inputStream, cmsPage.getPageName());
} catch (IOException e) {
e.printStackTrace();
}
//将html文件id更新到cmsPage中
cmsPage.setHtmlFileId(objectId.toHexString());
cmsPageRepository.save(cmsPage);
return cmsPage;
}
(5)发送消息到客户端
//向mq 发送消息
private void sendPostPage(String pageId){
//得到页面信息
CmsPage cmsPage = this.getById(pageId);
if(cmsPage == null){
ExceptionCast.cast(CommonCode.INVALID_PARAM);
}
//创建消息对象
Map<String,String> msg = new HashMap<>();
msg.put("pageId",pageId);
//转成json串
String jsonString = JSON.toJSONString(msg);
//发送给mq
//站点id
String siteId = cmsPage.getSiteId();
rabbitTemplate.convertAndSend(RabbitmqConfig.EX_ROUTING_CMS_POSTPAGE,siteId,jsonString);
}
(6)客户端监听到消息
@RabbitListener(queues = {"${xuecheng.mq.queue}"})
public void postPage(String msg){
//解析消息
Map map = JSON.parseObject(msg, Map.class);
//得到消息中的页面id
String pageId = (String) map.get("pageId");
//校验页面是否合法
CmsPage cmsPage = pageService.findCmsPageById(pageId);
if(cmsPage == null){
LOGGER.error("receive postpage msg,cmsPage is null,pageId:{}",pageId);
return ;
}
//调用service方法将页面从GridFs中下载到服务器
pageService.savePageToServerPath(pageId);
}
(7)客户端将最新页面文件下载到物理服务器
//保存html页面到服务器物理路径
public void savePageToServerPath(String pageId){
//根据pageId查询cmsPage
CmsPage cmsPage = this.findCmsPageById(pageId);
//得到html的文件id,从cmsPage中获取htmlFileId内容
String htmlFileId = cmsPage.getHtmlFileId();
//从gridFS中查询html文件
InputStream inputStream = this.getFileById(htmlFileId);
if(inputStream == null){
LOGGER.error("getFileById InputStream is null ,htmlFileId:{}",htmlFileId);
return ;
}
//得到站点id
String siteId = cmsPage.getSiteId();
//得到站点的信息
CmsSite cmsSite = this.findCmsSiteById(siteId);
//得到站点的物理路径
String sitePhysicalPath = cmsSite.getSitePhysicalPath();
//得到页面的物理路径
String pagePath = sitePhysicalPath + cmsPage.getPagePhysicalPath() + cmsPage.getPageName();
//将html文件保存到服务器物理路径上
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();
}
}
}
页面预览
通过页面发布(1)(2)(3)步骤可以获取到最终页面文本
//页面预览
@RequestMapping(value="/cms/preview/{pageId}",method = RequestMethod.GET)
public void preview(@PathVariable("pageId") String pageId) throws IOException {
//执行静态化
String pageHtml = pageService.getPageHtml(pageId);
//通过response对象将内容输出
ServletOutputStream outputStream = response.getOutputStream();
response.setHeader("Content-type","text/html;charset=utf-8");
outputStream.write(pageHtml.getBytes("utf-8"));
}