如何使用Node构建轻量级发票应用程序:数据库和API

来自菜鸟教程
跳转至:导航、​搜索

介绍

发票是企业可以向客户和客户出示的商品和服务文件。

数字发票工具需要跟踪客户、记录服务和价格、更新已付发票的状态,并提供显示发票的界面。 这将需要 CRUD(创建、读取、更新、删除)、数据库和路由。

注意: 这是一个 3 部分系列的第 1 部分。 第二个教程是如何使用Node构建一个轻量级的发票应用程序:用户界面。 第三篇教程是【X22X】如何用Vue和Node搭建轻量级的发票应用:JWT认证和发送发票【X125X】。


在本教程中,您将使用 VueNodeJS 构建一个发票应用程序。 此应用程序将执行创建、发送、编辑和删除发票等功能。

先决条件

要完成本教程,您需要:

  • Node.js 安装在本地,您可以按照【X57X】如何安装Node.js 并创建本地开发环境【X126X】进行。
  • SQLite安装在本地,您可以按照如何安装和使用SQLite来完成。
  • 测试 API 端点需要下载和安装类似 Postman 的工具。

注意: SQLite 目前默认预装在 macOS 和 Mac OS X 上。


本教程已使用 Node v16.1.0、npm v7.12.1 和 SQLite v3.32.3 进行了验证。

第 1 步 — 设置项目

现在我们已经设置了所有要求,接下来要做的是为应用程序创建后端服务器。 后端服务器将维护数据库连接。

首先为新项目创建一个目录:

mkdir invoicing-app

导航到新创建的项目目录:

cd invoicing-app

然后将其初始化为 Node 项目:

npm init -y

为了使服务器正常运行,需要安装一些 Node 包。 您可以通过运行以下命令来安装它们:

npm install bcrypt@5.0.1 bluebird@3.7.2 cors@2.8.5 express@4.17.1 lodash@4.17.21 multer@1.4.2 sqlite3@5.0.2^> umzug@2.3.0<^>

该命令安装以下软件包:

  • bcrypt 散列用户密码
  • bluebird 在编写迁移时使用 Promises
  • cors 用于跨域资源共享
  • express 为我们的 Web 应用程序提供动力
  • lodash 用于实用方法
  • multer 处理传入的表单请求
  • sqlite3 创建和维护数据库
  • umzug 作为任务运行器来运行我们的数据库迁移

注意: 自最初发布以来,本教程已更新为包括 lodash 用于 isEmpty()。 处理 multipart/form-data 的中间件库从 connect-multiparty 更改为 multer


创建一个包含应用程序逻辑的 server.js 文件。 在 server.js 文件中,导入必要的模块并创建一个 Express 应用程序:

服务器.js

const express = require('express');
const cors = require('cors');
const sqlite3 = require('sqlite3').verbose();
const PORT = process.env.PORT || 3128;

const app = express();

app.use(express.urlencoded({extended: false}));
app.use(express.json());

app.use(cors());

// ...

创建一个 / 路由来测试服务器是否工作:

服务器.js

// ...

app.get('/', function(req, res) {
  res.send('Welcome to Invoicing App.');
});

app.listen() 告诉服务器监听传入路由的端口:

服务器.js

// ...

app.listen(PORT, function() {
  console.log(`App running on localhost:${PORT}.`);
});

要启动服务器,请在项目目录中运行以下命令:

node server

您的应用程序现在将开始侦听传入的请求。

第 2 步 — 使用 SQLite 创建并连接到数据库

对于发票应用程序,需要一个数据库来存储现有发票。 SQLite 将成为此应用程序的首选数据库客户端。

首先创建一个 database 文件夹:

mkdir database

运行 sqlite3 客户端并在这个新目录中为您的数据库创建一个 InvoicingApp.db 文件:

sqlite3 database/InvoicingApp.db

现在已经选择了数据库,接下来就是创建所需的表。

此应用程序将使用三个表:

  • “用户” - 这将包含用户数据(idnameemailcompany_namepassword
  • “发票” - 存储发票数据(idnamepaiduser_id
  • “交易” - 一起制作发票的单一交易(namepriceinvoice_id

由于已经确定了必要的表,下一步是运行查询以创建表。

随着应用程序的增长,迁移用于跟踪数据库中的更改。 为此,在 database 目录中创建一个 migrations 文件夹。

mkdir database/migrations

这将是所有迁移文件的位置。

现在,在 migrations 文件夹中创建一个 1.0.js 文件。 此命名约定是为了跟踪最新的更改。

1.0.js 文件中,首先导入节点模块:

数据库/迁移 1.0.js

"use strict";
const path = require('path');
const Promise = require('bluebird');
const sqlite3 = require('sqlite3');

// ...

然后,导出将在运行迁移文件时执行的 up 函数和用于反转对数据库的更改的 down 函数。

数据库/迁移/1.0.js

// ...

module.exports = {
  up: function() {
    return new Promise(function(resolve, reject) {
      let db = new sqlite3.Database('./database/InvoicingApp.db');

      db.run(`PRAGMA foreign_keys = ON`);

      // ...

up 函数中,首先与数据库建立连接。 然后在 sqlite 数据库上启用外键。 在 SQLite 中,默认情况下禁用外键以实现向后兼容性,因此必须在每个连接上启用外键。

接下来,指定查询以创建表:

数据库/迁移/1.0.js

// ...

      db.serialize(function() {
        db.run(`CREATE TABLE users (
          id INTEGER PRIMARY KEY,
          name TEXT,
          email TEXT,
          company_name TEXT,
          password TEXT
        )`);

        db.run(`CREATE TABLE invoices (
          id INTEGER PRIMARY KEY,
          name TEXT,
          user_id INTEGER,
          paid NUMERIC,
          FOREIGN KEY(user_id) REFERENCES users(id)
        )`);

        db.run(`CREATE TABLE transactions (
          id INTEGER PRIMARY KEY,
          name TEXT,
          price INTEGER,
          invoice_id INTEGER,
          FOREIGN KEY(invoice_id) REFERENCES invoices(id)
        )`);
      });

      db.close();
    });
  }

}

serialize() 函数用于指定查询将按顺序运行而不是同时运行。

创建迁移文件后,下一步是运行它们以在数据库中进行更改。 为此,请从应用程序的根目录创建一个 scripts 文件夹:

mkdir scripts

然后在这个新目录中创建一个名为 migrate.js 的文件。 并将以下内容添加到 migrate.js 文件中:

脚本/migrate.js

const path = require('path');
const Umzug = require('umzug');

let umzug = new Umzug({
  logging: function() {
    console.log.apply(null, arguments);
  },
  migrations: {
    path: './database/migrations',
    pattern: /\.js$/
  },
  upName: 'up'
});

// ...

首先,导入所需的节点模块。 然后使用配置创建一个新的 umzug 对象。 还指定了迁移脚本的 pathpattern。 要了解有关配置的更多信息,请参阅 umzug README

为了提供一些详细的反馈,请创建一个函数来记录事件,如下所示,然后最后执行 up 函数来运行迁移文件夹中指定的数据库查询:

脚本/migrate.js

// ...

function logUmzugEvent(eventName) {
  return function(name, migration) {
    console.log(`${name} ${eventName}`);
  };
}

// using event listeners to log events
umzug.on('migrating', logUmzugEvent('migrating'));
umzug.on('migrated', logUmzugEvent('migrated'));
umzug.on('reverting', logUmzugEvent('reverting'));
umzug.on('reverted', logUmzugEvent('reverted'));

// this will run your migrations
umzug.up().then(console.log('all migrations done'));

现在,要执行脚本,请转到您的终端并在应用程序的根目录中运行:

node scripts/migrate.js

您将看到类似于以下内容的输出:

Outputall migrations done
== 1.0: migrating =======
1.0 migrating

此时,运行 migrate.js 脚本已将 1.0.js 配置应用到 InvoicingApp.db

第 3 步 - 创建应用程序路由

现在数据库已经设置好了,接下来就是回到 server.js 文件并创建应用程序路由。 对于此应用程序,将提供以下路线:

网址 方法 功能
/register POST 注册新用户
/login POST 登录现有用户
/invoice POST 创建新发票
/invoice/user/{user_id} GET 获取用户的所有发票
/invoice/user/{user_id}/{invoice_id} GET 获取特定发票
/invoice/send POST 向客户发送发票

发布 /register

要注册新用户,将向服务器的 /register 路由发出 POST 请求。

重新访问 server.js 并添加以下代码行:

服务器.js

// ...

const _ = require('lodash');

const multer  = require('multer');
const upload = multer();

const bcrypt = require('bcrypt');
const saltRounds = 10;

// POST /register - begin

app.post('/register', upload.none(), function(req, res) {
  // check to make sure none of the fields are empty
  if (
    _.isEmpty(req.body.name)
    || _.isEmpty(req.body.email)
    || _.isEmpty(req.body.company_name)
    || _.isEmpty(req.body.password)
  ) {
    return res.json({
      "status": false,
      "message": "All fields are required."
    });
  }

  // any other intended checks

// ...

检查是否有任何字段为空,以及发送的数据是否符合所有规范。 如果发生错误,则会将错误消息作为响应发送给用户。 如果不是,则对密码进行哈希处理,然后将数据存储在数据库中,并向用户发送响应,通知他们已注册。

服务器.js

// ...

    bcrypt.hash(req.body.password, saltRounds, function(err, hash) {

    let db = new sqlite3.Database('./database/InvoicingApp.db');

    let sql = `INSERT INTO
                users(
                  name,
                  email,
                  company_name,
                  password
                )
                VALUES(
                  '${req.body.name}',
                  '${req.body.email}',
                  '${req.body.company_name}',
                  '${hash}'
                )`;

    db.run(sql, function(err) {
      if (err) {
        throw err;
      } else {
        return res.json({
          "status": true,
          "message": "User Created."
        });
      }
    });

    db.close();
  });

});

// POST /register - end

现在,如果我们使用 Postman 之类的工具通过 nameemailcompany_namepassword/register 发送 POST 请求,它将创建一个新用户:

钥匙 价值
name 测试用户
email example@example.com
company_name 测试公司
password 密码

我们可以使用查询并显示 Users 表来验证用户创建:

select * from users;

数据库现在包含一个新创建的用户:

Output1|Test User|example@example.com|Test Company|[hashed password]

您的 /register 路线现已通过验证。

发布 /login

如果现有用户尝试使用 /login 路由登录系统,则需要提供其电子邮件地址和密码。 一旦他们这样做了,路由就会按如下方式处理请求:

服务器.js

// ...

// POST /login - begin

app.post('/login', upload.none(), function(req, res) {
  let db = new sqlite3.Database('./database/InvoicingApp.db');

  let sql = `SELECT * from users where email='${req.body.email}'`;

  db.all(sql, [], (err, rows) => {
    if (err) {
      throw err;
    }

    db.close();

    if (rows.length == 0) {
      return res.json({
        "status": false,
        "message": "Sorry, wrong email."
      });
    }

// ...

对数据库进行查询以获取具有特定电子邮件的用户的记录。 如果结果返回一个空数组,则表示用户不存在,并发送响应通知用户错误。

如果数据库查询返回用户数据,则进一步检查输入的密码是否与数据库中的密码匹配。 如果是这样,那么将与用户数据一起发送响应。

服务器.js

// ...

    let user = rows[0];

    let authenticated = bcrypt.compareSync(req.body.password, user.password);

    delete user.password;

    if (authenticated) {
      return res.json({
        "status": true,
        "user": user
      });
    }

    return res.json({
      "status": false,
      "message": "Wrong password. Please retry."
    });
  });

});

// POST /login - end

// ...

测试路由时,您将收到成功或失败的结果。

现在,如果我们使用 Postman 之类的工具通过 emailpassword/login 发送 POST 请求,它会发回响应。

钥匙 价值
email example@example.com
password 密码

由于该用户存在于数据库中,我们得到以下响应:

Output{
    "status": true,
    "user": {
        "id": 1,
        "name": "Test User",
        "email": "example@example.com",
        "company_name": "Test Company"
    }
}

您的 /login 路线现已通过验证。

发布 /invoice

/invoice 路由处理发票的创建。 传递给路由的数据将包括用户 ID、发票名称和发票状态。 它还将包括构成发票的单一交易。

服务器处理请求如下:

服务器.js

// ...

// POST /invoice - begin

app.post('/invoice', upload.none(), function(req, res) {
  // validate data
  if (_.isEmpty(req.body.name)) {
    return res.json({
      "status": false,
      "message": "Invoice needs a name."
    });
  }

  // perform other checks

// ...

首先,验证发送到服务器的数据。 然后连接到数据库以进行后续查询。

服务器.js

// ...

  // create invoice
  let db = new sqlite3.Database('./database/InvoicingApp.db');

  let sql = `INSERT INTO invoices(
                name,
                user_id,
                paid
              )
              VALUES(
                '${req.body.name}',
                '${req.body.user_id}',
                0
              )`;

// ...

创建发票所需的 INSERT 查询被写入然后执行。 之后,将单个事务插入到 transactions 表中,并使用 invoice_id 作为外键来引用它们。

服务器.js

// ...

  db.serialize(function() {
    db.run(sql, function(err) {
      if (err) {
        throw err;
      }

      let invoice_id = this.lastID;

      for (let i = 0; i < req.body.txn_names.length; i++) {
        let query = `INSERT INTO
                      transactions(
                        name,
                        price,
                        invoice_id
                      ) VALUES(
                        '${req.body.txn_names[i]}',
                        '${req.body.txn_prices[i]}',
                        '${invoice_id}'
                      )`;

        db.run(query);
      }

      return res.json({
        "status": true,
        "message": "Invoice created."
      });
    });
  });

});
// POST /invoice - end

// ...

现在,如果我们使用 Postman 之类的工具通过 nameuser_idtxn_namestxn_prices/invoice 发送 POST 请求,它将创建一个新发票并记录交易:

钥匙 价值
name 测试发票
user_id 1
txn_names 苹果手机
txn_prices 600
txt_names MacBook
txn_prices 1700

然后,检查 Invoices 表:

select * from invoices;

观察以下结果:

Output1|Test Invoice|1|0

运行以下命令:

select * from transactions;

观察以下结果:

Output1|iPhone|600|1
2|Macbook|1700|1

您的 /invoice 路线现已通过验证。

获取 /invoice/user/{user_id}

现在,当用户想要查看所有创建的发票时,客户端将向 /invoice/user/:id 路由发出 GET 请求。 user_id 作为路由参数传递。 请求处理如下:

index.js

// ...

// GET /invoice/user/:user_id - begin

app.get('/invoice/user/:user_id', upload.none(), function(req, res) {
  let db = new sqlite3.Database('./database/InvoicingApp.db');

  let sql = `SELECT * FROM invoices WHERE user_id='${req.params.user_id}' ORDER BY invoices.id`;

  db.all(sql, [], (err, rows) => {
    if (err) {
      throw err;
    }

    return res.json({
      "status": true,
      "invoices": rows
    });
  });
});

// GET /invoice/user/:user_id - end

// ...

运行查询以获取所有发票以及与属于特定用户的发票相关的交易。

考虑一个对用户所有发票的请求:

localhost:3128/invoice/user/1

它将响应以下数据:

Output{"status":true,"invoices":[{"id":1,"name":"Test Invoice","user_id":1,"paid":0}]}

您的 /invoice/user/:user_id 路线现已通过验证。

获取 /invoice/user/{user_id}/{invoice_id}

要获取特定发票,使用 user_idinvoice_id/invoice/user/{user_id}/{invoice_id} 路由发出 GET 请求。 请求处理如下:

index.js

// ...

// GET /invoice/user/:user_id/:invoice_id - begin

app.get('/invoice/user/:user_id/:invoice_id', upload.none(), function(req, res) {
  let db = new sqlite3.Database('./database/InvoicingApp.db');

  let sql = `SELECT * FROM invoices LEFT JOIN transactions ON invoices.id=transactions.invoice_id WHERE user_id='${req.params.user_id}' AND invoice_id='${req.params.invoice_id}' ORDER BY transactions.id`;

  db.all(sql, [], (err, rows) => {
    if (err) {
      throw err;
    }

    return res.json({
      "status": true,
      "transactions": rows
    });
  });
});

// GET /invoice/user/:user_id/:invoice_id - end

// set application port
// ...

运行查询以获取单个发票以及与属于用户的发票相关的交易。

考虑为用户请求特定发票:

localhost:3128/invoice/user/1/1

它将响应以下数据:

Output{"status":true,"transactions":[{"id":1,"name":"iPhone","user_id":1,"paid":0,"price":600,"invoice_id":1},{"id":2,"name":"Macbook","user_id":1,"paid":0,"price":1700,"invoice_id":1}]}

您的 /invoice/user/:user_id/:invoice_id 路线现已通过验证。

结论

在本教程中,您将使用轻量级发票应用程序所需的所有路由设置您的服务器。

继续学习 如何使用 Node 构建轻量级发票应用程序:用户界面