TP5 自动注册Hook机制钩子扩展,带完整案例

钩子机制我这里分为两种使用方法,注:二者只能选其一
1、采用thinkphp5内置的钩子调用方案。 配置参数 jntoo_hook_call 为true 情况下才有效,为了兼容已经写好了钩子机制的使用
2、采用自有的钩子调用方案,此方法支持钩子分类,钩子权重自动排序

个人不建议使用think内置钩子作为系统的扩展。

如何写自动加载文件:
在模块下新建一个hook目录,并在目录下创建Category.php文件,如下图:


类名等于钩子类型,方法名等于钩子名称。(注:类型只存在于钩子机制方法2。方法1只有方法名生效)
如图所示:


我们创建了 Category 类型的钩子
里面有两个方法:
index 和 index_5

方法1的调用方法:\think\Hook::listen('index'); 就可以直接调用到Category 类 index 方法。
输出结果:


方法2的调用方法:\app\common\Hook::call('Category','index'); 就可以调用到 Category 类 index 方法和index_5 方法
输出结果:


方法2为什么能 调用Category.index 钩子时会调用到Category.index_5 这个钩子呢?这个我们后面进行一个讲解。


讲个小故事
有一天客户说要求实现一个会员的注册模块。
我这里键盘敲了半天完成了代码会员的注册模块。
第二天客户说“现在客户注册没问题了,我想能不能在注册完成后给他添加50积分,让他能进行一些消费。”,我说“没问题”
敲敲敲半天代码我完成了客户的要求。现在注册会员后,有赠送50积分了。
第三天同事问我你前两天不是在系统里弄了个注册模块和赠送积分?我说是啊。同事说“把你这个提交一下svn库,我这边也有个客户想弄个注册模块和赠送积分,还要添加发送邮件验证”,我:“好的,不过你要自己添加发送邮件验证。”代码提交到svn库,
同事敲了半天代码,完成了发送邮件。
第四天我找同事让他提交一下SVN库,让我进行一下代码同步。
第五天另一个客户问我“你们的注册模块有没有会员注册验证手机”,这个我当然只能说有(实际上是没有短信认证),客户说”能装好环境让我测试一下?“,我只能说:“抱歉先生,我们现在还没装好环境,这个我明天给您装好,您在测试一下”,客户说“好”
蹭蹭蹭敲了半天代码在会员注册模块上面加了会员注册手机验证。
第六天客户觉得挺好的。
故事讲到这里。
假设以上会员注册模块的形成是这样的一段代码片段:

  1. namespace app\index\controller;
  2.  
  3. class member{
  4.        function register($username , $password , $tel , $email){
  5.               // 第五天加的代码:
  6.               if(是否短信验证)
  7.                      VerifTel($tel , 验证码);
  8.               // 第一天写的代码:
  9.               if(table('member')->where('username' , $username)->count())
  10.               {
  11.                      $this->error = '用户名已存在'
  12.                      return false;
  13.               }
  14.               $uid = 写入数据库得到新的用户UID
  15.               ....省略大部分代码
  16.              
  17.               // 第二天加的代码:
  18.               if(赠送积分)
  19.                     sendPoints($uid . 赠送积分); // 发送积分50
  20.               // 第三天
  21.              if(发送邮件)
  22.                    sendEmail($uid , 邮件);
  23.              
  24.              return true;
  25.        }
  26. }

从以上我们可以看出,当写完一个功能模块的时候,并不知道客户一些需求的情况,我们会在当前模块中加入代码。
如果一直下去加下去,回头看代码的时候你会发现你的代码实在是太多了。
这个代码是不是可以变得优雅一些

如果我们在这个代码中加入钩子机制会是怎么样的呢?
看如下代码

  1. namespace app\index\controller;
  2. class member{
  3.        function register($username , $password , $tel , $email){
  4.               
  5.               // 我不知道以后会变成什么样,先加个前置钩子,参数:$username,$password,$tel,$email
  6.               $param = [ $username,$password,$tel,$email ];
  7.               \app\common\Hook::call('member','registerPre',$param);
  8.                
  9.               // 第一天写的代码:
  10.               if(table('member')->where('username' , $username)->count())
  11.               {
  12.                      $this->error = '用户名已存在'
  13.                      return false;
  14.               }
  15.               $uid = 写入数据库得到新的用户UID
  16.               ....省略大部分代码
  17.              
  18.              // 我不知道以后会变成什么样,先加个注册成功后的钩子,参数 $uid
  19.               $param = [ $uid ];
  20.               \app\common\Hook::call('member','registerSuccess',$param);
  21.               
  22.              return true;
  23.        }
  24. }

那么问题来了我们怎么去解决钩子的问题?
假设我们的类都已经注册进了钩子里:
我们来完成第二天的事情,如下代码:

  1. namespace app\index\hook;
  2. class member{
  3.        // 钩子类型,挂入 member 类型钩子,中的registerSuccess钩子
  4.        function registerSuccess($uid)
  5.        {
  6.               if(赠送积分)
  7.                     sendPoints($uid . 赠送积分); // 发送积分50
  8.        }
  9. }

第三天的时候同事要用我的代码怎么办?那么我会告诉他:“我这边已经下了两个钩子,一个是注册前置钩子,一个是注册成功后的钩子”并告诉他钩子类型,名称,还有相应的参数,同事的代码将变成这样

  1. namespace app\tongshi\hook;
  2. class member{
  3.        // 钩子类型,挂入 member 类型钩子,中的registerSuccess钩子
  4.        function registerSuccess($uid)
  5.        {
  6.               if(发送邮件)
  7.                    sendEmail($uid , 邮件);
  8.        }
  9. }

第五天的时候这样的:

  1. namespace app\index\hook;
  2. class member{
  3.        // 挂入 member 类型钩子,中的registerSuccess钩子
  4.        function registerSuccess($uid)
  5.        {
  6.               if(赠送积分)
  7.                     sendPoints($uid . 赠送积分); // 发送积分50
  8.        }
  9.        // 挂入member 类型钩子中的registerPre钩子
  10.        function registerPre()
  11.        {
  12.               if(是否短信验证)
  13.                      VerifTel($tel , 验证码);
  14.        }
  15. }

回到我们上面讲的“为什么能 调用Category.index 钩子时会调用到Category.index_5 这个钩子呢?”

在钩子机制设计时想到钩子有可能有先后顺序,而设计了一个能写入权重排序的数字,在尾部添加数字,排序从小到大排序,实际的钩子注册会自动删除尾部的“_数字”:



一:先来讲讲钩子机制
在项目代码中,你认为要扩展(暂时不扩展)的地方放置一个钩子函数,等需要扩展的时候,把需要实现的类和函数挂载到这个钩子上,就可以实现扩展了。
思想就是这样听起来比较笼统。
在二次开发别人写的代码时,如果有钩子机制,作者会告诉你哪些地方给下了钩子,当你扩展时就可以在不改动原代码的情况下进行一个升级扩展。


本插件扩展配置参数信息:

  1. 1、jntoo_hook_cache
  2.      逻辑值,是否开启钩子编译缓存,开启后只需要编译一次,以后都将成为惰性加载,如果安装了新的钩子,需要先调用Hook::clearCache() 清除缓存
  3.  
  4. 2、jntoo_hook_call
  5.      逻辑值,是否使用think钩子系统。值为真使用think钩子后,将无法使用权重排序和钩子自动分类,只有类中的方法将自动注册到think钩子机制中
  6.  
  7. 3、jntoo_hook_path
  8.      某个文件夹下的PHP文件都自动为其注册钩子
  9.      配置实现
  10.      jntoo_hook_path => [
  11.            [
  12.                  'path'=>'你的路径', // 路径尾部必须加斜杠 "/"
  13.                  'pattern'=> '规则,类的匹配规则' 例如:'/plugin\\\\module\\\\hook\\\\([0-9a-zA-Z_]+)/'
  14.            ],
  15.            ....
  16.     ]
  17.  
  18. 4、jntoo_hook_plugin
  19.      多模块目录自动编译,在模块文件夹下加入hook目录,此目录下的php文件会自动注册钩子
  20.      配置实现:
  21.      'jntoo_hook_plugin' => [
  22.             [
  23.                  'path'=>'你的app模块路径',
  24.                  'pattern'=> '规则,类的匹配规则' 例如:'/plugin\\\\([0-9a-zA-Z_]+)\\\\hook\\\\([0-9a-zA-Z_]+)/'
  25.            ],
  26.            ....
  27.      ]


请在application/tags.php
'app_init'行为处加上:'\\app\\common\\Hook'
例如:
// 应用行为扩展定义文件
return [
// 应用初始化
'app_init' => [
'\\app\\common\\Hook'
],
// 应用开始
'app_begin' => [],
// 模块初始化
'module_init' => [],
// 操作开始执行
'action_begin' => [],
// 视图内容过滤
'view_filter' => [],
// 日志写入
'log_write' => [],
// 应用结束
'app_end' => [],
];






请自行创建文件:

application\index\controller\Index.php下钩子文件

  1. namespace app\index\controller;
  2.  
  3. use app\common\Hook;
  4. use think\Hook AS thinkHook;
  5. class Index
  6. {
  7.     public function index()
  8.     {
  9.         Hook::call('Category' , 'index');
  10.     }
  11. }

application\index\hook\Category.php钩子文件

  1. <?php
  2. /**
  3.  * Created by PhpStorm.
  4.  * User: JnToo
  5.  * Date: 2016/11/12
  6.  * Time: 1:11
  7.  */
  8.  
  9. namespace app\index\hook;
  10. class Category
  11. {
  12.     function index()
  13.     {
  14.         echo '我是Category类型钩子中的index方法<br>';
  15.     }
  16.  
  17.     function index_5()
  18.     {
  19.         echo '我是Category类型钩子中的index方法,我的权重比较低<br>';
  20.     }
  21. }

application\common\hook.php主文件

  1. <?php
  2. /**
  3.  * Created by PhpStorm.
  4.  * User: JnToo
  5.  * Date: 2016/11/11
  6.  * Time: 22:57
  7.  */
  8. namespace app\common;
  9. use think\Config;
  10. use think\Hook as thinkHook;
  11. use think\Cache;
  12.  
  13. /**
  14.  * 请在application/tags.php
  15.  * 'app_init'行为处加上:'\\app\\common\\Hook'
  16.  * 例如:
  17.  * // 应用行为扩展定义文件
  18. return [
  19. // 应用初始化
  20. 'app_init'     => [
  21. '\\app\\common\\Hook'
  22. ],
  23. // 应用开始
  24. 'app_begin'    => [],
  25. // 模块初始化
  26. 'module_init'  => [],
  27. // 操作开始执行
  28. 'action_begin' => [],
  29. // 视图内容过滤
  30. 'view_filter'  => [],
  31. // 日志写入
  32. 'log_write'    => [],
  33. // 应用结束
  34. 'app_end'      => [],
  35. ];
  36.  
  37.  * Class Hook
  38.  * @package app\common
  39.  */
  40.  
  41.  
  42. class Hook
  43. {
  44.     /**
  45.      * 编译钩子时使用计数器
  46.      * @var int
  47.      */
  48.     static protected $index = 0;
  49.  
  50.     /**
  51.      * 添加引用计数
  52.      * @var int
  53.      */
  54.     static protected $indexAdd = 1;
  55.  
  56.     /**
  57.      * 已编译好的钩子列表
  58.      * @var array
  59.      */
  60.     static protected $hookList = array();
  61.  
  62.     /**
  63.      * application/config.php 文件中加入如下的配置信息
  64.      * @var array
  65.      */
  66.     static protected $default =[
  67.         // 是否开启钩子编译缓存,开启后只需要编译一次,以后都将成为惰性加载,如果安装了新的钩子,需要先调用Hook::clearCache() 清除缓存
  68.         'jntoo_hook_cache'=>false,
  69.         // 钩子是否使用think钩子系统
  70.         'jntoo_hook_call'=>false ,
  71.         /**
  72.          * 某个文件夹下hook加载,配置文件方法实现
  73.          * jntoo_hook_path => [
  74.          *     [
  75.          *          'path'=>'你的路径', // 路径尾部必须加斜杠 "/"
  76.          *          'pattern'=> '规则,类的匹配规则' 例如:'/plugin\\\\module\\\\hook\\\\([0-9a-zA-Z_]+)/'
  77.          *     ],
  78.          *     ....
  79.          * ]
  80.          */
  81.         'jntoo_hook_plugin'=>[],
  82.         /**
  83.          *  多模块目录下自动搜索,配置文件方法实现
  84.          * 'jntoo_hook_plugin' => [
  85.          *     [,
  86.          *          'path'=>'你的app模块路径'
  87.          *          'pattern'=> '规则,类的匹配规则' 例如:'/plugin\\\\([0-9a-zA-Z_]+)\\\\hook\\\\([0-9a-zA-Z_]+)/'
  88.          *     ],
  89.          *     ....
  90.          * ]
  91.          */
  92.         'jntoo_hook_plugin'=>[],
  93.     ];
  94.  
  95.     /**
  96.      * 提供行为调用
  97.      */
  98.     public function run()
  99.     {
  100.         self::init();
  101.     }
  102.  
  103.     /**
  104.      * 注册钩子
  105.      * @param $type 钩子类型
  106.      * @param $name 钩子名称
  107.      * @param $param \Closure|array
  108.      */
  109.     static public function add($type , $name , $param , $listorder = 1)
  110.     {
  111.         $key = strtolower($type .'_'.$name);
  112.         isset(self::$hookList[$key]) or self::$hookList[$key] = [];
  113.         self::$hookList[$key][$listorder.'_'.self::$indexAdd++] = $param;
  114.         ksort(self::$hookList[$key]);
  115.         // 兼容
  116.         if(Config::get('jntoo_hook_call'))
  117.         {
  118.             thinkHook::add($name , $param);
  119.         }
  120.         return;
  121.     }
  122.  
  123.     /**
  124.      * 清除编译钩子的缓存
  125.      */
  126.     static public function clearCache()
  127.     {
  128.         // 清楚编译钩子缓存
  129.         if(Config::get('jntoo_hook_cache')){
  130.             cache('jntoo_hook_cache' , null);
  131.         }
  132.     }
  133.  
  134.     /**
  135.      * 执行钩子
  136.      * @param $type string
  137.      * @param $name string
  138.      * @param array $array
  139.      * @param mixe
  140.      */
  141.     static public function call($type , $name , &$array = array())
  142.     {
  143.         static $_cls = array();
  144.  
  145.         $ret = '';
  146.         if(Config::get('jntoo_hook_call')){
  147.             return thinkHook::listen($name , $array);
  148.         }else{
  149.  
  150.             $key = strtolower($type.'_'.$name);
  151.             // 自有的调用方案
  152.             if(isset(self::$hookList[$key]))
  153.             {
  154.  
  155.                 foreach(self::$hookList[$key] as $r){
  156.                     // 闭包处理
  157.                     $result = '';
  158.                     if(is_callable($r)){
  159.                         $result = call_user_func_array($r, $array);
  160.                     }elseif(is_object($r)){
  161.                         // 自己定义对象钩子
  162.                         if(method_exists($r , $name)){
  163.                             $result = call_user_func_array(array($r , $name), $array);
  164.                         }
  165.                     }else{
  166.                         // 自动搜索出来的钩子
  167.                         $class = $r['class'];
  168.                         if(class_exists($class , false)){
  169.                             // 如果不存在
  170.                             if($r['filename'])require_once(ROOT_PATH.$r['filename']);
  171.                         }
  172.                         if(class_exists($class , false)){
  173.                             if(!isset($_cls[$class])){
  174.                                 $_cls[$class] = new $class();
  175.                             }
  176.                             $func = $r['func'];
  177.                             $result = call_user_func_array(array($_cls[$class] , $func), $array);
  178.                         }
  179.                     }
  180.                     if($result)$ret.=$result;
  181.                 }
  182.             }
  183.         }
  184.         return $ret;
  185.     }
  186.  
  187.     /**
  188.      * 初始化钩子
  189.      */
  190.     static protected function init()
  191.     {
  192.         // 取钩子的缓存
  193.         self::$hookList = self::getCache();
  194.         if(!self::$hookList)
  195.         {
  196.             // 保存在当前变量中
  197.             $saveArray = [];
  198.  
  199.             // 钩子不存在,先搜索app目录下的模块
  200.             //echo APP_PATH;
  201.             //echo ROOT_PATH;
  202.  
  203.             $result = self::searchDir(APP_PATH);
  204.             // 先编译此模块
  205.             self::compileHook($result , '/app\\\\([0-9a-zA-Z_]+)\\\\hook\\\\([0-9a-zA-Z_]+)/' , $saveArray);
  206.             //print_r($saveArray);
  207.             // 多模块实现搜索加载
  208.             $jntooHook = Config::get('jntoo_hook_plugin');
  209.             if($jntooHook){
  210.                 foreach($jntooHook as $t){
  211.                     $result = self::searchDir($t['path']);
  212.                     self::compileHook($result , $t['pattern'] , $saveArray);
  213.                 }
  214.             }
  215.  
  216.             // 单个路径的模块搜索
  217.             $jntooHook = Config::get('jntoo_hook_path');
  218.             if($jntooHook){
  219.                 foreach($jntooHook as $t){
  220.                     $result = [];
  221.                     self::searchHook($t['path'] , $result);
  222.                     self::compileHook($result , $saveArray);
  223.                 }
  224.             }
  225.             // 编译完成,现在进行一个权重排序
  226.             foreach($saveArray as $k=>$t){
  227.                 ksort($saveArray[$k]);
  228.             }
  229.             self::setCache($saveArray);
  230.             self::$hookList = $saveArray;
  231.         }
  232.         //print_r(self::$hookList);
  233.         $calltype = Config::get('jntoo_hook_call');
  234.         // 检测他的调用方法,是否需要注册到think中,不建议注册到 think 中,
  235.         // 因为这个系统含有分类的形式,注册进去后将无法使用排序功能
  236.         if($calltype){
  237.             // 注册进think 钩子中
  238.             self::registorThink();
  239.         }else{
  240.             // 注册系统行为钩子
  241.             self::registorCall();
  242.         }
  243.     }
  244.  
  245.     /**
  246.      * 注册系统行为调用
  247.      */
  248.     static protected function registorCall()
  249.     {
  250.         thinkHook::add('app_init' , function( &$params = null ){
  251.             $arg = [&$params];
  252.             Hook::call('system' , 'app_init' , $arg);
  253.         });
  254.         thinkHook::add('app_begin' , function( &$params = null ){
  255.             $arg = [&$params];
  256.             Hook::call('system' , 'app_begin' , $arg);
  257.         });
  258.         thinkHook::add('module_init' , function( &$params = null ){
  259.             $arg = [&$params];
  260.             Hook::call('system' , 'module_init' , $arg);
  261.         });
  262.         thinkHook::add('action_begin' , function( &$params = null ){
  263.             $arg = [&$params];
  264.             Hook::call('system' , 'action_begin' , $arg);
  265.         });
  266.         thinkHook::add('view_filter' , function( &$params = null ){
  267.             $arg = [&$params];
  268.             Hook::call('system' , 'view_filter' , $arg);
  269.         });
  270.         thinkHook::add('app_end' , function( &$params = null ){
  271.             $arg = [&$params];
  272.             Hook::call('system' , 'app_end' , $arg);
  273.         });
  274.         thinkHook::add('log_write' , function( &$params = null ){
  275.             $arg = [&$params];
  276.             Hook::call('system' , 'log_write' , $arg);
  277.         });
  278.         thinkHook::add('response_end' , function( &$params = null ){
  279.             $arg = [&$params];
  280.             Hook::call('system' , 'response_end' , $arg);
  281.         });
  282.     }
  283.  
  284.     /**
  285.      * 将钩子注册进thinkHook 钩子中
  286.      */
  287.     static protected function registorThink()
  288.     {
  289.         foreach(self::$hookList as $key=>$list)
  290.         {
  291.             foreach($list as $r){
  292.                 thinkHook::add($r['func'] , $r['class']);
  293.             }
  294.         }
  295.     }
  296.     /**
  297.      * 搜索目录下的钩子文件
  298.      * @param $path string
  299.      * @param $saveArray array 保存的文件路径
  300.      * @return null
  301.      */
  302.     static protected function searchHook( $path , &$saveArray)
  303.     {
  304.         $fp = opendir($path);
  305.         if($fp){
  306.             while($file = readdir($fp))
  307.             {
  308.                 if(substr($file , -4) == '.php')
  309.                 {
  310.                     $saveArray[] = $path.$file;
  311.                 }
  312.             }
  313.         }
  314.     }
  315.  
  316.     /**
  317.      * 编译钩子,编译后直接保存在静态成员变量 self::$hookList
  318.      * @param $filelist array 文件路径
  319.      * @param $namespace string 命名空间规则
  320.      * @param $saveHook array 保存Hook
  321.      * @return null
  322.      */
  323.     static protected function compileHook($filelist , $namespace , &$saveHook)
  324.     {
  325.         $root_path = strtr(ROOT_PATH,'\\' , '/');
  326.         //print_r($filelist);
  327.         // 当前引用计数
  328.         $index = self::$index;
  329.         $indexAdd = self::$indexAdd;
  330.         foreach ($filelist as $file)
  331.         {
  332.             require_once($file);
  333.             // 获取已经加载的类
  334.             $class_list = get_declared_classes();
  335.             // 搜索计数器
  336.             for($len = count($class_list);$index<$len;$index++)
  337.             {
  338.                 $classname = $class_list[$index];
  339.                 if(preg_match($namespace , $classname))
  340.                 {
  341.                     // 这个类满足我们的需求
  342.                     $ec = new \ReflectionClass($classname);
  343.                     // 钩子的类型
  344.                     $type = basename(strtr($classname , '\\' , '/'));
  345.                     foreach($ec->getMethods() as $r){
  346.                         if($r->name[0] != '_' && $r->class == $classname){
  347.                             // 暂时还不知道怎么实现排序 方法名后面有
  348.                             $name = $r->name;
  349.                             $listorder = 1;
  350.                             if(strpos($name , '_') !== false){
  351.                                 // 存在排序
  352.                                 $temp = explode('_',$name);
  353.                                 $num  = array_pop($temp);
  354.                                 if(is_numeric($num)){
  355.                                     $name = implode('_' , $temp);
  356.                                     $listorder = $num;
  357.                                 }
  358.                             }
  359.                             $typename = strtolower($type.'_'.$name);
  360.                             !isset($saveHook[$typename]) AND $saveHook[$typename] = [];
  361.  
  362.                             $saveHook[$typename][$listorder.'_'.$indexAdd++] = [
  363.                                 'filename'=>str_replace($root_path,'',$file), // 保存文件路径的好处是方便快速加载,无需在进行路径的查找
  364.                                 'class'=>$classname, // 保存类的名称
  365.                                 'func'=>$r->name,   // 保存方法名
  366.                                 'listorder'=>$listorder // 排序,编译完成后,进行一个权重的排序
  367.                             ];
  368.                         }
  369.                     }
  370.                 }
  371.             }
  372.         }
  373.         self::$index = $index;
  374.         self::$indexAdd = $indexAdd;
  375.     }
  376.  
  377.     /**
  378.      * @param $path 搜索模块路径
  379.      * @return array
  380.      */
  381.     static protected function searchDir( $path )
  382.     {
  383.         // 目录自动补全
  384.         $path = strtr(realpath($path),'\\' , '/');
  385.         $char = substr($path,-1);
  386.         if( $char != '/' || $char != '\\' ){
  387.             $path .= '/';
  388.         }
  389.         $path .= '*';
  390.  
  391.         $dirs = glob($path, GLOB_ONLYDIR );
  392.         $result = array();
  393.         foreach($dirs as $dir){
  394.             if(is_dir($dir .'/hook'))
  395.             {
  396.                 self::searchHook($dir .'/hook/' , $result);
  397.             }
  398.         }
  399.         return $result;
  400.     }
  401.     /**
  402.      * 获取编译好的钩子
  403.      * @return bool|array
  404.      */
  405.     static protected function getCache()
  406.     {
  407.         if(Config::get('jntoo_hook_cache')){
  408.             // 获取缓存
  409.             return cache('jntoo_hook_cache');
  410.         }
  411.         return false;
  412.     }
  413.  
  414.     /**
  415.      * 保存编译的缓存
  416.      * @param $value array
  417.      * @return bool
  418.      */
  419.     static protected function setCache( $value )
  420.     {
  421.         // 设置为永久缓存
  422.         if(Config::get('jntoo_hook_cache')){
  423.             cache('jntoo_hook_cache' , $value , null);
  424.         }
  425.         return true;
  426.     }
  427.  
  428. }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值