0 的明细),否则拒绝 * 2. 每个采购明细溯源「采购日当天、已汇总订单」中该商品的订货明细 * 3. 按订货数量比例分摊实际金额/数量/重量:bcmul(item.amount, bcdiv(item_qty, total_qty, 6), 2) * 尾差修正——最后一行承担舍入差额,保证 Σallocation.amount === item.amount(金额守恒) * 4. 重复分摊先删旧记录再重建(幂等) */ class PurchaseAllocateService { /** * @return int 生成的分摊记录数 */ public function allocate(PurchaseOrderModel $purchase): int { return DB::transaction(function () use ($purchase) { $items = PurchaseOrderItemModel::query() ->where('purchase_id', $purchase->id) ->lockForUpdate() ->get(); // 1. 须已录入实际金额 if (! $items->contains(static fn ($item) => (float) $item->amount > 0)) { throw new RepositoryException('采购单尚未录入实际金额,无法分摊'); } // 2. 溯源采购日当天「已汇总」订单的订货明细,按商品分组 $orderItems = StoreOrderItemModel::query() ->join('store_order', 'store_order.id', '=', 'store_order_item.order_id') ->whereDate('store_order.order_date', $purchase->purchase_date) ->where('store_order.status', StoreOrderModel::STATUS_SUMMARIZED) ->select('store_order_item.*') ->get() ->groupBy('product_id'); // 4. 幂等:删除旧分摊记录 PurchaseAllocationModel::query() ->whereIn('purchase_item_id', $items->pluck('id')) ->delete(); $count = 0; $now = now(); foreach ($items as $item) { $sources = $orderItems->get((int) $item->product_id); if ($sources === null || $sources->isEmpty()) { continue; } $totalQty = $sources->reduce( static fn (string $carry, $orderItem): string => bcadd($carry, (string) $orderItem->quantity, 2), '0' ); if ((float) $totalQty <= 0) { continue; } $sourceValues = $sources->values(); $lastIndex = $sourceValues->count() - 1; $allocatedAmount = '0'; $allocatedQuantity = '0'; $allocatedWeight = '0'; $records = []; foreach ($sourceValues as $index => $orderItem) { if ($index === $lastIndex) { // 3. 尾差修正:最后一行 = 总额 − 已分摊,保证守恒 $amount = bcsub((string) $item->amount, $allocatedAmount, 2); $quantity = bcsub((string) $item->quantity, $allocatedQuantity, 2); $weight = bcsub((string) $item->weight, $allocatedWeight, 3); } else { $ratio = bcdiv((string) $orderItem->quantity, $totalQty, 6); $amount = bcmul((string) $item->amount, $ratio, 2); $quantity = bcmul((string) $item->quantity, $ratio, 2); $weight = bcmul((string) $item->weight, $ratio, 3); $allocatedAmount = bcadd($allocatedAmount, $amount, 2); $allocatedQuantity = bcadd($allocatedQuantity, $quantity, 2); $allocatedWeight = bcadd($allocatedWeight, $weight, 3); } $records[] = [ 'purchase_item_id' => $item->id, 'order_item_id' => $orderItem->id, 'store_id' => $orderItem->store_id, 'product_id' => $item->product_id, 'quantity' => $quantity, 'weight' => $weight, 'amount' => $amount, 'created_at' => $now, 'updated_at' => $now, ]; } PurchaseAllocationModel::insert($records); $count += count($records); } return $count; }); } }