Prechádzať zdrojové kódy

Merge branch 'fixorder' into dev

joe 4 rokov pred
rodič
commit
667f9b0f15

+ 267 - 16
app/api/controller/order/StoreOrderController.php

@@ -15,6 +15,7 @@ use app\models\store\{
     StoreCouponIssue,
     StoreCouponUser,
     StoreOrder,
+    StoreOrderBatch,
     StoreOrderCartInfo,
     StoreOrderStatus,
     StorePink,
@@ -50,8 +51,11 @@ class StoreOrderController
         // TODO: 运费模板只能选择 ID 为 1 的模板?
         $temp = ShippingTemplates::get(1);
         if (!$temp) return app('json')->fail('默认模板未配置,无法下单');
+
         list($cartId) = UtilService::postMore(['cartId'], $request, true);
         if (!is_string($cartId) || !$cartId) return app('json')->fail('请提交购买的商品');
+
+        // 查询当前购物车有效的商品
         $uid = $request->uid();
         $cartGroup = StoreCart::getUserProductCartList($uid, $cartId, 1);
         if (count($cartGroup['invalid'])) {
@@ -60,15 +64,17 @@ class StoreOrderController
         if (!$cartGroup['valid']) {
             return app('json')->fail('请提交购买的商品');
         }
+        //
         $cartInfo = $cartGroup['valid'];
         $addr = UserAddress::getUserDefaultAddress($uid); //UserAddress::where('uid', $uid)->where('is_default', 1)->find();
+        // 获取当前购物车中各项价格
         $priceGroup = StoreOrder::getOrderPriceGroup($cartInfo, $addr);
         if ($priceGroup === false) {
             return app('json')->fail(StoreOrder::getErrorInfo('运费模板不存在'));
         }
         $other = [
-            'offlinePostage' => sys_config('offline_postage'),
-            'integralRatio' => sys_config('integral_ratio')
+            'offlinePostage' => sys_config('offline_postage'),  // 线下支付是否包邮
+            'integralRatio' => sys_config('integral_ratio')     // 积分抵用比例
         ];
         $usableCoupons = []; // TIP 屏蔽优惠券 StoreCouponUser::getUsableCouponList($uid, $cartGroup, $priceGroup['totalPrice']);
         $usableCoupon = isset($usableCoupons[0]) ? $usableCoupons[0] : null;
@@ -103,7 +109,7 @@ class StoreOrderController
         }
         $data['userInfo'] = $user;
         $data['integralRatio'] = $other['integralRatio'];
-        $data['offline_pay_status'] = (int)sys_config('offline_pay_status') ?? (int)2;
+        $data['offline_pay_status'] = (int)sys_config('offline_pay_status') ?? (int)2;  // 线下支付
         $data['store_self_mention'] = (int)sys_config('store_self_mention') ?? 0;//门店自提是否开启
         $data['system_store'] = []; // TIP: 屏蔽门店信息 ($res = SystemStore::getStoreDispose()) ? $res : [];//门店信息
         return app('json')->successful($data);
@@ -121,9 +127,9 @@ class StoreOrderController
      */
     public function computedOrder(Request $request, $key)
     {
-
-//        $priceGroup = StoreOrder::getOrderPriceGroup($cartInfo);
+        // $key 就是 confirm 返回的 orderKey
         if (!$key) return app('json')->fail('参数错误!');
+        // 是否已存在
         $uid = $request->uid();
         if (StoreOrder::be(['order_id|unique' => $key, 'uid' => $uid, 'is_del' => 0]))
             return app('json')->status('extend_order', '订单已生成', ['orderId' => $key, 'key' => $key]);
@@ -141,6 +147,7 @@ class StoreOrderController
             ['bargainId', ''],
             ['shipping_type', 1],
         ], $request, true);
+        // weixin / yue
         $payType = strtolower($payType);
         if ($bargainId) {
             $bargainUserTableId = StoreBargainUser::getBargainUserTableId($bargainId, $uid);//TODO 获取用户参与砍价表编号
@@ -161,7 +168,8 @@ class StoreOrderController
             if (StoreOrder::getIsOrderPink($pinkId, $request->uid()))
                 return app('json')->status('ORDER_EXIST', '订单生成失败,你已经参加该团了,请先支付订单', ['orderId' => StoreOrder::getStoreIdPink($pinkId, $request->uid())]);
         }
-        $priceGroup = StoreOrder::cacheKeyCreateOrder($request->uid(), $key, $addressId, $payType, (int)$useIntegral, $couponId, $mark, $combinationId, $pinkId, $seckill_id, $bargainId, true, 0, $shipping_type);
+        $priceGroup = StoreOrder::cacheKeyCreateOrder($request->uid(), $key, $addressId, $payType, (int)$useIntegral, 
+            $couponId, $mark, $combinationId, $pinkId, $seckill_id, $bargainId, true, 0, $shipping_type);
         // return app('json')->fail(StoreOrder::getErrorInfo('计算失败'));
         if ($priceGroup)
             return app('json')->status('NONE', 'ok', $priceGroup);
@@ -181,14 +189,24 @@ class StoreOrderController
      */
     public function create(Request $request, $key)
     {
+        // 还是那个 $orderKey
         if (!$key) return app('json')->fail('参数错误!');
+        // 订单是否已存在
         $uid = $request->uid();
         if (StoreOrder::be(['order_id|unique' => $key, 'uid' => $uid, 'is_del' => 0]))
             return app('json')->status('extend_order', '订单已生成', ['orderId' => $key, 'key' => $key]);
+        // 获得参数
         list($addressId, $couponId, $payType, $useIntegral, $mark, $combinationId, $pinkId, $seckill_id, $formId, $bargainId, $from, $shipping_type, $real_name, $phone, $storeId) = UtilService::postMore([
-            'addressId', 'couponId', 'payType', ['useIntegral', 0], 'mark', ['combinationId', 0], ['pinkId', 0], ['seckill_id', 0], ['formId', ''], ['bargainId', ''], ['from', 'weixin'],
+            'addressId', 
+            'couponId', 
+            'payType', 
+            ['useIntegral', 0], 
+            'mark', 
+            ['combinationId', 0], ['pinkId', 0], ['seckill_id', 0], ['formId', ''], ['bargainId', ''], 
+            ['from', 'weixin'],
             ['shipping_type', 1], ['real_name', ''], ['phone', ''], ['store_id', 0]
         ], $request, true);
+        // weixin / yue
         $payType = strtolower($payType);
         if ($bargainId) {
             $bargainUserTableId = StoreBargainUser::getBargainUserTableId($bargainId, $uid);//TODO 获取用户参与砍价表编号
@@ -209,18 +227,22 @@ class StoreOrderController
             if (StoreOrder::getIsOrderPink($pinkId, $request->uid()))
                 return app('json')->status('ORDER_EXIST', '订单生成失败,你已经参加该团了,请先支付订单', ['orderId' => StoreOrder::getStoreIdPink($pinkId, $request->uid())]);
         }
-        $isChannel = 1;
+        $isChannel = 1;         // 'routine'
         if ($from == 'weixin')
             $isChannel = 0;
         elseif ($from == 'weixinh5')
             $isChannel = 2;
-        $order = StoreOrder::cacheKeyCreateOrder($request->uid(), $key, $addressId, $payType, (int)$useIntegral, $couponId, $mark, $combinationId, $pinkId, $seckill_id, $bargainId, false, $isChannel, $shipping_type, $real_name, $phone, $storeId);
-        if ($order === false) return app('json')->fail(StoreOrder::getErrorInfo('订单生成失败'));
+        $order = StoreOrder::cacheKeyCreateOrder($request->uid(), $key, $addressId, $payType, (int)$useIntegral, 
+            $couponId, $mark, $combinationId, $pinkId, $seckill_id, $bargainId, false, $isChannel, 
+            $shipping_type, $real_name, $phone, $storeId);
+        if ($order === false) {
+            return app('json')->fail(StoreOrder::getErrorInfo('订单生成失败'));
+        }
         $orderId = $order['order_id'];
         $info = compact('orderId', 'key');
         if ($orderId) {
             event('OrderCreated', [$order]); //订单创建成功事件
-//            event('ShortMssageSend', [$orderId, 'AdminPlaceAnOrder']);//发送管理员通知
+            // event('ShortMssageSend', [$orderId, 'AdminPlaceAnOrder']);//发送管理员通知
             switch ($payType) {
                 case "weixin":
                     $orderInfo = StoreOrder::where('order_id', $orderId)->find();
@@ -270,7 +292,195 @@ class StoreOrderController
                     return app('json')->status('success', '订单创建成功', $info);
                     break;
             }
-        } else return app('json')->fail(StoreOrder::getErrorInfo('订单生成失败!'));
+        } else { // !$orderId
+            return app('json')->fail(StoreOrder::getErrorInfo('订单生成失败!'));
+        }
+    }
+
+        /**
+     * 计算订单金额
+     * @param Request $request
+     * @param $key
+     * @return mixed
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function computedOrder_v2(Request $request, $key)
+    {
+        // $key 就是 confirm 返回的 orderKey
+        if (!$key) return app('json')->fail('参数错误!');
+        // 是否已存在
+        $uid = $request->uid();
+        if (StoreOrderBatch::be(['unique' => $key, 'uid' => $uid]))
+            return app('json')->status('extend_order', '订单已生成', ['orderId' => $key, 'key' => $key]);
+        list($addressId, $couponId, $payType, $useIntegral, $mark, 
+            $combinationId, $pinkId, $seckill_id, $formId, $bargainId, $shipping_type) = UtilService::postMore([
+            'addressId', 
+            'couponId', 
+            ['payType', 'yue'], 
+            ['useIntegral', 0], 
+            'mark', 
+            ['combinationId', 0], 
+            ['pinkId', 0], 
+            ['seckill_id', 0], 
+            ['formId', ''], 
+            ['bargainId', ''],
+            ['shipping_type', 1],
+        ], $request, true);
+        // weixin / yue
+        $payType = strtolower($payType);
+        if ($bargainId) {
+            $bargainUserTableId = StoreBargainUser::getBargainUserTableId($bargainId, $uid);//TODO 获取用户参与砍价表编号
+            if (!$bargainUserTableId)
+                return app('json')->fail('砍价失败');
+            $status = StoreBargainUser::getBargainUserStatusEnd($bargainUserTableId);
+            if ($status == 3)
+                return app('json')->fail('砍价已支付');
+            StoreBargainUser::setBargainUserStatus($bargainId, $uid); //修改砍价状态
+        }
+        if ($pinkId) {
+            $cache_pink = Cache::get(md5('store_pink_' . $pinkId));
+            if ($cache_pink && bcsub($cache_pink['people'], $cache_pink['now_people'], 0) <= 0) {
+                return app('json')->status('ORDER_EXIST', '订单生成失败,该团人员已满', ['orderId' => StoreOrder::getStoreIdPink($pinkId, $request->uid())]);
+            }
+            if (StorePink::getIsPinkUid($pinkId, $request->uid()))
+                return app('json')->status('ORDER_EXIST', '订单生成失败,你已经在该团内不能再参加了', ['orderId' => StoreOrder::getStoreIdPink($pinkId, $request->uid())]);
+            if (StoreOrder::getIsOrderPink($pinkId, $request->uid()))
+                return app('json')->status('ORDER_EXIST', '订单生成失败,你已经参加该团了,请先支付订单', ['orderId' => StoreOrder::getStoreIdPink($pinkId, $request->uid())]);
+        }
+        $priceGroup = StoreOrderBatch::cacheKeyCreateOrderBatch($request->uid(), $key, $addressId, $payType, (int)$useIntegral, 
+            $couponId, $mark, $combinationId, $pinkId, $seckill_id, $bargainId, true, 0, $shipping_type);
+        // return app('json')->fail(StoreOrder::getErrorInfo('计算失败'));
+        if ($priceGroup)
+            return app('json')->status('NONE', 'ok', $priceGroup);
+        else
+            return app('json')->fail(StoreOrder::getErrorInfo('计算失败'));
+    }
+
+    /**
+     * 订单创建, 支持子订单版本
+     * @param Request $request
+     * @param $key
+     * @return mixed
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function create_v2(Request $request, $key)
+    {
+        // 还是那个 $orderKey
+        if (!$key) {
+            return app('json')->fail('参数错误!');
+        }
+        // 订单是否已存在
+        $uid = $request->uid();
+        if (StoreOrderBatch::be(['unique' => $key, 'uid' => $uid]))
+            return app('json')->status('extend_order', '订单已生成', ['orderId' => $key, 'key' => $key]);
+        // 获得参数
+        list($addressId, $couponId, $payType, $useIntegral, $mark, $combinationId, $pinkId, $seckill_id, $formId, $bargainId, $from, $shipping_type, $real_name, $phone, $storeId) = UtilService::postMore([
+            'addressId', 
+            'couponId', 
+            'payType', 
+            ['useIntegral', 0], 
+            'mark', 
+            ['combinationId', 0], ['pinkId', 0], ['seckill_id', 0], ['formId', ''], ['bargainId', ''], 
+            ['from', 'weixin'],
+            ['shipping_type', 1], ['real_name', ''], ['phone', ''], ['store_id', 0]
+        ], $request, true);
+        // weixin / yue
+        $payType = strtolower($payType);
+        if ($bargainId) {
+            $bargainUserTableId = StoreBargainUser::getBargainUserTableId($bargainId, $uid);//TODO 获取用户参与砍价表编号
+            if (!$bargainUserTableId)
+                return app('json')->fail('砍价失败');
+            $status = StoreBargainUser::getBargainUserStatusEnd($bargainUserTableId);
+            if ($status == 3)
+                return app('json')->fail('砍价已支付');
+            StoreBargainUser::setBargainUserStatus($bargainId, $uid); //修改砍价状态
+        }
+        if ($pinkId) {
+            $cache_pink = Cache::get(md5('store_pink_' . $pinkId));
+            if ($cache_pink && bcsub($cache_pink['people'], $cache_pink['now_people'], 0) <= 0) {
+                return app('json')->status('ORDER_EXIST', '订单生成失败,该团人员已满', ['orderId' => StoreOrder::getStoreIdPink($pinkId, $request->uid())]);
+            }
+            if (StorePink::getIsPinkUid($pinkId, $request->uid()))
+                return app('json')->status('ORDER_EXIST', '订单生成失败,你已经在该团内不能再参加了', ['orderId' => StoreOrder::getStoreIdPink($pinkId, $request->uid())]);
+            if (StoreOrder::getIsOrderPink($pinkId, $request->uid()))
+                return app('json')->status('ORDER_EXIST', '订单生成失败,你已经参加该团了,请先支付订单', ['orderId' => StoreOrder::getStoreIdPink($pinkId, $request->uid())]);
+        }
+        $isChannel = 1;         // 'routine'
+        if ($from == 'weixin')
+            $isChannel = 0;
+        elseif ($from == 'weixinh5')
+            $isChannel = 2;
+        $porderId = StoreOrderBatch::cacheKeyCreateOrderBatch($request->uid(), $key, $addressId, $payType, (int)$useIntegral, 
+            $couponId, $mark, $combinationId, $pinkId, $seckill_id, $bargainId, false, $isChannel, 
+            $shipping_type, $real_name, $phone, $storeId);
+        if ($porderId === false) {
+            return app('json')->fail(StoreOrderBatch::getErrorInfo('订单生成失败'));
+        }
+        $orderId = $porderId;       // for compact
+        $info = compact('orderId', 'key');
+        if ($porderId) {
+            event('BatchOrderCreated', [$porderId]); //订单创建成功事件
+            // event('ShortMssageSend', [$orderId, 'AdminPlaceAnOrder']);//发送管理员通知
+            switch ($payType) {
+                case "weixin":
+                    $subOrders = StoreOrderBatch::getAllSubOrders($porderId);
+                    if (!$subOrders) return app('json')->fail('支付订单不存在!');
+                    $fieldValues = StoreOrderBatch::sumFields($subOrders, ['paid','pay_price']);
+                    if ($fieldValues['paid']) {
+                        return app('json')->fail('支付已支付!');
+                    }
+                    //支付金额为0
+                    if (bcsub($fieldValues['pay_price'], 0, 2) <= 0) {
+                        //创建订单jspay支付
+                        $payPriceStatus = StoreOrderBatch::jsPayPriceBatch($porderId, $uid, $formId);
+                        if ($payPriceStatus)//0元支付成功
+                            return app('json')->status('success', '微信支付成功', $info);
+                        else
+                            return app('json')->status('pay_error', StoreOrderBatch::getErrorInfo());
+                    } else {
+                        try {
+                            if ($from == 'routine') {
+                                $jsConfig = OrderRepository::jsPayBatch($porderId); //创建订单jspay
+                            } else if ($from == 'weixinh5') {
+                                $jsConfig = OrderRepository::h5PayBatch($porderId);
+                            } else {
+                                $jsConfig = OrderRepository::wxPayBatch($porderId);
+                            }
+                        } catch (\Exception $e) {
+                            return app('json')->status('pay_error', $e->getMessage(), $info);
+                        }
+                        $info['jsConfig'] = $jsConfig;
+                        if ($from == 'weixinh5') {
+                            return app('json')->status('wechat_h5_pay', '订单创建成功', $info);
+                        } else {
+                            return app('json')->status('wechat_pay', '订单创建成功', $info);
+                        }
+                    }
+                    break;
+                case 'yue':
+                    if (StoreOrderBatch::yuePayBatch($porderId, $request->uid(), $formId))
+                        return app('json')->status('success', '余额支付成功', $info);
+                    else {
+                        $errorinfo = StoreOrderBatch::getErrorInfo();
+                        if (is_array($errorinfo))
+                            return app('json')->status($errorinfo['status'], $errorinfo['msg'], $info);
+                        else
+                            return app('json')->status('pay_error', $errorinfo);
+                    }
+                    break;
+                case 'offline':
+                    return app('json')->status('success', '订单创建成功', $info);
+                    break;
+            }
+        } else { // !$orderId
+            return app('json')->fail(StoreOrderBatch::getErrorInfo('订单生成失败!'));
+        }
     }
 
     /**
@@ -396,6 +606,37 @@ class StoreOrderController
         return app('json')->successful(StoreOrder::getUserOrderSearchList($request->uid(), $type, $page, $limit, $search));
     }
 
+    /**
+     * 订单详情 v2 
+     * 订单支持子订单之后,订单详情协议传过来的 $uni 有可能是子订单的,也有可能是主订单的
+     */
+    public function detail_v2(Request $request, $uni)
+    {
+        if (!strlen(trim($uni))) return app('json')->fail('参数错误');
+        $uid = $request->uid();
+        $first4 = mb_substr($uni, 0, 4);
+        if ($first4 == 'wxcn') {
+            $porder = StoreOrderBatch::where('porder_id', $uni)->where('uid', $uid)->find();
+            if (!$porder) {
+                return app('json')->fail('订单不存在');
+            }
+            $porder = $porder->toArray();
+            $subOrders = StoreOrderBatch::getAllSubOrders($porder['porder_id']);
+            if (!$subOrders) {
+                return app('json')->fail('订单不存在');
+            }
+            $order = $this->__get_order_details($uid, $subOrders[0]['order_id']);
+            if (!$order) {
+                return app('json')->fail('订单不存在');
+            }
+            $order['order_id'] = $porder['porder_id'];
+            $order['pay_price'] = $porder['total_price'];
+            return app('json')->successful('ok', $order);
+        } else {
+            return $this->detail($request, $uni);
+        }
+    }
+
     /**
      * 订单详情
      * @param Request $request
@@ -404,9 +645,19 @@ class StoreOrderController
      */
     public function detail(Request $request, $uni)
     {
-        if (!strlen(trim($uni))) return app('json')->fail('参数错误');
-        $order = StoreOrder::getUserOrderDetail($request->uid(), $uni);
-        if (!$order) return app('json')->fail('订单不存在');
+        $order = $this->__get_order_details($request->uid(), $uni);
+        if (!$order) {
+            return app('json')->fail('订单不存在');
+        }
+        return app('json')->successful('ok', $order);
+    }
+
+    public function __get_order_details($uid, $uni)
+    {
+        $order = StoreOrder::getUserOrderDetail($uid, $uni);
+        if (!$order) {
+            return false;
+        }
         $order = $order->toArray();
         //是否开启门店自提
         $store_self_mention = sys_config('store_self_mention');
@@ -438,7 +689,7 @@ class StoreOrderController
             $order['code'] = $url;
         }
         $order['mapKey'] = sys_config('tengxun_map_key');
-        return app('json')->successful('ok', StoreOrder::tidyOrder($order, true, true));
+        return StoreOrder::tidyOrder($order, true, true);
     }
 
     /**

+ 7 - 3
app/models/store/StoreCart.php

@@ -135,7 +135,7 @@ class StoreCart extends BaseModel
      * 
      * @uid
      * @cartIds: string
-     * @status: 1/0 是否为立即购买
+     * @status: 1/0 是否为立即购买, 如果传入1,则不过滤 is_new 字段
      */
     public static function getUserProductCartList($uid, $cartIds = '', $status = 0)
     {
@@ -155,9 +155,11 @@ class StoreCart extends BaseModel
         $model = $model->order('c.add_time DESC');
         $list = $model->select()->toArray();
         if (!count($list)) return compact('valid', 'invalid');
+
         $now = time();
         // TODO: for 里调 sql
         foreach ($list as $k => $cart) {
+            // 根据购物车中每一件商品ID 找到商品信息
             if ($cart['seckill_id']) {
                 $product = StoreSeckill::field($seckillInfoField)
                     ->find($cart['seckill_id'])->toArray();
@@ -210,7 +212,8 @@ class StoreCart extends BaseModel
                     }
 
                 }
-                if ($cart['product_attr_unique']) {
+                if ($cart['product_attr_unique']) { // 大部分走这个分支
+                    // 从 product_attr_value 表中根据 unique 找到 sku 详情
                     $attrInfo = StoreProductAttr::uniqueByAttrInfo($cart['product_attr_unique']);
                     //商品没有对应的属性
                     if (!$attrInfo || !$attrInfo['stock'])
@@ -251,7 +254,8 @@ class StoreCart extends BaseModel
                     $valid[] = $cart;
                 }
             }
-        }
+        } // foreach
+
         // TODO: for 里 sql
         foreach ($valid as $k => $cart) {
             if ($cart['trueStock'] < $cart['cart_num']) {

+ 17 - 10
app/models/store/StoreOrder.php

@@ -50,7 +50,7 @@ class StoreOrder extends BaseModel
 
     protected $insert = ['add_time'];
 
-    protected static $payType = ['weixin' => '微信支付', 'yue' => '余额支付', 'offline' => '线下支付'];
+    public static $payType = ['weixin' => '微信支付', 'yue' => '余额支付', 'offline' => '线下支付'];
 
     protected static $deliveryType = ['send' => '商家配送', 'express' => '快递配送'];
 
@@ -208,13 +208,14 @@ class StoreOrder extends BaseModel
     }
 
     /**
-     * 缓存订单信息
+     * 缓存大订单信息
+     * 
      * @param $uid
      * @param $cartInfo
      * @param $priceGroup
      * @param array $other
      * @param int $cacheTime
-     * @return string
+     * @return string 
      * @throws \Psr\SimpleCache\InvalidArgumentException
      */
     public static function cacheOrderInfo($uid, $cartInfo, $priceGroup, $other = [], $cacheTime = 600)
@@ -251,21 +252,22 @@ class StoreOrder extends BaseModel
     /**
      * 生成订单
      * @param $uid
-     * @param $key
+     * @param $key      orderKey 由客户端传入
      * @param $addressId
-     * @param $payType
-     * @param bool $useIntegral
+     * @param $payType  weixin / yue
+     * @param bool $useIntegral: 是否使用积分
      * @param int $couponId
-     * @param string $mark
+     * @param string $mark: 订单留言
      * @param int $combinationId
      * @param int $pinkId
      * @param int $seckill_id
      * @param int $bargain_id
      * @param bool $test
-     * @param int $isChannel
+     * @param int $isChannel 0, 1, 2 : routine, weixin, weixinh5
      * @param int $shipping_type
      * @param string $real_name
      * @param string $phone
+     * @param int $storeId: 门店ID
      * @return StoreOrder|bool|\think\Model
      * @throws \think\Exception
      * @throws \think\db\exception\DataNotFoundException
@@ -273,7 +275,9 @@ class StoreOrder extends BaseModel
      * @throws \think\exception\DbException
      */
 
-    public static function cacheKeyCreateOrder($uid, $key, $addressId, $payType, $useIntegral = false, $couponId = 0, $mark = '', $combinationId = 0, $pinkId = 0, $seckill_id = 0, $bargain_id = 0, $test = false, $isChannel = 0, $shipping_type = 1, $real_name = '', $phone = '', $storeId = 0)
+    public static function cacheKeyCreateOrder($uid, $key, $addressId, $payType, $useIntegral = false, $couponId = 0, 
+        $mark = '', $combinationId = 0, $pinkId = 0, $seckill_id = 0, $bargain_id = 0, $test = false, $isChannel = 0, 
+        $shipping_type = 1, $real_name = '', $phone = '', $storeId = 0)
     {
         self::beginTrans();
         try {
@@ -282,14 +286,17 @@ class StoreOrder extends BaseModel
             if ($offlinePayStatus == 2) unset(self::$payType['offline']);
             if (!array_key_exists($payType, self::$payType)) return self::setErrorInfo('选择支付方式有误!', true);
             if (self::be(['unique' => $key, 'uid' => $uid])) return self::setErrorInfo('请勿重复提交订单', true);
+            // 用户
             $userInfo = User::getUserInfo($uid);
             if (!$userInfo) return self::setErrorInfo('用户不存在!', true);
+            // 取订单缓存
             $cartGroup = self::getCacheOrderInfo($uid, $key);
             if (!$cartGroup) return self::setErrorInfo('订单已过期,请刷新当前页面!', true);
             $cartInfo = $cartGroup['cartInfo'];
             $priceGroup = $cartGroup['priceGroup'];
             $other = $cartGroup['other'];
             $payPrice = (float)$priceGroup['totalPrice'];
+            // 收货地址
             $addr = UserAddress::where('uid', $uid)->where('id', $addressId)->find();
             if ($payType == 'offline' && sys_config('offline_postage') == 1) {
                 $payPostage = 0;
@@ -1449,7 +1456,7 @@ class StoreOrder extends BaseModel
             if (!$res) throw new \Exception('更新错误');
             unset($orderList, $res, $pages);
             return null;
-        } catch (PDOException $e) {
+        } catch (\PDOException $e) {
             Log::error('未支付自动取消时发生数据库查询错误,错误原因为:' . $e->getMessage());
             throw new \Exception($e->getMessage());
         } catch (\think\Exception $e) {

+ 593 - 0
app/models/store/StoreOrderBatch.php

@@ -0,0 +1,593 @@
+<?php
+/**
+ *
+ * @author: xaboy<365615158@qq.com>
+ * @day: 2017/12/20
+ */
+
+namespace app\models\store;
+
+use crmeb\basic\BaseModel;
+use think\facade\Cache;
+use crmeb\traits\ModelTrait;
+use app\models\system\SystemStore;
+use app\models\user\{
+    User, UserAddress, UserBill, WechatUser
+};
+use crmeb\repositories\{
+    GoodsRepository, PaymentRepositories, OrderRepository, ShortLetterRepositories, UserRepository
+};
+use EasyWeChat\Store\Store;
+
+/**
+ *  正常情況, 一次性下單購物車中多個商品,只產生一個訂單。 支付,積分,發貨,退貨,等等都是基於這一個訂單操作。
+ * 一個明顯的問題是:當此訂單中多個商品不能在一個快遞中發貨時,訂單就變得不易管理
+ * 
+ *  這個類的目的就是。已最小的侵入性來實現:一個訂單中包含多個商品時,自動按照一定的邏輯把這個訂單拆分爲若幹個訂單
+ * 每個訂單中的商品可在同一個快遞中發貨。
+ * 
+ *  有點類似一個電商平臺多個商家的情況,肯定是各個商家派發購物車中屬於自己家的商品。
+ * 
+ *  實現:
+ * 
+ *  單獨建一個表,此表只記錄一個大訂單號(用於支付接口傳遞訂單參數)和關聯的所有子訂單
+ *  支付接口按大訂單號走,發貨等訂單管理按子訂單走。
+ *  這樣有一個問題:子訂單退貨沒法執行三方支付退款(暫定)
+ */
+class StoreOrderBatch extends BaseModel {
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'store_order_batch';
+
+    use ModelTrait;
+
+    /**
+     * 获取订单各项金额
+     * 
+     */
+    public static function getOrderPriceGroup($cartInfo, $addr)
+    {
+        return StoreOrder::getOrderPriceGroup($cartInfo, $addr);
+    }
+
+    /**
+     * 缓存订单信息
+     * @param $uid
+     * @param $cartInfo
+     * @param $priceGroup
+     * @param array $other
+     * @param int $cacheTime
+     * @return string 总订单ID
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     */
+    public static function cacheOrderInfo($uid, $cartInfo, $priceGroup, $other = [], $cacheTime = 600)
+    {
+        $key = md5(time() . $uid);
+        Cache::set('user_order_' . $uid . $key, compact('cartInfo', 'priceGroup', 'other'), $cacheTime);
+        return $key;
+    }
+
+    /**
+     * 获取订单缓存信息
+     * @param $uid
+     * @param $key
+     * @return mixed|null
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     */
+    public static function getCacheOrderInfo($uid, $key)
+    {
+        $cacheName = 'user_order_' . $uid . $key;
+        if (!Cache::has($cacheName)) return null;
+        return Cache::get($cacheName);
+    }
+
+    /**
+     * 生成总订单唯一id
+     * @param $uid 用户uid
+     * @return string
+     */
+    public static function getNewPOrderId()
+    {
+        $prefix = 'wxcn';
+        list($msec, $sec) = explode(' ', microtime());
+        $msectime = number_format((floatval($msec) + floatval($sec)) * 1000, 0, '', '');
+        $porderId = $prefix . $msectime . mt_rand(10000, 99999);
+        if (self::be(['porder_id' => $porderId])) {
+            $porderId = $prefix . $msectime . mt_rand(10000, 99999);
+        }
+        return $porderId;
+    }
+
+    /**
+     * 获取所有子订单
+     * @param string $porderId: porder_id
+     */
+    public static function getAllSubOrders(string $porderId): Array
+    {
+        $subOrders = self::alias('b')->join('StoreOrder o', 'o.id=b.oid')
+            ->where('b.porder_id', $porderId)->select()->toArray();
+        foreach($subOrders as &$subOrder) {
+            $subOrder['cart_id'] = json_decode($subOrder['cart_id'], true);
+        }
+        return $subOrders;
+    }
+
+    /**
+     * 字段累加
+     * @param Array $subOrders
+     * @param Array $field_name_list
+     * @return Array
+     */
+    public static function sumFields(Array $marr, Array $field_name_list): Array
+    {
+        $sums = [];
+        foreach($marr as $row) {
+            foreach($field_name_list as $fname) {
+                $fv = $row[$fname] ?? 0;    // field value
+                $sums[$fname] = (float)bcadd($sums[$fname] ?? 0, $fv, 2);
+            }
+        }
+        return $sums;
+    }
+
+     /**
+     * 微信支付 为 0元时
+     * @param $order_id
+     * @param $uid
+     * @param string $formId
+     * @return bool
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public static function jsPayPriceBatch($porder_id, $uid, $formId = '')
+    {
+        $subOrders = self::getAllSubOrders($porder_id);
+        if (!$subOrders) return self::setErrorInfo('订单不存在!');
+        $fieldValues = self::sumFields($subOrders, ['paid']);
+        if ($fieldValues['paid']) return self::setErrorInfo('该订单已支付!');
+        $userInfo = User::getUserInfo($uid);
+        self::beginTrans();
+        $res1 = $res2 = true;
+        foreach($subOrders as $orderInfo) {
+            $res1 = $res1 && UserBill::expend('购买商品', $uid, 'now_money', 'pay_product', $orderInfo['pay_price'], $orderInfo['id'], $userInfo['now_money'], '微信支付' . floatval($orderInfo['pay_price']) . '元购买商品');
+            $res2 = $res2 && StoreOrder::paySuccess($orderInfo['order_id'], 'weixin', $formId);//微信支付为0时
+        }
+        $res = $res1 && $res2;
+        self::checkTrans($res);
+        return $res;
+    }
+
+    /**
+     * 查找购物车里的所有产品标题
+     * @param $cartId 购物车id
+     * @return bool|string
+     */
+    public static function getProductTitleBatch($subOrders)
+    {
+        $title = '';
+        $cartId = [];
+        foreach($subOrders as $order) {
+            $cartId = array_merge($cartId, $order['cart_id']);
+        }
+        try {
+            $orderCart = StoreOrderCartInfo::where('cart_id', 'in', $cartId)->field('cart_info')->select();
+            foreach ($orderCart as $item) {
+                if (isset($item['cart_info']['productInfo']['store_name'])) {
+                    $title .= $item['cart_info']['productInfo']['store_name'] . '|';
+                }
+            }
+            unset($item);
+            if (!$title) {
+                $productIds = StoreCart::where('id', 'in', $cartId)->column('product_id');
+                $productlist = ($productlist = StoreProduct::getProductField($productIds, 'store_name')) ? $productlist->toArray() : [];
+                foreach ($productlist as $item) {
+                    if (isset($item['store_name'])) $title .= $item['store_name'] . '|';
+                }
+            }
+            if ($title) $title = substr($title, 0, strlen($title) - 1);
+            unset($item);
+        } catch (\Exception $e) {
+        }
+        return $title;
+    }
+
+    /**
+     * 余额支付
+     * @param $order_id
+     * @param $uid
+     * @param string $formId
+     * @return bool
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public static function yuePayBatch($porder_id, $uid, $formId = '')
+    {
+        $porderInfo = self::where('uid', $uid)->where('porder_id', $porder_id)->find();
+        if (!$porderInfo) {
+            return self::setErrorInfo('订单不存在!');
+        }
+        $subOrders = self::getAllSubOrders($porder_id);
+        if (!$subOrders) {
+            return self::setErrorInfo('订单不存在!');
+        }
+        $fieldValues = self::sumFields($subOrders, ['paid']);
+        if ($fieldValues['paid']) {
+            return self::setErrorInfo('该订单已支付!');
+        }
+        $userInfo = User::getUserInfo($uid);
+        if ($userInfo['now_money'] < $porderInfo['total_price'])
+            return self::setErrorInfo(['status' => 'pay_deficiency', 'msg' => '余额不足' . floatval($porderInfo['total_price'])]);
+
+        self::beginTrans();
+        $res1 = $res2 = $res3 = true;
+        foreach($subOrders as $orderInfo) {
+            $res1 = $res1 && (false !== User::bcDec($uid, 'now_money', $orderInfo['pay_price'], 'uid'));
+            $res2 = $res2 && UserBill::expend('购买商品', $uid, 'now_money', 'pay_product', $orderInfo['pay_price'], $orderInfo['id'], $userInfo['now_money'], '余额支付' . floatval($orderInfo['pay_price']) . '元购买商品');
+            $res3 = $res3 && StoreOrder::paySuccess($orderInfo['order_id'], 'yue', $formId);//余额支付成功
+            try {
+                PaymentRepositories::yuePayProduct($userInfo, $orderInfo);
+            } catch (\Exception $e) {
+                self::rollbackTrans();
+                return self::setErrorInfo($e->getMessage());
+            }
+        }
+       
+        $res = $res1 && $res2 && $res3;
+        self::checkTrans($res);
+        return $res;
+    }
+
+    public static function paySuccessBatch($porderId, $paytype = 'weixin', $formId = '')
+    {
+        $subOrders = self::getAllSubOrders($porderId);
+        $res = true;
+        foreach($subOrders as $order) {
+            $res = $res && StoreOrder::paySuccess($order['order_id'], $paytype, $formId);
+        }
+        return $res;
+    }
+
+    /**
+     * 生成订单
+     * @param $uid
+     * @param $key  orderKey 由客户端传入,总订单ID
+     * @param $addressId
+     * @param $payType  weixin / yue
+     * @param bool $useIntegral: 是否使用积分
+     * @param int $couponId
+     * @param string $mark: 订单留言
+     * @param int $combinationId
+     * @param int $pinkId
+     * @param int $seckill_id
+     * @param int $bargain_id
+     * @param bool $test
+     * @param int $isChannel 0, 1, 2 : routine, weixin, weixinh5
+     * @param int $shipping_type
+     * @param string $real_name
+     * @param string $phone
+     * @param int $storeId: 门店ID
+     * @return StoreOrder|bool|\think\Model
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public static function cacheKeyCreateOrderBatch($uid, $key, $addressId, $payType, $useIntegral = false, $couponId = 0, 
+        $mark = '', $combinationId = 0, $pinkId = 0, $seckill_id = 0, $bargain_id = 0, $test = false, $isChannel = 0, 
+        $shipping_type = 1, $real_name = '', $phone = '', $storeId = 0)
+    {
+        StoreOrder::beginTrans();
+        try {
+            $shipping_type = (int)$shipping_type;
+            // 支付方式判断
+            $offlinePayStatus = (int)sys_config('offline_pay_status') ?? (int)2;    //线下支付状态 2 关闭
+            if ($offlinePayStatus == 2) {
+                unset(StoreOrder::$payType['offline']);
+            }
+            if (!array_key_exists($payType, StoreOrder::$payType)) {
+                return self::setErrorInfo('选择支付方式有误!', true);
+            }
+            // 查总订单 sql-1
+            if (self::be(['unique' => $key, 'uid' => $uid])) {
+                return self::setErrorInfo('请勿重复提交订单', true);
+            }
+
+            // 用户信息 sql-2
+            $userInfo = User::getUserInfo($uid);
+            if (!$userInfo) return self::setErrorInfo('用户不存在!', true);
+            // 取订单缓存
+            $cartGroup = self::getCacheOrderInfo($uid, $key);
+            if (!$cartGroup) return self::setErrorInfo('订单已过期,请刷新当前页面!', true);
+            $cartInfo = $cartGroup['cartInfo'];
+            $priceGroup = $cartGroup['priceGroup'];
+            $other = $cartGroup['other'];
+            // $payPrice = (float)$priceGroup['totalPrice'];
+            // 收货地址 sql-3
+            $addr = UserAddress::where('uid', $uid)->where('id', $addressId)->find();
+            // if ($payType == 'offline' && sys_config('offline_postage') == 1) {
+            //     $payPostage = 0;
+            // } else {
+            //     $payPostage = $priceGroup['storePostage'];
+            // }
+            if ($shipping_type === 1) {
+                if (!$test && !$addressId) {
+                    return self::setErrorInfo('请选择收货地址!', true);
+                }
+                // sql-4,5
+                if (!$test && (!UserAddress::be(['uid' => $uid, 'id' => $addressId, 'is_del' => 0]) || !($addressInfo = UserAddress::find($addressId))))
+                    return self::setErrorInfo('地址选择有误!', true);
+            } else {
+                if ((!$real_name || !$phone) && !$test) {
+                    return self::setErrorInfo('请填写姓名和电话', true);
+                }
+                $addressInfo['real_name'] = $real_name;
+                $addressInfo['phone'] = $phone;
+                $addressInfo['province'] = '';
+                $addressInfo['city'] = '';
+                $addressInfo['district'] = '';
+                $addressInfo['detail'] = '';
+            }
+
+            $totalPrice = 0.0;
+            $totalPayPrice = 0.0;
+            $SurplusIntegral = 0;
+            $totalPostage = 0.0;
+            $totalDeductionPrice = 0.0;
+            $totalCouponPrice = 0.0;
+            $counter = 0;
+            $subOrders = [];
+            // 子订单逐个处理
+            foreach ($cartInfo as $ci) {
+                $counter += 1;
+
+                // 一个订单的购物车
+                $subOrderCart = [$ci];
+                
+                $subOrderId = StoreOrder::getNewOrderId();
+                $subPriceGroup = StoreOrder::getOrderPriceGroup($subOrderCart, $addr);
+                if (!$subPriceGroup) {
+                    return false;
+                }
+                $payPrice = (float)$subPriceGroup['totalPrice'];
+                $totalPrice = (float)bcadd($totalPrice, $payPrice, 2);
+
+                $cartIds = [];      // 子订单购物车ID
+                $totalNum = 0;      // 总数量
+                $gainIntegral = 0;  // 总可得积分
+                foreach ($subOrderCart as $cart){
+                    // sql-6 x n
+                    if (!$test && !StoreOrder::checkProductStock($uid, $cart['product_id'], $cart['cart_num'], $cart['product_attr_unique'], $cart['combination_id'], $cart['seckill_id'], $cart['bargain_id'])) {
+                        return false;
+                    }
+                    $cartIds[] = $cart['id'];
+                    $totalNum += $cart['cart_num'];
+                    if (!$seckill_id) $seckill_id = $cart['seckill_id'];
+                    if (!$bargain_id) $bargain_id = $cart['bargain_id'];
+                    if (!$combinationId) $combinationId = $cart['combination_id'];
+                    // 可赚积分
+                    $cartInfoGainIntegral = isset($cart['productInfo']['give_integral']) ? bcmul($cart['cart_num'], $cart['productInfo']['give_integral'], 2) : 0;
+                    // 汇总
+                    $gainIntegral = bcadd($gainIntegral, $cartInfoGainIntegral, 2);
+                } // foreach
+                
+                // 活动
+                $deduction = $seckill_id || $bargain_id || $combinationId;
+                if ($deduction) {
+                    $couponId = 0;
+                    $useIntegral = false;
+                    if (!$test) {
+                        unset(StoreOrder::$payType['offline']);
+                        if (!array_key_exists($payType, StoreOrder::$payType)) {
+                            return self::setErrorInfo('营销产品不能使用线下支付!', true);
+                        } 
+                    }
+                }// 
+
+                //使用优惠劵
+                $res1 = true;
+                if ($couponId) {
+                    $couponInfo = StoreCouponUser::validAddressWhere()->where('id', $couponId)->where('uid', $uid)->find();
+                    if (!$couponInfo) return self::setErrorInfo('选择的优惠劵无效!', true);
+                    $coupons = StoreCouponUser::getUsableCouponList($uid, ['valid' => $subOrderCart], $payPrice);
+                    $flag = false;
+                    foreach ($coupons as $coupon) {
+                        if ($coupon['id'] == $couponId) {
+                            $flag = true;
+                            continue;
+                        }
+                    }
+                    if (!$flag)
+                        return self::setErrorInfo('不满足优惠劵的使用条件!', true);
+                    $payPrice = (float)bcsub($payPrice, $couponInfo['coupon_price'], 2);
+                    $res1 = StoreCouponUser::useCoupon($couponId);
+                    $couponPrice = $couponInfo['coupon_price'];
+                } else {
+                    $couponId = 0;
+                    $couponPrice = 0;
+                }
+                if (!$res1) {
+                    return self::setErrorInfo('使用优惠劵失败!', true);
+                }
+                $totalCouponPrice = (float)bcadd($totalCouponPrice, $couponPrice, 2);
+
+                //$shipping_type = 1 快递发货 $shipping_type = 2 门店自提
+                $store_self_mention = sys_config('store_self_mention') ?? 0;
+                if (!$store_self_mention) $shipping_type = 1;
+                $payPostage = 0;
+                if ($shipping_type === 1) {
+                    //是否包邮
+                    if ((isset($other['offlinePostage']) && $other['offlinePostage'] && $payType == 'offline')) {
+                        $payPostage = 0;
+                    }
+                    $payPrice = (float)bcadd($payPrice, $payPostage, 2);
+                } else if ($shipping_type === 2) {
+                    //门店自提没有邮费支付
+                    $subPriceGroup['storePostage'] = 0;
+                    $payPostage = 0;
+                    if (!$storeId && !$test) {
+                        return self::setErrorInfo('请选择门店', true);
+                    }
+                }
+                $totalPostage = (float)bcadd($totalPostage, $payPostage, 2);
+
+                //积分抵扣
+                $res2 = true;
+                $usedIntegral = 0.0;
+                if ($useIntegral && $userInfo['integral'] > 0) {
+                    $deductionPrice = (float)bcmul($userInfo['integral'], $other['integralRatio'], 2);
+                    if ($deductionPrice < $payPrice) {
+                        $payPrice = bcsub($payPrice, $deductionPrice, 2);
+                        $usedIntegral = $userInfo['integral'];
+                        $SurplusIntegral = 0;
+                        // sql
+                        $res2 = false !== User::edit(['integral' => 0], $userInfo['uid'], 'uid');
+                    } else {
+                        $deductionPrice = $payPrice;
+                        $usedIntegral = (float)bcdiv($payPrice, $other['integralRatio'], 2);
+                        $SurplusIntegral = bcsub($userInfo['integral'], $usedIntegral, 2);
+                        $res2 = false !== User::bcDec($userInfo['uid'], 'integral', $usedIntegral, 'uid');
+                        $payPrice = 0;
+                    }
+                    $res2 = $res2 && false != UserBill::expend('积分抵扣', $uid, 'integral', 'deduction', $usedIntegral, $subOrderId, $userInfo['integral'], '购买商品使用' . floatval($usedIntegral) . '积分抵扣' . floatval($deductionPrice) . '元');
+                } else {
+                    $deductionPrice = 0;
+                    $usedIntegral = 0;
+                }
+                if (!$res2) {
+                    return self::setErrorInfo('使用积分抵扣失败!', true);
+                }
+                // fix at runtime
+                $userInfo['integral'] = (float)bcsub($userInfo['integral'], $usedIntegral, 2);
+                if ($userInfo['integral'] < 0) {
+                    $userInfo['integral'] = 0;
+                }
+                
+                $totalDeductionPrice = (float)bcadd($totalDeductionPrice, $deductionPrice, 2);
+                $totalPayPrice = (float)bcadd($totalPayPrice, $payPrice, 2);
+
+                if ($test) {
+                    continue;
+                }
+
+                $unique = mb_substr($key, 0, mb_strlen($key)-3) . $counter;
+                // 增加子订单
+                $orderInfo = [
+                    'uid' => $uid,
+                    'order_id' => $test ? 0 : $subOrderId,
+                    'real_name' => $addressInfo['real_name'],
+                    'user_phone' => $addressInfo['phone'],
+                    'user_address' => $addressInfo['province'] . ' ' . $addressInfo['city'] . ' ' . $addressInfo['district'] . ' ' . $addressInfo['detail'],
+                    'cart_id' => $cartIds,
+                    'total_num' => $totalNum,
+                    'total_price' => $subPriceGroup['totalPrice'],
+                    'total_postage' => $subPriceGroup['storePostage'],
+                    'coupon_id' => $couponId,
+                    'coupon_price' => $couponPrice,
+                    'pay_price' => $payPrice,
+                    'pay_postage' => $payPostage,
+                    'deduction_price' => $deductionPrice,
+                    'paid' => 0,
+                    'pay_type' => $payType,
+                    'use_integral' => $usedIntegral,
+                    'gain_integral' => $gainIntegral,
+                    'mark' => htmlspecialchars($mark),
+                    'combination_id' => $combinationId,
+                    'pink_id' => $pinkId,
+                    'seckill_id' => $seckill_id,
+                    'bargain_id' => $bargain_id,
+                    'cost' => $subPriceGroup['costPrice'],
+                    'is_channel' => $isChannel,
+                    'add_time' => time(),
+                    'unique' => $unique,
+                    'shipping_type' => $shipping_type,
+                ];
+                if ($shipping_type === 2) {
+                    $orderInfo['verify_code'] = StoreOrder::getStoreCode();
+                    $orderInfo['store_id'] = SystemStore::getStoreDispose($storeId, 'id');
+                    if (!$orderInfo['store_id']) return self::setErrorInfo('暂无门店无法选择门店自提!', true);
+                }
+                $order = StoreOrder::create($orderInfo);
+                if (!$order) {
+                    return self::setErrorInfo('订单生成失败!', true);
+                }
+                $res5 = true;
+                foreach ($subOrderCart as $cart) {
+                    //减库存加销量
+                    if ($combinationId) {
+                        $res5 = $res5 && StoreCombination::decCombinationStock($cart['cart_num'], $combinationId, isset($cart['productInfo']['attrInfo']) ? $cart['productInfo']['attrInfo']['unique'] : '');
+                    } else if ($seckill_id) {
+                        $res5 = $res5 && StoreSeckill::decSeckillStock($cart['cart_num'], $seckill_id, isset($cart['productInfo']['attrInfo']) ? $cart['productInfo']['attrInfo']['unique'] : '');
+                    } else if ($bargain_id) {
+                        $res5 = $res5 && StoreBargain::decBargainStock($cart['cart_num'], $bargain_id, isset($cart['productInfo']['attrInfo']) ? $cart['productInfo']['attrInfo']['unique'] : '');
+                    } else {
+                        $res5 = $res5 && StoreProduct::decProductStock($cart['cart_num'], $cart['productInfo']['id'], isset($cart['productInfo']['attrInfo']) ? $cart['productInfo']['attrInfo']['unique'] : '');
+                    }
+                }
+                //保存购物车商品信息
+                $res4 = false !== StoreOrderCartInfo::setCartInfo($order['id'], $subOrderCart);
+                //购物车状态修改
+                $res6 = false !== StoreCart::where('id', 'IN', $cartIds)->update(['is_pay' => 1]);
+                if (!$res4 || !$res5 || !$res6) {
+                    return self::setErrorInfo('订单生成失败!', true);
+                }
+                StoreOrderStatus::status($order['id'], 'cache_key_create_order', '订单生成');
+                $subOrders[] = $order;
+            } // foreach 
+            
+            if ($totalPayPrice <= 0) $totalPayPrice = 0;
+
+            if ($test) {
+                StoreOrder::rollbackTrans();
+                return [
+                    'total_price' => $totalPrice,
+                    'pay_price' => $totalPayPrice,
+                    'pay_postage' => $totalPostage,
+                    'coupon_price' => $totalCouponPrice,
+                    'deduction_price' => $totalDeductionPrice,
+                    'SurplusIntegral' => $SurplusIntegral,
+                ];
+            }
+
+            $pOrderId = self::getNewPOrderId();
+            foreach($subOrders as $subOrder) {
+                $porderInfo = [
+                    'porder_id' => $pOrderId,
+                    'oid' => $subOrder['id'],
+                    'total_price' => $totalPayPrice,
+                    'unique' => $key,
+                    'uid' => $uid,
+                ];
+                $porder = self::create($porderInfo);
+                if(!$porder) {
+                    return self::setErrorInfo('订单生成失败!', true);
+                }
+            }
+            //自动设置默认地址
+            if (count($subOrders)) {
+                UserRepository::storeProductOrderCreateEbApi($subOrders[0], compact('cartInfo', 'addressId'));
+            }
+            StoreOrder::clearCacheOrderInfo($uid, $key);
+            StoreOrder::commitTrans();
+            return $pOrderId;
+        } catch (\PDOException $e) {
+            StoreOrder::rollbackTrans();
+            return self::setErrorInfo('生成订单时SQL执行错误错误原因:' . $e->getMessage());
+        } catch (\Exception $e) {
+            StoreOrder::rollbackTrans();
+            throw $e;
+            return self::setErrorInfo('生成订单时系统错误错误原因:' . $e->getMessage());
+        }
+    }
+}

+ 82 - 1
crmeb/repositories/OrderRepository.php

@@ -7,6 +7,7 @@ use app\models\user\User;
 use app\models\user\UserBill;
 use app\models\user\WechatUser;
 use app\admin\model\order\StoreOrder as AdminStoreOrder;
+use app\models\store\StoreOrderBatch;
 use crmeb\services\MiniProgramService;
 use crmeb\services\WechatService;
 
@@ -42,6 +43,33 @@ class OrderRepository
         return MiniProgramService::jsPay($openid, $orderInfo['order_id'], $orderInfo['pay_price'], 'product', StoreOrder::getSubstrUTf8($site_name . ' - ' . $bodyContent, 30));
     }
 
+        /**
+     * TODO 小程序JS支付
+     * @param $orderId
+     * @param string $field
+     * @return array|string
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public static function jsPayBatch($porderId, $field = 'porder_id')
+    {
+        if (is_string($porderId))
+            $porderInfo = StoreOrderBatch::where($field, $porderId)->find();
+        else
+            $porderInfo = $porderId;
+        $subOrders = StoreOrderBatch::getAllSubOrders($porderId);
+        if (!$subOrders) exception('支付订单不存在!');
+        $fieldValues = StoreOrderBatch::sumFields($subOrders, ['paid', 'pay_price']);
+        if ($fieldValues['paid']) exception('支付已支付!');
+        if ($fieldValues['pay_price'] <= 0) exception('该支付无需支付!');
+        $openid = WechatUser::getOpenId($porderInfo['uid']);
+        $bodyContent = StoreOrderBatch::getProductTitleBatch($subOrders);
+        $site_name = sys_config('site_name');
+        if (!$bodyContent && !$site_name) exception('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称');
+        return MiniProgramService::jsPay($openid, $porderInfo['porder_id'], $fieldValues['pay_price'], 'product', StoreOrder::getSubstrUTf8($site_name . ' - ' . $bodyContent, 30));
+    }
+
     /**
      * 微信公众号JS支付
      * @param $orderId
@@ -64,7 +92,35 @@ class OrderRepository
         $bodyContent = StoreOrder::getProductTitle($orderInfo['cart_id']);
         $site_name = sys_config('site_name');
         if (!$bodyContent && !$site_name) exception('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称');
-        return WechatService::jsPay($openid, $orderInfo['order_id'], $orderInfo['pay_price'], 'product', StoreOrder::getSubstrUTf8($site_name . ' - ' . $bodyContent, 30));
+        return WechatService::jsPay($openid, $orderInfo['porder_id'], $orderInfo['total_price'], 'product', StoreOrder::getSubstrUTf8($site_name . ' - ' . $bodyContent, 30));
+    }
+
+    /**
+     * 微信公众号JS支付
+     * @param $orderId
+     * @param string $field
+     * @return array|string
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public static function wxPayBatch($porderId, $field = 'porder_id')
+    {
+        if (is_string($porderId))
+            $porderInfo = StoreOrderBatch::where($field, $porderId)->find();
+        else
+            $porderInfo = $porderId;
+        
+        $subOrders = StoreOrderBatch::getAllSubOrders($porderId);
+        if (!$subOrders) exception('支付订单不存在!');
+        $fieldValues = StoreOrderBatch::sumFields($subOrders, ['paid', 'pay_price']);
+        if ($fieldValues['paid']) exception('支付已支付!');
+        if ($fieldValues['pay_price'] <= 0) exception('该支付无需支付!');
+        $openid = WechatUser::uidToOpenid($porderInfo['uid'], 'openid');
+        $bodyContent = StoreOrderBatch::getProductTitleBatch($subOrders);
+        $site_name = sys_config('site_name');
+        if (!$bodyContent && !$site_name) exception('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称');
+        return WechatService::jsPay($openid, $porderInfo['porder_id'], $porderInfo['total_price'], 'product', StoreOrder::getSubstrUTf8($site_name . ' - ' . $bodyContent, 30));
     }
 
     /**
@@ -91,6 +147,31 @@ class OrderRepository
         return WechatService::paymentPrepare(null, $orderInfo['order_id'], $orderInfo['pay_price'], 'product', StoreOrder::getSubstrUTf8($site_name . ' - ' . $bodyContent, 30), '', 'MWEB');
     }
 
+    /**
+     * 微信h5支付
+     * @param $orderId
+     * @param string $field
+     * @return array|string
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public static function h5PayBatch($porderId, $field = 'order_id')
+    {
+        if (is_string($porderId))
+            $porderInfo = StoreOrderBatch::where($field, $porderId)->find();
+        else
+            $porderInfo = $porderId;
+        $subOrders = StoreOrderBatch::getAllSubOrders($porderId);
+        if (!$subOrders) exception('支付订单不存在!');
+        $fieldValues = StoreOrderBatch::sumFields($subOrders, ['paid', 'pay_price']);
+        if ($fieldValues['paid']) exception('支付已支付!');
+        if ($fieldValues['pay_price'] <= 0) exception('该支付无需支付!');
+        $bodyContent = StoreOrderBatch::getProductTitleBatch($subOrders);
+        $site_name = sys_config('site_name');
+        if (!$bodyContent && !$site_name) exception('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称');
+        return WechatService::paymentPrepare(null, $porderInfo['porder_id'], $porderInfo['total_price'], 'product', StoreOrder::getSubstrUTf8($site_name . ' - ' . $bodyContent, 30), '', 'MWEB');
+    }
     /**
      * 用户确认收货
      * @param $order

+ 9 - 2
crmeb/repositories/PaymentRepositories.php

@@ -3,6 +3,7 @@
 namespace crmeb\repositories;
 
 use app\models\store\StoreOrder;
+use app\models\store\StoreOrderBatch;
 use app\models\user\UserRecharge;
 
 /**
@@ -51,8 +52,14 @@ class PaymentRepositories
     public static function wechatProduct(string $order_id = null)
     {
         try {
-            if (StoreOrder::be(['order_id' => $order_id, 'paid' => 1])) return true;
-            return StoreOrder::paySuccess($order_id);
+            $first4 = mb_substr($order_id, 0, 4);
+            if ($first4 == 'wxcn') {
+                return StoreOrderBatch::paySuccessBatch($order_id);
+            } else {
+                // single version commented
+                if (StoreOrder::be(['order_id' => $order_id, 'paid' => 1])) return true;
+                return StoreOrder::paySuccess($order_id);
+            }
         } catch (\Exception $e) {
             return false;
         }

+ 0 - 0
docs/scripts/mt/funcs/__init__.py


BIN
docs/scripts/mt/funcs/__pycache__/config.cpython-39.pyc


+ 9 - 32
docs/scripts/mt/funcs/products.py

@@ -4,12 +4,14 @@ import json
 import time
 import pymysql
 
+from tw.config import conn, table
 from datetime import datetime
 from random import randint
 from collections import namedtuple
 from openpyxl import Workbook
 from openpyxl.styles import PatternFill, colors
 
+
 '''
 twong 辅助工具,扫描商品表,及其关联表
 
@@ -22,19 +24,6 @@ twong 辅助工具,扫描商品表,及其关联表
 
 SHOW_MAX_DAYS = 800  # 超过天数自动下架
 
-# database config type
-DbConf = namedtuple('DbConf', 'type, host, port, username, password, database, prefix, charset')
-
-conf = DbConf('mysql', '127.0.0.1', 3306, 'toor', '123456', 'twong', 'eb_', 'utf8mb4')
-
-conn = pymysql.connect(
-    host=conf.host,
-    port=conf.port,
-    user=conf.username,
-    password=conf.password,
-    database=conf.database,
-    charset=conf.charset,
-)
 
 NOW = int(time.time())      # 当前时间戳
 
@@ -62,18 +51,6 @@ for cols in sheet.iter_cols(min_col=1,max_col=8,min_row=1,max_row=1):
         cell.fill = PatternFill('solid', fgColor='C5C1AA')
 sheet['A1'].fill = PatternFill('solid', fgColor='C5C1AA')
 
-# 初始化表名
-tables = {
-    'product': 'store_product',
-    'product_attr_result': 'store_product_attr_result',
-    'product_attr_value': 'store_product_attr_value',
-    'user': 'user',
-    'order': 'store_order',
-}
-
-for k, v in tables.items():
-    tables[k] = conf.prefix + v
-
 
 def updateResult(productId, result, skuName, stock=0, price=0, otPrice=0, brokerage=0, brokerage2=0):
     '''
@@ -160,7 +137,7 @@ with conn:
     with conn.cursor() as cursor:
         sql = '''SELECT id, store_name, image, slider_image, spec_type, price, cost, stock, ficti, add_time
             FROM {} 
-            WHERE is_show=1 AND is_del=0 '''.format(tables['product'])
+            WHERE is_show=1 AND is_del=0 '''.format(table('store_product'))
         cursor.execute(sql)
         products = cursor.fetchall()
         errors = {}
@@ -181,7 +158,7 @@ with conn:
 
             sq2 = '''SELECT result 
                 FROM {}
-                WHERE product_id={}'''.format(tables['product_attr_result'], productId)
+                WHERE product_id={}'''.format(table('store_product_attr_result'), productId)
             cursor.execute(sq2)
             result = cursor.fetchone()
             if not result or len(result) <= 0:
@@ -194,7 +171,7 @@ with conn:
 
             sq3 = '''SELECT suk, stock, sales, price, cost, ot_price, brokerage, brokerage_two, `unique`
                 FROM {}
-                WHERE product_id={}'''.format(tables['product_attr_value'], productId)
+                WHERE product_id={}'''.format(table('store_product_attr_value'), productId)
             cursor.execute(sq3)
             values = cursor.fetchall()
 
@@ -209,7 +186,7 @@ with conn:
             # 上架超过一定时间自动下架,并将 add_time 改为当前日期
             days = int( (NOW - addTime) / 86400 )
             if days > SHOW_MAX_DAYS:
-                sql = '''UPDATE {} SET is_show=0, add_time={} WHERE id={}'''.format(tables['product'], NOW, productId)
+                sql = '''UPDATE {} SET is_show=0, add_time={} WHERE id={}'''.format(table('store_product'), NOW, productId)
                 with conn.cursor() as cs:
                     cs.execute(sql)
                     print('{}-{} 过期{}天 已启动下架'.format(productId, productName, days))
@@ -292,7 +269,7 @@ with conn:
                     sparts = ','.join(parts)
                     sql = ''' UPDATE {} 
                         SET {}, brokerage=0, brokerage_two=0
-                        WHERE `unique`='{}' '''.format(tables['product_attr_value'], sparts, unique)
+                        WHERE `unique`='{}' '''.format(table('store_product_attr_value'), sparts, unique)
                     with conn.cursor() as cs:
                         cs.execute(sql)
                         print('{}-{}-{} 自动修改库存名称等: sql={}'.format(productId, productName, skuName, sql))
@@ -303,7 +280,7 @@ with conn:
             if chResult:
                 sql = '''UPDATE {}
                     SET result='{}'
-                    WHERE product_id={}'''.format(tables['product_attr_result'], json.dumps(result, ensure_ascii=False), productId)
+                    WHERE product_id={}'''.format(table('store_product_attr_result'), json.dumps(result, ensure_ascii=False), productId)
 
                 with conn.cursor() as cs:
                     cs.execute(sql)
@@ -320,7 +297,7 @@ with conn:
                 sparts = ','.join(parts)
                 sql = '''UPDATE {} 
                 SET {}
-                WHERE id={}'''.format(tables['product'], sparts, productId)
+                WHERE id={}'''.format(table('store_product'), sparts, productId)
                 with conn.cursor() as cs:
                     cs.execute(sql)
                     print('{}-{} 更改虚拟销量和库存 {}'.format(productId, productName, sparts))

+ 35 - 0
docs/scripts/mt/funcs/remove_order.py

@@ -0,0 +1,35 @@
+from tw.config import conn, table
+
+
+def clear_orders():
+    sqls = [
+        'DELETE FROM {}'.format(table('store_order_batch')),
+        'DELETE FROM {}'.format(table('store_order')),
+        'DELETE FROM {}'.format(table('store_order_cart_info')),
+        'DELETE FROM {}'.format(table('store_order_status')),
+        'DELETE FROM {}'.format(table('user_bill')),
+    ]
+    with conn:
+        with conn.cursor() as cs:
+            try:
+                for sql in sqls:
+                    print(sql)
+                    cs.execute(sql)
+                conn.commit()
+            except Exception as e:
+                print(e)
+
+
+def remove_order(orderId:list) -> bool:
+    sql1 = ''' DELETE FROM {} WHERE id={}'''.format(table('store_order'), orderId)
+    sql2 = ''' DELETE FROM {} WHERE oid={}'''.format(table('store_order_cart_info'), orderId)
+    sql3 = ''' DELETE FROM {} WHERE oid={}'''.format(table('store_status'), orderId)
+
+
+
+def remove_porder(porder):
+    pass
+
+
+if __name__ == '__main__':
+    clear_orders()

+ 0 - 0
docs/scripts/mt/funcs/tw/__init__.py


BIN
docs/scripts/mt/funcs/tw/__pycache__/__init__.cpython-39.pyc


BIN
docs/scripts/mt/funcs/tw/__pycache__/config.cpython-39.pyc


+ 22 - 0
docs/scripts/mt/funcs/tw/config.py

@@ -0,0 +1,22 @@
+import pymysql
+
+from collections import namedtuple
+
+
+# database config type
+DbConf = namedtuple('DbConf', 'type, host, port, username, password, database, prefix, charset')
+
+conf = DbConf('mysql', '127.0.0.1', 3306, 'toor', '123456', 'twong', 'eb_', 'utf8mb4')
+
+conn = pymysql.connect(
+    host=conf.host,
+    port=conf.port,
+    user=conf.username,
+    password=conf.password,
+    database=conf.database,
+    charset=conf.charset,
+)
+
+# 表名
+def table(name:str) -> str:
+    return conf.prefix + name

+ 42 - 0
docs/已有协议分析/address.md

@@ -0,0 +1,42 @@
+# address
+
+## GET address/default
+
+returns
+```json
+{
+  "status": 200,
+  "msg": "ok",
+  "data": {
+    "id": 2,
+    "real_name": "fd",
+    "phone": "13222224444",
+    "province": "北京市",
+    "city": "北京市",
+    "district": "东城区",
+    "detail": "dfdf",
+    "is_default": 1
+  }
+}
+```
+## GET address/list?page=1&limit=5
+
+returns
+```json
+{
+  "status": 200,
+  "msg": "ok",
+  "data": [
+    {
+      "id": 2,
+      "real_name": "fd",
+      "phone": "13222224444",
+      "province": "北京市",
+      "city": "北京市",
+      "district": "东城区",
+      "detail": "dfdf",
+      "is_default": 1
+    }
+  ]
+}
+```

+ 24 - 0
docs/已有协议分析/cart.md

@@ -0,0 +1,24 @@
+# 购物车
+
+## POST cart/add
+
+params
+```json
+{
+    "cartNum": 1,
+    "new": 1,
+    "productId": "807",
+    "uniqueId": "cfd3efef"
+}
+```
+
+returns
+```json
+{
+  "status": 200,
+  "msg": "ok",
+  "data": {
+    "cartId": "38"
+  }
+}
+```

+ 18 - 0
public/install/crmeb.sql

@@ -1014,6 +1014,24 @@ CREATE TABLE IF NOT EXISTS `eb_store_coupon_user` (
 
 -- --------------------------------------------------------
 
+--
+-- eb_store_order_batch
+--
+
+CREATE TABLE `eb_store_order_batch` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '总订单ID',
+  `porder_id` varchar(32) NOT NULL COMMENT '总订单号 主要用于关联和支付',
+  `oid` int(11) unsigned NOT NULL COMMENT '子订单号,关联到 store_order 表',
+  `total_price` decimal(8,2) unsigned NOT NULL DEFAULT 0.00 COMMENT '订单总价',
+  `unique` char(32) NOT NULL COMMENT '唯一id(md5加密)类似id',
+  `uid` int(11) unsigned NOT NULL COMMENT '用户id,主要用于过滤',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE KEY `poid` (`porder_id`,`oid`) USING BTREE,
+  KEY `unique` (`unique`) USING BTREE,
+  KEY `uid` (`uid`) USING BTREE
+) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COMMENT='父级订单表' AUTO_INCREMENT=1;
+
+-- --------------------------------------------------------
 --
 -- 表的结构 `eb_store_order`
 --

+ 3 - 0
route/api/route.php

@@ -116,10 +116,13 @@ Route::group(function () {
     //订单类
     Route::post('order/confirm', 'order.StoreOrderController/confirm')->name('orderConfirm'); //订单确认
     Route::post('order/computed/:key', 'order.StoreOrderController/computedOrder')->name('computedOrder'); //计算订单金额
+    Route::post('order/v2/computed/:key', 'order.StoreOrderController/computedOrder_v2')->name('computedOrder'); //计算订单金额
     Route::post('order/create/:key', 'order.StoreOrderController/create')->name('orderCreate'); //订单创建
+    Route::post('order/v2/create/:key', 'order.StoreOrderController/create_v2')->name('orderCreate'); //订单创建
     Route::get('order/data', 'order.StoreOrderController/data')->name('orderData'); //订单统计数据
     Route::get('order/list', 'order.StoreOrderController/lst')->name('orderList'); //订单列表
     Route::get('order/detail/:uni', 'order.StoreOrderController/detail')->name('orderDetail'); //订单详情
+    Route::get('order/v2/detail/:uni', 'order.StoreOrderController/detail_v2')->name('orderDetail'); //订单详情
     Route::get('order/refund/reason', 'order.StoreOrderController/refund_reason')->name('orderRefundReason'); //订单退款理由
     Route::post('order/refund/verify', 'order.StoreOrderController/refund_verify')->name('orderRefundVerify'); //订单退款审核
     Route::post('order/take', 'order.StoreOrderController/take')->name('orderTake'); //订单收货