WebFlux-11路由匹配算法

部署运行你感兴趣的模型镜像

WebFlux 路由匹配算法详解

概述

WebFlux 的路由匹配算法是其核心功能之一,负责将 HTTP 请求映射到对应的处理器方法。WebFlux 支持两种主要的路由方式:基于注解的 @RequestMapping 和基于函数的 RouterFunction。本文深入剖析 WebFlux 路由匹配算法的设计原理、实现机制和优化策略。

路由算法架构

1. 整体路由架构

结果层
匹配算法层
路由匹配层
请求层
@Controller方法
HandlerMethod
HandlerFunction
简单Handler
Ant路径匹配
路径匹配
正则表达式匹配
RequestPredicate
谓词匹配
组合谓词
最佳匹配选择
优先级排序
歧义消除
RequestMappingHandlerMapping
HandlerMapping
RouterFunctionMapping
SimpleUrlHandlerMapping
注解路由匹配
函数式路由匹配
简单URL匹配
ServerWebExchange
HTTP请求
请求信息提取

2. 路由匹配流程

ServerWebExchangeDispatcherHandlerHandlerMappingMappingRegistryPathMatcherHandlerMethodhandle(exchange)getHandler(exchange)路由匹配开始提取请求信息获取候选映射getMappingsByUrl(lookupPath)返回候选列表路径匹配检查匹配结果谓词评估优先级排序选择最佳匹配返回HandlerMethod继续处理ServerWebExchangeDispatcherHandlerHandlerMappingMappingRegistryPathMatcherHandlerMethod

注解路由匹配算法

1. RequestMappingHandlerMapping 架构

RequestMappingHandlerMapping
初始化阶段
匹配阶段
扫描@Controller
解析@RequestMapping
注册映射关系
接收请求
路径匹配
条件匹配
返回Handler
Bean扫描
注解解析
MappingRegistry
提取请求信息
Ant路径匹配
谓词评估
HandlerMethod

2. 映射注册过程

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping {
    
    private final MappingRegistry mappingRegistry = new MappingRegistry();
    
    @Override
    protected void initHandlerMethods() {
        // 1. 获取所有候选Bean
        String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class);
        
        for (String beanName : beanNames) {
            // 2. 检查是否是Handler
            if (!isHandler(getApplicationContext().getType(beanName))) {
                continue;
            }
            
            // 3. 处理方法级别的映射
            processHandlerMethod(beanName);
        }
        
        // 4. 初始化处理器方法
        initializeHandlerMethods();
    }
    
    private void processHandlerMethod(String beanName) {
        Class<?> handlerType = obtainApplicationContext().getType(beanName);
        Class<?> userType = ClassUtils.getUserClass(handlerType);
        
        // 获取所有方法
        Method[] methods = MethodIntrospector.selectMethods(userType,
            (MethodIntrospector.MetadataLookup<RequestMappingInfo>) method -> {
                try {
                    // 为每个方法创建RequestMappingInfo
                    return getMappingForMethod(method, userType);
                } catch (Throwable ex) {
                    throw new IllegalStateException("Invalid mapping on handler class [" +
                        userType.getName() + "]: " + method, ex);
                }
            });
        
        // 注册映射关系
        methods.forEach(method -> {
            RequestMappingInfo mapping = methods.get(method);
            registerHandlerMethod(beanName, method, mapping);
        });
    }
    
    @Override
    protected boolean isHandler(Class<?> beanType) {
        // 检查是否有@Controller或@RequestMapping注解
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }
}

3. RequestMappingInfo 构建算法

public class RequestMappingInfo {
    
    private final PathPatternsRequestCondition patternsCondition;
    private final RequestMethodsRequestCondition methodsCondition;
    private final ParamsRequestCondition paramsCondition;
    private final HeadersRequestCondition headersCondition;
    private final ConsumesRequestCondition consumesCondition;
    private final ProducesRequestCondition producesCondition;
    
    public static Builder paths(String... paths) {
        return new DefaultBuilder(paths);
    }
    
    public RequestMappingInfo combine(RequestMappingInfo other) {
        // 组合两个RequestMappingInfo(用于类级别和方法级别的组合)
        return new RequestMappingInfo(
            this.patternsCondition.combine(other.patternsCondition),
            this.methodsCondition.combine(other.methodsCondition),
            this.paramsCondition.combine(other.paramsCondition),
            this.headersCondition.combine(other.headersCondition),
            this.consumesCondition.combine(other.consumesCondition),
            this.producesCondition.combine(other.producesCondition)
        );
    }
}

4. 路径匹配算法

请求路径
路径标准化
Ant路径匹配
路径变量提取
通配符匹配
最佳匹配选择
去除上下文路径
模式匹配检查
提取变量值
匹配得分计算
排序选择
/api/users/123
/api/users/{id}
id=123
精确度评分
最高得分

5. AntPathMatcher 实现

public class AntPathMatcher implements PathMatcher {
    
    private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}");
    
    @Override
    public boolean match(String pattern, String path) {
        return doMatch(pattern, path, true, null);
    }
    
    protected boolean doMatch(String pattern, String path, boolean fullMatch, 
                             @Nullable Map<String, String> uriTemplateVariables) {
        
        if (path.startsWith("/") != pattern.startsWith("/")) {
            return false;
        }
        
        String[] pattDirs = tokenizePattern(pattern);
        String[] pathDirs = tokenizePath(path);
        
        int pattIdxStart = 0;
        int pattIdxEnd = pattDirs.length - 1;
        int pathIdxStart = 0;
        int pathIdxEnd = pathDirs.length - 1;
        
        // 匹配路径段
        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
            String pattDir = pattDirs[pattIdxStart];
            
            if ("**".equals(pattDir)) {
                break;
            }
            
            if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
                return false;
            }
            
            pattIdxStart++;
            pathIdxStart++;
        }
        
        if (pathIdxStart > pathIdxEnd) {
            // 路径已经匹配完毕
            if (pattIdxStart > pattIdxEnd) {
                return (pattern.endsWith("/") == path.endsWith("/"));
            }
            if (!fullMatch) {
                return true;
            }
            if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && 
                path.endsWith("/")) {
                return true;
            }
            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
                if (!pattDirs[i].equals("**")) {
                    return false;
                }
            }
            return true;
        } else if (pattIdxStart > pattIdxEnd) {
            // 模式已经匹配完毕,但路径还有剩余
            return false;
        } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
            return true;
        }
        
        // 处理**通配符
        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
            String pattDir = pattDirs[pattIdxEnd];
            if (pattDir.startsWith("**")) {
                break;
            }
            if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
                return false;
            }
            pattIdxEnd--;
            pathIdxEnd--;
        }
        
        if (pathIdxStart > pathIdxEnd) {
            // 路径已经匹配完毕
            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
                if (!pattDirs[i].equals("**")) {
                    return false;
                }
            }
            return true;
        }
        
        return true;
    }
    
    private boolean matchStrings(String pattern, String str, 
                                @Nullable Map<String, String> uriTemplateVariables) {
        // 处理路径变量
        if (pattern.startsWith("{") && pattern.endsWith("}")) {
            if (uriTemplateVariables != null) {
                String variableName = pattern.substring(1, pattern.length() - 1);
                uriTemplateVariables.put(variableName, str);
            }
            return true;
        }
        
        return pattern.equals(str);
    }
}

函数式路由匹配算法

1. RouterFunction 架构

RouterFunction
RequestPredicate
HandlerFunction
路径谓词
方法谓词
头信息谓词
参数谓词
业务处理函数
PathPatternPredicate
RequestMethodPredicate
HeaderPredicate
ParamPredicate
返回ServerResponse
路径匹配
HTTP方法匹配
头信息匹配
参数匹配

2. RouterFunction 构建过程

public class RouterFunctions {
    
    public static <T extends ServerResponse> RouterFunction<T> route() {
        return new RouterFunctionBuilder<>();
    }
    
    public static class RouterFunctionBuilder<T extends ServerResponse> {
        
        private final List<RouterFunction<T>> routerFunctions = new ArrayList<>();
        
        public RouterFunctionBuilder<T> GET(String pattern, 
                                           HandlerFunction<T> handlerFunction) {
            return route(RequestPredicates.GET(pattern), handlerFunction);
        }
        
        public RouterFunctionBuilder<T> POST(String pattern, 
                                            HandlerFunction<T> handlerFunction) {
            return route(RequestPredicates.POST(pattern), handlerFunction);
        }
        
        public RouterFunctionBuilder<T> route(RequestPredicate predicate, 
                                             HandlerFunction<T> handlerFunction) {
            this.routerFunctions.add(new DefaultRouterFunction<>(predicate, handlerFunction));
            return this;
        }
        
        public RouterFunction<T> build() {
            return routerFunctions.stream()
                .reduce(RouterFunction::andOther)
                .orElseThrow(() -> new IllegalStateException("No routes defined"));
        }
    }
}

3. RequestPredicate 实现

public interface RequestPredicate {
    
    /**
     * 评估请求是否匹配谓词条件
     */
    boolean test(ServerWebExchange exchange);
    
    /**
     * 组合两个谓词(AND关系)
     */
    default RequestPredicate and(RequestPredicate other) {
        return new AndRequestPredicate(this, other);
    }
    
    /**
     * 组合两个谓词(OR关系)
     */
    default RequestPredicate or(RequestPredicate other) {
        return new OrRequestPredicate(this, other);
    }
    
    /**
     * 取反谓词
     */
    default RequestPredicate negate() {
        return new NegateRequestPredicate(this);
    }
}

4. 路径谓词实现

public class PathPatternPredicate implements RequestPredicate {
    
    private final PathPattern pattern;
    
    public PathPatternPredicate(String pattern) {
        this.pattern = PathPatternParser.defaultInstance.parse(pattern);
    }
    
    @Override
    public boolean test(ServerWebExchange exchange) {
        // 1. 获取请求路径
        String path = exchange.getRequest().getURI().getPath();
        
        // 2. 进行路径匹配
        PathPattern.PathMatchInfo matchInfo = this.pattern.matchAndExtract(path);
        
        if (matchInfo != null) {
            // 3. 提取路径变量
            Map<String, String> uriVariables = matchInfo.getUriVariables();
            
            // 4. 存储路径变量到exchange属性中
            exchange.getAttributes().put(
                RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, 
                uriVariables);
            
            return true;
        }
        
        return false;
    }
}

5. 组合谓词算法

public class AndRequestPredicate implements RequestPredicate {
    
    private final RequestPredicate left;
    private final RequestPredicate right;
    
    @Override
    public boolean test(ServerWebExchange exchange) {
        // 短路求值:如果左边为false,直接返回false
        return this.left.test(exchange) && this.right.test(exchange);
    }
}

public class OrRequestPredicate implements RequestPredicate {
    
    private final RequestPredicate left;
    private final RequestPredicate right;
    
    @Override
    public boolean test(ServerWebExchange exchange) {
        // 短路求值:如果左边为true,直接返回true
        return this.left.test(exchange) || this.right.test(exchange);
    }
}

路由优先级算法

1. 匹配优先级规则

多个匹配结果
精确度评分
路径精确度
谓词精确度
声明顺序
具体路径 > 通配符
具体条件 > 通用条件
先声明 > 后声明
/users/123 > /users/*
GET /users > ANY /users
先定义的优先

2. 匹配比较器实现

public class RequestMappingInfoComparator implements Comparator<RequestMappingInfo> {
    
    @Override
    public int compare(RequestMappingInfo info1, RequestMappingInfo info2) {
        int result;
        
        // 1. 比较路径模式(更具体的优先)
        result = info1.getPatternsCondition().compareTo(info2.getPatternsCondition());
        if (result != 0) {
            return result;
        }
        
        // 2. 比较HTTP方法(更具体的优先)
        result = info1.getMethodsCondition().compareTo(info2.getMethodsCondition());
        if (result != 0) {
            return result;
        }
        
        // 3. 比较参数条件
        result = info1.getParamsCondition().compareTo(info2.getParamsCondition());
        if (result != 0) {
            return result;
        }
        
        // 4. 比较头信息条件
        result = info1.getHeadersCondition().compareTo(info2.getHeadersCondition());
        if (result != 0) {
            return result;
        }
        
        // 5. 比较consumes条件
        result = info1.getConsumesCondition().compareTo(info2.getConsumesCondition());
        if (result != 0) {
            return result;
        }
        
        // 6. 比较produces条件
        result = info1.getProducesCondition().compareTo(info2.getProducesCondition());
        if (result != 0) {
            return result;
        }
        
        return 0;
    }
}

3. 路径模式比较算法

public class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
    
    private final List<PathPattern> patterns;
    
    @Override
    protected int compareTo(PatternsRequestCondition other) {
        // 比较两个路径模式的精确度
        PathPattern pattern1 = this.patterns.get(0);
        PathPattern pattern2 = other.patterns.get(0);
        
        // 1. 计算路径变量数量(越少越具体)
        int vars1 = pattern1.getPatternString().split("\\{").length - 1;
        int vars2 = pattern2.getPatternString().split("\\{").length - 1;
        int result = Integer.compare(vars1, vars2);
        if (result != 0) {
            return result;
        }
        
        // 2. 计算通配符数量(越少越具体)
        int wildcards1 = StringUtils.countOccurrencesOf(pattern1.getPatternString(), "*");
        int wildcards2 = StringUtils.countOccurrencesOf(pattern2.getPatternString(), "*");
        result = Integer.compare(wildcards1, wildcards2);
        if (result != 0) {
            return result;
        }
        
        // 3. 计算路径段数量(越多越具体)
        int length1 = pattern1.getPatternString().split("/").length;
        int length2 = pattern2.getPatternString().split("/").length;
        result = Integer.compare(length2, length1);
        if (result != 0) {
            return result;
        }
        
        // 4. 按字母顺序比较(确保确定性)
        return pattern1.getPatternString().compareTo(pattern2.getPatternString());
    }
}

路径变量提取算法

1. 路径变量解析

路径模式
路径匹配
变量提取
类型转换
参数绑定
/users/{id}/posts/{postId}
/users/123/posts/456
id=123, postId=456
类型转换
方法参数绑定

2. 路径变量提取实现

public class PathPattern {
    
    private final List<PathElement> pathElements;
    
    @Nullable
    public PathMatchInfo matchAndExtract(String path) {
        MatchingContext matchingContext = new MatchingContext(path, true);
        
        // 1. 尝试匹配路径
        if (matches(0, matchingContext)) {
            // 2. 提取路径变量
            Map<String, String> uriVariables = new LinkedHashMap<>();
            extractUriVariables(0, matchingContext, uriVariables);
            
            // 3. 提取矩阵变量(如果有)
            Map<String, MultiValueMap<String, String>> matrixVariables = null;
            if (!this.matrixVariableNames.isEmpty()) {
                matrixVariables = new LinkedHashMap<>();
                extractMatrixVariables(0, matchingContext, matrixVariables);
            }
            
            return new PathMatchInfo(uriVariables, matrixVariables);
        }
        
        return null;
    }
    
    private boolean extractUriVariables(int pathIndex, MatchingContext matchingContext,
                                      Map<String, String> uriVariables) {
        // 遍历路径元素,提取变量
        for (int i = pathIndex; i < this.pathElements.size(); i++) {
            PathElement element = this.pathElements.get(i);
            
            if (element instanceof CaptureVariablePathElement) {
                // 处理路径变量
                CaptureVariablePathElement captureElement = (CaptureVariablePathElement) element;
                String variableName = captureElement.getVariableName();
                String variableValue = matchingContext.getCapturedVariable(variableName);
                uriVariables.put(variableName, variableValue);
            }
        }
        
        return true;
    }
}

3. 类型转换机制

public class PathVariableMethodArgumentResolver implements HandlerMethodArgumentResolver {
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(PathVariable.class);
    }
    
    @Override
    public Mono<Object> resolveArgument(MethodParameter parameter, ServerWebExchange exchange) {
        // 1. 获取路径变量注解
        PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
        String variableName = pathVariable.value().isEmpty() ? 
            parameter.getParameterName() : pathVariable.value();
        
        // 2. 从exchange属性中获取路径变量
        Map<String, String> uriTemplateVariables = exchange.getAttribute(
            HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
        
        if (uriTemplateVariables == null || !uriTemplateVariables.containsKey(variableName)) {
            return Mono.error(new IllegalStateException(
                "Missing path variable '" + variableName + "' for method parameter " + parameter));
        }
        
        String variableValue = uriTemplateVariables.get(variableName);
        
        // 3. 类型转换
        return convertVariableValue(variableValue, parameter);
    }
    
    private Mono<Object> convertVariableValue(String value, MethodParameter parameter) {
        Class<?> parameterType = parameter.getParameterType();
        
        // 基本类型转换
        if (parameterType == String.class) {
            return Mono.just(value);
        } else if (parameterType == Integer.class || parameterType == int.class) {
            return Mono.just(Integer.parseInt(value));
        } else if (parameterType == Long.class || parameterType == long.class) {
            return Mono.just(Long.parseLong(value));
        } else if (parameterType == Boolean.class || parameterType == boolean.class) {
            return Mono.just(Boolean.parseBoolean(value));
        }
        
        // 使用ConversionService进行复杂类型转换
        return Mono.just(conversionService.convert(value, parameterType));
    }
}

性能优化算法

1. 路由缓存机制

请求到达
检查缓存
缓存命中?
直接返回Handler
执行匹配算法
路径匹配
谓词评估
优先级排序
选择最佳匹配
存入缓存
继续处理

2. 匹配缓存实现

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping {
    
    // 路径匹配缓存
    private final Map<String, List<RequestMappingInfo>> pathLookup = new ConcurrentHashMap<>();
    
    // HandlerMethod缓存
    private final Map<RequestMappingInfo, HandlerMethod> handlerMethods = new ConcurrentHashMap<>();
    
    @Override
    protected HandlerMethod getHandlerInternal(ServerWebExchange exchange) throws Exception {
        String lookupPath = initLookupPath(exchange);
        
        // 1. 检查HandlerMethod缓存
        HandlerMethod handlerMethod = this.handlerMethodLookup.get(lookupPath);
        if (handlerMethod != null) {
            return handlerMethod;
        }
        
        // 2. 执行匹配算法
        handlerMethod = lookupHandlerMethod(lookupPath, exchange);
        
        // 3. 缓存结果
        if (handlerMethod != null) {
            this.handlerMethodLookup.put(lookupPath, handlerMethod);
        }
        
        return handlerMethod;
    }
}

3. 路径模式预编译

public class PathPatternParser {
    
    private final Map<String, PathPattern> patternCache = new ConcurrentHashMap<>();
    
    public PathPattern parse(String pattern) {
        // 1. 检查缓存
        PathPattern pathPattern = this.patternCache.get(pattern);
        if (pathPattern != null) {
            return pathPattern;
        }
        
        // 2. 解析路径模式
        pathPattern = doParse(pattern);
        
        // 3. 缓存解析结果
        this.patternCache.put(pattern, pathPattern);
        
        return pathPattern;
    }
    
    private PathPattern doParse(String pattern) {
        // 将路径模式解析为PathElement列表
        List<PathElement> pathElements = new ArrayList<>();
        String[] pathSegments = pattern.split("/");
        
        for (String segment : pathSegments) {
            if (segment.isEmpty()) {
                continue;
            }
            
            if (segment.startsWith("{") && segment.endsWith("}")) {
                // 路径变量
                String variableName = segment.substring(1, segment.length() - 1);
                pathElements.add(new CaptureVariablePathElement(variableName));
            } else if (segment.equals("*")) {
                // 单级通配符
                pathElements.add(new WildcardPathElement());
            } else if (segment.equals("**")) {
                // 多级通配符
                pathElements.add(new WildcardTheRestPathElement());
            } else {
                // 普通路径段
                pathElements.add(new LiteralPathElement(segment));
            }
        }
        
        return new PathPattern(pattern, pathElements);
    }
}

路由冲突检测算法

1. 冲突检测机制

新路由注册
检查现有路由
路径模式冲突?
检查谓词条件
注册成功
条件重叠?
报告冲突
警告或错误
开发者处理

2. 冲突检测实现

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping {
    
    @Override
    protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
        // 1. 检查是否存在冲突
        checkForConflicts(mapping);
        
        // 2. 注册映射关系
        super.registerHandlerMethod(handler, method, mapping);
    }
    
    private void checkForConflicts(RequestMappingInfo newMapping) {
        // 获取所有现有映射
        Set<RequestMappingInfo> existingMappings = this.mappingRegistry.getMappings().keySet();
        
        for (RequestMappingInfo existingMapping : existingMappings) {
            // 检查路径模式是否相同
            if (hasSamePattern(newMapping, existingMapping)) {
                // 检查谓词条件是否重叠
                if (hasOverlappingConditions(newMapping, existingMapping)) {
                    // 报告冲突
                    reportMappingConflict(newMapping, existingMapping);
                }
            }
        }
    }
    
    private boolean hasSamePattern(RequestMappingInfo mapping1, RequestMappingInfo mapping2) {
        Set<String> patterns1 = mapping1.getPatternsCondition().getPatterns();
        Set<String> patterns2 = mapping2.getPatternsCondition().getPatterns();
        
        return !Collections.disjoint(patterns1, patterns2);
    }
    
    private boolean hasOverlappingConditions(RequestMappingInfo mapping1, RequestMappingInfo mapping2) {
        // 检查HTTP方法
        RequestMethodsRequestCondition methods1 = mapping1.getMethodsCondition();
        RequestMethodsRequestCondition methods2 = mapping2.getMethodsCondition();
        if (!methods1.getMethods().isEmpty() && !methods2.getMethods().isEmpty()) {
            Set<RequestMethod> intersection = new HashSet<>(methods1.getMethods());
            intersection.retainAll(methods2.getMethods());
            if (intersection.isEmpty()) {
                return false; // 方法不重叠
            }
        }
        
        // 检查其他条件...
        
        return true; // 条件重叠,存在冲突
    }
}

总结

WebFlux 的路由匹配算法通过以下机制实现高效、准确的路由:

  1. 多层次匹配:支持路径、方法、参数、头信息等多维度匹配
  2. 优先级排序:通过精确度评分选择最佳匹配
  3. 缓存优化:预编译和缓存提高匹配性能
  4. 冲突检测:及时发现和报告路由冲突
  5. 类型安全:支持路径变量的类型转换和验证
  6. 扩展性:支持自定义谓词和匹配条件

理解这些算法对于:

  • 优化路由性能
  • 避免路由冲突
  • 设计合理的URL结构
  • 调试路由问题

具有重要的实践意义。

您可能感兴趣的与本文相关的镜像

FLUX.1-dev

FLUX.1-dev

图片生成
FLUX

FLUX.1-dev 是一个由 Black Forest Labs 创立的开源 AI 图像生成模型版本,它以其高质量和类似照片的真实感而闻名,并且比其他模型更有效率

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值