支持预发布、灰度发布

1.x
tanghc 5 years ago
parent 6125625b87
commit 202267b686
  1. 31
      doc/docs/files/10110_预发布灰度发布.md
  2. 12
      sop-1.14.0.sql
  3. 6
      sop-admin/sop-admin-server/pom.xml
  4. 4
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/SopAdminServerApplication.java
  5. 3
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/IsvApi.java
  6. 12
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/IPBlacklistApi.java
  7. 63
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/ServiceApi.java
  8. 26
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/ServiceInstanceGrayParam.java
  9. 27
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/ServiceInstanceParam.java
  10. 5
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/ServiceSearchParam.java
  11. 5
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/ChannelMsg.java
  12. 12
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/UserKeyDefinition.java
  13. 4
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/ZookeeperContext.java
  14. 60
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/common/ChannelOperation.java
  15. 42
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/entity/ConfigGrayUserkey.java
  16. 12
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/mapper/ConfigGrayUserkeyMapper.java
  17. 7
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/service/RouteConfigService.java
  18. 5
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/service/RoutePermissionService.java
  19. 4
      sop-admin/sop-admin-server/src/main/resources/application-dev.properties
  20. 2
      sop-admin/sop-admin-vue/src/views/service/ipBlacklist.vue
  21. 225
      sop-admin/sop-admin-vue/src/views/service/serviceList.vue
  22. 30
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SpringContext.java
  23. 12
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/UserKeyDefinition.java
  24. 4
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ZookeeperContext.java
  25. 8
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/BaseParamBuilder.java
  26. 2
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamNames.java
  27. 10
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java
  28. 8
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/configuration/BaseZuulConfiguration.java
  29. 9
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/param/ZuulParamBuilder.java
  30. 2
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/bean/ParamNames.java
  31. 4
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingRequestCondition.java
  32. 5
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/util/OpenUtil.java
  33. 42
      sop-gateway/src/main/java/com/gitee/sop/gateway/entity/ConfigGrayUserkey.java
  34. 116
      sop-gateway/src/main/java/com/gitee/sop/gateway/loadbalancer/EnvironmentServerChooser.java
  35. 55
      sop-gateway/src/main/java/com/gitee/sop/gateway/loadbalancer/PreEnvironmentServerChooser.java
  36. 33
      sop-gateway/src/main/java/com/gitee/sop/gateway/loadbalancer/ServiceGrayConfig.java
  37. 4
      sop-gateway/src/main/java/com/gitee/sop/gateway/loadbalancer/SopPropertiesFactory.java
  38. 132
      sop-gateway/src/main/java/com/gitee/sop/gateway/manager/UserKeyManager.java
  39. 11
      sop-gateway/src/main/java/com/gitee/sop/gateway/mapper/ConfigGrayUserkeyMapper.java
  40. 12
      sop.sql

@ -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`
## 使用灰度发布
灰度发布可允许指定的用户进行访问,其它用户则走正常流程。

@ -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> {
}

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

Loading…
Cancel
Save