Multiple Attachments in Rails

本文介绍如何使用Ruby on Rails实现多文件上传功能,并将其应用于博客文章的附件上传中。通过自定义视图和控制器,结合JavaScript增强用户体验,实现限制每篇文章最多上传五个文件,并允许用户移除已选文件。

Multiple Attachments in Rails

the orginal URL:http://www.practicalecommerce.com/blogs/post/432-Multiple-Attachments-in-Rails

 

Creating intuitive interfaces can be challenging, particularly when it comes to uploading files such as images or attachments. Suppose that we are creating an application that let's someone submit a blog post, to which they can add multiple attachments. A great example of this kind of thing is BaseCamp , which is a project management application. BaseCamp is one of those applications that leaves developers in awe, as they have managed to create extremely intuitive interfaces. Inspired by the way that BaseCamp handles multiple file uploads, I set out to create my own version. Here is what I learned.

Objective

My objective was to create an interface that would a user to submit a blog post, with the ability to attach multiple files to that job post. The interface needed to be intuitive, graceful, and (hopefully) on par with the way that BaseCamp does it. To start out, I knew that I would be using Ruby on Rails to create my application, and that I would also be using the attachment_fu plugin to handle file uploads.

A bit of searching about multiple file uploads, and I was ready to get started. Having done a little research and working out the logic of the problem, I figured I would have the following objectives:

  • Allow user to attach files to a blog post.
  • Enforce a limit of 5 files per blog post.
  • Allow a user to remove files from a blog post.

The Models

Let's start with the models that we will need in order to pull this off. Since we are trying to let a user create a blog post, we will start with a model called Post . We'll also need a model that will represent our attached file, which I am going to call Attachment . The following is a sample migration file to create these models:

class CreatePosts < ActiveRecord::Migration
      def self.up
        create_table :posts do |t|
          t.string    :title
          t.text      :content
          t.timestamps
        end
        create_table :attachments do |t|
          t.integer     :size, :height, :width, :parent_id, :attachable_id, :position
          t.string      :content_type, :filename, :thumbnail, :attachable_type
          t.timestamps
          t.timestamps
        end
        add_index :attachments, :parent_id
        add_index :attachments, [:attachable_id, :attachable_type]
      end

      def self.down
        drop_table :posts
            drop_table :attachments
      end
    end

Most of the fields in the attachments table are required by the attachment_fu plugin, but you'll notice that I have added an integer field called attachable_id and a string field called attachable_type . These fields are going to be used for a polymorphic relationship. After all, if this thing works I don't want to be limited to only attaching files to blog posts, but would rather have the option of adding attachments to other models in the future. Additionally, I've added an indexes to the attachments table based on my experiences with the SQL queries that attachment_fu generates. Without going in to detail, these indexes help immensely when your application begins to scale.

So once you have migrated your database, it's time to move on to the actual models themselves. Let's start with the Post model (/app/models/post.rb ):

class Post < ActiveRecord::Base

      has_many  :attachments, :as => :attachable, :dependent => :destroy

      validates_presence_of   :title
      validates_uniqueness_of :title
      validates_presence_of   :content

    end

This is a pretty basic model file. To start with, we declare that this model has_many attachments, which we will reference as "attachable" (remember the extra fields we added to the attachments table?), and that if someone deletes a post, the attached files should also be deleted. Then we do some simple validations to make sure that each post has a unique title and some content.

Moving on, let's take a look at our Attachment model (/app/models/attachment.rb ):

class Attachment < ActiveRecord::Base

      belongs_to :attachable, :polymorphic => true

      has_attachment :storage => :file_system,
                     :path_prefix => 'public/uploads',
                     :max_size => 1.megabyte

    end

Again, this is an extremely basic model file. The first thing we do is set the belongs_to relationship, which is polymorphic. Notice that we are referring to attachments as attachable , like we did in our Post model.

We are going to be storing our uploaded files in /public/uploads . When it comes to deployment, be sure that this directory is symlinked to a shared directory, or you will lose all your attachments each time you deploy.

Now that we have our two models in place, let's set up some controllers and get our views figured out.

Setting Up Controllers

Ok, so what we know is that we will be creating blog posts. First off, let's create some controllers to handle all of this. I'll be coming back to the controller actions later, but for now we want to generate them:

script/generate controller posts
    script/generate controller attachments

I like to create a controller for each resource in order to keep things RESTful. You never know where your app may go, so better safe than sorry. Speaking of resources, let's create some routes as well (/config/routes.rb ):

map.resources :attachments
map.resources :posts

We'll come back to write our controller actions later, as first we want to tackle our view files.

The Basic Views

I'm going to operate under the assumption that we are using a layout template, which loads in all of the default JavaScript files for Rails:

<%= javascript_include_tag :defaults %>

From there, let's start by looking at the basic forms that we are starting with to manage blog posts:

(/app/views/posts/new.html.erb )

    <% form_for(:post, @post, :url => posts_path, :html => {:onsubmit => 'return post.validate();', :multipart => true}) do |f| %>
        <%= error_messages_for 'post' %>
        <%= render :partial => 'post_form', :locals => {:f => f, :post => @post} %>
        <p>
            <%= f.submit "Create Blog Post", :id => 'post_submit' %>
        </p>
    <% end -%>

(/app/views/posts/edit.html.erb )

    <% form_for(:post, @post, :url => post_path(@post), :html => {:onsubmit => 'return post.validate();', :multipart => true, :method => :put}) do |f| %>
        <%= error_messages_for 'post' %>
        <%= render :partial => 'post_form', :locals => {:f => f, :post => @post} %>
        <p>
            <%= f.submit "Save Changes", :id => 'post_submit' %>
        </p>
    <% end -%>

(/app/views/posts/_post_form.html.erb )

<p>
      <label for="post_title">Title:</label>
      <%= f.text_field :title %>
    </p>
    <p>
      <label for="post_content">Content:</label>
      <%= f.text_area :content, :rows => 7 %>
    </p>

By using a partial to abstract out the blog post fields, we can concentrate on the other parts of the form. You'll notice that there are no file fields or any uploading stuff at all in our forms. We are going to add this in, but it needs to be one at a time. The reason for this is that we have two very different scenarios that require different logic:

  1. A user is creating a new blog post – under this scenario, there are no attachments to our post, since it is new. All we need to be concerned about is allowing multiple uploads and uploading them to the server.
  2. A user is editing a blog post – things get a little more complicated here. Let's assume that we are limiting the number of attachments to 5 per blog post. If we are editing a post that already has 2 attachments, we need a way to make sure that they cannot go over the limit. Additionally, the user will need a method of removing attachments that are already assigned to the blog post (which also plays in with the attachment limit).

First of all, let's build the JavaScript that we will need to make this happen, and then we will come back to our views and make the adjustments that we need.

The JavaScript

Alright, some of you have probably been reading through this post wondering when I am going to actually start talking about how to upload multiple files. Your patience is about to pay off! The way that this trick works is that we use JavaScript to store the files that a user wants to attach, and also to display an interface. To set the expectation of what we are looking at, here is a timeline of events for uploading multiple files:

  1. A user selects a file to attach.
  2. That file is stored via JavaScript.
  3. The file name is appended to the page with a link to remove the file.
  4. The number of attachments is evaluated, and the file field is either reset or de-activated.

Attached Files As you can see in the screenshot at the left, once a user has selected a file to attach, it is displayed to the user and they have the option of removing it. Note: these files have not been uploaded yet, they are simply stored in a JavaScript object .

First of all, I found a nice little script by someone called Stickman after searching a bit on this. However, I found that I needed to make some small adjustments to the original script and the examples provided, particularly:

  • Changed the uploaded attachments from :params['file_x'] to params[:attachment]['file_x'] .
  • Changed to update a ul element with li elements, rather than creating div elements.
  • Changed to "Remove" button to a text link.
  • Uses Prototype and Scriptaculous .

These sample files are included at the bottom of this post. So let's go through the steps that we need to get this working on our example. The first thing we need to do is add the scripts to /public/javascripts/application.js so that we have the JavaScript methods that we need in place. You will notice that there is also a validation script in there to handle client-side validation of the blog post form (post.validate() ).

The "New Blog Post" Form

Let's take a look at what we need to add to our "new blog post" template:

(/app/views/posts/new.html.erb )

    <% form_for(:post, @post, :url => posts_path, :html => {:onsubmit => 'return post.validate();', :multipart => true}) do |f| %>
        <%= error_messages_for 'post' %>
        <%= render :partial => 'post_form', :locals => {:f => f, :post => @post} %>
        <% fields_for @attachment do |attachment| -%>
        <p>
            <label for="attachment_data">Attach Files:</label>
            <%= attachment.file_field :data %>
        </p>
        <% end -%>
        <ul id="pending_files"></ul>
        <script type="text/javascript">
            var multi_selector = new MultiSelector($('pending_files'), 5);
            multi_selector.addElement($('attachment_data'));
        </script>
        <p>
            <%= f.submit "Create Blog Post", :id => 'post_submit' %>
        </p>
    <% end -%>

As you can see, we have added some code between our partial (below the blog post fields) and the submit button. Let's take a quick look at what each of these does:

<% fields_for @attachment do |attachment| -%>

This block allows us to create form fields for another model. In our case, the form that we are working with is linked to the Post model, but we would like to upload files to the Attachment model. Inside this block we have a variable called attachment that maps to an Attachment object, similar to the @post variable mapping to a Post object.

<p>
        <label for="attachment_data">Attach Files:</label>
        <%= attachment.file_field :data %>
    </p>

Here we are adding the file field. The script overrides much of the attributes for this one, but just for good form I have called it data. Note how the label corresponds to attachment_data , since that is the ID that Rails will generate for that file field.

<ul id="pending_files"></ul>

Here are have put an empty ul element that will hold the "pending files" that we want to upload. Remember when we select a file for upload, it will be stored and displayed here (with the option to remove it). Notice that this unordered list element has an id of pending_files .

<script type="text/javascript">
        var multi_selector = new MultiSelector($('pending_files'), 5);
        multi_selector.addElement($('attachment_data'));
    </script>

This is the real meat of the script, where we create an instance of the MultiSelector object. It takes two parameters, the first is the id of the element to update after a file has been selected, and the second is an option limit number. In our example, we are putting a limit of 5 attachments per blog post. We could leave this blank to allow unlimited attachments.

Secondly, we add the file field element to our MultiSelector instance, which hooks it all up to the file field that we had created above. And really, that is about all that we need to do.

The "Edit Blog Post" Form

With the edit form, we have a little bit more to deal with. Specifically, because there could already be attachments to the blog post that we want to edit, we have a few issues to face:

  1. We need to evaluate how many new attachments are allowed.
  2. We need to display the attachments that already exist.
  3. A user needs to be able to remove the attachments that already exist.

So let's take a look at the final code for our edit form, and go through what each of those changes is:

(/app/views/posts/edit.html.erb )

    <% form_for(:post, @post, :url => post_path(@post), :html => {:onsubmit => 'return post.validate();', :multipart => true, :method => :put}) do |f| %>
        <%= error_messages_for 'post' %>
        <%= render :partial => 'post_form', :locals => {:f => f, :post => @post} %>
        <% fields_for @newfile do |newfile| -%>
        <p>
            <label for="newfile_data">Attach Files:</label>
            <% if @post.attachments.count >= 5 -%>
                            <input id="newfile_data" type="file" />
            <% else -%>
            <input id="newfile_data" type="file" disabled />
            <% end -%>
        </p>
        <% end -%>
        <ul id="pending_files">
            <% if @post.attachments.size > 0 -%>
            <%= render :partial => "attachment", :collection => @post.attachments %>
            <% end -%>
        </ul>
        <script type="text/javascript">
            var multi_selector = new MultiSelector($('pending_files'), <%= @allowed %>);
            multi_selector.addElement($('newfile_data'));
        </script>
        <p>
            <%= f.submit "Save Changes", :id => 'post_submit' %>
        </p>
    <% end -%>

You'll notice some similarities, but we have to make adjustments here on the edit form. Let's start with the changes that we've made:

<% fields_for @newfile do |newfile| -%>
    <p>
        <label for="newfile_data">Attach Files:</label>
        <input id="newfile_data" type="file" />
        <% if @post.attachments.count >= 5 -%>
        <% else -%>
        <input id="newfile_data" type="file" disabled />
        <% end -%>
    </p>
    <% end -%>

Again we are using a fields_for block here to assign fields to a new model. However, in order to control whether or not the file field is disabled I have hard-coded the file field. The only reason that the fields_for block is there is to keep consistent, and for example purposes. Notice that if the post that we are going to edit already has 5 attachments, the file field is disabled to prevent more attachments.

<ul id="pending_files">
        <% if @post.attachments.size > 0 -%>
        <%= render :partial => "attachment", :collection => @post.attachments %>
        <% end -%>
    </ul>

Once again we have our ul element that displays the attachments. Since the blog post that we are editing may already have attachments, we loop through each attachment that it may already have an display a partial for each one:

(/app/views/posts/_attachment.html.erb )

<%= "<li id=\"attachment_#{attachment.id}\">#{attachment.filename} %>
    <%= link_to_remote "Remove", :url  => attachment_path(:id => attachment), :method => :delete, :html => { :title  => "Remove this attachment" } %></li>

As you can see, all this partial does is display a li element that displays the filename of the attachment and also an Ajax link to remove that attachment. We'll deal with the Ajax link more when we get to our controllers. We still have one more section left that is new in our edit form:

<script type="text/javascript">
        var multi_selector = new MultiSelector($('pending_files'), <%= @allowed %>);
        multi_selector.addElement($('newfile_data'));
    </script>

This is the familiar script that creates our MultiSelector object. However, this time we are using a variable called @allowed to declare the number of attachments that are allowed to be uploaded. Remember that we have a limit of 5 attachments for each blog post, so we need to evaluate how many this particular post already has, and then respond accordingly.

The Controllers

So far we have put everything in place that we need to get multiple file uploads to happen. We've created the models that we need to hold our data, and we've created the views that we need to create and edit our models. Now all we need to do is to tie it all together. Remember that we have two resources, Post and Attachment , that we are working with, so we also have two controllers. Since most of the action happens in the Post controller, let's start with that one:

(/apps/controllers/\posts_controller.rb )

def new
        @post = Post.new
        @attachment = Attachment.new
    end

    def create
        @post = Post.new(params[:post])
        success = @post && @post.save
        if success && @post.errors.empty?
            process_file_uploads
            flash[:notice] = "Blog post created successfully."
            redirect_to(post_path(:id => @post))
        else
            render :action => :new
        end
    end

    def edit
        @post = Post.find(params[:id])
        @newfile = Attachment.new
        @allowed = 5 - @post.attachments.count
    end

    def update
        if @post.update_attributes(params[:post])
            process_file_uploads
            flash[:notice] = "Blog post update successfully."
            redirect_to(post_pat(:id => @post))
        else
            render :action => :edit
        end
    end

    protected

    def process_file_uploads
        i = 0
        while params[:attachment]['file_'+i.to_s] != "" && !params[:attachment]['file_'+i.to_s].nil?
            @attachment = Attachment.new(Hash["uploaded_data" => params[:attachment]['file_'+i.to_s]])
            @post.attachments << @attachment
            i += 1
        end
    end

Obviously, you would want to include the other RESTful actions to your actual controller such as index , show and destroy . However, since these are the only ones that are different for multiple file uploads, they are the only ones that I am showing. You'll notice that we have a nice little action that handles the multiple file uploads. Not a bad 8 lines of code, but basically the create and update actions are expecting the following parameters:

{
    "post" => {"title" => "Blog Post Title",
                            "content" => "Blog post content..."},
    "attachment" => {"file_0" => "FILE#1",
                                        "file_1" => "FILE#2"}
    }

It will then loop through all of the valid attachment files, save them and assign them to the blog post in question. Also notice that we have set the @allowed value in the edit action to make sure that our view knows how many attachments a blog post already has.

At this point we are in good shape. Our forms will work and we can upload multiple attachments. However we still haven't solved one of our problems, which is that we need to be able to remove attachments that were previously assigned to a blog post, which comes up on our edit form. Let's take a look at our other controller and see how we handle this challenge:

(/apps/controllers/\attachments_controller.rb )

def destroy
        @attachment = Attachment.find(params[:id])
        @attachment.destroy
        asset = @attachment.attachable
        @allowed = 5 - asset.attachments.count
    end

So let's take a look at this, which is a pretty basic destroy controller. Remember that we have Ajax links to this action from the attached files on our edit form. Each link sends the id of the attachment to be removed, which is passed to this controller. Once we have deleted the attachment from the database, we then want to get the "asset" that this attachment belongs to. In our example, the "asset" is a blog post, but remember that we made the Attachment relationship polymorphic for a reason. By getting the "asset" that the attachment was assigned to, we can get a count of how many attachments are left, which let's us update our familiar allowed variable.

The only thing left to do is to update the edit form to remove the attachment. Remember that any attachments that are added to the list via the file field are handled by our MultiSelector object, including removing them. However, since we are trying to remove the attachments that were already assigned to our blog post, we'll need to use an rjs template:

(/app/views/attachments/destroy.rjs )

page.hide "attachment_#{@attachments.id.to_s}"
    page.remove "attachments_#{@attachments.id.to_s}"
    page.assign 'multi_selector.max', @allowed
    if @allowed < 5
      page << "if ($('newfile_data').disabled) { $('newfile_data').disabled = false };"
    end

Again this is a pretty simple little file that does a lot. The first thing that we do is to hide the li element for this attachment, and then remove it completely from the DOM just to be sure. In order to update the number of allowed uploads, we assign a new value to multi_selector.max , which is the variable in our script that controls the maximum number of attachments (use -1 for unlimited attachments). Finally, just in case there were 5 attachments before we removed one, which would mean that the file field is disabled, we re-enable that file field if it is appropriate.

And that is about it! Aside from some CSS styling, we now have the ability to upload multiple files and attach them to a blog post using Ruby on Rails. Please download the sample files to see the code in action, and I would love to hear feedback and comments.

 

<think>我们正在处理一个关于PowerAutomate的问题:用户希望在保存电子邮件附件时,在文件名中包含日期信息。在PowerAutomate中,我们可以通过创建流来自动保存电子邮件附件,并且可以通过表达式来修改文件名,以包含当前日期或电子邮件接收日期等。步骤:1.创建一个自动化的云流,触发器选择“当新邮件到达时(V3)”(也可以是其他邮件触发器)。2.添加一个操作“获取附件(邮件V3)”。3.然后使用“应用每个附件”循环来处理每一个附件。4.在循环内部,添加一个“创建文件”操作(比如保存到OneDrive或SharePoint等),在文件名中我们可以使用表达式来动态添加日期。关键点:如何在文件名中添加日期?-我们可以使用当前日期(触发时间)或者邮件的接收日期。-在PowerAutomate中,我们可以使用表达式函数`formatDateTime`来格式化日期。例如,假设我们想使用当前日期(流运行的时间),可以在文件名中这样写:-表达式:`formatDateTime(utcNow(),'yyyy-MM-dd')`-然后将其与原始文件名结合,例如:`concat(formatDateTime(utcNow(),'yyyy-MM-dd'),'_',触发邮件的附件名称字段)`但是,用户可能希望使用邮件的接收日期。在触发器输出中,通常会有一个字段表示邮件的接收日期时间,例如`ReceivedDateTime`(在V3触发器中)。我们可以使用这个日期。因此,表达式可以是:-`formatDateTime(triggerBody()?['ReceivedDateTime'],'yyyy-MM-dd')`注意:在创建文件操作中,文件名字段我们可以使用表达式来构建。具体步骤:1.在“创建文件”操作的文件名字段中,点击“表达式”选项卡。2.输入表达式,例如:```concat(formatDateTime(triggerBody()?['ReceivedDateTime'],'yyyy-MM-dd'),'_',items('Apply_to_each')?['name'])```3.这里`items('Apply_to_each')`是循环中的当前附件,其`name`属性就是附件的原始文件名。另外,注意日期的格式可以根据需要调整,比如'yyyyMMdd'表示不带连字符。但是,我们也要考虑文件名中可能包含非法字符,但日期格式是安全的。示例流结构:触发:当新邮件到达时(Outlook)[V3]操作:获取附件(邮件V3)->输入:邮件ID(来自触发器)操作:应用每个附件(循环)在循环内:创建文件(OneDrive或SharePoint)文件内容:当前附件的内容文件名:使用上述表达式注意:如果同一个附件多次保存,可能会因为文件名相同而覆盖?但这里我们加了日期前缀,通常不会覆盖,除非同一邮件在同一个日期有多个同名附件。但通常,我们还可以在文件名中加入时间(精确到分或秒)来避免重复。如果用户需要更精确的时间,可以使用:`formatDateTime(triggerBody()?['ReceivedDateTime'],'yyyyMMddHHmmss')`或者,如果用户希望使用当前时间(流运行的时间),则用`utcNow()`。根据用户需求,我们通常使用邮件接收日期,因为它是邮件本身的属性。因此,我们可以这样构建文件名:`concat(formatDateTime(triggerBody()?['ReceivedDateTime'],'yyyyMMdd'),'_',附件名)`或者,如果用户希望有空格或其他分隔符,可以调整,但注意文件名中避免使用非法字符(如\/:*?"<>|)。另外,如果附件名本身已经很长,加上日期前缀不会导致文件名过长(不同存储系统有不同的限制,一般不会超过)。我们还可以考虑将日期放在文件名后面,但通常前缀更常见。最后,我们给出一个示例流的截图是不可能的,但可以描述步骤。因此,回答如下:</think>### 在 Power Automate 中保存带日期信息的邮件附件 要在 Power Automate 中保存电子邮件附件并在文件名中包含日期信息,请按照以下步骤操作: --- #### **步骤 1:创建流并设置触发器** 1. 新建一个 **自动化云流** 2. 选择触发器: `当新邮件到达时(V3)` (或 `When a new email arrives (V3)`) --- #### **步骤 2:获取邮件附件** 1. 添加操作: `获取附件(邮件 V3)` (`Get attachments (V3)`) 2. 配置参数: - **消息 ID**:从触发器动态内容中选择 `Message Id` --- #### **步骤 3:循环处理附件并添加日期** 1. 添加操作: `应用每个附件` (`Apply to each attachment`) 2. 在循环内部添加 **创建文件** 操作(如 OneDrive/SharePoint): ```typescript // 文件名表达式示例 concat( formatDateTime(triggerBody()?['ReceivedDateTime'], 'yyyyMMdd'), // 邮件接收日期 '_', items('Apply_to_each')?['name'] // 原始附件名 ) ``` - **关键表达式说明**: - `formatDateTime(triggerBody()?['ReceivedDateTime'], 'yyyyMMdd')` 将邮件接收日期格式化为 `20240515` 样式[^1] - 分隔符 `_` 可替换为 `-` 等合法字符 - 动态获取附件名:`items('Apply_to_each')?['name']` --- #### **步骤 4:完整流结构示例** ```plaintext 触发: 当新邮件到达时 (V3) │ 操作: 获取附件 (V3) │ 操作: 应用每个附件 (循环) ├─ 创建文件 (OneDrive) │ ├─ 文件内容 = 当前附件内容 │ └─ 文件名 = concat(formatDateTime(triggerBody()?['ReceivedDateTime'],'yyyyMMdd'),'_',附件名) │ └─ (可选) 添加时间戳: formatDateTime(utcNow(),'HHmmss') // 精确到秒避免重名 ``` --- #### **日期格式自定义选项** 通过修改 `formatDateTime` 的格式参数实现: | 格式示例 | 输出结果 | 用途 | |------------|---------------|--------------------| | `yyyyMMdd` | `20240515` | 基本日期 | | `yyyy-MM-dd`| `2024-05-15` | 标准日期格式 | | `ddMMyyyy` | `15052024` | 日-月-年格式 | | `yyMMddHHmm`| `2405150830` | 日期+时间(避免重名)| --- #### **注意事项** 1. **时区处理**: 使用 `convertTimeZone` 表达式调整时区(如邮件服务器时区不一致): ```typescript formatDateTime( convertTimeZone(triggerBody()?['ReceivedDateTime'], 'UTC', 'Tokyo Standard Time'), 'yyyyMMdd' ) ``` 2. **文件名冲突**: 当同一天存在同名附件时,添加时间戳或随机数: ```typescript concat( formatDateTime(utcNow(), 'yyyyMMdd_HHmmss_'), items('Apply_to_each')?['name'] ) ``` 3. **非法字符过滤**: 使用 `replace` 表达式移除文件名中的非法字符(如 `\/:*?"<>|`)[^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值