E-commerce 中订单系统的设计

数据库设计#

Order#

订单系统的核心表自然是 orders 系列表,laravel 的迁移文件如下

Schema::create('orders', function (Blueprint $table) {
    $table->increments('id');
    $table->string('number')->nullable()->comment('订单号');
    $table->unsignedInteger('address_id')->nullable()->comment('订单地址');
    $table->unsignedInteger('user_id')->index()->comment('用户id');
    $table->integer('items_total')->default(0)->comment('order每一个item的total的和 unit/分');
    $table->integer('adjustments_total')->default(0)->comment('调整金额 unit/分');
    $table->integer('total')->default(0)->comment('需支付金额 unit/分');

    $table->string('local_code')->comment('语言编号');
    $table->string('currency_code')->comment('货币编号');

    $table->string('state')->comment('主状态 checkout/new/cancelled/fulfilled');
    $table->string('payment_state')->comment('支付状态 checkout/awaiting_payment/partially_paid/cancelled/paid/partially_refunded/refunded');
    $table->string('shipment_state')->comment('运输状态 checkout/ready/cancelled/partially_shipped/shipped');

    $table->ipAddress('user_ip')->comment('用户ip ip2long后的结果');

    $table->timestamp('paid_at')->nullable()->comment('支付时间');
    $table->timestamp('confirmed_at')->nullable()->comment('确认订单时间');
    $table->timestamp('reviewed_at')->nullable()->comment('评论时间');
    $table->timestamp('fulfilled_at')->nullable()->comment('订单完成时间');

    $table->json('rest')->nullable()->comment('非核心字段冗余');

    $table->timestamps();
});

接下来是 order_items 表,用于记录 order 的 item

Schema::create('order_items', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedInteger('order_id')->index()->comment('外键');
    $table->unsignedInteger('variant_id')->comment('variant是国外的称呼,国内通常称为sku. 既库存最小单位');
    $table->unsignedInteger('product_id')->comment('冗余字段');
    $table->unsignedInteger('quantity')->comment('购买数量');

    // adjustment calculate
    $table->integer('units_total')->default(0)->comment('item中每一个unit的和. 单位/分');
    $table->integer('adjustments_total')->default(0);
    $table->integer('total')->default(0)->comment('units_total + adjustments_total');
    $table->integer('unit_price')->default(0)->comment('variant单价,冗余字段');

    $table->json('rest')->nullable()->comment('非核心字段冗余');
    $table->timestamps();
});

做过海外电商或者亚马逊的朋友应该对 variant (变体) 不陌生。国内称为 sku. 每一个商品都会有多个变体

接下来是 order_item_units

Schema::create('order_item_units', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedInteger('item_id')->index();
    $table->unsignedInteger('shipment_id')->comment();
    $table->integer('adjustments_total')->default(0);
    $table->timestamps();
});

对于用户购买的每一件实体,我们都需要谨慎的做一条记录,其会涉及到运输 / 促销 / 退货等问题,例如 variantA 我们购买了三件,那么我们就需要为这三件相同的变体分别创建三条记录.

上面三张表的关系从上往下 一个 order 会有多个 item, 一个 item 根据 quantity 的值,会有对应数量的 unit.

order 和 order_item 表大家应该都知道.

order_item_units 表可能有些同学第一次知道,但是其是必要存在的

tip: 所有的价格字段都使用分为单位存储,从而避免小数在计算机系统中存在的一些问题

可以消化梳理一下上面的三张订单系统核心表,然后再介绍一下其他相关表的设计。数据库的设计应该是灵活的,可以根据实际的需求任意添加和修改字段

Adjustment#

上面三张表都出现了 adjustment_total 字段,可能会有些疑惑.

如果我们每个变体的价格是 10 元,那我买三个这件变体则需要 30 元,但是实际支付的金额往往都不是 30 元., 会有各种各样的情况影响我们最终支付的价格.

比如运费 + 5 元,促销折扣 -8 元,税收 + 3 元,退还服务 +0.5 元,最后实际需要支付 35.5 元。为什么 30 元的金额最后却支付了 35.5 元?

我们不能凭空蹦出个 35.5 元,影响商品实际支付金额的每一个因素都是至关重要,我们需要负责任的记录下来。这便是 adjustment 表的来源.

首先看看迁移文件

Schema::create('adjustments', function (Blueprint $table) {
    $table->increments('id');

    $table->unsignedInteger('order_id')->nullable();
    $table->unsignedInteger('order_item_id')->nullable();
    $table->unsignedInteger('order_item_unit_id')->nullable();

    $table->string('type')->comment('调整的类型 shipping/promotion/tax等等');

    $table->string('label')->comment('结合type决定');

    $table->string('origin_code')->comment('结合label决定');

    $table->bool('included')->comment('是否会影响最终订单需要支付的价格')
        $table->integer('amount');
    $table->timestamps();

    $table->index('order_id');
    $table->index('order_item_id');
    $table->index('order_item_unit_id');
});

调整对订单价格的影响分为三种类型,分别是 影响整个 order, 影响 order_item (较少预见), 影响 order_item_units.

included 字段 用来判断本条 adjustment 记录,是否会影响消费者最终需要支付的金额

大部分的 adjustment 都会影响最终结算的价格,小部分如商品税,通常已经计算在了商品的单价中,不会影响消费者最终需要支付的金额。但是在开具发票时 却需要展示,因为我们做必要的记录

举个例子,假设我们一笔订单的运费是 5 元,那么会有这样一条 adjustment 记录

{
    id: 1,
    order_id: 1,
    order_item_id: null,
    order_item_unit_id: null,
    amount: 500,
    type: 'shipping',
    label: 'UPS',
    origin_code: null,
    included: 1,
}

假设我们消费者在一个订单中购买了三条 1.5 米数据线,并使用了一张 8 元的代金券,那么会有这样三条 adjustment 记录

[
    {
        id: 2,
        order_id: null,
        order_item_id: null,
        order_item_unit_id: 1,
        amount: -267,
        type: 'promotion',
        label: '8元代金券',
        origin_code: 'KSDI12K2', // 代金券code
        included: 1
    },
    {
        id: 2,
        order_id: null,
        order_item_id: null,
        order_item_unit_id: 2,
        amount: -267,
        type: 'promotion',
        label: '8元代金券',
        origin_code: 'KSDI12K2', // 代金券code
        included: 1
    },
    {
        id: 2,
        order_id: null,
        order_item_id: null,
        order_item_unit_id: 3,
        amount: -266,
        type: 'promotion',
        label: '8元代金券',
        origin_code: 'KSDI12K2', // 代金券code
        included: 1
    },
]

实际上对于大部分的促销需求 我们都应该将促销的折扣金额均分到每一个 unit 中.

这样设计的一个好处是,当消费者退调用其中一根数据线时,我们可以很清楚的计算出应该退多少金额给消费者。既 单价 + order_item_unit.adjustment

实际上清楚的记录每一笔影响最终支付金额的 adjustment, 无论对消费者还是对供应商来说都是负责的做法.

运费为什么不需要分摊到 unit?

运费对于一笔订单来说,是固定的外部消费 (由快递公司获利), 退款时商家并不需要为运费负责,只需要退还商品的等额价值即可

更加白话的说法就是 你在淘宝买了一个商品 20 元,运费 10 元,你觉得商品不好想要退货 (不考虑寄回的运费), 商家需要退你 30 元吗?

Shipment/Payment#

shipment 为订单的运输信息存储,payment 为支付信息存储。先来看看迁移文件

Schema::create('shipments', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedInteger('method_id')->comment('运输方式 外键');
    $table->unsignedInteger('order_id')->comment('订单 外键');
    $table->string('state')->comment('运输状态');
    $table->string('tracking_number')->nullable()->comment('订单号码');
    $table->timestamps();

    $table->index('order_id');
});
Schema::create('payments', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedInteger('method_id')->comment('支付方式');
    $table->unsignedInteger('order_id');7
    $table->string('currency_code', 3)->comment('冗余 货币编码');
    $table->unsignedInteger('amount')->default(0)->comment('支付金额');
    $table->string('state');
    $table->text('details')->nullable();
    $table->timestamps();

    $table->index('order_id');
});

上面在 order_item_units 表中存在一个 shipment_id 就对应这里的 shipment 表. shipment 和 order_item_units 之间是一对多的关系,订单中的每一个实体都可以被分别运输,例如京东购物时经常会见到这种情况.

一条 shipment/payment 会和一条实际存在的货运记录 / 支付记录 (退款记录) 挂钩.

上面就是订单系统的核心表了,对于后端来说,数据库就已经可以反映出整个系统的设计了.

接下来抽出一些细节进行详细的介绍

业务设计#

状态的设计#

相信很多小伙伴在做订单系统时会被各种状态 待确认,待支付,待发货,已发货,关闭订单 等等弄的晕头转向,今天我们就来梳理一下订单系统中的各种状态

如果各种状态只在 order 表使用一个 state 字段来记录显得有些力不从心,因此推荐使用三个字段,它们分别是 state,shipment_state,payment_state. 来分别记录在订单中我们或者消费者最关心的三种状态.

先来分别看看三个 state 的状态转移图

order.state↓

这是一笔订单的几个最基本的几个状态.

先讲一讲初始状态,既 checkout, 这与订单在什么时候创建有关系,当消费者在购物车点击结账时,就创建了一个订单,用于本次结账,因此订单的初始状态为 checkout

结账也就是所谓的确认订单页,在该页面中,消费者可以选择优惠券,选择地址等操作

处于该状态的订单对于后台管理系统 / 用户个人中心都是不可见的,且 checkout 类型订单的创建,也不会是库存有任何的变化

当用户在结账界面操作完成后需要用户点击确认订单。既行为 confirm 的触发,使订单的状态从 checkout 转换成了 new. 此时的订单无论是对于消费者 / 运营人员 / 仓储系统来说,都是真实存在且有效的。且响应的购物车记录也被清空。对于一笔状态为 new 的订单,消费者可以对其行使付款的权利.

order.payment_state↓

payment 的初始状态为 checkout 与上述一致.

当消费者触发 confirm 后,我们就可以触发 request_payment 行为,将订单的付款状态转换为 await_payment, 且将消费者引导到支付界面,当消费者支付成功后,在支付成功的回调中,触发 pay 行为,将支付状态转换为 paid.

关于退款的状态如上图所示,需要注意的是,对于退款,会出现只需要退订单中的部分商品的情况,因此加入了 partially_refunded (部分退款的状态).

order.shipment_state↓

当消费者 confirm 后,我们同时也需要调用响应的 request_shipment, 将我们的运输状态设置为一个 ready 状态,此时库存已经锁定.

关于仓库具体的备货时机 是在用户确认订单之后,还是等用户支付完成之后,需要根据实际的产品需求确定.

上面的状态图属于前者,当消费者确认订单后,便锁定了库存,并开始了备货阶段。如果是后一种情况可以将 checkout 修改为 pending, 等待消费者付款完成后再将状态转移到 ready

对于上面繁杂的状态转换,可以手动处理,也可以选择使用 state-machine 进行处理

订单价格的计算#

单价作为一件商品的固有属性,不会受到运输 / 促销折扣等等因素的影响。当商家对一个价值 100 元商品进行一个 30% 的折扣时,消费者只需要用 70 元的价格买入,但实际上商品的单价依旧是 100 元.

当一笔订单不存在任何的 adjustment 时,我们可以很容易的计算出订单的实际支付价格,只需要把各个 order_item 的 unit_price * quantity 相加起来即可

但是有了 adjustment 参与之后,我们必须自下往上的计算。下面的例子是在 laravel 项目且使用了上述的数据库设计后的一个计算方法.

public function calculator(Order $order)
{
    $items = $order->items;
    $items->load('adjustments', 'units.adjustments');
    $order->load('adjustments');

    $items->each(function ($item) {
        $item->units->each(function ($unit) {
            $unit->adjustments_total = $unit->adjustments->sum('amount');
        });

        $item->units()->saveMany($item->units);

        $item->adjustments_total = $item->adjustments->sum('amount');

        $item->units_total = $item->quantity * $item->unit_price + $item->units->sum('adjustments_total');

        $item->total = $item->units_total + $item->adjustments_total;
    });

    $order->items()->saveMany($items);

    $order->adjustments_total = $order->adjustments->sum('amount');
    $order->items_total = $order->items->sum('total');
    $order->total = $order->items_total + $order->adjustments_total;
    $order->save();
}

补充#

  1. 当订单创建的同时 (结账阶段) 就分别创建了一条 payment / 和 shipment 记录。在 payment 和 shipment 中分别记录了用户选择的支付方式与运输方式.

    在电商系统中,通常会有多种多样的支付方式和运输方式.

    但是在实际的业务编写时,业务层并不希望关心和处理繁杂的支付与运输方式,此时支付网关和运输网关便应运而生,其对业务层隐藏了繁杂的细节,而暴露出了统一的 api 接口.

    支付网关如提供商业服务的 ping++, 当然也有一些开源项目对这方面有所支持。如 yansongda/pay , Payum/Payum 等等

  2. 对于确认了但超过一定时间没有付款的订单,我们可以选择主动关闭该订单。将 order.state/order.payment_state/order.shipment_state 设置为 cancelled, 并对库存进行归还等系列操作

下一篇将会介绍促销系统的设计与实现,本篇的主要目的是介绍订单系统的相关设计,为下一篇做一个铺垫.

由于篇幅有限并没有过多的细节,有疑问或者不妥的地方欢迎留言.

本作品采用《CC 协议》,转载必须注明作者和本文链接
附言 1  ·  6年前
本帖由系统于 6年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 52

@Max 电商系统使用分作为金额单位,在库中存整型,不仅考虑到数据库中读写的精度,更重要的是外部处理程序的精度。 PHP 中,可以使用 bc_math 进行高精度计算,有效解决精度问题,如果是其他的客户端来操作数据库呢?比如 C 、 Erlang 、Rust 、其他各新兴语言?能保证其他的语言也能自如地进行高精度运算么?但整型,几乎所有语言都可以。所以为了避免引入浮点数在各个平台 / 语言上处理不一致的问题,统一使用整型,这是一种避坑的动作,前人流血流泪总结出来的。我们用就是了。

金融系统还使用皮分作为单位呢。

6年前 评论

下一篇什么时候出?

6年前 评论

金额字段应该是 Decimal 类型吧!

6年前 评论

@halweg 这两天,急需的话可以私聊个微信,交流一下促销这一块

6年前 评论

@米休 love
高性能 mysql 推荐使用整形存储 (金额单位为分)
跨境电商通常会有汇率转换问题因此使用分作为金额的单位.
使用整形存储可以避免一些浮点型的精度问题

6年前 评论

@Max 浮点精度可以用 bcmath 这种第三方库来解决 decimal 本来就是为了高精度货币设计的,如果单位只到了分,遇到大币种或者超小币种低价值商品价格一换算很容易精度丢失造成更大的损失

6年前 评论

@tradzero 使用 decimal 可以很好的避免精度问题。但是在业务中 (php 中) 难免也会需要使用金额进行一些判断.
因此我们整条业务线 (mysql -> php -> js) 都统一使用了整形 (分为单位) 来存储和处理所有与金额相关的东西.

6年前 评论

@tradzero 使用分作为货币基本单位。在一些开源的电子商务项目中是很常见的.

6年前 评论

@Max 电商系统使用分作为金额单位,在库中存整型,不仅考虑到数据库中读写的精度,更重要的是外部处理程序的精度。 PHP 中,可以使用 bc_math 进行高精度计算,有效解决精度问题,如果是其他的客户端来操作数据库呢?比如 C 、 Erlang 、Rust 、其他各新兴语言?能保证其他的语言也能自如地进行高精度运算么?但整型,几乎所有语言都可以。所以为了避免引入浮点数在各个平台 / 语言上处理不一致的问题,统一使用整型,这是一种避坑的动作,前人流血流泪总结出来的。我们用就是了。

金融系统还使用皮分作为单位呢。

6年前 评论

@qufo 确实应该用整形存储。哎,目前还在补别人的坑 ing

6年前 评论

文章超赞,作者辛苦了。期待下一篇的到来!!

6年前 评论

作者代码格式和我看到的一个人很像(写一行空一行),很漂亮

6年前 评论

高性能 mysql 推荐使用整形存储 (金额单位为分)

@Max 高性能 MySQL 讲的是几年前的 MySQL 了。

6年前 评论

个人更支持 @tradzero 的想法。而且,例如像打折、拼团、部分退款等场景需要进行除法运算,用分做单位会丢失精度,改单位需要刷一遍表,还得确保各端代码都同步更新,int 或许也不够大。

6年前 评论

@Wi1dcard 分配的时候有一个小技巧。
比如把 11 成 3 份。
11 % 3 = 2。
那每一份会占有 3。
还多出了 2 份,分别添加到前两个头上。
会分成
4,4,3 这样三份。
分配算法需要封装成辅助函数。

文中 unit 表和 adjustment 表就是充分考虑了分配需求和促销需求设计出来的。

6年前 评论

感谢分享~

6年前 评论

@Wi1dcard 哥们, 皮分了解一下? unsign bigint 了解一下?

6年前 评论

@Max 我们的算法是直接按分四舍五入的,最后一个补全,比如 3 个人付 11 分(仅为演示),则第一第二个是 round(11/3,0) 也就是 4 分,最后一个是 11-4-4 =3 分,保障整体不会错。 如果是 7 个人付 50 分,则前 6 个付 7 分,最后人个付 8 分。偏差在一分钱。要是有客诉,赔 1 分钱给他就是。

6年前 评论

@qufo 首先,unsign bigint 应当为 unsigned bigint。其次,为何不对比一下 decimal 和 bigint 的存储字节长度,以及它们精度的灵活性呢。而且我认为使用如此细小的单位转换成整型存储,对于开发人员来说,调试时肉眼看起来实在是痛苦。

6年前 评论

@qufo 如果精度再大,照你的说法,甚至可以转换成 16 进制使用 text 存储,运算时使用任意精度 BC Math 进行运算,再转换输出给客户端,但却越来越陡增复杂度,使用专为此设计的 decimal 就没有此问题。

6年前 评论

顺便问问 订单地址 用了 id 。 那如果这个地址改了。 那这个订单的地址也会跟着变化?

6年前 评论

@Yu addresses 表存在两种类型的地址。address.user_id 不为 null 的和 address.user_id 为 null 的。
后者是如何产生的?
消费者确认订单后,将消费者选择的 address (包含 user_id) copy 一份,然后另 address.user_id = null。并插入数据库 (创建操作)。将返回的新的 address.id 重新赋值给 order。
此时该地址只与 order 关联,不与消费关联

6年前 评论

@Wi1dcard 抱歉。我们讨论的始终是电商,不是微观世界,没那么多小数位的,你可以继续选择用 decimal ,并不影响。就止停止,不再回复。

6年前 评论

@qufo 是我提出的那么多小数位吗?你提出了皮分让我了解一下,我提出了不断增加小数位的问题,何必自己说自己?本身就是讨论交流,谁都不会影响谁的选择,没必要这么枪。你若是能提出反驳我的理由,欢迎指出,求知若渴。

使用 bigint 浪费 MySQL 存储空间,后期也不好调整。我觉得这是不明智的选择。

6年前 评论

@Max 嗯这样处理是可以的。

6年前 评论

这个 E-commerce 是现有的开源系统么?

6年前 评论

@WensonSmith 自家商业项目,结合公司的需求,然后参考了一些开源电商 算是比较不错的实践了

参考的开源电商 https://spreecommerce.org/

6年前 评论

@Max 那 address 会不会存在好多 address.user_id = null,但是其他字段都一样的数据

6年前 评论

@灰太狼来了 会呀,但是对每一笔订单,单独冗余一个地址是必要的.

6年前 评论
Destiny

好奇,commerce sku 怎么设计的。:thumbsup:

6年前 评论

虽然以前自己设计的表结构大致和您一样,但是发现您的设计还是有更多的考量在里面,非常感谢!期待下一篇 :thumbsup:

6年前 评论

@Destiny
prdoucts 商品表
product_variants 也就是 sku 表
product_option_values 可选属性表 颜色:红色 / 内存: 16G
product_attribute_values 展示属性表 厂商:小米,频带 3.2hz 等

product 一对多 product_variant

在 product_option_values 这里遇到了点问题,没有得到比较好的实践。所以没有写 产品系统相关的文章. :smile:

6年前 评论

@overtrue 谢谢超哥赞赏 😁, 有动力下一篇了

6年前 评论

可以聊聊订单号唯一性么

6年前 评论

可以聊聊优惠券的设计吗?
比如 折扣券 , 满减券 , 免运费券 等等

6年前 评论

@dinghua
博客:E-commerce 中促销系统的设计 这篇文章,不知道你看了没有.

优惠卷这里还有些特殊.
如果用户对一笔订单使用了一张优惠卷 (本质既促销,包含 actions 和 rules).
计算 adjustments 的时候 其实只需要直接应用 action 就好了。不需要再进行 rule 的验证了.

大概的逻辑像是这样
file

但优惠卷其实还是需要 checke 的. checker 的时间点是在

结账界面的,当前用户,拥有的优惠卷的列表. 要分别判断每张优惠卷是否能够应用于当前订单. 本质上就是 checke 每张优惠卷的 rules. 看看返回的是 true 还是 false.

只有为 true 的优惠卷才能被用户所使用.

6年前 评论

@安静 没有做复杂的处理,简单的加入时间和随机数在订单号中,防止重复

6年前 评论

@Max 如果把地址快照到 order ,是不是会更好。

6年前 评论

@Wi1dcard 项目中,使用的是这种。 decimal 储存,获取时使用 appends 添加字段到对象,把金额转为分。得到对象时就有两个字段,一个是以元为单位,一个是以分为单位的字段。

6年前 评论

@FreeMason 不会,跟原有地址相同格式保存,可以保存关联关系,保持数据结构一致,订单地址修改也比较方便。和用户地址使用同一份表单

6年前 评论

@Max

1、保存关联系关系的用意好处?

2、订单地址比较方便修改,体现在哪?

下面是自己在数据层以的一些想法 MySQL

如果放在 orders 优缺点:查询优、更新次

1、优点:当订单发货后,收货地址用户无法修改,只能后台才能修改,整体来说是读多改少,查询速度更快(少一 
     条 SQL)。
2、缺点:当发生修改收货地址时,锁定 order 数据对应数据,对 order 状态更新,可能产生影响(同时更新的机率较低)。

如果放在 addresses 优缺点:更新优、查询次

 1、优点:当更新收货地址时,未锁定 order 表对就数据。只锁定当前 address 一条数据,对 order 更新没有影响(只想到 
      这一条,请补充)
 2、缺点:每生成一条订单就 copy 一条地址到 addresses,占用索引空间更多(主键索引)。
 3、缺点:查看订单收货地址时,多查询一条 SQL,在订单未收到货之前,查询收货地址的机率还是不低的。
6年前 评论

$item->units->each(function ($unit) {
$unit->adjustments_total = $unit->adjustments->sum('amount');
});

    $item->units()->saveMany($item->units);

最后这句是不是有点多余呀?

6年前 评论
叶小许

有个疑问,文中 “三条 1.5 米数据线,并使用了一张 8 元的代金券,那么会有这样三条 adjustment 记录” 只使用了一张代金券,为什么会存在三条记录呢

6年前 评论

非常不错 帮助理解订单系统

6年前 评论

@叶小许 8 元 = 800 分 / 3,然后均分到 3 个商品里面, 作者讲的很详细

5年前 评论

可以抽空具体说说商品系列的表结构吗?求知若渴 :stuck_out_tongue_closed_eyes:

5年前 评论

不太能理解 OrderItemUnits , 举个栗子, 用户下了一个订单,买了 3 个 1.5m 数据线, 2 个转接头。

那么 1 个 order , 2 个 item , 5 个 orderItemUnits ?

这么设计的原因是什么?

如果用户下单 1000 个,要创建 1000 个 unit 有点不能理解啊

类似 Odoo 里面的 Stock.move 和 Stock.move_line, 按照不同的转移详情分多条 line, 而不是数量是 10 而创建 10 条 line

Example: You want 10 Widgets for a Transfer. 

The stock.move would show

  • 10 units from Stock

There could actually be THREE stock.move.lines that might show

  • 5 units from Stock/Shelf A

  • 4 units from Stock/Shelf B

  • 1 unit from Stock/Shelf C

5年前 评论

state-machine 不支持 laravel5.8,有没有支持 laravel5.8 的扩展包

4年前 评论

没搞懂 payments 表的作用,method_id 直接作为 orders 的一个字段不就好了吗

4年前 评论

大神,payments 表和 pay_methods 表,能贴出来看看吗

4年前 评论
Max (楼主) 4年前
williamQian (作者) 4年前