如何使用 Node.Net 创建轻量级发票应用程序?数据库和应用程序接口

介绍

发票是商品和服务的文件,只要一个企业可以向客户和客户呈现。

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

<美元 > [注] 注: 这是由3个部分组成的系列的第一部分。 第二个教程是如何用节点构建一个轻量级的发票App:用户界面. 第三个教程是[如何用Vue和Node来构建轻量级的开具发票的应用程序:JWT认证和发出发票] (https://andsky.com/tech/tutorials/how-to-build-a-lightweight-invoicing-app-with-vue-and-node-jwt-authentication-and-sending-invoices). < $ > (美元)

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

前提条件

要完成本教程,您将需要:

<$>[注] 注: SQLite 目前默认情况下在 macOS 和 Mac OS X 上预安装。

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

步骤1 - 设置项目

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

开始创建新项目的目录:

1mkdir invoicing-app

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

1cd invoicing-app

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

1npm init -y

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

该命令会安装以下软件包:

  • bcrypt 用来哈希用户密码 * bluebird 用来编写迁移时的承诺 * cors 用来交叉源资源共享 * express 用来支持我们的 Web 应用 * lodash 用来使用实用方法 * multer 用来处理接收表单请求 * sqlite3 用来创建和维护数据库 * umzug 作为一个任务运行器来运行我们的数据库迁移

<$>[注] 注: 自最初发布以来,本教程已被更新为包含 lodash for isEmpty(). 处理 multipart/form-data' 的中间软件库从 connect-multiparty改为multer`。

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

 1[label server.js]
 2const express = require('express');
 3const cors = require('cors');
 4const sqlite3 = require('sqlite3').verbose();
 5const PORT = process.env.PORT || 3128;
 6
 7const app = express();
 8
 9app.use(express.urlencoded({extended: false}));
10app.use(express.json());
11
12app.use(cors());
13
14// ...

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

1[label server.js]
2// ...
3
4app.get('/', function(req, res) {
5  res.send('Welcome to Invoicing App.');
6});

’app.listen()’ 告知服务器该端口要听取接入路线:

1[label server.js]
2// ...
3
4app.listen(PORT, function() {
5  console.log(`App running on localhost:${PORT}.`);
6});

要启动服务器,请在项目目录中运行下列操作:

1node server

您的应用程序现在将开始听取接收请求。

步骤 2 — 使用 SQLite 创建和连接到数据库

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

开始创建一个数据库文件夹:

1mkdir database

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

1sqlite3 database/InvoicingApp.db

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

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

  • ) * ) * )

既然已确定必要的表,下一步是运行查询来创建表。

迁移用于跟踪数据库中的变化,因为应用程序的增长. 要做到这一点,在数据库目录中创建一个迁移文件夹。

1mkdir database/migrations

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

现在,在迁移文件夹中创建一个1.0.js文件。

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

1[label database/migrations 1.0.js]
2"use strict";
3const path = require('path');
4const Promise = require('bluebird');
5const sqlite3 = require('sqlite3');
6
7// ...

然后,导出一个函数,该函数将在迁移文件运行时执行,以及一个函数来逆转数据库的更改。

 1[label database/migrations/1.0.js]
 2// ...
 3
 4module.exports = {
 5  up: function() {
 6    return new Promise(function(resolve, reject) {
 7      let db = new sqlite3.Database('./database/InvoicingApp.db');
 8
 9      db.run(`PRAGMA foreign_keys = ON`);
10
11      // ...

函数中,首先连接到数据库,然后在sqlite数据库中启用外部密钥,在SQLite中,外部密钥默认被禁用以允许反向兼容,因此外部密钥必须在每个连接中启用。

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

 1[label database/migrations/1.0.js]
 2// ...
 3
 4      db.serialize(function() {
 5        db.run(`CREATE TABLE users (
 6          id INTEGER PRIMARY KEY,
 7          name TEXT,
 8          email TEXT,
 9          company_name TEXT,
10          password TEXT
11        )`);
12
13        db.run(`CREATE TABLE invoices (
14          id INTEGER PRIMARY KEY,
15          name TEXT,
16          user_id INTEGER,
17          paid NUMERIC,
18          FOREIGN KEY(user_id) REFERENCES users(id)
19        )`);
20
21        db.run(`CREATE TABLE transactions (
22          id INTEGER PRIMARY KEY,
23          name TEXT,
24          price INTEGER,
25          invoice_id INTEGER,
26          FOREIGN KEY(invoice_id) REFERENCES invoices(id)
27        )`);
28      });
29
30      db.close();
31    });
32  }
33
34}

serialize()函数用于指定查询将连续运行,而不是同时运行。

一旦已创建迁移文件,下一步是运行它们以对数据库进行更改. 要做到这一点,请从应用程序的根源创建一个脚本文件夹:

1mkdir scripts

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

 1[label scripts/migrate.js]
 2const path = require('path');
 3const Umzug = require('umzug');
 4
 5let umzug = new Umzug({
 6  logging: function() {
 7    console.log.apply(null, arguments);
 8  },
 9  migrations: {
10    path: './database/migrations',
11    pattern: /\.js$/
12  },
13  upName: 'up'
14});
15
16// ...

首先,需要的节点模块被导入,然后与配置一起创建一个新的)。

若要提供一些可言的反馈,请创建一个如下所示的记录事件的函数,然后最终执行函数来运行在迁移文件夹中指定的数据库查询:

 1[label scripts/migrate.js]
 2// ...
 3
 4function logUmzugEvent(eventName) {
 5  return function(name, migration) {
 6    console.log(`${name} ${eventName}`);
 7  };
 8}
 9
10// using event listeners to log events
11umzug.on('migrating', logUmzugEvent('migrating'));
12umzug.on('migrated', logUmzugEvent('migrated'));
13umzug.on('reverting', logUmzugEvent('reverting'));
14umzug.on('reverted', logUmzugEvent('reverted'));
15
16// this will run your migrations
17umzug.up().then(console.log('all migrations done'));

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

1node scripts/migrate.js

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

1[secondary_label Output]
2all migrations done
3== 1.0: migrating =======
41.0 migrating

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

步骤3 - 创建应用路径

现在,数据库已经正确设置,下一步是回到 server.js 文件并创建应用程序路径。

URL 方法 职能 | ————————————————————————————————. ————————————————————————————————. | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ · 开出新的发票 为用户取取所有发票 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 将发票发给客户

POST /注册

要注册新用户,将向您的服务器的/register路线发送一个 POST 请求。

重新定义server.js并添加以下代码行:

 1[label server.js]
 2// ...
 3
 4const _ = require('lodash');
 5
 6const multer  = require('multer');
 7const upload = multer();
 8
 9const bcrypt = require('bcrypt');
10const saltRounds = 10;
11
12// POST /register - begin
13
14app.post('/register', upload.none(), function(req, res) {
15  // check to make sure none of the fields are empty
16  if (
17    _.isEmpty(req.body.name)
18    || _.isEmpty(req.body.email)
19    || _.isEmpty(req.body.company_name)
20    || _.isEmpty(req.body.password)
21  ) {
22    return res.json({
23      "status": false,
24      "message": "All fields are required."
25    });
26  }
27
28  // any other intended checks
29
30// ...

检查是否有任何字段是空的,如果发送的数据符合所有规格。如果出现错误,则发送错误消息给用户作为回应。

 1[label server.js]
 2// ...
 3
 4    bcrypt.hash(req.body.password, saltRounds, function(err, hash) {
 5
 6    let db = new sqlite3.Database('./database/InvoicingApp.db');
 7
 8    let sql = `INSERT INTO
 9                users(
10                  name,
11                  email,
12                  company_name,
13                  password
14                )
15                VALUES(
16                  '${req.body.name}',
17                  '${req.body.email}',
18                  '${req.body.company_name}',
19                  '${hash}'
20                )`;
21
22    db.run(sql, function(err) {
23      if (err) {
24        throw err;
25      } else {
26        return res.json({
27          "status": true,
28          "message": "User Created."
29        });
30      }
31    });
32
33    db.close();
34  });
35
36});
37
38// POST /register - end

现在,如果我们使用像Postman 这样的工具将一个 POST 请求发送到/register中,包含名称,电子邮件,公司名称密码,它将创建一个新的用户:

KeyValue
nameTest User
email[email protected]
company_nameTest Company
passwordpassword

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

1select * from users;

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

1[secondary_label Output]
21|Test User|[email protected]|Test Company|[hashed password]

您的/注册路线已被验证。

POST / 登录

如果现有用户试图使用/login路径登录系统,他们需要提供他们的电子邮件地址和密码。

 1[label server.js]
 2// ...
 3
 4// POST /login - begin
 5
 6app.post('/login', upload.none(), function(req, res) {
 7  let db = new sqlite3.Database('./database/InvoicingApp.db');
 8
 9  let sql = `SELECT * from users where email='${req.body.email}'`;
10
11  db.all(sql, [], (err, rows) => {
12    if (err) {
13      throw err;
14    }
15
16    db.close();
17
18    if (rows.length == 0) {
19      return res.json({
20        "status": false,
21        "message": "Sorry, wrong email."
22      });
23    }
24
25// ...

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

如果数据库查询返回用户数据,则进行进一步检查,以查看输入的密码是否匹配该数据库中的密码。

 1[label server.js]
 2// ...
 3
 4    let user = rows[0];
 5
 6    let authenticated = bcrypt.compareSync(req.body.password, user.password);
 7
 8    delete user.password;
 9
10    if (authenticated) {
11      return res.json({
12        "status": true,
13        "user": user
14      });
15    }
16
17    return res.json({
18      "status": false,
19      "message": "Wrong password. Please retry."
20    });
21  });
22
23});
24
25// POST /login - end
26
27// ...

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

现在,如果我们使用像邮件人这样的工具将邮件请求发送到/login电子邮件密码,它将发送回复。

KeyValue
email[email protected]
passwordpassword

由于这个用户存在于数据库中,我们得到以下答案:

 1[secondary_label Output]
 2{
 3    "status": true,
 4    "user": {
 5        "id": 1,
 6        "name": "Test User",
 7        "email": "[email protected]",
 8        "company_name": "Test Company"
 9    }
10}

您的/login路线已被验证。

POST /发票

/ 发票路线处理发票的创建。传递到路线的数据将包括用户 ID、发票名称和发票状态。

服务器将如下处理请求:

 1[label server.js]
 2// ...
 3
 4// POST /invoice - begin
 5
 6app.post('/invoice', upload.none(), function(req, res) {
 7  // validate data
 8  if (_.isEmpty(req.body.name)) {
 9    return res.json({
10      "status": false,
11      "message": "Invoice needs a name."
12    });
13  }
14
15  // perform other checks
16
17// ...

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

 1[label server.js]
 2// ...
 3
 4  // create invoice
 5  let db = new sqlite3.Database('./database/InvoicingApp.db');
 6
 7  let sql = `INSERT INTO invoices(
 8                name,
 9                user_id,
10                paid
11              )
12              VALUES(
13                '${req.body.name}',
14                '${req.body.user_id}',
15                0
16              )`;
17
18// ...

创建发票所需的输入查询被写入,然后执行,然后单一交易被插入到交易表中,以发票_id作为外部密钥来参考它们。

 1[label server.js]
 2// ...
 3
 4  db.serialize(function() {
 5    db.run(sql, function(err) {
 6      if (err) {
 7        throw err;
 8      }
 9
10      let invoice_id = this.lastID;
11
12      for (let i = 0; i < req.body.txn_names.length; i++) {
13        let query = `INSERT INTO
14                      transactions(
15                        name,
16                        price,
17                        invoice_id
18                      ) VALUES(
19                        '${req.body.txn_names[i]}',
20                        '${req.body.txn_prices[i]}',
21                        '${invoice_id}'
22                      )`;
23
24        db.run(query);
25      }
26
27      return res.json({
28        "status": true,
29        "message": "Invoice created."
30      });
31    });
32  });
33
34});
35// POST /invoice - end
36
37// ...

现在,如果我们使用像 Postman 这样的工具将一个 POST 请求发送到 /invoice 以 nameuser_idtxn_namestxn_prices`,它将创建一个新的发票并记录交易:

KeyValue
nameTest Invoice
user_id1
txn_namesiPhone
txn_prices600
txt_namesMacBook
txn_prices1700

接下来,看看发票表:

1select * from invoices;

观察以下结果:

1[secondary_label Output]
21|Test Invoice|1|0

运行以下命令:

1select * from transactions;

观察以下结果:

1[secondary_label Output]
21|iPhone|600|1
32|Macbook|1700|1

您的 / 发票路线已被验证。

GET /发票/用户/{user_id}

现在,当用户想要查看所有创建的发票时,客户端将向 /invoice/user/:id 路线发送一个 'GET' 请求。

 1[label index.js]
 2// ...
 3
 4// GET /invoice/user/:user_id - begin
 5
 6app.get('/invoice/user/:user_id', upload.none(), function(req, res) {
 7  let db = new sqlite3.Database('./database/InvoicingApp.db');
 8
 9  let sql = `SELECT * FROM invoices WHERE user_id='${req.params.user_id}' ORDER BY invoices.id`;
10
11  db.all(sql, [], (err, rows) => {
12    if (err) {
13      throw err;
14    }
15
16    return res.json({
17      "status": true,
18      "invoices": rows
19    });
20  });
21});
22
23// GET /invoice/user/:user_id - end
24
25// ...

运行一个查询来检索与属于特定用户的发票相关的所有发票和交易。

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

1localhost:3128/invoice/user/1

它将用以下数据回复:

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

您的 /invoice/user/:user_id 路线已被验证。

GET /invoice/user/{user_id}/{invoice_id}

若要收取特定发票,则使用user_idinvoice_id/invoice/user/{user_id}/{invoice_id}路线提出GET请求。

 1[label index.js]
 2// ...
 3
 4// GET /invoice/user/:user_id/:invoice_id - begin
 5
 6app.get('/invoice/user/:user_id/:invoice_id', upload.none(), function(req, res) {
 7  let db = new sqlite3.Database('./database/InvoicingApp.db');
 8
 9  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`;
10
11  db.all(sql, [], (err, rows) => {
12    if (err) {
13      throw err;
14    }
15
16    return res.json({
17      "status": true,
18      "transactions": rows
19    });
20  });
21});
22
23// GET /invoice/user/:user_id/:invoice_id - end
24
25// set application port
26// ...

执行查询以获取单一发票和与用户所属的发票相关的交易。

考虑一个用户的特定发票请求:

1localhost:3128/invoice/user/1/1

它将用以下数据回复:

1[secondary_label Output]
2{"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 路线已被验证。

结论

在本教程中,您将配置您的服务器,为轻量级发票应用程序提供所有必要的路径。

继续你的学习与 如何用节点构建一个轻量化发票应用程序:用户界面

Published At
Categories with 技术
Tagged with
comments powered by Disqus