增加了手机操作端
This commit is contained in:
876
server-api/app/support/FulfillmentFlowService.php
Normal file
876
server-api/app/support/FulfillmentFlowService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user