不败君

前端萌新&初级后端攻城狮

商城或外卖购物车功能开发

商城或外卖购物车功能开发

2020-03-26 18:34:35

围观(19716)

最近在开发一款外卖的 APP, 遇到了博主上班一年多以来从来没遇到过的难点.

以前找工作面试, 去过一些比较大型的企业, 会问这样一个问题: 有没有遇到过一些什么技术难点?

这问题对当时的我来说, 并没有遇到过, 所以我也只能如实回答, 因为当时是找实习的工作, 确实也没遇到过什么技术难点...

这不, 工作一年多了, 遇到上一个难点了, 果然当时的我还是太年轻...


商城相关的系统, 订单系统会比较复杂. 比如商城的商品规格, 假设是卖衣服的, 规格就会出现蓝色 / 红色等, 除了颜色, 还可能有其他选项, 比如 S / M / L 尺码, 这就是多规格的问题了. 当然这不是博主所遇到的问题.


难点描述: 

多商铺的系统, 或者是外卖的 APP, 选择 A 店铺的 B 商品添加到购物车, 再选择 C 店铺的 D 商品添加到购物车. 再一起提交订单时, 会有个麻烦, 比如外卖的, 需要 B 商品订单提交给店铺 A, 而 D 商品需要给店铺 C.

其实就是需要将这两个商品的相关订单, 分开一次性入库到订单表, 而区分订单是根据店铺 ID 的, 比如 orders 表, 需要入库 A 店铺 / C 店铺 两条订单记录, 然后再次入库订单明细, 订单明细指的是记录订单的 ID 及 B 商品信息 以及 C 商品信息和购买数量. 

假如两个表的结构设计是这样的(实际肯定不止这几个字段):

orders 表结构:

id 自增

user_id 下单用户 ID

shop_id 商铺 ID

total_price 总价格


order_details 表结构:

id 自增

order_id 上方 order 表的 ID

product_id 商品 ID

product_sku_id 商品规格 ID


难点解决方法:

后端需要先获取到需要购买的数据, 也就是前端拿到购物车的数据, 做一些数据的处理然后提交这样的数据 (JSON 字串):

[
    {
        "product_id": 1,
        "product_sku_id": 1,
        "buy_num": 1
    },
    {
        "product_id": 2,
        "product_sku_id": 2,
        "buy_num": 1
    }
]

需要先将前端提交的数据进行处理, 比如:

定义一个 lst_product_sku_ids 变量存入所有拿到的 product_sku_id . 使用 array_column 函数处理.

定义 lst_product_ids 变量存入所有前端提交的商品ID.

定义 map_buy_num 变量存入经过 Map 处理的数据, 索引 Key 为上面 JSON 数据的 product_sku_id.

关于 Map 处理博主有写过这样的文章: Laravel多表查询优化


后端拿到这个购物数据转成对象或数组, 就可以查库得到商品信息, 比如商品 ID 为 1 和 商品 ID 为 2 的商品信息. 有了商品信息, 可以对这些商品信息用商品 ID 做 Map 处理.

其实 Map 处理很容易理解, 比如查库拿到的商品数据 可能是这样的:

[
    {
        "id": 1,
        "name" : "商品名称",
        "product_sku_id": 1
    },
    {
        "id": 2,
        "name" : "商品名称",
        "product_sku_id": 2
    }
]

当然实际肯定不止这么些数据, 进行处理之后, 数据是这样的:

[{
    "1": {
        "id": 1,
        "name": "商品名称",
        "product_sku_id": 1
    },
    "2": {
        "id": 2,
        "name": "商品名称",
        "product_sku_id": 2
    }
}]

其实也就是, 将商品的 ID 作为一维数组的 Key 索引.

接下来还需要获取规格信息, 因为一个商品的价格其实不是存放在 Product 商品表, 而是什么规格就什么价格的. 同样, 前端提交过来的购物信息, 就包含了 product_sku_id 这个参数, 所以非常简单就可以拿到提交过来的全部 peoduct_sku_id, 这里也说一下, 获取前端提交过来的 JSON 并转为数组之后, PHP 可以用这个参数直接获取到想要的值:

$input = 前端提交过来的购物信息 且已经转为数组
$product_sku_ids = array_column($input, 'product_sku_id');

这样就一次性获取到了提交过来的全部商品规格 ID.

同样, 从库获取到了规格信息, 以 product_id 作为索引 Key 做 Map处理. 注意: product_sku 商品规格表需要存 product_id 商品 ID 字段. 不然这里是无法做 Map 处理的.


接下来是拿商铺信息, 直接从库里面拿就行, 商铺的 ID 存放在 Product 表的 shop_id 字段. 拿到后直接使用 ID 即商铺的 ID 作为索引 Key 处理. 接下来可以获取每家店唯一的运费计算方式. 同样都是需要做 Map 处理, 把 shop_id 作为索引 Key. 


接下来就是重点了, 可以分开 order 订单存储了.

先创建两个数组 $map_shop_index 和 $lst_buy_detail.

然后循环遍历一次经过 Map 处理的规格信息, 因为价格那些都在这个数组, 通过购物信息拿到比较准确的数据都在这.


然后就是使用当前循环的 product_sku 的 product_id 拿到对应的商品信息(因为前面商品信息数组 已经处理了 所以很容易的拿到), 暂且使用命名 row_product


接着使用 row_product 的 shop_id 拿到 shop 店铺的信息, 暂且命名 row_shop 注意的是, 每次拿到数组 都应该判断是否存在这个索引的数据. 不存在就抛出异常或者跳过.


拿到了商品信息和店铺信息, 接下来就是判断库存了, 因为一开始就将规格对应的购买数量进行了 Map 处理 (即变量 map_buy_num), 所以这里很容易拿到当前规格对应想要购买的数量, 直接判断一下就行了. 如: $map_buy_num[$row_product_sku['id']]['buy_num'] 大于规格库存就抛出异常或者直接跳过.


此时, 判断一下 $lst_buy_detail[$shop_id] 是否存在, 即使用 Map 方法判断一下这个商铺是不是已经在 $lst_buy_detail 数组里面有存入过数据了. 如果没有则创建这个索引并存入一些信息, 比如商铺信息 $lst_buy_detail[$shop_id]['lst_shop'] = ['id' => '商铺ID', 'name' => '商铺名称'];

还要加入一些当前规格的购物信息, 比如:

$lst_buy_detail[$shop_id]['lst_shop_detai'][] = ['shop_id' => '商铺ID', 'product_id' => '商品ID'];

当然还有当前商铺购物的总价:

$lst_buy_detail[$shop_id]['total_price'] += $buy_num * $row_product['price'];


至此, 这个遍历循环就结束了. 接下来还有邮费计算和优惠券, 拿到这些信息后, 就可以循环 $lst_buy_detail 拿到商铺信息和优惠券信息还有规格等信息, 再次循环 $lst_buy_detail 里面的 lst_shop_detai 还能拿到更多的购物信息. 下面直接放伪代码:

$lst_buy_data = I('lst_buy_data/s', ''); // [{"product_id": 1,"product_sku_id": 1,"buy_num": 1},{"product_id": 2,"product_sku_id": 2,"buy_num": 1}]
$arr_buy_data = json_decode($lst_buy_data, true);   // 如果不能转为数组 请检查数据传输过来是否经过了转译
if (empty($arr_buy_data)) {
    // 抛出异常
}
$address_id = '地址信息. 比如用户有创建/保存地址信息, 其中有经纬度信息和详细信息, 主要用于判断运费';
$note = '下单备注';
$coupon_id = 1; // 优惠券 ID
$user_id = 1;   // 当前用户的 ID

// 以下代码可以放到逻辑层(业务逻辑)
// 地址信息处理
$model_user_address = new UserAddress();
$row_user_address = $model_user_address->get_user_address($address_id, $user_id);
if (empty($row_user_address)) {
    // 没有找到用户保存的地址 抛出异常
}

// 商品规格 IDS
$lst_product_sku_ids = array_column($arr_buy_data, 'product_sku_id');
// 基本商品 IDS
$lst_product_ids = array_column($arr_buy_data, 'product_id');
// 对应规格的购买数量 做了 Map 处理
$map_buy_num = CommonKit::set_index($arr_buy_data, 'product_sku_id');

// 读库拿商品信息
$model_product = new Product();
// 直接操作 ORM 获取 这里就不详细写了
$rows_product = $model_product->get_product_by_ids($lst_product_ids);
$map_product = CommonKit::set_index($rows_product, 'id');

// 读库拿规格信息
$model_product_sku = new ProductSku();
$map_product_sku = $model_product_sku->get_product_sku_by_ids($lst_product_sku_ids);    // 其实这里没有做 Map 处理

// 读库拿商铺信息
$lst_shop_ids = array_column($rows_product, 'shop_id');
$model_shop = new Shop();
$rows_shop = $model_shop->get_shop_by_ids($lst_shop_ids);
$map_shop = CommonKit::set_index($rows_shop, 'id');

// 获取各店铺运费计算规则
$model_freight = new Freight();
$rows_shop_freight_rule = $model_freight->shop_freight_rules($lst_shop_ids);
$map_shop_freight_rule = CommonKit::set_index($rows_shop_freight_rule, 'shop_id');

$map_shop_index = [];
$lst_buy_detail = [];

foreach ($map_product_sku as $key => $row_product_sku) {

    // 获取商品信息
    $product_id = $row_product_sku['product_id'];
    if (!isset($map_product[$product_id])) {
        continue;   // 也可以直接抛出异常了 说明想要购买的规格没有对应的商品
    }
    $row_product = $map_product[$product_id];

    // 获取门店
    $shop_id = $row_product['shop_id'];
    if (!isset($map_shop[$shop_id])) {
        // 店铺不存在的跳过 也可以直接抛出异常 店铺都不存在 还买个啥...
        continue;
    }
    $row_shop = $map_shop[$shop_id];

    if (!isset($map_shop_index[$shop_id])) {
        // 上文讲解是直接使用商品 ID 作为索引 Key, 当然也可以像下面这样 这样的好处就是创建的数组是一个有序的数组
        $map_shop_index[$shop_id] = sizeof($map_shop_index);
    }

    $service_product = new ProductService;  // 业务逻辑
    $service_product->format_data($row_product);    // 这里也就判断一下读库拿到的商品数据是不是完整的 例如商品名是不是缺少之类的... 或者是将商品的某些字段处理一下. 仅此而已 也可以直接去掉

    if (!isset($map_buy_num[$row_product_sku['id']])) {
        $buy_num = 1;   // 找不到前端提交的对应规格的购买信息 则直接给个购买数量默认值
    } else {
        $buy_num = $map_buy_num[$row_product_sku['id']]['buy_num'];
    }

    // 判断库存
    if ($buy_num > $row_product_sku['stock']) {
        // 库存不足 抛出异常
    }

    // 取当前店铺的索引 Key
    $shop_index = $map_shop_index[$shop_id];
    if (!isset($lst_buy_detail[$shop_index])) {
        $lst_buy_detail[$shop_index] = [];
        // 当前店铺购物信息
        $lst_buy_detail[$shop_index]['lst_shopping_detail'] = [];
        // 店铺信息
        $lst_buy_detail[$shop_index]['lst_shop'] = [
            'id' => $row_shop['id'],
            'name' => $row_shop['name'],
            'lng' => $row_shop['lng'],
            'lat' => $row_shop['lng'],
        ];
        // 当前店铺总价 默认值
        $lst_buy_detail[$shop_index]['total_price'] = 0;
        // 当前门店所需运费 后续还需要计算的 默认值
        $lst_buy_detail[$shop_index]['freight_price'] = 0;
        // 默认店铺优惠 ID 为空
        $lst_buy_detail[$shop_index]['coupon_id'] = '';
        // 默认店铺优惠为 0
        $lst_buy_detail[$shop_index]['coupon_discount'] = 0;
    }

    // 加入购物信息
    $lst_buy_detail[$shop_index]['lst_shopping_detail'][] = [
        'shop_id' => $row_product['shop_id'],   // 商品 ID
        'product_id' => $row_product['id'],     // 商品 ID
        'product_name' => $row_product['name'], // 商品名称
        'product_cover_img' => $row_product['cover_img'],   // 商品主图
        // 商品库存 (并非实际 实际库存在规格)
        'product_stock' => $row_product['stock'],
        'product_sold_num' => $row_product['sold_num'],     // 商品售量
        'product_describe' => $row_product['describe'],     // 商品描述
        'product_price' => $row_product_sku['price'],       // 商品价格
        // 值得注意 商品价格后端存储有一定的要求
        // 比如有些公司要求存小数并且精确到五六位 有些公司是全部都存单位为分的整数
        'product_sku_id' => $row_product_sku['id'], // 商品规格 ID
        'product_sku_name' => $row_product_sku['name'], // 商品规格名称
        // 商品库存 这个才是准确的库存
        'product_sku_stock' => $row_product_sku['stock'],
        'product_sku_sold_num' => $row_product_sku['sold_num'], // 商品规格售量
        'buy_num' => $buy_num   // 购买数量
    ];

    $lst_buy_detail[$shop_index]['total_price'] += $buy_num * $row_product['price'];    // 总价
}

// 计算运费
foreach ($lst_buy_detail as &$buy_detail) {
    $lst_shop = $buy_detail['lst_shop'];
    $shop_id = $lst_shop['id'];
    if (!isset($map_shop_freight_rule[$shop_id])) {
        continue;
        // 该店铺没设置运费计算规格 比如是否包邮等 直接抛出异常或者跳过
    }
    $row_shop_freight_rule = $map_shop_freight_rule[$shop_id];
    $buy_detail['freight_price'] = 0; // 这里自己计算一下运费吧 比如可以让店铺设置满多少包邮之类的 这里也能拿到该店铺的消费总金额.
}

// 计算优惠券
if (!empty($coupon_id)) {
    // 获取用户优惠券
    $model_user_coupon = new UserCoupon();
    $rows_user_coupon = $model_user_coupon->get_user_coupons($user_id, $coupon_id);
    // 每个店铺获取一张券
    $map_user_coupon = CommonKit::set_index($rows_user_coupon, 'shop_id');
    foreach ($lst_buy_detail as &$buy_detail) {
        $lst_shop = $buy_detail['lst_shop'];
        if (!isset($map_user_coupon[$lst_shop['id']])) {
            continue;
        }
        // 改变优惠券的状态
        $row_user_coupon = $model_user_coupon->use_coupon($map_user_coupon[$lst_shop['id']], $buy_detail['lst_shopping_detail']);
        $buy_detail['coupon_id'] = $row_user_coupon['id'];
        $buy_detail['coupon_discount'] = StringKit::change2yuan($row_user_coupon['discount']);
    }
}

// 如果有订单页面结算需求 可以在此返回数据给其他方法. 如果光是下单 可以继续
// return $lst_buy_detail;

// 以下代码可以再次优化

$lst_order_detail = [];
$lst_update_product = [];
$lst_update_product_sku = [];
$lst_order_id = [];
$order_no = $this->crete_order_no(); // 商户订单号

foreach ($lst_buy_detail as $buy_detail) {

    $shop_id = $buy_detail['lst_shop']['id'];
    // 订单金额 可能需要处理 因为后端存储的金额单位为分
    $order_price = $buy_detail['total_price'];
    // 优惠券id
    $coupon_id = $buy_detail['coupon_id'];
    $coupon_discount = $buy_detail['coupon_discount']; // 优惠券折扣
    $freight_price = $buy_detail['freight_price']; // 运费

    // 订单表数据
    $order = [
        'order_no' => $order_no,
        'user_id' => $user_id,
        'address_id' => $address_id,
        'shop_id' => $shop_id,
        'coupon_id' => $coupon_id,

        'order_price' => $order_price,
        'coupon_discount' => $coupon_discount,
        'freight_price' => $freight_price,
        'total_price' => $order_price - $coupon_discount + $freight_price,

        'phone' => $row_user_address['phone'],
        'receiver' => $row_user_address['receiver'],
        'address' => $row_user_address['address'],
        'note' => $note,

        'order_status' => OrderModel::ORDER_STATUS_WAITING_PAY,
    ];

    // 优惠券数据
    $lst_use_coupon = [
        'coupon_id' => $coupon_id
    ];

    $model_order = new OrderModel();
    $model_order->save($order);
    $order_id = $model_order->getLastInsID();
    $lst_order_id[] = $order_id;

    foreach ($buy_detail['lst_shopping_detail'] as $row_shopping_detail) {

        // 商家订单明细
        $lst_order_detail[] = [
            'user_id' => $user_id,
            'order_id' => $order_id,
            'product_id' => $row_shopping_detail['product_id'],
            'product_name' => $row_shopping_detail['product_name'],
            'product_sku_id' => $row_shopping_detail['product_sku_id'],
            'product_describe' => $row_shopping_detail['product_describe'],
            'product_cover_img' => $row_shopping_detail['product_cover_img'],
            'product_price' => $row_shopping_detail['product_price'],
            'buy_num' => $row_shopping_detail['buy_num'],
            'status' => OrderDetailModel::STATUS_NORMAL
        ];

        // 减少商品表总库存
        $lst_update_product[] = [
            'id' => $row_shopping_detail['product_id'],
            'stock' => $row_shopping_detail['product_stock'] + $row_shopping_detail['buy_num'],
            'sold_num' => $row_shopping_detail['product_sold_num'] + $row_shopping_detail['buy_num']
        ];

        // 减少商品规格表库存
        $lst_update_product_sku[] = [
            'id' => $row_shopping_detail['product_sku_id'],
            'stock' => $row_shopping_detail['product_sku_stock'] + $row_shopping_detail['buy_num'],
            'sold_num' => $row_shopping_detail['product_sku_sold_num'] + $row_shopping_detail['buy_num']
        ];
    }

    // 优惠券
    if (!empty($coupon_id)) {
        // 修改为已使用
        $user_coupon = new UserCoupon();
        $user_coupon->to_using($user_id, $coupon_id);
    }
}

if (empty($lst_order_detail) || empty($lst_update_product) || empty($lst_update_product_sku)) {
    // 抛出异常 因为根本没有可以执行的数据
}

// 更新商品表
$model_product = new ProductModel();
$model_product->saveAll($lst_update_product);

// 更新商品规格表
$model_product_sku = new ProductSkuModel();
$model_product_sku->saveAll($lst_update_product_sku);

// 存储订单明细表
$model_order_detail = new OrderDetail();
$order_detail_save_res = $model_order_detail->saveAll($lst_order_detail);
if (!$order_detail_save_res) {
    // 抛出异常 订单明细存储失败
}

// 更新优惠券
if (!empty($lst_use_coupon)) {
    $model_user_coupon = new UserCoupon();
    $model_user_coupon->change_to_using($user_id, $lst_use_coupon);
}

return $order_no;

伪代码如上, 可能会有些小问题, 比如数据库存储的金额单位之类的, 如果金额不对, 说明上面这个伪代码需要根据需求而自行修改.

另外, 伪代码中还出现了这个 CommonKit::set_index 方法, 其实代码很简单:

static public function set_index($array, $key_name)
{
    $new_array = [];
    foreach ($array as $key => $value) {
        $new_array[$value[$key_name]] = $value;
    }
    return $new_array;
}

伪代码还出现了多次重复的循环遍历, 因为 其实这段伪代码是可以分开为两个逻辑方法的, 一个可用于直接显示在结算页面价格计算.

所以这就是为什么 淘宝 / 京东 多个不同店铺的商品同时下单, 会自动分成几个订单的原因了. 而 美团 / 饿了么 则直接不给多店铺的购物车, 进去一家店, 点个 + 号直接到底部的购物车, 这样确实就少了很多工作量.

本文地址 : bubaijun.com/page.php?id=170

版权声明 : 未经允许禁止转载!

评论:我要评论
发布评论:
Copyright © 不败君 粤ICP备18102917号-1

不败君

首 页 作 品 微 语