mirror of
https://gitee.com/durcframework/SOP.git
synced 2025-08-12 07:02:14 +08:00
全面使用nacos,舍弃zookeeper
This commit is contained in:
@@ -56,7 +56,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul class="sub-menu">
|
<ul class="sub-menu">
|
||||||
<li date-refresh="1">
|
<li date-refresh="1">
|
||||||
<a href="todo.html">
|
<a href="config.html">
|
||||||
<i class="layui-icon layui-icon-table"></i>
|
<i class="layui-icon layui-icon-table"></i>
|
||||||
<cite>配置列表</cite>
|
<cite>配置列表</cite>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
package com.gitee.sop.adminserver.api.service;
|
package com.gitee.sop.adminserver.api.service;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
|
||||||
import com.alibaba.nacos.api.annotation.NacosInjected;
|
import com.alibaba.nacos.api.annotation.NacosInjected;
|
||||||
import com.alibaba.nacos.api.naming.NamingService;
|
import com.alibaba.nacos.api.naming.NamingService;
|
||||||
import com.gitee.easyopen.annotation.Api;
|
import com.gitee.easyopen.annotation.Api;
|
||||||
@@ -19,7 +18,6 @@ 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.NacosConfigs;
|
import com.gitee.sop.adminserver.bean.NacosConfigs;
|
||||||
import com.gitee.sop.adminserver.bean.ServiceGrayDefinition;
|
import com.gitee.sop.adminserver.bean.ServiceGrayDefinition;
|
||||||
import com.gitee.sop.adminserver.bean.ServiceRouteInfo;
|
|
||||||
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.ChannelOperation;
|
||||||
import com.gitee.sop.adminserver.common.StatusEnum;
|
import com.gitee.sop.adminserver.common.StatusEnum;
|
||||||
@@ -32,7 +30,6 @@ 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;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang.BooleanUtils;
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -41,7 +38,6 @@ import org.springframework.util.CollectionUtils;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -70,14 +66,14 @@ public class ServiceApi {
|
|||||||
@NacosInjected
|
@NacosInjected
|
||||||
private NamingService namingService;
|
private NamingService namingService;
|
||||||
|
|
||||||
@Api(name = "zookeeper.service.list")
|
@Api(name = "registry.service.list")
|
||||||
@ApiDocMethod(description = "zk中的服务列表", elementClass = RouteServiceInfo.class)
|
@ApiDocMethod(description = "注册中心的服务列表", elementClass = RouteServiceInfo.class)
|
||||||
List<RouteServiceInfo> listServiceInfo(ServiceSearchParam param) {
|
List<RouteServiceInfo> listServiceInfo(ServiceSearchParam param) {
|
||||||
List<ServiceInfo> servicesOfServer = null;
|
List<ServiceInfo> servicesOfServer = null;
|
||||||
try {
|
try {
|
||||||
servicesOfServer = registryService.listAllService(1, Integer.MAX_VALUE);
|
servicesOfServer = registryService.listAllService(1, Integer.MAX_VALUE);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("nacos获取服务列表失败", e);
|
log.error("获取服务列表失败", e);
|
||||||
throw new BizException("获取服务列表失败");
|
throw new BizException("获取服务列表失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,46 +105,15 @@ public class ServiceApi {
|
|||||||
@Api(name = "service.custom.add")
|
@Api(name = "service.custom.add")
|
||||||
@ApiDocMethod(description = "添加服务")
|
@ApiDocMethod(description = "添加服务")
|
||||||
void addService(ServiceAddParam param) {
|
void addService(ServiceAddParam param) {
|
||||||
// TODO:添加服务
|
// TODO: 添加服务
|
||||||
String serviceId = param.getServiceId();
|
throw new BizException("该功能已下线");
|
||||||
// String servicePath = ZookeeperContext.buildServiceIdPath(serviceId);
|
|
||||||
ServiceRouteInfo serviceRouteInfo = new ServiceRouteInfo();
|
|
||||||
Date now = new Date();
|
|
||||||
serviceRouteInfo.setServiceId(serviceId);
|
|
||||||
serviceRouteInfo.setDescription("自定义服务");
|
|
||||||
serviceRouteInfo.setCreateTime(now);
|
|
||||||
serviceRouteInfo.setUpdateTime(now);
|
|
||||||
serviceRouteInfo.setCustom(BooleanUtils.toInteger(true));
|
|
||||||
String serviceData = JSON.toJSONString(serviceRouteInfo);
|
|
||||||
|
|
||||||
/* try {
|
|
||||||
ZookeeperContext.addPath(servicePath, serviceData);
|
|
||||||
} catch (ZookeeperPathExistException e) {
|
|
||||||
throw new BizException("服务已存在");
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Api(name = "service.custom.del")
|
@Api(name = "service.custom.del")
|
||||||
@ApiDocMethod(description = "删除自定义服务")
|
@ApiDocMethod(description = "删除自定义服务")
|
||||||
void delService(ServiceSearchParam param) {
|
void delService(ServiceSearchParam param) {
|
||||||
// TODO:删除自定义服务
|
// TODO: 删除自定义服务
|
||||||
/*String serviceId = param.getServiceId();
|
throw new BizException("该功能已下线");
|
||||||
String servicePath = ZookeeperContext.buildServiceIdPath(serviceId);
|
|
||||||
String data = null;
|
|
||||||
try {
|
|
||||||
data = ZookeeperContext.getData(servicePath);
|
|
||||||
} catch (ZookeeperPathNotExistException e) {
|
|
||||||
throw new BizException("服务不存在");
|
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(data)) {
|
|
||||||
throw new BizException("非自定义服务,无法删除");
|
|
||||||
}
|
|
||||||
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(data, ServiceRouteInfo.class);
|
|
||||||
int custom = serviceRouteInfo.getCustom();
|
|
||||||
if (!BooleanUtils.toBoolean(custom)) {
|
|
||||||
throw new BizException("非自定义服务,无法删除");
|
|
||||||
}
|
|
||||||
ZookeeperContext.deletePathDeep(servicePath);*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Api(name = "service.instance.list")
|
@Api(name = "service.instance.list")
|
||||||
@@ -257,12 +222,12 @@ public class ServiceApi {
|
|||||||
@Api(name = "service.instance.env.gray.open")
|
@Api(name = "service.instance.env.gray.open")
|
||||||
@ApiDocMethod(description = "开启灰度发布")
|
@ApiDocMethod(description = "开启灰度发布")
|
||||||
void serviceEnvGray(ServiceInstanceParam param) throws IOException {
|
void serviceEnvGray(ServiceInstanceParam param) throws IOException {
|
||||||
try {
|
|
||||||
String serviceId = param.getServiceId().toLowerCase();
|
String serviceId = param.getServiceId().toLowerCase();
|
||||||
ConfigGray configGray = this.getConfigGray(serviceId);
|
ConfigGray configGray = this.getConfigGray(serviceId);
|
||||||
if (configGray == null) {
|
if (configGray == null) {
|
||||||
throw new BizException("请先设置灰度参数");
|
throw new BizException("请先设置灰度参数");
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
MetadataEnum envPre = MetadataEnum.ENV_GRAY;
|
MetadataEnum envPre = MetadataEnum.ENV_GRAY;
|
||||||
registryService.setMetadata(param.buildServiceInstance(), envPre.getKey(), envPre.getValue());
|
registryService.setMetadata(param.buildServiceInstance(), envPre.getKey(), envPre.getValue());
|
||||||
|
|
||||||
|
@@ -2,6 +2,9 @@ package com.gitee.sop.adminserver.bean;
|
|||||||
|
|
||||||
import com.gitee.sop.adminserver.common.ChannelOperation;
|
import com.gitee.sop.adminserver.common.ChannelOperation;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author tanghc
|
* @author tanghc
|
||||||
@@ -9,11 +12,20 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class ChannelMsg {
|
public class ChannelMsg {
|
||||||
|
|
||||||
|
private static final String TIME_PATTERN = "yyyy-MM-dd HH:mm:ss:SSS";
|
||||||
|
|
||||||
public ChannelMsg(ChannelOperation channelOperation, Object data) {
|
public ChannelMsg(ChannelOperation channelOperation, Object data) {
|
||||||
this.operation = channelOperation.getOperation();
|
this.operation = channelOperation.getOperation();
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
this.timestamp = DateFormatUtils.format(new Date(), TIME_PATTERN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String operation;
|
private String operation;
|
||||||
private Object data;
|
private Object data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加个时间戳,格式yyyy-MM-dd HH:mm:ss:SSS,确保每次推送内容都不一样
|
||||||
|
* nacos监听基于MD5值,如果每次推送的内容一样,则监听不会触发,因此必须确保每次推送的MD5不一样
|
||||||
|
*/
|
||||||
|
private String timestamp;
|
||||||
}
|
}
|
||||||
|
@@ -21,10 +21,10 @@ public class ConfigPushService {
|
|||||||
|
|
||||||
public void publishConfig(String dataId, String groupId, ChannelMsg channelMsg) {
|
public void publishConfig(String dataId, String groupId, ChannelMsg channelMsg) {
|
||||||
try {
|
try {
|
||||||
log.info("nacos配置, dataId:{}, groupId:{}, operation:{}", dataId, groupId, channelMsg.getOperation());
|
log.info("nacos配置, dataId={}, groupId={}, operation={}", dataId, groupId, channelMsg.getOperation());
|
||||||
configService.publishConfig(dataId, groupId, JSON.toJSONString(channelMsg));
|
configService.publishConfig(dataId, groupId, JSON.toJSONString(channelMsg));
|
||||||
} catch (NacosException e) {
|
} catch (NacosException e) {
|
||||||
log.error("nacos配置失败, dataId:{}, groupId:{}, operation:{}", dataId, groupId, channelMsg.getOperation(), e);
|
log.error("nacos配置失败, dataId={}, groupId={}, operation={}", dataId, groupId, channelMsg.getOperation(), e);
|
||||||
throw new BizException("nacos配置失败");
|
throw new BizException("nacos配置失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +1 @@
|
|||||||
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel=icon href=favicon.ico><title>SOP Admin</title><link href=static/css/chunk-elementUI.81cf475c.css rel=stylesheet><link href=static/css/chunk-libs.3dfb7769.css rel=stylesheet><link href=static/css/app.4f0872ef.css rel=stylesheet></head><body><noscript><strong>We're sorry but SOP Admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script>(function(e){function n(n){for(var r,c,a=n[0],f=n[1],i=n[2],l=0,h=[];l<a.length;l++)c=a[l],u[c]&&h.push(u[c][0]),u[c]=0;for(r in f)Object.prototype.hasOwnProperty.call(f,r)&&(e[r]=f[r]);d&&d(n);while(h.length)h.shift()();return o.push.apply(o,i||[]),t()}function t(){for(var e,n=0;n<o.length;n++){for(var t=o[n],r=!0,c=1;c<t.length;c++){var a=t[c];0!==u[a]&&(r=!1)}r&&(o.splice(n--,1),e=f(f.s=t[0]))}return e}var r={},c={runtime:0},u={runtime:0},o=[];function a(e){return f.p+"static/js/"+({}[e]||e)+"."+{"chunk-238a81e9":"5955f13d","chunk-25908fca":"ca176fa6","chunk-2d2085ef":"7c741493","chunk-2d221c34":"c8ef105a","chunk-34c76be7":"98e1e7e5","chunk-37401378":"4e39ec9b","chunk-6f78c9fe":"f1ed64fa","chunk-73b2dcec":"14f248eb","chunk-9b31c83a":"2758df30","chunk-9f479afe":"53fe8d4e","chunk-ea2e58a4":"f3f85b0e"}[e]+".js"}function f(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.e=function(e){var n=[],t={"chunk-238a81e9":1,"chunk-25908fca":1,"chunk-34c76be7":1,"chunk-37401378":1,"chunk-73b2dcec":1,"chunk-9b31c83a":1,"chunk-ea2e58a4":1};c[e]?n.push(c[e]):0!==c[e]&&t[e]&&n.push(c[e]=new Promise(function(n,t){for(var r="static/css/"+({}[e]||e)+"."+{"chunk-238a81e9":"e8e2beee","chunk-25908fca":"89ab33e8","chunk-2d2085ef":"31d6cfe0","chunk-2d221c34":"31d6cfe0","chunk-34c76be7":"f531fb07","chunk-37401378":"a43114f3","chunk-6f78c9fe":"31d6cfe0","chunk-73b2dcec":"99cf6327","chunk-9b31c83a":"3b12267b","chunk-9f479afe":"31d6cfe0","chunk-ea2e58a4":"d10599db"}[e]+".css",u=f.p+r,o=document.getElementsByTagName("link"),a=0;a<o.length;a++){var i=o[a],l=i.getAttribute("data-href")||i.getAttribute("href");if("stylesheet"===i.rel&&(l===r||l===u))return n()}var h=document.getElementsByTagName("style");for(a=0;a<h.length;a++){i=h[a],l=i.getAttribute("data-href");if(l===r||l===u)return n()}var d=document.createElement("link");d.rel="stylesheet",d.type="text/css",d.onload=n,d.onerror=function(n){var r=n&&n.target&&n.target.src||u,o=new Error("Loading CSS chunk "+e+" failed.\n("+r+")");o.code="CSS_CHUNK_LOAD_FAILED",o.request=r,delete c[e],d.parentNode.removeChild(d),t(o)},d.href=u;var s=document.getElementsByTagName("head")[0];s.appendChild(d)}).then(function(){c[e]=0}));var r=u[e];if(0!==r)if(r)n.push(r[2]);else{var o=new Promise(function(n,t){r=u[e]=[n,t]});n.push(r[2]=o);var i,l=document.createElement("script");l.charset="utf-8",l.timeout=120,f.nc&&l.setAttribute("nonce",f.nc),l.src=a(e),i=function(n){l.onerror=l.onload=null,clearTimeout(h);var t=u[e];if(0!==t){if(t){var r=n&&("load"===n.type?"missing":n.type),c=n&&n.target&&n.target.src,o=new Error("Loading chunk "+e+" failed.\n("+r+": "+c+")");o.type=r,o.request=c,t[1](o)}u[e]=void 0}};var h=setTimeout(function(){i({type:"timeout",target:l})},12e4);l.onerror=l.onload=i,document.head.appendChild(l)}return Promise.all(n)},f.m=e,f.c=r,f.d=function(e,n,t){f.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,n){if(1&n&&(e=f(e)),8&n)return e;if(4&n&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)f.d(t,r,function(n){return e[n]}.bind(null,r));return t},f.n=function(e){var n=e&&e.__esModule?function(){return e["default"]}:function(){return e};return f.d(n,"a",n),n},f.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},f.p="",f.oe=function(e){throw console.error(e),e};var i=window["webpackJsonp"]=window["webpackJsonp"]||[],l=i.push.bind(i);i.push=n,i=i.slice();for(var h=0;h<i.length;h++)n(i[h]);var d=l;t()})([]);</script><script src=static/js/chunk-elementUI.8ebdfbab.js></script><script src=static/js/chunk-libs.9cf9cc40.js></script><script src=static/js/app.8145abe4.js></script></body></html>
|
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel=icon href=favicon.ico><title>SOP Admin</title><link href=static/css/chunk-elementUI.81cf475c.css rel=stylesheet><link href=static/css/chunk-libs.3dfb7769.css rel=stylesheet><link href=static/css/app.4f0872ef.css rel=stylesheet></head><body><noscript><strong>We're sorry but SOP Admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script>(function(e){function n(n){for(var r,c,o=n[0],f=n[1],i=n[2],l=0,h=[];l<o.length;l++)c=o[l],u[c]&&h.push(u[c][0]),u[c]=0;for(r in f)Object.prototype.hasOwnProperty.call(f,r)&&(e[r]=f[r]);d&&d(n);while(h.length)h.shift()();return a.push.apply(a,i||[]),t()}function t(){for(var e,n=0;n<a.length;n++){for(var t=a[n],r=!0,c=1;c<t.length;c++){var o=t[c];0!==u[o]&&(r=!1)}r&&(a.splice(n--,1),e=f(f.s=t[0]))}return e}var r={},c={runtime:0},u={runtime:0},a=[];function o(e){return f.p+"static/js/"+({}[e]||e)+"."+{"chunk-238a81e9":"5955f13d","chunk-25908fca":"ca176fa6","chunk-2d2085ef":"7c741493","chunk-2d221c34":"c8ef105a","chunk-37401378":"4e39ec9b","chunk-510c5a69":"93406082","chunk-6f78c9fe":"f1ed64fa","chunk-73b2dcec":"14f248eb","chunk-9b31c83a":"355cc725","chunk-9f479afe":"c1cbb02b","chunk-ea2e58a4":"f3f85b0e"}[e]+".js"}function f(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.e=function(e){var n=[],t={"chunk-238a81e9":1,"chunk-25908fca":1,"chunk-37401378":1,"chunk-510c5a69":1,"chunk-73b2dcec":1,"chunk-9b31c83a":1,"chunk-ea2e58a4":1};c[e]?n.push(c[e]):0!==c[e]&&t[e]&&n.push(c[e]=new Promise(function(n,t){for(var r="static/css/"+({}[e]||e)+"."+{"chunk-238a81e9":"e8e2beee","chunk-25908fca":"89ab33e8","chunk-2d2085ef":"31d6cfe0","chunk-2d221c34":"31d6cfe0","chunk-37401378":"a43114f3","chunk-510c5a69":"5e48e29a","chunk-6f78c9fe":"31d6cfe0","chunk-73b2dcec":"99cf6327","chunk-9b31c83a":"3b12267b","chunk-9f479afe":"31d6cfe0","chunk-ea2e58a4":"d10599db"}[e]+".css",u=f.p+r,a=document.getElementsByTagName("link"),o=0;o<a.length;o++){var i=a[o],l=i.getAttribute("data-href")||i.getAttribute("href");if("stylesheet"===i.rel&&(l===r||l===u))return n()}var h=document.getElementsByTagName("style");for(o=0;o<h.length;o++){i=h[o],l=i.getAttribute("data-href");if(l===r||l===u)return n()}var d=document.createElement("link");d.rel="stylesheet",d.type="text/css",d.onload=n,d.onerror=function(n){var r=n&&n.target&&n.target.src||u,a=new Error("Loading CSS chunk "+e+" failed.\n("+r+")");a.code="CSS_CHUNK_LOAD_FAILED",a.request=r,delete c[e],d.parentNode.removeChild(d),t(a)},d.href=u;var s=document.getElementsByTagName("head")[0];s.appendChild(d)}).then(function(){c[e]=0}));var r=u[e];if(0!==r)if(r)n.push(r[2]);else{var a=new Promise(function(n,t){r=u[e]=[n,t]});n.push(r[2]=a);var i,l=document.createElement("script");l.charset="utf-8",l.timeout=120,f.nc&&l.setAttribute("nonce",f.nc),l.src=o(e),i=function(n){l.onerror=l.onload=null,clearTimeout(h);var t=u[e];if(0!==t){if(t){var r=n&&("load"===n.type?"missing":n.type),c=n&&n.target&&n.target.src,a=new Error("Loading chunk "+e+" failed.\n("+r+": "+c+")");a.type=r,a.request=c,t[1](a)}u[e]=void 0}};var h=setTimeout(function(){i({type:"timeout",target:l})},12e4);l.onerror=l.onload=i,document.head.appendChild(l)}return Promise.all(n)},f.m=e,f.c=r,f.d=function(e,n,t){f.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,n){if(1&n&&(e=f(e)),8&n)return e;if(4&n&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)f.d(t,r,function(n){return e[n]}.bind(null,r));return t},f.n=function(e){var n=e&&e.__esModule?function(){return e["default"]}:function(){return e};return f.d(n,"a",n),n},f.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},f.p="",f.oe=function(e){throw console.error(e),e};var i=window["webpackJsonp"]=window["webpackJsonp"]||[],l=i.push.bind(i);i.push=n,i=i.slice();for(var h=0;h<i.length;h++)n(i[h]);var d=l;t()})([]);</script><script src=static/js/chunk-elementUI.8ebdfbab.js></script><script src=static/js/chunk-libs.9cf9cc40.js></script><script src=static/js/app.4f45e42d.js></script></body></html>
|
@@ -1 +1 @@
|
|||||||
.custom-tree-node{-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;font-size:14px;padding-right:8px}.el-input.is-disabled .el-input__inner,.el-radio__input.is-disabled+span.el-radio__label{color:#909399}.limit-tip[data-v-51d6f4a2]{cursor:pointer;margin-left:10px}
|
.custom-tree-node{-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;font-size:14px;padding-right:8px}.el-input.is-disabled .el-input__inner,.el-radio__input.is-disabled+span.el-radio__label{color:#909399}.limit-tip[data-v-385a3259]{cursor:pointer;margin-left:10px}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -337,7 +337,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
// 加载树
|
// 加载树
|
||||||
loadTree: function() {
|
loadTree: function() {
|
||||||
this.post('zookeeper.service.list', {}, function(resp) {
|
this.post('registry.service.list', {}, function(resp) {
|
||||||
const respData = resp.data
|
const respData = resp.data
|
||||||
this.treeData = this.convertToTreeData(respData, 0)
|
this.treeData = this.convertToTreeData(respData, 0)
|
||||||
})
|
})
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
plain
|
plain
|
||||||
size="mini"
|
size="mini"
|
||||||
icon="el-icon-plus"
|
icon="el-icon-plus"
|
||||||
|
style="display: none;"
|
||||||
@click.stop="addService"
|
@click.stop="addService"
|
||||||
>
|
>
|
||||||
新建服务
|
新建服务
|
||||||
@@ -343,7 +344,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
// 加载树
|
// 加载树
|
||||||
loadTree: function() {
|
loadTree: function() {
|
||||||
this.post('zookeeper.service.list', {}, function(resp) {
|
this.post('registry.service.list', {}, function(resp) {
|
||||||
const respData = resp.data
|
const respData = resp.data
|
||||||
this.treeData = this.convertToTreeData(respData, 0)
|
this.treeData = this.convertToTreeData(respData, 0)
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@@ -192,7 +192,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
const regex = /^\w+(,\w+)*$/
|
const regex = /^\S+(,\S+)*$/
|
||||||
const userKeyContentValidator = (rule, value, callback) => {
|
const userKeyContentValidator = (rule, value, callback) => {
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
callback(new Error('不能为空'))
|
callback(new Error('不能为空'))
|
||||||
@@ -272,18 +272,20 @@ export default {
|
|||||||
this.loadTable()
|
this.loadTable()
|
||||||
},
|
},
|
||||||
onDisable: function(row) {
|
onDisable: function(row) {
|
||||||
this.confirm('确定要禁用【' + row.serviceId + '】吗?', function(done) {
|
this.confirm(`确定要禁用 ${row.serviceId}(${row.ipPort}) 吗?`, function(done) {
|
||||||
this.post('service.instance.offline', row, function() {
|
this.post('service.instance.offline', row, function() {
|
||||||
this.tip('禁用成功')
|
this.tip('禁用成功')
|
||||||
done()
|
done()
|
||||||
|
this.loadTableDelay()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onEnable: function(row) {
|
onEnable: function(row) {
|
||||||
this.confirm('确定要启用【' + row.serviceId + '】吗?', function(done) {
|
this.confirm(`确定要启用 ${row.serviceId}(${row.ipPort}) 吗?`, function(done) {
|
||||||
this.post('service.instance.online', row, function() {
|
this.post('service.instance.online', row, function() {
|
||||||
this.tip('启用成功')
|
this.tip('启用成功')
|
||||||
done()
|
done()
|
||||||
|
this.loadTableDelay()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -293,34 +295,38 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onEnvPreOpen: function(row) {
|
onEnvPreOpen: function(row) {
|
||||||
this.confirm(`确定要开启 ${row.instanceId} 预发布吗?`, function(done) {
|
this.confirm(`确定要开启 ${row.serviceId}(${row.ipPort}) 预发布吗?`, function(done) {
|
||||||
this.post('service.instance.env.pre.open', row, function() {
|
this.post('service.instance.env.pre.open', row, function() {
|
||||||
this.tip('预发布成功')
|
this.tip('预发布成功')
|
||||||
done()
|
done()
|
||||||
|
this.loadTableDelay()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onEnvPreClose: function(row) {
|
onEnvPreClose: function(row) {
|
||||||
this.confirm(`确定要结束 ${row.instanceId} 预发布吗?`, function(done) {
|
this.confirm(`确定要结束 ${row.serviceId}(${row.ipPort}) 预发布吗?`, function(done) {
|
||||||
this.doEnvOnline(row, function() {
|
this.doEnvOnline(row, function() {
|
||||||
this.tip('操作成功')
|
this.tip('操作成功')
|
||||||
done()
|
done()
|
||||||
|
this.loadTableDelay()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onEnvGrayOpen: function(row) {
|
onEnvGrayOpen: function(row) {
|
||||||
this.confirm(`确定要开启 ${row.instanceId} 灰度吗?`, function(done) {
|
this.confirm(`确定要开启 ${row.serviceId}(${row.ipPort}) 灰度吗?`, function(done) {
|
||||||
this.post('service.instance.env.gray.open', row, function() {
|
this.post('service.instance.env.gray.open', row, function() {
|
||||||
this.tip('开启成功')
|
this.tip('开启成功')
|
||||||
done()
|
done()
|
||||||
|
this.loadTableDelay()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onEnvGrayClose: function(row) {
|
onEnvGrayClose: function(row) {
|
||||||
this.confirm(`确定要结束 ${row.instanceId} 灰度吗?`, function(done) {
|
this.confirm(`确定要结束 ${row.serviceId}(${row.ipPort}) 灰度吗?`, function(done) {
|
||||||
this.doEnvOnline(row, function() {
|
this.doEnvOnline(row, function() {
|
||||||
this.tip('操作成功')
|
this.tip('操作成功')
|
||||||
done()
|
done()
|
||||||
|
this.loadTableDelay()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -329,6 +335,7 @@ export default {
|
|||||||
this.loadRouteList(serviceId)
|
this.loadRouteList(serviceId)
|
||||||
this.post('service.gray.config.get', { serviceId: serviceId }, function(resp) {
|
this.post('service.gray.config.get', { serviceId: serviceId }, function(resp) {
|
||||||
this.grayDialogVisible = true
|
this.grayDialogVisible = true
|
||||||
|
this.$nextTick(() => {
|
||||||
const data = resp.data
|
const data = resp.data
|
||||||
Object.assign(this.grayForm, {
|
Object.assign(this.grayForm, {
|
||||||
serviceId: serviceId,
|
serviceId: serviceId,
|
||||||
@@ -336,6 +343,7 @@ export default {
|
|||||||
grayRouteConfigList: this.createGrayRouteConfigList(data.nameVersionContent)
|
grayRouteConfigList: this.createGrayRouteConfigList(data.nameVersionContent)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onGrayConfigSave: function() {
|
onGrayConfigSave: function() {
|
||||||
this.$refs.grayForm.validate((valid) => {
|
this.$refs.grayForm.validate((valid) => {
|
||||||
@@ -403,6 +411,12 @@ export default {
|
|||||||
instanceCount = `(${onlineCount}/${childCount})`
|
instanceCount = `(${onlineCount}/${childCount})`
|
||||||
}
|
}
|
||||||
return row.serviceId + instanceCount
|
return row.serviceId + instanceCount
|
||||||
|
},
|
||||||
|
loadTableDelay: function() {
|
||||||
|
const that = this
|
||||||
|
setTimeout(function() {
|
||||||
|
that.loadTable()
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -39,11 +39,13 @@ public abstract class BaseRouteCache<T extends TargetRoute> implements RouteLoad
|
|||||||
@Override
|
@Override
|
||||||
public void load(ServiceRouteInfo serviceRouteInfo) {
|
public void load(ServiceRouteInfo serviceRouteInfo) {
|
||||||
try {
|
try {
|
||||||
|
String serviceId = serviceRouteInfo.getServiceId();
|
||||||
String newMd5 = serviceRouteInfo.getMd5();
|
String newMd5 = serviceRouteInfo.getMd5();
|
||||||
String md5 = serviceIdMd5Map.putIfAbsent(serviceRouteInfo.getServiceId(), newMd5);
|
String oldMd5 = serviceIdMd5Map.get(serviceId);
|
||||||
if (Objects.equals(newMd5, md5)) {
|
if (Objects.equals(newMd5, oldMd5)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
serviceIdMd5Map.put(serviceId, newMd5);
|
||||||
List<GatewayRouteDefinition> routeDefinitionList = serviceRouteInfo.getRouteDefinitionList();
|
List<GatewayRouteDefinition> routeDefinitionList = serviceRouteInfo.getRouteDefinitionList();
|
||||||
for (GatewayRouteDefinition gatewayRouteDefinition : routeDefinitionList) {
|
for (GatewayRouteDefinition gatewayRouteDefinition : routeDefinitionList) {
|
||||||
T routeDefinition = this.buildRouteDefinition(serviceRouteInfo, gatewayRouteDefinition);
|
T routeDefinition = this.buildRouteDefinition(serviceRouteInfo, gatewayRouteDefinition);
|
||||||
|
@@ -16,11 +16,11 @@ public interface EnvGrayManager extends BeanInitializer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 实例是否允许
|
* 实例是否允许
|
||||||
* @param instanceId 实例id
|
* @param serviceId serviceId
|
||||||
* @param userKey 用户key,如appKey
|
* @param userKey 用户key,如appKey
|
||||||
* @return true:允许访问
|
* @return true:允许访问
|
||||||
*/
|
*/
|
||||||
boolean containsKey(String instanceId, Object userKey);
|
boolean containsKey(String serviceId, Object userKey);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取灰度发布新版本号
|
* 获取灰度发布新版本号
|
||||||
|
@@ -39,7 +39,7 @@ public class PreVersionDecisionFilter extends BaseZuulFilter {
|
|||||||
String serviceId = targetRoute.getServiceRouteInfo().fetchServiceIdLowerCase();
|
String serviceId = targetRoute.getServiceRouteInfo().fetchServiceIdLowerCase();
|
||||||
// 如果服务在灰度阶段,返回一个灰度版本号
|
// 如果服务在灰度阶段,返回一个灰度版本号
|
||||||
String version = envGrayManager.getVersion(serviceId, nameVersion);
|
String version = envGrayManager.getVersion(serviceId, nameVersion);
|
||||||
if (version != null) {
|
if (version != null && envGrayManager.containsKey(serviceId, apiParam.fetchAppKey())) {
|
||||||
requestContext.addZuulRequestHeader(ParamNames.HEADER_VERSION_NAME, version);
|
requestContext.addZuulRequestHeader(ParamNames.HEADER_VERSION_NAME, version);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@@ -42,6 +42,7 @@ public abstract class BaseServerChooser extends ZoneAvoidanceRule {
|
|||||||
*/
|
*/
|
||||||
protected abstract boolean canVisitPre(Server server, HttpServletRequest request);
|
protected abstract boolean canVisitPre(Server server, HttpServletRequest request);
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Server choose(Object key) {
|
public Server choose(Object key) {
|
||||||
ILoadBalancer lb = getLoadBalancer();
|
ILoadBalancer lb = getLoadBalancer();
|
||||||
@@ -60,8 +61,6 @@ public abstract class BaseServerChooser extends ZoneAvoidanceRule {
|
|||||||
|
|
||||||
List<Server> grayServers = allServers.stream()
|
List<Server> grayServers = allServers.stream()
|
||||||
.filter(this::isGrayServer)
|
.filter(this::isGrayServer)
|
||||||
// 这句暂时不需要,放到了PreVersionDecisionFilter中判断
|
|
||||||
//.filter(server -> canVisitGray(server, request))
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (!grayServers.isEmpty()) {
|
if (!grayServers.isEmpty()) {
|
||||||
return doChoose(grayServers, key);
|
return doChoose(grayServers, key);
|
||||||
|
@@ -33,13 +33,19 @@ public class RegistryServiceNacos implements RegistryService {
|
|||||||
|
|
||||||
static HttpTool httpTool = new HttpTool();
|
static HttpTool httpTool = new HttpTool();
|
||||||
|
|
||||||
@Value("${nacos.discovery.server-addr:}")
|
@Value("${registry.nacos-server-addr:}")
|
||||||
private String nacosAddr;
|
private String nacosAddr;
|
||||||
|
|
||||||
|
@Value("${nacos.discovery.server-addr:}")
|
||||||
|
private String nacosAddrNew;
|
||||||
|
|
||||||
private NamingService namingService;
|
private NamingService namingService;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void after() throws NacosException {
|
public void after() throws NacosException {
|
||||||
|
if (StringUtils.isNotBlank(nacosAddrNew)) {
|
||||||
|
nacosAddr = nacosAddrNew;
|
||||||
|
}
|
||||||
if (StringUtils.isNotBlank(nacosAddr)) {
|
if (StringUtils.isNotBlank(nacosAddr)) {
|
||||||
namingService = NamingFactory.createNamingService(nacosAddr);
|
namingService = NamingFactory.createNamingService(nacosAddr);
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
package com.gitee.sop.gateway.loadbalancer;
|
package com.gitee.sop.gateway.loadbalancer;
|
||||||
|
|
||||||
import com.gitee.sop.gateway.manager.DbEnvGrayManager;
|
|
||||||
import com.gitee.sop.gatewaycommon.bean.SpringContext;
|
import com.gitee.sop.gatewaycommon.bean.SpringContext;
|
||||||
import com.gitee.sop.gatewaycommon.param.Param;
|
|
||||||
import com.gitee.sop.gatewaycommon.zuul.loadbalancer.BaseServerChooser;
|
import com.gitee.sop.gatewaycommon.zuul.loadbalancer.BaseServerChooser;
|
||||||
import com.netflix.loadbalancer.Server;
|
import com.netflix.loadbalancer.Server;
|
||||||
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;
|
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;
|
||||||
@@ -59,19 +57,4 @@ public class EnvironmentServerChooser extends BaseServerChooser {
|
|||||||
return domain.equals(serverName);
|
return domain.equals(serverName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是灰度用户,可修改此方法实现自己想要的
|
|
||||||
*
|
|
||||||
* @param param 接口参数
|
|
||||||
* @param userKeyManager userKey管理
|
|
||||||
* @param server 服务器实例
|
|
||||||
* @param request request
|
|
||||||
* @return true:是
|
|
||||||
*/
|
|
||||||
protected boolean isGrayUser(Param param, DbEnvGrayManager userKeyManager, Server server, HttpServletRequest request) {
|
|
||||||
String instanceId = server.getMetaInfo().getInstanceId();
|
|
||||||
// 这里的灰度用户为appKey,包含此appKey则为灰度用户,允许访问
|
|
||||||
String appKey = param.fetchAppKey();
|
|
||||||
return userKeyManager.containsKey(instanceId, appKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cloud.alibaba.nacos.NacosConfigProperties;
|
import org.springframework.cloud.alibaba.nacos.NacosConfigProperties;
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
@@ -44,9 +43,6 @@ public class DbEnvGrayManager extends DefaultEnvGrayManager {
|
|||||||
private static final Function<String[], String> FUNCTION_KEY = arr -> arr[0];
|
private static final Function<String[], String> FUNCTION_KEY = arr -> arr[0];
|
||||||
private static final Function<String[], String> FUNCTION_VALUE = arr -> arr[1];
|
private static final Function<String[], String> FUNCTION_VALUE = arr -> arr[1];
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private Environment environment;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ConfigGrayMapper configGrayMapper;
|
private ConfigGrayMapper configGrayMapper;
|
||||||
|
|
||||||
|
@@ -11,7 +11,6 @@ import lombok.Data;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cloud.alibaba.nacos.NacosConfigProperties;
|
import org.springframework.cloud.alibaba.nacos.NacosConfigProperties;
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
@@ -27,10 +26,7 @@ import java.util.List;
|
|||||||
public class DbIPBlacklistManager extends DefaultIPBlacklistManager {
|
public class DbIPBlacklistManager extends DefaultIPBlacklistManager {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
IPBlacklistMapper ipBlacklistMapper;
|
private IPBlacklistMapper ipBlacklistMapper;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
Environment environment;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private NacosConfigProperties nacosConfigProperties;
|
private NacosConfigProperties nacosConfigProperties;
|
||||||
|
@@ -14,7 +14,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cloud.alibaba.nacos.NacosConfigProperties;
|
import org.springframework.cloud.alibaba.nacos.NacosConfigProperties;
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
@@ -28,10 +27,7 @@ import java.util.List;
|
|||||||
public class DbIsvManager extends CacheIsvManager {
|
public class DbIsvManager extends CacheIsvManager {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
IsvInfoMapper isvInfoMapper;
|
private IsvInfoMapper isvInfoMapper;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
Environment environment;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private NacosConfigProperties nacosConfigProperties;
|
private NacosConfigProperties nacosConfigProperties;
|
||||||
|
@@ -42,10 +42,11 @@ public class DocManagerImpl implements DocManager, ApplicationListener<Heartbeat
|
|||||||
@Override
|
@Override
|
||||||
public void addDocInfo(String serviceId, String docInfoJson) {
|
public void addDocInfo(String serviceId, String docInfoJson) {
|
||||||
String newMd5 = DigestUtils.md5DigestAsHex(docInfoJson.getBytes(StandardCharsets.UTF_8));
|
String newMd5 = DigestUtils.md5DigestAsHex(docInfoJson.getBytes(StandardCharsets.UTF_8));
|
||||||
String md5 = serviceIdMd5Map.putIfAbsent(serviceId, newMd5);
|
String oldMd5 = serviceIdMd5Map.get(serviceId);
|
||||||
if (Objects.equals(newMd5, md5)) {
|
if (Objects.equals(newMd5, oldMd5)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
serviceIdMd5Map.put(serviceId, newMd5);
|
||||||
JSONObject docRoot = JSON.parseObject(docInfoJson, Feature.OrderedField, Feature.DisableCircularReferenceDetect);
|
JSONObject docRoot = JSON.parseObject(docInfoJson, Feature.OrderedField, Feature.DisableCircularReferenceDetect);
|
||||||
DocParser docParser = this.buildDocParser(docRoot);
|
DocParser docParser = this.buildDocParser(docRoot);
|
||||||
DocInfo docInfo = docParser.parseJson(docRoot);
|
DocInfo docInfo = docParser.parseJson(docRoot);
|
||||||
|
Reference in New Issue
Block a user