Files
xin-procurement/app/Http/Controllers/Mini/OrderController.php
T
2026-08-17 16:27:15 +08:00

296 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Mini\MiniOrderRequest;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Services\BillNumberService;
use App\Services\ItemImageResolver;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
/**
* 小程序门店订单
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class OrderController extends BaseMiniController
{
/**
* 下单
*/
#[PostRoute('/order', authorize: true)]
public function store(MiniOrderRequest $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$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);
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',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$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');
}
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
// 按数据库方言选择周期分组表达式(生产 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
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$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
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$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([], '订单已取消');
}
}