mirror of
https://gitee.com/durcframework/SOP.git
synced 2025-08-11 21:57:56 +08:00
支持预发布
This commit is contained in:
@@ -10,6 +10,7 @@ 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.ServiceInfoVo;
|
||||
import com.gitee.sop.adminserver.api.service.result.ServiceInstanceVO;
|
||||
import com.gitee.sop.adminserver.bean.MetadataEnum;
|
||||
import com.gitee.sop.adminserver.bean.ServiceRouteInfo;
|
||||
import com.gitee.sop.adminserver.bean.ZookeeperContext;
|
||||
import com.gitee.sop.adminserver.common.BizException;
|
||||
@@ -144,6 +145,9 @@ public class ServiceApi {
|
||||
int id = idGen.getAndIncrement();
|
||||
instanceVO.setId(id);
|
||||
instanceVO.setParentId(pid);
|
||||
if (instanceVO.getMetadata() == null) {
|
||||
instanceVO.setMetadata(Collections.emptyMap());
|
||||
}
|
||||
serviceInfoVoList.add(instanceVO);
|
||||
}
|
||||
});
|
||||
@@ -152,21 +156,57 @@ public class ServiceApi {
|
||||
}
|
||||
|
||||
@Api(name = "service.instance.offline")
|
||||
@ApiDocMethod(description = "服务下线")
|
||||
@ApiDocMethod(description = "服务禁用")
|
||||
void serviceOffline(ServiceInstance param) {
|
||||
try {
|
||||
registryService.offlineInstance(param);
|
||||
} catch (Exception e) {
|
||||
log.error("下线失败,param:{}", param, e);
|
||||
throw new BizException("下线失败,请查看日志");
|
||||
log.error("服务禁用失败,param:{}", param, e);
|
||||
throw new BizException("服务禁用失败,请查看日志");
|
||||
}
|
||||
}
|
||||
|
||||
@Api(name = "service.instance.online")
|
||||
@ApiDocMethod(description = "服务上线")
|
||||
@ApiDocMethod(description = "服务启用")
|
||||
void serviceOnline(ServiceInstance param) throws IOException {
|
||||
try {
|
||||
registryService.onlineInstance(param);
|
||||
} catch (Exception e) {
|
||||
log.error("服务启用失败,param:{}", param, e);
|
||||
throw new BizException("服务启用失败,请查看日志");
|
||||
}
|
||||
}
|
||||
|
||||
@Api(name = "service.instance.env.pre")
|
||||
@ApiDocMethod(description = "预发布")
|
||||
void serviceEnvPre(ServiceInstance param) throws IOException {
|
||||
try {
|
||||
MetadataEnum envPre = MetadataEnum.ENV_PRE;
|
||||
registryService.setMetadata(param, envPre.getKey(), envPre.getValue());
|
||||
} catch (Exception e) {
|
||||
log.error("预发布失败,param:{}", param, e);
|
||||
throw new BizException("预发布失败,请查看日志");
|
||||
}
|
||||
}
|
||||
|
||||
@Api(name = "service.instance.env.gray")
|
||||
@ApiDocMethod(description = "灰度发布")
|
||||
void serviceEnvGray(ServiceInstance param) throws IOException {
|
||||
try {
|
||||
MetadataEnum envPre = MetadataEnum.ENV_GRAY;
|
||||
registryService.setMetadata(param, envPre.getKey(), envPre.getValue());
|
||||
} catch (Exception e) {
|
||||
log.error("灰度发布失败,param:{}", param, e);
|
||||
throw new BizException("灰度发布失败,请查看日志");
|
||||
}
|
||||
}
|
||||
|
||||
@Api(name = "service.instance.env.online")
|
||||
@ApiDocMethod(description = "上线")
|
||||
void serviceEnvOnline(ServiceInstance param) throws IOException {
|
||||
try {
|
||||
MetadataEnum envPre = MetadataEnum.ENV_ONLINE;
|
||||
registryService.setMetadata(param, envPre.getKey(), envPre.getValue());
|
||||
} catch (Exception e) {
|
||||
log.error("上线失败,param:{}", param, e);
|
||||
throw new BizException("上线失败,请查看日志");
|
||||
|
@@ -3,6 +3,8 @@ package com.gitee.sop.adminserver.api.service.result;
|
||||
import com.gitee.easyopen.doc.annotation.ApiDocField;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
*/
|
||||
@@ -35,6 +37,9 @@ public class ServiceInstanceVO {
|
||||
@ApiDocField(description = "parentId")
|
||||
private Integer parentId;
|
||||
|
||||
@ApiDocField(description = "metadata")
|
||||
private Map<String, String> metadata;
|
||||
|
||||
public String getIpPort() {
|
||||
return ip != null && port > 0 ? ip + ":" + port : "";
|
||||
}
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package com.gitee.sop.adminserver.bean;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
*/
|
||||
public enum MetadataEnum {
|
||||
/**
|
||||
* 预发布环境
|
||||
*/
|
||||
ENV_PRE("env", "pre"),
|
||||
|
||||
/**
|
||||
* 上线环境
|
||||
*/
|
||||
ENV_ONLINE("env", ""),
|
||||
|
||||
/**
|
||||
* 灰度环境
|
||||
*/
|
||||
ENV_GRAY("env", "gray"),
|
||||
;
|
||||
private String key,value;
|
||||
|
||||
MetadataEnum(String key, String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
@@ -28,16 +28,36 @@
|
||||
label="IP端口"
|
||||
width="250"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="metadata"
|
||||
label="当前环境"
|
||||
width="100"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.metadata.env === 'pre'" type="warning">预发布</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.metadata.env === 'gray'" type="info">灰度</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && !scope.row.metadata.env" type="success">线上</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="metadata"
|
||||
label="metadata"
|
||||
width="250"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.parentId > 0">{{ JSON.stringify(scope.row.metadata) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="服务状态"
|
||||
width="100"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'UP'" type="success">已上线</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'UP'" type="success">已启用</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'STARTING'" type="info">正在启动</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'UNKNOWN'">未知</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && (scope.row.status === 'OUT_OF_SERVICE' || scope.row.status === 'DOWN')" type="danger">已下线</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && (scope.row.status === 'OUT_OF_SERVICE' || scope.row.status === 'DOWN')" type="danger">已禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -47,11 +67,14 @@
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="100"
|
||||
width="200"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="scope.row.parentId > 0 && scope.row.status === 'UP'" type="text" size="mini" @click="onOffline(scope.row)">下线</el-button>
|
||||
<el-button v-if="scope.row.parentId > 0 && scope.row.status === 'OUT_OF_SERVICE'" type="text" size="mini" @click="onOnline(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="onEnvGray(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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -103,22 +126,46 @@ export default {
|
||||
onSearchTable: function() {
|
||||
this.loadTable()
|
||||
},
|
||||
onOffline: function(row) {
|
||||
this.confirm('确定要下线【' + row.serviceId + '】吗?', function(done) {
|
||||
onDisable: function(row) {
|
||||
this.confirm('确定要禁用【' + row.serviceId + '】吗?', function(done) {
|
||||
this.post('service.instance.offline', row, function() {
|
||||
this.tip('下线成功')
|
||||
done()
|
||||
})
|
||||
})
|
||||
},
|
||||
onOnline: function(row) {
|
||||
this.confirm('确定要上线【' + row.serviceId + '】吗?', function(done) {
|
||||
onEnable: function(row) {
|
||||
this.confirm('确定要启用【' + row.serviceId + '】吗?', function(done) {
|
||||
this.post('service.instance.online', row, function() {
|
||||
this.tip('上线成功')
|
||||
done()
|
||||
})
|
||||
})
|
||||
},
|
||||
onEnvOnline: function(row) {
|
||||
this.confirm('确定要上线【' + row.serviceId + '】吗?', function(done) {
|
||||
this.post('service.instance.env.online', row, function() {
|
||||
this.tip('上线成功')
|
||||
done()
|
||||
})
|
||||
})
|
||||
},
|
||||
onEnvPre: function(row) {
|
||||
this.confirm('确定要预发布【' + row.serviceId + '】吗?', function(done) {
|
||||
this.post('service.instance.env.pre', row, function() {
|
||||
this.tip('预发布成功')
|
||||
done()
|
||||
})
|
||||
})
|
||||
},
|
||||
onEnvGray: function(row) {
|
||||
this.confirm('确定要灰度发布【' + row.serviceId + '】吗?', function(done) {
|
||||
this.post('service.instance.env.gray', row, function() {
|
||||
this.tip('灰度发布发成功')
|
||||
done()
|
||||
})
|
||||
})
|
||||
},
|
||||
renderServiceName: function(row) {
|
||||
let instanceCount = ''
|
||||
if (row.children && row.children.length > 0) {
|
||||
@@ -128,6 +175,14 @@ export default {
|
||||
instanceCount = ` (${onlineCount}/${row.children.length})`
|
||||
}
|
||||
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 />')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -54,4 +54,5 @@ public class SopConstants {
|
||||
public static final String UNKNOWN_SERVICE= "_sop_unknown_service_";
|
||||
public static final String UNKNOWN_METHOD = "_sop_unknown_method_";
|
||||
public static final String UNKNOWN_VERSION = "_sop_unknown_version_";
|
||||
|
||||
}
|
||||
|
@@ -69,7 +69,7 @@ public class GatewayResultExecutor extends BaseExecutorAdapter<ServerWebExchange
|
||||
}
|
||||
|
||||
if (error == null) {
|
||||
error = ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getError();
|
||||
error = ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta().getError();
|
||||
}
|
||||
|
||||
JSONObject jsonObject = (JSONObject) JSON.toJSON(error);
|
||||
|
@@ -84,7 +84,7 @@ public class ReadBodyRoutePredicateFactory extends AbstractRoutePredicateFactory
|
||||
return Mono.just(test);
|
||||
} catch (ClassCastException e) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Predicate test failed because class in predicate does not match the cached body object",
|
||||
LOGGER.debug("Predicate test failed because class in predicate does not canVisit the cached body object",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
@@ -9,9 +9,7 @@ public enum ErrorEnum {
|
||||
SUCCESS(Codes.CODE_SUCCESS, ""),
|
||||
|
||||
/** 服务暂不可用 */
|
||||
ISP_UNKNOW_ERROR(Codes.CODE_UNKNOW, "isp.unknow-error"),
|
||||
/** 服务暂不可用 */
|
||||
AOP_UNKNOW_ERROR(Codes.CODE_UNKNOW, "aop.unknow-error"),
|
||||
ISP_UNKNOWN_ERROR(Codes.CODE_UNKNOW, "isp.unknown-error"),
|
||||
/** 服务不可用,路由被禁用 */
|
||||
ISP_API_DISABLED(Codes.CODE_UNKNOW, "isp.service-not-available"),
|
||||
|
||||
|
@@ -76,7 +76,7 @@ public class ErrorFactory {
|
||||
// open.error_20000=Service is temporarily unavailable
|
||||
String msg = getErrorMessage(modulePrefix + code, locale);
|
||||
String subCode = errorMeta.getSubCode();
|
||||
// open.error_20000_isp.unknow-error=Service is temporarily unavailable
|
||||
// open.error_20000_isp.unknown-error=Service is temporarily unavailable
|
||||
String subMsg = getErrorMessage(modulePrefix + code + UNDERLINE + subCode, locale, params);
|
||||
if (StringUtils.isEmpty(msg)) {
|
||||
msg = SYS_ERR;
|
||||
@@ -85,7 +85,7 @@ public class ErrorFactory {
|
||||
subMsg = SYS_ERR;
|
||||
}
|
||||
// solution暂未实现,如果要实现,可以这样配置:
|
||||
// open.error_20000_isp.unknow-error_solution=Service is temporarily unavailable
|
||||
// open.error_20000_isp.unknown-error_solution=Service is temporarily unavailable
|
||||
// <code>String solution = getErrorMessage(modulePrefix + code + UNDERLINE + subCode + "_solution", locale, params);</code>
|
||||
error = new ErrorImpl(code, msg, subCode, subMsg, null);
|
||||
errorCache.put(key, error);
|
||||
|
@@ -51,7 +51,7 @@ public abstract class BaseParamBuilder<T> implements ParamBuilder<T> {
|
||||
RouteRepository<? extends TargetRoute> routeRepository = RouteRepositoryContext.getRouteRepository();
|
||||
if (routeRepository == null) {
|
||||
log.error("RouteRepositoryContext.setRouteRepository()方法未使用");
|
||||
throw ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getException();
|
||||
throw ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta().getException();
|
||||
}
|
||||
|
||||
String nameVersion = Optional.ofNullable(apiParam.fetchNameVersion()).orElse(String.valueOf(System.currentTimeMillis()));
|
||||
|
@@ -35,7 +35,7 @@ import java.util.Optional;
|
||||
@Slf4j
|
||||
public abstract class BaseExecutorAdapter<T, R> implements ResultExecutor<T, R> {
|
||||
private static final ErrorMeta SUCCESS_META = ErrorEnum.SUCCESS.getErrorMeta();
|
||||
private static final ErrorMeta ISP_UNKNOW_ERROR_META = ErrorEnum.ISP_UNKNOW_ERROR.getErrorMeta();
|
||||
private static final ErrorMeta ISP_UNKNOW_ERROR_META = ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta();
|
||||
private static final ErrorMeta ISP_BIZ_ERROR = ErrorEnum.BIZ_ERROR.getErrorMeta();
|
||||
|
||||
public static final String GATEWAY_CODE_NAME = "code";
|
||||
|
@@ -6,7 +6,7 @@ package com.gitee.sop.gatewaycommon.result;
|
||||
* "result": {
|
||||
* "code": "20000",
|
||||
* "msg": "Service Currently Unavailable",
|
||||
* "sub_code": "isp.unknow-error",
|
||||
* "sub_code": "isp.unknown-error",
|
||||
* "sub_msg": "系统繁忙"
|
||||
* },
|
||||
* "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
|
||||
|
@@ -8,7 +8,7 @@ package com.gitee.sop.gatewaycommon.result;
|
||||
* "alipay_trade_order_settle_response": {
|
||||
* "code": "20000",
|
||||
* "msg": "Service Currently Unavailable",
|
||||
* "sub_code": "isp.unknow-error",
|
||||
* "sub_code": "isp.unknown-error",
|
||||
* "sub_msg": "系统繁忙"
|
||||
* },
|
||||
* "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
|
||||
|
@@ -22,7 +22,7 @@ package com.gitee.sop.gatewaycommon.result;
|
||||
* "alipay_trade_fastpay_refund_query_response": {
|
||||
* "code": "20000",
|
||||
* "msg": "Service Currently Unavailable",
|
||||
* "sub_code": "isp.unknow-error",
|
||||
* "sub_code": "isp.unknown-error",
|
||||
* "sub_msg": "系统繁忙"
|
||||
* },
|
||||
* "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
|
||||
|
@@ -36,7 +36,7 @@ public class ApiSessionManager implements SessionManager {
|
||||
return cache.get(sessionId);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
throw ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getException();
|
||||
throw ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta().getException();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,72 @@
|
||||
package com.gitee.sop.gatewaycommon.zuul.loadbalancer;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.netflix.loadbalancer.ILoadBalancer;
|
||||
import com.netflix.loadbalancer.Server;
|
||||
import com.netflix.loadbalancer.ZoneAvoidanceRule;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 服务实例选择器
|
||||
*
|
||||
* @author tanghc
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class BaseServerChooser extends ZoneAvoidanceRule {
|
||||
|
||||
/**
|
||||
* 是否匹配对应的服务器,可在此判断是否是预发布,灰度环境
|
||||
*
|
||||
* @param server 指定服务器
|
||||
* @return 返回true:是
|
||||
*/
|
||||
protected abstract boolean match(Server server);
|
||||
|
||||
/**
|
||||
* 客户端能否够访问服务器
|
||||
*
|
||||
* @param server 服务器实例
|
||||
* @param request request
|
||||
* @return 返回true:能访问
|
||||
*/
|
||||
protected abstract boolean canVisit(Server server, HttpServletRequest request);
|
||||
|
||||
@Override
|
||||
public Server choose(Object key) {
|
||||
ILoadBalancer lb = getLoadBalancer();
|
||||
// 获取服务实例列表
|
||||
List<Server> allServers = new ArrayList<>(lb.getAllServers());
|
||||
int index = -1;
|
||||
for (int i = 0; i < allServers.size(); i++) {
|
||||
Server server = allServers.get(i);
|
||||
if (match(server)) {
|
||||
index = i;
|
||||
if (canVisit(server, RequestContext.getCurrentContext().getRequest())) {
|
||||
return server;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 调用默认的算法
|
||||
// 如果选出了特殊环境服务器,需要移除命中的服务器
|
||||
if (index > -1) {
|
||||
allServers.remove(index);
|
||||
}
|
||||
if (CollectionUtils.isEmpty(allServers)) {
|
||||
log.error("无可用服务实例,key:", key);
|
||||
return null;
|
||||
}
|
||||
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(allServers, key);
|
||||
if (server.isPresent()) {
|
||||
return server.get();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -76,7 +76,7 @@ public class ZuulResultExecutor extends BaseExecutorAdapter<RequestContext, Stri
|
||||
}
|
||||
}
|
||||
if (error == null) {
|
||||
error = ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getError();
|
||||
error = ErrorEnum.ISP_UNKNOWN_ERROR.getErrorMeta().getError();
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
@@ -6,6 +6,45 @@ import org.springframework.util.CollectionUtils;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {
|
||||
* "instance": {
|
||||
* "instanceId": "demo-order2:11101",
|
||||
* "app": "demo-order2",
|
||||
* "appGroutName": null,
|
||||
* "ipAddr": "127.0.0.1",
|
||||
* "sid": "na",
|
||||
* "homePageUrl": null,
|
||||
* "statusPageUrl": null,
|
||||
* "healthCheckUrl": null,
|
||||
* "secureHealthCheckUrl": null,
|
||||
* "vipAddress": "demo-order2",
|
||||
* "secureVipAddress": "demo-order2",
|
||||
* "countryId": 1,
|
||||
* "dataCenterInfo": {
|
||||
* "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
|
||||
* "name": "MyOwn"
|
||||
* },
|
||||
* "hostName": "127.0.0.1",
|
||||
* "status": "UP",
|
||||
* "leaseInfo": null,
|
||||
* "isCoordinatingDiscoveryServer": false,
|
||||
* "lastUpdatedTimestamp": 1529391461000,
|
||||
* "lastDirtyTimestamp": 1529391461000,
|
||||
* "actionType": null,
|
||||
* "asgName": null,
|
||||
* "overridden_status": "UNKNOWN",
|
||||
* "port": {
|
||||
* "$": 11102,
|
||||
* "@enabled": "false"
|
||||
* },
|
||||
* "securePort": {
|
||||
* "$": 7002,
|
||||
* "@enabled": "false"
|
||||
* },
|
||||
* "metadata": {
|
||||
* "@class": "java.util.Collections$EmptyMap"
|
||||
* }* }
|
||||
* }
|
||||
* @author tanghc
|
||||
*/
|
||||
@Data
|
||||
@@ -19,6 +58,8 @@ public class EurekaInstance {
|
||||
private String healthCheckUrl;
|
||||
private String lastUpdatedTimestamp;
|
||||
|
||||
private Map<String, String> metadata;
|
||||
|
||||
public String fetchPort() {
|
||||
if (CollectionUtils.isEmpty(port)) {
|
||||
return "";
|
||||
|
@@ -16,6 +16,10 @@ public enum EurekaUri {
|
||||
* 查询所有实例 Query for all instances
|
||||
*/
|
||||
QUERY_APPS("GET", "/apps"),
|
||||
/**
|
||||
* 查询一个实例
|
||||
*/
|
||||
QUERY_INSTANCES("GET", "/instances/%s"),
|
||||
/**
|
||||
* 下线 Take instance out of service
|
||||
*/
|
||||
@@ -24,6 +28,12 @@ public enum EurekaUri {
|
||||
* 上线 Move instance back into service (remove override)
|
||||
*/
|
||||
ONLINE_SERVICE("DELETE", "/apps/%s/%s/status?value=UP"),
|
||||
/**
|
||||
* 设置metadata信息
|
||||
*
|
||||
* /apps/{appID}/{instanceID}/metadata?key=value
|
||||
*/
|
||||
SET_METADATA("PUT", "/apps/%s/%s/metadata?%s=%s")
|
||||
;
|
||||
public static final String URL_PREFIX = "/";
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.gitee.sop.websiteserver.bean;
|
||||
package com.gitee.sop.registryapi.bean;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
@@ -110,7 +110,7 @@ public class HttpTool {
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public String request(String url, Map<String, String> form, Map<String, String> header, HTTPMethod method) throws IOException {
|
||||
public String request(String url, Map<String, ?> form, Map<String, String> header, HTTPMethod method) throws IOException {
|
||||
Request.Builder requestBuilder = buildRequestBuilder(url, form, method.value());
|
||||
// 添加header
|
||||
addHeader(requestBuilder, header);
|
||||
@@ -153,7 +153,7 @@ public class HttpTool {
|
||||
}
|
||||
}
|
||||
|
||||
public static Request.Builder buildRequestBuilder(String url, Map<String, String> form, String method) {
|
||||
public static Request.Builder buildRequestBuilder(String url, Map<String, ?> form, String method) {
|
||||
switch (method) {
|
||||
case "get":
|
||||
return new Request.Builder()
|
||||
@@ -178,18 +178,18 @@ public class HttpTool {
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpUrl buildHttpUrl(String url, Map<String, String> form) {
|
||||
public static HttpUrl buildHttpUrl(String url, Map<String, ?> form) {
|
||||
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
|
||||
for (Map.Entry<String, String> entry : form.entrySet()) {
|
||||
urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
|
||||
for (Map.Entry<String, ?> entry : form.entrySet()) {
|
||||
urlBuilder.addQueryParameter(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
return urlBuilder.build();
|
||||
}
|
||||
|
||||
public static FormBody buildFormBody(Map<String, String> form) {
|
||||
public static FormBody buildFormBody(Map<String, ?> form) {
|
||||
FormBody.Builder paramBuilder = new FormBody.Builder(StandardCharsets.UTF_8);
|
||||
for (Map.Entry<String, String> entry : form.entrySet()) {
|
||||
paramBuilder.add(entry.getKey(), entry.getValue());
|
||||
for (Map.Entry<String, ?> entry : form.entrySet()) {
|
||||
paramBuilder.add(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
return paramBuilder.build();
|
||||
}
|
||||
@@ -262,6 +262,9 @@ public class HttpTool {
|
||||
HEAD,
|
||||
DELETE;
|
||||
|
||||
private HTTPMethod() {
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return this.name();
|
||||
}
|
@@ -2,6 +2,9 @@ package com.gitee.sop.registryapi.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author tanghc
|
||||
*/
|
||||
@@ -37,4 +40,9 @@ public class ServiceInstance {
|
||||
*/
|
||||
private String updateTime;
|
||||
|
||||
/**
|
||||
* user extended attributes
|
||||
*/
|
||||
private Map<String, String> metadata = new HashMap<String, String>();
|
||||
|
||||
}
|
@@ -38,4 +38,13 @@ public interface RegistryService {
|
||||
void offlineInstance(ServiceInstance serviceInstance) throws Exception;
|
||||
|
||||
|
||||
/**
|
||||
* 设置实例元数据
|
||||
*
|
||||
* @param serviceInstance 实例
|
||||
* @param key key
|
||||
* @param value 值
|
||||
* @throws Exception 设置实例元数据失败抛出异常
|
||||
*/
|
||||
void setMetadata(ServiceInstance serviceInstance, String key, String value) throws Exception;
|
||||
}
|
||||
|
@@ -60,6 +60,7 @@ public class RegistryServiceEureka implements RegistryService {
|
||||
serviceInstance.setStatus(eurekaInstance.getStatus());
|
||||
Date updateTime = new Date(Long.valueOf(eurekaInstance.getLastUpdatedTimestamp()));
|
||||
serviceInstance.setUpdateTime(DateFormatUtils.format(updateTime, TIMESTAMP_PATTERN));
|
||||
serviceInstance.setMetadata(eurekaInstance.getMetadata());
|
||||
serviceInfo.getInstances().add(serviceInstance);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +81,11 @@ public class RegistryServiceEureka implements RegistryService {
|
||||
this.requestEurekaServer(EurekaUri.OFFLINE_SERVICE, serviceInstance.getServiceId(), serviceInstance.getInstanceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMetadata(ServiceInstance serviceInstance, String key, String value) throws Exception {
|
||||
this.requestEurekaServer(EurekaUri.SET_METADATA, serviceInstance.getServiceId(), serviceInstance.getInstanceId(), key, value);
|
||||
}
|
||||
|
||||
private String requestEurekaServer(EurekaUri eurekaUri, String... args) throws IOException {
|
||||
Request request = eurekaUri.getRequest(this.eurekaUrl, args);
|
||||
Response response = client.newCall(request).execute();
|
||||
|
@@ -1,17 +1,17 @@
|
||||
package com.gitee.sop.registryapi.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.naming.NamingFactory;
|
||||
import com.alibaba.nacos.api.naming.NamingService;
|
||||
import com.alibaba.nacos.api.naming.pojo.Instance;
|
||||
import com.alibaba.nacos.api.naming.pojo.ListView;
|
||||
import com.gitee.sop.registryapi.bean.HttpTool;
|
||||
import com.gitee.sop.registryapi.bean.ServiceInfo;
|
||||
import com.gitee.sop.registryapi.bean.ServiceInstance;
|
||||
import com.gitee.sop.registryapi.service.RegistryService;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -24,13 +24,15 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* nacos接口实现
|
||||
* nacos接口实现, https://nacos.io/zh-cn/docs/open-api.html
|
||||
* @author tanghc
|
||||
*/
|
||||
public class RegistryServiceNacos implements RegistryService {
|
||||
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
|
||||
static HttpTool httpTool = new HttpTool();
|
||||
|
||||
@Value("${registry.nacos-server-addr:}")
|
||||
private String nacosAddr;
|
||||
|
||||
@@ -63,6 +65,7 @@ public class RegistryServiceNacos implements RegistryService {
|
||||
boolean isOnline = instance.getWeight() > 0;
|
||||
String status = isOnline ? "UP" : "OUT_OF_SERVICE";
|
||||
serviceInstance.setStatus(status);
|
||||
serviceInstance.setMetadata(instance.getMetadata());
|
||||
serviceInfo.getInstances().add(serviceInstance);
|
||||
}
|
||||
}
|
||||
@@ -73,34 +76,50 @@ public class RegistryServiceNacos implements RegistryService {
|
||||
|
||||
@Override
|
||||
public void onlineInstance(ServiceInstance serviceInstance) throws Exception {
|
||||
Map<String, String> params = new HashMap<>(8);
|
||||
// 查询实例
|
||||
Instance instance = this.getInstance(serviceInstance);
|
||||
// 上线,把权重设置成1
|
||||
params.put("weight", "1");
|
||||
this.updateInstance(serviceInstance, params);
|
||||
instance.setWeight(1);
|
||||
this.updateInstance(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offlineInstance(ServiceInstance serviceInstance) throws Exception {
|
||||
Map<String, String> params = new HashMap<>(8);
|
||||
// 查询实例
|
||||
Instance instance = this.getInstance(serviceInstance);
|
||||
// 下线,把权重设置成0
|
||||
params.put("weight", "0");
|
||||
this.updateInstance(serviceInstance, params);
|
||||
instance.setWeight(0);
|
||||
this.updateInstance(instance);
|
||||
}
|
||||
|
||||
private Response updateInstance(ServiceInstance serviceInstance, Map<String, String> params) throws IOException {
|
||||
FormBody.Builder builder = new FormBody.Builder();
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
builder.add(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
@Override
|
||||
public void setMetadata(ServiceInstance serviceInstance, String key, String value) throws Exception {
|
||||
// 查询实例
|
||||
Instance instance = this.getInstance(serviceInstance);
|
||||
// 设置metadata
|
||||
Map<String, String> metadata = instance.getMetadata();
|
||||
metadata.put(key, value);
|
||||
this.updateInstance(instance);
|
||||
}
|
||||
builder.add("serviceName", serviceInstance.getServiceId())
|
||||
.add("ip", serviceInstance.getIp())
|
||||
.add("port", String.valueOf(serviceInstance.getPort()));
|
||||
FormBody formBody = builder.build();
|
||||
final Request request = new Request.Builder()
|
||||
.url("http://" + nacosAddr + "/nacos/v1/ns/instance")
|
||||
.put(formBody)
|
||||
.build();
|
||||
|
||||
return client.newCall(request).execute();
|
||||
protected void updateInstance(Instance instance) throws IOException {
|
||||
String json = JSON.toJSONString(instance);
|
||||
JSONObject jsonObject = JSON.parseObject(json);
|
||||
httpTool.request("http://" + nacosAddr + "/nacos/v1/ns/instance", jsonObject, null, HttpTool.HTTPMethod.PUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询实例
|
||||
* @param serviceInstance 实例信息
|
||||
* @return 返回nacos实例
|
||||
* @throws IOException 查询异常
|
||||
*/
|
||||
protected Instance getInstance(ServiceInstance serviceInstance) throws IOException {
|
||||
Map<String, String> params = new HashMap<>(8);
|
||||
params.put("serviceName", serviceInstance.getServiceId());
|
||||
params.put("ip", serviceInstance.getIp());
|
||||
params.put("port", String.valueOf(serviceInstance.getPort()));
|
||||
String instanceJson = httpTool.request("http://" + nacosAddr + "/nacos/v1/ns/instance", params, null, HttpTool.HTTPMethod.GET);
|
||||
return JSON.parseObject(instanceJson, Instance.class);
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ package com.gitee.sop.servercommon.message;
|
||||
*/
|
||||
public enum ServiceErrorEnum {
|
||||
/** 系统繁忙 */
|
||||
ISP_UNKNOW_ERROR("isp.unknow-error"),
|
||||
ISP_UNKNOW_ERROR("isp.unknown-error"),
|
||||
/** 参数错误 */
|
||||
ISV_PARAM_ERROR("isv.invalid-parameter"),
|
||||
/** 通用错误 */
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# 错误配置
|
||||
|
||||
isp.error_isp.unknow-error=Service is temporarily unavailable
|
||||
isp.error_isp.unknown-error=Service is temporarily unavailable
|
||||
isp.error_isv.invalid-parameter=Invalid parameter
|
@@ -1,5 +1,5 @@
|
||||
# 错误配置
|
||||
|
||||
isp.error_isp.unknow-error=\u670d\u52a1\u6682\u4e0d\u53ef\u7528
|
||||
isp.error_isp.unknown-error=\u670d\u52a1\u6682\u4e0d\u53ef\u7528
|
||||
# 参数无效
|
||||
isp.error_isv.invalid-parameter=\u53c2\u6570\u65e0\u6548
|
||||
|
@@ -6,8 +6,11 @@ package com.gitee.sop.gateway.config;
|
||||
* 注意:下面两个只能使用一个
|
||||
*/
|
||||
|
||||
import com.gitee.sop.gateway.loadbalancer.SopPropertiesFactory;
|
||||
import com.gitee.sop.gateway.manager.ManagerInitializer;
|
||||
import com.gitee.sop.gatewaycommon.zuul.configuration.AlipayZuulConfiguration;
|
||||
import org.springframework.cloud.netflix.ribbon.PropertiesFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
@@ -21,6 +24,11 @@ public class ZuulConfig extends AlipayZuulConfiguration {
|
||||
new ManagerInitializer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PropertiesFactory propertiesFactory() {
|
||||
return new SopPropertiesFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,55 @@
|
||||
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,39 @@
|
||||
package com.gitee.sop.gateway.loadbalancer;
|
||||
|
||||
import com.netflix.loadbalancer.IRule;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.netflix.ribbon.PropertiesFactory;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* 自定义PropertiesFactory,用来动态添加LoadBalance规则
|
||||
* @author tanghc
|
||||
*/
|
||||
public class SopPropertiesFactory extends PropertiesFactory {
|
||||
|
||||
/**
|
||||
* 可在配置文件中设置<code>zuul.custom-rule-classname=com.xx.ClassName</code>指定负载均衡规则类
|
||||
* 默认使用com.gitee.sop.gateway.loadbalancer.PreEnvironmentServerChooser
|
||||
*/
|
||||
private static final String PROPERTIES_KEY = "zuul.custom-rule-classname";
|
||||
|
||||
private static final String CUSTOM_RULE_CLASSNAME = PreEnvironmentServerChooser.class.getName();
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
/**
|
||||
* 配置文件配置:<serviceId>.ribbon.NFLoadBalancerRuleClassName=com.gitee.sop.gateway.loadbalancer.PreEnvironmentServerChooser
|
||||
* @param clazz
|
||||
* @param name serviceId
|
||||
* @return 返回class全限定名
|
||||
*/
|
||||
@Override
|
||||
public String getClassName(Class clazz, String name) {
|
||||
if (clazz == IRule.class) {
|
||||
return this.environment.getProperty(PROPERTIES_KEY, CUSTOM_RULE_CLASSNAME);
|
||||
} else {
|
||||
return super.getClassName(clazz, name);
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ package com.gitee.sop.sdk.common;
|
||||
* "result": {
|
||||
* "code": "20000",
|
||||
* "msg": "Service Currently Unavailable",
|
||||
* "sub_code": "isp.unknow-error",
|
||||
* "sub_code": "isp.unknown-error",
|
||||
* "sub_msg": "系统繁忙"
|
||||
* },
|
||||
* "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
|
||||
|
@@ -8,7 +8,7 @@ package com.gitee.sop.sdk.common;
|
||||
* "alipay_trade_order_settle_response": {
|
||||
* "code": "20000",
|
||||
* "msg": "Service Currently Unavailable",
|
||||
* "sub_code": "isp.unknow-error",
|
||||
* "sub_code": "isp.unknown-error",
|
||||
* "sub_msg": "系统繁忙"
|
||||
* },
|
||||
* "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
|
||||
|
@@ -11,7 +11,7 @@ import lombok.Setter;
|
||||
* "alipay_trade_close_response": {
|
||||
* "code": "20000",
|
||||
* "msg": "Service Currently Unavailable",
|
||||
* "sub_code": "isp.unknow-error",
|
||||
* "sub_code": "isp.unknown-error",
|
||||
* "sub_msg": "系统繁忙"
|
||||
* },
|
||||
* "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
|
||||
|
@@ -110,7 +110,7 @@ public class HttpTool {
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public String request(String url, Map<String, String> form, Map<String, String> header, HTTPMethod method) throws IOException {
|
||||
public String request(String url, Map<String, ?> form, Map<String, String> header, HTTPMethod method) throws IOException {
|
||||
Request.Builder requestBuilder = buildRequestBuilder(url, form, method.value());
|
||||
// 添加header
|
||||
addHeader(requestBuilder, header);
|
||||
@@ -153,7 +153,7 @@ public class HttpTool {
|
||||
}
|
||||
}
|
||||
|
||||
public static Request.Builder buildRequestBuilder(String url, Map<String, String> form, String method) {
|
||||
public static Request.Builder buildRequestBuilder(String url, Map<String, ?> form, String method) {
|
||||
switch (method) {
|
||||
case "get":
|
||||
return new Request.Builder()
|
||||
@@ -178,18 +178,18 @@ public class HttpTool {
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpUrl buildHttpUrl(String url, Map<String, String> form) {
|
||||
public static HttpUrl buildHttpUrl(String url, Map<String, ?> form) {
|
||||
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
|
||||
for (Map.Entry<String, String> entry : form.entrySet()) {
|
||||
urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
|
||||
for (Map.Entry<String, ?> entry : form.entrySet()) {
|
||||
urlBuilder.addQueryParameter(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
return urlBuilder.build();
|
||||
}
|
||||
|
||||
public static FormBody buildFormBody(Map<String, String> form) {
|
||||
public static FormBody buildFormBody(Map<String, ?> form) {
|
||||
FormBody.Builder paramBuilder = new FormBody.Builder(StandardCharsets.UTF_8);
|
||||
for (Map.Entry<String, String> entry : form.entrySet()) {
|
||||
paramBuilder.add(entry.getKey(), entry.getValue());
|
||||
for (Map.Entry<String, ?> entry : form.entrySet()) {
|
||||
paramBuilder.add(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
return paramBuilder.build();
|
||||
}
|
||||
|
@@ -141,7 +141,7 @@
|
||||
<div>服务不可用</div>
|
||||
</td>
|
||||
<td rowspan="1" colspan="1">
|
||||
<div>isp.unknow-error</div>
|
||||
<div>isp.unknown-error</div>
|
||||
</td>
|
||||
<td rowspan="1" colspan="1">
|
||||
<div>服务暂不可用(业务系统不可用)</div>
|
||||
|
@@ -93,7 +93,7 @@ function createResponseCode(docItem) {
|
||||
' "'+method+'_response": {\n' +
|
||||
' "code": "20000",\n' +
|
||||
' "msg": "Service is temporarily unavailable",\n' +
|
||||
' "sub_code": "isp.unknow-error",\n' +
|
||||
' "sub_code": "isp.unknown-error",\n' +
|
||||
' "sub_msg": "服务暂不可用"\n' +
|
||||
' }' +
|
||||
'}';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.gitee.sop.websiteserver.controller;
|
||||
|
||||
import com.gitee.sop.websiteserver.bean.HttpTool;
|
||||
import com.gitee.sop.websiteserver.bean.HttpTool.*;
|
||||
import com.gitee.sop.registryapi.bean.HttpTool;
|
||||
import com.gitee.sop.registryapi.bean.HttpTool.*;
|
||||
import com.gitee.sop.websiteserver.sign.AlipayApiException;
|
||||
import com.gitee.sop.websiteserver.sign.AlipaySignature;
|
||||
import com.gitee.sop.websiteserver.util.UploadUtil;
|
||||
|
Reference in New Issue
Block a user