如何使用Node构建轻量级发票应用程序:数据库和API
介绍
发票是企业可以向客户和客户出示的商品和服务文件。
数字发票工具需要跟踪客户、记录服务和价格、更新已付发票的状态,并提供显示发票的界面。 这将需要 CRUD(创建、读取、更新、删除)、数据库和路由。
注意: 这是一个 3 部分系列的第 1 部分。 第二个教程是如何使用Node构建一个轻量级的发票应用程序:用户界面。 第三篇教程是【X22X】如何用Vue和Node搭建轻量级的发票应用:JWT认证和发送发票【X125X】。
在本教程中,您将使用 Vue 和 NodeJS 构建一个发票应用程序。 此应用程序将执行创建、发送、编辑和删除发票等功能。
先决条件
要完成本教程,您需要:
- 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在编写迁移时使用 Promisescors用于跨域资源共享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
现在已经选择了数据库,接下来就是创建所需的表。
此应用程序将使用三个表:
- “用户” - 这将包含用户数据(
id、name、email、company_name、password) - “发票” - 存储发票数据(
id、name、paid、user_id) - “交易” - 一起制作发票的单一交易(
name、price、invoice_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 对象。 还指定了迁移脚本的 path 和 pattern。 要了解有关配置的更多信息,请参阅 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 之类的工具通过 name、email、company_name 和 password 向 /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 之类的工具通过 email 和 password 向 /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 之类的工具通过 name、user_id、txn_names 和 txn_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_id 和 invoice_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 构建轻量级发票应用程序:用户界面 。