$stores 按门店提交的配送费/周转筐/托盘数量 * @param int $operatorId 生成人(后台系统用户ID) * @return BillModel[] 生成的账单列表 * @throws Throwable */ public function generate(PurchaseOrderModel $purchase, array $stores, int $operatorId): array { return DB::transaction(function () use ($purchase, $stores, $operatorId) { // 1. 行锁采购单全部有效订单(并发防护),按门店分组 $orders = StoreOrderModel::query() ->where('purchase_id', $purchase->id) ->whereNull('deleted_at') ->lockForUpdate() ->get(); if ($orders->isEmpty()) { throw new RepositoryException('采购单下无门店订单,无法生成账单'); } $ordersByStore = $orders->groupBy('store_id'); // 提交的门店必须完整覆盖采购单门店,避免漏门店造成订单未入账 $submitted = []; foreach ($stores as $row) { $submitted[(int) $row['store_id']] = $row; } $missing = array_diff(array_map('intval', $ordersByStore->keys()->all()), array_keys($submitted)); if ($missing !== []) { throw new RepositoryException('提交不完整,缺少门店账单信息:' . implode('、', $missing)); } if (array_diff(array_keys($submitted), array_map('intval', $ordersByStore->keys()->all())) !== []) { throw new RepositoryException('包含不属于该采购单的门店'); } // 2. 防重复生成:任一门店已出账则整批拒绝 $billedStoreIds = BillModel::query() ->where('purchase_id', $purchase->id) ->pluck('store_id') ->map(static fn ($id) => (int) $id) ->all(); if ($billedStoreIds !== []) { $names = StoreModel::withTrashed() ->whereIn('id', $billedStoreIds) ->pluck('name') ->implode('、'); throw new RepositoryException('以下门店已生成账单,不允许重复生成:' . $names); } // 3. 逐门店生成账单并关联订单 $boxPrice = (string) site_config('services.box_amount', 0); $trayPrice = (string) site_config('services.tray_amount', 0); $billDate = now()->toDateString(); $bills = []; foreach ($ordersByStore as $storeId => $storeOrders) { $row = $submitted[(int) $storeId]; $productAmount = $storeOrders->reduce( static fn (string $carry, StoreOrderModel $order): string => bcadd($carry, (string) $order->total_amount, 2), '0' ); $deliveryFee = bcadd((string) $row['delivery_fee'], '0', 2); $addedAmount = bcadd( bcmul((string) (int) $row['box_num'], $boxPrice, 2), bcmul((string) (int) $row['tray_num'], $trayPrice, 2), 2 ); $totalAmount = bcadd(bcadd($productAmount, $deliveryFee, 2), $addedAmount, 2); $bill = BillModel::create([ 'bill_no' => $this->billNumberService->make('ZD'), 'purchase_id' => $purchase->id, 'store_id' => (int) $storeId, 'bill_date' => $billDate, 'product_amount' => $productAmount, 'delivery_fee' => $deliveryFee, 'box_num' => (int) $row['box_num'], 'tray_num' => (int) $row['tray_num'], 'box_price' => bcadd($boxPrice, '0', 2), 'tray_price' => bcadd($trayPrice, '0', 2), 'added_amount' => $addedAmount, 'total_amount' => $totalAmount, 'operator_id' => $operatorId, ]); // 关联该门店在采购单中的全部订单与订单明细到账单 StoreOrderModel::query() ->whereIn('id', $storeOrders->pluck('id')) ->update(['bill_id' => $bill->id]); StoreOrderItemModel::query() ->whereIn('order_id', $storeOrders->pluck('id')) ->update(['bill_id' => $bill->id]); // 压筐:累加门店待回筐/托盘(行锁防并发),并写入压筐记录 $store = StoreModel::query()->lockForUpdate()->find((int) $storeId); if ($store !== null) { $store->pending_box_num = (int) $store->pending_box_num + (int) $row['box_num']; $store->pending_tray_num = (int) $store->pending_tray_num + (int) $row['tray_num']; $store->save(); } ContainerReturnModel::create([ 'store_id' => (int) $storeId, 'bill_id' => $bill->id, 'type' => ContainerReturnModel::TYPE_PRESS, 'box_num' => (int) $row['box_num'], 'tray_num' => (int) $row['tray_num'], 'return_date' => $billDate, 'operator_id' => $operatorId, ]); $bills[] = $bill; } return $bills; }); } }