介绍
发票是商品和服务的文件,只要一个企业可以向客户和客户呈现。
数字发票工具需要跟踪客户、记录服务和价格,更新付费发票的状态,并提供显示发票的界面,这将需要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). < $ > (美元)
在本教程中,您将使用 Vue和 NodeJS构建发票应用程序,该应用程序将执行创建,发送,编辑和删除发票等功能。
前提条件
要完成本教程,您将需要:
- 本地安装的节点.js,您可以通过跟踪[如何安装节点.js并创建本地开发环境]来完成(https://www.digitalocean.com/community/tutorial_series/how-to-install-node-js-and-create-a-local-development-environment).
- 本地安装的 SQLite, 您可以遵循 [如何安装和使用 SQLite] (https://www.digitalocean.com/community/tutorial_collections/how-to-install-and-use-sqlite) 。
- 测试API端点需要下载并安装像Postman这样的工具. .
<$>[注] 注: 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 包,您可以通过运行以下命令来安装它们:
1npm install [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected]^> [email protected]
该命令会安装以下软件包:
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
中,包含名称
,电子邮件
,公司名称
和密码
,它将创建一个新的用户:
Key | Value |
---|---|
name | Test User |
email | [email protected] |
company_name | Test Company |
password | password |
我们可以使用查询并显示用户
表来验证用户创建:
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
与电子邮件
和密码
,它将发送回复。
Key | Value |
---|---|
email | [email protected] |
password | password |
由于这个用户存在于数据库中,我们得到以下答案:
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 以
name、
user_id、
txn_names和
txn_prices`,它将创建一个新的发票并记录交易:
Key | Value |
---|---|
name | Test Invoice |
user_id | 1 |
txn_names | iPhone |
txn_prices | 600 |
txt_names | MacBook |
txn_prices | 1700 |
接下来,看看发票表:
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_id
和invoice_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
路线已被验证。
结论
在本教程中,您将配置您的服务器,为轻量级发票应用程序提供所有必要的路径。
继续你的学习与 如何用节点构建一个轻量化发票应用程序:用户界面。