一、页面发布
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、配置文件
server:
port: 31000
spring:
application:
name: xc-service-manage-cms-client
data:
mongodb:
uri: mongodb://root:root@localhost:27017
database: xc_cms
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
xuecheng:
mq:
#cms客户端监控的队列名称(不同的客户端监控的队列不能重复)
queue: queue_cms_postpage_01
routingKey: 5a751fab6abb5044e0d19ea1 #此routingKey为门户站点ID
在配置文件中配置队列的名称,每个cms client在部署时注意队列名称不能重复
3、启动类
package com.xuecheng.managecms.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
/**
* 扫描实体类
*/
@EntityScan("com.xuecheng.framework.domain.cms")
/**
* 扫描common工程下的类
*/
@ComponentScan(basePackages = {"com.xuecheng.framework"})
/**
* 扫描本项目下的所有包
*/
@ComponentScan(basePackages = {"com.xuecheng.managecms.client"})
/**
* @Author: 98050
* @Time: 2019-04-01 16:40
* @Feature:
*/
public class ManageCmsClientApplication {
public static void main(String[] args) {
SpringApplication.run(ManageCmsClientApplication.class,args);
}
}
1.2.3 RabbitmqConfig配置类
消息队列设置如下:
1、创建"ex_cms_postpage"交换机
2、每个Cms Client创建一个队列与交换机绑定
3、每个Cms Client程序配置队列名称和routingKey
package com.xuecheng.managecms.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: 98050
* @Time: 2019-04-01 16:47
* @Feature: Rabbitmq配置
*/
@Configuration
public class RabbitmqConfig {
/**
* 队列bean名称
*/
public static final String QUEUE_CMS_POSTPAGE = "queue_cms_postpage";
/**
* 交换机bean的名称
*/
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() {
/**
* durable(true)持久化,消息队列重启后交换机仍然存在
*/
return ExchangeBuilder.directExchange(EX_ROUTING_CMS_POSTPAGE).durable(true).build();
}
/**
* 声明队列
* @return
*/
@Bean(QUEUE_CMS_POSTPAGE)
public Queue QUEUE_CMS_POSTPAGE(){
return new Queue(queue_cms_postpage_name);
}
/**
* 绑定队列到交换机
* @param queue
* @param exchange
* @return
*/
@Bean
public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_CMS_POSTPAGE) Queue queue,@Qualifier(EX_ROUTING_CMS_POSTPAGE) Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(routingKey).noargs();
}
}
1.2.4 定义消息格式
消息内容采用json格式存储数据,如下:
页面id:发布页面的id
{
"pageId":""
}
1.2.5 PageDao
package com.xuecheng.managecms.client.dao;
import com.xuecheng.framework.domain.cms.CmsPage;
import org.springframework.data.mongodb.repository.MongoRepository;
/**
* @Author: 98050
* @Time: 2019-03-19 22:10
* @Feature: mongodb dao
*/
public interface CmsPageRepository extends MongoRepository<CmsPage,String> {
}
package com.xuecheng.managecms.client.dao;
import com.xuecheng.framework.domain.cms.CmsSite;
import org.springframework.data.mongodb.repository.MongoRepository;
/**
* @Author: 98050
* @Time: 2019-03-30 22:57
* @Feature:
*/
public interface CmsSiteRepository extends MongoRepository<CmsSite,String> {
}
1.2.6 PageService
在Service中定义保存页面静态文件到服务器物理路径的方法。
接口:
package com.xuecheng.managecms.client.service;
/**
* @Author: 98050
* @Time: 2019-04-01 18:39
* @Feature:
*/
public interface PageService {
/**
* 将页面保存到服务器指定路径下
* @param pageId
*/
void savePageToServerPath(String pageId);
}
实现:
package com.xuecheng.managecms.client.service.impl;
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.managecms.client.dao.CmsPageRepository;
import com.xuecheng.managecms.client.dao.CmsSiteRepository;
import com.xuecheng.managecms.client.mq.ConsumerPostPage;
import com.xuecheng.managecms.client.service.PageService;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
/**
* @Author: 98050
* @Time: 2019-04-01 18:40
* @Feature:
*/
@Service
public class PageServiceImpl implements PageService {
private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerPostPage.class);
@Autowired
private CmsPageRepository cmsPageRepository;
@Autowired
private CmsSiteRepository cmsSiteRepository;
@Autowired
private GridFsTemplate gridFsTemplate;
@Autowired
private GridFSBucket gridFSBucket;
/**
* 将静态html页面保存到服务器物理路径下
* @param pageId 页面id
*/
@Override
public void savePageToServerPath(String pageId) {
Optional<CmsPage> optional1 = this.cmsPageRepository.findById(pageId);
if (!optional1.isPresent()){
LOGGER.info("get cmsPage by id is null,page id is :{}",pageId);
return;
}
//1.取出页面的物理路径
CmsPage cmsPage = optional1.get();
String pagePhysicalPath = cmsPage.getPagePhysicalPath();
Optional<CmsSite> optional2 = this.cmsSiteRepository.findById(cmsPage.getSiteId());
if (!optional2.isPresent()){
LOGGER.info("get cmsSite by id is null,site id is :{}",cmsPage.getSiteId());
return;
}
//2.取出站点的物理路径
CmsSite cmsSite = optional2.get();
String sitePhysicalPath = cmsSite.getSitePhysicalPath();
//3.拼接页面的物理
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(sitePhysicalPath).append(pagePhysicalPath).append(cmsPage.getPageName());
//4.根据文件id下载html文件
String fileId = cmsPage.getHtmlFileId();
InputStream inputStream = findHtmlFileById(fileId);
if (inputStream == null){
LOGGER.info("getFileById InputStream is null,htmlFileId is :{}",fileId);
return;
}
//5.复制文件
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(new File(stringBuffer.toString()));
IOUtils.copy(inputStream, fileOutputStream);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (fileOutputStream != null) {
fileOutputStream.close();
}
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private InputStream findHtmlFileById(String fileId) {
try {
//4.1根据id查询文件
GridFSFile gridFSFile = this.gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
//4.2打开下载流对象
GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
//4.3创建gridResource用于获取流对象
GridFsResource gridFsResource = new GridFsResource(gridFSFile,gridFSDownloadStream);
return gridFsResource.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
1.2.7 ConsumerPostPage
在cms client工程的mq包下创建ConsumerPostPage类,ConsumerPostPage作为发布页面的消费客户端,监听页面发布队列的消息,收到消息后从mongodb下载文件,保存在本地。
package com.xuecheng.managecms.client.mq;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.cms.CmsPage;
import com.xuecheng.managecms.client.dao.CmsPageRepository;
import com.xuecheng.managecms.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: 98050
* @Time: 2019-04-01 20:38
* @Feature: cms消费客户端
*/
@Component
public class ConsumerPostPage {
private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerPostPage.class);
@Autowired
private CmsPageRepository cmsPageRepository;
@Autowired
private PageService pageService;
@RabbitListener(queues = {"${xuecheng.mq.queue}"})
public void postPage(String msg){
//1.解析消息
Map map = JSON.parseObject(msg,Map.class);
LOGGER.info("receive cms post page:{}", msg);
//2.取出页面id
String pageId = (String) map.get("pageId");
//3.查询页面信息
Optional<CmsPage> optional = cmsPageRepository.findById(pageId);
if (!optional.isPresent()){
LOGGER.info("receive cms post page,cmsPage is null:{}",msg);
return;
}
//4.将页面保存到服务器物理路径下
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.2 RabbitMQ配置
1、在xc-service-manage-cms中配置Rabbitmq的连接参数
在application.yml中添加如下配置:

2、在pom.xml添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3、RabbitMQConfig配置
由于cms作为页面发布方要面对很多不同站点的服务器,面对很多页面发布队列,所以这里不再配置队列,只需要配置交换机即可。
在cms工程只配置交换机名称即可。
package com.xuecheng.managecms.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author: 98050
* @Time: 2019-04-01 16:47
* @Feature: Rabbitmq配置
*/
@Configuration
public class RabbitmqConfig {
/**
* 交换机bean的名称
*/
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() {
/**
* durable(true)持久化,消息队列重启后交换机仍然存在
*/
return ExchangeBuilder.directExchange(EX_ROUTING_CMS_POSTPAGE).durable(true).build();
}
}
1.3.3 API接口
/**
* 页面发布
* @param pageId
* @return
*/
@ApiOperation("发布页面")
@ApiImplicitParams({
@ApiImplicitParam(name = "id",value = "页面Id",required = true,paramType = "path",dataType = "String")
})
@ApiResponses({
@ApiResponse(code = 10000,message = "操作成功"),
@ApiResponse(code = 11111,message = "操作失败")
})
@PostMapping("/postPage/{pageId}")
ResponseResult post(@PathVariable("pageId") String pageId);
1.3.4 CmsService
接口:
新增方法:

实现:
/**
* 页面发布
* @param pageId
* @return
*/
@Override
public ResponseResult postPage(String pageId) {
//1.执行静态化
String pageHtml = getPageHtml(pageId);
if (StringUtils.isEmpty(pageHtml)){
ExceptionCast.cast(CmsCode.CMS_GENERATEHTML_HTMLISNULL);
}
//2.保存静态化文件
CmsPage cmsPage = saveHtml(pageId,pageHtml);
//3.发送消息到消息队列
sendPostPage(cmsPage);
return new ResponseResult(CommonCode.SUCCESS);
}
private void sendPostPage(CmsPage cmsPage) {
Map<String,String> map = new HashMap<>();
map.put("pageId", cmsPage.getPageId());
//1.消息内容
String msg = JSON.toJSONString(map);
//2.获取站点id作为routingKey
String routingKey = cmsPage.getSiteId();
this.rabbitTemplate.convertAndSend(RabbitmqConfig.EX_ROUTING_CMS_POSTPAGE,routingKey,msg);
}
/**
* 页面静态化后的文件上传到GridFS中
* @param pageId
* @param pageHtml
* @return
*/
private CmsPage saveHtml(String pageId, String pageHtml){
//1.校验页面
Optional<CmsPage> optional = this.cmsPageRepository.findById(pageId);
if (!optional.isPresent()){
ExceptionCast.cast(CmsCode.CMS_PAGE_NOTEXISTS);
}
CmsPage cmsPage = optional.get();
//2.存储之前先删除
String htmlFileId = cmsPage.getHtmlFileId();
if (StringUtils.isNotEmpty(htmlFileId)){
this.gridFsTemplate.delete(Query.query(Criteria.where("_id").is(htmlFileId)));
}
//3.保存html文件到GridFS中
InputStream inputStream = null;
try {
inputStream = IOUtils.toInputStream(pageHtml, "utf-8");
ObjectId objectId = this.gridFsTemplate.store(inputStream, cmsPage.getPageName());
//4.将文件id存储在cmsPage中返回
cmsPage.setHtmlFileId(objectId.toString());
this.cmsPageRepository.save(cmsPage);
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return cmsPage;
}
1.3.5 CmsPageController
编写Controller实现api接口,接收页面请求,调用service执行页面发布。
@Override
@PostMapping("/postPage/{pageId}")
public ResponseResult post(@PathVariable("pageId") String pageId) {
return this.cmsService.postPage(pageId);
}
1.4 页面发布前端
用户操作流程:
1、用户进入cms页面列表。
2、点击“发布”请求服务端接口,发布页面。
3、提示“发布成功”,或发布失败。
1.4.1 API方法
// 页面发布
export const pagePost = id => {
return http.requestPost(apiUrl + '/cms/page/postPage/' + id)
}
1.4.2 页面
发布按钮:
<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 (pageId) {
this.$confirm('确认发布该页面吗?', '提示', {}).then(() => {
cmsApi.pagePost(pageId).then((res) => {
if (res.success) {
this.$message.success('页面发布成功,请稍后查看效果')
} else {
this.$message.error('页面发布失败!')
}
})
})
},
1.5 测试
对门户中的轮播图进行替换
1.5.1 原来的效果

1.5.2 更新轮播图
修改页面列表中的轮播图:

修改后:


1.5.3 页面预览

1.5.4 站点物理路径修改
修改mongodb数据库中cms_cite中门户主站的物理路径:

1.5.5 页面发布
发布前先保存一下原来的index_banner.html

点击发布:

效果:

1.5.6 查看页面

1.6 思考
1、如果发布到服务器的页面内容不正确怎么办?
添加撤销功能,然后修改模板重新发布。
如何返回原来的页面呢?即在页面静态化存储的时候不应该直接删除上一次静态化的文件,将其保留,以便撤销发布后,返回原来的页面。具体如何实现?在CmsPage对象中增加一个字段(preHtmlFileId),用来保存上一次的发布结果(htmlFileId)。所以页面发布的时候,将当前CmsPage对象中的htmlFileId字段的值赋值给preHtmlFileId,然后htmlFileId再保存新的文件id,进行撤销的时候相当于拿着preHtmlFileId再进行一次发布。
实现
1.6.1 修改页面发布生产方代码
在给mq发送消息的时候加上页面发布类型post(发布)或者redo(撤销)
修改sendPostPage方法
private void sendPostPage(CmsPage cmsPage,String type) {
Map<String,String> map = new HashMap<>();
map.put("pageId", cmsPage.getPageId());
map.put("type", type);
//1.消息内容
String msg = JSON.toJSONString(map);
//2.获取站点id作为routingKey
String routingKey = cmsPage.getSiteId();
this.rabbitTemplate.convertAndSend(RabbitmqConfig.EX_ROUTING_CMS_POSTPAGE,routingKey,msg);
}
页面静态化后的文件在上传GridFS的时候,判断preHtmlFileId字段是否为空,不空的话就根据preHtmlFileId字段中存储的html文件id删除对应的文件,最后将当前htmlFileId中保存的值赋值给
preHtmlFileId以便撤销的时候可以恢复到上一次的页面效果。

1.6.2 修改页面发布消费方
public void postPage(String msg){
//1.解析消息
Map map = JSON.parseObject(msg,Map.class);
LOGGER.info("receive cms post page:{}", msg);
//2.取出页面id
String pageId = (String) map.get("pageId");
//3.取出发布类型
String type = (String) map.get("type");
//4.查询页面信息
Optional<CmsPage> optional = cmsPageRepository.findById(pageId);
if (!optional.isPresent()){
LOGGER.info("receive cms post page,cmsPage is null:{}",msg);
return;
}
//5.根据发布类型,将页面保存到服务器物理路径下
this.pageService.savePageToServerPath(pageId,type);
}
携带发布类型参数
修改savePageToServerPath方法:
根据发布类型下载相应的html文件

1.6.3 添加页面发布撤销接口
1.6.3.1 API
/**
* 页面发布撤销
* @param pageId
* @return
*/
@ApiOperation("发布页面")
@ApiImplicitParams({
@ApiImplicitParam(name = "id",value = "页面Id",required = true,paramType = "path",dataType = "String")
})
@ApiResponses({
@ApiResponse(code = 10000,message = "操作成功"),
@ApiResponse(code = 11111,message = "操作失败")
})
@PostMapping("/postPage/{pageId}")
ResponseResult postRollBack(@PathVariable("pageId") String pageId);
1.6.3.2 Service
接口:
/**
* 发布页面撤销
* @param pageId
* @return
*/
ResponseResult postPageRollBack(String pageId);
实现:
/**
* 页面发布撤销
* @param pageId
* @return
*/
@Override
public ResponseResult postPageRollBack(String pageId) {
//1.校验页面
CmsPage cmsPage = this.findById(pageId);
//2.发送消息到消息队列
sendPostPage(cmsPage,"redo");
return new ResponseResult(CommonCode.SUCCESS);
}
1.6.3.3 Controller
@Override
@PostMapping("/redoPage/{pageId}")
public ResponseResult postRollBack(@PathVariable("pageId") String pageId) {
return this.cmsService.postPageRollBack(pageId);
}
1.6.3.4 前端API
// 页面发布撤销
export const pagePostRollBack = id => {
return http.requestPost(apiUrl + '/cms/page/redoPage/' + id)
}
1.6.3.4 前端页面修改
在pageList.vue中添加:
<el-table-column label="撤销" width="80">
<template slot-scope="scope">
<el-button
size="small" type="primary" plain @click="redoPage(scope.row.pageId)">撤销
</el-button>
</template>
</el-table-column>
redoPage方法:
redoPage (pageId) {
this.$confirm('确认撤销发布的页面吗?', '提示', {}).then(() => {
cmsApi.pagePostRollBack(pageId).then((res) => {
if (res.success) {
this.$message.success('页面撤销成功,请稍后查看效果')
} else {
this.$message.error('页面撤销失败!')
}
})
})
},
2、一个页面需要发布很多服务器,点击“发布”后如何知道详细的发布结果?
3、一个页面发布到多个服务器,其中有一个服务器发布失败时怎么办否变化。


2721

被折叠的 条评论
为什么被折叠?



