2020-03-26 18:34:35
围观(104864)
最近在开发一款外卖的 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实现礼品奖池抽奖