Files
xin-procurement/app/Http/Controllers/Order/StoreOrderController.php
T
2026-08-11 21:08:01 +08:00

300 lines
11 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\Order;
use App\Exceptions\RepositoryException;
use App\Models\NoticeModel;
use App\Models\ProductModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 门店订单管理(订单只读 + 状态管理;创建/取消在小程序端)
*/
#[RequestAttribute('/order/store', 'order.store')]
class StoreOrderController extends BaseController
{
/** 状态中文名(通知文案用) */
private const array STATUS_NAMES = [
StoreOrderModel::STATUS_PENDING => '待接单',
StoreOrderModel::STATUS_SUMMARIZED => '已接单',
StoreOrderModel::STATUS_DELIVERING => '采购中',
StoreOrderModel::STATUS_DISTRIBUTION => '配送中',
StoreOrderModel::STATUS_COMPLETED => '已完成',
StoreOrderModel::STATUS_CANCELLED => '已取消',
];
protected array $searchField = [
'store_id' => '=',
'status' => '=',
'order_no' => 'like',
'order_date' => 'betweenDate',
];
/** 订单列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$query = StoreOrderModel::query()->with([
'store:id,name,address,contact,phone',
'items:id,order_id,product_id,product_name,product_spec,price,quantity,amount',
'items.product'
]);
$data = $this->buildSearch($params, $query)
->orderBy('order_date', 'desc')
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
$box_amount = site_config('services.box_amount');
$tray_amount = site_config('services.tray_amount');
foreach ($data['data'] as &$item) {
$item['box_price'] = number_format($box_amount, 2);
$item['tray_price'] = number_format($tray_amount, 2);
$item['box_amount'] = number_format($box_amount * $item['box_num'], 2);
$item['tray_amount'] = number_format($tray_amount * $item['tray_num'], 2);
if(count($item['items']) > 0) {
foreach ($item['items'] as &$product) {
if($product['product'] && $product['product']['images_arr'] > 0) {
$product['image'] = $product['product']['images_arr'][0]['preview_url'] ?? '';
unset($product['product']);
}
}
}
}
return $this->success($data);
}
/**
* 已接单预览:聚合所有已接单订单明细(按商品分组),
* 供生成采购单前确认(C1 前置)
*/
#[GetRoute('/summary', 'query')]
public function summary(): JsonResponse
{
$rows = StoreOrderItemModel::query()
->select('product_id')
->selectRaw('MAX(product_name) as product_name')
->selectRaw('MAX(product_spec) as product_spec')
->selectRaw('SUM(quantity) as total_quantity')
->selectRaw('COUNT(DISTINCT store_id) as store_count')
->whereHas('order', function ($query) {
$query->where('status', StoreOrderModel::STATUS_SUMMARIZED);
})
->groupBy('product_id')
->orderBy('product_id')
->get()
->toArray();
// 补充计价单位(商品档案,含已下架/软删除)
$units = ProductModel::withTrashed()
->whereIn('id', array_column($rows, 'product_id'))
->pluck('unit', 'id');
foreach ($rows as &$row) {
$row['unit'] = $units[$row['product_id']] ?? '';
}
return $this->success($rows);
}
/** 订单详情:订单头 + 明细(含商品快照) */
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
{
$order = StoreOrderModel::with(['store:id,name', 'items'])->find($id);
if (empty($order)) {
throw new RepositoryException('订单不存在');
}
return $this->success($order->toArray());
}
/**
* 状态流转:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
*/
#[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])]
public function status(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'status' => 'required|integer|in:1,3,4,9',
], [
'status.required' => '目标状态不能为空',
'status.in' => '目标状态值不正确',
]);
$order = StoreOrderModel::find($id);
if (empty($order)) {
throw new RepositoryException('订单不存在');
}
$target = (int) $data['status'];
if (! in_array($target, $this->allowedTransitions($order->status), true)) {
throw new RepositoryException(
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作'
);
}
$order->status = $target;
$order->save();
$this->notifyStore($order);
return $this->success();
}
/**
* 批量状态流转:先全量校验流转路径,任一订单不允许则整批中止;
* 全部合法时在事务内流转并逐单通知门店
*/
#[PutRoute(route: '/batchStatus', authorize: 'update')]
public function batchStatus(Request $request): JsonResponse
{
$data = $request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'integer|exists:store_order,id',
'status' => 'required|integer|in:1,3,4,9',
], [
'ids.required' => '请选择要流转的订单',
'ids.min' => '请选择要流转的订单',
'ids.*.exists' => '订单不存在',
'status.required' => '目标状态不能为空',
'status.in' => '目标状态值不正确',
]);
$target = (int) $data['status'];
$orders = StoreOrderModel::query()->whereIn('id', $data['ids'])->get();
// 全量预校验:任一订单不满足流转条件,整批中止,不做任何修改
$blocked = [];
foreach ($orders as $order) {
if (! in_array($target, $this->allowedTransitions($order->status), true)) {
$blocked[] = $order->order_no . '' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '';
}
}
if (! empty($blocked)) {
throw new RepositoryException(
'以下订单不允许流转为「' . (self::STATUS_NAMES[$target] ?? $target) . '」,批量操作已中止:' . implode('、', $blocked)
);
}
DB::transaction(function () use ($orders, $target) {
foreach ($orders as $order) {
$order->status = $target;
$order->save();
$this->notifyStore($order);
}
});
return $this->success(['success' => $orders->count()]);
}
/**
* 状态流转合法路径(与迁移状态定义一致):
* 待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
* (已接单→采购中 只能经「生成采购单」完成,不在本流转内)
*
* @return int[]
*/
private function allowedTransitions(int $status): array
{
return match ($status) {
StoreOrderModel::STATUS_PENDING => [
StoreOrderModel::STATUS_SUMMARIZED,
StoreOrderModel::STATUS_CANCELLED,
],
StoreOrderModel::STATUS_DELIVERING => [
StoreOrderModel::STATUS_DISTRIBUTION,
StoreOrderModel::STATUS_COMPLETED,
],
StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED],
default => [],
};
}
/**
* 修改周转框/周转托盘数量(仅已接单、采购中、配送中可改),
* 自动重算附加金额与订单总金额
*/
#[PutRoute(route: '/{id}/container', authorize: 'update', where: ['id' => '[0-9]+'])]
public function container(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'box_num' => 'required|integer|min:0',
'tray_num' => 'required|integer|min:0',
], [
'box_num.required' => '周转框数量不能为空',
'box_num.integer' => '周转框数量必须为整数',
'box_num.min' => '周转框数量不能小于 0',
'tray_num.required' => '周转托盘数量不能为空',
'tray_num.integer' => '周转托盘数量必须为整数',
'tray_num.min' => '周转托盘数量不能小于 0',
]);
$order = StoreOrderModel::find($id);
if (empty($order)) {
throw new RepositoryException('订单不存在');
}
$editable = [
StoreOrderModel::STATUS_SUMMARIZED,
StoreOrderModel::STATUS_DELIVERING,
StoreOrderModel::STATUS_DISTRIBUTION,
];
if (! in_array($order->status, $editable, true)) {
throw new RepositoryException(
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改周转框/托盘数量'
);
}
$boxPrice = (float) site_config('services.box_amount', 0);
$trayPrice = (float) site_config('services.tray_amount', 0);
$addedAmount = round($data['box_num'] * $boxPrice + $data['tray_num'] * $trayPrice, 2);
// 历史订单未写商品金额:按「总额 - 附加」反推并回写,保证 总额 = 商品 + 附加 恒成立
$productAmount = (float) $order->product_amount;
if ($productAmount <= 0 && (float) $order->total_amount > 0) {
$productAmount = round((float) $order->total_amount - (float) $order->added_amount, 2);
}
$order->box_num = (int) $data['box_num'];
$order->tray_num = (int) $data['tray_num'];
$order->product_amount = $productAmount;
$order->added_amount = $addedAmount;
$order->total_amount = round($productAmount + $addedAmount, 2);
$order->save();
return $this->success();
}
/**
* 状态流转后通知门店用户
*/
private function notifyStore(StoreOrderModel $order): void
{
$userIds = UserModel::query()
->where('store_id', $order->store_id)
->where('status', UserModel::STATUS_NORMAL)
->pluck('id');
$statusName = self::STATUS_NAMES[$order->status] ?? (string) $order->status;
foreach ($userIds as $userId) {
NoticeModel::create([
'user_id' => $userId,
'type' => NoticeModel::TYPE_ORDER,
'title' => '订单状态更新',
'content' => mb_substr("您的订单 {$order->order_no} 状态已更新为「{$statusName}」", 0, 500),
'data' => ['order_id' => $order->id],
'is_read' => NoticeModel::UNREAD,
]);
}
}
}