操作系统-多用户的二级文件系统(C#)

一、程序基本要求

设计一个多用户的二级文件系统,能够实现简单的文件操作。具体包括如下几条命令:
(1)Dir 列文件目录;
(2)Create 创建文件;
(3)Delete 删除文件;
(4)Deldir 删除目录;
(5)Open 打开文件;
(6)Close 关闭文件;
(7)Read、Write读写文件;
(8)Search 查询文件;
(9)Copy 拷贝文件;
(10)Cut 剪切文件。

二、二级文件系统说明

二级目录结构是将文件目录分为主文件目录和用户文件目录两级。
系统为每个用户建立一个文件目录(UFD),每个用户的文件目录登记了该用户建立的所有文件名及其在辅存中的位置和有关说明信息。
主目录(MFD)则登记了进入系统的各个用户文件目录的情况,每个用户占一个表目,说明该用户目录的属性。
在这里插入图片描述

三、程序开发环境与思路

在看到这个题目的时候,我第一时间就想到了利用数据库的表来模拟二级目录,即直接建立两张表,一张为用户User表,一张为文件信息File表。虽然本人最终实现的程序中没有真正模拟物理块,大多数也是直接调用的C#中的方法,行了很多便利,但大致将功能实现了,希望大家能够多多海涵。

  1. 开发环境:Visual Studio 2013(C#)、SQL server 2014数据库
  2. 基本思路:本人做的是控制台程序,在输入的时候命令分为三部分:
    (1) 只有单独的命令,如:Dir(列出文件目录)、Logout(切换用户);
    (2)一个命令加上一个目录或文件名,如:Create(创建文件)、Delete(删除文件)等;
    (3)一个命令加上一个文件源地址和一个文件目的地址,如:Copy(复制文件)、Cut(剪切文件)等;
    因此,首先用一个数组存放输入的操作命令,当数组的长度分别为 1 / 2 / 3 时,分别进行判断,再决定是调用哪个方法。

(一)表的建立与字段说明

User表:

  • ID为用户ID,Password为用户密码,currentPath为用户当前所在路径。
  • currentPath字段是为增加的cd(进入文件目录)功能做准备。

File表:

  • ID为用户ID,fileName为文件名,filePath(主键)为文件绝对路径,fileOpen为文件是否打开,processID为进程号。
  • fileOpen和processID字段是为了实现文件开启/关闭功能而添加的。

(二)功能的具体实现

在真正开始讲解之前,我想说明一下我是如何从数据库中取到数据的。

  1. 首先连接数据库,网上许多文章都有讲解,这里不再赘述。
  2. 在项目中添加数据集,右键项目-添加-新建项,找到数据集文件:
    在这里插入图片描述紧接着点开刚刚连接的数据库左侧三角,将表拖入进去:
    注意到两张表上分别有FileTableAdapter和UserTableAdapter,这是两张表的适配器,我们可以在这里编写SQL语句,并在主程序中引用。
    在最后我也展示了程序中我所添加的所有查询方法。

Main主函数

	//在主函数之前先设置一个全局变量,保存根目录
	static string rootFolder = "D:\\OS\\User\\";
	
        static void Main(string[] args)
        {
            string userId = Login();
            if (userId != "0")
            {
                Console.WriteLine("欢迎进入二级文件系统");
                Console.WriteLine("*********************** 二级文件系统演示 ************************");
                Console.WriteLine("*\t\t命令     说明   *");
                Console.WriteLine("*---------------------------------------------------------------*");
                Console.WriteLine("*\t\tcd        进入/回退目录  *");
                Console.WriteLine("*\t\tDir      列文件目录  *");
                Console.WriteLine("*\t\tCreate                  创建文件  *");
                Console.WriteLine("*\t\tDelete          删除文件  *");
                Console.WriteLine("*\t\tDeldir              删除目录         *");
                Console.WriteLine("*\t\tOpen              打开文件  *");
                Console.WriteLine("*\t\tClose          关闭文件  *");
                Console.WriteLine("*\t\tRead              读取文件  *");
                Console.WriteLine("*\t\tWrite              写入文件  *");
                Console.WriteLine("*\t\tSearch          查询文件  *");
                Console.WriteLine("*\t\tCopy          拷贝文件  *");
                Console.WriteLine("*\t\tCut          剪切文件  *");
                Console.WriteLine("*\t\tLogout          切换用户  *");
                Console.WriteLine("*\t\tQuit          退出系统  *");
                Console.WriteLine("*****************************************************************");
                bool flag = true;   //用于现在是否还是这个用户在进行命令操作
                while (flag)
                {
                    Console.WriteLine("-----------------------------------------------------------------");
                    UserTableAdapter userAdapter = new UserTableAdapter();
                    DataTable userTable = userAdapter.GetDataByID(userId);
                    string currentPath = userTable.Rows[0]["currentPath"].ToString();
                    Console.WriteLine("您现在所在的目录是:" + currentPath.Replace(rootFolder, @"\User\"));
                    Console.WriteLine("请输入您的命令:");
                    //取到操作命令
                    string[] operation = Console.ReadLine().Split(' ');
                    //如果只有操作命令
                    if (operation.Length == 1)
                    {
                        string oper = operation[0];
                        int operNum = 0;
                        if (oper.Equals("Dir")) { operNum = 1; }
                        if (oper.Equals("Logout")) { operNum = 13; }
                        switch (operNum)
                        {
                            case 1: Dir(userId); break;
                            case 13: Logout(userId); flag = false; break;   //如果时切换用户,跳出内while循环
                            default: Console.WriteLine("输入的操作命令格式错误!"); break;
                        }
                    }
                    //如果是操作命令后只接一个目录或文件名
                    if (operation.Length == 2)
                    {
                        string oper = operation[0];
                        string fName = operation[1];
                        int operNum = 0;
                        if (oper.Equals("Create")) { operNum = 2; }
                        if (oper.Equals("Delete")) { operNum = 3; }
                        if (oper.Equals("Deldir")) { operNum = 4; }
                        if (oper.Equals("Open")) { operNum = 5; }
                        if (oper.Equals("Close")) { operNum = 6; }
                        if (oper.Equals("Read")) { operNum = 7; }
                        if (oper.Equals("Write")) { operNum = 8; }
                        if (oper.Equals("Search")) { operNum = 9; }
                        if (oper.Equals("cd")) { operNum = 12; }
                        switch (operNum)
                        {
                            case 2: Create(userId, fName); break;
                            case 3: Delete(userId, fName); break;
                            case 4: Deldir(userId, fName); break;
                            case 5: Open(userId, fName); break;
                            case 6: Close(userId, fName); break;
                            case 7: Read(userId, fName); break;
                            case 8: Write(userId, fName); break;
                            case 9: Search(userId, fName); break;
                            case 12: cd(userId, fName); break;
                            default: Console.WriteLine("输入的操作命令格式错误!"); break;
                        }
                    }
                    //如果操作命令后接两个目录或文件名
                    if (operation.Length == 3)
                    {
                        string oper = operation[0];
                        string fName = operation[1];
                        string destFolder = operation[2];
                        int operNum = 0;
                        if (oper.Equals("Copy")) { operNum = 10; }
                        if (oper.Equals("Cut")) { operNum = 11; }
                        switch (operNum)
                        {
                            case 10: Copy(userId, fName, destFolder); break;
                            case 11: Cut(userId, fName, destFolder); break;
                            default: Console.WriteLine("输入的操作命令格式错误!"); break;
                        }
                    }
                }
            }
        }

Login 登录

  1. 在用户输入ID的时候,首先进行判断,如果没有该用户,则提示用户不存在,如果有该用户,则再将输入的密码与数据库中存放的密码相匹配,如果正确则登录成功,如果错误提示密码错误。
  2. 值得注意的是,Login方法我最开始写的类型是void,但后来发现这是不正确的,因为只有一张File表保存所有用户的文件信息,所以后续所有功能的方法都需要取得该用户ID,则Login方法需要有一个返回值,即用户ID。
  3. 首先,在数据集文件DataSet中【右键UserTableAdapter - 添加查询 - 使用SQL语句 - SELECT(返回行)- 填写SQL语句 - 给该查询方法起名】,表示通过用户ID取到该用户的数据:
SELECT ID, Password, currentPath FROM [User] WHERE (ID = @ID)
  1. 我给我的查询方法起名为GetDataByID,则Login的完整代码为:
static public string Login()
{	
	bool flag = true;
        while (flag)
        {
            Console.WriteLine("-----------------------------------------------------------------");
            Console.WriteLine("请输入用户ID:");
            string userId = Console.ReadLine();
            
            //创建一个用户表适配器,并根据适配器中的查询方法存储到表中
            //需引用System.Data和DataSetTableAdapters的命名空间
            UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            
            //如果用户ID输入正确
           if (userTable.Rows.Count == 1)
           {
               Console.WriteLine("请输入密码:");
               string userPwd = Console.ReadLine();
               if (userPwd == userTable.Rows[0]["Password"].ToString())
               {
                   Console.WriteLine("登录成功!");
                   flag = false;
                   return userId;
               }
               else
               {
                   Console.WriteLine("密码错误,请重新输入!");
                   flag = true;
               }
           }
           //如果ID不正确
           else
           {
               Console.WriteLine("该用户不存在,请重新输入!");
               flag = true;
           }
       }
       return "0";
  }
  1. 运行结果:

    在这里插入图片描述

cd 进入/退出文件目录

  1. 这个功能是我后来在写其他功能的时候决定加上的,因为如果没有进入文件目录的功能,那么只能在用户根目录下输入文件相对根目录的文件全路径进行创建/删除等操作。
  2. 分为两种情况,一个是返回上一级父目录,一个是进入新目录,如果返回上一级目录,则命令后跟 “ . . ”:
static public void cd(string userId, string fName)
{
    string path = rootFolder + userId;
    UserTableAdapter userAdapter = new UserTableAdapter();
    DataTable userTable = userAdapter.GetDataByID(userId);
    string currentPath = userTable.Rows[0]["currentPath"].ToString();
    
    //如果cd命令是返回上一级目录
    if(fName.Equals(".."))
    {
        if (currentPath.Equals(path))
        {
            Console.WriteLine("已经在用户根目录!");
        }
        else
        {
            //folder字符串数组存放通过“/”拆分后的目录字符串
            string[] folder = currentPath.Split('\\');
            //currentPath保存着上一级目录路径
            currentPath = currentPath.Replace((@'\' + folder[folder.Length - 1]), "");
            //更新数据库中该用户的当前路径
            userAdapter.UpdatePath(currentPath, userId);
        }
    }
    //如果命令是进入目录
    else
    {
        currentPath = Path.Combine(currentPath, fName);
        if(Directory.Exists(currentPath))
        {
            userAdapter.UpdatePath(currentPath, userId);
        }
        else
        {
            Console.WriteLine("您输入的目录不存在!");
        }
    }
}
  1. 运行结果:在这里插入图片描述

Dir列出文件目录

  1. 代码:
        static public void Dir(string userId)
        {
            UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            string[] file = Directory.GetFiles(currentPath);
            string[] directory = Directory.GetDirectories(currentPath);
            Console.WriteLine("您文件夹下的文件夹和文件有:");
            foreach (string item in directory)
            {
                Console.WriteLine(item.Replace(currentPath, ""));
            }
            foreach (string item in file)
            {
                Console.WriteLine(Path.GetFileName(item));
            }
        }
  1. 运行结果:
    在这里插入图片描述

Create 创建文件/文件夹

  1. 代码:
        static public void Create(string userId,string fName)
        {
	    UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            
            FileTableAdapter fileAdapter = new FileTableAdapter();
            string pathString = Path.Combine(currentPath, fName);
            string[] pathArray = fName.Split('\\');
            //fileName存放不带路径的文件名
            string fileName = pathArray[pathArray.Length - 1];
            //如果名字中不带格式,则是创建文件夹(有“.”表示后面写了格式)
            if(!fName.Contains("."))
            {
                Directory.CreateDirectory(pathString);
                Console.WriteLine("已成功创建文件夹" + fileName + "!");
            }
            //带格式,则是创建文件
            else
            {
                if (!File.Exists(pathString))
                {
                    FileStream newfile = File.Create(pathString);
                    //创建文件时向数据库增添一条数据
                    fileAdapter.InsertFile(userId, fileName, pathString, "否", 0);
                    newfile.Close();
                    Console.WriteLine("已成功创建文件" + fileName + "!");                }
                else
                {
                    Console.WriteLine("文件\"{0}\"已经存在!", fileName);
                    return;
                }
            }
        }
  1. 运行结果:
    在这里插入图片描述

Delete 删除文件 / Deldir 删除目录

  1. 代码:
        static public void Delete(string userId,string fName)
        {
            UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            FileTableAdapter fileAdapter = new FileTableAdapter();
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            string pathString = Path.Combine(currentPath, fName);
            //fileName1存放删除文件的文件名
            string[] pathArray1 = fName.Split('\\');
            string fileName1 = pathArray1[pathArray1.Length - 1];
            if (File.Exists(pathString))
            {
                try
                {
                    File.Delete(pathString);
                    fileAdapter.DeleteAll(pathString);
                    Console.WriteLine("已成功删除文件" + fileName1 + "!");
                }
                catch (IOException e)
                {
                    Console.WriteLine(e.Message);
                    return;
                }
            }
        }
        
        static public void Deldir(string userId,string fName)
        {
            UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            string pathString = Path.Combine(currentPath, fName);
            if (Directory.Exists(pathString))
            {
                try
                {
                    Directory.Delete(pathString, true);
                    Console.WriteLine("已成功删除文件夹" + fName + "及其下所有子文件!");
                }
                catch (IOException e)
                {
                    Console.WriteLine(e.Message);
                }
            }
        }
  1. 运行结果:
    在这里插入图片描述

Open 打开文件 / Close 关闭文件

  1. 思路:打开文件很好实现,只需要通过路径开始进程就可以,但关闭文件困扰了我很久,进程的Stop()功能似乎不能直接将刚刚打开的文件关闭。于是上网查阅资料,发现有人回答关于“如何关闭文本文件”的问题,但那个方法是遍历目前运行的所有进程,通过找到“notepad”程序名,将进程杀死,但此方法只能关闭txt文本文件,并且会一次性将所有打开的文本文件关闭。最终,我意识到只要拿到刚刚打开文件的进程ID,就可以在关闭方法中通过进程ID将进程杀死,从而关闭文件。
  2. 代码:
        static public void Open(string userId,string fName)
        {
            FileTableAdapter fileAdapter = new FileTableAdapter();
            UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            string pathString = Path.Combine(currentPath, fName);
            ProcessStartInfo psi = new ProcessStartInfo(pathString);
            Process pro = new Process();
            Console.WriteLine("正在读取文件……");
            pro.StartInfo = psi;
            pro.Start();
            
            //修改数据库中该文件的信息
            fileAdapter.UpdateProId("是", pro.Id, userId, fName);
            Console.WriteLine("文件打开成功!");
        }
        
        static public void Close(string userId,string fName)
        {
            FileTableAdapter fileAdapter = new FileTableAdapter();
            DataTable fileTable = fileAdapter.GetDataByFilename(fName, userId);
            int processID = Convert.ToInt32(fileTable.Rows[0]["processID"]);
            Console.WriteLine("正在关闭文件……");
            Process.GetProcessById(processID).Kill();
            
            //修改数据库中该文件的信息
            fileAdapter.UpdateProId("否", 0, userId, fName);
            Console.WriteLine("文件关闭成功!");
        }

Read / Write读写文件

  1. 代码:
        static public void Read(string userId,string fName)
        {
            UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            string pathString = Path.Combine(currentPath, fName);
            string[] lines = File.ReadAllLines(pathString);
            Console.WriteLine("正在读取文件内容……");
            
            //遍历每行读取出文件的内容
            System.Console.WriteLine("读取的文件"+fName+"中的内容为:");
            foreach (string line in lines)
            {
                Console.WriteLine("\t" + line);
            }
            Console.WriteLine("文件读取成功!");
        }
        
        static public void Write(string userId, string fName)
        {
            UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            string pathString = Path.Combine(currentPath, fName);
            Console.WriteLine("请输入您要写入的内容,每行以空格隔开:");
            string[] lines = Console.ReadLine().Split(' ');
            File.WriteAllLines(pathString, lines);
            Console.WriteLine("正在写入文件……");
            Console.WriteLine("文件写入成功!");
        }
  1. 运行结果:
    在这里插入图片描述

Search 查询文件

  1. 用户输入的是文件名,可以从数据库中查找到该文件的路径,由于每个文件文件名可能相同,但路径不同,所以输入同一个文件名可能出现不同的路径。
  2. 代码
        static public void Search(string userId, string fName)
        {
            Console.WriteLine("正在进行搜索……");
            //从数据库中提取路径的值
            FileTableAdapter fileAdapter = new FileTableAdapter();
            DataTable fileTable = fileAdapter.GetDataByFilename(fName, userId);
            if(fileTable.Rows.Count>0)
            {
                Console.WriteLine("您查找的文件存在于以下路径中:");
                //string str = fileTable.Rows[0]["filePath"].ToString();
                //string[] filePath = { "" };
                List<string> filePath = new List<string>();
                for (int i = 0; i < fileTable.Rows.Count; i++)
                {
                    filePath.Add(fileTable.Rows[i]["filePath"].ToString());
                    Console.WriteLine(filePath[i].Replace(rootFolder, @'\User\'));
                }
            }
            else
            {
                Console.WriteLine("您要查找的文件不存在!");
            }
        }
  1. 运行结果:
    在这里插入图片描述

Copy 拷贝文件 / Cut剪切文件

  1. 代码:
        static public void Copy(string userId, string fName,string destFolder)
        {
            UserTableAdapter userAdapter = new UserTableAdapter();
            FileTableAdapter fileAdapter = new FileTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            //fileName1存放原本的文件名
            string[] pathArray1 = fName.Split('\\');
            string fileName1 = pathArray1[pathArray1.Length - 1];
            //fileName2存放复制后的文件名
            string[] pathArray2 = destFolder.Split('\\');
            string fileName2 = pathArray2[pathArray2.Length - 1];
            //目标文件夹的绝对路径
            string targetPath = Path.Combine(currentPath, destFolder);
            //如果是复制文件
            if(fName.Contains("."))
            {
                if (File.Exists(Path.Combine(currentPath, fName)))
                {
                    //前面为文件原本所在路径,后面为目的路径
                    File.Copy(Path.Combine(currentPath, fName), Path.Combine(rootFolder + userId, destFolder), true);
                    fileAdapter.InsertFile(userId, fileName2, Path.Combine(rootFolder + userId, destFolder), "否", 0);
                    Console.WriteLine("文件复制成功!");
                }
                else
                {
                    Console.WriteLine("您要复制的文件不存在!");
                }
            }
            //如果是复制文件夹里的所有文件
            else
            {
                if (Directory.Exists(Path.Combine(currentPath, fName)))
                {
                    string[] files = System.IO.Directory.GetFiles(Path.Combine(currentPath, fName));
                    foreach (string s in files)
                    {
                        fileName1 = Path.GetFileName(s);
                        targetPath = Path.Combine(targetPath, fileName1);
                        File.Copy(s, targetPath, true);
                    }
                }
                else
                {
                    Console.WriteLine("源目录不存在!");
                }
            }
        }
        static public void Cut(string userId, string fName, string destFolder)
        {
            UserTableAdapter userAdapter = new UserTableAdapter();
            FileTableAdapter fileAdapter = new FileTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            //fileName1存放原本的文件名
            string[] pathArray1 = fName.Split('\\');
            string fileName1 = pathArray1[pathArray1.Length - 1];
            //fileName2存放复制后的文件名
            string[] pathArray2 = destFolder.Split('\\');
            string fileName2 = pathArray2[pathArray2.Length - 1];
            //如果是移动文件
            if(fName.Contains("."))
            {
                if (File.Exists(Path.Combine(currentPath, fName)))
                {
                    File.Move(Path.Combine(currentPath, fName), Path.Combine(rootFolder + userId, destFolder));
                    fileAdapter.UpdatePath(Path.Combine(rootFolder + userId, destFolder), fileName2, userId, Path.Combine(currentPath, fName));
                    Console.WriteLine("文件剪切成功!");
                }
                else
                {
                    Console.WriteLine("您要剪切的文件不存在!");
                }
            }
            //如果是移动文件夹
            else
            {
                if (Directory.Exists(Path.Combine(currentPath, fName)))
                {
                    Directory.Move(Path.Combine(currentPath, fName), Path.Combine(currentPath, destFolder));
                    Console.WriteLine("文件夹剪切成功!");
                }
                else
                {
                    Console.WriteLine("您要剪切的文件夹不存在!");
                }
            }
        }
  1. 值得注意的是,我这里设定的是命令后接的第一个参数为当前目录下的文件或文件夹(源文件),第二个参数是相对于用户根目录的文件全路径(目的文件),就算在同一个文件夹下,也要输入全部路径。
  2. 运行结果(Cut和Copy同理):
    在这里插入图片描述

Logout 切换用户

  1. 真正的切换用户功能是在主程序中实现的,即设定一个flag,如果输入的命令是Logout,将flag置为false,跳出循环。但在Logout方法中,要将User表中的currentPath当前路径字段重新置为用户根目录。
  2. 代码:
        static public void Logout(string userId)
        {
            Console.WriteLine("正在切换用户……");
            UserTableAdapter userAdapter = new UserTableAdapter();
            DataTable userTable = userAdapter.GetDataByID(userId);
            string currentPath = userTable.Rows[0]["currentPath"].ToString();
            userAdapter.UpdatePath(rootFolder + userId, userId);
        }

User表和File表适配器中的所有查询方法

在这里插入图片描述

四、程序总结与注意点

大致讲一下我在写程序的过程中遇到的要多注意的问题吧。

  1. 在Login方法中,一定要在最外套一个while循环,并利用flag进行判断。如果不加while,在用户ID或密码输入错误之后不会让你重新输入,会直接向下执行。
  2. 为了完成cd功能,我思考过很多种方式,其中一个就是在cd方法中直接返回一个当前路径,用一个全局变量进行保存,但这样相对较麻烦,最终决定在User表中新增一个字段,直接保存当前路径。
  3. 在Create创建文件的时候,要用FileStream文件流,创建完成后关闭文件流,否则在后续对刚刚创建的此文件进行Delete、Copy等操作时会提示“进程被占用”。
  4. 为了完成Close关闭文件功能,我在表中新增了两个字段,一个是“文件是否开启”,一个是“进程ID”。在开启文件时,将文件状态置为“开”,并获取到当前进程ID,Update文件表中的processID,以便于Close文件的时候获取进程ID,将进程杀死。
  5. 在创建、删除、剪切、复制文件时要记得同时进行数据库中的关于此文件的INSERT、DELETE、UPDATE操作。

完整代码下载地址:利用C#控制台完成多用户二级文件系统

  • 10
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值