first version

This commit is contained in:
liu
2026-07-23 20:41:25 +08:00
parent 00a0938a1b
commit 10cff754a4
211 changed files with 11577 additions and 842 deletions
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace App\Services;
use App\Exceptions\RepositoryException;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\PurchaseOrderItemModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreOrderModel;
use Illuminate\Support\Facades\DB;
/**
* C1 订单汇总生成采购单
*
* 流程(事务内):
* 1. 行锁查询当日全部「待汇总」订单(无则报错;状态条件天然排除已汇总订单,幂等)
* 2. 展开明细按商品聚合(Σquantity,快照品名/规格;供应商取商品默认供应商)
* 3. 估算单价 = 该商品最低等级价(product_price MIN),amount = quantity × 估算单价
* 4. 创建采购单头(PO 单号,estimate_amount = Σitems.amount
* 5. 明细按「分类 sort → 商品 sort」排序写入 sort 行号
* 6. 源订单批量回写 status = 已汇总
*/
class PurchaseGenerateService
{
public function __construct(private readonly BillNumberService $billNumberService)
{
}
/**
* @param string $date 订货/采购日期(Y-m-d
* @param int $operatorId 制单人(后台系统用户ID)
*/
public function generate(string $date, int $operatorId): PurchaseOrderModel
{
return DB::transaction(function () use ($date, $operatorId) {
// 1. 行锁当日待汇总订单(并发防护)
$orders = StoreOrderModel::query()
->whereDate('order_date', $date)
->where('status', StoreOrderModel::STATUS_PENDING)
->lockForUpdate()
->get();
if ($orders->isEmpty()) {
throw new RepositoryException('当日无待汇总订单');
}
// 2. 展开明细按商品聚合
$aggregated = [];
$orderIds = [];
foreach ($orders as $order) {
$orderIds[] = $order->id;
foreach ($order->items as $item) {
$productId = (int) $item->product_id;
if (! isset($aggregated[$productId])) {
$aggregated[$productId] = [
'product_id' => $productId,
'product_name' => $item->product_name,
'product_spec' => $item->product_spec,
'quantity' => '0',
];
}
$aggregated[$productId]['quantity'] = bcadd(
$aggregated[$productId]['quantity'],
(string) $item->quantity,
2
);
}
}
if ($aggregated === []) {
throw new RepositoryException('当日待汇总订单均无明细,无法生成采购单');
}
$products = ProductModel::withTrashed()
->with('category:id,sort')
->whereIn('id', array_keys($aggregated))
->get()
->keyBy('id');
// 3. 估算单价 = 最低等级价
$minPrices = ProductPriceModel::query()
->whereIn('product_id', array_keys($aggregated))
->groupBy('product_id')
->selectRaw('product_id, MIN(price) as min_price')
->pluck('min_price', 'product_id');
// 组装明细行并按「分类 sort → 商品 sort」排序
$rows = [];
foreach ($aggregated as $productId => $item) {
$product = $products->get($productId);
$price = (string) ($minPrices[$productId] ?? '0');
$rows[] = [
'product_id' => $productId,
'supplier_id' => (int) ($product->supplier_id ?? 0),
'product_name' => $item['product_name'],
'product_spec' => $item['product_spec'],
'price' => $price,
'quantity' => $item['quantity'],
'amount' => bcmul($item['quantity'], $price, 2),
'category_sort' => (int) ($product->category->sort ?? 9999),
'product_sort' => (int) ($product->sort ?? 9999),
];
}
usort($rows, static fn (array $a, array $b): int =>
[$a['category_sort'], $a['product_sort'], $a['product_id']]
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
$estimateAmount = array_reduce(
$rows,
static fn (string $carry, array $row): string => bcadd($carry, $row['amount'], 2),
'0'
);
$totalQuantity = array_reduce(
$rows,
static fn (string $carry, array $row): string => bcadd($carry, $row['quantity'], 2),
'0'
);
// 4. 采购单头
$purchase = PurchaseOrderModel::create([
'purchase_no' => $this->billNumberService->make('PO'),
'purchase_date' => $date,
'status' => PurchaseOrderModel::STATUS_PENDING,
'total_quantity' => $totalQuantity,
'total_weight' => 0,
'estimate_amount' => $estimateAmount,
'actual_amount' => 0,
'operator_id' => $operatorId,
]);
// 5. 采购明细(sort 行号)
$sort = 1;
foreach ($rows as $row) {
PurchaseOrderItemModel::create([
'purchase_id' => $purchase->id,
'product_id' => $row['product_id'],
'supplier_id' => $row['supplier_id'],
'product_name' => $row['product_name'],
'product_spec' => $row['product_spec'],
'price' => $row['price'],
'quantity' => $row['quantity'],
'weight' => 0,
'amount' => $row['amount'],
'sort' => $sort++,
'is_sent' => PurchaseOrderItemModel::NOT_SENT,
]);
}
// 6. 源订单回写「已汇总」
StoreOrderModel::whereIn('id', $orderIds)
->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
return $purchase;
});
}
}