海南老脚数

vuePress-theme-reco 海南老脚数    2017 - 2021
海南老脚数 海南老脚数

Choose mode

  • dark
  • auto
  • light
主页
指南
  • 应用介绍
  • cH5-PWA应用 (opens new window)
  • SSR-个人官网 (opens new window)
  • 微前端框架应用 (opens new window)
印记
高级
  • 小程序Node后端实践
  • JS开发灵活的数据应用
  • Node核心知识
  • Git原理详解及实战
进阶
  • 大厂H5开发实战
  • 前端性能优化
  • 前端面试指南
组件库
  • Vue3.0
  • Nuxt
  • 吃吃吃
分类
  • 问题集中营
  • VUE
  • 前端小笔记
  • Cookie
  • 深夜食堂
标签
Github (opens new window)
掘金 (opens new window)
author-avatar

海南老脚数

5

文章

4

标签

主页
指南
  • 应用介绍
  • cH5-PWA应用 (opens new window)
  • SSR-个人官网 (opens new window)
  • 微前端框架应用 (opens new window)
印记
高级
  • 小程序Node后端实践
  • JS开发灵活的数据应用
  • Node核心知识
  • Git原理详解及实战
进阶
  • 大厂H5开发实战
  • 前端性能优化
  • 前端面试指南
组件库
  • Vue3.0
  • Nuxt
  • 吃吃吃
分类
  • 问题集中营
  • VUE
  • 前端小笔记
  • Cookie
  • 深夜食堂
标签
Github (opens new window)
掘金 (opens new window)
  • 开篇:小程序的 Node.js 全栈之路
  • 基础篇 1:业务需求分析与基础设计
  • 基础篇 2 :后端技术选型 —— Node.js & hapi
  • 基础篇 3:欲善事先利器 —— Node.js 调试技巧
  • 实战篇 1 :项目工程初始化 —— 使用 hapi
  • 实战篇 2:接口契约与入参校验 —— 使用 Swagger & Joi
  • 实战篇 3:表结构设计、迁移与数据填充 —— 使用 Sequelize-cli
  • 实战篇 4:小程序列表获取 —— 使用 Sequelize
  • 实战篇 5 :身份验证设计 —— 使用 JWT
  • 实战篇 6:身份验证实现 —— 使用 hapi-auth-jwt2
  • 实战篇 7:小程序登录授权与 JWT 签发
  • 实战篇 8:订单创建 —— 使用事务
  • 实战篇 9:小程序订单支付 —— 小程序支付
  • 服务部署发布 —— 使用小程序开发者工具
  • 拓展篇 1:系统监控与记录 —— 使用 Good 插件
  • 拓展篇 2:系统稳定性测试 —— 使用 Lab & Code
  • 尾声:项目回顾,温故知新

vuePress-theme-reco 海南老脚数    2017 - 2021

实战篇 8:订单创建 —— 使用事务

海南老脚数

# 实战篇 8:订单创建 —— 使用事务

前面三节,我们用了较多的篇幅来交代用户身份管理与验证的技术实现,解决完用户身份验证的问题之后,我们终于可以开始实现与用户身份相关的订单创建的功能。

# 用户下单的订单表结构设计

对于一笔订单记录,往往涉及订单的订单编号,创建时间,订单的用户,支付状态,支付流水号,以及订单的商品详情。由于订单的收件地址涉及地址管理的相关业务逻辑,实际逻辑与订单商品的逻辑相仿,故而在小册中作精简设计。

# 1. orders 表结构定义与迁移

订单 orders 的表结构定义:

字段 字段类型 字段说明
id integer 订单的 ID,自增
user_id integer 用户的 ID
payment_status enum 付款状态

创建 orders 表的迁移文件 create-orders-table:

$ node_modules/.bin/sequelize migration:create --name create-orders-table
1
// migrations/create-orders-table.js
module.exports = {
  up: (queryInterface, Sequelize) => queryInterface.createTable(
    'orders',
    {
      id: {
        type: Sequelize.INTEGER,
        autoIncrement: true,
        primaryKey: true,
      },
      user_id: {
        type: Sequelize.INTEGER,
        allowNull: false,
      },
      payment_status: {
        type: Sequelize.ENUM('0', '1'),  // 0 未支付, 1 已支付
        defaultValue: '0',
      },
      created_at: Sequelize.DATE,
      updated_at: Sequelize.DATE,
    },
  ),

  down: queryInterface => queryInterface.dropTable('orders'),
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

在 models 中定义 orders 表结构:

// models/orders.js

module.exports = (sequelize, DataTypes) => sequelize.define(
  'orders',
  {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    user_id: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    payment_status: {
      type: DataTypes.ENUM('0', '1'),  // 0 未支付, 1 已支付
      defaultValue: '0',
    },
  },
  {
    tableName: 'orders',
  },
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 2. order_goods 表结构定义与迁移

订单商品表 order_goods 的表结构定义:

字段 字段类型 字段说明
id integer 订单商品的 ID,自增
order_id integer 订单的 ID
goods_id integer 商品的 ID
single_price float 商品的价格
count integer 商品的数量

创建 order-goods 表的迁移文件 create-order-goods-table:

$ node_modules/.bin/sequelize migration:create --name create-order-goods-table
1
// migrations/create-order-goods-table.js
module.exports = {
  up: (queryInterface, Sequelize) => queryInterface.createTable(
    'order_goods',
    {
      id: {
        type: Sequelize.INTEGER,
        autoIncrement: true,
        primaryKey: true,
      },
      order_id: {
        type: Sequelize.INTEGER,
        allowNull: false,
      },
      goods_id: {
        type: Sequelize.INTEGER,
        allowNull: false,
      },
      single_price: {
        type: Sequelize.FLOAT,
        allowNull: false,
      },
      count: {
        type: Sequelize.INTEGER,
        allowNull: false,
      },
      created_at: Sequelize.DATE,
      updated_at: Sequelize.DATE,
    },
  ),

  down: queryInterface => queryInterface.dropTable('order_goods'),
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

在 models 中定义表结构:

// models/order-goods.js

module.exports = (sequelize, DataTypes) => sequelize.define(
  'order_goods',
  {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true,
      
    },
    order_id: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    goods_id: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    single_price: {
      type: DataTypes.FLOAT,
      allowNull: false,
    },
    count: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
  },
  {
    tableName: 'order_goods',
  },
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

向数据库迁移 orders 与 order-goods 表:

$ node_modules/.bin/sequelize db:migrate
1

# 为通过身份验证的用户创建订单

我们在《实战篇 2:接口契约与入参校验——使用 Swagger & Joi》一节,在 routes/orders.js 中为订单创建,预留过如下的入参校验的 API 接口配置:


// 参数校验
{
  method: 'POST',
  path: `/${GROUP_NAME}`,
  handler: async (request, reply) => {
    reply();
  },
  config: {
    tags: ['api', GROUP_NAME],
    description: '创建订单',
    validate: {
      payload: {
        goodsList: Joi.array().items(
          Joi.object().keys({
            goods_id: Joi.number().integer(),
            count: Joi.number().integer(),
          }),
        ),
      },
      ...jwtHeaderDefine,
    },
  },
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

传入的订单商品信息以数组的方式描述,例如:

// 参数示例
[
  { goods_id: 123, count: 1 },  // 1件 id 为123 的商品
  { goods_id: 124, count: 2 },  // 2件 id 为124 的商品
]
1
2
3
4
5

后续的订单创建,将在 handler 的处理方法中继续展开。

# 理解事务的使用场景

从表结构的设计关系来看,创建一次订单,依赖于先创建产生一条 orders 表的记录,获得一个 order_id, 然后在 order_goods 表中通过 order_id 插入订单中的每一条商品记录,以最终完成一次完整的订单创建行为。中途若商品记录的插入遇到了失败,则一个订单记录的创建行为便是不完整的,orders 表中却产生了一条数据不完整的垃圾数据。在这样的场景下,我们可以尝试引入事务操作。

数据库中的事务是指单个逻辑所包含的一系列数据操作,要么全部执行,要么全部不执行。在一个事务中,可能会包含开始(start)、提交(commit)、回滚(rollback)等操作,Sequelize 通过 Transaction 类来实现事务相关功能。以满足一些对操作过程的完整性比较高的使用场景。

Sequelize 支持两种使用事务的方法:

  • 托管事务
  • 非托管事务

托管事务基于 Promise 结果链进行自动提交或回滚。非托管事务则交由用户自行控制提交或回滚。

# 使用托管事务创建订单

async handler(request, reply) => {
  await models.sequelize.transaction((t) => {
    const result = models.orders.create(
      { user_id: request.auth.credentials.userId },
      { transaction: t },
    ).then((order) => {
      const goodsList = [];
      request.payload.goodsList.forEach((item) => {
        goodsList.push(models.order_goods.create({
          order_id: order.dataValues.id,
          goods_id: item.goods_id,
          // 此处单价的数值应该从商品表中反查出写入,出于教程的精简性而省略该步骤
          single_price: 4.9,
          count: item.count,
        }));
      });
      return Promise.all(goodsList);
    });
    return result;
  }).then(() => {
    // 事务已被提交
    reply('success');
  }).catch(() => {
    // 事务已被回滚
    reply('error');
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

无论是托管事务还是非托管事务,只要 sequelize.transaction 中抛出异常,sequelize.transaction 中所有关于数据库的操作都将被回滚。

更多功能请查看官方手册 Transactions (opens new window)。

GitHub 参考代码 chapter12/hapi-tutorial-1 (opens new window)

# 小结

关键词:request.auth.credentials,sequelize.transaction

本小节我们通过订单创建的案例,给同学们介绍了利用 request.auth.credentials 来解析 JWT,获取当前用户的身份标识,再利用 sequelize.transaction 完成数据库事务的使用,确保跨表的订单数据创建完整性。以帮助同学们在日后实现其他重要而涉及连续操作的业务时,很好地举一反三,有的放矢。

本小节参考代码汇总

GitHub 参考代码:chapter12/hapi-tutorial-1 (opens new window)

sequelize.transaction 更多操作参考官方手册:Transactions (opens new window)

欢迎来到 海南老脚数
看板娘