使用nodejs数据库ORM(Object Relational Mapping)模块

使用nodejs数据库ORM(Object Relational Mapping)模块

Build Status


Flattr this git repo

安装

npm install orm

Node.js 版本支持

支持版本: 0.12 - 6.0 +

相关测试已在 Travis CI 上运行,当然你也可以在本地运行测试程序:

npm test

支持的数据库管理系统DBMS

  • MySQL 和 MariaDB
  • PostgreSQL
  • Amazon Redshift
  • SQLite
  • MongoDB (beta版本, 当前暂未汇总进来)

功能

介绍

这是一个node.js的对象-关系映射(ORM)模块。

示例:

var orm = require("orm");

orm.connect("mysql://username:password@host/database", function (err, db) {
  if (err) throw err;

    // 定义声明对象模型
    var Person = db.define("person", {
        name      : String,
        surname   : String,
        age       : Number, // 浮点型
        male      : Boolean,
        continent : [ "Europe", "America", "Asia", "Africa", "Australia", "Antartica" ], // ENUM枚举型
        photo     : Buffer, // 二进制BLOB数据
        data      : Object // 以JSON编码
    }, {
        methods: {
            fullName: function () {
                return this.name + ' ' + this.surname;
            }
        },
        validations: {
            age: orm.enforce.ranges.number(18, undefined, "under-age")
        }
    });

    // 将声明的对象模型以建表的形式同步添加到数据库
    db.sync(function(err) {
        if (err) throw err;

        // 添加一条内容到 person 表
        Person.create({ id: 1, name: "John", surname: "Doe", age: 27 }, function(err) {
            if (err) throw err;

                // 以 surname 的值查询person表
                Person.find({ surname: "Doe" }, function (err, people) {
                    // 相当于执行SQL语句: "SELECT * FROM person WHERE surname = 'Doe'"
                    if (err) throw err;

                    console.log("People found: %d", people.length);
                    console.log("First person: %s, age %d", people[0].fullName(), people[0].age);

                    people[0].age = 16;
                    people[0].save(function (err) {
                        // err.msg = "under-age";
                });
            });

        });
    });
});

以Promises方式调用

你可以使用这个模块 q-orm.它将orm封装成基于Promise方式

Express中间件

如果你使用Express开发, 你可以很容易地将orm以中间件的形式整合到你的Express项目中。

var express = require('express');
var orm = require('orm');
var app = express();

app.use(orm.express("mysql://username:password@host/database", {
    define: function (db, models, next) {
        models.person = db.define("person", { ... });
        next();
    }
}));
app.listen(80);

app.get("/", function (req, res) {
    // req.models就是上面define函数声明中的models
    req.models.person.find(...);
});

你可以调用orm.express多次来实现多个数据库的连接。不同数据库连接的对象模型全部统一都放在req.models对象中。
记住要在路由app.use(app.router)之前使用orm中间件。另外,最好在定义static公共目录路由之后使用。

示例代码

在项目路径examples/anontxt中包含了一个基于Express框架的orm用例代码供参考。

说明文档

详见 wiki.

配置设置

详见 wiki.

数据库连接

详见 wiki.

数据模型

一个数据模型是基于数据库中一个或多个表抽象化而成的类。数据模型支持关系型数据 (详见下文)。数据模型类的名称对应数据库中的表名。

数据模型类提供了对数据库表数据的访问和修改功能。

定义数据模型

详见 wiki.

数据模型属性(Properties)

详见 wiki.

数据模型实例方法(Instance Methods)

数据模型的实例方法在定义模型时声明

var Person = db.define('person', {
    name    : String,
    surname : String
}, {
    methods: {  //定义实例方法
        fullName: function () {
            return this.name + ' ' + this.surname;
        }
    }
});

//调用内置的数据模型类get方法
Person.get(4, function(err, person) {
    //返回的person为数据模型person实例
    console.log( person.fullName() );
})

数据模型类方法(Model Methods)

数据模型类方法直接声明在类对象上。

var Person = db.define('person', {
    name    : String,
    height  : { type: 'integer' }
});
Person.tallerThan = function(height, callback) {
    this.find({ height: orm.gt(height) }, callback);
};

Person.tallerThan( 192, function(err, tallPeople) { ... } );

加载数据模型声明

数据模型可以分别在不同的模块中定义。只要确保声明数据模型的模块给module.exports赋值为你的函数,函数接收一个数据库连接db对象和一个回调cb,你就可以在函数中定义数据模型了。
注 - 利用这个技巧,就能够以级联层叠的方式去加载定义数据模型。

// 你的程序主入口,如main.js  (在数据库db连接成功之后)
db.load("./models", function (err) {
    // loaded!
    var Person = db.models.person;
    var Pet    = db.models.pet;
});

// 定义数据模型的一个模块models.js
module.exports = function (db, cb) {
    db.load("./models-extra", function (err) {
        if (err) {
            return cb(err);
        }

        db.define('person', {
            name : String
        });

        return cb();
    });
};

//  定义数据模型的另一个模块models-extra.js
module.exports = function (db, cb) {
    db.define('pet', {
        name : String
    });

    return cb();
};

同步和删除数据模型(Sync/Drop)

详见 wiki.

高级选项(Options)

ORM支持在定义数据模型时设置附加的高级选项,你可以在ORM模块settings设置中或者define定义数据模型时配置这些高级选项。

例如,每一个数据模型实例在数据库中默认都有一个唯一的ID,这一列ID作为主键自动被加到表中的,列字段名默认为”id”.

若你在定义模型时以附加选项key:true声明了自己的主键,则不会再添加默认的主键”id”列。

var Person = db.define("person", {
    personId : { type: 'serial', key: true },
    name     : String
});

// 你也可以用全局的方式修改默认的"id"列名
db.settings.set("properties.primary_key", "UID");

// ..然后定义你的数据模型
var Pet = db.define("pet", {
    name : String
});

数据模型Pet 数据表将会有两列字段,分别是UIDname

你还可以声明多个主键作为复合主键:

var Person = db.define("person", {
    firstname : { type: 'text', key: true },
    lastname  : { type: 'text', key: true }
});

其他高级选项:

  • identityCache:(默认:false)设为true则启用身份缓存(Singletons(单例))或设为超时时长(秒);
  • autoSave:(默认:false)设为true则当数据模型属性被修改时立刻自动保存;
  • autoFetch:(默认:false)设为true则当从数据库中查询获取对象的同时自动获取其关联的数据。
  • autoFetchLimit:(默认:1)如果autoFetchtrue,则该值代表自动获取数据的关联层级数或环节数。

钩子函数(Hooks)

详见 wiki.

查询数据条目

Model.get(id, [ options ], cb)

Model.get以主键值获取指定的单个元素。

Person.get(123, function (err, person) {
    // 查找id=123的元素
});

Model.find([ conditions ] [, options ] [, limit ] [, order ] [, cb ])

以更多的条件(conditions)和附加选项参数来获取一个或多个元素,每项参数没有规定的顺序,只要保证optionsconditions之后即可(即使条件为空对象)。

Person.find({ name: "John", surname: "Doe" }, 3, function (err, people) {
    // 查找name='John' 并且 surname='Doe' 的元素,并且只返回符合条件的前3个元素
});

如果你需要对结果进行排序,因为某些原因或仅仅因为你希望他们排序,可以这么做:

Person.find({ surname: "Doe" }, "name", function (err, people) {
    // 查找surname='Doe'的元素,并且返回结果以name升序排列
});
Person.find({ surname: "Doe" }, [ "name", "Z" ], function (err, people) {
    // 查找surname='Doe'的元素,并且返回结果以name降序排列
    // ('Z' 表示DESC降序; 'A' 表示ASC升序 - 默认升序)
});

你可以传进更多的选项参数来进行查找,将这些选项放到一个对象中以第二个参数传递:

Person.find({ surname: "Doe" }, { offset: 2 }, function (err, people) {
    // 查找surname='Doe'的元素,跳过前两个然后返回剩余的元素
});

你也可以使用原生的SQL语句进行查询,这将在下面的链式调用部分说明

Model.count([ conditions, ] cb)

假如你只需要统计符合某条件下的元素个数, 你只需要使用.count()而不需要用.find()查找它们再计数。因为这实际上会让数据库自己去做计数操作(这样就不会占用node自身的进程来完成)。

Person.count({ surname: "Doe" }, function (err, count) {
    console.log("We have %d Does in our db", count);
});

Model.exists([ conditions, ] cb)

.count()方法类似, 这个方法只是判断count结果是否大于0.

Person.exists({ surname: "Doe" }, function (err, exists) {
    console.log("We %s Does in our db", exists ? "have" : "don't have");
});

Aggregating Functions

如果你想从数据模型中以不同条件获取聚合的值,你可以使用Model.aggregate()方法,例如:

Person.aggregate({ surname: "Doe" }).min("age").max("age").get(function (err, min, max) {
    console.log("The youngest Doe guy has %d years, while the oldest is %d", min, max);
});

传递一个属性名数组来查询指定的属性,相应也需要接收一个对象来指定查询条件。

以下代码演示了如何使用.groupBy()方法:

//相当于SQL语句"select avg(weight), age from person where country='someCountry' group by age;"
Person.aggregate(["age"], { country: "someCountry" }).avg("weight").groupBy("age").get(function (err, stats) {
    // stats为一个数组,每项都包含 'age' 和 'avg_weight'
});

基础的 .aggregate() 方法

  • .limit(): 传递一个数字代表限制元素个数,传递两个数字则分别代表offset偏移和limit个数
  • .order(): 类似于 Model.find().order()

附加的 .aggregate() 方法

  • min
  • max
  • avg
  • sum
  • count (相当于 - Model.count)

根据不同的数据库driver类型有更多可用的聚合函数。 (例如Math函数).

链式调用

假如你不想让你的代码结构变复杂,你可以在调用.find()方法时不传递回调函数,改用链式调用的方式。

Person.find({ surname: "Doe" }).limit(3).offset(2).only("name", "surname").run(function (err, people) {
    // 查找surname='Doe'的元素, 跳过前2个元素返回3个符合条件的元素
    // 返回的每个元素只有'name' 和 'surname' 属性
});

如果你只想忽略一两个属性,你可以用.omit()代替.only方法。

链式调用允许更复杂的查询。比如可以用自定义的SQL语句来查询:

Person.find({ age: 18 }).where("LOWER(surname) LIKE ?", ['dea%']).all( ... );

不推荐手动拼接SQL字符串参数,因为这很容易出错并且导致你的程序会被SQL注入。
使用?语法可以帮你完成转义拼接,它可以安全地将查询语句中的问号替换成你提供的参数。
你也可以根据需要链式调用多个where子句。

.find, .where.all 都是做同样的事情;它们都可以互相替换和链式调用。实际上都是同一个方法。

你可以用 orderorderRaw方法进行排序:

Person.find({ age: 18 }).order('-name').all( ... );
// 更多细节请看下文 '原生查询' 部分
Person.find({ age: 18 }).orderRaw("?? DESC", ['age']).all( ... );

你可以在链式调用最后用.count()获取元素个数,这种情况下.offset(),.limit().order()操作会被忽略。

Person.find({ surname: "Doe" }).count(function (err, people) {
    // people 为满足surname="Doe"的元素个数
});

此外,你还可以将所获取的元素删除。
要注意的是链式调用后的remove操作不会触发相关的钩子函数(hooks)。

Person.find({ surname: "Doe" }).remove(function (err) {
    // 完成..
});

你也可以像遍历数组一样在迭代回调方法中修改你的数据对象实例,最后将它们保存。

Person.find({ surname: "Doe" }).each(function (person) {
    person.surname = "Dean";
}).save(function (err) {
    // 完成!
});

Person.find({ surname: "Doe" }).each().filter(function (person) {
    return person.age >= 18;
}).sort(function (person1, person2) {
    return person1.age < person2.age;
}).get(function (people) {
    // 获取所有年龄18以上的人,并以年龄排序
});

当然这些操作你可以直接在.find()方法中实现,但是如果需要做更复杂一些的操作的话,这种方式还是很有用的。

Model.find() 方法返回的并不一定是个数组,所以安全起见不能直接用数组方法链式调用,你需要先调用
.each()方法(你顺便可以在该方法回调中做遍历操作)。然后你就可以用数组共有的方法
.filter(), .sort().forEach()在后面进行多次的链式调用了。

在链式调用的最后(或在中间环节..)你可以调用:
- .count() 获取当前的数据项个数,如果你只想知道个数的话;
- .get() 获取当前数据列表。
- .save() 将对数据项所做的修改保存到数据库。

查询条件(Conditions)

每项条件都被定义在一个以属性(列名)为key的对象中,各项key对应的条件都用AND(逻辑与)关联。
对象值为字符串表示数据列值完全匹配该字符串才满足条件,如果对象值为数组则表示数据只要匹配其中一项就满足条件。

{ col1: 123, col2: "foo" } // `col1` = 123 AND `col2` = 'foo'
{ col1: [ 1, 3, 5 ] } // `col1` IN (1, 3, 5)

如果你还需要其他的比较条件,你可以使用内置的一些辅助方法来定义各种条件,这里列出一些例子:

{ col1: orm.eq(123) } // `col1` = 123 (默认)
{ col1: orm.ne(123) } // `col1` <> 123
{ col1: orm.gt(123) } // `col1` > 123
{ col1: orm.gte(123) } // `col1` >= 123
{ col1: orm.lt(123) } // `col1` < 123
{ col1: orm.lte(123) } // `col1` <= 123
{ col1: orm.between(123, 456) } // `col1` BETWEEN 123 AND 456
{ col1: orm.not_between(123, 456) } // `col1` NOT BETWEEN 123 AND 456
{ col1: orm.like(12 + "%") } // `col1` LIKE '12%'
{ col1: orm.not_like(12 + "%") } // `col1` NOT LIKE '12%'
{ col1: orm.not_in([1, 4, 8]) } // `col1` NOT IN (1, 4, 8)
原生查询
db.driver.execQuery("SELECT id, email FROM user", function (err, data) { ... })

// 你可以将SQL中的标识符和值进行转义替换处理
// 用双问号: ?? 对标识符(如表名列名)进行替换
// 用单问号: ?  对值(数值或字符串字面量)进行替换
db.driver.execQuery(
  "SELECT user.??, user.?? FROM user WHERE user.?? LIKE ? AND user.?? > ?",
  ['id', 'name', 'name', 'john', 'id', 55],
  function (err, data) { ... }
)

// 大多数情况下标识符不需要进行转义替换
db.driver.execQuery(
  "SELECT user.id, user.name FROM user WHERE user.name LIKE ? AND user.id > ?",
  ['john', 55],
  function (err, data) { ... }
)

单例模式(Identity pattern)

你可以对数据模型启用缓存的单例模式(默认禁用)。启用后,对数据库多个查询操作会返回同一个结果 - 即你获得的是同一个数据对象。
如果你有其他地方会修改数据库或者你要手动执行SQL查询操作,那么就不应该开启这个功能。
另外在关系复杂的数据模型下同时与autofetch功能使用会引起一些问题,请慎用。

该功能可针对单个数据模型分别开启或禁用:

var Person = db.define('person', {
    name          : String
}, {
    identityCache : true
});

也可以对全局开启或禁用:

orm.connect('...', function(err, db) {
  db.settings.set('instance.identityCache', true);
});

identityCache除了设置布尔值外,还可以设置成一个数值。表示单例的缓存的有效时间,该数值以秒为单位(可以设置浮点数)。

注意: 还有一个例外就是当一个数据实例未保存时,则不会使用缓存单例。 例如你获取到一个Person的数据对象然后修改它,
那么直到它被.save()保存之前,这之间的处理操作都不会以缓存来对他进行传递。

插入数据项(Creating Items)

Model.create(items, cb)

使用Model.create方法来插入新元素到数据表中。

Person.create([
    {
        name: "John",
        surname: "Doe",
        age: 25,
        male: true
    },
    {
        name: "Liza",
        surname: "Kollan",
        age: 19,
        male: false
    }
], function (err, items) {
    // err - 错误的描述或null
    // items - 插入的数据项数组
});

修改数据项(Updating Items)

所获得的每个数据对象中都包含了定义数据模型时的属性(列),还有一些成员方法。你可以通过它们对数据项进行修改。

Person.get(1, function (err, John) {
    John.name = "Joe";
    John.surname = "Doe";
    John.save(function (err) {
        console.log("saved!");
    });
});

可以在一个.save()调用中同时修改和保存数据对象。

Person.get(1, function (err, John) {
    John.save({ name: "Joe", surname: "Doe" }, function (err) {
        console.log("saved!");
    });
});

如果要删除一项数据,只要这么做:

// 你也可以不获取而直接删除数据,参考上文链式调用部分
Person.get(1, function (err, John) {
    John.remove(function (err) {
        console.log("removed!");
    });
});

约束校验(Validations)

详见 wiki.

模型关联(Associations)

一个模型关联代表一个或多个表之间的关系。

多对一(hasOne)

这是一个多对一关系。这和 属于… 有些类似

例如: Animal.hasOne('owner', Person).

一个动物(Animal)只能有一个拥有者(owner),但一个人(Person)却可以拥有多个动物

数据模型Animal将会被自动加上owner_id这个属性。

有了这层关系后你就能使用以下这些方法:

animal.getOwner(function..)         // 获取它的拥有者
animal.setOwner(person, function..) // 设置它的拥有者,实际是改变owner_id
animal.hasOwner(function..)         // 检查它的拥有者是否存在
animal.removeOwner()                // 移除它的拥有者,实际是设置owner_id为0

链式调用Find

多对一关联后可以根据拥有者查找数据,该方法和find一样能够返回ChainFind对象在后面接链式调用。

Animal.findByOwner({ /* options */ })

反向访问(Reverse access)

在定义关联时,可以同时定义反向访问,如:

Animal.hasOne('owner', Person, {reverse: 'pets'})

关联后将同时会为Person模型添加以下方法:

// 数据实例方法
person.getPets(function..)
person.setPets(cat, function..)

// 数据模型类方法
Person.findByPets({ /* options */ }) // 返回对象支持链式调用

多对一(hasMany)

这是一个多对多关系(增加一个联接表)。

例如: Patient.hasMany('doctors', Doctor, { why: String }, { reverse: 'patients', key: true }).

每个病人(Patient)将会有多个不同的医生(Doctor),每个医生也会有多个不同的病人。

进行多对多关联并且调用Patient.sync()同步数据模型后会在数据库中建立一个名为patient_doctors的联接表:

列名数据类型
patient_idInteger (组合主键)
doctor_idInteger (组合主键)
whyvarchar(255)

关联后的Patient和Doctor模型可以使用以下的方法:

patient.getDoctors(function..)           // 获取该病人的医生列表
patient.addDoctors(docs, function...)    // 给该病人添加医生,实际是在联接表中添加条目
patient.setDoctors(docs, function...)    // 修改病人的医生列表,实际是从联接表中移除相关条目并重新添加
patient.hasDoctors(docs, function...)    // 根据联接表检查病人是否与指定的医生关联
patient.removeDoctors(docs, function...) // 从联接表中移除病人与指定医生的关联条目

doctor.getPatients(function..)patient类似,略... 

// 你也可以这样做:
patient.doctors = [doc1, doc2];
patient.save(...)

将一个医生关联到一个病人:

patient.addDoctor(surgeon, {why: "remove appendix"}, function(err) { ... } )

这将会在联接表中插入一项条目:
{patient_id: 4, doctor_id: 6, why: "remove appendix"}

get访问器(getAccessor)

在多对多关联中提供的get访问器可以不传递回调函数,那么它会返回一个支持链式调用的ChainFind对象。也就是说你可以这么做:

patient.getDoctors().order("name").offset(1).run(function (err, doctors), {
    // ... 获取病人的除了第一个之外的所有医生,并以name排序
});

扩展数据模型(extendsTo)

你可能会想要把一个数据模型的其中一些属性分别存放到不同的数据表中。那么你可以使用extendsTo来扩充属性,
每次扩充的属性都会在一个新建的子表中存放,子表中会有一列唯一的id与主表的主键id对应。例如:

var Person = db.define("person", {
    name : String
});
var PersonAddress = Person.extendsTo("address", {
    street : String,
    number : Number
});

以上的数据模型定义将会在数据库中建立一个主表person其中包含 idname两列,还建立了一个扩展子表person_address其中包含数据列person_idstreetnumber
扩展之后的数据模型拥有类似多对一关联之后可用的功能方法。
在这个例子中,你可以对数据模型使用.getAddress(cb), .setAddress(Address, cb), …等方法..

注意: 你不一定非得把Person.extendsTo的返回值保存下来。它返回的是一个扩展数据模型,你可以通过它直接访问扩展子表(甚至可以查找关联的数据模型),
这都是随便你的。如果你只想通过主数据模型来访问扩展属性的话,可以忽略它的返回值。

options示例

如果你想定义一个一对多关系,你可以使用hasOne(属于)来关联两个模型。

var Person = db.define('person', {
    name : String
});
var Animal = db.define('animal', {
    name : String
});
Animal.hasOne("owner", Person); // 将在'animal'表中增加一列'owner_id'字段

//查找id为123的元素
Animal.get(123, function (err, animal) {
    // 如果找到元素,变量animal就是Animal数据模型的一个实例
    animal.getOwner(function (err, person) {
        // 如果该animal元素有一个拥有者owner, 那么变量person就是这个拥有者实例
    });
});

你可以通过设置required选项将数据表中这个owner_id字段标记为必填字段:

Animal.hasOne("owner", Person, { required: true });

如果你需要对一个非必填甚至还没有给值字段进行有效性检验,那么可以设置alwaysValidate选项。
(这有可能发生,比如当一个空字段的验证取决于其他字段的时候)

Animal.hasOne("owner", Person, { required: false, alwaysValidate: true });

如果你偏好用其他名字作为字段名(owner_id)你可以在settings中配置。

db.settings.set("properties.association_key", "{field}_{name}"); // 该例子中{name} 会替换成 'owner', {field}会替换成'id'

注意: 这必须在定义关联之前进行设置才有效。

定义多对多关联 hasMany时可以在联接表中定义额外的属性。

var Person = db.define('person', {
    name : String
});
Person.hasMany("friends", {
    rate : Number
}, {}, { key: true });

Person.get(123, function (err, John) {
    John.getFriends(function (err, friends) {
        // 假设rate字段是person_friends表的其他列之一,你可以用friends[N].extra.rate来访问它
    });
});

假如你启用了autoFetch功能,那么当你查找获取到数据实例的时候会自动将相关联的模型数据一并获取。

var Person = db.define('person', {
  name : String
});
Person.hasMany("friends", {
    rate : Number
}, {
    key       : true, // 将联接表中的外键设为一个组合主键
    autoFetch : true
});

Person.get(123, function (err, John) {
    // 不需要再调用John.getFriends()方法 , 可以直接访问John.friends获得数组
});

你也可以在定义主模型时配置这个选项,而不是基于每个关联来配置,它将适用于主模型的所有的关联。

var Person = db.define('person', {
    name : String
}, {
    autoFetch : true
});
Person.hasMany("friends", {
    rate : Number
}, {
  key: true
});

在定义关联时配置reverse选项则会在被关联的数据模型中添加一些方法调用。例如,你从ModelA定义一个关联到ModelB后,ModelB中会创建一个访问器用来获取ModelA的实例。
懵逼了吗? 看看下一个例子:

var Pet = db.define('pet', {
    name : String
});
var Person = db.define('person', {
    name : String
});
Pet.hasOne("owner", Person, {
    reverse : "pets"
});

Person(4).getPets(function (err, pets) {
    // 尽管这个关联是从Pet模型上建立的,但是Person模型上也会有一个访问器getPets
    // 在这个例子中,ORM会获取所有owner_id为4的Pet元素
});

reverse选项在hasMany多对多关联中的意义更好理解,你可以在关联的数据模型两者相互访问。

var Pet = db.define('pet', {
    name : String
});
var Person = db.define('person', {
    name : String
});
Person.hasMany("pets", Pet, {
    bought  : Date
}, {
    key     : true,
    reverse : "owners"
});

Person(1).getPets(...);
Pet(2).getOwners(...);

添加外部的数据库适配器(database adapters)

要为orm添加一个外部的数据库适配的话,调用addAdapter方法,接收的参数为适配器别名和适配器的构造函数:

require('orm').addAdapter('cassandra', CassandraAdapter);

详见 创建Adapter

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值