【译】三层架构代码生成器(NetTierGenerator)

NET分层架构生成器

·         下载源码 - 157 KB

·         下载示例 - 456 KB

简介

         现在存在许多的对象关系映射(ORM)和代码生成程序(Code generator),比如(NHibernatenetTiersEntity Framework等),它们中的一些是基于模板驱动引擎,而另外一些是基于解决方案框架的。本文将要展示的程序就是基于我自己的解决方案框架,从而它不仅仅是一个对象关系映射工具,它关注的重点将是如何为任何的项目或者工程呈现一种优越的开发环境。

解决方案框架

简言之,我的解决方案框架依然可以用经典的三层架构来描述。这个框架包含数据存取层(DAL),业务逻辑层(BLL),以及一些表现层的规则(GUI)。这个框架遵循了微软出版的对.NET的应用程序架构(Application Architecture for .NET)中的被公认的最佳实践。这个架构可以用以下图形表示:

 

但是就我个人观点而言,这个程序的最大优点就是在代码生成程序和底层数据存储之间存在一个中间转换层(我称其为MetaLayer),这一层是一组描述底层数据存储结构的XML文件,从而就给开发者带来了一种扩展基本功能的简便方式(声明新的业务实体类,新的数据读取方法等)。

 

解决方案框架中的各层

在这个解决方案中最重要的一个名词就是“服务(Services)”,解决方案通过各个层操作这些服务就像垂直放置的砖头给了混凝土以粘合各个系统中的功能一样。比如,对于一个存储有各个国家的字典,我们就会有一个CountryInfo DTO对象,一个CountryServiceDAL,一个ICountryServiceDAL,以及在业务逻辑层中的CountryService关键点在于每一种服务都被实现为无状态类(stateless class)。各个层以如下一种方式进行构造:DAL服务的方法应该被相应的业务逻辑的服务调用。数据存取曾被实现为一种通过数据工厂存取的数据提供者。这样就允许我们从具体的DAL实现中获得一种完美的抽象化。每一个层都被实现为一个独立存在的项目(或者说程序集),这样,一个典型的解决方案将包含以下几个工程:CommonConfiguration management MetaLayerBusiness Entities Model Data Access Layer InterfacesData Access Layer implementation, DAL Factory 以及 Business logic,如下图所示:

 

 

以下将分述各个工程的功能,将会给出部分实例。

Common

这个工程是放置一些公共代码的地方(包括程序中的全局查找啊,公共程序等等),有两个重要的紧紧融入到了解决方案以及代码生成器程序的特征:

1、  程序中的全局查找可以被用来进行业务实体的映射;

2、  业务实体验证引擎和修改历史追踪引擎也位于此程序集中。

Configuration Management

这是个通过“*.config”文件实现的配置管理的一个工程,之前我在我的这篇文章里面详细叙述了配置管理的具体实现。这个项目的主要一个益处就是实现了一个使用简单的代码来获得配置信息的功能。

string applicationName = CustomSettings.Current.ApplicationName;

Business Entity Model

它是一个实现了一组数据传输对象的独立项目,除了简单的对后台数据的一种容器外,数据传输对象还具有以下功能:

应用程序级别的数据验证,IClonable, IEquatable<>

数据验证功能借鉴自netTiers这个项目,它的核心是实现一组独立静态的方法,每一个数据转换对象类通过以下的方式使用这些静态方法:

 

private static void AddDatabaseChemaRules()

{

  GoodInfoModel.VALIDATION_RULES.AddRule(CommonRules.NotNull, "Name");

  GoodInfoModel.VALIDATION_RULES.AddRule(CommonRules.StringMaxLength,

                new CommonRules.MaxLengthRuleArgs("Name", 50));

  GoodInfoModel.VALIDATION_RULES.AddRule(CommonRules.NotNull, "Cost");

  GoodInfoModel.VALIDATION_RULES.AddRule(CommonRules.NotNull, "Quantity");

}

Data Access Layer

有很多种方法来给应用程序的需求呈现数据,一种是微软的ADO.NET这种形式,它是使用DataTablesDataAdapters进行基于数据表的数据映射;另外一种是被众多的ORM(比如NhibernatenetTiers等)使用的方式,这是基于数据表行级别的映射。这就意味着解决方案框架就像别的ORM一样在数据表的行级别进行数据映射,这种DAL的实现可以分立为一下三点:

1、一组DAL的服务实现一个具体的功能;

2、  一组 接口覆盖了一组DAL的对象;

3、  DAL工厂在运行时创建某一个具体DAL服务类的实例。

具体的DAL功能的实现将视具体情况而定,我在附件的实例代码中使用的就是一种接近于Data Access Application Block v1的方式,读者可以在源代码中看到。

底层数据库规则

我在进行数据库开发时严格遵循一些规则:

1、  数据库对象的命名规范:

a.  数据表---tblNamespace.Essence(例子:tblAdministration.UsertblAdministration.RoletblDictionary.Country);

b.  视图---vwNamespace.Essence(例子:vwDictionary.Country),tbl/vw是代表table/view的前缀,Administration/Dictionary代表某种命名空间,User/Role/Country的后缀代表对象。

2、  只将底层的数据库当做一个数据存储的容器,不要在这里进行其他的业务的操作,换言之,就是要避免向业务目标使用触发器,存储过程。将数据库视作只存储数据、索引以及数据完整性的容器,仅此而已;

3、  使用GUID来作为主键(不要只用整数来作为身份标识)。

Business Layer

在这一层需要解决以下几个问题:

1、  最重要的事情就是会话管理(Session Management)的实现,特别地史事务管理(Transaction Management)的实现;

2、  实际的问题是进行对底层数据库(或者其他地方)读取来的数据进行缓存的能力;

3、  如果一个正规的开发人员可以避免犯常识性的错误,那么这将是一个非常有用的结果。

会话管理和事务管理的问题已经在其他的ORM中被解决了,一个会话和事务的接口类以一种静态线程的方式在业务逻辑层进行维护,所以不需要在函数调用间对会话进行维护,而且只有在业务逻辑层有一些严格的规则来处理会话和事务。缓存是基于MS Enterprise Library Caching作为一个独立的服务来实现的。缓存的实现大量的用到了.NET2.0中的新特性----匿名方法,所以可以非常容易的实现数据缓存。

我已经咋BLL级别上实现了DAL的用法,这种方法是微软在他的开放项目PetShop 4中的一种实现方法。BLL的类使用IDAL的借口作为内部静态成员,用DAL工厂来进行具体实现的实例化。

private static readonly IAgreementServiceDal dal =

  DalManager.CreateInstance("Economy.AgreementServiceDal") as IAgreementServiceDal;

以后,一个正规的BLL方法就像以下的例子一样实现获得数据!

public AgreementInfoModel GetAgreementInfoById(Guid id)

{

    string key = String.Concat(AgreementService.AGREEMENT_BY_ID, id.ToString());

    return CacheService.GetData<agreementinfomodel>(

            key,

            AgreementService.AGREEMENT_BY_ID_SINC_KEY,

            TimeSpan.FromSeconds(AgreementService.AGREEMENT_BY_ID_CACHE_INTERVAL),

            delegate

            {

                AgreementInfoModel agreement = null;

                using (Session session = base.OpenSession())

                {

                    agreement = dal.GetAgreementInfoById(session.Current, id);

                    if (agreement != null)

                    {

                        agreement.CurrentAmount =

                          dal.GetAgreementTransferAmountByAgreementId(

                          session.Current, agreement.Id, DateTime.Today);

                    }

                }

                return agreement;

            }

        );

}

一个正规的BLL方法像以下的例子一样实现一个操作活动:

public void DeleteAgreementInfoById(AgreementToInfoModel agreementTO)

{

    using (Session session = base.OpenSession())

    using (Transaction tx = session.BeginTransaction())       

    {

        if (agreement.ImageId != Guid.Empty)

        {

            ServiceFacade.ImageService.DeleteImageInfoById(agreement.ImageId);

        }

        dal.DeleteAgreementInfoById(session.Current, agreement.Id);

        tx.Commit();

    }

}

GUI在下面各层的使用

这是一种聚集了所有BLL服务于一点的一种外观模式的服务,这种服务使得下面各层从GUI调用方法显得非常的容易。

protected void btnSave_Click(object sender, EventArgs e)

{

    if (Page.IsValid)

    {

        BranchInfoModel item = this.pc.GetObject() as BranchInfoModel;

        ServiceFacade.BranchService.SaveBranch(item);

 

        this.ShowListPage();

    }

}

public override object GetObject()

{

    BranchInfoModel item = ServiceFacade.BranchService.GetBranchInfoById(this.ItemId);

    if (item == null)

    {

        item = new BranchInfoModel();

    }

    item.Name = this.tbName.Text;

    item.Email = this.etbEmail.Text;

    return item;

}

NetTierGenerator 代码生成程序

上面的解决方案框架给了我们一个完美的代码生成的环境,推荐的代码生成器处理数据传输对象(DTO)模型,DAL+IDAL),以及BLL层。而且在底层的数据库结构和输出代码之间还存在一个中加级别的层次,这就允许我们再中间层实现创建额外的数据库调用方法。

这就是这个程序的另外一个重要的特性了,它给每一个类生成了两个物理文件(一个是生成的内容,另外一个是开发者的需求,这俩文件在每一个层次都有包含对C#分部类项的声明):

 

推荐的代码生成程序解决了以下的任务:

1、它允许将生成的内容放进一个目标命名空间;

2、它把数据库的表或者视图映射成了它自己的中间声明结构;

3、它把每一个中间层的XML声明文件映射成了它自己的应用程序服务;

4、每一个中间层的XML文件包含了多个数据库表或者视图的映射(这就允许将它们约束到一个单一的服务);

5、它允许轻松地将数据库表的行映射为C#的枚举器;

6、它允许声明额外的与底层数据库进行交互的方法;

7、它允许在每一层进行代码生成操作,从而我们可以很容易的使用地定义代码来重写生成的内容;

8、对于每一个从底层数据库创建而来的XML声明都有一个相应的GUI码;

9、它有一个获得分页数据的功能(这里我将用一个这篇文章描述的方法之一):http://www.codeproject.com/KB/aspnet/PagingLarge.aspx

一个简单的XML声明将像如下的例子:

<TierModel Namespace="Economy" ServiceName="City">

    <Declare Type="Solution.Common.Economy.BranchConditionEnum" />

    <Declare Type="Solution.Common.Economy.PaymentTypeEnum" />

 

    <Include Path="Economy\Image.xml" Type="ImageInfo" />

   

    <ItemModel DbTable="tblEconomy_City"

             ClassName="CityInfo"

             Caching="True" Parent="">

        <Comment />

        <KeyProperty NeedToGenerate="true" ReadOnly="False">

            <Comment />

            <CSharp CSharpName="id" CSharpType="Guid" />

            <Db DbName="rowguid" DbType="uniqueidentifier"

                     IsNullable="False" Length="16" />

        </KeyProperty>

        <Property ReadOnly="False">

            <Comment />

            <CSharp CSharpName="BranchId" CSharpType="Guid" />

            <Db DbName="BranchId" DbType="uniqueidentifier"

                      IsNullable="False" Length="16" />

        </Property>

        <Property ReadOnly="False">

            <Comment />

            <CSharp CSharpName="Name" CSharpType="string" Length="100" />

            <Db DbName="Name" DbType="nvarchar"

                     IsNullable="False" Length="100" />

        </Property>

        <SelectMethod NeedToCreate="True" DalAccess="True" BllAccess="True" />

        <InsertMethod NeedToCreate="True" DalAccess="True" BllAccess="True" />

        <UpdateMethod NeedToCreate="True" DalAccess="True" BllAccess="True" />

        <DeleteMethod NeedToCreate="True" DalAccess="True" BllAccess="False" />

    </ItemModel>

   

    <ListItemModel DbView="vwEconomy_City" ClassName="CityListItem" Parent="">

        <Comment />

        <KeyProperty>

            <Comment />

            <CSharp CSharpName="id" CSharpType="Guid" />

            <Db DbName="rowguid" DbType="uniqueidentifier"

                      IsNullable="False" Length="16" />

        </KeyProperty>

        <Property>

            <Comment />

            <CSharp CSharpName="BranchId" CSharpType="Guid" />

            <Db DbName="BranchId" DbType="uniqueidentifier"

                     IsNullable="False" Length="16" />

        </Property>

        <Property>

            <Comment />

            <CSharp CSharpName="Name" CSharpType="string" Length="100" />

            <Db DbName="Name" DbType="nvarchar"

                    IsNullable="False" Length="100" />

        </Property>

        <Property ReadOnly="False">

            <Comment />

            <CSharp CSharpName="BranchName"

                     CSharpType="string" Length="100" />

            <Db DbName="BranchName" DbType="nvarchar"

                     IsNullable="False" Length="100" />

        </Property>

    </ListItemModel>

   

    <SelectMethod Name="GetCitiesByBranchId"

               DalAccess="True" BllAccess="True">

        <Comment />

        <Return ReturnType="IList" Type="CityInfo">

            <Comment />

        </Return>

        <Property>

            <Comment />

            <CSharp CSharpName="BranchId" CSharpType="Guid" />

            <Db DbName="BranchId" DbType="uniqueidentifier"

                    IsNullable="False" Length="16" />

        </Property>

        <Sql>

            <Query><![CDATA[SELECT * FROM tblEconomy_City

                         WHERE BranchId = @BranchId]]></Query>

        </Sql>

    </SelectMethod>

    <SelectMethod Name="GetListPage" DalAccess="True" BllAccess="True">

        <Comment />

        <Return ReturnType="ListPage" Type="CityListItem">

            <Comment />

        </Return>

        <Sql>

            <Query><![CDATA[SELECT * FROM vwEconomy_City]]></Query>

        </Sql>

    </SelectMethod>

 

    <UpdateMethod Name="InsertCityImage"

              DalAccess="True" BllAccess="False">

        <Comment></Comment>

        <Property>

            <Comment />

            <CSharp CSharpName="CityId" CSharpType="Guid" />

            <Db DbName="CityId" DbType="uniqueidentifier"

                   IsNullable="False" Length="16" />

        </Property>

        <Property>

            <Comment />

            <CSharp CSharpName="ImageId" CSharpType="Guid" />

            <Db DbName="ImageId" DbType="uniqueidentifier"

                      IsNullable="False" Length="16" />

        </Property>

        <Sql>

            <Query><![CDATA[INSERT INTO tblEconomy_CityImages

                     (CityId, ImageId) VALUES (@CityId, @ImageId)]]></Query>

        </Sql>

    </UpdateMethod>

</TierModel>

你可以看到,这就是简洁明了,而且对于我来说,很有吸引力的是开发者可以在这一点上决定SQL的声明。因为每一个数据库都有其优点和缺点,而且对于不同的数据库引擎使用相同的有限的方式来编写或者生成SQL语句是不可取的,比如微软的SQLServer包含了一些独一无二的SQL语句,像Existsrecursive CTE, HierarcyId等等。

 

这里有一个对以上的XML元素声明的详细描述:

TierModel

这是定义了服务名称和在每层的位置的根节点,上面的例子中就是放在每一层中的Economy命名空间中,且他的名称是CityService

Declare

这个节点允许我们针对服务类声明附加的 using(引用),从而我们可以使用一个已经声明的命名空间里面的类型。

Include

这个节点允许使用其他XML文件里面的声明,比如在这个例子中我们可以引用一个ImageInfo模型。

ItemModel

这个是整个代码生成程序的基节点类型,它映射将一个数据库的表映射成一个DTO类,它允许声明主键属性,这个主键属性将会在生成的增删改查(CRUD)操作中用到,它允许将每一个数据表中的列映射到DTO类属性中,也允许指出生成的验证代码的细节。

ListItemModel

这个节点主要是用来声明列表的DTO类(与ItemModel的最大区别就是ListItemModel可以包含来自其他对象的字段,比如CityListItem可以包含一个BranchName的字段),同时,虽然他们不具有增删改查操作,但是他们的主键属性被用来保持列表在运行时的严格的次序。

SelectMethod

这个节点允许我拿定义自定义的方法,这些方法可以返回C#中的类型和XML中定义的类型之一;并且,它包含一些可以被进一步传送到方法签名或者是SQL语句中的属性;还有,我们可以通过操作DalAccess/BllAccess给在每一层的某一个方法一个自定义的实现;最后,它包含一个SQL节点,我们可以编写出一个SQL语句。这就是我们在需要时可以拆分一个具体的方法实现给不同的数据库之所在。

UpdateMethod

它和绝大多数的SelectMethod的特性一致,出了一点:它没有他任何类型的返回值。

怎么使用NetTierGenerator

这里我讲给大家展示一种最好的使用这个工具的方法,为了达到这个目的,我将使用一个实例WinForm程序。

MS SQLServer 底层数据库

我使用一个有几个表的实例数据库结构。他们都对一套小的规则兼容,“Administration”命名空间里面的表将不会在GUI应用程序中使用,他们被包含进项目的主要目的是为了展示NetTierGenerator以及推荐的应用程序框架的一些有用的技巧。

1、/视图必须对应命名空间和项目的名称;

2、不要使用触发器和存储过程(为了SQLServer有足够的能力来执行我们的绝大多数的查询请求,参阅[sys.dm_exec_query_stats]),使用SQLServer数据库的直接目的---存储数据,以及保持数据索引和数据完整性;

3、经常使用uniqueidentifier列来作为表的主键,因为各种各样的原因,这种做法很有用(比如保持数据库复制性等)。

使用NetTierGenerator的第一步

l   首先,我们定义一个适当的地方来放置NetTierGenerator的二进制文件然后修改一些设置,下图是一个解决方案的物理文件夹视图    ,“tools”文件夹就包含了NetTierGenerator的二进制文件:

 

l   修改NetTierGenerator 的“App.config”来对应你当前的数据库设置;

l   下面,我们将进行NetTierGenerator的配置工作,可以修改“TierGeneratorSettings.xml”或者使用可执行程序“NetTierGenerator.WinUI.exe”来完成配置;

l   下面,我们将这些可执行的组件在VS中注册为外部工具,顺便给他们设置快捷键。

 

 

将底层数据库映射到你的解决方案中

现在我们将要创建XML映射,我们组要做的就是在VS中打开解决方案,然后打开NetTierGeneratorWinGUI

1、在“DataBase tables”组合框中选择“tblStore_Good”,然后程序会自动为当前的模型设置“命名空间”以及“服务名称”,然后选择标签“List Item”然后选择vwStore_Good,点击“Generate XML only”按钮;

2、NetTierGenerator在“TierModel”文件夹中生成一个XML文件;

3、VS中打开上述XM文件,然后使用NetTierGenerator的控制台版本。此时你就可以添加任何你想要的自定义方法了;

4、系统将会产生如下东西:

A、       在项目Sample.Model中生成“GoodInfo”, “GoodListItem”

B、       在项目Sample.IDAL中生成IgoodServiceDal

C、       在项目Sample.MSSqlDal中生成GoodServiceDAL

D、       在项目Sample.BusinessLogic中生成 GoodService (同时修改ServiceFacade)

 

 

系统会给每一个类产生两个文件,一个用于NetTierGenerator,它将每次用这些文件来进行重新生成,另外一个给用户定义代码。

怎么使用以前生成的东西?

只有一个地方允许我们进入BLL的所有方法,那就是---ServiceFacade,开发者在表现层不必要生成BLL服务的实例

列表支持

最常见得目的之一就是---现实列表,示例应用程序使用虚拟模式的DataGridView控件来在网格中显示数据,此时垂直滚动条已经被激活了。该控件每次从底层数据库获得一小部分的数据,以下就是执行这个任务的代码(排序和过滤都可以在这里轻松地实现):

this.currentGoodListQuery = new ListQuery();

this.currentGoodListQuery.RowsPerPage =

     this.dgvOrderList.DisplayedRowCount(false);

this.currentGoodListQuery.FirstRowIndex =

     this.dgvOrderList.FirstDisplayedScrollingRowIndex;

 

this.currentGoodListPage =

  ServiceFacade.GoodService.GetListPage(this.currentGoodListQuery);

this.dgvOrderList.RowCount = this.currentGoodListPage.TotalRowCount;

数据行级别的读取和增删改查

本应用程序框架提供了便捷增删改查的操作方法,以下代码演示了一个将输入数据存储到底层数据库的DTO实例:

private void SaveInfoModel()

{

    if (this.goodInfoModel == null)

    {

        this.goodInfoModel = new GoodInfoModel();

    }

 

    this.goodInfoModel.Name = this.tbName.Text;

    this.goodInfoModel.Cost = FormatHelper.ParseDecimal(this.tbCost.Text);

    this.goodInfoModel.Quantity = FormatHelper.ParseInt32(this.tbQuantity.Text);

 

    if (this.goodInfoModel.ID == Guid.Empty)

    {

        ServiceFacade.GoodService.InsertGoodInfo(this.goodInfoModel);

    }

    else

    {

        ServiceFacade.GoodService.UpdateGoodInfo(this.goodInfoModel);

    }

}

怎样个性化框架的代码呢?

假想我们需要个性化一些具体实现代码,可以使用的代码修改方法有多种

重写生成的代码

举个例子来说吧,假如某一个项目被移除了,我们需要给一些发送一封邮件,在此种情况下,我们需要继承BLL中的DeleteGoodInfo的实现,要完成这个任务,我们需要做一下两步:

1、从生成的代码中移动DeleteGoodInfoByID到一个用户想要的文件里面,然后应用代码修改;

2、OpenGood.XML”,设置GoodInfo中的DeleteGoodInfoByIDBllAccessfalse,然后重新生成代码;

添加一个用户定义方法

举个例子吧,假设我们为了实现得到商品中价格小于某一数字的商品的总数,这时我们就要以我们自定义的方式来定义一个我们自己的方法,可以像如下代码一样声明:

<SelectMethod Name="GetGoodAmountByCost">

    <Comment />

    <Return ReturnType="int">

        <Comment />

    </Return>

    <Property>

        <Comment />

        <CSharp CSharpName="Cost" CSharpType="decimal" />

        <Db DbName="Cost" DbType="numeric"

          IsNullable="False" Length="9" />

    </Property>

    <Sql><Query>

<![CDATA[SELECT COUNT(ID) AS [count] FROM tblStore_Good WHERE Cost <= @Cost]]>

    </Query></Sql>

</SelectMethod>

当你在XML中定义这一些方法以后,你应该重新生成代码。DAL层、IDAL层以及BLL层将会做相应改变,从而你就可以在表现层中使用这一些带有特定参数的方法啦!

总结

这个代码生成器的作用可能不能满足所有开发者的需求,现在我描述一下这个代码生成器的功能和使命吧:

1、他可以在几分钟之内帮你完成一个应用程序的骨架,而且它可以保证任何下层的代码做了改变,上层的代码都会得到更新;

2、该框架只是定义了一些业务规则,数据开发,和图形界面开发的实现,但是不包含一大堆乱七八杂的这些个项目的具体实例;

3、解决方案框架和代码生成器放在了一起;

4、存在一个中间变换层(XML声明),这就使我们可以控制代码生成的输出,换言之,我们在进行“中间变换层开发”;

5、将各开发好的应用程序严格分层,这就允许我们可以进行数据库事务管理。它定义了一个对业务规则的严格的容器,你就可以避免了你的代码的混乱;

6、所有活动都在设计时(design time)完成,而不是运行时(run time);

7、让程序开发变得易管理,便捷以及更高层次的:开发具有乐趣;

8、该框架避免了开发者的一些常犯错误;

9、NetTierGenerator可以被用作任何程序开发

证书

这篇文章,以及附属的源代码文件都遵循The Code Project Open License (CPOL)证书管理

关于作者

Dmitry Zubrilin



职业: Software Developer (Senior)
所在地: Russian Federation Russian Federation Member

其他热门的关于代码生成的文章

A powerful tool for rapid application development.

Using NArrange to organize C# source code.

Generate class shells from SQL Server database tables, (SQL 2005 & 2008 only). Output languages supported: C# and VB.NET.

A look at the minimal metadata needed (database mapping and user interface) for CRUD applications code generation using the example of a to do list.

An open source code generation utility with some useful features to generate procedures,class for tables and .net code for procedures automatically.

 

转载于:https://www.cnblogs.com/lkmmmj/archive/2009/06/27/1512321.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值