currentStore($request); $level = $store->level_id > 0 ? $store->level : null; if ($level === null) { throw new RepositoryException('门店未设置客户等级,无法下单,请联系客服'); } $items = $request->validated('items'); $remark = (string) ($request->validated('remark') ?? ''); $order = DB::transaction(function () use ($store, $level, $items, $remark) { $productIds = array_map(static fn ($row) => (int) $row['product_id'], $items); $products = ProductModel::query() ->where('status', ProductModel::STATUS_ON) ->whereIn('id', $productIds) ->get() ->keyBy('id'); $totalQuantity = '0'; $totalAmount = '0'; $now = now(); $rows = []; foreach ($items as $row) { $productId = (int) $row['product_id']; $product = $products->get($productId); if ($product === null) { throw new RepositoryException('存在已下架或不存在的商品,请刷新后重试'); } // 实际价(按门店等级上浮比例换算,$products 已含 cost_price) $price = CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent); $quantity = (string) $row['quantity']; $amount = bcmul($price, $quantity, 2); $totalQuantity = bcadd($totalQuantity, $quantity, 2); $totalAmount = bcadd($totalAmount, $amount, 2); $rows[] = [ 'store_id' => $store->id, 'product_id' => $productId, 'category_id' => (int) $product->category_id, 'supplier_id' => (int) $product->supplier_id, 'product_name' => $product->name, 'product_spec' => $product->spec, 'unit' => (string) $product->unit, 'price' => $price, 'image_ids' => implode(',', (array) $product->image_ids), 'content' => (string) $product->content, 'shelf_life' => (int) $product->shelf_life, 'quantity' => $quantity, 'weight' => 0, 'amount' => $amount, 'cost_price' => (string) $product->cost_price, 'remark' => '', 'created_at' => $now, 'updated_at' => $now, ]; } $order = StoreOrderModel::create([ 'order_no' => app(BillNumberService::class)->make('SO'), 'store_id' => $store->id, 'order_date' => $now->toDateString(), 'total_quantity' => $totalQuantity, 'total_weight' => 0, 'total_amount' => $totalAmount, 'status' => StoreOrderModel::STATUS_PENDING, 'remark' => $remark, ]); foreach ($rows as &$itemRow) { $itemRow['order_id'] = $order->id; } StoreOrderItemModel::insert($rows); // 下单成功后自动清空购物车中已下单的商品 CartModel::query() ->where('store_id', $store->id) ->whereIn('product_id', $productIds) ->delete(); return $order; }); return $this->success([ 'id' => $order->id, 'order_no' => $order->order_no, 'total_amount' => $order->total_amount, ], '下单成功'); } /** * 历史订单:当前门店强制过滤 * ?status= 状态筛选;?start_date=&end_date= 订货日期区间(配合 /order/summary 周期下钻); * ?page=&pageSize= 分页(pageSize 上限 50) * 行数据附带 status_name / can_cancel / 商品预览(前 3 条明细)/ item_count,完整明细走详情接口 */ #[GetRoute('/order', authorize: true)] public function index(Request $request): JsonResponse { $params = $request->validate([ 'status' => 'nullable|integer|in:0,1,2,3,4,9', 'start_date' => 'nullable|date_format:Y-m-d', 'end_date' => 'nullable|date_format:Y-m-d|after_or_equal:start_date', 'page' => 'nullable|integer|min:1', 'pageSize' => 'nullable|integer|min:1|max:50', ], [ 'status.in' => '订单状态不正确', 'start_date.date_format' => '开始日期格式为 Y-m-d', 'end_date.date_format' => '结束日期格式为 Y-m-d', 'end_date.after_or_equal' => '结束日期不能早于开始日期', 'pageSize.max' => '每页数量最大 50', ]); $store = $this->currentStore($request); $query = StoreOrderModel::query() ->where('store_id', $store->id) ->select([ 'id', 'order_no', 'order_date', 'total_quantity', 'total_weight', 'total_amount', 'status', 'remark', 'purchase_id', 'bill_id', 'created_at', ]) ->with(['items' => static fn ($itemsQuery) => $itemsQuery ->select(['id', 'order_id', 'product_name', 'product_spec', 'quantity', 'unit', 'image_ids']) ->orderBy('id')]); if (isset($params['status'])) { $query->where('status', (int) $params['status']); } if (! empty($params['start_date'])) { $query->whereDate('order_date', '>=', $params['start_date']); } if (! empty($params['end_date'])) { $query->whereDate('order_date', '<=', $params['end_date']); } $paginator = $query->orderBy('order_date', 'desc') ->orderBy('id', 'desc') ->paginate((int) ($params['pageSize'] ?? 10)); // 商品预览(每单前 3 条明细):首图跨订单一次性批量解析 $previewMap = []; $flatItems = []; foreach ($paginator->getCollection() as $order) { foreach ($order->items->take(3) as $item) { $flatItems[] = [ 'order_id' => $order->id, 'product_name' => $item->product_name, 'product_spec' => $item->product_spec, 'quantity' => $item->quantity, 'unit' => $item->unit, 'image_ids' => (array) $item->image_ids, ]; } } app(ItemImageResolver::class)->resolve($flatItems); foreach ($flatItems as $flatItem) { $orderId = $flatItem['order_id']; unset($flatItem['order_id']); $previewMap[$orderId][] = $flatItem; } $paginator->getCollection()->transform( static fn (StoreOrderModel $order): array => [ 'id' => $order->id, 'order_no' => $order->order_no, 'order_date' => $order->order_date->toDateString(), 'status' => $order->status, 'status_name' => StoreOrderModel::STATUS_NAMES[$order->status] ?? '', 'can_cancel' => $order->status === StoreOrderModel::STATUS_PENDING, 'total_quantity' => $order->total_quantity, 'total_weight' => $order->total_weight, 'total_amount' => $order->total_amount, 'remark' => $order->remark, 'purchase_id' => $order->purchase_id, 'bill_id' => $order->bill_id, 'created_at' => $order->created_at?->toDateTimeString(), 'item_count' => $order->items->count(), 'items' => $previewMap[$order->id] ?? [], ] ); return $this->success($paginator->toArray()); } /** * 按周期聚合金额/数量:?period=day|week|month(分组列表,period_label 可作下钻查询参数) */ #[GetRoute('/order/summary', authorize: true)] public function summary(Request $request): JsonResponse { $period = (string) $request->query('period', 'month'); if (! in_array($period, ['day', 'week', 'month'], true)) { throw new RepositoryException('period 参数只能是 day/week/month'); } $store = $this->currentStore($request); // 按数据库方言选择周期分组表达式(生产 MySQL / 测试 SQLite) $driver = DB::connection()->getDriverName(); if ($driver === 'sqlite') { $format = match ($period) { 'day' => '%Y-%m-%d', 'week' => '%Y-W%W', default => '%Y-%m', }; $labelExpr = "strftime('{$format}', order_date)"; } else { $format = match ($period) { 'day' => '%Y-%m-%d', 'week' => '%x-W%v', default => '%Y-%m', }; $labelExpr = "DATE_FORMAT(order_date, '{$format}')"; } $rows = StoreOrderModel::query() ->where('store_id', $store->id) ->where('status', '<>', StoreOrderModel::STATUS_CANCELLED) ->selectRaw("{$labelExpr} as period_label") ->selectRaw('SUM(total_amount) as total_amount, SUM(total_quantity) as total_quantity, COUNT(*) as order_count') ->groupBy('period_label') ->orderByDesc('period_label') ->limit(50) ->get() ->toArray(); return $this->success(['period' => $period, 'groups' => $rows]); } /** 订单详情(校验归属:仅能查看本店订单) */ #[GetRoute('/order/{id}', authorize: true, where: ['id' => '[0-9]+'])] public function detail(int $id, Request $request): JsonResponse { $store = $this->currentStore($request); $order = StoreOrderModel::with('items') ->where('store_id', $store->id) ->find($id); if ($order === null) { throw new RepositoryException('订单不存在'); } return $this->success($order->toArray()); } /** 取消订单(仅待汇总可取消) */ #[PutRoute('/order/{id}/cancel', authorize: true, where: ['id' => '[0-9]+'])] public function cancel(int $id, Request $request): JsonResponse { $store = $this->currentStore($request); $order = StoreOrderModel::where('store_id', $store->id)->find($id); if ($order === null) { throw new RepositoryException('订单不存在'); } if ($order->status !== StoreOrderModel::STATUS_PENDING) { throw new RepositoryException('仅待汇总的订单可以取消'); } $order->status = StoreOrderModel::STATUS_CANCELLED; $order->save(); return $this->success([], '订单已取消'); } }