Android学习笔记——内容提供器

参考书籍:Android第一行代码(第二版).郭霖著

文件存储、SharedPreferences存储及数据库存储所保存的数据只能在当前应用程序中访问(虽然文件存储和SharedPreferences存储中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE操作模式供其它应用程序访问当前应用数据,但在Android4.2中已废弃)。
跨程序数据共享应使用更安全可靠的内容提供器(Android跨程序共享数据的标准方式)。内容提供器提供一套完整机制,允许一个程序访问另一个程序中数据(保证数据安全性),可选择对哪部分数据进行共享(保证隐私不会泄露)。

1、运行时权限

之前的Android权限机制在保护用户安全和隐私方面作用有限。在Android6.0中引入运行时权限功能,更好保护用户安全隐私。
(1)Android权限机制
项目中添加权限声明后,如果用户在低于6.0系统的设备上安装此程序,就会清楚知晓此程序申请了哪些权限从而决定是否安装,但容易造成”店大欺客”的情况。
在6.0系统中加入了运行时权限功能,用户不需在安装软件时一次性授予所有申请的权限,在软件使用过程中可再对某一权限进行授予。
Android将所有权限归成两类:普通权限(不会直接威胁用户安全隐私,自动授权)和危险权限(可能会触及隐私、影响设备安全性,必须用户手动点击授权)。
危险权限:
这里写图片描述

除以上9组24个权限外都是普通权限(在AndroidManifest.xml中添加权限声明即可)。注意:每个危险权限都属于一个权限组,在进行运行时权限处理时使用的是权限名(用户一旦同意授权,对应权限组中所有其他权限会同时被授权)。

(2)在程序运行时申请权限

新建一个RuntimePermissionTest项目。修改布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/make_call"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Make Call" />
</LinearLayout>

修改MainAcitivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button makeCall = (Button) findViewById(R.id.make_call);
        makeCall.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {//直接拨打电话,必须声明权限,为防止程序崩溃,所有操作都放在异常捕获代码块中
                    Intent intent = new Intent(Intent.ACTION_CALL);
                    intent.setData(Uri.parse("tel:10086"));
                    startActivity(intent);
                }catch (SecurityException e){
                    e.printStackTrace();
                }
            }
        });
    }
}

修改AndroidManifest.xml文件,添加:

<uses-permission android:name="android.permission.CALL_PHONE" />

这样在低于Android6.0系统手机上可以运行,但在6.0及更高版本系统运行没有任何效果,会提示权限被禁止的错误信息(必须进行运行时处理)。
修改MainActivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button makeCall = (Button) findViewById(R.id.make_call);
        makeCall.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
                    //判断用户是否已给我们授权,第二个参数是具体权限名,使用checkSelfPermission方法的返回值与PackageManager.PERMISSION_GRANTED作比较,相等说明已授权
                    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE }, 1);
                    //没授权则需调用ActivityCompat.requestPermissions方法向用户申请授权,第二个参数是String数组,把要申请的权限名放入即可,
                    // 第三个参数是请求码,只要是唯一值即可
                }else {
                    //已授权则直接执行拨打电话逻辑操作
                    call();
                }
            }
        });
    }
    private void call(){
        try {//直接拨打电话,必须声明权限,为防止程序崩溃,所有操作都放在异常捕获代码块中
            Intent intent = new Intent(Intent.ACTION_CALL);
            intent.setData(Uri.parse("tel:10086"));
            startActivity(intent);
        }catch (SecurityException e){
            e.printStackTrace();
        }
    }
    //调用requestPermissions()方法后,系统会弹出一个权限申请对话框(用户选择同意或拒绝),不论哪种结果,最终都会回调到onRequestPermissionsResult()方法中
    //授权结果会封装在grantResults参数中
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                //判断授权结果
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    call();
                }else {
                    Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
    }
}

重新运行程序,点击按钮。
这里写图片描述
点击DENY:
这里写图片描述
再次点击按钮,选择ALLOW:这里写图片描述
如果再次点击Make Call按钮就不会再弹出权限申请对话框了。如想关闭授权,进入Settings->Apps->RuntimePermissionTest->Permissions,进行关闭即可。

2、访问其他程序中的数据

内容提供器有两种用法:使用现有的内容提供器读取和操作相应程序中数据,创建自己的内容提供器给程序数据提供外部访问接口。
如果应用程序通过内容提供器对其数据提供了外部访问接口,其他任何应用程序都可对这部分数据进行访问。(Android中自带的电话簿、短信、媒体库等都提供了类似的访问接口)

(1)ContentResolver的基本用法

如果要想访问内容提供器中共享的数据,就一定要借助ContentResolver类(通过Context中的getContentResolver方法获取实例)。此类提供了一系列CRUD方法。
不同于SQLiteDatabases,此类中的增删查改方法不接收表名,用Uri参数代替(内容URI)。内容URI由两部分构成:authority(区分不同应用程序,一般用程序包名)和path(区分不同表),如:
content://com.example.app.provider/table1
得到内容URI后,需解析成Uri对象(Uri.parse()方法)才可作为参数传入。可使用此Uri对象查询table1中的数据:
Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
这里写图片描述
仍然返回Cursor对象。
增删改方法类似。

(2)读取系统联系人
需先手动添加几个联系人以便稍后读取。新建一个ContactsTest项目,编写布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/contacts_View"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

为了将关注重点放在读取系统联系人上,没有使用RecyclerView(代码偏多)。修改MainActivity:

public class MainActivity extends AppCompatActivity {
    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView contactsView = (ListView) findViewById(R.id.contacts_View);
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
        contactsView.setAdapter(adapter);
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, 1);

        }else {
            readContacts();
        }
    }
    private void readContacts(){
        Cursor cursor = null;
        try{
            //查询联系人数据
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
            //ContactsContract.CommonDataKinds.Phone.CONTENT_URI常量为Uri.parse()解析出来的结果
            if (cursor != null){
                while (cursor.moveToNext()){
                    //获取联系人姓名
                    String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                    //获取联系人手机号
                    String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                    contactsList.add(displayName + "\n" + number);
                }
                //通知刷新ListView
                adapter.notifyDataSetChanged();
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (cursor != null){
                //关闭Cursor对象
                cursor.close();
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    readContacts();
                }else {
                    Toast.makeText(this, "You denied the permisson", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
    }
}

还需声明读取系统联系人权限:

运行程序测试。
这里写图片描述

3、创建自己的内容提供器

(1)创建步骤

新建类继承ContentProvider来创建内容提供器。ContentProvider类中有6个抽象方法,继承时需重写。新建MyProvider:

public class MyProvider extends ContentProvider {
    @Override
    //初始化内容提供器时调用,通常在此完成对数据库的创建和升级等操作,返回true表示初始化成功
    //只有当存在ContentResolver尝试访问程序中数据时,内容提供器才会被初始化
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    //从内容提供器中查询数据,参数:第一哪张表,第二哪些列,第三第四约束查询哪些行,第五对结果进行排序,结果存在Cursor对象中
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    //根据传入的内容URI返回相应的MIME类型
    public String getType(Uri uri) {
        return null;
    }

    @Nullable
    @Override
    //向内容提供器添加一条数据,uri参数确定目标表,新数据保存在values参数中。返回用于表示这条新记录的URI
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    //删除内容提供器中数据。返回被删除的行数
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    //更新内容提供器中已有数据。返回受影响的行数
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

几乎每个方法都有Uri参数(调用ContentResolver的增删查改方法时传递过来的)。
需要对传入的Uri进行解析,分析出调用方期望访问的表和数据。内容URI主要有两种格式,可通过通配符分别匹配:
匹配任意表(*表示匹配任意长度任意字符)
content://com.example.app.provider/*
匹配table1表中任意行数据(#表示匹配任意长度数字)
content://com.example.app.provider/table1/#
接着借助UriMatcher类实现匹配内容URI功能。此类提供了addURI()方法,接收三个参数:authority,path,自定义代码。当调用UriMatcher的match()方法时,将Uri对象闯入,返回值为能匹配这个Uri对象对应的自定义代码(可判断调用方期望访问哪张表中数据)。修改MyProvider:

public class MyProvider extends ContentProvider {
    public static final int TABLE1_DIR = 0;
    public static final int TABLE1_ITEM = 1;
    public static final int TABLE2_DIR = 2;
    public static final int TABLE2_ITEM = 3;
    private static UriMatcher uriMatcher;
    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        //将期望匹配的内容URI格式传入
        uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
        uriMatcher.addURI("com.example.app.provider", "table1/#", TABLE1_ITEM);
        uriMatcher.addURI("com.example.app.provider", "table2", TABLE2_DIR);
        uriMatcher.addURI("com.example.app.provider", "table2/#", TABLE2_ITEM);
    }
   ...

    @Nullable
    @Override
    //从内容提供器中查询数据,参数:第一哪张表,第二哪些列,第三第四约束查询哪些行,第五对结果进行排序,结果存在Cursor对象中
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        switch (uriMatcher.match(uri)){//匹配成功,返回相应自定义代码
            case TABLE1_DIR:
                //查询table1表中所有数据
                break;
            case TABLE1_ITEM:
                //查询table1表中的单条数据
                break;
            case TABLE2_DIR:
                //查询table2表中所有数据
                break;
            case TABLE2_ITEM:
                //查询table2表中的单条数据
                break;
            default:
                break;
        }
       ...
    }

    ...

以查询方法为例,其他方法类似。getType()用于获取Uri对象对应的MIME类型,MIME字符串主要由三部分组成:
a.以vnd开头
b.如内容URI以路径结尾,后接android.cursor.dir/,如以id结尾,则接android.cursor.item/.
c.最后接上vnd..。

继续实现getType()中逻辑:

...
public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
            case TABLE1_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
            case TABLE2_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
            case TABLE2_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
            default:
                break;
        }
        return null;
    }
    ...

(2)跨程序数据共享

在DatabaseTest项目上开发,通过内容提供器加入外部访问接口。首先将MyDatabaseHelper中的Toast去掉(跨程序访问不能使用Toast),然后创建内容提供器,右击包名->New->Other->Content Provider,命名为DatabaseProvider, authority指定为包名.provider.Exported是否允许外部程序访问,Enable是否启用,都勾选。
修改DatabaseProvider:

public class DatabaseProvider extends ContentProvider {
    public static final int BOOK_DIR = 0;
    public static final int BOOK_ITEM = 1;
    public static final int CATEGORY_DIR = 2;
    public static final int CATEGORY_ITEM = 3;
    public static final String AUTHORITY = "com.example.jojo.databasetest.provider";
    private static UriMatcher uriMatcher;
    private MyDatabaseHelper dbHelper;
    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 删除数据
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        int deletedRows = 0;
        switch (uriMatcher.match(uri)){
            case BOOK_DIR:
                deletedRows = db.delete("Book", selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Book", "id = ?", new String[]{ bookId });
                break;
            case CATEGORY_DIR:
                deletedRows = db.delete("Category",selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Category", "id = ?", new String[]{ categoryId });
                break;
            default:
                break;
        }
        return deletedRows;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.jojo.databasetest.provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.jojo.databasetest.provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.jojo.databasetest.provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.jojo.databasetest.provider.category";
            default:
                break;
        }
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        //添加数据
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Uri uriReturn = null;
        switch (uriMatcher.match(uri)){
            case BOOK_DIR:
            case BOOK_ITEM:
                long newBookId = db.insert("Book", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
                break;
            case CATEGORY_DIR:
            case CATEGORY_ITEM:
                long newCategoryId = db.insert("Category", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/Category/" + newCategoryId);
                break;
            default:
                break;
        }
        return uriReturn;
    }

    @Override
    public boolean onCreate() {
        //创建MyDatabaseHelper实例,返回true表示内容提供器创建成功,这时数据库已经完成创建升级
        dbHelper = new MyDatabaseHelper(getContext(),"BookStore.db", null, 2);
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        //查询数据
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)){
            case BOOK_DIR:
                cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case BOOK_ITEM:
                //getPathSegments将内容URI权限后的部分以“/”符号分割,把分割后的结果放入到一个字符串列表中,
                // 此列表的第0个位置存放的就是路径,第1个位置存放的是id
                String bookId = uri.getPathSegments().get(1);
                cursor = db.query("Book", projection, "id = ?", new String[]{bookId},null, null, sortOrder);
                break;
            case CATEGORY_DIR:
                cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                cursor = db.query("Category", projection, "id = ?", new String[]{categoryId},null, null, sortOrder);
                break;
            default:
                break;
        }
        return cursor;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        //更新数据
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        int updateRows = 0;
        switch (uriMatcher.match(uri)){
            case BOOK_DIR:
                updateRows = db.update("Book", values, selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                updateRows = db.update("Book", values, "id = ?", new String[]{bookId});
                break;
            case CATEGORY_DIR:
                updateRows = db.update("Category", values, selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                updateRows = db.update("Category", values, "id = ?", new String[]{categoryId});
                break;
            default:
                break;
        }
        return updateRows;
    }
}

内容提供器一定要注册才能使用(Android Studio快捷方式创建的已自动完成这一步)。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.jojo.databasetest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ...

        <provider
            android:name=".DatabaseProvider"
            android:authorities="com.example.jojo.databasetest.provider"
            android:enabled="true"
            android:exported="true"></provider>
    </application>

</manifest>

现在先将模拟器中的DatabaseTest程序删掉(放置遗留数据造成干扰),然后重新运行安装。关闭掉此项目,新建一个ProviderTest项目(用于访问DatabaseTest中数据)。修改布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add To Book" />
    <Button
        android:id="@+id/query_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Query From Book" />
    <Button
        android:id="@+id/update_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update Book" />
    <Button
        android:id="@+id/delete_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete From Book" />

</LinearLayout>

修改MainActivity:

public class MainActivity extends AppCompatActivity {
    private String newId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button addData = (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //添加数据
                Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book");
                ContentValues values = new ContentValues();
                values.put("name", "A Clash of Kings");
                values.put("author", "George Martin");
                values.put("pages",1040);
                values.put("price", 22.85);
                Uri newUri = getContentResolver().insert(uri, values);
                newId = newUri.getPathSegments().get(1);
            }
        });
        Button queryData = (Button) findViewById(R.id.query_data);
        queryData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book");
                Cursor cursor = getContentResolver().query(uri, null, null, null, null);
                if (cursor != null){
                    while (cursor.moveToNext()){
                        String name = cursor.getString(cursor.getColumnIndex("name") );
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d("MainActivity", "book name is " + name);
                        Log.d("MainActivity", "book author is " + author);
                        Log.d("MainActivity", "book pages is " + pages);
                        Log.d("MainActivity", "book price is " + price);
                    }
                    cursor.close();
                }

            }
        });
        Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            //更新刚添加的数据,表中其他行不受影响
                Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book/" + newId);
                ContentValues values = new ContentValues();
                values.put("name", "A Storm of Swords");
                values.put("pages",1216);
                values.put("price", 24.05);
                getContentResolver().update(uri, values, null, null);

            }
        });
        Button deleteData = (Button) findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book/" + newId);
                getContentResolver().delete(uri, null, null);

            }
        });
    }
}

运行程序,分别点击增删改按钮,点击查询按钮查看日志信息。
添加后查询
添加
更新后查询
更新

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值