lumen5.6配合jwt开发api

目录

通过Composer Create-Project安装lumen

配置lumen

安装 jwt

配置 table

运行 php artisan migrate 生成数据库

配置 路由

配置 guard

配置 app.php

配置 助手函数

配置 用户模型

配置handle 错误处理

配置中间件

配置控制器

最后配置 composer.json

运行 composer dumpauto 命令, 更新自动运行文件

目录结构如下:

生成密钥

用postman请求接口测试

注册请求:

登录请求:

个人信息请求 post => me:

个人信息请求 get => profile:

版本科普

α(Alpha)版

β(Beta)版

RC/ Preview版

普通发行版本

LTS(Long Term Support) 版


通过Composer Create-Project安装lumen

你还可以在终端中通过Composer的create-project命令来安装Lumen

composer create-project --prefer-dist laravel/lumen lumen5.6

配置lumen

Lumen框架的所有配置都存放在.env文件,安装好Lumen后,配置 .env内容:

APP_ENV=local                                                 #开发: local ,测试: testing ,预上线: staging ,正式环境: production
APP_DEBUG=true                                                #开启debug模式
APP_KEY=base64:yl2XKMVlbjke4e/y1kWanFG9ecaCrteWFBTg4QV4je8=   #随机字符串
APP_TIMEZONE=PRC                                              #时区
APP_LOCALE=en                                                 #语言英文

LOG_CHANNEL=daily                                             #日志记录-按天记录 , vendor /laravel/lumen-framework/config/logging.php 可以配置记录多少天内的日志
LOG_SLACK_WEBHOOK_URL=                                        #暂未使用

DB_CONNECTION=mysql                                           #数据库类型
DB_HOST=127.0.0.1                                             #数据库地址
DB_PORT=3306                                                  #数据库端口
DB_DATABASE=laravel                                           #数据库名称
DB_USERNAME=root                                              #用户名
DB_PASSWORD=root                                              #密码
DB_PREFIX=lumen_                                              #表前缀

CACHE_DRIVER=file                                             #缓存类型
QUEUE_DRIVER=sync                                             #队列驱动 sync 是同步

JWT_SECRET=KpH6rJFuMxAGZDyXbDhMPmHdOeT7JhFB                   #jwt_secret
JWT_TTL=60                                                    #jwt token的有效期
JWT_REFRESH_TTL=1440                                          #jwt 可以刷新 token 的有效期 , 有效期过后要重新请求token

 

安装 jwt

jwt-auth 最新版本是 1.0.0 rc.2 版本,已经支持了 Laravel 5.6。如果你是 Laravel 5.6 以下版本,也推荐使用最新版本,RC.1 前的版本都存在多用户token认证的安全问题。

cd lumen5.6  //切换到项目目录中
// tymon包 https://packagist.org/packages/tymon/jwt-auth
composer require tymon/jwt-auth 1.0.0-rc.2

安装doctrine/dbal扩展

在Laravel和lumen中,使用migration作为数据库的版本控制工具,当需要对已存在的数据表作更改,需要额外引入doctrine/dbal扩展。

composer require doctrine/dbal

配置 table

新建 lumen5.6/database/migrations/2014_10_12_000000_create_users_table.php

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email','50')->unique();
            $table->string('password');
            $table->string('api_token','500');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

新建 lumen5.6/database/migrations/2014_10_12_100000_create_password_resets_table.php

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePasswordResetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('password_resets');
    }
}

运行 php artisan migrate 生成数据库

配置 路由

新建 lumen5.6/routes/api.php

<?php

Route::group([
    'middleware' => 'api',  //这个本来用来实现api接口的处理,暂未用到
    'prefix' => 'auth'
], function ($app) {
    $app->post('register', 'Auth\AuthController@register');  //注册
    $app->post('login', 'Auth\AuthController@login');  //登录
    $app->post('logout', 'Auth\AuthController@logout'); //登出
    $app->post('refresh', 'Auth\AuthController@refresh'); //刷新token
    $app->post('me', 'Auth\AuthController@me'); //获取个人信息
});

Route::group(['middleware'=>'refresh.token'],function($app){
    $app->get('profile','User\UserController@profile');  //个人中心
});

配置 guard

新建 lumen5.6/config/auth.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'api'),
        'passwords' => 'users'
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "token"
    |
    */

    'guards' => [
        'api' => [
            'driver' => 'jwt',
            'provider' => 'users'
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent' ,
            'model' => App\User::class,
        ]
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | Here you may set the options for resetting passwords including the view
    | that is your password reset e-mail. You may also set the name of the
    | table that maintains all of the reset tokens for your application.
    |
    | You may specify multiple password reset configurations if you have more
    | than one user table or model in the application and you want to have
    | separate password reset settings based on the specific user types.
    |
    | The expire time is the number of minutes that the reset token should be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    */

    'passwords' => [
        //
    ],

];

配置 app.php

lumen5.6/bootstrap/app.php

<?php

require_once __DIR__.'/../vendor/autoload.php';

try {
    (new Dotenv\Dotenv(__DIR__.'/../'))->load();
} catch (Dotenv\Exception\InvalidPathException $e) {
    //
}

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| Here we will load the environment and create the application instance
| that serves as the central piece of this framework. We'll use this
| application as an "IoC" container and router for this framework.
|
*/

$app = new Laravel\Lumen\Application(
    realpath(__DIR__.'/../')
);

#官方好像没有这一步,但是使用lumen这步也是需要注意的,我是添加了这行,因为我沿用laravel的目录风格
$app->configure('auth');
//取消下面2个的注释
 $app->withFacades();
 $app->withEloquent();

/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
|
| Now we will register a few bindings in the service container. We will
| register the exception handler and the console kernel. You may add
| your own bindings here if you like or you can make another file.
|
*/

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

/*
|--------------------------------------------------------------------------
| Register Middleware
|--------------------------------------------------------------------------
|
| Next, we will register the middleware with the application. These can
| be global middleware that run before and after each request into a
| route or middleware that'll be assigned to some specific routes.
|
*/

// $app->middleware([
//    App\Http\Middleware\ExampleMiddleware::class
// ]);

 $app->routeMiddleware([
     'refresh.token' => App\Http\Middleware\RefreshToken::class,
     'api' => App\Http\Middleware\Api::class,
     'auth' => App\Http\Middleware\Authenticate::class,
 ]);

/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
|
| Here we will register all of the application's service providers which
| are used to bind services into the container. Service providers are
| totally optional, so you are not required to uncomment this line.
|
*/

 $app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class);
 $app->register(App\Providers\AppServiceProvider::class);
 $app->register(App\Providers\AuthServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);

/*
|--------------------------------------------------------------------------
| Load The Application Routes
|--------------------------------------------------------------------------
|
| Next we will include the routes file so that they can all be added to
| the application. This will provide all of the URLs the application
| can respond to, as well as the controllers that may handle them.
|
*/

$app->router->group([
    'namespace' => 'App\Http\Controllers',
], function ($router) {
    require __DIR__.'/../routes/web.php';
    require __DIR__.'/../routes/api.php';
});

return $app;

配置 助手函数

新建 lumen5.6/app/helpers.php

<?php
/**
 * Created by IntelliJ IDEA.
 * User: Administrator
 * Date: 2018-08-01
 * Time: 下午 4:28
 */
use Illuminate\Contracts\Auth\Factory as AuthFactory;

if(!function_exists('config_path')){
    /**
      * @description get the configuration path
      *
      * @param string $path
      * @return string
      * @author guilong
      * @date 2018-08-01
    */
    function config_path($path = ''){
        return app()->basePath().'/config'.($path ? '/' . $path : $path);
    }
}

if (! function_exists('bcrypt')) {
    /**
     * Hash the given value against the bcrypt algorithm.
     *
     * @param  string  $value
     * @param  array  $options
     * @return string
     */
    function bcrypt($value, $options = [])
    {
        return app('hash')->driver('bcrypt')->make($value, $options);
    }
}

if (! function_exists('request')) {
    /**
     * Get an instance of the current request or an input item from the request.
     *
     * @param  array|string  $key
     * @param  mixed   $default
     * @return \Illuminate\Http\Request|string|array
     */
    function request($key = null, $default = null)
    {
        if (is_null($key)) {
            return app('request');
        }

        if (is_array($key)) {
            return app('request')->only($key);
        }

        $value = app('request')->__get($key);

        return is_null($value) ? value($default) : $value;
    }
}

if (! function_exists('auth')) {
    /**
     * Get the available auth instance.
     *
     * @param  string|null  $guard
     * @return \Illuminate\Contracts\Auth\Factory|\Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
     */
    function auth($guard = null)
    {
        if (is_null($guard)) {
            return app(AuthFactory::class);
        }

        return app(AuthFactory::class)->guard($guard);
    }
}

配置 用户模型

<?php

namespace App;

use Tymon\JWTAuth\Contracts\JWTSubject;
//use Illuminate\Notifications\Notifiable;
//use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Lumen\Auth\Authorizable;

use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;

class User extends Model implements AuthenticatableContract, JWTSubject
{
    protected $table = 'users';
//    use Notifiable;
    use Authenticatable;

    protected $fillable = [
        'email','name','password','api_token'
    ];

    // Rest omitted for brevity

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

配置handle 错误处理

lumen5.6/app/Exceptions/Handler.php

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that should not be reported.
     *
     * @var array
     */
    protected $dontReport = [
        AuthorizationException::class,
        HttpException::class,
        ModelNotFoundException::class,
        ValidationException::class,
    ];

    /**
     * Report or log an exception.
     *
     * This is a great spot to send exceptions to Sentry, Bugsnag, etc.
     *
     * @param  \Exception  $e
     * @return void
     */
    public function report(Exception $e)
    {
        parent::report($e);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $e
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $e)
    {
        //参数验证错误的异常,我们需要返回400 的http code  和一句错误信息
        if($e instanceof ValidationException){
            return response(['error'=>array_first(array_collapse($e->errors()))],400);
        }
        //用户认证的异常,我们需要返回401的 http code 和错误信息
        if($e instanceof UnauthorizedHttpException){
            return response($e->getMessage(),401);
        }

        //http错误,返回404 错误
        if($e instanceof HttpException){
            return response($e->getMessage(),404);
        }


        return parent::render($request, $e);
    }
}

配置中间件

新建 lumen5.6/app/Http/Middleware/Api.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;

class Api
{
    /**
     * The authentication guard factory instance.
     *
     * @var \Illuminate\Contracts\Auth\Factory
     */
    protected $auth;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory  $auth
     * @return void
     */
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        if ($this->auth->guard($guard)->guest()) {
//            return response('Unauthorized.', 401);
        }
        return $next($request);
    }
}

新建 lumen5.6/app/Http/Middleware/RefreshToken.php

<?php

namespace App\Http\Middleware;

use Illuminate\Support\Facades\Auth;
use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class RefreshToken extends BaseMiddleware
{

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        //检查此次请求汇总是否带有token ,如果没有则抛出异常
        $this->checkForToken($request);

        try{
            //检查用户的登录状态,如果正常则通过
            if($this->auth->parseToken()->authenticate()){
                return $next($request);
            }

            throw new UnauthorizedHttpException('jwt-auth','未登录');
        }
        catch(TokenExpiredException $e){
            //此处捕获到了token 过期所抛出的 tokenexpiredexception 异常,我们在这里需要做的是刷新该用户的token
            try{
                //刷新用户的token
                $token = $this->auth->refresh();

                //使用一次性登录以保证此处请求的成功
                Auth::guard('api')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']);
            }
            catch(JWTException $e){
                //如果捕获到了异常,即代表refresh 也过期了 ,用户无法刷新令牌 ,需要重新登录
                throw new UnauthorizedHttpException('jwt-auth',$e->getMessage());
            }
        }
        //在响应头中返回新的token
        return $this->setAuthenticationHeader($next($request),$token);
    }
}

配置控制器

新建 lumen5.6/app/Http/Controllers/Auth/AuthController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Exception;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
    private $rule = [
        'email' => 'required|email|max:255|unique:users',
        'password' => 'required',
        'name' => 'required',
    ];

    private $message = [
        'name.required' => '姓名必须',
        'email.required'  => '邮箱必须',
        'email.email'  => '邮箱格式不正确',
        'email.max'  => '邮箱最大255个字',
        'email.unique'  => '该邮箱已存在',
        'password.required'  => '密码必须',
    ];

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        //在 Authenticate 里 使用 guard => api  验证用户信息
        $this->middleware('auth:api',['except'=>['login','register']]);
    }

    /**
     * @description register user
     *
     * @param
     * @return
     * @author guilong
     * @date 2018-08-02
     */
    public function register(){

        //直接输出错误
//        $this->validate($request, $this->rule,$this->message);
//        捕获错误
        $validator = Validator::make(request()->all(), $this->rule,$this->message);
        if($validator->fails()){
            $messages = $validator->errors();
            return response()->json([
                'code' => 501,
                'msg' => $messages->first()
            ]);
        }

        $user = [
            'email' => request()->input('email'),
            'name' => request()->input('name'),
            'password' => bcrypt(request()->input('password')),
        ];
        try{
            //插入数据库
            $user_info = \App\User::create($user);
            //获取token
            $token = JWTAuth::fromUser($user_info);
            //更新token
            \App\User::where('id','=',$user_info['id'])->update(['api_token'=>$token]);


        }
        catch(Exception $e){
//            var_dump($e->getMessage());
//            var_dump($e->getCode());
            return response()->json([
                'code' => 502,
                'msg' => $this->message['email.unique']
            ]);
        }

        return response()->json([
            'code' => 200,
            'msg' => '',
            'access_token' => $token
        ]);
    }

    public function login(){
        //直接输出错误
//        $this->validate($request, $this->rule,$this->message);
//        捕获错误
        $validator = Validator::make(request()->all(), ['email'=>'required|email|max:255','password'=>$this->rule['password']],$this->message);
        if($validator->fails()){
            $messages = $validator->errors();
            return response()->json([
                'code' => 501,
                'msg' => $messages->first()
            ]);
        }

        $credentials = request(['email','password']);

        if(! $token = auth()->attempt($credentials)){
            return response()->json([
                'code' => 401,
                'msg' => '登录失败'
            ]);
        }

        return response()->json([
            'code' => 200,
            'msg' => '',
            'data' => [
                'access_token' => $token,
                'token_type' => 'bearer',
                'expires_in' => auth()->factory()->getTTL()*60
            ]
        ]);
    }

    public function me(){

        try {
            $user = auth()->userOrFail();
        } catch (\Tymon\JWTAuth\Exceptions\UserNotDefinedException $e) {
            return response()->json([
                'code' => 401,
                'msg' => '登录失败',
            ]);
        }

        return response()->json([
            'code' => 200,
            'msg' => '',
            'data' => $user,
        ]);
    }

    public function logout(){
        auth()->logout();

        return response()->json([
           'code' => 200,
           'msg' => 'logged out',
        ]);
    }

    public function refresh(){
        return response()->json([
            'code' => 200,
            'msg' => '',
            'data' => [
                'access_token' => auth()->refresh(),
                'token_type' => 'bearer',
                'expires_in' => auth()->factory()->getTTL()*60
            ]
        ]);
    }


}

新建 lumen5.6/app/Http/Controllers/User/UserController.php

<?php

namespace App\Http\Controllers\User;

use App\Http\Controllers\Controller;

class UserController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api');
    }

    public function profile(){
        try {
            $user = auth()->userOrFail();
        } catch (\Tymon\JWTAuth\Exceptions\UserNotDefinedException $e) {
            return response()->json([
                'code' => 401,
                'msg' => '登录失败',
            ]);
        }

        return response()->json([
            'code' => 200,
            'msg' => '',
            'data' => $user,
        ]);
    }
}

最后配置 composer.json

    //在 autoload 里加入  files 这一段    

    "autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "files":[
            "app/helpers.php"
        ]
    },

运行 composer dumpauto 命令, 更新自动运行文件

目录结构如下:

生成密钥

jwt-auth 已经预先定义好了一个 Artisan 命令方便你生成 Secret,你只需要在你的 shell 中运行如下命令即可:

shell

$ php artisan jwt:secret

此命令会在你的 .env 文件中新增一行 JWT_SECRET=secret

 

用postman请求接口测试

注册请求:

登录请求:

个人信息请求 post => me:

个人信息请求 get => profile:

 

如图可以看到我们已经拿到了新的 token,接下来的事情便会交由我们前面设置的 axios 拦截器处理,它会将本地的 token 替换为此 token。

版本科普

感觉蛮多人对版本没什么概念,所以在这里科普下常见的版本。

  • α(Alpha)版

    ​ 这个版本表示该 Package 仅仅是一个初步完成品,通常只在开发者内部交流,也有很少一部分发布给专业测试人员。一般而言,该版本软件的 Bug 较多,普通用户最好不要安装。

  • β(Beta)版

    该版本相对于 α(Alpha)版已有了很大的改进,修复了严重的错误,但还是存在着一些缺陷,需要经过大规模的发布测试来进一步消除。通过一些专业爱好者的测试,将结果反馈给开发者,开发者们再进行有针对性的修改。该版本也不适合一般用户安装。

  • RC/ Preview版

    RC 即 Release Candidate 的缩写,作为一个固定术语,意味着最终版本准备就绪。一般来说 RC 版本已经完成全部功能并清除大部分的 BUG。一般到了这个阶段 Package 的作者只会修复 Bug,不会对软件做任何大的更改。

  • 普通发行版本

    一般在经历了上面三个版本后,作者会推出此版本。此版本修复了绝大部分的 Bug,并且会维护一定的时间。(时间根据作者的意愿而决定,例如 Laravel 的一般发行版本会提供为期一年的维护支持。)

  • LTS(Long Term Support) 版

    该版本是一个特殊的版本,和普通版本旨在支持比正常时间更长的时间。(例如 Laravel 的 LTS 版本会提供为期三年的 维护支持。)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值