1652 lines
68 KiB
PHP
1652 lines
68 KiB
PHP
<?php
|
||
|
||
namespace app\controller\admin;
|
||
|
||
use app\support\AppraisalServicePricePackageService;
|
||
use app\support\AppraisalEvidenceService;
|
||
use app\support\EnterpriseWebhookService;
|
||
use app\support\MessageDispatcher;
|
||
use app\support\OrderLogisticsSyncService;
|
||
use app\support\WarehouseService;
|
||
use support\Request;
|
||
use support\think\Db;
|
||
|
||
class OrdersController
|
||
{
|
||
private const MANUAL_ENTRY_SOURCE = 'manual_entry';
|
||
|
||
public function index(Request $request)
|
||
{
|
||
$keyword = trim((string)$request->input('keyword', ''));
|
||
$externalOrderNo = trim((string)$request->input('external_order_no', ''));
|
||
$trackingNo = trim((string)$request->input('tracking_no', ''));
|
||
$userMobile = trim((string)$request->input('user_mobile', ''));
|
||
$status = trim((string)$request->input('status', ''));
|
||
$serviceProvider = trim((string)$request->input('service_provider', ''));
|
||
$sourceChannel = $this->normalizeOrderSourceChannel((string)$request->input('source_channel', ''));
|
||
$paginationEnabled = $request->input('page', null) !== null || $request->input('page_size', null) !== null;
|
||
$page = max(1, (int)$request->input('page', 1));
|
||
$pageSize = max(1, min(100, (int)$request->input('page_size', 20)));
|
||
|
||
$query = Db::name('orders')
|
||
->alias('o')
|
||
->leftJoin('order_products p', 'p.order_id = o.id')
|
||
->leftJoin('enterprise_customer_order_refs ecor', 'ecor.order_id = o.id')
|
||
->field([
|
||
'o.id',
|
||
'o.order_no',
|
||
'o.appraisal_no',
|
||
'ecor.external_order_no',
|
||
'o.service_provider',
|
||
'o.order_status',
|
||
'o.display_status',
|
||
'o.estimated_finish_time',
|
||
'o.source_channel',
|
||
'o.source_customer_id',
|
||
'o.price_package_name',
|
||
'o.price_package_code',
|
||
'o.price_package_price',
|
||
'o.pay_amount',
|
||
'o.created_at',
|
||
'p.product_name',
|
||
'p.category_name',
|
||
'p.brand_name',
|
||
])
|
||
->order('o.id', 'desc');
|
||
|
||
if ($keyword !== '') {
|
||
$query->where(function ($builder) use ($keyword) {
|
||
$builder->whereRaw(
|
||
'(o.order_no LIKE :keyword_order OR o.appraisal_no LIKE :keyword_appraisal OR p.product_name LIKE :keyword_product)',
|
||
[
|
||
'keyword_order' => "%{$keyword}%",
|
||
'keyword_appraisal' => "%{$keyword}%",
|
||
'keyword_product' => "%{$keyword}%",
|
||
]
|
||
);
|
||
});
|
||
}
|
||
|
||
if ($externalOrderNo !== '') {
|
||
$query->whereRaw('ecor.external_order_no LIKE :external_order_no', [
|
||
'external_order_no' => "%{$externalOrderNo}%",
|
||
]);
|
||
}
|
||
|
||
if ($trackingNo !== '') {
|
||
$query->whereRaw(
|
||
"EXISTS (SELECT 1 FROM order_logistics ol WHERE ol.order_id = o.id AND ol.logistics_type IN ('send_to_center', 'return_to_user') AND ol.tracking_no LIKE :tracking_no)",
|
||
[
|
||
'tracking_no' => "%{$trackingNo}%",
|
||
]
|
||
);
|
||
}
|
||
|
||
if ($userMobile !== '') {
|
||
$query->leftJoin('users u', 'u.id = o.user_id')
|
||
->leftJoin('order_return_addresses ra', 'ra.order_id = o.id');
|
||
|
||
$query->where(function ($builder) use ($userMobile) {
|
||
$builder->whereRaw(
|
||
'(u.mobile LIKE :user_mobile OR ra.mobile LIKE :return_mobile)',
|
||
[
|
||
'user_mobile' => "%{$userMobile}%",
|
||
'return_mobile' => "%{$userMobile}%",
|
||
]
|
||
);
|
||
});
|
||
}
|
||
|
||
$warehouseStatusFilters = [
|
||
'warehouse_active',
|
||
'warehouse_pending_inbound',
|
||
'warehouse_in_transit',
|
||
'warehouse_received',
|
||
'warehouse_pending_return',
|
||
];
|
||
$specialStatusFilters = array_merge(['returning', 'completed_signed'], $warehouseStatusFilters);
|
||
if ($status !== '' && !in_array($status, $specialStatusFilters, true)) {
|
||
$query->where('o.order_status', $status);
|
||
}
|
||
|
||
if (in_array($status, $warehouseStatusFilters, true)) {
|
||
$warehouseActiveStatuses = [
|
||
'pending_shipping',
|
||
'received',
|
||
'in_first_review',
|
||
'pending_supplement',
|
||
'in_final_review',
|
||
'generating_report',
|
||
'report_published',
|
||
];
|
||
if ($status === 'warehouse_in_transit') {
|
||
$query->where('o.order_status', 'pending_shipping');
|
||
} elseif ($status === 'warehouse_pending_inbound') {
|
||
$query->where('o.order_status', 'pending_shipping')
|
||
->where('o.source_channel', self::MANUAL_ENTRY_SOURCE);
|
||
} elseif ($status === 'warehouse_received') {
|
||
$query->whereIn('o.order_status', array_values(array_diff($warehouseActiveStatuses, ['pending_shipping', 'report_published'])));
|
||
} elseif ($status === 'warehouse_pending_return') {
|
||
$query->where('o.order_status', 'report_published');
|
||
} else {
|
||
$query->whereIn('o.order_status', $warehouseActiveStatuses);
|
||
}
|
||
}
|
||
|
||
if ($serviceProvider !== '') {
|
||
$query->where('o.service_provider', $serviceProvider);
|
||
}
|
||
|
||
if ($sourceChannel !== '') {
|
||
$query->where('o.source_channel', $sourceChannel);
|
||
}
|
||
|
||
$rows = $query->select()->toArray();
|
||
|
||
$orderIds = array_map('intval', array_column($rows, 'id'));
|
||
$sendTrackingMap = $this->latestLogisticsMap($orderIds, 'send_to_center');
|
||
$returnTrackingMap = $this->latestLogisticsMap($orderIds, 'return_to_user');
|
||
$transferFlowMap = $this->latestTransferFlowMap($orderIds);
|
||
|
||
$list = array_map(function (array $item) use ($sendTrackingMap, $returnTrackingMap, $transferFlowMap) {
|
||
$orderId = (int)$item['id'];
|
||
$sendTrackingNo = $sendTrackingMap[$orderId]['tracking_no'] ?? '';
|
||
$sendTrackingStatus = $sendTrackingMap[$orderId]['tracking_status'] ?? '';
|
||
$warehouseBucket = $this->warehouseOrderBucket(
|
||
(string)$item['order_status'],
|
||
$sendTrackingNo,
|
||
$sendTrackingStatus,
|
||
(string)($item['display_status'] ?? ''),
|
||
(string)($item['source_channel'] ?? '')
|
||
);
|
||
|
||
return [
|
||
'id' => $orderId,
|
||
'order_no' => $item['order_no'],
|
||
'appraisal_no' => $item['appraisal_no'],
|
||
'external_order_no' => (string)($item['external_order_no'] ?? ''),
|
||
'product_name' => $item['product_name'] ?: '待完善物品信息',
|
||
'category_name' => $item['category_name'] ?: '',
|
||
'brand_name' => $item['brand_name'] ?: '',
|
||
'service_provider' => $item['service_provider'],
|
||
'service_provider_text' => $item['service_provider'] === 'zhongjian' ? '中检鉴定' : '实物鉴定',
|
||
'price_package_name' => (string)($item['price_package_name'] ?? ''),
|
||
'price_package_code' => (string)($item['price_package_code'] ?? ''),
|
||
'price_package_price' => (float)($item['price_package_price'] ?? 0),
|
||
'source_channel' => $this->normalizeOrderSourceChannel((string)($item['source_channel'] ?? '')),
|
||
'source_channel_text' => $this->sourceChannelText((string)($item['source_channel'] ?? '')),
|
||
'source_customer_id' => (string)($item['source_customer_id'] ?? ''),
|
||
'order_status' => $item['order_status'],
|
||
'display_status' => $this->displayStatus(
|
||
(string)$item['order_status'],
|
||
(string)$item['display_status'],
|
||
$returnTrackingMap[$orderId]['tracking_no'] ?? '',
|
||
$returnTrackingMap[$orderId]['tracking_status'] ?? '',
|
||
),
|
||
'internal_tag_no' => $transferFlowMap[$orderId]['internal_tag_no'] ?? '',
|
||
'warehouse_bucket' => $warehouseBucket,
|
||
'warehouse_bucket_text' => $this->warehouseOrderBucketText($warehouseBucket),
|
||
'estimated_finish_time' => $item['estimated_finish_time'],
|
||
'pay_amount' => (float)$item['pay_amount'],
|
||
'created_at' => $item['created_at'],
|
||
];
|
||
}, $rows);
|
||
|
||
if ($status === 'returning') {
|
||
$list = array_values(array_filter($list, function (array $item) {
|
||
return $item['order_status'] === 'completed' && $item['display_status'] === '物品已寄回';
|
||
}));
|
||
}
|
||
|
||
if ($status === 'completed_signed') {
|
||
$list = array_values(array_filter($list, function (array $item) {
|
||
return $item['order_status'] === 'completed' && $item['display_status'] === '已完成';
|
||
}));
|
||
}
|
||
|
||
if (in_array($status, $warehouseStatusFilters, true)) {
|
||
$list = array_values(array_filter($list, function (array $item) use ($status) {
|
||
if ($status === 'warehouse_active') {
|
||
return in_array($item['warehouse_bucket'], [
|
||
'warehouse_pending_inbound',
|
||
'warehouse_in_transit',
|
||
'warehouse_received',
|
||
'warehouse_pending_return',
|
||
], true);
|
||
}
|
||
|
||
return $item['warehouse_bucket'] === $status;
|
||
}));
|
||
}
|
||
|
||
$total = count($list);
|
||
if ($paginationEnabled) {
|
||
$offset = ($page - 1) * $pageSize;
|
||
$list = array_slice($list, $offset, $pageSize);
|
||
|
||
return api_success([
|
||
'list' => $list,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'page_size' => $pageSize,
|
||
]);
|
||
}
|
||
|
||
return api_success([
|
||
'list' => $list,
|
||
]);
|
||
}
|
||
|
||
public function detail(Request $request)
|
||
{
|
||
$id = (int)$request->input('id', 0);
|
||
if (!$id) {
|
||
return api_error('订单 ID 不能为空', 422);
|
||
}
|
||
|
||
$order = Db::name('orders')->where('id', $id)->find();
|
||
if (!$order) {
|
||
return api_error('订单不存在', 404);
|
||
}
|
||
|
||
$product = Db::name('order_products')->where('order_id', $id)->find();
|
||
$extra = Db::name('order_extras')->where('order_id', $id)->find();
|
||
$sendLogistics = Db::name('order_logistics')
|
||
->where('order_id', $id)
|
||
->where('logistics_type', 'send_to_center')
|
||
->order('id', 'desc')
|
||
->find();
|
||
$returnLogistics = Db::name('order_logistics')
|
||
->where('order_id', $id)
|
||
->where('logistics_type', 'return_to_user')
|
||
->order('id', 'desc')
|
||
->find();
|
||
$transferFlow = Db::name('order_transfer_flows')
|
||
->where('order_id', $id)
|
||
->order('id', 'desc')
|
||
->find();
|
||
$enterpriseOrderRef = Db::name('enterprise_customer_order_refs')->where('order_id', $id)->find();
|
||
$timeline = Db::name('order_timelines')
|
||
->where('order_id', $id)
|
||
->order('occurred_at', 'asc')
|
||
->select()
|
||
->toArray();
|
||
|
||
$timeline = array_map(fn (array $item) => [
|
||
'node_text' => $item['node_text'],
|
||
'node_desc' => $item['node_desc'],
|
||
'occurred_at' => $item['occurred_at'],
|
||
], $timeline);
|
||
|
||
$supplement = Db::name('order_supplement_tasks')->where('order_id', $id)->order('id', 'desc')->find();
|
||
$supplementItems = [];
|
||
if ($supplement) {
|
||
$supplementItems = Db::name('order_supplement_task_items')
|
||
->where('task_id', $supplement['id'])
|
||
->select()
|
||
->toArray();
|
||
|
||
$supplementItems = array_map(fn (array $item) => [
|
||
'item_name' => $item['item_name'],
|
||
'guide_text' => $item['guide_text'],
|
||
], $supplementItems);
|
||
}
|
||
|
||
$report = Db::name('reports')->where('order_id', $id)->order('id', 'desc')->find();
|
||
$hasPublishedOrderReport = $report && ($report['report_status'] ?? '') === 'published';
|
||
$canAttemptReturnLogistics = in_array($order['order_status'], ['report_published', 'completed'], true)
|
||
&& (($returnLogistics['tracking_status'] ?? '') !== 'received');
|
||
$shippingTarget = Db::name('order_shipping_targets')->where('order_id', $id)->find();
|
||
$returnAddress = Db::name('order_return_addresses')->where('order_id', $id)->find();
|
||
if (!$returnAddress) {
|
||
$returnAddress = Db::name('user_addresses')
|
||
->where('user_id', (int)$order['user_id'])
|
||
->where('is_default', 1)
|
||
->order('id', 'desc')
|
||
->find()
|
||
?: Db::name('user_addresses')
|
||
->where('user_id', (int)$order['user_id'])
|
||
->order('id', 'desc')
|
||
->find();
|
||
if ($returnAddress) {
|
||
$returnAddress = [
|
||
'user_address_id' => (int)$returnAddress['id'],
|
||
'consignee' => $returnAddress['consignee'],
|
||
'mobile' => $returnAddress['mobile'],
|
||
'province' => $returnAddress['province'],
|
||
'city' => $returnAddress['city'],
|
||
'district' => $returnAddress['district'],
|
||
'detail_address' => $returnAddress['detail_address'],
|
||
];
|
||
}
|
||
}
|
||
$logisticsNodes = [];
|
||
if ($sendLogistics) {
|
||
$logisticsNodes = Db::name('order_logistics_nodes')
|
||
->where('logistics_id', $sendLogistics['id'])
|
||
->order('node_time', 'desc')
|
||
->select()
|
||
->toArray();
|
||
}
|
||
$inboundAttachments = $this->inboundAttachments($id, $request);
|
||
$returnLogisticsNodes = [];
|
||
if ($returnLogistics) {
|
||
$returnLogisticsNodes = Db::name('order_logistics_nodes')
|
||
->where('logistics_id', $returnLogistics['id'])
|
||
->order('node_time', 'desc')
|
||
->select()
|
||
->toArray();
|
||
}
|
||
$syncService = new OrderLogisticsSyncService();
|
||
$sendSyncStatus = $sendLogistics ? $syncService->formatSyncStatus((int)$sendLogistics['id']) : [
|
||
'provider_status_text' => '',
|
||
'sync_status_text' => '未同步',
|
||
'sync_error' => '',
|
||
];
|
||
$returnSyncStatus = $returnLogistics ? $syncService->formatSyncStatus((int)$returnLogistics['id']) : [
|
||
'provider_status_text' => '',
|
||
'sync_status_text' => '未同步',
|
||
'sync_error' => '',
|
||
];
|
||
|
||
return api_success([
|
||
'order_info' => [
|
||
'id' => (int)$order['id'],
|
||
'order_no' => $order['order_no'],
|
||
'appraisal_no' => $order['appraisal_no'],
|
||
'external_order_no' => (string)($enterpriseOrderRef['external_order_no'] ?? ''),
|
||
'service_provider' => $order['service_provider'],
|
||
'service_provider_text' => $order['service_provider'] === 'zhongjian' ? '中检鉴定' : '实物鉴定',
|
||
'price_package_name' => (string)($order['price_package_name'] ?? ''),
|
||
'price_package_code' => (string)($order['price_package_code'] ?? ''),
|
||
'price_package_price' => (float)($order['price_package_price'] ?? 0),
|
||
'source_channel' => $this->normalizeOrderSourceChannel((string)($order['source_channel'] ?? '')),
|
||
'source_channel_text' => $this->sourceChannelText((string)($order['source_channel'] ?? '')),
|
||
'source_customer_id' => (string)($order['source_customer_id'] ?? ''),
|
||
'order_status' => $order['order_status'],
|
||
'display_status' => $this->displayStatus(
|
||
(string)$order['order_status'],
|
||
(string)$order['display_status'],
|
||
$returnLogistics['tracking_no'] ?? '',
|
||
$returnLogistics['tracking_status'] ?? '',
|
||
),
|
||
'pay_amount' => (float)$order['pay_amount'],
|
||
'estimated_finish_time' => $order['estimated_finish_time'],
|
||
'created_at' => $order['created_at'],
|
||
'can_reassign_warehouse' => $order['order_status'] === 'pending_shipping' && empty($sendLogistics['tracking_no']),
|
||
'can_mark_received' => $order['order_status'] === 'pending_shipping'
|
||
&& (!empty($sendLogistics['tracking_no']) || ($order['source_channel'] ?? '') === 'enterprise_push'),
|
||
'can_submit_return_logistics' => $hasPublishedOrderReport && $canAttemptReturnLogistics,
|
||
'return_logistics_block_reason' => (!$hasPublishedOrderReport && $canAttemptReturnLogistics)
|
||
? '订单报告未发布前,物品不允许寄回'
|
||
: '',
|
||
'can_mark_return_received' => $order['order_status'] === 'completed' && !empty($returnLogistics['tracking_no']) && ($returnLogistics['tracking_status'] ?? '') !== 'received',
|
||
],
|
||
'product_info' => [
|
||
'product_name' => $product['product_name'] ?? '',
|
||
'category_id' => (int)($product['category_id'] ?? 0),
|
||
'category_name' => $product['category_name'] ?? '',
|
||
'brand_id' => (int)($product['brand_id'] ?? 0),
|
||
'brand_name' => $product['brand_name'] ?? '',
|
||
'color' => $product['color'] ?? '',
|
||
'size_spec' => $product['size_spec'] ?? '',
|
||
'serial_no' => $product['serial_no'] ?? '',
|
||
],
|
||
'extra_info' => [
|
||
'purchase_channel' => $extra['purchase_channel'] ?? '',
|
||
'purchase_price' => (float)($extra['purchase_price'] ?? 0),
|
||
'usage_status' => $extra['usage_status'] ?? '',
|
||
'condition_desc' => $extra['condition_desc'] ?? '',
|
||
'remark' => $extra['remark'] ?? '',
|
||
],
|
||
'shipping_target' => $shippingTarget ? [
|
||
'warehouse_id' => (int)($shippingTarget['warehouse_id'] ?? 0),
|
||
'warehouse_name' => $shippingTarget['warehouse_name'],
|
||
'warehouse_code' => $shippingTarget['warehouse_code'],
|
||
'receiver_name' => $shippingTarget['receiver_name'],
|
||
'receiver_mobile' => $shippingTarget['receiver_mobile'],
|
||
'full_address' => trim(sprintf(
|
||
'%s%s%s%s',
|
||
$shippingTarget['province'] ?? '',
|
||
$shippingTarget['city'] ?? '',
|
||
$shippingTarget['district'] ?? '',
|
||
$shippingTarget['detail_address'] ?? ''
|
||
)),
|
||
'service_time' => $shippingTarget['service_time'],
|
||
'notice' => $shippingTarget['notice'],
|
||
] : null,
|
||
'return_address' => $returnAddress ? [
|
||
'user_address_id' => (int)($returnAddress['user_address_id'] ?? 0),
|
||
'consignee' => $returnAddress['consignee'],
|
||
'mobile' => $returnAddress['mobile'],
|
||
'full_address' => trim(sprintf(
|
||
'%s%s%s%s',
|
||
$returnAddress['province'] ?? '',
|
||
$returnAddress['city'] ?? '',
|
||
$returnAddress['district'] ?? '',
|
||
$returnAddress['detail_address'] ?? ''
|
||
)),
|
||
] : null,
|
||
'timeline' => $timeline,
|
||
'transfer_flow' => $transferFlow ? [
|
||
'internal_tag_no' => (string)($transferFlow['internal_tag_no'] ?? ''),
|
||
] : null,
|
||
'logistics_info' => $sendLogistics ? [
|
||
'express_company' => $sendLogistics['express_company'],
|
||
'tracking_no' => $sendLogistics['tracking_no'],
|
||
'tracking_status' => $sendLogistics['tracking_status'],
|
||
'tracking_status_text' => $this->trackingStatusText($sendLogistics['tracking_status'], 'send_to_center'),
|
||
'provider_status_text' => $sendSyncStatus['provider_status_text'],
|
||
'sync_status_text' => $sendSyncStatus['sync_status_text'],
|
||
'sync_error' => $sendSyncStatus['sync_error'],
|
||
'latest_desc' => (string)($sendLogistics['latest_desc'] ?? ''),
|
||
'latest_time' => $sendLogistics['latest_time'],
|
||
'nodes' => array_map(fn (array $item) => [
|
||
'node_time' => $item['node_time'],
|
||
'node_desc' => $item['node_desc'],
|
||
'node_location' => $item['node_location'],
|
||
], $logisticsNodes),
|
||
] : null,
|
||
'inbound_attachments' => $inboundAttachments,
|
||
'return_logistics' => $returnLogistics ? [
|
||
'express_company' => $returnLogistics['express_company'],
|
||
'tracking_no' => $returnLogistics['tracking_no'],
|
||
'tracking_status' => $returnLogistics['tracking_status'],
|
||
'tracking_status_text' => $this->trackingStatusText($returnLogistics['tracking_status'], 'return_to_user'),
|
||
'provider_status_text' => $returnSyncStatus['provider_status_text'],
|
||
'sync_status_text' => $returnSyncStatus['sync_status_text'],
|
||
'sync_error' => $returnSyncStatus['sync_error'],
|
||
'latest_desc' => (string)($returnLogistics['latest_desc'] ?? ''),
|
||
'latest_time' => $returnLogistics['latest_time'],
|
||
'nodes' => array_map(fn (array $item) => [
|
||
'node_time' => $item['node_time'],
|
||
'node_desc' => $item['node_desc'],
|
||
'node_location' => $item['node_location'],
|
||
], $returnLogisticsNodes),
|
||
] : null,
|
||
'supplement_task' => $supplement ? [
|
||
'reason' => $supplement['reason'],
|
||
'deadline' => $supplement['deadline'],
|
||
'status' => $supplement['status'],
|
||
'items' => $supplementItems,
|
||
] : null,
|
||
'report_summary' => $report ? [
|
||
'id' => (int)$report['id'],
|
||
'report_no' => $report['report_no'],
|
||
'report_title' => $report['report_title'],
|
||
'report_status' => $report['report_status'],
|
||
'report_status_text' => $this->reportStatusText((string)$report['report_status']),
|
||
'publish_time' => $report['publish_time'],
|
||
] : null,
|
||
]);
|
||
}
|
||
|
||
public function warehouseOptions(Request $request)
|
||
{
|
||
$id = (int)$request->input('id', 0);
|
||
if ($id <= 0) {
|
||
return api_error('订单 ID 不能为空', 422);
|
||
}
|
||
|
||
$order = Db::name('orders')->where('id', $id)->find();
|
||
if (!$order) {
|
||
return api_error('订单不存在', 404);
|
||
}
|
||
|
||
$product = Db::name('order_products')->where('order_id', $id)->find();
|
||
$options = (new WarehouseService())->optionsForOrder(
|
||
(string)($order['service_provider'] ?? 'anxinyan'),
|
||
!empty($product['category_id']) ? (int)$product['category_id'] : null
|
||
);
|
||
|
||
return api_success([
|
||
'list' => $options,
|
||
]);
|
||
}
|
||
|
||
public function reassignWarehouse(Request $request)
|
||
{
|
||
$id = (int)$request->input('id', 0);
|
||
$warehouseId = (int)$request->input('warehouse_id', 0);
|
||
if ($id <= 0 || $warehouseId <= 0) {
|
||
return api_error('订单 ID 和仓库 ID 不能为空', 422);
|
||
}
|
||
|
||
$order = Db::name('orders')->where('id', $id)->find();
|
||
if (!$order) {
|
||
return api_error('订单不存在', 404);
|
||
}
|
||
|
||
$logistics = Db::name('order_logistics')
|
||
->where('order_id', $id)
|
||
->where('logistics_type', 'send_to_center')
|
||
->order('id', 'desc')
|
||
->find();
|
||
if ($order['order_status'] !== 'pending_shipping' || !empty($logistics['tracking_no'])) {
|
||
return api_error('当前订单已进入寄送流程,暂不支持改派仓库', 422);
|
||
}
|
||
|
||
$warehouse = Db::name('shipping_warehouses')
|
||
->where('id', $warehouseId)
|
||
->where('status', 'enabled')
|
||
->find();
|
||
if (!$warehouse) {
|
||
return api_error('目标仓库不存在或已停用', 404);
|
||
}
|
||
|
||
$product = Db::name('order_products')->where('order_id', $id)->find();
|
||
$categoryId = !empty($product['category_id']) ? (int)$product['category_id'] : null;
|
||
$allowedWarehouses = (new WarehouseService())->optionsForOrder((string)$order['service_provider'], $categoryId);
|
||
$allowedIds = array_column($allowedWarehouses, 'id');
|
||
if (!in_array($warehouseId, $allowedIds, true)) {
|
||
return api_error('目标仓库不适用于当前订单服务类型或品类', 422);
|
||
}
|
||
|
||
$currentTarget = Db::name('order_shipping_targets')->where('order_id', $id)->find();
|
||
if ($currentTarget && (int)($currentTarget['warehouse_id'] ?? 0) === $warehouseId) {
|
||
return api_error('当前订单已绑定该仓库,无需重复改派', 422);
|
||
}
|
||
|
||
$snapshot = [
|
||
'warehouse_id' => (int)$warehouse['id'],
|
||
'warehouse_name' => $warehouse['warehouse_name'],
|
||
'warehouse_code' => $warehouse['warehouse_code'],
|
||
'receiver_name' => $warehouse['receiver_name'],
|
||
'receiver_mobile' => $warehouse['receiver_mobile'],
|
||
'province' => $warehouse['province'],
|
||
'city' => $warehouse['city'],
|
||
'district' => $warehouse['district'],
|
||
'detail_address' => $warehouse['detail_address'],
|
||
'service_time' => $warehouse['service_time'],
|
||
'notice' => $warehouse['notice'],
|
||
];
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
Db::startTrans();
|
||
try {
|
||
(new WarehouseService())->bindOrderTarget($id, (string)$order['service_provider'], $categoryId);
|
||
Db::name('order_shipping_targets')->where('order_id', $id)->update([
|
||
'warehouse_id' => $snapshot['warehouse_id'],
|
||
'warehouse_name' => $snapshot['warehouse_name'],
|
||
'warehouse_code' => $snapshot['warehouse_code'],
|
||
'service_provider' => $order['service_provider'],
|
||
'receiver_name' => $snapshot['receiver_name'],
|
||
'receiver_mobile' => $snapshot['receiver_mobile'],
|
||
'province' => $snapshot['province'],
|
||
'city' => $snapshot['city'],
|
||
'district' => $snapshot['district'],
|
||
'detail_address' => $snapshot['detail_address'],
|
||
'service_time' => $snapshot['service_time'],
|
||
'notice' => $snapshot['notice'],
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_timelines')->insert([
|
||
'order_id' => $id,
|
||
'node_code' => 'warehouse_reassigned',
|
||
'node_text' => '仓库已改派',
|
||
'node_desc' => sprintf('订单收货仓库已改派至 %s', $snapshot['warehouse_name']),
|
||
'operator_type' => 'admin',
|
||
'operator_id' => (int)$request->header('x-admin-id', 0) ?: null,
|
||
'occurred_at' => $now,
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return api_error('仓库改派失败', 500, [
|
||
'detail' => $e->getMessage(),
|
||
]);
|
||
}
|
||
|
||
return api_success([
|
||
'id' => $id,
|
||
'warehouse_id' => $snapshot['warehouse_id'],
|
||
'warehouse_name' => $snapshot['warehouse_name'],
|
||
], '仓库已改派');
|
||
}
|
||
|
||
public function receiveLogistics(Request $request)
|
||
{
|
||
$id = (int)$request->input('id', 0);
|
||
if ($id <= 0) {
|
||
return api_error('订单 ID 不能为空', 422);
|
||
}
|
||
|
||
$order = Db::name('orders')->where('id', $id)->find();
|
||
if (!$order) {
|
||
return api_error('订单不存在', 404);
|
||
}
|
||
|
||
$logistics = Db::name('order_logistics')
|
||
->where('order_id', $id)
|
||
->where('logistics_type', 'send_to_center')
|
||
->order('id', 'desc')
|
||
->find();
|
||
$allowEnterpriseManualReceive = ($order['source_channel'] ?? '') === 'enterprise_push';
|
||
if ((!$logistics || $logistics['tracking_no'] === '') && !$allowEnterpriseManualReceive) {
|
||
return api_error('当前订单还没有有效运单信息', 422);
|
||
}
|
||
|
||
if ($order['order_status'] !== 'pending_shipping') {
|
||
return api_error('当前订单状态不支持标记签收', 422);
|
||
}
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
$latestDesc = '鉴定中心已签收包裹,等待鉴定师开始处理。';
|
||
|
||
Db::startTrans();
|
||
try {
|
||
if ($logistics) {
|
||
Db::name('order_logistics')->where('id', $logistics['id'])->update([
|
||
'tracking_status' => 'received',
|
||
'latest_desc' => $latestDesc,
|
||
'latest_time' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
$logisticsId = (int)$logistics['id'];
|
||
} else {
|
||
$latestDesc = '大客户推送订单已确认到仓,等待鉴定师开始处理。';
|
||
$logisticsId = (int)Db::name('order_logistics')->insertGetId([
|
||
'order_id' => $id,
|
||
'logistics_type' => 'send_to_center',
|
||
'express_company' => '',
|
||
'tracking_no' => '',
|
||
'tracking_status' => 'received',
|
||
'latest_desc' => $latestDesc,
|
||
'latest_time' => $now,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
|
||
Db::name('order_logistics_nodes')->insert([
|
||
'logistics_id' => $logisticsId,
|
||
'node_time' => $now,
|
||
'node_desc' => $latestDesc,
|
||
'node_location' => '鉴定中心',
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
Db::name('orders')->where('id', $id)->update([
|
||
'order_status' => 'in_first_review',
|
||
'display_status' => '鉴定中',
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
$taskUpdate = [
|
||
'status' => 'processing',
|
||
'updated_at' => $now,
|
||
];
|
||
$task = Db::name('appraisal_tasks')
|
||
->where('order_id', $id)
|
||
->where('task_stage', 'first_review')
|
||
->order('id', 'asc')
|
||
->find();
|
||
if ($task && empty($task['started_at'])) {
|
||
$taskUpdate['started_at'] = $now;
|
||
}
|
||
if ($task) {
|
||
Db::name('appraisal_tasks')->where('id', (int)$task['id'])->update($taskUpdate);
|
||
}
|
||
|
||
Db::name('order_timelines')->insert([
|
||
'order_id' => $id,
|
||
'node_code' => 'first_review',
|
||
'node_text' => '鉴定中',
|
||
'node_desc' => $logistics
|
||
? '包裹已由鉴定中心签收,订单已进入鉴定流程'
|
||
: '大客户推送订单已确认到仓,订单已进入鉴定流程',
|
||
'operator_type' => 'admin',
|
||
'operator_id' => (int)$request->header('x-admin-id', 0) ?: null,
|
||
'occurred_at' => $now,
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return api_error('标记签收失败', 500, [
|
||
'detail' => $e->getMessage(),
|
||
]);
|
||
}
|
||
|
||
(new EnterpriseWebhookService())->recordOrderEvent($id, 'inbound_received', [
|
||
'express_company' => (string)($logistics['express_company'] ?? ''),
|
||
'tracking_no' => (string)($logistics['tracking_no'] ?? ''),
|
||
'received_at' => $now,
|
||
]);
|
||
|
||
return api_success(['id' => $id], '已标记鉴定中心签收');
|
||
}
|
||
|
||
public function saveReturnLogistics(Request $request)
|
||
{
|
||
$id = (int)$request->input('id', 0);
|
||
$expressCompany = trim((string)$request->input('express_company', ''));
|
||
$trackingNo = trim((string)$request->input('tracking_no', ''));
|
||
|
||
if ($id <= 0 || $expressCompany === '' || $trackingNo === '') {
|
||
return api_error('订单、快递公司和运单号不能为空', 422);
|
||
}
|
||
|
||
$order = Db::name('orders')->where('id', $id)->find();
|
||
if (!$order) {
|
||
return api_error('订单不存在', 404);
|
||
}
|
||
if (!in_array($order['order_status'], ['report_published', 'completed'], true)) {
|
||
return api_error('当前订单状态不支持登记回寄运单', 422);
|
||
}
|
||
|
||
$report = Db::name('reports')->where('order_id', $id)->order('id', 'desc')->find();
|
||
if (!$report || ($report['report_status'] ?? '') !== 'published') {
|
||
return api_error('订单报告未发布前,物品不允许寄回', 422);
|
||
}
|
||
|
||
$returnAddress = Db::name('order_return_addresses')->where('order_id', $id)->find();
|
||
if (!$returnAddress) {
|
||
$fallbackAddress = Db::name('user_addresses')
|
||
->where('user_id', (int)$order['user_id'])
|
||
->where('is_default', 1)
|
||
->order('id', 'desc')
|
||
->find()
|
||
?: Db::name('user_addresses')
|
||
->where('user_id', (int)$order['user_id'])
|
||
->order('id', 'desc')
|
||
->find();
|
||
|
||
if (!$fallbackAddress) {
|
||
return api_error('当前订单尚未确认寄回地址,且用户账户下没有可用地址', 422);
|
||
}
|
||
|
||
$returnAddress = [
|
||
'user_address_id' => (int)$fallbackAddress['id'],
|
||
'consignee' => $fallbackAddress['consignee'],
|
||
'mobile' => $fallbackAddress['mobile'],
|
||
'province' => $fallbackAddress['province'],
|
||
'city' => $fallbackAddress['city'],
|
||
'district' => $fallbackAddress['district'],
|
||
'detail_address' => $fallbackAddress['detail_address'],
|
||
];
|
||
}
|
||
|
||
$existing = Db::name('order_logistics')
|
||
->where('order_id', $id)
|
||
->where('logistics_type', 'return_to_user')
|
||
->order('id', 'desc')
|
||
->find();
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
$latestDesc = sprintf('平台已通过 %s 回寄商品,运单号 %s。', $expressCompany, $trackingNo);
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$existingReturnAddress = Db::name('order_return_addresses')->where('order_id', $id)->find();
|
||
if (!$existingReturnAddress) {
|
||
Db::name('order_return_addresses')->insert([
|
||
'order_id' => $id,
|
||
'user_address_id' => $returnAddress['user_address_id'] ?? null,
|
||
'consignee' => $returnAddress['consignee'] ?? '',
|
||
'mobile' => $returnAddress['mobile'] ?? '',
|
||
'province' => $returnAddress['province'] ?? '',
|
||
'city' => $returnAddress['city'] ?? '',
|
||
'district' => $returnAddress['district'] ?? '',
|
||
'detail_address' => $returnAddress['detail_address'] ?? '',
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
|
||
if ($existing) {
|
||
Db::name('order_logistics')->where('id', $existing['id'])->update([
|
||
'logistics_type' => 'return_to_user',
|
||
'express_company' => $expressCompany,
|
||
'tracking_no' => $trackingNo,
|
||
'tracking_status' => 'in_transit',
|
||
'latest_desc' => $latestDesc,
|
||
'latest_time' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
$logisticsId = (int)$existing['id'];
|
||
$nodeText = '已更新回寄运单';
|
||
$nodeDesc = sprintf('平台更新回寄运单:%s %s', $expressCompany, $trackingNo);
|
||
} else {
|
||
$logisticsId = (int)Db::name('order_logistics')->insertGetId([
|
||
'order_id' => $id,
|
||
'logistics_type' => 'return_to_user',
|
||
'express_company' => $expressCompany,
|
||
'tracking_no' => $trackingNo,
|
||
'tracking_status' => 'in_transit',
|
||
'latest_desc' => $latestDesc,
|
||
'latest_time' => $now,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
$nodeText = '已寄回用户';
|
||
$nodeDesc = sprintf('平台已通过 %s 回寄商品,运单号 %s', $expressCompany, $trackingNo);
|
||
}
|
||
|
||
Db::name('order_logistics_nodes')->insert([
|
||
'logistics_id' => $logisticsId,
|
||
'node_time' => $now,
|
||
'node_desc' => $latestDesc,
|
||
'node_location' => $returnAddress['city'] ?? '用户地址',
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
Db::name('orders')->where('id', $id)->update([
|
||
'order_status' => 'completed',
|
||
'display_status' => '物品已寄回',
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_timelines')->insert([
|
||
'order_id' => $id,
|
||
'node_code' => 'return_shipped',
|
||
'node_text' => $nodeText,
|
||
'node_desc' => $nodeDesc,
|
||
'operator_type' => 'admin',
|
||
'operator_id' => (int)$request->header('x-admin-id', 0) ?: null,
|
||
'occurred_at' => $now,
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
(new MessageDispatcher())->sendInboxEvent('return_shipped', [
|
||
'user_id' => (int)($order['user_id'] ?? 0),
|
||
'biz_type' => 'return_shipped',
|
||
'biz_id' => $id,
|
||
'express_company' => $expressCompany,
|
||
'tracking_no' => $trackingNo,
|
||
'fallback_title' => '鉴定物品已寄回',
|
||
'fallback_content' => sprintf('平台已通过%s回寄鉴定物品,运单号 %s,可前往订单详情查看物流进度。', $expressCompany, $trackingNo),
|
||
]);
|
||
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return api_error('回寄运单登记失败', 500, [
|
||
'detail' => $e->getMessage(),
|
||
]);
|
||
}
|
||
|
||
(new EnterpriseWebhookService())->recordOrderEvent($id, 'return_shipped', [
|
||
'express_company' => $expressCompany,
|
||
'tracking_no' => $trackingNo,
|
||
'shipped_at' => $now,
|
||
]);
|
||
(new OrderLogisticsSyncService())->subscribeAsync($logisticsId);
|
||
|
||
return api_success([
|
||
'id' => $id,
|
||
'express_company' => $expressCompany,
|
||
'tracking_no' => $trackingNo,
|
||
], '回寄运单已登记');
|
||
}
|
||
|
||
public function receiveReturnLogistics(Request $request)
|
||
{
|
||
$id = (int)$request->input('id', 0);
|
||
if ($id <= 0) {
|
||
return api_error('订单 ID 不能为空', 422);
|
||
}
|
||
|
||
$order = Db::name('orders')->where('id', $id)->find();
|
||
if (!$order) {
|
||
return api_error('订单不存在', 404);
|
||
}
|
||
|
||
$logistics = Db::name('order_logistics')
|
||
->where('order_id', $id)
|
||
->where('logistics_type', 'return_to_user')
|
||
->order('id', 'desc')
|
||
->find();
|
||
if (!$logistics || $logistics['tracking_no'] === '') {
|
||
return api_error('当前订单还没有有效回寄运单', 422);
|
||
}
|
||
if (($logistics['tracking_status'] ?? '') === 'received') {
|
||
return api_error('当前订单已标记用户签收,无需重复操作', 422);
|
||
}
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
$latestDesc = '用户已签收回寄商品,本次订单已完成。';
|
||
|
||
Db::startTrans();
|
||
try {
|
||
Db::name('order_logistics')->where('id', $logistics['id'])->update([
|
||
'tracking_status' => 'received',
|
||
'latest_desc' => $latestDesc,
|
||
'latest_time' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_logistics_nodes')->insert([
|
||
'logistics_id' => $logistics['id'],
|
||
'node_time' => $now,
|
||
'node_desc' => $latestDesc,
|
||
'node_location' => '用户地址',
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
Db::name('orders')->where('id', $id)->update([
|
||
'order_status' => 'completed',
|
||
'display_status' => '已完成',
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_timelines')->insert([
|
||
'order_id' => $id,
|
||
'node_code' => 'return_received',
|
||
'node_text' => '用户已签收',
|
||
'node_desc' => '回寄商品已由用户签收,本次订单已完成。',
|
||
'operator_type' => 'admin',
|
||
'operator_id' => (int)$request->header('x-admin-id', 0) ?: null,
|
||
'occurred_at' => $now,
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
(new MessageDispatcher())->sendInboxEvent('return_received', [
|
||
'user_id' => (int)($order['user_id'] ?? 0),
|
||
'biz_type' => 'return_received',
|
||
'biz_id' => $id,
|
||
'fallback_title' => '回寄商品已签收',
|
||
'fallback_content' => '系统已确认您签收回寄商品,本次鉴定订单已完成。',
|
||
]);
|
||
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return api_error('标记用户签收失败', 500, [
|
||
'detail' => $e->getMessage(),
|
||
]);
|
||
}
|
||
|
||
(new EnterpriseWebhookService())->recordOrderEvent($id, 'completed', [
|
||
'express_company' => (string)($logistics['express_company'] ?? ''),
|
||
'tracking_no' => (string)($logistics['tracking_no'] ?? ''),
|
||
'completed_at' => $now,
|
||
]);
|
||
|
||
return api_success(['id' => $id], '已标记用户签收');
|
||
}
|
||
|
||
public function createManualOrder(Request $request)
|
||
{
|
||
$serviceProvider = $this->normalizeServiceProvider((string)$request->input('service_provider', 'anxinyan'));
|
||
$pricePackageId = (int)$request->input('price_package_id', 0);
|
||
$pricePackageCode = trim((string)$request->input('price_package_code', ''));
|
||
$productInput = $this->requestArray($request, 'product_info');
|
||
$extraInput = $this->requestArray($request, 'extra_info');
|
||
$returnAddressInput = $this->requestArray($request, 'return_address');
|
||
$materialsInput = $request->input('materials', []);
|
||
$materials = is_array($materialsInput) ? $materialsInput : [];
|
||
|
||
$categoryId = (int)($productInput['category_id'] ?? 0);
|
||
$brandId = (int)($productInput['brand_id'] ?? 0);
|
||
$brandName = $this->limitManualText(trim((string)($productInput['brand_name'] ?? '')), 128);
|
||
$productName = trim((string)($productInput['product_name'] ?? ''));
|
||
$consignee = trim((string)($returnAddressInput['consignee'] ?? ''));
|
||
$mobile = trim((string)($returnAddressInput['mobile'] ?? ''));
|
||
$province = trim((string)($returnAddressInput['province'] ?? ''));
|
||
$city = trim((string)($returnAddressInput['city'] ?? ''));
|
||
$district = trim((string)($returnAddressInput['district'] ?? ''));
|
||
$detailAddress = trim((string)($returnAddressInput['detail_address'] ?? ''));
|
||
|
||
if ($serviceProvider === '') {
|
||
return api_error('服务类型不正确', 422);
|
||
}
|
||
if ($categoryId <= 0) {
|
||
return api_error('请选择品类', 422);
|
||
}
|
||
if ($consignee === '' || $mobile === '' || $province === '' || $city === '' || $district === '' || $detailAddress === '') {
|
||
return api_error('请完整填写寄回收件信息', 422);
|
||
}
|
||
|
||
$category = Db::name('catalog_categories')->where('id', $categoryId)->find();
|
||
if (!$category) {
|
||
return api_error('品类不存在', 422);
|
||
}
|
||
$brand = null;
|
||
if ($brandId > 0) {
|
||
$brand = Db::name('catalog_brands')->where('id', $brandId)->find();
|
||
if (!$brand) {
|
||
return api_error('品牌不存在', 422);
|
||
}
|
||
if ($brandName === '') {
|
||
$brandName = (string)$brand['name'];
|
||
}
|
||
}
|
||
if ($productName === '') {
|
||
$productName = trim((string)$category['name'] . ' ' . $brandName);
|
||
}
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
try {
|
||
$servicePackage = $this->pricePackageSnapshot($serviceProvider, $pricePackageId, $pricePackageCode);
|
||
} catch (\RuntimeException $e) {
|
||
return api_error($e->getMessage(), 422);
|
||
}
|
||
$orderNo = $this->generateOrderNo();
|
||
$appraisalNo = $this->generateAppraisalNo();
|
||
$estimated = date('Y-m-d H:i:s', strtotime(sprintf('+%d hours', (int)$servicePackage['sla_hours'])));
|
||
$operatorId = (int)$request->header('x-admin-id', 0) ?: null;
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$user = $this->resolveManualOrderUser($consignee, $mobile, $now);
|
||
$addressId = $this->ensureUserAddress((int)$user['id'], [
|
||
'consignee' => $consignee,
|
||
'mobile' => $mobile,
|
||
'province' => $province,
|
||
'city' => $city,
|
||
'district' => $district,
|
||
'detail_address' => $detailAddress,
|
||
], $now);
|
||
|
||
$orderId = (int)Db::name('orders')->insertGetId([
|
||
'order_no' => $orderNo,
|
||
'appraisal_no' => $appraisalNo,
|
||
'user_id' => (int)$user['id'],
|
||
'service_mode' => 'physical',
|
||
'service_provider' => $serviceProvider,
|
||
'payment_status' => 'paid',
|
||
'order_status' => 'pending_shipping',
|
||
'display_status' => '待入库',
|
||
'estimated_finish_time' => $estimated,
|
||
'source_channel' => self::MANUAL_ENTRY_SOURCE,
|
||
'source_customer_id' => '',
|
||
'price_package_id' => $servicePackage['price_package_id'],
|
||
'price_package_name' => $servicePackage['price_package_name'],
|
||
'price_package_code' => $servicePackage['price_package_code'],
|
||
'price_package_price' => $servicePackage['price_package_price'],
|
||
'pay_amount' => (float)$servicePackage['pay_amount'],
|
||
'paid_at' => $now,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_products')->insert([
|
||
'order_id' => $orderId,
|
||
'category_id' => $categoryId,
|
||
'category_name' => (string)$category['name'],
|
||
'brand_id' => $brandId > 0 ? $brandId : null,
|
||
'brand_name' => $brandName,
|
||
'color' => trim((string)($productInput['color'] ?? '')),
|
||
'size_spec' => trim((string)($productInput['size_spec'] ?? '')),
|
||
'serial_no' => trim((string)($productInput['serial_no'] ?? '')),
|
||
'product_name' => $productName,
|
||
'product_cover' => '',
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_extras')->insert([
|
||
'order_id' => $orderId,
|
||
'purchase_channel' => trim((string)($extraInput['purchase_channel'] ?? '')),
|
||
'purchase_price' => (float)($extraInput['purchase_price'] ?? 0),
|
||
'purchase_date' => null,
|
||
'usage_status' => trim((string)($extraInput['usage_status'] ?? '')),
|
||
'condition_desc' => trim((string)($extraInput['condition_desc'] ?? '')),
|
||
'has_accessories' => 0,
|
||
'accessories_json' => json_encode([], JSON_UNESCAPED_UNICODE),
|
||
'remark' => trim((string)($extraInput['remark'] ?? '')),
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_return_addresses')->insert([
|
||
'order_id' => $orderId,
|
||
'user_address_id' => $addressId,
|
||
'consignee' => $consignee,
|
||
'mobile' => $mobile,
|
||
'province' => $province,
|
||
'city' => $city,
|
||
'district' => $district,
|
||
'detail_address' => $detailAddress,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
$shippingTarget = (new WarehouseService())->bindOrderTarget($orderId, $serviceProvider, $categoryId, [
|
||
'province' => $province,
|
||
'city' => $city,
|
||
'district' => $district,
|
||
'detail_address' => $detailAddress,
|
||
]);
|
||
|
||
$this->insertManualOrderMaterials($orderId, $materials, $now);
|
||
|
||
Db::name('appraisal_tasks')->insert([
|
||
'order_id' => $orderId,
|
||
'task_stage' => 'first_review',
|
||
'service_provider' => $serviceProvider,
|
||
'status' => 'pending',
|
||
'assignee_id' => null,
|
||
'assignee_name' => '未分配',
|
||
'started_at' => null,
|
||
'submitted_at' => null,
|
||
'sla_deadline' => $estimated,
|
||
'is_overtime' => 0,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_timelines')->insertAll([
|
||
[
|
||
'order_id' => $orderId,
|
||
'node_code' => 'manual_created',
|
||
'node_text' => '补录订单已创建',
|
||
'node_desc' => '后台已补录订单资料,等待仓管入库。',
|
||
'operator_type' => 'admin',
|
||
'operator_id' => $operatorId,
|
||
'occurred_at' => $now,
|
||
'created_at' => $now,
|
||
],
|
||
[
|
||
'order_id' => $orderId,
|
||
'node_code' => 'pending_inbound',
|
||
'node_text' => '待入库',
|
||
'node_desc' => sprintf('可使用订单号或鉴定单号匹配入库,目标仓库:%s。', $shippingTarget['warehouse_name'] ?: '鉴定中心'),
|
||
'operator_type' => 'system',
|
||
'operator_id' => null,
|
||
'occurred_at' => $now,
|
||
'created_at' => $now,
|
||
],
|
||
]);
|
||
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return api_error('补录订单创建失败', 500, ['detail' => $e->getMessage()]);
|
||
}
|
||
|
||
return api_success([
|
||
'order_id' => $orderId,
|
||
'order_no' => $orderNo,
|
||
'appraisal_no' => $appraisalNo,
|
||
'user_id' => (int)$user['id'],
|
||
'price_package_name' => $servicePackage['price_package_name'],
|
||
'pay_amount' => (float)$servicePackage['pay_amount'],
|
||
'next_status' => 'pending_shipping',
|
||
], '补录订单已创建');
|
||
}
|
||
|
||
public function manualOrderMeta(Request $request)
|
||
{
|
||
$categories = Db::name('catalog_categories')
|
||
->field(['id', 'name', 'code', 'is_enabled', 'supported_service_types'])
|
||
->where('is_enabled', 1)
|
||
->order('sort_order', 'asc')
|
||
->select()
|
||
->toArray();
|
||
$brands = Db::name('catalog_brands')
|
||
->alias('b')
|
||
->leftJoin('catalog_brand_categories cbc', 'cbc.brand_id = b.id')
|
||
->field([
|
||
'b.id',
|
||
'b.name',
|
||
'b.en_name',
|
||
'b.code',
|
||
'b.is_enabled',
|
||
'b.supported_service_types',
|
||
'GROUP_CONCAT(DISTINCT cbc.category_id) AS category_ids',
|
||
])
|
||
->where('b.is_enabled', 1)
|
||
->group('b.id')
|
||
->order('b.sort_order', 'asc')
|
||
->select()
|
||
->toArray();
|
||
|
||
return api_success([
|
||
'categories' => array_map(fn (array $item) => [
|
||
'id' => (int)$item['id'],
|
||
'name' => (string)$item['name'],
|
||
'code' => (string)$item['code'],
|
||
'supported_service_types' => $this->decodeJsonArray($item['supported_service_types'] ?? null),
|
||
], $categories),
|
||
'brands' => array_map(fn (array $item) => [
|
||
'id' => (int)$item['id'],
|
||
'name' => (string)$item['name'],
|
||
'en_name' => (string)($item['en_name'] ?? ''),
|
||
'code' => (string)($item['code'] ?? ''),
|
||
'category_ids' => $this->decodeIntList($item['category_ids'] ?? ''),
|
||
'supported_service_types' => $this->decodeJsonArray($item['supported_service_types'] ?? null),
|
||
], $brands),
|
||
'service_price_packages' => (new AppraisalServicePricePackageService())->serviceOptions(),
|
||
]);
|
||
}
|
||
|
||
public function uploadManualOrderFile(Request $request)
|
||
{
|
||
try {
|
||
return api_success((new AppraisalEvidenceService())->upload($request));
|
||
} catch (\Throwable $e) {
|
||
return api_error($e->getMessage(), 422);
|
||
}
|
||
}
|
||
|
||
private function inboundAttachments(int $orderId, Request $request): array
|
||
{
|
||
$logs = Db::name('order_transfer_flow_logs')
|
||
->where('order_id', $orderId)
|
||
->where('action_code', 'inbound_received')
|
||
->order('id', 'desc')
|
||
->select()
|
||
->toArray();
|
||
|
||
$attachments = [];
|
||
foreach ($logs as $log) {
|
||
$payload = $this->decodeJsonObject($log['payload_json'] ?? null);
|
||
foreach ($this->decodeJsonArray($payload['inbound_attachments'] ?? []) as $item) {
|
||
if (is_array($item)) {
|
||
$attachments[] = $item;
|
||
}
|
||
}
|
||
}
|
||
|
||
$normalized = (new AppraisalEvidenceService())->normalize($attachments, $request);
|
||
|
||
return array_values(array_filter($normalized, function (array $item) {
|
||
return in_array((string)($item['file_type'] ?? ''), ['image', 'video'], true);
|
||
}));
|
||
}
|
||
|
||
private function decodeJsonArray(mixed $value): array
|
||
{
|
||
if (is_array($value)) {
|
||
return array_values($value);
|
||
}
|
||
if (is_string($value) && $value !== '') {
|
||
$decoded = json_decode($value, true);
|
||
return is_array($decoded) ? array_values($decoded) : [];
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
private function decodeJsonObject(mixed $value): array
|
||
{
|
||
if (is_array($value)) {
|
||
return $value;
|
||
}
|
||
if (is_string($value) && $value !== '') {
|
||
$decoded = json_decode($value, true);
|
||
return is_array($decoded) ? $decoded : [];
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
private function decodeIntList(mixed $value): array
|
||
{
|
||
if (is_array($value)) {
|
||
return array_values(array_filter(array_map('intval', $value), fn (int $item) => $item > 0));
|
||
}
|
||
if (!is_string($value) || trim($value) === '') {
|
||
return [];
|
||
}
|
||
|
||
return array_values(array_filter(array_map('intval', explode(',', $value)), fn (int $item) => $item > 0));
|
||
}
|
||
|
||
private function requestArray(Request $request, string $key): array
|
||
{
|
||
$value = $request->input($key, []);
|
||
return is_array($value) ? $value : [];
|
||
}
|
||
|
||
private function normalizeServiceProvider(string $serviceProvider): string
|
||
{
|
||
$serviceProvider = trim($serviceProvider);
|
||
return in_array($serviceProvider, ['anxinyan', 'zhongjian'], true) ? $serviceProvider : '';
|
||
}
|
||
|
||
private function pricePackageSnapshot(string $serviceProvider, int $packageId = 0, string $packageCode = ''): array
|
||
{
|
||
return (new AppraisalServicePricePackageService())->snapshotForOrder($serviceProvider, $packageId, $packageCode);
|
||
}
|
||
|
||
private function generateOrderNo(): string
|
||
{
|
||
do {
|
||
$orderNo = 'AXY' . date('YmdHis') . mt_rand(100, 999);
|
||
} while (Db::name('orders')->where('order_no', $orderNo)->find());
|
||
|
||
return $orderNo;
|
||
}
|
||
|
||
private function generateAppraisalNo(): string
|
||
{
|
||
do {
|
||
$appraisalNo = 'AXY-APP-' . date('Ymd') . '-' . mt_rand(1000, 9999);
|
||
} while (Db::name('orders')->where('appraisal_no', $appraisalNo)->find());
|
||
|
||
return $appraisalNo;
|
||
}
|
||
|
||
private function resolveManualOrderUser(string $consignee, string $mobile, string $now): array
|
||
{
|
||
$user = Db::name('users')
|
||
->where('mobile', $mobile)
|
||
->whereNull('deleted_at')
|
||
->find();
|
||
if ($user) {
|
||
return $user;
|
||
}
|
||
|
||
$userId = (int)Db::name('users')->insertGetId([
|
||
'nickname' => $consignee,
|
||
'avatar' => '',
|
||
'mobile' => $mobile,
|
||
'password' => '',
|
||
'status' => 'enabled',
|
||
'last_login_at' => null,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
return Db::name('users')->where('id', $userId)->find();
|
||
}
|
||
|
||
private function ensureUserAddress(int $userId, array $address, string $now): int
|
||
{
|
||
$existing = Db::name('user_addresses')
|
||
->where('user_id', $userId)
|
||
->where('consignee', (string)$address['consignee'])
|
||
->where('mobile', (string)$address['mobile'])
|
||
->where('province', (string)$address['province'])
|
||
->where('city', (string)$address['city'])
|
||
->where('district', (string)$address['district'])
|
||
->where('detail_address', (string)$address['detail_address'])
|
||
->find();
|
||
if ($existing) {
|
||
return (int)$existing['id'];
|
||
}
|
||
|
||
$hasDefault = Db::name('user_addresses')
|
||
->where('user_id', $userId)
|
||
->where('is_default', 1)
|
||
->find();
|
||
|
||
return (int)Db::name('user_addresses')->insertGetId([
|
||
'user_id' => $userId,
|
||
'consignee' => (string)$address['consignee'],
|
||
'mobile' => (string)$address['mobile'],
|
||
'province' => (string)$address['province'],
|
||
'city' => (string)$address['city'],
|
||
'district' => (string)$address['district'],
|
||
'detail_address' => (string)$address['detail_address'],
|
||
'is_default' => $hasDefault ? 0 : 1,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
|
||
private function insertManualOrderMaterials(int $orderId, array $materials, string $now): void
|
||
{
|
||
$evidenceService = new AppraisalEvidenceService();
|
||
foreach ($materials as $index => $item) {
|
||
if (!is_array($item)) {
|
||
continue;
|
||
}
|
||
$files = $evidenceService->normalize($item['files'] ?? [], null, true);
|
||
if (!$files) {
|
||
continue;
|
||
}
|
||
|
||
$orderUploadId = (int)Db::name('order_upload_items')->insertGetId([
|
||
'order_id' => $orderId,
|
||
'template_id' => null,
|
||
'item_code' => trim((string)($item['item_code'] ?? 'manual_material_' . ($index + 1))),
|
||
'item_name' => trim((string)($item['item_name'] ?? '补录资料')),
|
||
'is_required' => !empty($item['is_required']) ? 1 : 0,
|
||
'source_type' => 'initial',
|
||
'status' => 'uploaded',
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
foreach ($files as $file) {
|
||
Db::name('order_upload_files')->insert([
|
||
'order_upload_item_id' => $orderUploadId,
|
||
'file_id' => (string)($file['file_id'] ?? ''),
|
||
'file_url' => ltrim((string)($file['file_url'] ?? ''), '/'),
|
||
'thumbnail_url' => ltrim((string)($file['thumbnail_url'] ?? ''), '/'),
|
||
'quality_status' => 'uploaded',
|
||
'quality_message' => '',
|
||
'uploaded_by_user_id' => null,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
private function trackingStatusText(string $status, string $logisticsType = 'send_to_center'): string
|
||
{
|
||
if ($logisticsType === 'return_to_user') {
|
||
return match ($status) {
|
||
'submitted' => '已登记回寄运单',
|
||
'in_transit' => '回寄途中',
|
||
'received' => '用户已签收',
|
||
default => $status === '' ? '待回寄' : $status,
|
||
};
|
||
}
|
||
|
||
return match ($status) {
|
||
'submitted' => '用户已提交运单',
|
||
'in_transit' => '用户已寄出,运输中',
|
||
'received' => '鉴定中心已签收',
|
||
default => $status === '' ? '待提交' : $status,
|
||
};
|
||
}
|
||
|
||
private function reportStatusText(string $status): string
|
||
{
|
||
return match ($status) {
|
||
'draft' => '草稿中',
|
||
'pending_publish' => '待发布',
|
||
'published' => '已发布',
|
||
'updated' => '已更新',
|
||
'invalid' => '已作废',
|
||
default => $status,
|
||
};
|
||
}
|
||
|
||
private function displayStatus(string $orderStatus, string $displayStatus, string $returnTrackingNo = '', string $returnTrackingStatus = ''): string
|
||
{
|
||
if ($orderStatus === 'report_published') {
|
||
return '待寄回';
|
||
}
|
||
|
||
if ($orderStatus === 'completed') {
|
||
if ($returnTrackingStatus === 'received') {
|
||
return '已完成';
|
||
}
|
||
if ($returnTrackingNo !== '') {
|
||
return '物品已寄回';
|
||
}
|
||
}
|
||
|
||
return $displayStatus;
|
||
}
|
||
|
||
private function latestLogisticsMap(array $orderIds, string $logisticsType): array
|
||
{
|
||
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
|
||
if (!$orderIds) {
|
||
return [];
|
||
}
|
||
|
||
$rows = Db::name('order_logistics')
|
||
->whereIn('order_id', $orderIds)
|
||
->where('logistics_type', $logisticsType)
|
||
->order('id', 'desc')
|
||
->select()
|
||
->toArray();
|
||
|
||
$map = [];
|
||
foreach ($rows as $row) {
|
||
$orderId = (int)($row['order_id'] ?? 0);
|
||
if ($orderId > 0 && !isset($map[$orderId])) {
|
||
$map[$orderId] = [
|
||
'tracking_no' => (string)($row['tracking_no'] ?? ''),
|
||
'tracking_status' => (string)($row['tracking_status'] ?? ''),
|
||
];
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
private function latestTransferFlowMap(array $orderIds): array
|
||
{
|
||
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
|
||
if (!$orderIds) {
|
||
return [];
|
||
}
|
||
|
||
$rows = Db::name('order_transfer_flows')
|
||
->whereIn('order_id', $orderIds)
|
||
->order('id', 'desc')
|
||
->select()
|
||
->toArray();
|
||
|
||
$map = [];
|
||
foreach ($rows as $row) {
|
||
$orderId = (int)($row['order_id'] ?? 0);
|
||
if ($orderId > 0 && !isset($map[$orderId])) {
|
||
$map[$orderId] = [
|
||
'internal_tag_no' => (string)($row['internal_tag_no'] ?? ''),
|
||
];
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
private function warehouseOrderBucket(
|
||
string $orderStatus,
|
||
string $sendTrackingNo = '',
|
||
string $sendTrackingStatus = '',
|
||
string $displayStatus = '',
|
||
string $sourceChannel = ''
|
||
): string
|
||
{
|
||
if ($orderStatus === 'pending_shipping') {
|
||
if ($sourceChannel === self::MANUAL_ENTRY_SOURCE && $sendTrackingNo === '') {
|
||
return 'warehouse_pending_inbound';
|
||
}
|
||
|
||
$hasSubmittedTracking = $sendTrackingNo !== '' && $sendTrackingStatus !== 'received';
|
||
$hasSubmittedDisplayStatus = in_array($displayStatus, ['已提交运单', '用户已提交运单'], true)
|
||
&& $sendTrackingStatus !== 'received';
|
||
if ($hasSubmittedTracking || $hasSubmittedDisplayStatus) {
|
||
return 'warehouse_in_transit';
|
||
}
|
||
}
|
||
|
||
if (in_array($orderStatus, [
|
||
'received',
|
||
'in_first_review',
|
||
'pending_supplement',
|
||
'in_final_review',
|
||
'generating_report',
|
||
], true)) {
|
||
return 'warehouse_received';
|
||
}
|
||
|
||
if ($orderStatus === 'report_published') {
|
||
return 'warehouse_pending_return';
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
private function warehouseOrderBucketText(string $bucket): string
|
||
{
|
||
return match ($bucket) {
|
||
'warehouse_pending_inbound' => '待入库',
|
||
'warehouse_in_transit' => '在途',
|
||
'warehouse_received' => '已入仓',
|
||
'warehouse_pending_return' => '待寄回',
|
||
default => '',
|
||
};
|
||
}
|
||
|
||
private function normalizeOrderSourceChannel(string $sourceChannel): string
|
||
{
|
||
$sourceChannel = trim($sourceChannel);
|
||
$aliases = [
|
||
'wechat_mini_program' => 'mini_program',
|
||
'weixin_mini_program' => 'mini_program',
|
||
'mp_weixin' => 'mini_program',
|
||
'miniapp' => 'mini_program',
|
||
'user_app' => 'mini_program',
|
||
'web_h5' => 'h5',
|
||
'enterprise' => 'enterprise_push',
|
||
'enterprise_order' => 'enterprise_push',
|
||
'customer_push' => 'enterprise_push',
|
||
'large_customer_push' => 'enterprise_push',
|
||
'manual' => self::MANUAL_ENTRY_SOURCE,
|
||
'manual_order' => self::MANUAL_ENTRY_SOURCE,
|
||
'manual_entry' => self::MANUAL_ENTRY_SOURCE,
|
||
];
|
||
$sourceChannel = $aliases[$sourceChannel] ?? $sourceChannel;
|
||
|
||
return in_array($sourceChannel, ['mini_program', 'h5', 'enterprise_push', self::MANUAL_ENTRY_SOURCE], true) ? $sourceChannel : '';
|
||
}
|
||
|
||
private function sourceChannelText(string $sourceChannel): string
|
||
{
|
||
return match ($this->normalizeOrderSourceChannel($sourceChannel)) {
|
||
'mini_program' => '小程序',
|
||
'h5' => 'H5',
|
||
'enterprise_push' => '大客户推送订单',
|
||
self::MANUAL_ENTRY_SOURCE => '后台补录订单',
|
||
default => '未知渠道',
|
||
};
|
||
}
|
||
|
||
private function limitManualText(string $value, int $maxLength): string
|
||
{
|
||
if (function_exists('mb_substr')) {
|
||
return mb_substr($value, 0, $maxLength, 'UTF-8');
|
||
}
|
||
|
||
return substr($value, 0, $maxLength);
|
||
}
|
||
|
||
private function formatAdminLogisticsDesc(string $logisticsType, string $status, string $expressCompany, string $trackingNo, string $fallback): string
|
||
{
|
||
$expressCompany = trim($expressCompany);
|
||
$trackingNo = trim($trackingNo);
|
||
|
||
if ($logisticsType === 'return_to_user') {
|
||
if (in_array($status, ['submitted', 'in_transit'], true) && $expressCompany !== '' && $trackingNo !== '') {
|
||
return sprintf('平台已登记回寄运单:%s %s,商品正在回寄途中。', $expressCompany, $trackingNo);
|
||
}
|
||
|
||
if ($status === 'received') {
|
||
return '用户已签收回寄商品,订单已完成。';
|
||
}
|
||
|
||
return $fallback;
|
||
}
|
||
|
||
if ($status === 'submitted' && $expressCompany !== '' && $trackingNo !== '') {
|
||
return sprintf('用户已提交寄送运单:%s %s,等待鉴定中心签收。', $expressCompany, $trackingNo);
|
||
}
|
||
|
||
if ($status === 'in_transit' && $expressCompany !== '' && $trackingNo !== '') {
|
||
return sprintf('用户已寄出商品:%s %s,当前运输中。', $expressCompany, $trackingNo);
|
||
}
|
||
|
||
if ($status === 'received') {
|
||
return '鉴定中心已签收包裹,等待鉴定师开始处理。';
|
||
}
|
||
|
||
return $fallback;
|
||
}
|
||
}
|