2020-03-26 18:34:35
围观(21923)
最近在开发一款外卖的 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
版权声明 : 未经允许禁止转载!
上一篇文章: 使用HTML和JS开发生命计算器
下一篇文章: PHP实现礼品奖池抽奖