Node.js+MongoDB的环境搭建+学习实例

春节前闲来无事,整理了一下以前学习的Node.js和NoSql数据库的资料,总结了自己一路踩过来的坑,希望能对初学者有帮助。

在开发环境搭建和实例编写之前,还是需要先了解一下基本概念。

参考文章:

1. 目前流行的几个NoSql数据库对比

2. 深入了解MongoDB的mmap(内存映射文件)的原理

3. MongoDB数据库命令

4. Node.js的概念

5. 事件循环,Node.js的核心概念

6. JavaScript:彻底理解同步,异步和事件循环

7. nodejs async库使用


长篇大论的概念我就不复述了,用几句简单的话来概括一下重点:

1. NoSql数据库

NoSql=not only sql,与传统关系型数据库的最大区别在于,不能JOIN,且保存的数据没有固定的格式,常见的如MongoDB以key-value形式进行存储。

与传统数据库如MySql是硬盘数据库+关系型数据库相反,NoSql是内存数据库+非关系型数据库。

硬盘数据库的数据可靠性高,因为数据是直接写入硬盘。内存数据库读写速度更快,因为是直接通过内存进行存取,但是需要定期写回到硬盘,这就导致在存入内存到写入硬盘的时间内,如果系统崩溃,会导致数据丢失,可靠性会降低,虽然有通过日志在启动后进行恢复的方法,但是多少还是会影响体验。

所以到底是用关系型数据库还是NoSql数据库,还是要根据场景进行分析,不能一概而论。


2. MongoDB

内存映射文件是OS通过mmap在内存中创建一个文件,并直接映射到虚拟内存。在数据操作的时候,OS会把需要操作的数据直接映射到物理内存中。

MongoDB通过journal进行故障恢复和持久化,系统在内存中分配一块区域给journal使用,称为private view。每隔100ms会刷新privateview到journal。因此发生故障时,丢失的也只是100ms的数据而已。但是开启journal后,虚拟内存的使用也会倍增。


3. Node.js

Node是服务器程序。本身运行的是Google的V8引擎,用于解释Javascript。它可以直接用Javascript写后台程序,使前台开发者也能快速开发后台代码,即HTML+Javascript+MongoDB,而无需像Tomcat的前台(HTML+Javascript)+后台(Java)+数据库(MySql)。

与Java等不同的是它连接到服务器的方式:

Java是多线程的,一个连接就是一个新线程,每个线程需要2M的配套内存的话,8G内存就只能维持4000个用户,想要达到更高的连接数只能增加增加服务器。

Node.js是单线程的,它支持数万的并发,在处理非计算型高密集I/O请求时,可以有更好的表现。

为什么是非计算型?

因为Node.js虽然称为单线程,但并不意味着只有一个线程在进行处理,单的是主线程,还有其他如Ajax线程,MongoDB线程进行其他的处理。

如当主线程调用Ajax线程时,主线程并不是等待Ajax线程返回结果,而是直接跳到下一步操作,Ajax线程结束后会去执行它的回调函数,这是同步和异步的差异。

此时,主线程可以去做其他的请求。

借一张图来说明处理的流程:


但是如果主线程的计算量很大,由于是单线程的关系,其他请求就无法进入主线程了。

为什么用内存数据库(MongoDB)?

用Node.js就是追求性能和并发,而传统的关系型数据库(如MySql)在读写性能方面不如内存数据库,如果是Node.js+关系型数据库的话,数据库这边势必将称为I/O的瓶颈。


--------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------

接下来是环境搭建和实例

一. MongoDB环境搭建:

1. MongoDB下载地址:http://dl.mongodb.org/dl/win32/x86_64

2. 解压在任意路径,  D:\mongodb

3. 在mongodb目录下创建data文件夹,在data文件夹下创建db和log文件夹,在log文件夹下创建MongoDB.log文件。

目录结构如下:


4. 在cmd下进行安装:

d:\mongodb\bin>mongod -dbpath "d:\mongodb\data\db"


5.安装完成之后打开http://127.0.0.1:27017/ 
看到
It looks like you are trying to access MongoDB over HTTP on the native driver port. 
说明已经安装成功了。

6.DB操作:

双击bin下的mongod.exe(启动mongodb)

然后运行mongo.exe,界面如下:


7. MongoDB中的collection相当于table,与关系型数据库的db和table不同的是,mongodb的db和collection如果不存在的话,会自动创建,而无需create table。

简单介绍几个命令:

7.1  指定当前DB:use {dbName};

如:use sun;(指定sun为当前db)

7.2 查询collection下的所有数据:db.{collection}.find();

如:db.test.find();(显示test这个collection下所有的数据)

7.3 删除当前数据库:db.dropDatabase();(注意是当前库哦!)

7.4 删除指定collection:db.{collection}.drop();

7.5 显示所有db:show dbs;

7.6 显示当前db下的所有collection:show collections;

7.7 插入数据:db.{collection}.insert({'{key1}':'{value1}', '{key2}':'{value2}'});

如:db.test.insert({'name':'aaa', 'age':10});

7.8 更新数据:db.{collection}.update({'{type1}':'{value1}'}, {$set:{'{type2}':'{value2}'}});

如:db.test.update({'name':'aaa'}, {$set, {'age':20}});

7.9 删除数据:db.{collection}.remove({'{key1}':'{value1}', '{key2}':'{value2}'});

如:db.test.remove({'name':'aaa'});

7.10 查询指定条件的数据:db.{collection}.find({'{key1}':'{value1}', '{key2}':'{value2}'});

如:db.test.find({'name':'aaa'});

其他命令可查询Mongo教程

例:



二. 开发环境WebStorm搭建:

去官网下载最新版的WebStorm并安装


三. Node.js环境搭建:

去官网下载最新的Node.js并安装


四. 创建Node.js+Express项目并启动服务器:

1. 启动WebStorm,File->New->Project,选择Node.js Express App,Location是项目路径,Template是页面模板和解析引擎。



2. 创建后的项目结构如下(下图有我增加的文件和文件夹,但是整体结构改动不大),可直接在左下角Run窗口按绿三角启动,显示如下图的文字则表示服务启动成功。



3. 输入http://localhost:3000/,显示如下(显示内容我也改成自己的了):



4. 至此,简单的Express项目创建成功,但是有些文件的内容还是需要说明一下。

bin\www:

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app'); // 服务启动后,需要用到的Module在app下面定义
var debug = require('debug')('nodejsproject:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');  // 端口3000就是在这里定义的
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

app.js:(里面有些代码是后面例子中用到的,先放着)

var express = require('express');  // require类似于Java的import,需要用到其他包里面的东西,就把它引用过来
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

///=======路由信息 (接口地址)开始 存放在./routes目录下===========//
var index = require('./routes/index');  //home page接口
var users = require('./routes/users');  //用户接口
var sun = require('./routes/sun');  //sun接口

var mongoDelete = require('./routes/mongodb/delete');  //mongo接口
var mongoFind = require('./routes/mongodb/find');       //mongo接口
var mongoInsert = require('./routes/mongodb/insert');  //mongo接口
var mongoUpdate = require('./routes/mongodb/update');  //mongo接口

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);  //在app中注册routes该接口,当输入根目录(http://localhost:3000)的时候,调用index.js
app.use('/users', users); //在app中注册users接口
app.use('/sun', sun); //在app中注册sun接口

app.use('/mongo', mongoDelete); //在app中注册mongo接口
app.use('/mongo', mongoFind);   //在app中注册mongo接口
app.use('/mongo', mongoInsert); //在app中注册mongo接口
app.use('/mongo', mongoUpdate); //在app中注册mongo接口

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

package.json:这个文件定义了引用的模块的信息,比如如果引用到了mongodb则这里必须添加,否则会报错。

{
  "name": "nodejsproject",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "assert": "^1.4.1",
    "async": "^2.6.0",
    "body-parser": "~1.18.2",
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.9",
    "ejs": "~2.5.7",
    "express": "~4.15.5",
    "mongodb": "~3.0.2",
    "morgan": "~1.9.0",
    "serve-favicon": "~2.4.5"
  }
}

既然说到了引用Module,那就顺便把模块的添加也说一下,比如添加mongodb模块,命令:

npm install --save -dev mongodb

“--save -dev”的用处是把引用模块的信息在安装成功后自动写入package.json,如果你想自己写,那么只要用

npm install mongodb

即可。


页面index.ejs:
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
  </body>
</html>

index.js:

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Sun test Express' });  // 打开index.ejs页面,并将指定内容赋予变量title
});

module.exports = router;


五. 创建一个Node.js的接口:

1. 创建user.js,定义user对象:

function User() {
    this.name;
    this.city;
    this.age;
}
module.exports = User;

2. users.js:

var URL = require('url');
var User = require('./user'); // 用到其他文件的时候,必须require引用过来
var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});

//http://localhost:3000/users/getUserInfo?id=1
router.get('/getUserInfo', function(req, res, next){
  var user = new User();

  var params = URL.parse(req.url, true).query;

  if (params.id = '1'){
      user.name = "test";
      user.age = "10";
      user.city = "上海";
  }else{
      user.name = "sun";
      user.age = "20";
      user.city = "东京";
  }

  var response = {status:1, data:user};
  res.send(JSON.stringify(response));

})

module.exports = router;

3. 在app.js中,不要忘了声明用到的模块:

var users = require('./routes/users');  //用户接口

app.use('/users', users); //在app中注册users接口

4. 在index.ejs下,添加:

后台接口调用:<a href="http://localhost:3000/users/getUserInfo?id=1">getUserInfo</a>

5. 在画面中点击getUserInfo链接,可以看到返回结果:





六. 使用Node.js+MongoDB,实现增删改查:

这个例子是基于上面的接口实例,通过前台页面使用jquery异步调用node.js接口的方式实现的。

1. 项目结构说明:

先说明一下MongoDB的结构,

DB名:sun

Collection有两个,test和other,test中保存name和age,other中保存name和job。

把他们分开是为了说明async.map的时候更方便。
这里会详细说明一下查询find的实现,增删改相对容易。


在创建Collection的时候,可以指定唯一索引。默认是只有id唯一。

db.collection.createIndex( <key and index type specification>, { unique: true } )

例:(将test中的name设为唯一索引)

db.test.createIndex({name:1},{unique:true})


下面是两个Collection里面准备的数据,在新增做好以后就可以自己插入了。


项目结构如下:


新增:

routes/mongodb/find.js <- 查询

routes/mongodb/insert.js  <- 新增

routes/mongodb/update.js <-更新

routes/mongodb/delete.js  <-删除

routes/mongodb/collectionTest.js <-Collection对象

routest/sun.js  <-sun.ejs对应的js文件(类似于index.js)

views/sun.ejs  <-页面(类似于index.ejs)


2. collectionTest.js:

function CollectionTest() {
    this.id;
    this.name;
    this.age;
    this.job;
}
module.exports = CollectionTest;


3. find.js:

var mongodb = require('mongodb')
var MongoClient = require('mongodb').MongoClient;
var DB_CONN_STR = 'mongodb://localhost:27017/sun';

var URL = require('url');
var CollectionTest = require('./collectionTest');
var express = require('express');
var router = express.Router();
var async = require('async');

var selectData = function(client, params, callback) {

    //连接到表
    var db = client.db('sun');
    var collectionTest = db.collection('test');
    var collectionOther = db.collection('other');

    //查询数据
    var whereStr;
    if (params.name) whereStr = {"name" : params.name};
    collectionTest.find(whereStr, function(error, results){
        if (error) throw error;

        results.toArray(function(err, arr){
            if (error) throw error;

            // async.map会将循环处理完以后,统一执行callback而不像其他异步调用执行分别执行
            async.map(arr, function(item, callback){
                var whereStr = {"name": item.name};
                collectionOther.find(whereStr, function(error, result){
                    result.toArray(function(err, arrJob){
                        console.log(arrJob);
                        console.log("------------------------");
                        if (arrJob){
                            item.job = arrJob[0].job;
                            console.log(item);
                            console.log("*************************");
                        }
                        callback(null, item);
                    });
                })
            }, function(err, result){
                // result是上表面item组成的数组
                callback(result);
            })
        });
    });
}

router.get('/find', function(req, res, next){

    var params = URL.parse(req.url, true).query;

    MongoClient.connect(DB_CONN_STR, function(err, client) {
        console.log("连接成功!");
        selectData(client, params, function(result) {
            res.send(JSON.stringify(result));

            client.close();
            console.log("连接断开!");
        });
    });
})

module.exports = router;


增删改和页面的jquery之类的没啥好说的,跟前台开发差不多,详细说一下在find.js中遇到的问题。

初用nodes.js的时候可能对异步调用的处理顺序有点不太习惯。

比如我在接口中,一开始是这样写的:

router.get('/find', function(req, res, next){

    var params = URL.parse(req.url, true).query;

    MongoClient.connect(DB_CONN_STR, function(err, client) {
        console.log("连接成功!");
        selectData(client, params, function(result) {
        });
    });
    
    res.send(JSON.stringify(result));

    client.close();
    console.log("连接断开!");
})

想象中的输出结果:

连接成功!

连接断开!


本以为它会在MongoClient.connect中的处理权都执行完以后,再断开连接,可实际显示的是:

连接断开!

连接成功!

由此想到,原来node.js在调用其他线程时,不是等待他们执行完,而是做自己的下一步,等到其他线程的处理完成后,再去调用回调函数。

简单的处理当然没问题,但是遇到复杂的循环调用其他线程时,就很麻烦了。

比方说在find.js中,我需要将test中的name和age取到以后,再循环所有数据,以name为key,去检索other并取出job后一起返回,不可能是每次检索都去调用callback回调函数。


因此我用到了async中的map函数。

安装async的命令:

npm install --save -dev async;

async库的命令的解析可以参照这篇文章:http://blog.csdn.net/dai_jing/article/details/47058579

我这里用到了map方法,map内循环的结果以数组形式保存后,一并返回给callback。


async.map的使用:

            // async.map会将循环处理完以后,统一执行callback而不像其他异步调用执行分别执行
            async.map(arr, function(item, callback){
                var whereStr = {"name": item.name};
                collectionOther.find(whereStr, function(error, result){
                    result.toArray(function(err, arrJob){
                        console.log(arrJob);
                        console.log("------------------------");
                        if (arrJob){
                            item.job = arrJob[0].job;
                            console.log(item);
                            console.log("*************************");
                        }
                        callback(null, item);
                    });
                })
            }, function(err, result){
                // result是上表面item组成的数组
                callback(result);
            })

4. insert.js:

var mongodb = require('mongodb')
var MongoClient = require('mongodb').MongoClient;
var DB_CONN_STR = 'mongodb://localhost:27017/sun';

var URL = require('url');
var express = require('express');
var router = express.Router();

function insertData(client, params, callback)
{
    var db = client.db("sun");
    var connectionTest = db.collection("test");
    var testData = {"name":params.name,"age":params.age};
    connectionTest.insert(testData,function(error, result){
        if(error){
            console.log('Error:'+ error);
        }else{
            console.log(result.result.n);
        }

        callback(result.result.n);
    });

    var connectionOther = db.collection("other");
    var otherData = {"name":params.name,"job":params.job};
    connectionOther.insert(otherData,function(error, result){
        if(error){
            console.log('Error:'+ error);
        }else{
            console.log(result.result.n);
        }
    });
}

router.get('/insert', function(req, res, next){

    var params = URL.parse(req.url, true).query;

    MongoClient.connect(DB_CONN_STR, function(err, client) {
        console.log("连接成功!");
        insertData(client, params, function(result) {
            res.send("Delete Success:" + result);

            client.close();
            console.log("连接断开!");
        });
    });
})

module.exports = router;

5. update.js:

var mongodb = require('mongodb')
var MongoClient = require('mongodb').MongoClient;
var DB_CONN_STR = 'mongodb://localhost:27017/sun';

var URL = require('url');
var express = require('express');
var router = express.Router();

function updateData(client, params, callback)
{
    var db = client.db('sun');
    var connectionTest = db.collection('test');
    var whereData = {"name":params.oldName}
    var updateDat = {$set: {"name":params.newName}}; //如果不用$set,替换整条数据
    connectionTest.update(whereData, updateDat, function(error, result){
        if (error) {
            console.log('Error:'+ error);
        }else{
            console.log(result);
        }

        callback(result.result.n);
    });

    var connectionOther = db.collection('other');
    connectionOther.update(whereData, updateDat, function(error, result){
        if (error) {
            console.log('Error:'+ error);
        }else{
            console.log(result);
        }
    });
}

router.get('/update', function(req, res, next){

    var params = URL.parse(req.url, true).query;

    MongoClient.connect(DB_CONN_STR, function(err, client) {
        console.log("连接成功!");
        updateData(client, params, function(result) {
            res.send("Delete Success:" + result);

            client.close();
            console.log("连接断开!");
        });
    });
})

module.exports = router;


6. delete.js:

var mongodb = require('mongodb')
var MongoClient = require('mongodb').MongoClient;
var DB_CONN_STR = 'mongodb://localhost:27017/sun';

var URL = require('url');
var express = require('express');
var router = express.Router();

function deleteData(client, params, callback)
{
    var db = client.db('sun');
    var devices = db.collection('test');
    var data = {"name":params.name};
    devices.remove(data, function(error, result){
        if (error) {
            console.log('Error:'+ error);
        }else{
            console.log(result.result.n);
        }

        callback(result.result.n);
    })
}

router.get('/delete', function(req, res, next){

    var params = URL.parse(req.url, true).query;

    MongoClient.connect(DB_CONN_STR, function(err, client) {
        console.log("连接成功!");
        deleteData(client, params, function(result) {
            res.send("Delete Success:" + result);

            client.close();
            console.log("连接断开!");
        });
    });
})

module.exports = router;

7. sun.js:

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('sun', { title: 'Sun'});
});

module.exports = router;

8. sun.ejs:

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <script src="https://cdn.bootcss.com/jquery/1.9.1/jquery.min.js"></script>

    <script type="text/javascript">
        function mongoInsert() {
            $.ajax({
                url: "http://localhost:3000/mongo/insert",//接口服务器地址
                dataType:"text",
                type:"get",
                data:{"name":document.getElementById("insertName").value,
                      "age":document.getElementById("insertAge").value,
                      "job":document.getElementById("insertJob").value},//请求数据
                success:function(data){
                    alert("插入成功");
                },
                error:function(e){
                    alert("Error:" + e.status+','+ e.statusText);
                }
            })
        }

        function mongoUpdate() {
            $.ajax({
                url: "http://localhost:3000/mongo/update",//接口服务器地址
                dataType:"text",
                type:"get",
                data:{"oldName":document.getElementById("oldName").value,
                    "newName":document.getElementById("newName").value},//请求数据
                success:function(data){
                    alert("更新成功");
                },
                error:function(e){
                    alert("Error:" + e.status+','+ e.statusText);
                }
            })
        }

        function mongoDelete() {
            $.ajax({
                url: "http://localhost:3000/mongo/delete",//接口服务器地址
                dataType:"text",
                type:"get",
                data:{"name":document.getElementById("deleteName").value},//请求数据
                success:function(data){
                    alert("删除成功");
                },
                error:function(e){
                    alert("Error:" + e.status+','+ e.statusText);
                }
            })
        }

        function mongoFind() {
            $.ajax({
                url: "http://localhost:3000/mongo/find",//接口服务器地址
                dataType:"text",
                type:"get",
                data:{"name":document.getElementById("findName").value},//请求数据
                success:function(data){
                    var jsonData = $.parseJSON(data);
                    document.getElementById("divList").innerHTML = "<br>Result:<br>----------------<br>";
                    for (i=0;i<jsonData.length;i++){
                        document.getElementById("divList").innerHTML += "Name:" + jsonData[i].name
                            + "<br>Age:" + jsonData[i].age
                            + "<br>Job:" + jsonData[i].job
                            + "<br>----------------<br>";
                    }
                },
                error:function(e){
                    document.getElementById("divList").innerHTML = "Error:" + e.status+','+ e.statusText;
                }
            })
        }
    </script>
  </head>
  <body>
    <h1><%= title %>'s Test for nodeJS + MongoDB + Express + Async</h1>
    <p>Welcome to <%= title %></p>

    <ul>
      <li style="margin-bottom:10px">
        后台接口调用:<a href="http://localhost:3000/users/getUserInfo?id=1">getUserInfo</a>
      </li>
      <li style="margin-bottom:10px">
        MongoDB新增数据:Name:<input id="insertName"> Age:<input id="insertAge"> Job:<input id="insertJob">  <button id="btnInsert" οnclick="mongoInsert();">新增</button>
      </li>
      <li style="margin-bottom:10px">
        MongoDB修改数据:NameOld:<input id="oldName"> NameNew:<input id="newName"> <button id="btnUpdate" οnclick="mongoUpdate();">修改</button>
      </li>
      <li style="margin-bottom:10px">
        MongoDB删除数据:Name:<input id="deleteName"> <button id="btnDelete" οnclick="mongoDelete();">删除</button>
      </li>
      <li>
        从MongoDB检索数据:Name:<input id="findName"> <button id="btnFind" οnclick="mongoFind();">检索</button><a href="http://localhost:3000/mongo/find?name=sun">Find</a>
        <div id="divList">
        </div>
      </li>
    </ul>
  </body>
</html>

9. 画面效果如下:



整个实例在这里下载:NodeJS+MongoDB实例

  • 9
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值