如何使用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 构建轻量级发票应用程序:用户界面 。