mirror of
https://gitee.com/durcframework/SOP.git
synced 2025-08-11 21:57:56 +08:00
支持预发布、灰度发布
This commit is contained in:
31
doc/docs/files/10110_预发布灰度发布.md
Normal file
31
doc/docs/files/10110_预发布灰度发布.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 预发布灰度发布
|
||||||
|
|
||||||
|
从1.14.0开始支持预发布、灰度发布,可登陆`SOP-Admin`,然后选择`服务列表`进行操作。
|
||||||
|
|
||||||
|
## 使用预发布
|
||||||
|
|
||||||
|
假设网关工程在阿里云负载均衡有两台服务器,域名分别为:
|
||||||
|
|
||||||
|
|域名|说明|
|
||||||
|
|:---- |:---- |
|
||||||
|
|open1.domain.com |网关服务器1 |
|
||||||
|
|openpre.domain.com | 网关服务器2,作为预发布请求入口|
|
||||||
|
|
||||||
|
线上域名为`open.domain.com`,请求网关`http://open.domain.com/api`会负载均衡到这两台服务器
|
||||||
|
|
||||||
|
在网关工程打开`com.gitee.sop.gateway.loadbalancer.EnvironmentServerChooser`类,修改`PRE_DOMAIN`变量
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 预发布机器域名
|
||||||
|
*/
|
||||||
|
private static final String PRE_DOMAIN = "openpre.domain.com";
|
||||||
|
```
|
||||||
|
|
||||||
|
网关工程打包发布到阿里云
|
||||||
|
|
||||||
|
登录SOP-Admin,在服务列表中点击预发布,然后接口的请求地址变成:`http://openpre.domain.com/api`
|
||||||
|
|
||||||
|
## 使用灰度发布
|
||||||
|
|
||||||
|
灰度发布可允许指定的用户进行访问,其它用户则走正常流程。
|
12
sop-1.14.0.sql
Normal file
12
sop-1.14.0.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use sop;
|
||||||
|
|
||||||
|
CREATE TABLE `config_gray_userkey` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`instance_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'serviceId',
|
||||||
|
`user_key_content` text COMMENT '用户key,多个用引文逗号隔开',
|
||||||
|
`name_version_content` text COMMENT '需要灰度的接口,goods.get=1.2,order.list=1.2',
|
||||||
|
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_instanceid` (`instance_id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='灰度发布用户key';
|
@@ -24,6 +24,8 @@
|
|||||||
<zookeeper.version>3.4.12</zookeeper.version>
|
<zookeeper.version>3.4.12</zookeeper.version>
|
||||||
<curator-recipes.version>4.0.1</curator-recipes.version>
|
<curator-recipes.version>4.0.1</curator-recipes.version>
|
||||||
<okhttp.version>3.11.0</okhttp.version>
|
<okhttp.version>3.11.0</okhttp.version>
|
||||||
|
<easyopen.version>1.16.2</easyopen.version>
|
||||||
|
<fastmybatis.version>1.8.0</fastmybatis.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -37,13 +39,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.oschina.durcframework</groupId>
|
<groupId>net.oschina.durcframework</groupId>
|
||||||
<artifactId>easyopen-spring-boot-starter</artifactId>
|
<artifactId>easyopen-spring-boot-starter</artifactId>
|
||||||
<version>1.16.2</version>
|
<version>${easyopen.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.oschina.durcframework</groupId>
|
<groupId>net.oschina.durcframework</groupId>
|
||||||
<artifactId>fastmybatis-spring-boot-starter</artifactId>
|
<artifactId>fastmybatis-spring-boot-starter</artifactId>
|
||||||
<version>1.7.2</version>
|
<version>${fastmybatis.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>mysql</groupId>
|
<groupId>mysql</groupId>
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
package com.gitee.sop.adminserver;
|
package com.gitee.sop.adminserver;
|
||||||
|
|
||||||
|
import com.gitee.fastmybatis.core.FastmybatisConfig;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class SopAdminServerApplication {
|
public class SopAdminServerApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
FastmybatisConfig.defaultIgnoreUpdateColumns = Arrays.asList("gmt_create", "gmt_modified");
|
||||||
SpringApplication.run(SopAdminServerApplication.class, args);
|
SpringApplication.run(SopAdminServerApplication.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,7 @@ import com.gitee.sop.adminserver.api.isv.result.RoleVO;
|
|||||||
import com.gitee.sop.adminserver.bean.ChannelMsg;
|
import com.gitee.sop.adminserver.bean.ChannelMsg;
|
||||||
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
||||||
import com.gitee.sop.adminserver.common.BizException;
|
import com.gitee.sop.adminserver.common.BizException;
|
||||||
|
import com.gitee.sop.adminserver.common.ChannelOperation;
|
||||||
import com.gitee.sop.adminserver.common.IdGen;
|
import com.gitee.sop.adminserver.common.IdGen;
|
||||||
import com.gitee.sop.adminserver.common.RSATool;
|
import com.gitee.sop.adminserver.common.RSATool;
|
||||||
import com.gitee.sop.adminserver.common.ZookeeperPathNotExistException;
|
import com.gitee.sop.adminserver.common.ZookeeperPathNotExistException;
|
||||||
@@ -229,7 +230,7 @@ public class IsvApi {
|
|||||||
if (isvDetail == null) {
|
if (isvDetail == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ChannelMsg channelMsg = new ChannelMsg("update", isvDetail);
|
ChannelMsg channelMsg = new ChannelMsg(ChannelOperation.ISV_INFO_UPDATE, isvDetail);
|
||||||
String path = ZookeeperContext.getIsvInfoChannelPath();
|
String path = ZookeeperContext.getIsvInfoChannelPath();
|
||||||
String data = JSON.toJSONString(channelMsg);
|
String data = JSON.toJSONString(channelMsg);
|
||||||
try {
|
try {
|
||||||
|
@@ -16,6 +16,7 @@ import com.gitee.sop.adminserver.api.service.result.ConfigIpBlacklistVO;
|
|||||||
import com.gitee.sop.adminserver.bean.ChannelMsg;
|
import com.gitee.sop.adminserver.bean.ChannelMsg;
|
||||||
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
||||||
import com.gitee.sop.adminserver.common.BizException;
|
import com.gitee.sop.adminserver.common.BizException;
|
||||||
|
import com.gitee.sop.adminserver.common.ChannelOperation;
|
||||||
import com.gitee.sop.adminserver.entity.ConfigIpBlacklist;
|
import com.gitee.sop.adminserver.entity.ConfigIpBlacklist;
|
||||||
import com.gitee.sop.adminserver.mapper.ConfigIpBlacklistMapper;
|
import com.gitee.sop.adminserver.mapper.ConfigIpBlacklistMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -51,7 +52,7 @@ public class IPBlacklistApi {
|
|||||||
CopyUtil.copyPropertiesIgnoreNull(form, rec);
|
CopyUtil.copyPropertiesIgnoreNull(form, rec);
|
||||||
configIpBlacklistMapper.saveIgnoreNull(rec);
|
configIpBlacklistMapper.saveIgnoreNull(rec);
|
||||||
try {
|
try {
|
||||||
this.sendIpBlacklistMsg(rec, BlacklistMsgType.ADD);
|
this.sendIpBlacklistMsg(rec, ChannelOperation.BLACKLIST_ADD);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("推送IP黑名单失败, rec:{}",rec, e);
|
log.error("推送IP黑名单失败, rec:{}",rec, e);
|
||||||
throw new BizException("推送IP黑名单失败");
|
throw new BizException("推送IP黑名单失败");
|
||||||
@@ -75,19 +76,18 @@ public class IPBlacklistApi {
|
|||||||
}
|
}
|
||||||
configIpBlacklistMapper.deleteById(id);
|
configIpBlacklistMapper.deleteById(id);
|
||||||
try {
|
try {
|
||||||
this.sendIpBlacklistMsg(rec, BlacklistMsgType.DELETE);
|
this.sendIpBlacklistMsg(rec, ChannelOperation.BLACKLIST_DELETE);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("推送IP黑名单失败, rec:{}",rec, e);
|
log.error("推送IP黑名单失败, rec:{}",rec, e);
|
||||||
throw new BizException("推送IP黑名单失败");
|
throw new BizException("推送IP黑名单失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendIpBlacklistMsg(ConfigIpBlacklist configIpBlacklist, BlacklistMsgType blacklistMsgType) throws Exception {
|
public void sendIpBlacklistMsg(ConfigIpBlacklist configIpBlacklist, ChannelOperation channelOperation) throws Exception {
|
||||||
String configData = JSON.toJSONString(configIpBlacklist);
|
ChannelMsg channelMsg = new ChannelMsg(channelOperation, configIpBlacklist);
|
||||||
ChannelMsg channelMsg = new ChannelMsg(blacklistMsgType.name().toLowerCase(), configData);
|
|
||||||
String jsonData = JSON.toJSONString(channelMsg);
|
String jsonData = JSON.toJSONString(channelMsg);
|
||||||
String path = ZookeeperContext.getIpBlacklistChannelPath();
|
String path = ZookeeperContext.getIpBlacklistChannelPath();
|
||||||
log.info("消息推送--IP黑名单设置({}), path:{}, data:{}",blacklistMsgType.name(), path, jsonData);
|
log.info("消息推送--IP黑名单设置({}), path:{}, data:{}",channelOperation.getOperation(), path, jsonData);
|
||||||
ZookeeperContext.createOrUpdateData(path, jsonData);
|
ZookeeperContext.createOrUpdateData(path, jsonData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,16 +6,23 @@ import com.gitee.easyopen.annotation.ApiService;
|
|||||||
import com.gitee.easyopen.doc.annotation.ApiDoc;
|
import com.gitee.easyopen.doc.annotation.ApiDoc;
|
||||||
import com.gitee.easyopen.doc.annotation.ApiDocMethod;
|
import com.gitee.easyopen.doc.annotation.ApiDocMethod;
|
||||||
import com.gitee.sop.adminserver.api.service.param.ServiceAddParam;
|
import com.gitee.sop.adminserver.api.service.param.ServiceAddParam;
|
||||||
|
import com.gitee.sop.adminserver.api.service.param.ServiceInstanceGrayParam;
|
||||||
|
import com.gitee.sop.adminserver.api.service.param.ServiceInstanceParam;
|
||||||
import com.gitee.sop.adminserver.api.service.param.ServiceSearchParam;
|
import com.gitee.sop.adminserver.api.service.param.ServiceSearchParam;
|
||||||
import com.gitee.sop.adminserver.api.service.result.RouteServiceInfo;
|
import com.gitee.sop.adminserver.api.service.result.RouteServiceInfo;
|
||||||
import com.gitee.sop.adminserver.api.service.result.ServiceInfoVo;
|
import com.gitee.sop.adminserver.api.service.result.ServiceInfoVo;
|
||||||
import com.gitee.sop.adminserver.api.service.result.ServiceInstanceVO;
|
import com.gitee.sop.adminserver.api.service.result.ServiceInstanceVO;
|
||||||
|
import com.gitee.sop.adminserver.bean.ChannelMsg;
|
||||||
import com.gitee.sop.adminserver.bean.MetadataEnum;
|
import com.gitee.sop.adminserver.bean.MetadataEnum;
|
||||||
import com.gitee.sop.adminserver.bean.ServiceRouteInfo;
|
import com.gitee.sop.adminserver.bean.ServiceRouteInfo;
|
||||||
|
import com.gitee.sop.adminserver.bean.UserKeyDefinition;
|
||||||
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
||||||
import com.gitee.sop.adminserver.common.BizException;
|
import com.gitee.sop.adminserver.common.BizException;
|
||||||
|
import com.gitee.sop.adminserver.common.ChannelOperation;
|
||||||
import com.gitee.sop.adminserver.common.ZookeeperPathExistException;
|
import com.gitee.sop.adminserver.common.ZookeeperPathExistException;
|
||||||
import com.gitee.sop.adminserver.common.ZookeeperPathNotExistException;
|
import com.gitee.sop.adminserver.common.ZookeeperPathNotExistException;
|
||||||
|
import com.gitee.sop.adminserver.entity.ConfigGrayUserkey;
|
||||||
|
import com.gitee.sop.adminserver.mapper.ConfigGrayUserkeyMapper;
|
||||||
import com.gitee.sop.registryapi.bean.ServiceInfo;
|
import com.gitee.sop.registryapi.bean.ServiceInfo;
|
||||||
import com.gitee.sop.registryapi.bean.ServiceInstance;
|
import com.gitee.sop.registryapi.bean.ServiceInstance;
|
||||||
import com.gitee.sop.registryapi.service.RegistryService;
|
import com.gitee.sop.registryapi.service.RegistryService;
|
||||||
@@ -45,6 +52,9 @@ public class ServiceApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RegistryService registryService;
|
private RegistryService registryService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConfigGrayUserkeyMapper configGrayUserkeyMapper;
|
||||||
|
|
||||||
@Api(name = "zookeeper.service.list")
|
@Api(name = "zookeeper.service.list")
|
||||||
@ApiDocMethod(description = "zk中的服务列表", elementClass = RouteServiceInfo.class)
|
@ApiDocMethod(description = "zk中的服务列表", elementClass = RouteServiceInfo.class)
|
||||||
List<RouteServiceInfo> listServiceInfo(ServiceSearchParam param) {
|
List<RouteServiceInfo> listServiceInfo(ServiceSearchParam param) {
|
||||||
@@ -157,9 +167,9 @@ public class ServiceApi {
|
|||||||
|
|
||||||
@Api(name = "service.instance.offline")
|
@Api(name = "service.instance.offline")
|
||||||
@ApiDocMethod(description = "服务禁用")
|
@ApiDocMethod(description = "服务禁用")
|
||||||
void serviceOffline(ServiceInstance param) {
|
void serviceOffline(ServiceInstanceParam param) {
|
||||||
try {
|
try {
|
||||||
registryService.offlineInstance(param);
|
registryService.offlineInstance(param.buildServiceInstance());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("服务禁用失败,param:{}", param, e);
|
log.error("服务禁用失败,param:{}", param, e);
|
||||||
throw new BizException("服务禁用失败,请查看日志");
|
throw new BizException("服务禁用失败,请查看日志");
|
||||||
@@ -168,9 +178,9 @@ public class ServiceApi {
|
|||||||
|
|
||||||
@Api(name = "service.instance.online")
|
@Api(name = "service.instance.online")
|
||||||
@ApiDocMethod(description = "服务启用")
|
@ApiDocMethod(description = "服务启用")
|
||||||
void serviceOnline(ServiceInstance param) throws IOException {
|
void serviceOnline(ServiceInstanceParam param) throws IOException {
|
||||||
try {
|
try {
|
||||||
registryService.onlineInstance(param);
|
registryService.onlineInstance(param.buildServiceInstance());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("服务启用失败,param:{}", param, e);
|
log.error("服务启用失败,param:{}", param, e);
|
||||||
throw new BizException("服务启用失败,请查看日志");
|
throw new BizException("服务启用失败,请查看日志");
|
||||||
@@ -179,10 +189,10 @@ public class ServiceApi {
|
|||||||
|
|
||||||
@Api(name = "service.instance.env.pre")
|
@Api(name = "service.instance.env.pre")
|
||||||
@ApiDocMethod(description = "预发布")
|
@ApiDocMethod(description = "预发布")
|
||||||
void serviceEnvPre(ServiceInstance param) throws IOException {
|
void serviceEnvPre(ServiceInstanceParam param) throws IOException {
|
||||||
try {
|
try {
|
||||||
MetadataEnum envPre = MetadataEnum.ENV_PRE;
|
MetadataEnum envPre = MetadataEnum.ENV_PRE;
|
||||||
registryService.setMetadata(param, envPre.getKey(), envPre.getValue());
|
registryService.setMetadata(param.buildServiceInstance(), envPre.getKey(), envPre.getValue());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("预发布失败,param:{}", param, e);
|
log.error("预发布失败,param:{}", param, e);
|
||||||
throw new BizException("预发布失败,请查看日志");
|
throw new BizException("预发布失败,请查看日志");
|
||||||
@@ -191,10 +201,31 @@ public class ServiceApi {
|
|||||||
|
|
||||||
@Api(name = "service.instance.env.gray")
|
@Api(name = "service.instance.env.gray")
|
||||||
@ApiDocMethod(description = "灰度发布")
|
@ApiDocMethod(description = "灰度发布")
|
||||||
void serviceEnvGray(ServiceInstance param) throws IOException {
|
void serviceEnvGray(ServiceInstanceGrayParam param) throws IOException {
|
||||||
try {
|
try {
|
||||||
|
Boolean onlyUpdateGrayUserkey = param.getOnlyUpdateGrayUserkey();
|
||||||
|
if (onlyUpdateGrayUserkey == null || !onlyUpdateGrayUserkey) {
|
||||||
MetadataEnum envPre = MetadataEnum.ENV_GRAY;
|
MetadataEnum envPre = MetadataEnum.ENV_GRAY;
|
||||||
registryService.setMetadata(param, envPre.getKey(), envPre.getValue());
|
registryService.setMetadata(param.buildServiceInstance(), envPre.getKey(), envPre.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
String instanceId = param.getInstanceId();
|
||||||
|
String userKeyContent = param.getUserKeyContent();
|
||||||
|
String nameVersionContent = param.getNameVersionContent();
|
||||||
|
|
||||||
|
ConfigGrayUserkey configGrayUserkey = configGrayUserkeyMapper.getByColumn("instance_id", instanceId);
|
||||||
|
if (configGrayUserkey == null) {
|
||||||
|
configGrayUserkey = new ConfigGrayUserkey();
|
||||||
|
configGrayUserkey.setInstanceId(instanceId);
|
||||||
|
configGrayUserkey.setUserKeyContent(userKeyContent);
|
||||||
|
configGrayUserkey.setNameVersionContent(nameVersionContent);
|
||||||
|
configGrayUserkeyMapper.save(configGrayUserkey);
|
||||||
|
} else {
|
||||||
|
configGrayUserkey.setUserKeyContent(userKeyContent);
|
||||||
|
configGrayUserkey.setNameVersionContent(nameVersionContent);
|
||||||
|
configGrayUserkeyMapper.update(configGrayUserkey);
|
||||||
|
}
|
||||||
|
this.sendUserKeyMsg(instanceId, ChannelOperation.GRAY_USER_KEY_SET);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("灰度发布失败,param:{}", param, e);
|
log.error("灰度发布失败,param:{}", param, e);
|
||||||
throw new BizException("灰度发布失败,请查看日志");
|
throw new BizException("灰度发布失败,请查看日志");
|
||||||
@@ -207,10 +238,26 @@ public class ServiceApi {
|
|||||||
try {
|
try {
|
||||||
MetadataEnum envPre = MetadataEnum.ENV_ONLINE;
|
MetadataEnum envPre = MetadataEnum.ENV_ONLINE;
|
||||||
registryService.setMetadata(param, envPre.getKey(), envPre.getValue());
|
registryService.setMetadata(param, envPre.getKey(), envPre.getValue());
|
||||||
|
this.sendUserKeyMsg(param.getServiceId(), ChannelOperation.GRAY_USER_KEY_CLEAR);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("上线失败,param:{}", param, e);
|
log.error("上线失败,param:{}", param, e);
|
||||||
throw new BizException("上线失败,请查看日志");
|
throw new BizException("上线失败,请查看日志");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Api(name = "service.instance.gray.userkey.get")
|
||||||
|
ConfigGrayUserkey getGrayUserkey(ServiceSearchParam param) {
|
||||||
|
return configGrayUserkeyMapper.getByColumn("instance_id", param.getInstanceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendUserKeyMsg(String serviceId, ChannelOperation channelOperation) {
|
||||||
|
UserKeyDefinition userKeyDefinition = new UserKeyDefinition();
|
||||||
|
userKeyDefinition.setServiceId(serviceId);
|
||||||
|
ChannelMsg channelMsg = new ChannelMsg(channelOperation, userKeyDefinition);
|
||||||
|
String jsonData = JSON.toJSONString(channelMsg);
|
||||||
|
String path = ZookeeperContext.getUserKeyChannelPath();
|
||||||
|
log.info("消息推送--灰度发布({}), path:{}, data:{}",channelOperation.getOperation(), path, jsonData);
|
||||||
|
ZookeeperContext.createOrUpdateData(path, jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,26 @@
|
|||||||
|
package com.gitee.sop.adminserver.api.service.param;
|
||||||
|
|
||||||
|
import com.gitee.easyopen.doc.annotation.ApiDocField;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class ServiceInstanceGrayParam extends ServiceInstanceParam {
|
||||||
|
|
||||||
|
@ApiDocField(description = "灰度发布用户,多个用英文逗号隔开")
|
||||||
|
@NotBlank(message = "灰度发布用户不能为空")
|
||||||
|
private String userKeyContent;
|
||||||
|
|
||||||
|
@ApiDocField(description = "灰度发布接口名版本号如:order.get1.0=1.2,多个用英文逗号隔开")
|
||||||
|
@NotBlank(message = "灰度发布接口名版本号不能为空")
|
||||||
|
private String nameVersionContent;
|
||||||
|
|
||||||
|
@ApiDocField(description = "是否仅更新灰度用户")
|
||||||
|
private Boolean onlyUpdateGrayUserkey;
|
||||||
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
package com.gitee.sop.adminserver.api.service.param;
|
package com.gitee.sop.adminserver.api.service.param;
|
||||||
|
|
||||||
import com.gitee.easyopen.doc.annotation.ApiDocField;
|
import com.gitee.easyopen.doc.annotation.ApiDocField;
|
||||||
|
import com.gitee.easyopen.util.CopyUtil;
|
||||||
|
import com.gitee.sop.registryapi.bean.ServiceInstance;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
@@ -17,4 +19,29 @@ public class ServiceInstanceParam {
|
|||||||
@ApiDocField(description = "instanceId")
|
@ApiDocField(description = "instanceId")
|
||||||
@NotBlank(message = "instanceId不能为空")
|
@NotBlank(message = "instanceId不能为空")
|
||||||
private String instanceId;
|
private String instanceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ip
|
||||||
|
*/
|
||||||
|
@ApiDocField(description = "ip")
|
||||||
|
private String ip;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* port
|
||||||
|
*/
|
||||||
|
@ApiDocField(description = "port")
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务状态,UP:已上线,OUT_OF_SERVICE:已下线
|
||||||
|
*/
|
||||||
|
@ApiDocField(description = "status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
|
||||||
|
public ServiceInstance buildServiceInstance() {
|
||||||
|
ServiceInstance serviceInstance = new ServiceInstance();
|
||||||
|
CopyUtil.copyPropertiesIgnoreNull(this, serviceInstance);
|
||||||
|
return serviceInstance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,6 @@ import com.gitee.easyopen.doc.annotation.ApiDocField;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author tanghc
|
* @author tanghc
|
||||||
*/
|
*/
|
||||||
@@ -15,4 +13,7 @@ public class ServiceSearchParam {
|
|||||||
|
|
||||||
@ApiDocField(description = "服务名serviceId")
|
@ApiDocField(description = "服务名serviceId")
|
||||||
private String serviceId;
|
private String serviceId;
|
||||||
|
|
||||||
|
@ApiDocField(description = "instanceId")
|
||||||
|
private String instanceId;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.gitee.sop.adminserver.bean;
|
package com.gitee.sop.adminserver.bean;
|
||||||
|
|
||||||
|
import com.gitee.sop.adminserver.common.ChannelOperation;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,8 +9,8 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class ChannelMsg {
|
public class ChannelMsg {
|
||||||
|
|
||||||
public ChannelMsg(String operation, Object data) {
|
public ChannelMsg(ChannelOperation channelOperation, Object data) {
|
||||||
this.operation = operation;
|
this.operation = channelOperation.getOperation();
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
package com.gitee.sop.adminserver.bean;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserKeyDefinition {
|
||||||
|
private String serviceId;
|
||||||
|
private String data;
|
||||||
|
}
|
@@ -89,6 +89,10 @@ public class ZookeeperContext {
|
|||||||
return serviceIdPath + "/" + routeId;
|
return serviceIdPath + "/" + routeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getUserKeyChannelPath() {
|
||||||
|
return SOP_MSG_CHANNEL_PATH + "/userkey";
|
||||||
|
}
|
||||||
|
|
||||||
public static String getIsvInfoChannelPath() {
|
public static String getIsvInfoChannelPath() {
|
||||||
return SOP_MSG_CHANNEL_PATH + "/isvinfo";
|
return SOP_MSG_CHANNEL_PATH + "/isvinfo";
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,60 @@
|
|||||||
|
package com.gitee.sop.adminserver.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
public enum ChannelOperation {
|
||||||
|
/**
|
||||||
|
* 限流推送路由配置-修改
|
||||||
|
*/
|
||||||
|
LIMIT_CONFIG_UPDATE("update"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由信息更新
|
||||||
|
*/
|
||||||
|
ROUTE_CONFIG_UPDATE("update"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isv信息修改
|
||||||
|
*/
|
||||||
|
ISV_INFO_UPDATE("update"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黑名单消息类型:添加
|
||||||
|
*/
|
||||||
|
BLACKLIST_ADD("add"),
|
||||||
|
/**
|
||||||
|
* 黑名单消息类型:删除
|
||||||
|
*/
|
||||||
|
BLACKLIST_DELETE("delete"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由权限配置更新
|
||||||
|
*/
|
||||||
|
ROUTE_PERMISSION_UPDATE("update"),
|
||||||
|
/**
|
||||||
|
* 路由权限加载
|
||||||
|
*/
|
||||||
|
ROUTE_PERMISSION_RELOAD("reload"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 灰度发布用户key设置
|
||||||
|
*/
|
||||||
|
GRAY_USER_KEY_SET("set"),
|
||||||
|
/**
|
||||||
|
* 灰度发布用户key清除
|
||||||
|
*/
|
||||||
|
GRAY_USER_KEY_CLEAR("clear"),
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
private String operation;
|
||||||
|
|
||||||
|
ChannelOperation(String operation) {
|
||||||
|
this.operation = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOperation() {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
package com.gitee.sop.adminserver.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表名:config_gray_userkey
|
||||||
|
* 备注:灰度发布用户key
|
||||||
|
*
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Table(name = "config_gray_userkey")
|
||||||
|
@Data
|
||||||
|
public class ConfigGrayUserkey {
|
||||||
|
@Id
|
||||||
|
@Column(name = "id")
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
/** 数据库字段:id */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** instanceId, 数据库字段:instance_id */
|
||||||
|
private String instanceId;
|
||||||
|
|
||||||
|
/** 用户key,多个用引文逗号隔开, 数据库字段:user_key_content */
|
||||||
|
private String userKeyContent;
|
||||||
|
|
||||||
|
/** 需要灰度的接口,goods.get=1.2,order.list=1.2, 数据库字段:name_version_content */
|
||||||
|
private String nameVersionContent;
|
||||||
|
|
||||||
|
/** 数据库字段:gmt_create */
|
||||||
|
private Date gmtCreate;
|
||||||
|
|
||||||
|
/** 数据库字段:gmt_modified */
|
||||||
|
private Date gmtModified;
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
package com.gitee.sop.adminserver.mapper;
|
||||||
|
|
||||||
|
import com.gitee.fastmybatis.core.mapper.CrudMapper;
|
||||||
|
|
||||||
|
import com.gitee.sop.adminserver.entity.ConfigGrayUserkey;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
public interface ConfigGrayUserkeyMapper extends CrudMapper<ConfigGrayUserkey, Long> {
|
||||||
|
}
|
@@ -5,6 +5,7 @@ import com.gitee.sop.adminserver.bean.ChannelMsg;
|
|||||||
import com.gitee.sop.adminserver.bean.ConfigLimitDto;
|
import com.gitee.sop.adminserver.bean.ConfigLimitDto;
|
||||||
import com.gitee.sop.adminserver.bean.RouteConfigDto;
|
import com.gitee.sop.adminserver.bean.RouteConfigDto;
|
||||||
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
||||||
|
import com.gitee.sop.adminserver.common.ChannelOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -21,8 +22,7 @@ public class RouteConfigService {
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public void sendRouteConfigMsg(RouteConfigDto routeConfigDto) {
|
public void sendRouteConfigMsg(RouteConfigDto routeConfigDto) {
|
||||||
String configData = JSON.toJSONString(routeConfigDto);
|
ChannelMsg channelMsg = new ChannelMsg(ChannelOperation.ROUTE_CONFIG_UPDATE, routeConfigDto);
|
||||||
ChannelMsg channelMsg = new ChannelMsg("update", configData);
|
|
||||||
String jsonData = JSON.toJSONString(channelMsg);
|
String jsonData = JSON.toJSONString(channelMsg);
|
||||||
String path = ZookeeperContext.getRouteConfigChannelPath();
|
String path = ZookeeperContext.getRouteConfigChannelPath();
|
||||||
log.info("消息推送--路由配置(update), path:{}, data:{}", path, jsonData);
|
log.info("消息推送--路由配置(update), path:{}, data:{}", path, jsonData);
|
||||||
@@ -35,8 +35,7 @@ public class RouteConfigService {
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public void sendLimitConfigMsg(ConfigLimitDto routeConfigDto) throws Exception {
|
public void sendLimitConfigMsg(ConfigLimitDto routeConfigDto) throws Exception {
|
||||||
String configData = JSON.toJSONString(routeConfigDto);
|
ChannelMsg channelMsg = new ChannelMsg(ChannelOperation.LIMIT_CONFIG_UPDATE, routeConfigDto);
|
||||||
ChannelMsg channelMsg = new ChannelMsg("update", configData);
|
|
||||||
String jsonData = JSON.toJSONString(channelMsg);
|
String jsonData = JSON.toJSONString(channelMsg);
|
||||||
String path = ZookeeperContext.getLimitConfigChannelPath();
|
String path = ZookeeperContext.getLimitConfigChannelPath();
|
||||||
log.info("消息推送--限流配置(update), path:{}, data:{}", path, jsonData);
|
log.info("消息推送--限流配置(update), path:{}, data:{}", path, jsonData);
|
||||||
|
@@ -7,6 +7,7 @@ import com.gitee.sop.adminserver.bean.ChannelMsg;
|
|||||||
import com.gitee.sop.adminserver.bean.IsvRoutePermission;
|
import com.gitee.sop.adminserver.bean.IsvRoutePermission;
|
||||||
import com.gitee.sop.adminserver.bean.SopAdminConstants;
|
import com.gitee.sop.adminserver.bean.SopAdminConstants;
|
||||||
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
||||||
|
import com.gitee.sop.adminserver.common.ChannelOperation;
|
||||||
import com.gitee.sop.adminserver.entity.PermIsvRole;
|
import com.gitee.sop.adminserver.entity.PermIsvRole;
|
||||||
import com.gitee.sop.adminserver.entity.PermRolePermission;
|
import com.gitee.sop.adminserver.entity.PermRolePermission;
|
||||||
import com.gitee.sop.adminserver.mapper.IsvInfoMapper;
|
import com.gitee.sop.adminserver.mapper.IsvInfoMapper;
|
||||||
@@ -68,7 +69,7 @@ public class RoutePermissionService {
|
|||||||
isvRoutePermission.setAppKey(appKey);
|
isvRoutePermission.setAppKey(appKey);
|
||||||
isvRoutePermission.setRouteIdList(routeIdList);
|
isvRoutePermission.setRouteIdList(routeIdList);
|
||||||
isvRoutePermission.setRouteIdListMd5(roleCodeListMd5);
|
isvRoutePermission.setRouteIdListMd5(roleCodeListMd5);
|
||||||
ChannelMsg channelMsg = new ChannelMsg("update", isvRoutePermission);
|
ChannelMsg channelMsg = new ChannelMsg(ChannelOperation.ROUTE_PERMISSION_UPDATE, isvRoutePermission);
|
||||||
String jsonData = JSON.toJSONString(channelMsg);
|
String jsonData = JSON.toJSONString(channelMsg);
|
||||||
String path = ZookeeperContext.getIsvRoutePermissionChannelPath();
|
String path = ZookeeperContext.getIsvRoutePermissionChannelPath();
|
||||||
log.info("消息推送--路由权限(update), path:{}, data:{}", path, jsonData);
|
log.info("消息推送--路由权限(update), path:{}, data:{}", path, jsonData);
|
||||||
@@ -105,7 +106,7 @@ public class RoutePermissionService {
|
|||||||
});
|
});
|
||||||
IsvRoutePermission isvRoutePermission = new IsvRoutePermission();
|
IsvRoutePermission isvRoutePermission = new IsvRoutePermission();
|
||||||
isvRoutePermission.setListenPath(listenPath);
|
isvRoutePermission.setListenPath(listenPath);
|
||||||
ChannelMsg channelMsg = new ChannelMsg("reload", isvRoutePermission);
|
ChannelMsg channelMsg = new ChannelMsg(ChannelOperation.ROUTE_PERMISSION_RELOAD, isvRoutePermission);
|
||||||
String jsonData = JSON.toJSONString(channelMsg);
|
String jsonData = JSON.toJSONString(channelMsg);
|
||||||
String path = ZookeeperContext.getIsvRoutePermissionChannelPath();
|
String path = ZookeeperContext.getIsvRoutePermissionChannelPath();
|
||||||
log.info("消息推送--路由权限(reload), path:{}, data:{}", path, jsonData);
|
log.info("消息推送--路由权限(reload), path:{}, data:{}", path, jsonData);
|
||||||
|
@@ -44,9 +44,5 @@ registry.name=eureka
|
|||||||
|
|
||||||
logging.level.com.gitee=debug
|
logging.level.com.gitee=debug
|
||||||
|
|
||||||
# 不用改
|
|
||||||
mybatis.fill.com.gitee.fastmybatis.core.support.DateFillInsert=gmt_create
|
|
||||||
mybatis.fill.com.gitee.fastmybatis.core.support.DateFillUpdate=gmt_modified
|
|
||||||
|
|
||||||
# 不用改,如果要改,请全局替换修改
|
# 不用改,如果要改,请全局替换修改
|
||||||
zuul.secret=MZZOUSTua6LzApIWXCwEgbBmxSzpzC
|
zuul.secret=MZZOUSTua6LzApIWXCwEgbBmxSzpzC
|
||||||
|
@@ -88,11 +88,11 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
const regexIP = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
|
||||||
const ipValidator = (rule, value, callback) => {
|
const ipValidator = (rule, value, callback) => {
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
callback(new Error('请输入IP'))
|
callback(new Error('请输入IP'))
|
||||||
} else {
|
} else {
|
||||||
const regexIP = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
|
|
||||||
if (!regexIP.test(value)) {
|
if (!regexIP.test(value)) {
|
||||||
callback(new Error('IP格式不正确'))
|
callback(new Error('IP格式不正确'))
|
||||||
}
|
}
|
||||||
|
@@ -73,21 +73,151 @@
|
|||||||
<el-button v-if="scope.row.parentId > 0 && scope.row.metadata.env" type="text" size="mini" @click="onEnvOnline(scope.row)">上线</el-button>
|
<el-button v-if="scope.row.parentId > 0 && scope.row.metadata.env" type="text" size="mini" @click="onEnvOnline(scope.row)">上线</el-button>
|
||||||
<el-button v-if="scope.row.parentId > 0 && !scope.row.metadata.env" type="text" size="mini" @click="onEnvPre(scope.row)">预发布</el-button>
|
<el-button v-if="scope.row.parentId > 0 && !scope.row.metadata.env" type="text" size="mini" @click="onEnvPre(scope.row)">预发布</el-button>
|
||||||
<el-button v-if="scope.row.parentId > 0 && !scope.row.metadata.env" type="text" size="mini" @click="onEnvGray(scope.row)">灰度发布</el-button>
|
<el-button v-if="scope.row.parentId > 0 && !scope.row.metadata.env" type="text" size="mini" @click="onEnvGray(scope.row)">灰度发布</el-button>
|
||||||
|
<el-button v-if="scope.row.parentId > 0 && scope.row.metadata.env === 'gray'" type="text" size="mini" @click="onUpdateUserkey(scope.row)">灰度设置</el-button>
|
||||||
<el-button v-if="scope.row.parentId > 0 && scope.row.status === 'UP'" type="text" size="mini" @click="onDisable(scope.row)">禁用</el-button>
|
<el-button v-if="scope.row.parentId > 0 && scope.row.status === 'UP'" type="text" size="mini" @click="onDisable(scope.row)">禁用</el-button>
|
||||||
<el-button v-if="scope.row.parentId > 0 && scope.row.status === 'OUT_OF_SERVICE'" type="text" size="mini" @click="onEnable(scope.row)">启用</el-button>
|
<el-button v-if="scope.row.parentId > 0 && scope.row.status === 'OUT_OF_SERVICE'" type="text" size="mini" @click="onEnable(scope.row)">启用</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<!-- dialog -->
|
||||||
|
<el-dialog
|
||||||
|
title="灰度设置"
|
||||||
|
:visible.sync="grayDialogVisible"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="resetForm('grayForm')"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="grayForm"
|
||||||
|
:model="grayForm"
|
||||||
|
:rules="grayFormRules"
|
||||||
|
size="mini"
|
||||||
|
>
|
||||||
|
<el-form-item label="服务器实例">
|
||||||
|
{{ grayForm.serviceId + ' (' + grayForm.ipPort + ')' }}
|
||||||
|
</el-form-item>
|
||||||
|
<el-tabs v-model="tabsActiveName" type="card">
|
||||||
|
<el-tab-pane label="灰度用户" name="first">
|
||||||
|
<el-alert
|
||||||
|
title="可以是appId,或userId,多个用英文逗号隔开"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px;"
|
||||||
|
/>
|
||||||
|
<el-form-item prop="userKeyContent">
|
||||||
|
<el-input
|
||||||
|
v-model="grayForm.userKeyContent"
|
||||||
|
placeholder="可以是appId,或userId,多个用英文逗号隔开"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="接口配置" name="second">
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="text" @click="addNameVersion">新增灰度接口</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<table cellpadding="0" cellspacing="0">
|
||||||
|
<tr
|
||||||
|
v-for="(grayRouteConfig, index) in grayForm.grayRouteConfigList"
|
||||||
|
:key="grayRouteConfig.key"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<el-form-item
|
||||||
|
:key="grayRouteConfig.key"
|
||||||
|
:prop="'grayRouteConfigList.' + index + '.oldRouteId'"
|
||||||
|
:rules="{required: true, message: '不能为空', trigger: ['blur', 'change']}"
|
||||||
|
>
|
||||||
|
老接口:
|
||||||
|
<el-select
|
||||||
|
v-model="grayRouteConfig.oldRouteId"
|
||||||
|
style="margin-right: 10px;"
|
||||||
|
@change="onChangeOldRoute(grayRouteConfig)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="route in routeList"
|
||||||
|
:key="route.id"
|
||||||
|
:label="route.name + '(' + route.version + ')'"
|
||||||
|
:value="route.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<el-form-item
|
||||||
|
:key="grayRouteConfig.key + 1"
|
||||||
|
:prop="'grayRouteConfigList.' + index + '.newVersion'"
|
||||||
|
:rules="{required: true, message: '不能为空', trigger: ['blur', 'change']}"
|
||||||
|
>
|
||||||
|
灰度接口:
|
||||||
|
<el-select
|
||||||
|
v-model="grayRouteConfig.newVersion"
|
||||||
|
no-data-text="无数据"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="routeNew in getGraySelectData(grayRouteConfig.oldRouteId)"
|
||||||
|
:key="routeNew.id"
|
||||||
|
:label="routeNew.name + '(' + routeNew.version + ')'"
|
||||||
|
:value="routeNew.version"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</td>
|
||||||
|
<td style="vertical-align: baseline;">
|
||||||
|
<el-button v-if="index > 0" type="text" @click.prevent="removeNameVersion(grayRouteConfig)">删除</el-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-form>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="grayDialogVisible = false">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="onAddUserKey">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
const regex = /^\w+(,\w+)*$/
|
||||||
|
const userKeyContentValidator = (rule, value, callback) => {
|
||||||
|
if (value === '') {
|
||||||
|
callback(new Error('不能为空'))
|
||||||
|
} else {
|
||||||
|
if (!regex.test(value)) {
|
||||||
|
callback(new Error('格式不正确'))
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
searchFormData: {
|
searchFormData: {
|
||||||
serviceId: ''
|
serviceId: ''
|
||||||
},
|
},
|
||||||
|
grayDialogVisible: false,
|
||||||
|
grayForm: {
|
||||||
|
serviceId: '',
|
||||||
|
instanceId: '',
|
||||||
|
ipPort: '',
|
||||||
|
userKeyContent: '',
|
||||||
|
onlyUpdateGrayUserkey: false,
|
||||||
|
grayRouteConfigList: [{
|
||||||
|
oldRouteId: '',
|
||||||
|
newVersion: '',
|
||||||
|
key: Date.now()
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tabsActiveName: 'first',
|
||||||
|
routeList: [],
|
||||||
|
selectNameVersion: [],
|
||||||
|
grayFormRules: {
|
||||||
|
userKeyContent: [
|
||||||
|
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||||
|
{ validator: userKeyContentValidator, trigger: 'blur' }
|
||||||
|
]
|
||||||
|
},
|
||||||
tableData: []
|
tableData: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -100,6 +230,18 @@ export default {
|
|||||||
this.tableData = this.buildTreeData(resp.data)
|
this.tableData = this.buildTreeData(resp.data)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
loadRouteList: function(serviceId) {
|
||||||
|
if (this.routeList.length === 0) {
|
||||||
|
this.post('route.list/1.2', { serviceId: serviceId.toLowerCase() }, function(resp) {
|
||||||
|
this.routeList = resp.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getGraySelectData: function(oldRouteId) {
|
||||||
|
return this.routeList.filter(routeNew => {
|
||||||
|
return oldRouteId !== routeNew.id && oldRouteId.indexOf(routeNew.name) > -1
|
||||||
|
})
|
||||||
|
},
|
||||||
buildTreeData: function(data) {
|
buildTreeData: function(data) {
|
||||||
data.forEach(ele => {
|
data.forEach(ele => {
|
||||||
const parentId = ele.parentId
|
const parentId = ele.parentId
|
||||||
@@ -159,13 +301,82 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onEnvGray: function(row) {
|
onEnvGray: function(row) {
|
||||||
this.confirm('确定要灰度发布【' + row.serviceId + '】吗?', function(done) {
|
this.grayForm.onlyUpdateGrayUserkey = false
|
||||||
this.post('service.instance.env.gray', row, function() {
|
this.openGrayDialog(row)
|
||||||
|
},
|
||||||
|
onUpdateUserkey: function(row) {
|
||||||
|
this.grayForm.onlyUpdateGrayUserkey = true
|
||||||
|
this.openGrayDialog(row)
|
||||||
|
},
|
||||||
|
openGrayDialog: function(row) {
|
||||||
|
const serviceId = row.serviceId
|
||||||
|
this.loadRouteList(serviceId)
|
||||||
|
this.post('service.instance.gray.userkey.get', { instanceId: row.instanceId }, function(resp) {
|
||||||
|
this.grayDialogVisible = true
|
||||||
|
const data = resp.data
|
||||||
|
Object.assign(this.grayForm, {
|
||||||
|
serviceId: serviceId,
|
||||||
|
instanceId: row.instanceId,
|
||||||
|
ipPort: row.ipPort,
|
||||||
|
userKeyContent: data.userKeyContent || '',
|
||||||
|
grayRouteConfigList: this.createGrayRouteConfigList(data.nameVersionContent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createGrayRouteConfigList: function(nameVersionContent) {
|
||||||
|
if (!nameVersionContent) {
|
||||||
|
return [{
|
||||||
|
oldRouteId: '',
|
||||||
|
newVersion: '',
|
||||||
|
key: Date.now()
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
const list = []
|
||||||
|
const arr = nameVersionContent.split(',')
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
const el = arr[i]
|
||||||
|
const elArr = el.split('=')
|
||||||
|
list.push({
|
||||||
|
oldRouteId: elArr[0],
|
||||||
|
newVersion: elArr[1],
|
||||||
|
key: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
},
|
||||||
|
onAddUserKey: function() {
|
||||||
|
this.$refs.grayForm.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
const nameVersionContents = []
|
||||||
|
const grayRouteConfigList = this.grayForm.grayRouteConfigList
|
||||||
|
for (let i = 0; i < grayRouteConfigList.length; i++) {
|
||||||
|
const config = grayRouteConfigList[i]
|
||||||
|
nameVersionContents.push(config.oldRouteId + '=' + config.newVersion)
|
||||||
|
}
|
||||||
|
this.grayForm.nameVersionContent = nameVersionContents.join(',')
|
||||||
|
this.post('service.instance.env.gray', this.grayForm, function() {
|
||||||
|
this.grayDialogVisible = false
|
||||||
this.tip('灰度发布发成功')
|
this.tip('灰度发布发成功')
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onChangeOldRoute: function(config) {
|
||||||
|
config.newVersion = ''
|
||||||
|
},
|
||||||
|
addNameVersion: function() {
|
||||||
|
this.grayForm.grayRouteConfigList.push({
|
||||||
|
oldRouteId: '',
|
||||||
|
newVersion: '',
|
||||||
|
key: Date.now()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeNameVersion: function(item) {
|
||||||
|
const index = this.grayForm.grayRouteConfigList.indexOf(item)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.grayForm.grayRouteConfigList.splice(index, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
renderServiceName: function(row) {
|
renderServiceName: function(row) {
|
||||||
let instanceCount = ''
|
let instanceCount = ''
|
||||||
if (row.children && row.children.length > 0) {
|
if (row.children && row.children.length > 0) {
|
||||||
@@ -175,14 +386,6 @@ export default {
|
|||||||
instanceCount = ` (${onlineCount}/${row.children.length})`
|
instanceCount = ` (${onlineCount}/${row.children.length})`
|
||||||
}
|
}
|
||||||
return row.serviceId + instanceCount
|
return row.serviceId + instanceCount
|
||||||
},
|
|
||||||
renderMetadata: function(row) {
|
|
||||||
const metadata = row.metadata
|
|
||||||
const html = []
|
|
||||||
for (const key in metadata) {
|
|
||||||
html.push(key + '=' + metadata[key])
|
|
||||||
}
|
|
||||||
return html.join('<br />')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
package com.gitee.sop.gatewaycommon.bean;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
public class SpringContext implements ApplicationContextAware {
|
||||||
|
|
||||||
|
private static ApplicationContext ctx;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
ctx = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T getBean(Class<T> clazz) {
|
||||||
|
return ctx.getBean(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object getBean(String beanName) {
|
||||||
|
return ctx.getBean(beanName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationContext getApplicationContext() {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
package com.gitee.sop.gatewaycommon.bean;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserKeyDefinition {
|
||||||
|
private String serviceId;
|
||||||
|
private String data;
|
||||||
|
}
|
@@ -69,6 +69,10 @@ public class ZookeeperContext {
|
|||||||
return SOP_MSG_CHANNEL_PATH + "/isvinfo";
|
return SOP_MSG_CHANNEL_PATH + "/isvinfo";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getUserKeyChannelPath() {
|
||||||
|
return SOP_MSG_CHANNEL_PATH + "/userkey";
|
||||||
|
}
|
||||||
|
|
||||||
public static String getIsvRoutePermissionChannelPath() {
|
public static String getIsvRoutePermissionChannelPath() {
|
||||||
return SOP_MSG_CHANNEL_PATH + "/isv-route-permission";
|
return SOP_MSG_CHANNEL_PATH + "/isv-route-permission";
|
||||||
}
|
}
|
||||||
|
@@ -22,7 +22,7 @@ public abstract class BaseParamBuilder<T> implements ParamBuilder<T> {
|
|||||||
* @param ctx 请求request
|
* @param ctx 请求request
|
||||||
* @return 返回请求参数
|
* @return 返回请求参数
|
||||||
*/
|
*/
|
||||||
public abstract Map<String, String> buildRequestParams(T ctx);
|
public abstract Map<String, ?> buildRequestParams(T ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回客户端ip
|
* 返回客户端ip
|
||||||
@@ -34,10 +34,8 @@ public abstract class BaseParamBuilder<T> implements ParamBuilder<T> {
|
|||||||
@Override
|
@Override
|
||||||
public ApiParam build(T ctx) {
|
public ApiParam build(T ctx) {
|
||||||
ApiParam apiParam = this.newApiParam(ctx);
|
ApiParam apiParam = this.newApiParam(ctx);
|
||||||
Map<String, String> requestParams = this.buildRequestParams(ctx);
|
Map<String, ?> requestParams = this.buildRequestParams(ctx);
|
||||||
for (Map.Entry<String, ?> entry : requestParams.entrySet()) {
|
apiParam.putAll(requestParams);
|
||||||
apiParam.put(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
this.initOtherProperty(apiParam);
|
this.initOtherProperty(apiParam);
|
||||||
apiParam.setIp(this.getIP(ctx));
|
apiParam.setIp(this.getIP(ctx));
|
||||||
return apiParam;
|
return apiParam;
|
||||||
|
@@ -45,5 +45,7 @@ public class ParamNames {
|
|||||||
/** 返回sign名称 */
|
/** 返回sign名称 */
|
||||||
public static String RESPONSE_SIGN_NAME = "sign";
|
public static String RESPONSE_SIGN_NAME = "sign";
|
||||||
|
|
||||||
|
public static String HEADER_VERSION_NAME = "x-sop-version";
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package com.gitee.sop.gatewaycommon.util;
|
package com.gitee.sop.gatewaycommon.util;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import org.apache.commons.fileupload.FileItem;
|
import org.apache.commons.fileupload.FileItem;
|
||||||
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
|
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
|
||||||
import org.apache.commons.fileupload.servlet.ServletFileUpload;
|
import org.apache.commons.fileupload.servlet.ServletFileUpload;
|
||||||
@@ -124,15 +123,10 @@ public class RequestUtil {
|
|||||||
* @param request 请求类型为application/json的Request
|
* @param request 请求类型为application/json的Request
|
||||||
* @return 返回Map
|
* @return 返回Map
|
||||||
*/
|
*/
|
||||||
public static Map<String, String> convertJsonRequestToMap(HttpServletRequest request) {
|
public static Map<String, Object> convertJsonRequestToMap(HttpServletRequest request) {
|
||||||
try {
|
try {
|
||||||
String text = getText(request);
|
String text = getText(request);
|
||||||
JSONObject parseObject = JSON.parseObject(text);
|
return JSON.parseObject(text);
|
||||||
Map<String, String> params = new HashMap<>(parseObject.size());
|
|
||||||
for (Map.Entry<String, Object> entry : parseObject.entrySet()) {
|
|
||||||
params.put(entry.getKey(), String.valueOf(entry.getValue()));
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("解析json请求失败", e);
|
log.error("解析json请求失败", e);
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.gitee.sop.gatewaycommon.zuul.configuration;
|
package com.gitee.sop.gatewaycommon.zuul.configuration;
|
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.bean.ApiContext;
|
import com.gitee.sop.gatewaycommon.bean.ApiContext;
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.SpringContext;
|
||||||
import com.gitee.sop.gatewaycommon.manager.AbstractConfiguration;
|
import com.gitee.sop.gatewaycommon.manager.AbstractConfiguration;
|
||||||
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext;
|
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext;
|
||||||
import com.gitee.sop.gatewaycommon.zuul.filter.ErrorFilter;
|
import com.gitee.sop.gatewaycommon.zuul.filter.ErrorFilter;
|
||||||
@@ -14,6 +15,7 @@ import com.gitee.sop.gatewaycommon.zuul.route.SopRouteLocator;
|
|||||||
import com.gitee.sop.gatewaycommon.zuul.route.ZuulRouteRepository;
|
import com.gitee.sop.gatewaycommon.zuul.route.ZuulRouteRepository;
|
||||||
import com.gitee.sop.gatewaycommon.zuul.route.ZuulZookeeperRouteManager;
|
import com.gitee.sop.gatewaycommon.zuul.route.ZuulZookeeperRouteManager;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||||
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
|
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
|
||||||
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
|
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
|
||||||
@@ -33,6 +35,12 @@ public class BaseZuulConfiguration extends AbstractConfiguration {
|
|||||||
@Autowired
|
@Autowired
|
||||||
protected ServerProperties server;
|
protected ServerProperties server;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
SpringContext springContext() {
|
||||||
|
return new SpringContext();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由存储
|
* 路由存储
|
||||||
* @return
|
* @return
|
||||||
|
@@ -7,6 +7,7 @@ import com.gitee.sop.gatewaycommon.util.RequestUtil;
|
|||||||
import com.netflix.zuul.context.RequestContext;
|
import com.netflix.zuul.context.RequestContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.fileupload.servlet.ServletFileUpload;
|
import org.apache.commons.fileupload.servlet.ServletFileUpload;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@@ -20,14 +21,12 @@ import java.util.Map;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ZuulParamBuilder extends BaseParamBuilder<RequestContext> {
|
public class ZuulParamBuilder extends BaseParamBuilder<RequestContext> {
|
||||||
|
|
||||||
private static final String CONTENT_TYPE_JSON = MediaType.APPLICATION_JSON_VALUE;
|
|
||||||
private static final String CONTENT_TYPE_TEXT = MediaType.TEXT_PLAIN_VALUE;
|
|
||||||
private static final String GET = "get";
|
private static final String GET = "get";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> buildRequestParams(RequestContext ctx) {
|
public Map<String, ?> buildRequestParams(RequestContext ctx) {
|
||||||
HttpServletRequest request = ctx.getRequest();
|
HttpServletRequest request = ctx.getRequest();
|
||||||
Map<String, String> params;
|
Map<String, ?> params;
|
||||||
if (GET.equalsIgnoreCase(request.getMethod())) {
|
if (GET.equalsIgnoreCase(request.getMethod())) {
|
||||||
params = RequestUtil.convertRequestParamsToMap(request);
|
params = RequestUtil.convertRequestParamsToMap(request);
|
||||||
} else {
|
} else {
|
||||||
@@ -37,7 +36,7 @@ public class ZuulParamBuilder extends BaseParamBuilder<RequestContext> {
|
|||||||
}
|
}
|
||||||
contentType = contentType.toLowerCase();
|
contentType = contentType.toLowerCase();
|
||||||
// json或者纯文本形式
|
// json或者纯文本形式
|
||||||
if (contentType.contains(CONTENT_TYPE_JSON) || contentType.contains(CONTENT_TYPE_TEXT)) {
|
if (StringUtils.containsAny(contentType, MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE)) {
|
||||||
params = RequestUtil.convertJsonRequestToMap(request);
|
params = RequestUtil.convertJsonRequestToMap(request);
|
||||||
} else if (ServletFileUpload.isMultipartContent(request)) {
|
} else if (ServletFileUpload.isMultipartContent(request)) {
|
||||||
params = RequestUtil.convertMultipartRequestToMap(request);
|
params = RequestUtil.convertMultipartRequestToMap(request);
|
||||||
|
@@ -42,6 +42,8 @@ public class ParamNames {
|
|||||||
/** 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档 */
|
/** 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档 */
|
||||||
public static String BIZ_CONTENT_NAME = "biz_content";
|
public static String BIZ_CONTENT_NAME = "biz_content";
|
||||||
|
|
||||||
|
public static String HEADER_VERSION_NAME = "x-sop-version";
|
||||||
|
|
||||||
/** 时间戳格式 */
|
/** 时间戳格式 */
|
||||||
public static String TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
public static String TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
|
@@ -46,6 +46,10 @@ public class ApiMappingRequestCondition implements RequestCondition<ApiMappingRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String getVersion(HttpServletRequest request) {
|
protected String getVersion(HttpServletRequest request) {
|
||||||
|
String versionInHeader = request.getHeader(ParamNames.HEADER_VERSION_NAME);
|
||||||
|
if (versionInHeader != null) {
|
||||||
|
return versionInHeader;
|
||||||
|
}
|
||||||
String version = request.getParameter(ParamNames.VERSION_NAME);
|
String version = request.getParameter(ParamNames.VERSION_NAME);
|
||||||
return version == null ? defaultVersion : version;
|
return version == null ? defaultVersion : version;
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -19,8 +20,6 @@ import java.util.Set;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class OpenUtil {
|
public class OpenUtil {
|
||||||
|
|
||||||
private static final String UTF8 = "UTF-8";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取request中的参数
|
* 获取request中的参数
|
||||||
*
|
*
|
||||||
@@ -36,7 +35,7 @@ public class OpenUtil {
|
|||||||
JSONObject jsonObject;
|
JSONObject jsonObject;
|
||||||
if (StringUtils.containsAny(contentType, MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE)) {
|
if (StringUtils.containsAny(contentType, MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE)) {
|
||||||
try {
|
try {
|
||||||
String requestJson = IOUtils.toString(request.getInputStream(), UTF8);
|
String requestJson = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
|
||||||
jsonObject = JSON.parseObject(requestJson);
|
jsonObject = JSON.parseObject(requestJson);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
jsonObject = new JSONObject();
|
jsonObject = new JSONObject();
|
||||||
|
@@ -0,0 +1,42 @@
|
|||||||
|
package com.gitee.sop.gateway.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表名:config_gray_userkey
|
||||||
|
* 备注:灰度发布用户key
|
||||||
|
*
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Table(name = "config_gray_userkey")
|
||||||
|
@Data
|
||||||
|
public class ConfigGrayUserkey {
|
||||||
|
@Id
|
||||||
|
@Column(name = "id")
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
/** 数据库字段:id */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** instanceId, 数据库字段:instance_id */
|
||||||
|
private String instanceId;
|
||||||
|
|
||||||
|
/** 用户key,多个用引文逗号隔开, 数据库字段:user_key_content */
|
||||||
|
private String userKeyContent;
|
||||||
|
|
||||||
|
/** 需要灰度的接口,goods.get=1.2,order.list=1.2, 数据库字段:name_version_content */
|
||||||
|
private String nameVersionContent;
|
||||||
|
|
||||||
|
/** 数据库字段:gmt_create */
|
||||||
|
private Date gmtCreate;
|
||||||
|
|
||||||
|
/** 数据库字段:gmt_modified */
|
||||||
|
private Date gmtModified;
|
||||||
|
}
|
@@ -0,0 +1,116 @@
|
|||||||
|
package com.gitee.sop.gateway.loadbalancer;
|
||||||
|
|
||||||
|
import com.gitee.sop.gateway.manager.UserKeyManager;
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.SpringContext;
|
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam;
|
||||||
|
import com.gitee.sop.gatewaycommon.param.Param;
|
||||||
|
import com.gitee.sop.gatewaycommon.param.ParamNames;
|
||||||
|
import com.gitee.sop.gatewaycommon.zuul.ZuulContext;
|
||||||
|
import com.gitee.sop.gatewaycommon.zuul.loadbalancer.BaseServerChooser;
|
||||||
|
import com.netflix.loadbalancer.Server;
|
||||||
|
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
|
||||||
|
import com.netflix.zuul.context.RequestContext;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预发布、灰度环境选择,参考自:https://segmentfault.com/a/1190000017412946
|
||||||
|
*
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
public class EnvironmentServerChooser extends BaseServerChooser {
|
||||||
|
|
||||||
|
private static final String MEDATA_KEY_ENV = "env";
|
||||||
|
private static final String ENV_PRE_VALUE = "pre";
|
||||||
|
private static final String ENV_GRAY_VALUE = "gray";
|
||||||
|
/**
|
||||||
|
* 预发布机器域名
|
||||||
|
*/
|
||||||
|
private static final String PRE_DOMAIN = "localhost";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean match(Server server) {
|
||||||
|
// eureka存储的metadata
|
||||||
|
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
|
||||||
|
String env = metadata.get(MEDATA_KEY_ENV);
|
||||||
|
return StringUtils.isNotBlank(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这里判断客户端能否访问,可以根据ip地址,域名,header内容来决定是否可以访问预发布环境
|
||||||
|
*
|
||||||
|
* @param server 服务器实例
|
||||||
|
* @param request request
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected boolean canVisit(Server server, HttpServletRequest request) {
|
||||||
|
// eureka存储的metadata
|
||||||
|
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
|
||||||
|
String env = metadata.get(MEDATA_KEY_ENV);
|
||||||
|
boolean canVisit;
|
||||||
|
switch (env) {
|
||||||
|
case ENV_PRE_VALUE:
|
||||||
|
canVisit = canVisitPre(server, request);
|
||||||
|
break;
|
||||||
|
case ENV_GRAY_VALUE:
|
||||||
|
canVisit = canVisitGray(server, request);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
canVisit = false;
|
||||||
|
}
|
||||||
|
return canVisit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过判断hostname来确定是否是预发布请求,可修改此方法实现自己想要的
|
||||||
|
*
|
||||||
|
* @param request request
|
||||||
|
* @return 返回true:可以进入到预发环境
|
||||||
|
*/
|
||||||
|
protected boolean canVisitPre(Server server, HttpServletRequest request) {
|
||||||
|
String serverName = request.getServerName();
|
||||||
|
return PRE_DOMAIN.equals(serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 能否进入灰度环境,可修改此方法实现自己想要的
|
||||||
|
*
|
||||||
|
* @param request request
|
||||||
|
* @return 返回true:可以进入到预发环境
|
||||||
|
*/
|
||||||
|
protected boolean canVisitGray(Server server, HttpServletRequest request) {
|
||||||
|
ApiParam apiParam = ZuulContext.getApiParam();
|
||||||
|
UserKeyManager userKeyManager = SpringContext.getBean(UserKeyManager.class);
|
||||||
|
boolean canVisit = false;
|
||||||
|
if (this.isGrayUser(apiParam, userKeyManager, server, request)) {
|
||||||
|
// 指定灰度版本号
|
||||||
|
String instanceId = server.getId();
|
||||||
|
String newVersion = userKeyManager.getVersion(instanceId, apiParam.fetchNameVersion());
|
||||||
|
if (newVersion != null) {
|
||||||
|
RequestContext.getCurrentContext().getZuulRequestHeaders().put(ParamNames.HEADER_VERSION_NAME, newVersion);
|
||||||
|
canVisit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return canVisit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是灰度用户
|
||||||
|
*
|
||||||
|
* @param param 接口参数
|
||||||
|
* @param userKeyManager userKey管理
|
||||||
|
* @param server 服务器实例
|
||||||
|
* @param request request
|
||||||
|
* @return true:是
|
||||||
|
*/
|
||||||
|
protected boolean isGrayUser(Param param, UserKeyManager userKeyManager, Server server, HttpServletRequest request) {
|
||||||
|
String instanceId = server.getId();
|
||||||
|
// 这里的灰度用户为appKey,包含此appKey则为灰度用户,允许访问
|
||||||
|
String appKey = param.fetchAppKey();
|
||||||
|
return userKeyManager.containsKey(instanceId, appKey);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,55 +0,0 @@
|
|||||||
package com.gitee.sop.gateway.loadbalancer;
|
|
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.zuul.loadbalancer.BaseServerChooser;
|
|
||||||
import com.netflix.loadbalancer.Server;
|
|
||||||
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 预发布环境选择,参考自:https://segmentfault.com/a/1190000017412946
|
|
||||||
*
|
|
||||||
* @author tanghc
|
|
||||||
*/
|
|
||||||
public class PreEnvironmentServerChooser extends BaseServerChooser {
|
|
||||||
|
|
||||||
private static final String MEDATA_KEY_ENV = "env";
|
|
||||||
private static final String ENV_PRE_VALUE = "pre";
|
|
||||||
|
|
||||||
private static final String HOST_NAME = "localhost";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean match(Server server) {
|
|
||||||
// eureka存储的metadata
|
|
||||||
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
|
|
||||||
String env = metadata.get(MEDATA_KEY_ENV);
|
|
||||||
return StringUtils.isNotBlank(env);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 这里判断客户端能否访问,可以根据ip地址,域名,header内容来决定是否可以访问预发布环境
|
|
||||||
* @param server 服务器实例
|
|
||||||
* @param request request
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean canVisit(Server server, HttpServletRequest request) {
|
|
||||||
// eureka存储的metadata
|
|
||||||
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
|
|
||||||
String env = metadata.get(MEDATA_KEY_ENV);
|
|
||||||
return Objects.equals(ENV_PRE_VALUE, env) && canClientVisit(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过判断hostname来确定是否是预发布请求,如果需要通过其它条件判断,修改此方法
|
|
||||||
* @param request request
|
|
||||||
* @return 返回true:可以进入到预发环境
|
|
||||||
*/
|
|
||||||
protected boolean canClientVisit(HttpServletRequest request) {
|
|
||||||
String serverName = request.getServerName();
|
|
||||||
return HOST_NAME.equals(serverName);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,33 @@
|
|||||||
|
package com.gitee.sop.gateway.loadbalancer;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ServiceGrayConfig {
|
||||||
|
/**
|
||||||
|
* 用户id
|
||||||
|
*/
|
||||||
|
private Set<String> userKeys;
|
||||||
|
|
||||||
|
/** 存放接口隐射关系,key:nameversion,value:newVersion */
|
||||||
|
private Map<String, String> grayNameVersion;
|
||||||
|
|
||||||
|
public boolean containsKey(Object userKey) {
|
||||||
|
return userKeys.contains(String.valueOf(userKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion(String name) {
|
||||||
|
return grayNameVersion.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
this.userKeys.clear();
|
||||||
|
this.grayNameVersion.clear();
|
||||||
|
}
|
||||||
|
}
|
@@ -17,13 +17,13 @@ public class SopPropertiesFactory extends PropertiesFactory {
|
|||||||
*/
|
*/
|
||||||
private static final String PROPERTIES_KEY = "zuul.custom-rule-classname";
|
private static final String PROPERTIES_KEY = "zuul.custom-rule-classname";
|
||||||
|
|
||||||
private static final String CUSTOM_RULE_CLASSNAME = PreEnvironmentServerChooser.class.getName();
|
private static final String CUSTOM_RULE_CLASSNAME = EnvironmentServerChooser.class.getName();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Environment environment;
|
private Environment environment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置文件配置:<serviceId>.ribbon.NFLoadBalancerRuleClassName=com.gitee.sop.gateway.loadbalancer.PreEnvironmentServerChooser
|
* 配置文件配置:<serviceId>.ribbon.NFLoadBalancerRuleClassName=com.gitee.sop.gateway.loadbalancer.EnvironmentServerChooser
|
||||||
* @param clazz
|
* @param clazz
|
||||||
* @param name serviceId
|
* @param name serviceId
|
||||||
* @return 返回class全限定名
|
* @return 返回class全限定名
|
||||||
|
@@ -0,0 +1,132 @@
|
|||||||
|
package com.gitee.sop.gateway.manager;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.gitee.sop.gateway.entity.ConfigGrayUserkey;
|
||||||
|
import com.gitee.sop.gateway.loadbalancer.ServiceGrayConfig;
|
||||||
|
import com.gitee.sop.gateway.mapper.ConfigGrayUserkeyMapper;
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.UserKeyDefinition;
|
||||||
|
import com.gitee.sop.gatewaycommon.manager.ZookeeperContext;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存放用户key,这里放在本机内容,如果灰度发布保存的用户id数量偏多,可放在redis中
|
||||||
|
*
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class UserKeyManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KEY:instanceId
|
||||||
|
*/
|
||||||
|
private Map<String, ServiceGrayConfig> serviceUserKeyMap = Maps.newConcurrentMap();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConfigGrayUserkeyMapper configGrayUserkeyMapper;
|
||||||
|
|
||||||
|
public boolean containsKey(String serviceId, Object userKey) {
|
||||||
|
if (serviceId == null || userKey == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.getServiceGrayConfig(serviceId).containsKey(userKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion(String serviceId, String nameVersion) {
|
||||||
|
if (serviceId == null || nameVersion == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.getServiceGrayConfig(serviceId).getVersion(nameVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户key
|
||||||
|
*
|
||||||
|
* @param configGrayUserkey 灰度配置
|
||||||
|
*/
|
||||||
|
public void setServiceGrayConfig(String serviceId, ConfigGrayUserkey configGrayUserkey) {
|
||||||
|
if (configGrayUserkey == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clear(serviceId);
|
||||||
|
String userKeyData = configGrayUserkey.getUserKeyContent();
|
||||||
|
String nameVersionContent = configGrayUserkey.getNameVersionContent();
|
||||||
|
String[] userKeys = StringUtils.split(userKeyData, ',');
|
||||||
|
String[] nameVersionList = StringUtils.split(nameVersionContent, ',');
|
||||||
|
log.info("添加userKey,userKeys.length:{}, nameVersionList:{}", userKeys.length, Arrays.toString(nameVersionList));
|
||||||
|
|
||||||
|
List<String> list = Stream.of(userKeys).collect(Collectors.toList());
|
||||||
|
ServiceGrayConfig serviceGrayConfig = getServiceGrayConfig(serviceId);
|
||||||
|
serviceGrayConfig.getUserKeys().addAll(list);
|
||||||
|
|
||||||
|
Map<String, String> grayNameVersion = serviceGrayConfig.getGrayNameVersion();
|
||||||
|
for (String nameVersion : nameVersionList) {
|
||||||
|
String[] nameVersionInfo = StringUtils.split(nameVersion, '=');
|
||||||
|
String name = nameVersionInfo[0];
|
||||||
|
String version = nameVersionInfo[1];
|
||||||
|
grayNameVersion.put(name, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空用户key
|
||||||
|
*/
|
||||||
|
public void clear(String serviceId) {
|
||||||
|
getServiceGrayConfig(serviceId).clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceGrayConfig getServiceGrayConfig(String serviceId) {
|
||||||
|
ServiceGrayConfig serviceGrayConfig = serviceUserKeyMap.get(serviceId);
|
||||||
|
if (serviceGrayConfig == null) {
|
||||||
|
serviceGrayConfig = new ServiceGrayConfig();
|
||||||
|
serviceGrayConfig.setUserKeys(Sets.newConcurrentHashSet());
|
||||||
|
serviceGrayConfig.setGrayNameVersion(Maps.newConcurrentMap());
|
||||||
|
serviceUserKeyMap.put(serviceId, serviceGrayConfig);
|
||||||
|
}
|
||||||
|
return serviceGrayConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
protected void after() throws Exception {
|
||||||
|
ZookeeperContext.setEnvironment(environment);
|
||||||
|
String isvChannelPath = ZookeeperContext.getUserKeyChannelPath();
|
||||||
|
ZookeeperContext.listenPath(isvChannelPath, nodeCache -> {
|
||||||
|
String nodeData = new String(nodeCache.getCurrentData().getData());
|
||||||
|
ChannelMsg channelMsg = JSON.parseObject(nodeData, ChannelMsg.class);
|
||||||
|
String data = channelMsg.getData();
|
||||||
|
UserKeyDefinition userKeyDefinition = JSON.parseObject(data, UserKeyDefinition.class);
|
||||||
|
String serviceId = userKeyDefinition.getServiceId();
|
||||||
|
switch (channelMsg.getOperation()) {
|
||||||
|
case "set":
|
||||||
|
ConfigGrayUserkey configGrayUserkey = configGrayUserkeyMapper.getByColumn("service_id", serviceId);
|
||||||
|
this.setServiceGrayConfig(serviceId, configGrayUserkey);
|
||||||
|
break;
|
||||||
|
case "clear":
|
||||||
|
clear(serviceId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.error("userKey消息,错误的消息指令,nodeData:{}", nodeData);
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
package com.gitee.sop.gateway.mapper;
|
||||||
|
|
||||||
|
import com.gitee.fastmybatis.core.mapper.CrudMapper;
|
||||||
|
import com.gitee.sop.gateway.entity.ConfigGrayUserkey;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
public interface ConfigGrayUserkeyMapper extends CrudMapper<ConfigGrayUserkey, Long> {
|
||||||
|
}
|
12
sop.sql
12
sop.sql
@@ -15,6 +15,7 @@ DROP TABLE IF EXISTS `admin_user_info`;
|
|||||||
DROP TABLE IF EXISTS `config_common`;
|
DROP TABLE IF EXISTS `config_common`;
|
||||||
DROP TABLE IF EXISTS `isv_keys`;
|
DROP TABLE IF EXISTS `isv_keys`;
|
||||||
DROP TABLE IF EXISTS `config_ip_blacklist`;
|
DROP TABLE IF EXISTS `config_ip_blacklist`;
|
||||||
|
DROP TABLE IF EXISTS `config_gray_userkey`;
|
||||||
|
|
||||||
CREATE TABLE `admin_user_info` (
|
CREATE TABLE `admin_user_info` (
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
@@ -175,6 +176,17 @@ CREATE TABLE `config_ip_blacklist` (
|
|||||||
UNIQUE KEY `uk_ip` (`ip`) USING BTREE
|
UNIQUE KEY `uk_ip` (`ip`) USING BTREE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='IP黑名单';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='IP黑名单';
|
||||||
|
|
||||||
|
CREATE TABLE `config_gray_userkey` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`instance_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'serviceId',
|
||||||
|
`user_key_content` text COMMENT '用户key,多个用引文逗号隔开',
|
||||||
|
`name_version_content` text COMMENT '需要灰度的接口,goods.get=1.2,order.list=1.2',
|
||||||
|
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_instanceid` (`instance_id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='灰度发布用户key';
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = @PREVIOUS_FOREIGN_KEY_CHECKS;
|
SET FOREIGN_KEY_CHECKS = @PREVIOUS_FOREIGN_KEY_CHECKS;
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user