lookupInboundByInboundNo($trackingNo); } public function lookupInboundByInboundNo(string $inboundNo): array { $match = $this->resolveInboundOrder($inboundNo); return $this->formatOrderContext((int)$match['order_id']); } public function receiveInbound(string $inboundNo, string $tagNo, Request $request, mixed $attachments = []): array { $operator = $this->operator($request); $match = $this->resolveInboundOrder($inboundNo); $context = $this->formatOrderContext((int)$match['order_id']); $order = $context['order_info']; $orderId = (int)$order['id']; if (!in_array((string)$order['order_status'], ['pending_shipping', 'received'], true)) { throw new \InvalidArgumentException('当前订单状态不支持入库绑定'); } $tagNo = $this->normalizeTagNo($tagNo); if ($tagNo === '') { throw new \InvalidArgumentException('请扫描或输入内部流转挂牌编号'); } $now = date('Y-m-d H:i:s'); $inboundAttachments = (new AppraisalEvidenceService())->normalize($attachments, $request, true); $attachmentCount = count($inboundAttachments); $inboundRemark = $this->inboundMatchRemark($match); if ($attachmentCount > 0) { $inboundRemark .= sprintf(',已上传拆包附件 %d 个', $attachmentCount); } Db::startTrans(); try { $tag = $this->ensureTransferTag($tagNo, $operator, $now); $activeFlow = $this->findActiveFlowByTagId((int)$tag['id']); if ($activeFlow && (int)$activeFlow['order_id'] !== $orderId) { Db::rollback(); throw new \InvalidArgumentException('该内部流转挂牌已绑定其他未结束订单'); } $flow = Db::name('order_transfer_flows') ->where('order_id', $orderId) ->where('flow_status', '<>', 'ended') ->order('id', 'desc') ->find(); if ($flow && (int)$flow['internal_tag_id'] !== (int)$tag['id']) { Db::rollback(); throw new \InvalidArgumentException('当前订单已绑定其他内部流转挂牌'); } if (!$flow) { $flowId = (int)Db::name('order_transfer_flows')->insertGetId([ 'order_id' => $orderId, 'internal_tag_id' => (int)$tag['id'], 'internal_tag_no' => $tagNo, 'service_provider' => (string)$order['service_provider'], 'flow_status' => 'active', 'current_stage' => 'warehouse_received', 'current_location' => 'warehouse_pending_inspection', 'inbound_by' => $operator['id'], 'inbound_by_name' => $operator['name'], 'inbound_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]); $flow = Db::name('order_transfer_flows')->where('id', $flowId)->find(); } else { Db::name('order_transfer_flows')->where('id', (int)$flow['id'])->update([ 'current_stage' => 'warehouse_received', 'current_location' => 'warehouse_pending_inspection', 'inbound_by' => $flow['inbound_by'] ?: $operator['id'], 'inbound_by_name' => $flow['inbound_by_name'] ?: $operator['name'], 'inbound_at' => $flow['inbound_at'] ?: $now, 'updated_at' => $now, ]); $flow = Db::name('order_transfer_flows')->where('id', (int)$flow['id'])->find(); } Db::name('internal_transfer_tags')->where('id', (int)$tag['id'])->update([ 'bind_status' => 'bound', 'current_order_id' => $orderId, 'current_flow_id' => (int)$flow['id'], 'current_stage' => 'warehouse_received', 'current_location' => 'warehouse_pending_inspection', 'bound_by' => $operator['id'], 'bound_by_name' => $operator['name'], 'bound_at' => $now, 'released_by' => null, 'released_by_name' => '', 'released_at' => null, 'updated_at' => $now, ]); $logistics = Db::name('order_logistics') ->where('order_id', $orderId) ->where('logistics_type', 'send_to_center') ->order('id', 'desc') ->when(($match['match_type'] ?? '') === 'tracking_no', fn ($query) => $query->where('tracking_no', (string)$match['match_no'])) ->find(); if ($logistics) { Db::name('order_logistics')->where('id', (int)$logistics['id'])->update([ 'tracking_status' => 'received', 'latest_desc' => '仓库已扫描寄入包裹并完成入库。', 'latest_time' => $now, 'updated_at' => $now, ]); Db::name('order_logistics_nodes')->insert([ 'logistics_id' => (int)$logistics['id'], 'node_time' => $now, 'node_desc' => '仓库已扫描寄入包裹并完成入库。', 'node_location' => '仓库', 'created_at' => $now, ]); } Db::name('orders')->where('id', $orderId)->update([ 'order_status' => 'received', 'display_status' => '已入仓待检', 'updated_at' => $now, ]); $this->insertTimeline($orderId, 'inbound_received', '已入仓待检', '仓管扫描寄入运单并完成物品入库。', $operator, $now); $this->insertFlowLog($flow, 'inbound_received', '入库完成', '', '', 'warehouse_received', 'warehouse_pending_inspection', $operator, $inboundRemark, $now, [ 'match_type' => (string)($match['match_type'] ?? ''), 'match_no' => (string)($match['match_no'] ?? ''), 'inbound_attachments' => $inboundAttachments, ]); $this->insertFlowLog($flow, 'internal_tag_bound', '绑定内部流转挂牌', 'warehouse_received', 'warehouse_pending_inspection', 'warehouse_received', 'warehouse_pending_inspection', $operator, $tagNo, $now); Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } return $this->formatOrderContext($orderId, $request); } public function scanTransferForAppraisal(string $tagNo, Request $request): array { $operator = $this->operator($request); $flow = $this->findActiveFlowByTagNo($tagNo); if (!$flow) { throw new \RuntimeException('未找到可用的内部流转挂牌', 404); } $order = Db::name('orders')->where('id', (int)$flow['order_id'])->find(); if (!$order) { throw new \RuntimeException('订单不存在', 404); } $serviceProvider = (string)($order['service_provider'] ?? ''); $stage = (string)($flow['current_stage'] ?? ''); if ($serviceProvider === 'zhongjian' && !in_array($stage, ['zhongjian_returned', 'appraising'], true)) { throw new \InvalidArgumentException('中检订单需完成送检入库后才能录入报告'); } if ($serviceProvider !== 'zhongjian' && !in_array($stage, ['warehouse_received', 'appraising'], true)) { throw new \InvalidArgumentException('当前流转状态不支持进入鉴定作业'); } $task = Db::name('appraisal_tasks') ->where('order_id', (int)$flow['order_id']) ->where('task_stage', 'first_review') ->order('id', 'asc') ->find(); if (!$task) { throw new \RuntimeException('鉴定任务不存在', 404); } $now = date('Y-m-d H:i:s'); Db::startTrans(); try { $taskUpdate = [ 'status' => 'processing', 'started_at' => $task['started_at'] ?: $now, 'updated_at' => $now, ]; if (empty($task['assignee_id']) || empty($task['assignee_name']) || $task['assignee_name'] === '未分配') { $taskUpdate['assignee_id'] = $operator['id']; $taskUpdate['assignee_name'] = $operator['name']; } Db::name('appraisal_tasks')->where('id', (int)$task['id'])->update($taskUpdate); Db::name('orders')->where('id', (int)$flow['order_id'])->update([ 'order_status' => 'in_first_review', 'display_status' => $serviceProvider === 'zhongjian' ? '中检报告录入中' : '鉴定中', 'updated_at' => $now, ]); $this->updateFlowStage($flow, 'appraising', $serviceProvider === 'zhongjian' ? 'zhongjian_report_entry' : 'appraiser_workbench', [ 'appraisal_started_by' => $flow['appraisal_started_by'] ?: $operator['id'], 'appraisal_started_by_name' => $flow['appraisal_started_by_name'] ?: $operator['name'], 'appraisal_started_at' => $flow['appraisal_started_at'] ?: $now, ], $now); $this->insertTimeline((int)$flow['order_id'], 'appraisal_started', $serviceProvider === 'zhongjian' ? '中检报告录入中' : '鉴定中', $serviceProvider === 'zhongjian' ? '报告录入人已扫描内部流转码进入中检报告录入。' : '鉴定师已扫描内部流转码进入鉴定作业。', $operator, $now); $this->insertFlowLog($flow, 'appraisal_started', $serviceProvider === 'zhongjian' ? '中检报告录入开始' : '鉴定开始', (string)$flow['current_stage'], (string)$flow['current_location'], 'appraising', $serviceProvider === 'zhongjian' ? 'zhongjian_report_entry' : 'appraiser_workbench', $operator, '', $now); Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } return [ 'task_id' => (int)$task['id'], 'order_id' => (int)$flow['order_id'], 'service_provider' => $serviceProvider, 'service_provider_text' => $serviceProvider === 'zhongjian' ? '中检鉴定' : '实物鉴定', ]; } public function lookupZhongjianTransfer(string $tagNo): array { $flow = $this->findActiveFlowByTagNo($tagNo); if (!$flow) { throw new \RuntimeException('未找到可用的内部流转挂牌', 404); } if (($flow['service_provider'] ?? '') !== 'zhongjian') { throw new \InvalidArgumentException('非中检订单不能进行送检出入库'); } $stage = (string)$flow['current_stage']; $nextAction = match ($stage) { 'warehouse_received' => 'outbound', 'sent_to_zhongjian' => 'inbound', default => '', }; $nextActionText = match ($stage) { 'warehouse_received' => '送检出库', 'sent_to_zhongjian' => '送检入库', 'report_published' => '待寄回订单可填写回寄物流', default => '暂无可执行送检动作', }; return array_merge($this->formatOrderContext((int)$flow['order_id']), [ 'next_action' => $nextAction, 'next_action_text' => $nextActionText, ]); } public function zhongjianOutbound(string $tagNo, Request $request): array { return $this->moveZhongjian($tagNo, 'warehouse_received', 'sent_to_zhongjian', 'zhongjian_institution', 'zhongjian_outbound', '送检出库', '仓管扫描内部流转码,物品已送出至中检机构。', $request); } public function zhongjianInbound(string $tagNo, Request $request): array { return $this->moveZhongjian($tagNo, 'sent_to_zhongjian', 'zhongjian_returned', 'warehouse_pending_report_entry', 'zhongjian_inbound', '送检入库', '仓管扫描内部流转码,中检物品已回收入库。', $request); } public function lookupReturn(string $tagNo, Request $request): array { $flow = $this->findActiveFlowByTagNo($tagNo); if (!$flow) { throw new \RuntimeException('未找到可用的内部流转挂牌', 404); } $context = $this->formatOrderContext((int)$flow['order_id'], $request); $report = $context['report_info'] ?? null; if (!$report || ($report['report_status'] ?? '') !== 'published') { throw new \InvalidArgumentException('该报告未发布,不符合寄回条件'); } $this->ensurePendingReturnOrder($flow); return $context + [ 'return_confirmation' => [ 'confirmed' => (string)($flow['current_stage'] ?? '') === 'return_confirmed', 'confirmed_by_name' => (string)($flow['return_confirmed_by_name'] ?? ''), 'confirmed_at' => (string)($flow['return_confirmed_at'] ?? ''), ], ]; } public function verifyReturnMaterialTag(string $tagNo, string $qrInput, Request $request): array { $flow = $this->findActiveFlowByTagNo($tagNo); if (!$flow) { throw new \RuntimeException('未找到可用的内部流转挂牌', 404); } $report = $this->latestReport((int)$flow['order_id']); if (!$report || ($report['report_status'] ?? '') !== 'published') { throw new \InvalidArgumentException('该报告未发布,不符合寄回条件'); } $this->ensurePendingReturnOrder($flow); $tag = (new MaterialTagService())->findTagByInput($qrInput); if (!$tag || (int)($tag['report_id'] ?? 0) !== (int)$report['id']) { throw new \InvalidArgumentException('验真吊牌与当前订单报告不匹配'); } return array_merge($this->formatOrderContext((int)$flow['order_id'], $request), [ 'return_verification' => [ 'verified' => true, 'report_id' => (int)$report['id'], 'report_no' => (string)$report['report_no'], ], ]); } public function confirmZhongjianReturn(string $tagNo, Request $request): array { $operator = $this->operator($request); $flow = $this->findActiveFlowByTagNo($tagNo); if (!$flow) { throw new \RuntimeException('未找到可用的内部流转挂牌', 404); } if (($flow['service_provider'] ?? '') !== 'zhongjian') { throw new \InvalidArgumentException('非中检订单需扫描平台验真吊牌确认'); } $report = $this->latestReport((int)$flow['order_id']); $content = $report ? Db::name('report_contents')->where('report_id', (int)$report['id'])->find() : null; $files = $this->decodeJsonArray($content['zhongjian_report_files_json'] ?? null); if (!$report || ($report['report_status'] ?? '') !== 'published') { throw new \InvalidArgumentException('该报告未发布,不符合寄回条件'); } if (trim((string)($report['zhongjian_report_no'] ?? '')) === '' || !$files) { throw new \InvalidArgumentException('中检报告未完整录入,不能确认寄回'); } return $this->markReturnConfirmed($flow, $operator, 'return_confirmed', '中检报告已确认', '仓管已查看中检报告编号和报告文件。'); } public function confirmReturnReport(string $tagNo, int $reportId, Request $request): array { $operator = $this->operator($request); $flow = $this->findActiveFlowByTagNo($tagNo); if (!$flow) { throw new \RuntimeException('未找到可用的内部流转挂牌', 404); } $report = $this->latestReport((int)$flow['order_id']); if (!$report || ($report['report_status'] ?? '') !== 'published') { throw new \InvalidArgumentException('该报告未发布,不符合寄回条件'); } $this->ensurePendingReturnOrder($flow); if ((int)$report['id'] !== $reportId) { throw new \InvalidArgumentException('确认的报告与当前订单报告不匹配'); } if ((string)($flow['current_stage'] ?? '') === 'return_confirmed') { return $this->formatOrderContext((int)$flow['order_id'], $request); } return $this->markReturnConfirmed($flow, $operator, 'return_confirmed', '回寄确认', '仓管扫描内部流转码确认订单处于待寄回状态。'); } public function shipReturn(string $tagNo, string $expressCompany, string $trackingNo, Request $request, array $packingAttachments = []): array { $operator = $this->operator($request); $flow = $this->findActiveFlowByTagNo($tagNo); if (!$flow) { throw new \RuntimeException('未找到可用的内部流转挂牌', 404); } $report = $this->latestReport((int)$flow['order_id']); if (!$report || ($report['report_status'] ?? '') !== 'published') { throw new \InvalidArgumentException('该报告未发布,不符合寄回条件'); } if ((string)($flow['current_stage'] ?? '') !== 'return_confirmed') { throw new \InvalidArgumentException('请先完成报告确认,再登记回寄运单'); } $expressCompany = trim($expressCompany); $trackingNo = trim($trackingNo); if ($expressCompany === '' || $trackingNo === '') { throw new \InvalidArgumentException('请填写回寄快递公司和运单号'); } $packingAttachments = $this->normalizeAssetList($packingAttachments, $request); foreach ($packingAttachments as $asset) { if (!in_array((string)($asset['file_type'] ?? ''), ['image', 'video'], true)) { throw new \InvalidArgumentException('打包装箱附件仅支持图片或视频'); } } $orderId = (int)$flow['order_id']; $order = Db::name('orders')->where('id', $orderId)->find(); if (!$order) { throw new \RuntimeException('订单不存在', 404); } $returnAddress = $this->returnAddressForOrder($order); if (!$returnAddress) { throw new \InvalidArgumentException('当前订单尚未确认寄回地址,且用户账户下没有可用地址'); } $now = date('Y-m-d H:i:s'); $latestDesc = sprintf('平台已通过 %s 回寄商品,运单号 %s。', $expressCompany, $trackingNo); Db::startTrans(); try { $this->ensureReturnAddressSnapshot($orderId, $returnAddress, $now); $existing = Db::name('order_logistics') ->where('order_id', $orderId) ->where('logistics_type', 'return_to_user') ->order('id', 'desc') ->find(); if ($existing) { Db::name('order_logistics')->where('id', (int)$existing['id'])->update([ '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' => $orderId, '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', $orderId)->update([ 'order_status' => 'completed', 'display_status' => '物品已寄回', 'updated_at' => $now, ]); Db::name('order_transfer_flows')->where('id', (int)$flow['id'])->update([ 'flow_status' => 'ended', 'current_stage' => 'return_shipped', 'current_location' => 'ended', 'return_shipped_by' => $operator['id'], 'return_shipped_by_name' => $operator['name'], 'return_shipped_at' => $now, 'ended_at' => $now, 'updated_at' => $now, ]); Db::name('internal_transfer_tags')->where('id', (int)$flow['internal_tag_id'])->update([ 'bind_status' => 'released', 'current_order_id' => null, 'current_flow_id' => null, 'current_stage' => 'idle', 'current_location' => 'warehouse', 'released_by' => $operator['id'], 'released_by_name' => $operator['name'], 'released_at' => $now, 'updated_at' => $now, ]); $this->insertTimeline($orderId, 'return_shipped', $nodeText, $nodeDesc, $operator, $now); $this->insertFlowLog($flow, 'return_shipped', '物品寄回', 'return_confirmed', (string)$flow['current_location'], 'return_shipped', 'ended', $operator, $trackingNo, $now, [ 'packing_attachments' => $packingAttachments, ]); (new MessageDispatcher())->sendInboxEvent('return_shipped', [ 'user_id' => (int)($order['user_id'] ?? 0), 'biz_type' => 'return_shipped', 'biz_id' => $orderId, 'express_company' => $expressCompany, 'tracking_no' => $trackingNo, 'fallback_title' => '鉴定物品已寄回', 'fallback_content' => sprintf('平台已通过%s回寄鉴定物品,运单号 %s,可前往订单详情查看物流进度。', $expressCompany, $trackingNo), ]); Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } (new EnterpriseWebhookService())->recordOrderEvent($orderId, 'return_shipped', [ 'express_company' => $expressCompany, 'tracking_no' => $trackingNo, 'shipped_at' => $now, ]); (new OrderLogisticsSyncService())->subscribeAsync($logisticsId); return $this->formatOrderContext($orderId); } public function markReportPublished(int $orderId, Request $request): void { $flow = Db::name('order_transfer_flows') ->where('order_id', $orderId) ->where('flow_status', '<>', 'ended') ->order('id', 'desc') ->find(); if (!$flow) { return; } $operator = $this->operator($request); $now = date('Y-m-d H:i:s'); $this->updateFlowStage($flow, 'report_published', 'warehouse_return_pending', [ 'report_published_by' => $operator['id'], 'report_published_by_name' => $operator['name'], 'report_published_at' => $now, ], $now); $this->insertFlowLog($flow, 'report_published', '报告已发布', (string)$flow['current_stage'], (string)$flow['current_location'], 'report_published', 'warehouse_return_pending', $operator, '', $now); } public function formatOrderContext(int $orderId, ?Request $request = null): array { $order = Db::name('orders')->where('id', $orderId)->find(); if (!$order) { throw new \RuntimeException('订单不存在', 404); } $product = Db::name('order_products')->where('order_id', $orderId)->find() ?: []; $sendLogistics = Db::name('order_logistics')->where('order_id', $orderId)->where('logistics_type', 'send_to_center')->order('id', 'desc')->find(); $returnLogistics = Db::name('order_logistics')->where('order_id', $orderId)->where('logistics_type', 'return_to_user')->order('id', 'desc')->find(); $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' => '']; $flow = Db::name('order_transfer_flows')->where('order_id', $orderId)->order('id', 'desc')->find(); $report = $this->latestReport($orderId); $content = $report ? Db::name('report_contents')->where('report_id', (int)$report['id'])->find() : null; $returnAddress = $this->returnAddressForOrder($order); $flowLogs = $flow ? Db::name('order_transfer_flow_logs') ->where('flow_id', (int)$flow['id']) ->order('id', 'asc') ->select() ->toArray() : []; return [ 'order_info' => [ 'id' => (int)$order['id'], 'order_no' => (string)$order['order_no'], 'appraisal_no' => (string)$order['appraisal_no'], 'service_provider' => (string)$order['service_provider'], 'service_provider_text' => $this->serviceProviderText((string)$order['service_provider']), 'source_channel' => (string)($order['source_channel'] ?? ''), 'source_channel_text' => $this->sourceChannelText((string)($order['source_channel'] ?? '')), 'source_customer_id' => (string)($order['source_customer_id'] ?? ''), 'order_status' => (string)$order['order_status'], 'display_status' => (string)$order['display_status'], ], 'product_info' => [ 'product_name' => (string)($product['product_name'] ?? ''), 'category_name' => (string)($product['category_name'] ?? ''), 'brand_name' => (string)($product['brand_name'] ?? ''), 'color' => (string)($product['color'] ?? ''), 'size_spec' => (string)($product['size_spec'] ?? ''), 'serial_no' => (string)($product['serial_no'] ?? ''), ], 'logistics_info' => $sendLogistics ? [ 'express_company' => (string)$sendLogistics['express_company'], 'tracking_no' => (string)$sendLogistics['tracking_no'], 'tracking_status' => (string)$sendLogistics['tracking_status'], 'tracking_status_text' => $this->trackingStatusText((string)$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' => (string)($sendLogistics['latest_time'] ?? ''), 'nodes' => array_map(fn (array $item) => [ 'node_time' => (string)$item['node_time'], 'node_desc' => (string)$item['node_desc'], 'node_location' => (string)$item['node_location'], ], $syncService->nodesForLogistics((int)$sendLogistics['id'])), ] : null, 'return_address' => $returnAddress ? [ 'consignee' => (string)($returnAddress['consignee'] ?? ''), 'mobile' => (string)($returnAddress['mobile'] ?? ''), 'full_address' => trim(sprintf('%s%s%s%s', $returnAddress['province'] ?? '', $returnAddress['city'] ?? '', $returnAddress['district'] ?? '', $returnAddress['detail_address'] ?? '')), ] : null, 'return_logistics' => $returnLogistics ? [ 'express_company' => (string)$returnLogistics['express_company'], 'tracking_no' => (string)$returnLogistics['tracking_no'], 'tracking_status' => (string)$returnLogistics['tracking_status'], 'tracking_status_text' => $this->trackingStatusText((string)$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' => (string)($returnLogistics['latest_time'] ?? ''), 'nodes' => array_map(fn (array $item) => [ 'node_time' => (string)$item['node_time'], 'node_desc' => (string)$item['node_desc'], 'node_location' => (string)$item['node_location'], ], $syncService->nodesForLogistics((int)$returnLogistics['id'])), ] : null, 'transfer_flow' => $flow ? $this->formatFlow($flow) : null, 'report_info' => $report ? [ 'id' => (int)$report['id'], 'report_no' => (string)$report['report_no'], 'report_title' => (string)$report['report_title'], 'report_status' => (string)$report['report_status'], 'publish_time' => (string)($report['publish_time'] ?? ''), 'zhongjian_report_no' => (string)($report['zhongjian_report_no'] ?? ''), 'report_entry_admin_name' => (string)($report['report_entry_admin_name'] ?? ''), 'report_entered_at' => (string)($report['report_entered_at'] ?? ''), 'zhongjian_report_files' => $this->normalizeAssetList($this->decodeJsonArray($content['zhongjian_report_files_json'] ?? null), $request), ] : null, 'flow_logs' => array_map(fn (array $log) => $this->formatFlowLog($log, $request), $flowLogs), ]; } private function moveZhongjian(string $tagNo, string $expectedStage, string $nextStage, string $nextLocation, string $actionCode, string $actionText, string $desc, Request $request): array { $operator = $this->operator($request); $flow = $this->findActiveFlowByTagNo($tagNo); if (!$flow) { throw new \RuntimeException('未找到可用的内部流转挂牌', 404); } if (($flow['service_provider'] ?? '') !== 'zhongjian') { throw new \InvalidArgumentException('非中检订单不能进行送检出入库'); } if ((string)$flow['current_stage'] !== $expectedStage) { throw new \InvalidArgumentException('当前流转状态不支持该送检动作'); } $now = date('Y-m-d H:i:s'); $auditFields = $actionCode === 'zhongjian_outbound' ? ['zhongjian_outbound_by' => $operator['id'], 'zhongjian_outbound_by_name' => $operator['name'], 'zhongjian_outbound_at' => $now] : ['zhongjian_inbound_by' => $operator['id'], 'zhongjian_inbound_by_name' => $operator['name'], 'zhongjian_inbound_at' => $now]; Db::startTrans(); try { $this->updateFlowStage($flow, $nextStage, $nextLocation, $auditFields, $now); Db::name('orders')->where('id', (int)$flow['order_id'])->update([ 'display_status' => $actionCode === 'zhongjian_outbound' ? '中检送检中' : '中检待录入报告', 'updated_at' => $now, ]); $this->insertTimeline((int)$flow['order_id'], $actionCode, $actionText, $desc, $operator, $now); $this->insertFlowLog($flow, $actionCode, $actionText, (string)$flow['current_stage'], (string)$flow['current_location'], $nextStage, $nextLocation, $operator, '', $now); Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } return $this->lookupZhongjianTransfer($tagNo); } private function markReturnConfirmed(array $flow, array $operator, string $actionCode, string $actionText, string $timelineDesc): array { $now = date('Y-m-d H:i:s'); Db::startTrans(); try { $this->updateFlowStage($flow, 'return_confirmed', 'warehouse_return_pending', [ 'return_confirmed_by' => $operator['id'], 'return_confirmed_by_name' => $operator['name'], 'return_confirmed_at' => $now, ], $now); $this->insertTimeline((int)$flow['order_id'], $actionCode, $actionText, $timelineDesc, $operator, $now); $this->insertFlowLog($flow, $actionCode, $actionText, (string)$flow['current_stage'], (string)$flow['current_location'], 'return_confirmed', 'warehouse_return_pending', $operator, '', $now); Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } return $this->formatOrderContext((int)$flow['order_id']); } private function updateFlowStage(array $flow, string $stage, string $location, array $extra, string $now): void { Db::name('order_transfer_flows')->where('id', (int)$flow['id'])->update(array_merge([ 'current_stage' => $stage, 'current_location' => $location, 'updated_at' => $now, ], $extra)); Db::name('internal_transfer_tags')->where('id', (int)$flow['internal_tag_id'])->update([ 'current_stage' => $stage, 'current_location' => $location, 'updated_at' => $now, ]); } private function insertTimeline(int $orderId, string $code, string $text, string $desc, array $operator, string $now): void { Db::name('order_timelines')->insert([ 'order_id' => $orderId, 'node_code' => $code, 'node_text' => $text, 'node_desc' => $desc, 'operator_type' => 'admin', 'operator_id' => $operator['id'], 'occurred_at' => $now, 'created_at' => $now, ]); } private function insertFlowLog(array $flow, string $code, string $text, string $beforeStage, string $beforeLocation, string $afterStage, string $afterLocation, array $operator, string $remark, string $now, ?array $payload = null): void { Db::name('order_transfer_flow_logs')->insert([ 'flow_id' => (int)$flow['id'], 'order_id' => (int)$flow['order_id'], 'internal_tag_id' => (int)$flow['internal_tag_id'], 'internal_tag_no' => (string)$flow['internal_tag_no'], 'action_code' => $code, 'action_text' => $text, 'before_stage' => $beforeStage, 'before_location' => $beforeLocation, 'after_stage' => $afterStage, 'after_location' => $afterLocation, 'operator_id' => $operator['id'], 'operator_name' => $operator['name'], 'remark' => mb_substr($remark, 0, 500), 'payload_json' => $payload ? json_encode($payload, JSON_UNESCAPED_UNICODE) : null, 'created_at' => $now, ]); } private function formatFlowLog(array $log, ?Request $request): array { $payload = $this->decodeJsonObject($log['payload_json'] ?? null); $attachments = $this->normalizeAssetList($this->decodeJsonArray($payload['inbound_attachments'] ?? []), $request); $packingAttachments = $this->normalizeAssetList($this->decodeJsonArray($payload['packing_attachments'] ?? []), $request); return [ 'id' => (int)$log['id'], 'action_code' => (string)$log['action_code'], 'action_text' => (string)$log['action_text'], 'before_stage' => $this->stageText((string)($log['before_stage'] ?? '')), 'before_location' => $this->locationText((string)($log['before_location'] ?? '')), 'after_stage' => $this->stageText((string)($log['after_stage'] ?? '')), 'after_location' => $this->locationText((string)($log['after_location'] ?? '')), 'operator_name' => (string)($log['operator_name'] ?? ''), 'remark' => (string)($log['remark'] ?? ''), 'created_at' => (string)($log['created_at'] ?? ''), 'inbound_attachments' => $attachments, 'packing_attachments' => $packingAttachments, ]; } private function ensureTransferTag(string $tagNo, array $operator, string $now): array { $tag = Db::name('internal_transfer_tags')->where('tag_no', $tagNo)->find(); if ($tag) { if (($tag['status'] ?? 'active') === 'invalid') { throw new \InvalidArgumentException('该内部流转挂牌已失效'); } return $tag; } $id = (int)Db::name('internal_transfer_tags')->insertGetId([ 'tag_no' => $tagNo, 'status' => 'active', 'bind_status' => 'free', 'current_stage' => 'idle', 'current_location' => 'warehouse', 'created_by' => $operator['id'], 'created_by_name' => $operator['name'], 'created_at' => $now, 'updated_at' => $now, ]); return Db::name('internal_transfer_tags')->where('id', $id)->find(); } private function resolveInboundOrder(string $inboundNo): array { $inboundNo = trim($inboundNo); if ($inboundNo === '') { throw new \InvalidArgumentException('请先扫描快递单号或输入鉴定订单号'); } $logisticsRows = Db::name('order_logistics') ->where('logistics_type', 'send_to_center') ->where('tracking_no', $inboundNo) ->select() ->toArray(); if ($logisticsRows) { if (count($logisticsRows) > 1) { throw new \RuntimeException('该快递单号匹配到多笔订单,请人工核查后处理', 409); } return [ 'order_id' => (int)$logisticsRows[0]['order_id'], 'match_type' => 'tracking_no', 'match_no' => $inboundNo, ]; } $orderRows = Db::name('orders') ->where(function ($builder) use ($inboundNo) { $builder->whereRaw( '(order_no = :order_no OR appraisal_no = :appraisal_no)', [ 'order_no' => $inboundNo, 'appraisal_no' => $inboundNo, ] ); }) ->field(['id', 'order_no', 'appraisal_no']) ->select() ->toArray(); if ($orderRows) { if (count($orderRows) > 1) { throw new \RuntimeException('该订单号匹配到多笔订单,请人工核查后处理', 409); } $order = $orderRows[0]; return [ 'order_id' => (int)$order['id'], 'match_type' => $inboundNo === (string)($order['appraisal_no'] ?? '') ? 'appraisal_no' : 'order_no', 'match_no' => $inboundNo, ]; } $externalRows = Db::name('enterprise_customer_order_refs') ->where('external_order_no', $inboundNo) ->field(['order_id']) ->select() ->toArray(); if ($externalRows) { if (count($externalRows) > 1) { throw new \RuntimeException('该外部订单号匹配到多笔订单,请人工核查后处理', 409); } return [ 'order_id' => (int)$externalRows[0]['order_id'], 'match_type' => 'external_order_no', 'match_no' => $inboundNo, ]; } throw new \RuntimeException('未匹配到待入库订单', 404); } private function inboundMatchRemark(array $match): string { $label = match ((string)($match['match_type'] ?? '')) { 'tracking_no' => '快递单号', 'appraisal_no' => '鉴定单号', 'order_no' => '订单号', 'external_order_no' => '外部订单号', default => '入库编号', }; return sprintf('扫描%s入库:%s', $label, (string)($match['match_no'] ?? '')); } private function findActiveFlowByTagNo(string $tagNo): ?array { $tagNo = $this->normalizeTagNo($tagNo); if ($tagNo === '') { throw new \InvalidArgumentException('请扫描内部流转挂牌编号'); } return Db::name('order_transfer_flows') ->where('internal_tag_no', $tagNo) ->where('flow_status', '<>', 'ended') ->order('id', 'desc') ->find() ?: null; } private function findActiveFlowByTagId(int $tagId): ?array { return Db::name('order_transfer_flows') ->where('internal_tag_id', $tagId) ->where('flow_status', '<>', 'ended') ->order('id', 'desc') ->find() ?: null; } private function latestReport(int $orderId): ?array { return Db::name('reports') ->where('order_id', $orderId) ->where('report_type', 'appraisal') ->order('id', 'desc') ->find() ?: null; } private function returnAddressForOrder(array $order): ?array { $orderId = (int)($order['id'] ?? 0); $address = Db::name('order_return_addresses')->where('order_id', $orderId)->find(); if ($address) { return $address; } return Db::name('user_addresses') ->where('user_id', (int)($order['user_id'] ?? 0)) ->where('is_default', 1) ->order('id', 'desc') ->find() ?: Db::name('user_addresses') ->where('user_id', (int)($order['user_id'] ?? 0)) ->order('id', 'desc') ->find() ?: null; } private function ensureReturnAddressSnapshot(int $orderId, array $address, string $now): void { if (Db::name('order_return_addresses')->where('order_id', $orderId)->find()) { return; } Db::name('order_return_addresses')->insert([ 'order_id' => $orderId, 'user_address_id' => $address['user_address_id'] ?? ($address['id'] ?? null), 'consignee' => $address['consignee'] ?? '', 'mobile' => $address['mobile'] ?? '', 'province' => $address['province'] ?? '', 'city' => $address['city'] ?? '', 'district' => $address['district'] ?? '', 'detail_address' => $address['detail_address'] ?? '', 'created_at' => $now, 'updated_at' => $now, ]); } private function ensurePendingReturnOrder(array $flow): array { $order = Db::name('orders')->where('id', (int)$flow['order_id'])->find(); if (!$order) { throw new \RuntimeException('订单不存在', 404); } if ((string)($order['order_status'] ?? '') !== 'report_published') { throw new \InvalidArgumentException('当前订单不处于待寄回状态'); } return $order; } private function operator(Request $request): array { $id = (int)$request->header('x-admin-id', 0); $name = trim((string)$request->header('x-admin-name', '')); if ($id <= 0 || $name === '') { throw new \RuntimeException('当前登录管理员信息异常', 401); } return ['id' => $id, 'name' => $name]; } private function normalizeTagNo(string $tagNo): string { return mb_substr(trim($tagNo), 0, 80); } private function formatFlow(array $flow): array { return [ 'id' => (int)$flow['id'], 'internal_tag_no' => (string)$flow['internal_tag_no'], 'flow_status' => (string)$flow['flow_status'], 'current_stage' => (string)$flow['current_stage'], 'current_stage_text' => $this->stageText((string)$flow['current_stage']), 'current_location' => (string)$flow['current_location'], 'current_location_text' => $this->locationText((string)$flow['current_location']), 'inbound_by_name' => (string)($flow['inbound_by_name'] ?? ''), 'inbound_at' => (string)($flow['inbound_at'] ?? ''), 'zhongjian_outbound_by_name' => (string)($flow['zhongjian_outbound_by_name'] ?? ''), 'zhongjian_outbound_at' => (string)($flow['zhongjian_outbound_at'] ?? ''), 'zhongjian_inbound_by_name' => (string)($flow['zhongjian_inbound_by_name'] ?? ''), 'zhongjian_inbound_at' => (string)($flow['zhongjian_inbound_at'] ?? ''), 'appraisal_started_by_name' => (string)($flow['appraisal_started_by_name'] ?? ''), 'appraisal_started_at' => (string)($flow['appraisal_started_at'] ?? ''), 'report_published_by_name' => (string)($flow['report_published_by_name'] ?? ''), 'report_published_at' => (string)($flow['report_published_at'] ?? ''), 'return_confirmed_by_name' => (string)($flow['return_confirmed_by_name'] ?? ''), 'return_confirmed_at' => (string)($flow['return_confirmed_at'] ?? ''), 'return_shipped_by_name' => (string)($flow['return_shipped_by_name'] ?? ''), 'return_shipped_at' => (string)($flow['return_shipped_at'] ?? ''), ]; } private function stageText(string $stage): string { return match ($stage) { 'warehouse_received' => '已入仓待检', 'sent_to_zhongjian' => '中检送检出库', 'zhongjian_returned' => '中检送检入库', 'appraising' => '鉴定/报告录入中', 'report_published' => '报告已发布待寄回', 'return_confirmed' => '寄回确认完成', 'return_shipped' => '已寄回', default => $stage, }; } private function locationText(string $location): string { return match ($location) { 'warehouse_pending_inspection' => '仓库待检区', 'zhongjian_institution' => '中检机构', 'warehouse_pending_report_entry' => '仓库中检回收区', 'appraiser_workbench' => '鉴定师作业区', 'zhongjian_report_entry' => '中检报告录入区', 'warehouse_return_pending' => '仓库待寄回区', 'ended' => '流转结束', default => $location, }; } private function serviceProviderText(string $serviceProvider): string { return $serviceProvider === 'zhongjian' ? '中检鉴定' : '实物鉴定'; } private function sourceChannelText(string $sourceChannel): string { return match ($sourceChannel) { 'mini_program' => '小程序', 'h5' => 'H5', 'enterprise_push' => '大客户推送订单', 'manual_entry' => '后台补录订单', default => $sourceChannel ?: '未知渠道', }; } private function trackingStatusText(string $status, string $logisticsType): 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 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 normalizeAssetList(array $files, ?Request $request): array { if (!$request) { return $files; } return (new AppraisalEvidenceService())->normalize($files, $request); } }