Flutter Web CORS解决方案2-代理转发协议

本文介绍第二种解决FlutterWeb CORS问题的方案:通过启动本地shelf_proxy服务代理协议cgi请求,并给出通过 vscode、AndroidStudio IDE 配置代理脚本快速启动调试的方法。


在 1-禁用浏览器安全策略 中,通过禁用chrome浏览器的安全策略或在浏览器中使用Allow-CORS插件,可以解决CORS问题。但浏览器中使用 Allow-CORS 插件访问时,仍存在部分协议OPTIONS预检跨域问题。
以上方案都是基于浏览器,对于企业微信H5页面应用的开发调试,其内置webView无法禁用安全策略,也无法启用Allow-CORS插件。
迫切需要支持移动终端通过 LAN IP 访问局域网内的web服务,以便调试企业微信内的实际布局渲染效果,及时发现和解决一些跨端兼容性问题。

Why does my http://localhost CORS origin not work?

local-cors-proxy

npm install Local CORS Proxy: npm install -g local-cors-proxy

Simple proxy to bypass CORS issues. This was built as a local dev only solution to enable prototyping against existing APIs without having to worry about CORS.

This module was built to solve the issue of getting this error:

No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disable

执行 npm ls -g --depth=0 或 npm ll -g --depth=0 可查看全局安装的 node_modules 目录。
可打开 node_modules/local-cors-proxy 文件夹查看 local-cors-proxy 代码:

  • 实现文件:lib/index.js
  • node命令行:bin/lcp.js

假设本地调试导致跨域问题(CORS issues)的 cgi 请求路径如下:

http://xxx.coding.net/api/qci/

CGI_HOST 为 xxx.coding.net,也记做 API_BASE_URL,也可能是 IP:PORT 模式。

执行 lcp --proxyUrl 启动:代理目标域名路径:

支持通过 -p(–port) 选项参数指定端口,默认端口为 8010。

$ lcp --proxyUrl http://xxx.coding.net

 Proxy Active 

Proxy Url: http://xxx.coding.net
Proxy Partial: proxy
PORT: 8010
Credentials: false
Origin: *

To start using the proxy simply replace the proxied part of your url with: http://localhost:8010/proxy

根据提示,修改代码 http.dart,将 baseUrl 中的目标域名(CGI_HOST)替换为 local-cors-proxy 代理服务 API_BASE_URL —— localhost:8010/proxy

http://localhost:8010/proxy/api/qci/

接下来,在命令行执行 flutter run -d 启动chrome调试,即经过代理服务请求 CGI 协议,绕过跨域。

  • flutter run -d chrome --web-renderer=html --web-port=8080

shelf_proxy

  1. pubspec.yaml 中引入 shelf 依赖:

shelf_proxy 1.0.1 requires SDK version >=2.14.0 ❤️.0.0

  shelf: ^1.2.0
  shelf_proxy: ^1.0.1
  • shelf: A model for web server middleware that encourages composition and easy reuse
  • shelf_static: Static file server support for the shelf package and ecosystem
  • shelf_proxy: A shelf handler for proxying HTTP requests to another server
  • shelf_cors_headers: CORS headers middleware for Shelf
  1. 新建 scripts/proxy/shelf_lan_cgi_proxy.dart 文件:

支持传入启动参数 SHELF_PROXY_PORT 为 shelf_proxy 代理服务监听端口。
支持传入启动参数 SHELF_PROXY_DOMAIN 为被代理的 CGI 域名(CGI_HOST)。
configAccessControl 设置请求策略,以便允许跨域。实测只设置前两个即可?

// scripts/proxy/shelf_lan_cgi_proxy.dart
// dart run --define=SHELF_PROXY_DOMAIN=xxx.testing.coding.net
//          --define=SHELF_PROXY_PORT=8010 shelf_lan_cgi_proxy.dart

import 'dart:io';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_proxy/shelf_proxy.dart';

// 代理服务的地址(或域名)
// final String serveAddress = '127.0.0.1';
var serveAddress = InternetAddress.anyIPv6;
// 代理监听的服务端口
var listenPort = 8010;
// 需要代理的目标域名(默认测试环境)
var targetUrl = 'http://xxx.testing.coding.net';

// 设置请求策略,允许跨域
void configAccessControl(HttpServer server) {
  server.defaultResponseHeaders.add('Access-Control-Allow-Origin', '*');
  server.defaultResponseHeaders.add('Access-Control-Allow-Headers', '*');
  // server.defaultResponseHeaders.add('Access-Control-Allow-Methods', '*');
  // server.defaultResponseHeaders.add('Access-Control-Allow-Credentials', true);
}

Future main() async {
  // -D(--dart-define) 传参
  const proxyDomain = String.fromEnvironment('SHELF_PROXY_DOMAIN');
  if (proxyDomain.isNotEmpty) {
    targetUrl = 'http://$proxyDomain';
  }

  const proxyPort = int.fromEnvironment('SHELF_PROXY_PORT');
  if (proxyPort > 0) {
    listenPort = proxyPort;
  }

  var server = await shelf_io.serve(
    proxyHandler(targetUrl),
    serveAddress,
    listenPort,
  );

  configAccessControl(server);

  print('Shelf-Proxy @ http://${server.address.host}:${server.port}');
  print('Proxy Target Url: $targetUrl');
}

dart run shelf_proxy

  1. 执行 dart run shelf_lan_cgi_proxy.dart 命令,启动本地 shelf_proxy 代理服务。
  • dart run --define 传参 SHELF_PROXY_PORT 为代理监听端口,缺省为 8010;
  • dart run --define 传参 SHELF_PROXY_DOMAIN 为被代理的目标域名,缺省为 xxx.testing.coding.net;
$ dart run --define=SHELF_PROXY_DOMAIN=xxx.testing.coding.net --define=SHELF_PROXY_PORT=8010 scripts/proxy/shelf_lan_cgi_proxy.dart

Shelf-Proxy @ http://localhost:8010
Proxy Target Url: http://xxx.coding.net
  1. 修改 flutter web app 代码 http.dart,将 cgi 目标域名 apiBaseUrl(例如 xxx.coding.net)替换为代理服务 API_BASE_URL —— localhost:8010(LAN_IP:PORT):
// http.dart
baseUrl: 'http://localhost:8010/api/qci/',
  1. 执行 flutter run -d chrome 命令,启动 chrome 浏览器调试。
  • flutter run -d chrome --web-renderer=html --web-port=8080
  1. 如果要支持局域网访问调试,则可按以下步骤操作:
  • shelf_lan_cgi_proxy.dart 中的 serveAddress 已修改为 anyIP,可以是 localhost、127.0.0.1 或 192.168.0.106;
  • 执行 flutter run -d web-server 命令启动服务,指定 --web-hostname 0.0.0.0 为 anyIP:
    • flutter run -d web-server --profile --web-renderer html --web-port 8080 --web-hostname 0.0.0.0。
  • 这样,局域网内其他机器浏览器即可通过 LAN IP 地址链接(http://192.168.0.106:8080)访问服务。
  1. 思考:如何输出一些 proxyHandler 日志?

flutter run --dart-define

在上面第二步中,每次都要修改 http.dart 中写死的CGI域名(apiBaseUrl、APIBASEURL)、替换不同环境的认证token(authToken),这很不方便。

考虑将以上两个参数,抽出来由 flutter run 通过 --dart-define 传入参数,Dart 代码中调用 fromEnvironment 解析参数,替换占位变量 apiBaseUrlauthToken,如下图分支 ①。

wrap with shell

编写辅助启动 shelf_proxy 和 web app 的 Shell 脚本 launch_shelf.sh,主要功能是执行 dart run 启动代理、执行 flutter run 启动服务。

    # 从配置文件中读取配置
    get_env_config $mode $need_token

    # 兜底LAN IP和相关端口
    get_local_ipport

    if [ $role = server ]; then
        echo -e "✅  server listening on \033[4mhttp://$lan_ip:$web_port\033[0m"
    elif [ $role = proxy ]; then
        echo -e "✅  proxy listening on \033[4mhttp://$lan_ip:$proxy_port\033[0m"
    fi

    echo "------------------------------------------------------------"

    set -x

    if [ $role != proxy ]; then
        # 启动 shelf_proxy 代理(& 转移到后台)
        dart run --define=SHELF_PROXY_DOMAIN="${api_base_url:?unset or null}" --define=SHELF_PROXY_PORT=$proxy_port "$(dirname "$0")"/shelf_lan_cgi_proxy.dart &
        # 启动 client or server
        if [ $role = client ]; then
            flutter run --$mode -d chrome --web-renderer=html --web-port=$web_port --dart-define=API_BASE_URL="${lan_ip:?unset or null}":$proxy_port --dart-define=AUTH_TOKEN="${auth_token:?unset or null}"
        elif [ $role = server ]; then
            flutter run --$mode -d web-server --web-renderer=html --web-port=$web_port --web-hostname=0.0.0.0 --dart-define=API_BASE_URL="${lan_ip:?unset or null}":$proxy_port --dart-define=AUTH_TOKEN="${auth_token:?unset or null}"
        fi
    else
        # 启动 shelf_proxy 代理
        dart run --define=SHELF_PROXY_DOMAIN="${api_base_url:?unset or null}" --define=SHELF_PROXY_PORT=$proxy_port "$(dirname "$0")"/shelf_lan_cgi_proxy.dart
    fi

    set +x

    # 尝试杀死后台挂起的进程
    kill_run

执行 dart run shelf_lan_cgi_proxy.dart,启动 shelf_proxy 代理服务。

  • 从配置文件(config/debug.conf)中读取 CGI_HOST 到 api_base_url 传参给 dart run SHELF_PROXY_DOMAIN
  • 读取sh命令行参数 -s 存储到 proxy_port,传参给 dart run SHELF_PROXY_PORT
help & usage

执行 launch_shelf.sh -h 可查看脚本帮助:

$ ./scripts/proxy/launch_shelf.sh -h
launch_shelf.sh version: 1.0.0
Usage: launch_shelf.sh [-?hvCSdpr] [-i ip] [-w web-port] [-s shelf-port]

Options:
    -?,-h,--help            : show help and exit
    -v, --version           : show version and exit
    -C, --client            : start as client, default
    -S, --server            : start as server
    -P, --proxy             : start as proxy daemon
    -d, --debug             : run in debug mode, default
    -p, --profile           : run in profile mode
    -r, --release           : run in release mode
    -i, --ip ip             : set lan ip
    -w, --web-port port     : config flutter web port, default 8080
    -s, --shelf-port port   : config shelf proxy port, default 8010

主要涉及两组参数。

  1. 脚本启动角色(role):

    • -C 对应 flutter run -d chrome,启动web server,并拉起 chrome 运行;
    • -S 对应 flutter run -d web-server,启动web server,需要自行打开浏览器输入调试 url。
    • -P 只启动 shelf_proxy 代理,可搭配作为自行执行 flutter run 的代理使用,例如指定作为 vscode launch preLaunchTask 代理脚本。
    -C, --client            : start as client, default
    -S, --server            : start as server
    -P, --proxy             : start as proxy daemon
  • 代理模式(-P):可搭配作为自行执行 flutter run 的代理使用。

    • 外部基于 web server 监听端口访问 web 服务,web内部请求 cgi 经由 shelf_proxy 代理。
    • 这种模式目前主要用作指定作为 vscode launch preLaunchTask 代理脚本,参见下文。
  • 非代理模式(-C-S):sh 脚本还会执行 flutter run 启动 web app(-d chrome、-d web-server)。

    • 读取sh命令行参数 -w 作为 web_port。
    • flutter run 通过 --dart-define 传参 API_BASE_URL、AUTH_TOKEN。

    在 CS 模式中,dart run shelf_lan_cgi_proxy.dart 末尾需指定 & 转移到后台执行,避免阻塞,以便继续执行后续 flutter run 等命令。

  1. 运行模式(mode):-d调试模式、-p诊断模式、-r发布模式。

    • 可按需模拟调试某个环境的运行效果。
    • 在 get_env_config 函数中,根据运行模式读取对应的 token("$env_mode"_AUTH_TOKEN)。
    -d, --debug             : run in debug mode, default
    -p, --profile           : run in profile mode
    -r, --release           : run in release mode
mode & conf

脚本 aux_etc.sh 中定义的 get_env_config 函数,从配置文件 debug(profile,release).conf 中读取被代理的协议域名(CGI_HOST)和认证TOKEN(AUTH_TOKEN)。

conf 配置文件的 Shebang (#!/usr/bin/env bash)也申明为shell脚本,source导入后直接以 $var 形式引用变量,避免复杂的文本解析,方便处理。

由于 AUTH_TOKEN 涉及隐私和安全,故不再在conf中写死泄漏,改为读取环境变量("$env_mode"_AUTH_TOKEN),故请先在 zshrc 中申明导出三个环境的 AUTH_TOKEN:

# ~/.zshrc
export DEBUG_AUTH_TOKEN=  69c1********************************ccb4
export PROFILE_AUTH_TOKEN=2b57********************************fe89
export RELEASE_AUTH_TOKEN=2b57********************************fe89

无论是 -C 还是 -S 模式,–dart-define 的两个变量拼接如下,http.dart 中将解析这两个参数替换占位变量。

--dart-define=API_BASE_URL="${lan_ip:?unset or null}":$proxy_port
--dart-define=AUTH_TOKEN="${auth_token:?unset or null}"
  • 从脚本运行参数 -s 提取 proxy_port,作为 shelf_proxy 代理监听端口。
  • 主脚本的 main 函数,调用辅助脚本 aux_etc.sh 中的 get_env_config 函数,从 conf 中解析设置 auth_token
  • 主脚本的 main 函数调用 get_local_ipport 函数,调用 aux_etc.sh 中的帮助函数 get_lan_ip 解析获取 lan_ip
run & debug

在项目根目录执行 sh 脚本:

  1. 不指定角色,缺省 -C(-d chrome) 启动 client 模式,起好 web-server 之后,创建拉起 chrome 独立进程访问web服务。
$ ./scripts/proxy/launch_shelf.sh
  1. 指定角色 -S 启动 -d web-server 模式,点击提示中的 server listening 局域网链接打开浏览器即可访问web服务。

局域网中的其他机器,也可以输入该url访问服务。

$ ./scripts/proxy/launch_shelf.sh -S

✅  server listening on http://10.20.89.64:8080

  1. 指定角色 -P 启动纯代理模式,作为 vscode/Android Studio launch 的 preLaunchTask 任务,启动 proxy daemon。

launch with proxy

vscode/Android Studio 启动执行 main.dart,实际上是运行于 client 模式使用,相当于 flutter run -d chrome

故如果可以基于脚本或配置,在启动之前预启动 proxy daemon,并将监听的 IP:PORT 传给 flutter run --dart-define=API_BASE_URL,这样就可以实现 IDE 一键 launch with proxy,方便开发调试!

vscode

在 vscode 启动配置(.vscode/launch.json)中新建启动任务 debug - shelf

  • preLaunchTask 提前启动 tasks.json 中定义的 shelf_proxy 代理脚本——launch_shelf.sh。
  • 由于 json 无法读取环境变量,需要自行替换 API_BASE_URLAUTH_TOKEN

    关于局域网IP,可以执行 ./scripts/proxy/launch_shelf.sh -P 查看其输出的 proxy listening 信息。

// launch.json
    {
      "name": "debug - shelf",
      "request": "launch",
      "type": "dart",
      "program": "lib/main.dart",
      "flutterMode": "debug",
      "deviceId": "chrome",
      "args": ["--web-renderer=html", "--web-port=8080"],
      "toolArgs": [
        "--dart-define",
        "API_BASE_URL=LAN_IP:PROXY_PORT", // 局域网代理监听IP:PORT
        "--dart-define",
        "AUTH_TOKEN=DEBUG_AUTH_TOKEN"
      ],
      "preLaunchTask": "shelf_proxy - debug"
    },

注意:tasks.json 的 task config 中需指定 "isBackground": true,将代理服务运行于后台(等效命令行末尾置后运行的 &)。

// tasks.json
    {
      "label": "shelf_proxy - debug",
      "type": "shell",
      "command": "./scripts/proxy/launch_shelf.sh -P", // 默认代理端口为8010
      "group": "build",
      "presentation": {
        "reveal": "always",
        "panel": "new"
      },
      "isBackground": true
    },

这样,在 vscode 侧边栏 - Run and Debug 就可以选择运行 debug - shelf,启动代理调试,解决跨域问题。

在这里插入图片描述

Android Studio

能否按照 vscode preLaunchTask 预启动 proxy 的思路,支持 Android Studio launch + proxy 呢?答案是肯定的,Android Studio 启动配置也支持指定 Before launch 任务,定义启动之前的操作

工具栏 main.dart 下拉点选 Edit Configurations:

在这里插入图片描述

将打开 Run/Debug Configurations,Additional run args 为 --web-renderer=html --web-port=8080 --dart-define=API_BASE_URL=xxx.coding.net --dart-define=AUTH_TOKEN=69c1********************************ccb4

点击 Before launch ➕ Run External tool:

在这里插入图片描述

在打开的对话框中,点击➕ Create Tool,Program 右侧 Browse 打开点选 Finder 中的 scripts/proxy/launch_shelf_fuse.scpt,然后确认:

在这里插入图片描述

将 Additional run args 中的 API_BASE_URL 参数修改为局域网(LAN_IP):代理端口(PROXY_PORT):

关于局域网IP,可以执行 ./scripts/proxy/launch_shelf.sh -P 查看其输出的 proxy listening 信息。

在这里插入图片描述

如此设置确认后,点击Android Studio 工具栏绿色爬虫按钮(Debug ‘main.dart’),即可启动 launch with proxy。


思考:launch_shelf_fuse.scpt 为 Apple Script,封装启动 launch_shelf.sh -Pd,能否在 Program 直接指定启动 sh 呢?

-d 表示 debug, 还可指定 -p for profile、-r for release,注意填写对应环境的 AUTH_TOKEN。

#!/usr/bin/osascript

-- scpt dir
set cwd to quoted form of POSIX path of ((path to me as text) & "::")
do shell script "echo cwd = " & cwd
-- parent dir
set pcwd to do shell script "echo $(dirname " & cwd & ")"
do shell script "echo pcwd = " & pcwd
-- grandfather dir
set ppcwd to do shell script "echo $(dirname " & pcwd & ")"
do shell script "echo ppcwd = " & ppcwd

tell application "Terminal"
    do script "cd " & ppcwd & ";" & "./scripts/proxy/launch_shelf.sh" & space & "-P" & space & "-d"
    activate
end tell

问题是 Program 没法指定 sh 参数,且运行完 Before launch 任务,sh进程立即就会退出,无法运作为proxy daemon。

故这里写成 osascript,打开 Terminal 并使之 activate。这样 osascript 退出 Terminal 仍在,等效实现了proxy daemon。

遗留问题

目前发现构建详情页,点击切换到【构建物】tab,拉取构建物列表还是报 CORS error:

http://10.65.91.54:8010/api/qci/rest-api/totalresult/13819946/artifacts?page=1&ver=2

可能需要深入 shelf_proxy.dart 源码调试,分析一下具体原因。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Flutter中调用Android原生方法,可以使用Flutter插件。Flutter插件是一个将Flutter应用程序与原生平台通信的桥梁。下面是一些步骤来创建一个Flutter插件并在其中调用Android原生方法: 1. 使用Flutter插件模板创建一个Flutter插件: ``` flutter create --template=plugin <plugin-name> ``` 2. 在Flutter插件项目的`android`目录下,打开`build.gradle`文件,并添加以下代码: ``` dependencies { implementation 'io.flutter:flutter_embedding_v2.7.0' // 其他依赖项 } ``` 3. 在Flutter插件项目的`android/src/main`目录下,创建一个`java`包,并在其中创建一个类,该类将包含您要调用的Android原生方法。例如,您可以创建一个名为`MyPlugin`的类,并在其中添加以下代码: ``` package com.example.my_plugin; import android.content.Context; import android.widget.Toast; import io.flutter.embedding.engine.plugins.FlutterPlugin; public class MyPlugin implements FlutterPlugin { private Context context; @Override public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) { context = flutterPluginBinding.getApplicationContext(); } @Override public void onDetachedFromEngine(FlutterPluginBinding flutterPluginBinding) { context = null; } public void showToast(String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } } ``` 4. 在Flutter插件项目的`lib`目录下,创建一个文件夹,并在其中创建一个`dart`文件,该文件将包含您要在Flutter中调用的方法。例如,您可以创建一个名为`my_plugin.dart`的文件,并在其中添加以下代码: ``` import 'package:flutter/services.dart'; class MyPlugin { static const MethodChannel _channel = const MethodChannel('my_plugin'); static Future<void> showToast(String message) async { try { await _channel.invokeMethod('showToast', {'message': message}); } on PlatformException catch (e) { print(e.message); } } } ``` 5. 在Flutter插件项目的`android/src/main`目录下,创建一个`res`目录,并在其中创建一个`values`目录。在`values`目录中,创建一个`strings.xml`文件,并添加以下代码: ``` <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">My Plugin</string> </resources> ``` 6. 在Flutter插件项目的`android/src/main`目录下,打开`AndroidManifest.xml`文件,并添加以下代码: ``` <application android:name="io.flutter.app.FlutterApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher"> <activity android:name="io.flutter.embedding.android.FlutterActivity" android:exported="true" android:theme="@style/Theme.AppCompat.Light.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> ``` 7. 在Flutter插件项目的`android/src/main`目录下,打开`MyPlugin.java`文件,并添加以下代码: ``` package com.example.my_plugin; import android.content.Context; import android.widget.Toast; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry.Registrar; public class MyPlugin implements FlutterPlugin { private Context context; private MethodChannel channel; public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), "my_plugin"); channel.setMethodCallHandler(new MyPlugin(registrar.context(), channel)); } private MyPlugin(Context context, MethodChannel channel) { this.context = context; this.channel = channel; } @Override public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) { context = flutterPluginBinding.getApplicationContext(); channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "my_plugin"); channel.setMethodCallHandler(new MyPlugin(context, channel)); } @Override public void onDetachedFromEngine(FlutterPluginBinding flutterPluginBinding) { context = null; channel.setMethodCallHandler(null); channel = null; } public void showToast(String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } private void onMethodCall(MethodCall call, MethodChannel.Result result) { if (call.method.equals("showToast")) { String message = call.argument("message"); showToast(message); result.success(null); } else { result.notImplemented(); } } } ``` 8. 在Flutter应用程序中,导入您的Flutter插件,并使用以下代码调用Android原生方法: ``` import 'package:flutter/material.dart'; import 'package:my_plugin/my_plugin.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: ElevatedButton( onPressed: () { MyPlugin.showToast('Hello World!'); }, child: Text('Show Toast'), ), ), ), ); } } ``` 这样,您就可以在Flutter应用程序中调用Android原生方法了!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值