Skip to content
On this page

实战篇 8-小程序订单创建 —— 使用事务


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

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

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

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

1. orders 表结构定义与迁移

订单 orders 的表结构定义:

字段字段类型字段说明
idinteger订单的 ID,自增
user_idinteger用户的 ID
payment_statusenum付款状态

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

$ node_modules/.bin/sequelize migration:create --name create-orders-table
// 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'),
};

在 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',
  },
);

2. order_goods 表结构定义与迁移

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

字段字段类型字段说明
idinteger订单商品的 ID,自增
order_idinteger订单的 ID
goods_idinteger商品的 ID
single_pricefloat商品的价格
countinteger商品的数量

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

$ node_modules/.bin/sequelize migration:create --name create-order-goods-table
// 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'),
};

在 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',
  },
);

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

$ node_modules/.bin/sequelize db:migrate

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

我们在《实战篇 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,
    },
  },
},

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

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

后续的订单创建,将在 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');
  });
}

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

更多功能请查看官方手册 Transactions

GitHub 参考代码 chapter12/hapi-tutorial-1

小结

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

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

本小节参考代码汇总

GitHub 参考代码:chapter12/hapi-tutorial-1

sequelize.transaction 更多操作参考官方手册:Transactions