封装CRUD的方法

MongoDB 封装 CRUD 方法

CRUD是指创建(Create)、读取(Read)、更新(Update)和删除(Delete)四种基本的数据处理操作。

在软件开发中,CRUD方法通常用于对数据库或其他存储系统中的数据进行操作。

具体来说,CRUD方法包括以下四种操作:

  • 创建(Create) :向数据库或其他存储系统中插入新数据。
  • 读取(Read) :从数据库或其他存储系统中获取数据。
  • 更新(Update) :更新数据库或其他存储系统中已有的数据。
  • 删除(Delete) :从数据库或其他存储系统中删除数据。

这些操作可以通过编写相应的程序实现,以便在应用程序中对数据进行处理和管理。

例如,在一个商店的库存管理系统中:

  • 使用CRUD方法可以新增商品(Create)、
  • 查询某个商品的库存数量(Read)、
  • 修改商品的价格和库存数量(Update)
  • 从库存中删除商品(Delete)。

更多精彩内容,请微信搜索“前端爱好者戳我 查看

实现步骤

首先,需要安装相应的依赖项,包括 koakoa-routerkoa-bodyparsermongoose

可以通过在终端中运行以下命令进行安装:

npm install koa koa-router koa-bodyparser mongoose

然后,在项目中创建一个 db.js 文件,封装 MongoDB 的连接和操作方法:

const mongoose = require('mongoose');

// 连接数据库
mongoose.connect('mongodb://localhost/mydb', { useNewUrlParser:true })
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error(err));

// 定义数据模型
const userSchema = new mongoose.Schema({
  name: String,
  age: Number,
  email: String
});
const User = mongoose.model('User', userSchema);
 
// 封装 CRUD 操作 
module.exports = {
  // 获取所有用户数据
  async getUsers() {
    return await User.find();
  },
  // 新增用户数据
  async addUser(data) {
    const user = new User(data);
    return await user.save();
  },
  // 根据 ID 获取用户数据
  async getUserById(id) {
    return await User.findById(id);
  },
  // 根据 ID 更新用户数据
  async updateUserById(id, data) {
    return await User.findByIdAndUpdate(id, data);
  },
  // 根据 ID 删除用户数据
  async deleteUserById(id) {
    return await User.findByIdAndDelete(id);
  }
};

在上面的代码中,我们定义了 User 数据模型,并在 getUsers()、addUser()、getUserById()、updateUserById() 和 deleteUserById() 方法中封装了 MongoDB 的 CRUD 操作。

接下来,需要在项目的入口文件中创建一个 Koa 应用,并在路由中使用上述封装的方法:

const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const db = require('./db');

const app = new Koa();
const router = new Router();

// 解析请求体
app.use(bodyParser());

// 获取所有用户数据
router.get('/users', async (ctx) => {
  const users = await db.getUsers();
  ctx.body = users;
});

// 新增用户数据
router.post('/users', async (ctx) => {
  const { name, age, email } = ctx.request.body;
  const user = await db.addUser({ name, age, email });
  ctx.body = user;
});

// 根据 ID 获取用户数据
router.get('/users/:id', async (ctx) => {
  const { id } = ctx.params;
  const user = await db.getUserById(id);
  ctx.body = user;
});

// 根据 ID 更新用户数据
router.put('/users/:id', async (ctx) => {
  const { id } = ctx.params;
  const { name, age, email } = ctx.request.body;
  const user = await db.updateUserById(id, { name, age, email });
  ctx.body = user;
});

// 根据 ID 删除用户数据
router.delete('/users/:id', async (ctx) => {
  const { id } = ctx.params;
  await db.deleteUserById(id);
  ctx.status = 204;
});

app.use(router.routes());

app.listen(3000);

在上面的代码中,我们创建了一个 Koa 应用,并在路由中使用封装的 CRUD 方法处理对应的 HTTP 请求。其中,通过 koa-bodyparser 中间件解析请求体,使用 :id 对 ID 进行占位符处理。

最后,通过在终端中运行以下命令启动应用:

node app.js

即可通过访问 http://localhost:3000/users 等 API 进行 MongoDB 的 CRUD 操作。

案例:用户模块 CRUD 封装 改造

controller文件夹下,新建utils文件夹,并且新建index.js文件

// controller/utils/index.js
/**
 * 用于添加数据的公共方法
 * @param {*} model 
 * @param {*} params 
 * @param {*} ctx 
 * @returns 
 */
const add = (model, params, ctx) => (
    model.create(params).then(rel => {
        if (rel) {
            ctx.body = {
                code: 200,
                msg: '添加成功',
                data: rel
            }
        } else {
            ctx.body = {
                code: 300,
                msg: '添加失败'
            }
        }

    }).catch(err => {
        ctx.body = {
            code: 400,
            msg: '添加时出现异常'
        }
        console.error(err)
    })
)

/**
 * 用于修改数据的公共方法
 * @param {*} model 
 * @param {*} where 
 * @param {*} params 
 * @param {*} ctx 
 * @returns 
 */
const update = (model, where, params, ctx) => (
    model.updateOne(where, params).then(rel => {
        ctx.body = {
            reslut: rel
        }
    }).catch(err => {
        ctx.body = {
            code: 400,
            msg: '修改时出现异常'
        }
        console.error(err)
    })
)

/**
 * 用于删除的公共方法
 * @param {*} model 
 * @param {*} where 
 * @param {*} ctx 
 * @returns 
 */
const del = (model, where, ctx) => (
    model.findOneAndDelete(where).then(rel => {
        ctx.body = {
            reslut: rel
        }
    }).catch(err => {
        ctx.body = {
            code: 400,
            msg: '删除时出现异常'
        }
        console.error(err)
    })
)

/**
 * 用于查询所有数据的公共方法
 * @param {*} model 
 * @param {*} where 
 * @param {*} ctx 
 * @returns 
 */
const find = (model, where, ctx) => (
    model.find(where).then(rel => {
        ctx.body = {
            result: rel
        }
    }).catch(err => {
        ctx.body = {
            code: 400,
            msg: '查询时出现异常'
        }
        console.error(err)
    })
)

/**
 * 查询单条数据的公共方法
 * @param {*} model 
 * @param {*} where 
 * @param {*} ctx 
 * @returns 
 */
const findOne = (model, where, ctx) => (
    model.findOne(where).then(rel => {
        ctx.body = {
            result: rel
        }
    }).catch(err => {
        ctx.body = {
            code: 400,
            msg: '查询时出现异常'
        }
        console.error(err)
    })
)

module.exports = {
    find,
    findOne,
    add,
    update,
    del
}

controller文件夹下,修改user文件夹

// controller/user.js
const {User} = require('../models')
const crud = require('./crudUtil')

//添加系统用户
const userAdd = async (ctx) => {
  let {username = '',pwd = ''} = ctx.request.body
  await crud.add(User,{username,pwd},ctx)
}

//修改用户
const userUpdate = async (ctx) => {
    let params = ctx.request.body
    await crud.update(
            User,
            {_id:params._id},
            {username:params.username,pwd:params.pwd},
            ctx
        )
}

//删除用户
const userDel = async (ctx) => {
    let {_id} = ctx.request.body
    await crud.del(User,{_id},ctx)
}

//查询所有用户
const userFind = async (ctx) => {
    await crud.find(User,null,ctx)
}

//查询单个用户
const userFindOne = async (ctx) => {
    await crud.findOne(User,{_id:ctx.params.id},ctx)
}

module.exports = {
    userAdd,
    userUpdate,
    userDel,
    userFind,
    userFindOne
}

即可完成用户模块 CRUD 封装 改造。

koa2 跨域问题

跨域(Cross-Origin)指的是在浏览器中运行的脚本试图访问不同源(Protocol、域名、端口)的资源,即在不同源之间进行数据交互。

当 JavaScript 代码尝试向不同源的服务器发起请求时,浏览器会发出“跨域请求”(Cross-Origin Request)并受到浏览器的同源策略限制。

同源策略是浏览器为了保证用户的信息安全而实施的一种安全策略,它要求浏览器限制从脚本发出的跨域 HTTP 请求,只有在同源的情况下才允许相互通信。

同源指协议、域名和端口都相同。如果两个 URL 的协议、域名或端口其中之一不同,就被认为是不同源。

跨域问题在 Web 开发中比较常见,在开发基于 AJAX 和 RESTful API 的应用时尤其需要注意。

解决跨域的方法有很多种,包括 JSONP、CORS、反向代理、WebSocket 等。

具体选择哪种方法取决于实际应用场景和需求。

koa 处理 跨域问题

设置 HTTP 响应头

在 Koa2 中解决 CORS 跨域问题的常用方法是添加一个中间件来设置 HTTP 响应头。

具体实现如下:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  // 允许跨域的域名,* 代表所有域名都可以跨域访问
  ctx.set('Access-Control-Allow-Origin', '*');
  // 允许的请求方法
  ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  // 允许的请求头字段
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  if (ctx.method === 'OPTIONS') {
    // 处理预检请求,直接返回 204 状态码,表示允许跨域访问
    ctx.status = 204;
  } else {
    // 继续处理正常请求
    await next();
  }
});

// 在这里注册路由处理程序
app.use(router.routes());

app.listen(3000);

在上面的代码中,我们创建了一个中间件来设置 HTTP 响应头,包括 Access-Control-Allow-Origin(允许跨域的域名)、Access-Control-Allow-Methods(允许的请求方法)和 Access-Control-Allow-Headers(允许的请求头字段)。

同时,我们也处理了预检请求 OPTIONS,如果当前请求方法是 OPTIONS,则直接返回 204 状态码表示允许跨域访问;否则,继续处理正常请求。

请注意,为了方便起见,在上面的示例中 将 Access-Control-Allow-Origin 设置为了 *,表示允许所有域名进行跨域访问。在实际应用中,我们应该根据需求设置具体的允许跨域域名

koa2-cors 库

在 Koa2 中解决跨域问题可以使用 koa2-cors 库。

下面是一个简单的示例:

首先,安装 koa2-cors

npm install koa2-cors --save

然后,在应用程序中使用这个中间件:

const Koa = require('koa');
const cors = require('koa2-cors');

const app = new Koa();

// 配置跨域选项
app.use(cors({
  origin: '*', // 允许哪些源访问,默认为 *
  maxAge: 86400, // 预检请求缓存时间(秒),默认为 5 秒
  credentials: true, // 是否携带身份验证信息,默认为 false
  allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH'], // 允许哪些 HTTP 方法,默认为 GET,HEAD,PUT,POST,DELETE,PATCH
  allowHeaders: ['Content-Type', 'Authorization', 'Accept'], // 允许哪些 HTTP 头部,默认为 Content-Type,Authorization,Accept
  exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'] // 允许哪些自定义的 HTTP 头部暴露给浏览器,默认为空数组
}));

// 添加路由处理器
app.use(async (ctx, next) => {
  const name = ctx.query.name || 'World';
  ctx.body = { message: `Hello, ${name}!` };
});

app.listen(3000, () => {
  console.log('Server is running at http://localhost:3000');
});

在上面的代码中,我们导入了 koa2-cors 中间件,并将其作为第一个中间件使用。在配置选项中,我们指定了允许哪些源访问,以及是否携带身份验证信息等。这里的配置选项可以根据需要进行修改。

注意,koa2-cors 中间件必须放在其他中间件之前,否则会导致跨域请求失败。在本例中,我们添加了一个简单的路由处理器,在响应中返回一条消息,以便测试跨域请求是否起作用。

使用 koa2-cors 中间件可以方便地解决 Koa2 应用程序中的跨域问题。

每日一课: ES6 新增属性 第三部分

ES6 (ECMAScript 2015) 引入了许多新的语法和功能特性,其中一些新增的属性包括:

对象和数组的新方法:

ES6 中为对象和数组提供了很多新的方法,它们可以使开发者更加高效地操作数据。

下面是这些新方法的介绍和应用示例:

对象的新方法

Object.assign

Object.assign 方法用于将多个源对象的属性复制到目标对象中。它返回目标对象本身。

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);

console.log(target); // { a: 1, b: 2, c: 3 }

上面的例子中,我们声明了一个目标对象 target 和两个源对象 source1 和 source2,使用 Object.assign 方法将两个源对象的属性复制到目标对象中。

需要注意的是,Object.assign 是浅拷贝,只复制对象的属性值。如果属性值是一个对象,则只是复制了该对象的引用,而不是进行深拷贝

Object.keys、Object.values 和 Object.entries

  • Object.keys 方法返回一个数组,包含对象所有可枚举的属性名称。
  • Object.values 方法返回一个数组,包含对象所有可枚举的属性值。
  • Object.entries 方法返回一个数组,包含对象所有可枚举的属性键值对。
const obj = { a: 1, b: 2, c: 3 };

console.log(Object.keys(obj)); // [ 'a', 'b', 'c' ]
console.log(Object.values(obj)); // [ 1, 2, 3 ]
console.log(Object.entries(obj)); // [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]

上面的例子中,我们使用了 Object.keys、Object.values 和 Object.entries 方法分别获取了对象 obj 中所有属性名称、属性值和属性键值对。

需要注意的是,这些方法只返回可枚举的属性。

Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors 方法返回一个对象,描述了一个对象所有自身属性的属性描述符。

const obj = { a: 1 };

console.log(Object.getOwnPropertyDescriptors(obj));
/*
{
  a: {
    value: 1,
    writable: true,
    enumerable: true,
    configurable: true
  }
}
*/

上面的例子中,我们使用了 Object.getOwnPropertyDescriptors 方法获取了对象 obj 自身属性的所有属性描述符。

需要注意的是,这个方法返回的对象包含了所有属性的描述符,而不仅限于可枚举的属性。

数组的新方法

Array.from

Array.from 方法用于将类数组对象和可迭代对象转换为真正的数组。

const arr1 = Array.from('hello');
const arr2 = Array.from([1, 2, 3], x => x * 2);

console.log(arr1); // [ 'h', 'e', 'l', 'l', 'o' ]
console.log(arr2); // [ 2, 4, 6 ]

上面的例子中,我们将一个字符串和一个可迭代对象转换为了真正的数组。

在第二个示例中,我们还演示了如何使用箭头函数对数组元素进行转换。

Array.of

Array.of 方法用于创建一个新的、具有可变数量的参数的数组实例。

const arr1 = Array.of(1, 2, 3);
const arr2 = Array.of(4);

console.log(arr1); // [ 1, 2, 3 ]
console.log(arr2); // [ 4 ]

上面的例子中,我们使用 Array.of 方法创建了两个数组实例。

Array.prototype.includes

Array.prototype.includes 方法用于判断一个数组是否包含某个指定的值。

const arr = [1, 2, 3];

console.log(arr.includes(2)); // true
console.log(arr.includes(4)); // false

上面的例子中,我们使用 Array.prototype.includes 方法判断数组 arr 是否包含值 2 和值 4。

需要注意的是,这个方法不会区分 +0 和 -0,NaN 和 NaN 等特殊情况。

Array.prototype.find 和 Array.prototype.findIndex

  • Array.prototype.find 方法用于返回数组中满足条件的第一个元素,如果没有找到,则返回 undefined。
  • Array.prototype.findIndex 方法用于返回数组中满足条件的第一个元素的索引,如果没有找到,则返回 -1。
const arr = [1, 2, 3];

const result1 = arr.find(x => x > 1);
const result2 = arr.findIndex(x => x > 1);

console.log(result1); // 2
console.log(result2); // 1

在上面的例子中,我们使用 Array.prototype.find 和 Array.prototype.findIndex 方法找到了数组 arr 中第一个大于 1 的元素和它的索引。

需要注意的是,这两个方法都接受一个回调函数作为参数,用于定义搜索条件。

Proxy 和 Reflect 对象

ES6 中提供了 Proxy 和 Reflect 两个内置对象,它们可以帮助开发者更灵活地控制对象的行为实现元编程

Proxy 对象

Proxy 对象允许你创建一个代理对象,用来拦截对象的各种操作,比如访问一个属性、调用一个函数等。通过代理对象,我们可以完全控制原始对象的行为,实现自己的逻辑。

下面是一个简单的示例,使用 Proxy 对象对一个对象进行拦截:

const obj = {
  name: 'Alice',
  age: 18,
};

const handler = {
  get(target, key) {
    console.log(`Getting ${key}`);
    return target[key];
  },

  set(target, key, value) {
    console.log(`Setting ${key} to ${value}`);
    target[key] = value;
  },
};

const proxy = new Proxy(obj, handler);

console.log(proxy.name); // Getting name, Alice
proxy.age = 20; // Setting age to 20

在上面的示例中,我们定义了一个对象 obj,然后使用 Proxy 对象创建了一个代理对象 proxy。

我们还定义了一个处理器对象 handler,其中实现了 get 和 set 两个拦截器函数。

当我们通过代理对象 proxy 访问某个属性时,会触发 get 拦截器,并输出相应的日志;当我们通过代理对象 proxy 修改某个属性时,会触发 set 拦截器,并输出相应的日志。

需要注意的是,Proxy 对象不支持直接遍历,如果要实现遍历,需要把代理对象转换成一个数组或一个 Set 对象。

Reflect 对象

Reflect 对象提供了一组静态方法,用于操作对象。

这些方法与原来的全局方法、函数或操作符等功能类似,但具有更统一的函数签名和返回值,更易于使用和学习。

下面是一些常用的 Reflect 方法:

Reflect.get(target, propertyKey[, receiver])

读取目标对象的指定属性的值。如果指定属性不存在,则返回 undefined。

const obj = { name: 'Alice' };
const value = Reflect.get(obj, 'name');
console.log(value); // Alice

Reflect.set(target, propertyKey, value[, receiver])

设置目标对象的指定属性的值。如果指定属性不存在,则创建它。

const obj = { name: 'Alice' };
Reflect.set(obj, 'age', 18);
console.log(obj); // { name: 'Alice', age: 18 }

Reflect.has(target, propertyKey)

判断目标对象是否存在指定属性。

const obj = { name: 'Alice' };
const result1 = Reflect.has(obj, 'name');
const result2 = Reflect.has(obj, 'age');
console.log(result1); // true
console.log(result2); // false

Reflect.deleteProperty(target, propertyKey)

从目标对象中删除指定属性。

const obj = { name: 'Alice', age: 18 };
Reflect.deleteProperty(obj, 'age');
console.log(obj); // { name: 'Alice' }

Reflect.construct(target, argumentsList[, newTarget])

用指定的参数列表创建一个对象。相当于使用 new 关键字调用函数。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const args = ['Alice', 18];
const obj = Reflect.construct(Person, args);
console.log(obj); // { name: 'Alice', age: 18 }

需要注意的是,Reflect 对象并不支持通过 call 或 apply 方法调用。

Symbol 类型

ES6 引入了一种新的基本数据类型 Symbol,它表示一个独一无二的值,可以用作对象属性的键名,避免键名冲突。

Symbol 类型,用于创建唯一的标识符,并支持在对象中定义非字符串的属性键名。

一个 Symbol 值是通过 Symbol() 函数创建的,每次调用该函数生成的值都是唯一的,因为它在内部生成了一个独一无二的标识符。

下面是一个简单的示例,演示了如何创建和使用 Symbol 类型:

const sym1 = Symbol();
const sym2 = Symbol();

console.log(sym1); // Symbol()
console.log(sym2); // Symbol()

console.log(sym1 === sym2); // false

const obj = {
  [sym1]: 'value1',
  [sym2]: 'value2',
};

console.log(obj[sym1]); // value1
console.log(obj[sym2]); // value2

在上面的示例中,我们首先创建了两个不同的 Symbol 值,然后通过方括号语法将它们分别作为对象 obj 的属性名,并设置相应的属性值。

需要注意的是,由于 Symbol 值是独一无二的,因此即使两个 Symbol 值看起来相同,但它们也是不同的,不能互相替代。

除了传递空参外,Symbol() 函数还可以传递一个字符串参数,用于描述 Symbol 值,但这并不会影响 Symbol 值的唯一性。

可以使用 Symbol.for() 方法创建一个可共享的 Symbol 值,这样多个执行环境也可以共享同一个 Symbol 值。

// 在当前执行环境中创建一个 Symbol 值
const sym1 = Symbol('mySymbol');

// 在全局注册表中创建一个 Symbol 值
const sym2 = Symbol.for('mySymbol');

console.log(sym1); // Symbol(mySymbol)
console.log(sym2); // Symbol(mySymbol)

console.log(sym1 === sym2); // false
console.log(Symbol.keyFor(sym1)); // undefined
console.log(Symbol.keyFor(sym2)); // mySymbol

在上面的示例中,我们首先使用 Symbol() 函数创建了一个 Symbol 值 sym1,并传入了一个描述字符串 “mySymbol”。

然后,我们使用 Symbol.for() 方法创建了一个 Symbol 值 sym2,也传入了相同的描述字符串 “mySymbol”。

尽管两者描述字符串相同,但它们是两个不同的 Symbol 值,因此 sym1 和 sym2 不相等。

同时,我们还通过 Symbol.keyFor() 方法查看了 sym1 和 sym2 对应的键名,发现只有 sym2 有一个名为 “mySymbol” 的键名,因此 sym1 对应的键名返回了 undefined。

需要注意的是,Symbol.for() 方法创建的 Symbol 值会保存在全局注册表中,在不同的执行环境中可以被共享。

因此,如果多个执行环境中都使用了相同的 Symbol.for() 参数字符串,它们将共享同一个 Symbol 值。

如果想要在注册表中查找某个 Symbol 值对应的键名,在 ES6 中可以使用 Symbol.keyFor() 方法。

此外,ES6 还提供了一些内置的 Symbol 值,比如 Symbol.iterator、Symbol.hasInstance、Symbol.species 等,这些值被用于实现各种特定的功能。

Generator 函数和迭代器

ES6 引入的 Generator 函数是一种用于创建可暂停执行的函数,并且支持返回多个值的语法。

与普通函数不同,调用 Generator 函数并不会立即执行该函数体代码,而是返回一个迭代器对象,通过迭代器的 next() 方法来控制函数的执行。

Generator 函数定义的语法如下:

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();

console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
console.log(g.next()); // { value: undefined, done: true }

在上面的示例中,我们定义了一个 gen Generator 函数,并在内部通过 yield 关键字返回了三个值。

调用 gen() 函数返回的是一个迭代器对象 g,通过 g.next() 方法来逐个遍历 yield 返回的值。

需要注意的是,当函数所有的 yield 均已执行完后,再次调用 g.next() 的返回值为 { value: undefined, done: true },指示迭代器已经结束。

Generator 函数可以通过 return() 方法手动终止迭代器,并设置返回值。同时,Generator 函数还可以接受外部参数,并将其传递到内部使用,以控制函数的执行。

除了 手动调用 next() 方法 外,可以使用 for…of 循环来遍历 Generator 函数返回的迭代器对象中的值。需要注意的是,在循环结束后,return() 方法设置的返回值并不会被打印出来,因此要想获取该值,应该在循环内手动调用 return() 方法,并设置返回值。

总之,ES6 的 Generator 函数和迭代器为 JavaScript 增加了一种强大而灵活的编程方式,可以更加方便和高效地管理复杂的异步逻辑。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端布道人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值