网关服务GateWay

一:什么是SpringCloud gateWay

Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul 网关。网关作为流量的,在微服务系统中有着非常作用。据说性能是第一代网关 zuul的1.5倍。(基于Netty,WebFlux), 注意点:由于不是Sevlet容器,所以他不能打成war包, 只支持SpringBoot2.X不 支持1.x

1.1)网关作用: 网关常见的功能有路由转发、权限校验、限流控制等作用。

1.2)为什么要使用SpringCloudGateWay。

①:没有网关

②:使用了网关

二:搭建SpringCloudGateWay

2.1)创建一个gateWay的工程08-ms-cloud-gateway

①添加依赖:

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring‐cloud‐starter‐gateway</artifactId>
</dependency>

<!‐‐加入nacos的依赖‐‐>
<dependency>
<groupId>com.alibaba.cloud</groupId>
 <artifactId>spring‐cloud‐alibaba‐nacos‐discovery</artifactId>
 </dependency>


 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring‐boot‐starter‐actuator</artifactId>
 </dependency>

②:写配置文件

#规划GateWay的服务端口
server:
port: 8888
#规划gateWay注册到到nacos上的服务应用名称
spring:
application:
name: api‐gateway
cloud:
nacos:
 discovery:
 #gateway工程注册到nacos上的地址
 server‐addr: localhost:8848
 gateway:
 discovery:
 locator:
 #开启gateway从nacos上获取服务列表
 enabled: true
 #开启acutor端点
 management:
 endpoints:
 web:
 exposure:
 include: '*'
 endpoint:
 health:
 #打开端点详情
 show‐details: always

③:写注解 服务发现的注解,gateway没有注解

@SpringBootApplication
@EnableDiscoveryClient
public class MsCloudGatewayApplication {

public static void main(String[] args) {
SpringApplication.run(MsCloudGatewayApplication .class, args);
}
}

2.2)测试网关工程,分别启动

08-ms-cloud-gateway(8888),

08-ms-alibaba-gateway-order(8080)

08-ms-alibaba-gateway-product(8084)

通过网关地址访问订单微服务

http://localhost:8888/order-center/selectOrderInfoById/1

通过网关地址访问库存微服务

http://localhost:8888/product-center/selectProductInfoById/1

转发规则:

 

三:GateWay的核心概念 

3.1)基本核心概念.

路由网关的基本构建模块,它是由ID、目标URl、断言集合和过滤器集合定义, 如果集合断言为真,则匹配路由。

Predicate(断言):这是java 8的一个函数式接口predicate,可以用于lambda表 达式和方法引用,输入类型是:Spring Framework ServerWebExchange,允许 开发人员匹配来自HTTP请求的任何内容,例如请求头headers和参数paramers

Filter(过滤器):这些是使用特定工厂构建的Spring Framework GatewayFilter 实例,这里可以在发送下游请求之前或之后修改请求和响应

如下配置:

含义:我们浏览器 http://localhost:8888/projects/** 都会转发到 http://spring.io/projects/**下 并且带入响应头部: X-Response-Foo=Bar

spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://spring.io
predicates:
 - Path=/projects/**
 filters:
 - AddResponseHeader=X-Response-Foo, Bar

3.2)路由断言工厂 

https://cloud.spring.io/spring-cloud-gateway/2.1.x/multi/multi_spring-cloud-gateway.html

3.3)自定义谓词工厂 

第一步:写一个自定义谓词工厂,类名必须要以RoutePredicateFactory结尾 然后继承AbstractRoutePredicateFactory

@Component
@Slf4j
public class MyTimeBetweenRoutePredicateFactory extends AbstractRoute
PredicateFactory<TulingTimeBetweenConfig> {

public MyTimeBetweenRoutePredicateFactory() {
super(MyTimeBetweenConfig.class);
}

//真正的业务判断逻辑
 @Override
 public Predicate<ServerWebExchange> apply(MyTimeBetweenConfig confi
 {

 LocalTime startTime = config.getStartTime();

 LocalTime endTime = config.getEndTime();

 return new Predicate<ServerWebExchange>(){
 @Override
 public boolean test(ServerWebExchange serverWebExchange) {
 LocalTime now = LocalTime.now();
 //判断当前时间是否在在配置的时间范围类
 return now.isAfter(startTime) && now.isBefore(endTime);
 }
 };

 }

 //用于接受yml中的配置 ‐ MyTimeBetween=上午7:00,下午11:00
 public List<String> shortcutFieldOrder() {
 return Arrays.asList("startTime", "endTime");
 }

 }

第二步:书写一个配置类,用于接受配置

//写一个类用于接受配置
@Data
public class MyTimeBetweenConfig {

private LocalTime startTime;

private LocalTime endTime;

}

第三步:在yml配置中

谓词配置是以我们自定义类名MYTimeBetweenRoutePredicateFactory 去除了RoutePredicateFactory接受开头MyTimeBetween

spring:
cloud:
gateway:
routes:
- id: my-timeBetween #id必须要唯一
uri: lb://product-center
predicates:
#当前请求的时间必须在早上7点到 晚上11点 http://localhost:8888/selectProduct
InfoById/1
#才会被转发
 #到http://product-center/selectProductInfoById/1
 - TulingTimeBetween=上午7:00,下午11:00

3.4)过滤器工厂,SpringCloudGateway 内置了很多的过滤器工厂,我 们通过一些过滤器工厂可以进行一些业务逻辑处理器,比如添加剔除响 应头,添加去除参数等.

https://cloud.spring.io/spring-cloud-gateway/2.1.x/multi/multi__gatewayfilter_factories.html#_addrequestheader_gatewayfilter_factory

 这里拿出几个来演示。

1 spring:
2 cloud:
3 gateway:
4 routes:
5 ‐ id: my‐timeBetween #id必须要唯一
6 uri: lb://product‐center
7 predicates:
8 #当前请求的时间必须在早上7点到 晚上11点 http://localhost:8888/selectProduct
InfoById/1
9 #才会被转发
10 #到http://product‐center/selectProductInfoById/1
11 ‐ MyTimeBetween=上午7:00,下午11:00
12 filters:
13 ‐ AddRequestHeader=X‐Request‐Company,my

测试:http://localhost:8888/gateWay4Header

@RequestMapping("/gateWay4Header")
public Object gateWay4Header(@RequestHeader("X‐Request‐Company") String c
ompany) {
return "gateWay拿到请求头"+company;
}

②:添加请求参数

spring:
cloud:
gateway:
routes:
‐ id: tuling‐timeBetween #id必须要唯一
uri: lb://product‐center
predicates:
‐ TulingTimeBetween=上午7:00,下午11:00
filters:
 ‐ AddRequestParameter=company, tuling

测试地址:http://localhost:8888/gateWay4RequestParam

@RequestMapping("/gateWay4RequestParam")
public Object gateWay4RequestParam(@RequestParam("company") String compan
y) {

return "gateWay拿到请求参数"+company;
}

③:为匹配的路由统一添加前缀

spring:
cloud:
gateway:
routes:
‐ id: tuling‐timeBetween #id必须要唯一
uri: lb://product‐center
predicates:
‐ TulingTimeBetween=上午7:00,下午11:00
filters:
 ‐ PrefixPath=/product‐api
 #比如
 http://localhost:8888/selectProductInfoById/1
 会转发到路径
 http://product‐center/product‐api/selectProductInfoById/1

我们的product-center的需要添加一段配置:

server:
	servlet:
		context‐path: /product‐api

测试地址:http://localhost:8888/selectProductInfoById/2

更多的配置 具体查看官网 已经详细的列出了20多种. https://cloud.spring.io/spring-cloud-gateway/2.1.x/multi/multi__gatewayfilter_factories.html

④:自定义过滤器工厂 继承AbstractNameValueGatewayFilterFactory 且我们的自定义名称必须要以GatewayFilterFactory结尾 

@Slf4j
@Component
public class TimeMonitorGatewayFilterFactory extends AbstractNameValueGat
ewayFilterFactory {

private static final String COUNT_START_TIME = "countStartTime";


@Override
public GatewayFilter apply(NameValueConfig config) {

 return new GatewayFilter() {
 @Override
 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
//获取配置文件yml中的
filters:
‐ TimeMonitor=enabled,true
String name = config.getName();
String value = config.getValue();
log.info("name:{},value:{}",name,value);
if(value.equals("false")) {
return null;
}
exchange.getAttributes().put(COUNT_START_TIME,
System.currentTimeMillis());

//then方法相当于aop的后置通知一样
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
Long startTime = exchange.getAttribute(COUNT_START_TIME);
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().get
RawPath())
.append(": ")
.append(System.currentTimeMillis() ‐ startTime)
.append("ms");
sb.append(" params:").append(exchange.getRequest().getQueryParams());
log.info(sb.toString());
}
}
}));
}
};
}
}

访问打印的日志

缺陷: 通过自定义过滤器工程创建出来的过滤器是不能指定优先级的, 只能根据配置的先后顺序执行,若向指定优先级怎么办?

我们需要稍微改动一下代码: 写一个自定义的内部类实现 GateWayFilter接口 和ordered接口,

 

@Slf4j
@Component
public class TimeMonitorGatewayFilterFactory extends AbstractNameValueGat
ewayFilterFactory {

private static final String COUNT_START_TIME = "countStartTime";


@Override
public GatewayFilter apply(NameValueConfig config) {
 return new TimeMonitorGatewayFilter(config);
 }

 /**
 * 我们自己写一个静态内部类 实现GatewayFilter,Ordered 通过Orderd可以实现顺序
的控制
*/
public static class TimeMonitorGatewayFilter implements GatewayFilter,O
rdered{

private NameValueConfig nameValueConfig;

public TimeMonitorGatewayFilter(NameValueConfig nameValueConfig) {
this.nameValueConfig = nameValueConfig;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
String name = nameValueConfig.getName();
String value = nameValueConfig.getValue();
log.info("name:{},value:{}",name,value);
if(value.equals("false")) {
return null;
}
exchange.getAttributes().put(COUNT_START_TIME,
System.currentTimeMillis());

//then方法相当于aop的后置通知一样
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
Long startTime = exchange.getAttribute(COUNT_START_TIME);
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().get
RawPath())
.append(": ")
.append(System.currentTimeMillis() ‐ startTime)
.append("ms");
sb.append(" params:").append(exchange.getRequest().getQueryParams());
log.info(sb.toString());
}
}
}));
}

@Override
public int getOrder() {
return ‐100;
}
}
}

⑤:自定义全局过滤器,所有的请求都会经过全局过滤器 实现GlobalGateWayFilter ,那么所有的请求都会经过gateway 业务场景中。请求中必须带入token才会被转发.

/**
* 全局过滤器校验请求头中的token
* Created by smlz on 2019/12/17.
*/
@Component
@Slf4j
public class AuthGateWayFilter implements GlobalFilter,Ordered {

@Override
 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
List<String> token = exchange.getRequest().getHeaders().get("token");
if(StringUtils.isEmpty(token)) {
return null;
}else {
log.info("token:{}",token);
return chain.filter(exchange);
}
}

@Override
public int getOrder() {
return 0;
}
}

⑥:SpringCloudGateWay+Sentinel1.6.3(以上版本) 解释?为啥要1.6.3版本,若低于1.6.3版本的话,需要在gateway工程 进行大量的编码进行设置流控的规则。

若1.6.3版本以上,我们就可以通过sentinel页面进行配置规则

名称解释:

GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规 则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参 数、Header、来源 IP 等进行定制化的限流

ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组 合。比如我们可以定义一个 API 叫 my_api,请求 path 模式 为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以 针对这个自定义的 API 分组维度进行限流

resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组 名称。 resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。

grade:限流指标维度,同限流规则的 grade 字段。 count:限流阈值

intervalSec:统计时间窗口,单位是秒,默认是 1 秒。

controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字 段,目前支持快速失败和匀速排队两种模式,默认是快速失败。

burst:应对突发请求时额外允许的请求数目。

axQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅 在匀速排队模式下生效。

paramItem:参数限流配置。若不提供,则代表不针对参数进行限流,该网关 规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段: parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、 Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数 (PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。

fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应 的 header 名称或 URL 参数名称。 pattern:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流 控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)

matchStrategy:参数值的匹配策略,目前支持精确匹配 (PARAM_MATCH_STRATEGY_EXACT)、子串匹配 (PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配 (PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持) 用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或 通过 GatewayRuleManager.register2Property(property) 注册动态规则源动 态推送(推荐方式)。 

GateWay+Sentinel1.6.3版本整合

a)创建工程08-ms-cloud-gateway-sentinel

导入依赖: 

<!‐‐加入nacos的依赖‐‐>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring‐cloud‐alibaba‐nacos‐discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring‐cloud‐starter‐gateway</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring‐boot‐starter‐webflux</artifactId>
 </dependency>

 <dependency>
 <groupId>com.alibaba.csp</groupId>
 <artifactId>sentinel‐spring‐cloud‐gateway‐adapter</artifactId>
 </dependency>
 <dependency>
 <groupId>com.alibaba.csp</groupId>
 <artifactId>sentinel‐transport‐simple‐http</artifactId>
 </dependency>

增加配置类

@Configuration
public class GatewayConfiguration {

private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;

public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResol
versProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::e
mptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExcepti
onHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCo
decConfigurer);
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}

增加yml的配置

server:
port: 8888
spring:
application:
name: gateway-sentinel
cloud:
gateway:
discovery:
locator:
 lower-case-service-id: true
 enabled: true
 routes:
 - id: product_center
 uri: lb://product-center
 predicates:
 - Path=/product/**
 - id: order_center
 uri: lb://order-center
 predicates:
 - Path=/order/**
 nacos:
 discovery:
 server-addr: localhost:8848

打开sentinel的控制台,由于sentinel的控制台第一次打开没有,你需 要分别请求一下路径 http://localhost:8888/product/selectProductInfoById/2 http://localhost:8888/order/selectOrderInfoById/1

就会生成如下的流控节点

 添加流控规则(如下三个 测试不出效果) 他的本意是是控制调用网关的 ip是指定的Ip进行控制 Cookie选项中意思就是每次请求中带入指定的cookie k v 就会被限 流,不带就会被限制流量。(但是效果测试不出) 而Header模型 :指定是请求中带入的特定的header kv指就会被限流 URL参数:同理,也会针对请求参数名称进行限制流量。

 现在我们测试Header模式,如下配置

 频繁的请求如下地址:

现在测试

 

测试:

 

 业务场景:我们一个工程有多个请求的api,但是可能存在一种可能就 是不同的

api的请求控制不一样,怎么办,那么sentienl的routeId模式流控达 不到效果了。

比如:08-ms-alibaba-gateway-product工程中有三个api /product/selectProductInfoById/{productNo}

/product/gateWay4Header

/product/gateWay4RequestParam

若通过如下这种配置流控规则,不能做到细粒度配置,那么如何做?

自定义APi分组,我们把 如下的api进行分组

/product/selectProductInfoById/{productNo}

/product/gateWay4Header

/product/gateWay4RequestParam 

 通过API类型选择api分组可以做到细粒度配置.

GateWay+Sentienl全局异常处理。

 

Sentinel默认的情况下使用的是SentinelGatewayBlockExceptionHandler进行处理, 我们只需要 而我们的SentinelGatewayBlockExceptionHandler底层调用了我们的 BlockRequestHandler接口的实现类DefaultBlockRequestHandler,而我们只需要自己写 一个类继承父类就可以进行自定义异常处理

@Component
public class TulingBlockRequestHandler extends DefaultBlockRequestHandler
{

private static final String DEFAULT_BLOCK_MSG_PREFIX = "Blocked by Senti
nel: ";

//处理异常的
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Th
rowable ex) {
//处理html错误类型的
if (acceptsHtml(exchange)) {
return htmlErrorResponse(ex);
}
//处理Json类型的
// JSON result by default.
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(fromObject(buildErrorResult(ex)));
}

private Mono<ServerResponse> htmlErrorResponse(Throwable ex) {

return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.TEXT_PLAIN)
.syncBody(new String(JSON.toJSONString(buildErrorResult(ex))));
}

private TulingBlockRequestHandler.ErrorResult buildErrorResult(Throwabl
e ex) {
if(ex instanceof ParamFlowException) {
return new TulingBlockRequestHandler.ErrorResult(HttpStatus.TOO_MANY_RE
QUESTS.value(),"block");
}else if (ex instanceof DegradeException) {
return new TulingBlockRequestHandler.ErrorResult(HttpStatus.TOO_MANY_RE
QUESTS.value(),"fallback");
}else{
return new
TulingBlockRequestHandler.ErrorResult(HttpStatus.BAD_GATEWAY.value(),"gatew
ay error");
}

}

/**
* Reference from {@code DefaultErrorWebExceptionHandler} of Spring
Boot.
*/
private boolean acceptsHtml(ServerWebExchange exchange) {
try {
List<MediaType> acceptedMediaTypes =
exchange.getRequest().getHeaders().getAccept();
acceptedMediaTypes.remove(MediaType.ALL);
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes);
return acceptedMediaTypes.stream()
.anyMatch(MediaType.TEXT_HTML::isCompatibleWith);
} catch (InvalidMediaTypeException ex) {
return false;
}
}

private static class ErrorResult {
private final int code;
private final String message;

ErrorResult(int code, String message) {
this.code = code;
this.message = message;
}

public int getCode() {
return code;
}

public String getMessage() {
return message;
}
}
} if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
// This exception handler only handles rejection by Sentinel.
if (!BlockException.isBlockException(ex)) {
return Mono.error(ex);
}
return handleBlockedRequest(exchange, ex)
.flatMap(response ‐> writeResponse(response, exchange));
}

private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exc
hange, Throwable throwable) {
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange,
throwable);
}

private final Supplier<ServerResponse.Context> contextSupplier = () ‐>
new ServerResponse.Context() {
@Override
public List<HttpMessageWriter<?>> messageWriters() {
return TulingSentinelGatewayBlockExceptionHandler.this.messageWriters;
}

@Override
public List<ViewResolver> viewResolvers() {
return TulingSentinelGatewayBlockExceptionHandler.this.viewResolvers;
}
};


private Mono<Void> writeResponse(ServerResponse response, ServerWebExch
ange exchange) {
String reqPath = exchange.getRequest().getPath().value();
 Map<String,Object> retMap = new HashMap<>();

 ServerHttpResponse serverHttpResponse = exchange.getResponse();
 serverHttpResponse.getHeaders().add("Content‐Type", "application/json;c
harset=UTF‐8");

retMap.put("msg","被限流拉");
retMap.put("code","‐1");
retMap.put("reqPath",reqPath);
ObjectMapper objectMapper = new ObjectMapper();

byte[] datas = new byte[0];
try {
datas = objectMapper.writeValueAsString(retMap).getBytes(StandardCharse
ts.UTF_8);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
return serverHttpResponse.writeWith(Mono.just(buffer));
}

}

 

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
华为语言调试工具LMT是一种用于语言互通的络设备的调试工具。LMT是Language Migrating Tool的缩写,主要用于华为络设备的语言设置和管理。 想要下载华为语言调试工具LMT,可以按照以下步骤进行操作: 1. 首先,打开华为官方站:www.huawei.com。 2. 在站首页上方的菜单栏中,找到并单击“支持”选项。 3. 在支持页面上,选择“软件下载”或类似的选项。 4. 在软件下载页面,在搜索框中输入“华为语言调试工具LMT”。 5. 在搜索结果中,找到并选择适用于您设备型号和操作系统的LMT版本。 6. 单击下载按钮,开始下载华为语言调试工具LMT的安装包。 7. 下载完成后,找到下载的安装包文件,双击打开。 8. 按照安装向导的提示,选择安装路径和其他可选设置。 9. 完成安装后,您可以在指定的安装路径中找到LMT的可执行文件。 10. 双击运行LMT程序,进入调试工具的主界面。 通过以上步骤,您可以成功下载并安装华为语言调试工具LMT,然后就可以开始使用该工具进行相的语言互通设备的调试和管理了。 值得注意的是,为了确保LMT的正常使用和兼容性,建议下载与您设备型号和操作系统相匹配的软件版本,并且在安装和使用LMT之前,可以先阅读官方提供的使用手册或使用指南,以便更好地了解该工具的使用方法和注意事项。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小强同志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值