增加了手机操作端

This commit is contained in:
wushumin
2026-05-15 14:01:36 +08:00
parent 9aac78b8da
commit dd56e0861b
107 changed files with 23547 additions and 346 deletions

View File

@@ -0,0 +1,876 @@
<?php
namespace app\support;
use support\Request;
use support\think\Db;
class FulfillmentFlowService
{
public function lookupInboundByTrackingNo(string $trackingNo): array
{
$trackingNo = trim($trackingNo);
if ($trackingNo === '') {
throw new \InvalidArgumentException('请先扫描寄入运单号');
}
$rows = Db::name('order_logistics')
->where('logistics_type', 'send_to_center')
->where('tracking_no', $trackingNo)
->select()
->toArray();
if (!$rows) {
throw new \RuntimeException('未匹配到订单,请核对寄入运单号', 404);
}
if (count($rows) > 1) {
throw new \RuntimeException('该运单号匹配到多笔订单,请人工核查后处理', 409);
}
return $this->formatOrderContext((int)$rows[0]['order_id']);
}
public function receiveInbound(string $trackingNo, string $tagNo, Request $request): array
{
$operator = $this->operator($request);
$context = $this->lookupInboundByTrackingNo($trackingNo);
$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');
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')
->where('tracking_no', trim($trackingNo))
->order('id', 'desc')
->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, '扫描寄入运单号入库', $now);
$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);
}
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 => '',
};
return array_merge($this->formatOrderContext((int)$flow['order_id']), [
'next_action' => $nextAction,
'next_action_text' => $nextAction === 'outbound' ? '送检出库' : ($nextAction === 'inbound' ? '送检入库' : '暂无可执行送检动作'),
]);
}
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('订单报告未发布,不能进入寄回流程');
}
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
{
$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']);
if (!$report || ($report['report_status'] ?? '') !== 'published') {
throw new \InvalidArgumentException('订单报告未发布,不能确认寄回');
}
$tag = (new MaterialTagService())->findTagByInput($qrInput);
if (!$tag || (int)($tag['report_id'] ?? 0) !== (int)$report['id']) {
throw new \InvalidArgumentException('验真吊牌与当前订单报告不匹配');
}
return $this->markReturnConfirmed($flow, $operator, 'return_tag_verified', '验真吊牌确认', '仓管已扫描验真吊牌并确认报告信息。');
}
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' || trim((string)($report['zhongjian_report_no'] ?? '')) === '' || !$files) {
throw new \InvalidArgumentException('中检报告未完整录入,不能确认寄回');
}
return $this->markReturnConfirmed($flow, $operator, 'return_confirmed', '中检报告已确认', '仓管已查看中检报告编号和报告文件。');
}
public function shipReturn(string $tagNo, string $expressCompany, string $trackingNo, Request $request): array
{
$operator = $this->operator($request);
$flow = $this->findActiveFlowByTagNo($tagNo);
if (!$flow) {
throw new \RuntimeException('未找到可用的内部流转挂牌', 404);
}
if ((string)($flow['current_stage'] ?? '') !== 'return_confirmed') {
throw new \InvalidArgumentException('请先完成报告确认,再登记回寄运单');
}
$expressCompany = trim($expressCompany);
$trackingNo = trim($trackingNo);
if ($expressCompany === '' || $trackingNo === '') {
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);
(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,
]);
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();
$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'],
] : 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'],
] : 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) => [
'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'] ?? ''),
], $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): 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' => null,
'created_at' => $now,
]);
}
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 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 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' => '大客户推送订单',
default => $sourceChannel ?: '未知渠道',
};
}
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 normalizeAssetList(array $files, ?Request $request): array
{
if (!$request) {
return $files;
}
return (new AppraisalEvidenceService())->normalize($files, $request);
}
}