chore: prepare release build
This commit is contained in:
@@ -75,6 +75,7 @@ class AppraisalTasksController
|
||||
->select()
|
||||
->toArray();
|
||||
$this->applyTaskScopeFilterRows($allRows, $request, $scope);
|
||||
$this->attachTransferFlowToRows($allRows);
|
||||
|
||||
$list = $this->buildGroupedTaskList($allRows, $reportMap);
|
||||
$total = count($list);
|
||||
@@ -166,6 +167,7 @@ class AppraisalTasksController
|
||||
|
||||
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
||||
$materialTag = $report ? (new MaterialTagService())->findBoundTagForReport((int)$report['id']) : null;
|
||||
$transferFlow = $this->latestTransferFlowForOrder((int)$task['order_id']);
|
||||
$effectiveStatus = $this->effectiveTaskStatus($task, $report);
|
||||
if ($effectiveStatus !== $task['status']) {
|
||||
Db::name('appraisal_tasks')->where('id', (int)$task['id'])->update([
|
||||
@@ -232,6 +234,7 @@ class AppraisalTasksController
|
||||
->order('t.id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$this->attachTransferFlowToRows($stageTaskRows);
|
||||
|
||||
$stageTasks = array_map(function (array $item) use ($id, $stageReportMap) {
|
||||
$row = $this->normalizeTaskListRow($item, $stageReportMap[(int)$item['order_id']] ?? null);
|
||||
@@ -305,6 +308,7 @@ class AppraisalTasksController
|
||||
'submitted_at' => $task['submitted_at'],
|
||||
'sla_deadline' => $task['sla_deadline'],
|
||||
'is_overtime' => (bool)$task['is_overtime'],
|
||||
'internal_tag_no' => (string)($transferFlow['internal_tag_no'] ?? ''),
|
||||
],
|
||||
'report_summary' => $report ? [
|
||||
'id' => (int)$report['id'],
|
||||
@@ -373,10 +377,6 @@ class AppraisalTasksController
|
||||
if (!$task) {
|
||||
return api_error('任务不存在', 404);
|
||||
}
|
||||
if (($task['service_provider'] ?? '') === 'zhongjian') {
|
||||
return api_error('中检订单不使用平台验真吊牌', 422);
|
||||
}
|
||||
|
||||
$operatorGuard = $this->guardTaskOperator($request, $task);
|
||||
if ($operatorGuard['error']) {
|
||||
return $operatorGuard['error'];
|
||||
@@ -426,23 +426,25 @@ class AppraisalTasksController
|
||||
if (!$task) {
|
||||
return api_error('任务不存在', 404);
|
||||
}
|
||||
if (($task['service_provider'] ?? '') === 'zhongjian') {
|
||||
return api_error('中检订单不使用平台验真吊牌', 422);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$tag = (new MaterialTagService())->bindTagToReportByTask($id, $qrInput, $request);
|
||||
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
||||
if (!$report) {
|
||||
Db::rollback();
|
||||
return api_error('请先提交鉴定结论生成报告草稿', 422);
|
||||
}
|
||||
$publish = $this->publishReportRecord($report, $request);
|
||||
$publish = $this->publishReportRecord($report, $request, false);
|
||||
(new FulfillmentFlowService())->markReportPublished((int)$task['order_id'], $request);
|
||||
Db::commit();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
Db::rollback();
|
||||
return api_error($e->getMessage(), 422);
|
||||
} catch (\RuntimeException $e) {
|
||||
Db::rollback();
|
||||
return api_error($e->getMessage(), $e->getCode() ?: 404);
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return api_error('验真吊牌绑定或报告发布失败', 500, ['detail' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -457,6 +459,7 @@ class AppraisalTasksController
|
||||
{
|
||||
$id = (int)$request->input('id', 0);
|
||||
$reportNo = trim((string)$request->input('zhongjian_report_no', ''));
|
||||
$qrInput = trim((string)$request->input('qr_input', ''));
|
||||
$files = $this->evidenceService()->normalize($request->input('report_files', []), $request, true);
|
||||
if ($id <= 0) {
|
||||
return api_error('任务 ID 不能为空', 422);
|
||||
@@ -467,6 +470,9 @@ class AppraisalTasksController
|
||||
if (!$files) {
|
||||
return api_error('请至少上传 1 个中检报告文件', 422);
|
||||
}
|
||||
if ($qrInput === '') {
|
||||
return api_error('请扫描验真吊牌二维码', 422);
|
||||
}
|
||||
|
||||
$task = Db::name('appraisal_tasks')->where('id', $id)->find();
|
||||
if (!$task) {
|
||||
@@ -475,6 +481,20 @@ class AppraisalTasksController
|
||||
if (($task['service_provider'] ?? '') !== 'zhongjian') {
|
||||
return api_error('非中检订单不能录入中检报告', 422);
|
||||
}
|
||||
$order = Db::name('orders')->where('id', (int)$task['order_id'])->find() ?: [];
|
||||
$task['order_status'] = $order['order_status'] ?? '';
|
||||
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
||||
$effectiveStatus = $this->effectiveTaskStatus($task, $report);
|
||||
if ($effectiveStatus !== $task['status']) {
|
||||
Db::name('appraisal_tasks')->where('id', $id)->update([
|
||||
'status' => $effectiveStatus,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$task['status'] = $effectiveStatus;
|
||||
}
|
||||
if (in_array($effectiveStatus, ['submitted', 'completed'], true)) {
|
||||
return api_error('当前任务已流转完成,不能再录入中检报告', 422);
|
||||
}
|
||||
|
||||
$operatorGuard = $this->guardTaskOperator($request, $task);
|
||||
if ($operatorGuard['error']) {
|
||||
@@ -565,22 +585,25 @@ class AppraisalTasksController
|
||||
'created_at' => $now,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
|
||||
$freshReport = $this->findLatestAppraisalReport((int)$task['order_id']);
|
||||
$publish = $this->publishReportRecord($freshReport, $request);
|
||||
if (!$freshReport) {
|
||||
Db::rollback();
|
||||
return api_error('中检报告草稿生成失败', 500);
|
||||
}
|
||||
|
||||
$tag = (new MaterialTagService())->bindTagToReportByTask($id, $qrInput, $request);
|
||||
$publish = $this->publishReportRecord($freshReport, $request, false);
|
||||
(new FulfillmentFlowService())->markReportPublished((int)$task['order_id'], $request);
|
||||
|
||||
Db::commit();
|
||||
|
||||
return api_success([
|
||||
'id' => $id,
|
||||
'material_tag' => $tag,
|
||||
'report' => $publish,
|
||||
], '中检报告已录入并发布');
|
||||
], '验真吊牌已绑定,报告已发布');
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
Db::rollback();
|
||||
} catch (\Throwable $rollbackError) {
|
||||
// Transaction may already be committed before publishing.
|
||||
}
|
||||
Db::rollback();
|
||||
return api_error('中检报告录入失败', 500, ['detail' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
@@ -640,6 +663,7 @@ class AppraisalTasksController
|
||||
{
|
||||
$id = (int)$request->input('id', 0);
|
||||
$action = trim((string)$request->input('action', 'save'));
|
||||
$qrInput = trim((string)$request->input('qr_input', ''));
|
||||
|
||||
if (!$id) {
|
||||
return api_error('任务 ID 不能为空', 422);
|
||||
@@ -675,6 +699,9 @@ class AppraisalTasksController
|
||||
if ($action !== 'save' && $resultText === '') {
|
||||
return api_error('鉴定结论不能为空', 422);
|
||||
}
|
||||
if ($action !== 'save' && $qrInput === '') {
|
||||
return api_error('请扫描验真吊牌二维码', 422);
|
||||
}
|
||||
$productInput = $request->input('product_info', null);
|
||||
$productPayload = is_array($productInput) ? $this->normalizeProductInput($productInput) : null;
|
||||
$attachments = $this->evidenceService()->normalize($request->input('attachments', []), $request, true);
|
||||
@@ -774,6 +801,14 @@ class AppraisalTasksController
|
||||
]);
|
||||
|
||||
$this->createOrUpdateReportDraft((int)$task['order_id'], $task, $payload, $now);
|
||||
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
||||
if (!$report) {
|
||||
Db::rollback();
|
||||
return api_error('报告草稿生成失败', 500);
|
||||
}
|
||||
$tag = (new MaterialTagService())->bindTagToReportByTask($id, $qrInput, $request);
|
||||
$publish = $this->publishReportRecord($report, $request, false);
|
||||
(new FulfillmentFlowService())->markReportPublished((int)$task['order_id'], $request);
|
||||
|
||||
Db::commit();
|
||||
(new EnterpriseWebhookService())->recordOrderEvent((int)$task['order_id'], 'appraisal_finished', [
|
||||
@@ -781,7 +816,11 @@ class AppraisalTasksController
|
||||
'task_stage' => $task['task_stage'],
|
||||
'finished_at' => $now,
|
||||
]);
|
||||
return api_success(['id' => $id], '鉴定已完成,报告草稿已生成');
|
||||
return api_success([
|
||||
'id' => $id,
|
||||
'material_tag' => $tag,
|
||||
'report' => $publish,
|
||||
], '验真吊牌已绑定,报告已发布');
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return api_error('结论保存失败', 500, [
|
||||
@@ -976,6 +1015,15 @@ class AppraisalTasksController
|
||||
|
||||
public function uploadEvidenceFile(Request $request)
|
||||
{
|
||||
$taskId = (int)$request->input('task_id', 0);
|
||||
if ($taskId <= 0) {
|
||||
return api_error('任务 ID 不能为空', 422);
|
||||
}
|
||||
$editableGuard = $this->guardTaskEditable($taskId, '当前任务已流转完成,不能再上传附件');
|
||||
if ($editableGuard) {
|
||||
return $editableGuard;
|
||||
}
|
||||
|
||||
try {
|
||||
$asset = $this->evidenceService()->upload($request);
|
||||
return api_success($asset);
|
||||
@@ -986,6 +1034,15 @@ class AppraisalTasksController
|
||||
|
||||
public function deleteEvidenceFile(Request $request)
|
||||
{
|
||||
$taskId = (int)$request->input('task_id', 0);
|
||||
if ($taskId <= 0) {
|
||||
return api_error('任务 ID 不能为空', 422);
|
||||
}
|
||||
$editableGuard = $this->guardTaskEditable($taskId, '当前任务已流转完成,不能再删除附件');
|
||||
if ($editableGuard) {
|
||||
return $editableGuard;
|
||||
}
|
||||
|
||||
$fileUrl = trim((string)$request->input('file_url', ''));
|
||||
if ($fileUrl === '') {
|
||||
return api_error('文件地址不能为空', 422);
|
||||
@@ -1093,9 +1150,86 @@ class AppraisalTasksController
|
||||
'sla_deadline' => $item['sla_deadline'],
|
||||
'is_overtime' => (bool)$item['is_overtime'],
|
||||
'display_status' => $item['display_status'],
|
||||
'internal_tag_no' => (string)($item['internal_tag_no'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function attachTransferFlowToRows(array &$rows): void
|
||||
{
|
||||
$orderIds = array_values(array_unique(array_filter(array_map(fn (array $item) => (int)($item['order_id'] ?? 0), $rows))));
|
||||
if (!$orderIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
$flowMap = $this->latestTransferFlowMap($orderIds);
|
||||
foreach ($rows as &$row) {
|
||||
$orderId = (int)($row['order_id'] ?? 0);
|
||||
$row['internal_tag_no'] = (string)($flowMap[$orderId]['internal_tag_no'] ?? '');
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
private function latestTransferFlowForOrder(int $orderId): ?array
|
||||
{
|
||||
if ($orderId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Db::name('order_transfer_flows')
|
||||
->where('order_id', $orderId)
|
||||
->order('id', 'desc')
|
||||
->find() ?: null;
|
||||
}
|
||||
|
||||
private function latestTransferFlowMap(array $orderIds): array
|
||||
{
|
||||
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
|
||||
if (!$orderIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Db::name('order_transfer_flows')
|
||||
->whereIn('order_id', $orderIds)
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$orderId = (int)($row['order_id'] ?? 0);
|
||||
if ($orderId > 0 && !isset($map[$orderId])) {
|
||||
$map[$orderId] = [
|
||||
'internal_tag_no' => (string)($row['internal_tag_no'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function guardTaskEditable(int $taskId, string $message)
|
||||
{
|
||||
$task = Db::name('appraisal_tasks')->where('id', $taskId)->find();
|
||||
if (!$task) {
|
||||
return api_error('任务不存在', 404);
|
||||
}
|
||||
|
||||
$order = Db::name('orders')->where('id', (int)$task['order_id'])->find() ?: [];
|
||||
$task['order_status'] = $order['order_status'] ?? '';
|
||||
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
||||
$effectiveStatus = $this->effectiveTaskStatus($task, $report);
|
||||
if ($effectiveStatus !== (string)$task['status']) {
|
||||
Db::name('appraisal_tasks')->where('id', $taskId)->update([
|
||||
'status' => $effectiveStatus,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
return in_array($effectiveStatus, ['submitted', 'completed'], true)
|
||||
? api_error($message, 422)
|
||||
: null;
|
||||
}
|
||||
|
||||
private function formatResultInfo(array $task, ?Request $request = null): array
|
||||
{
|
||||
$resultId = 0;
|
||||
@@ -1866,7 +2000,7 @@ class AppraisalTasksController
|
||||
return $admin;
|
||||
}
|
||||
|
||||
private function publishReportRecord(array $report, Request $request): array
|
||||
private function publishReportRecord(array $report, Request $request, bool $wrapTransaction = true): array
|
||||
{
|
||||
if (!$report) {
|
||||
throw new \RuntimeException('报告不存在', 404);
|
||||
@@ -1878,10 +2012,11 @@ class AppraisalTasksController
|
||||
$operatorId = (int)$request->header('x-admin-id', 0);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$effectivePublishTime = $report['publish_time'] ?: $now;
|
||||
$usesPlatformVerify = (string)($report['service_provider'] ?? '') !== 'zhongjian';
|
||||
$verify = [];
|
||||
|
||||
Db::startTrans();
|
||||
if ($wrapTransaction) {
|
||||
Db::startTrans();
|
||||
}
|
||||
try {
|
||||
if (($report['report_status'] ?? '') !== 'published') {
|
||||
Db::name('reports')->where('id', (int)$report['id'])->update([
|
||||
@@ -1893,9 +2028,7 @@ class AppraisalTasksController
|
||||
$report['publish_time'] = $effectivePublishTime;
|
||||
}
|
||||
|
||||
if ($usesPlatformVerify) {
|
||||
$verify = $this->createOrUpdateVerifyRecord($report, $now);
|
||||
}
|
||||
$verify = $this->createOrUpdateVerifyRecord($report, $now);
|
||||
|
||||
if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) {
|
||||
Db::name('orders')->where('id', (int)$report['order_id'])->update([
|
||||
@@ -1933,15 +2066,19 @@ class AppraisalTasksController
|
||||
'report_title' => (string)$report['report_title'],
|
||||
'product_name' => $product['product_name'] ?? '',
|
||||
'publish_time' => $effectivePublishTime,
|
||||
'verify_url' => $usesPlatformVerify ? (string)($verify['verify_url'] ?? '') : '',
|
||||
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
||||
'fallback_title' => '报告已出具',
|
||||
'fallback_content' => '您的正式报告已生成,可前往报告中心查看。',
|
||||
]);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
if ($wrapTransaction) {
|
||||
Db::commit();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
if ($wrapTransaction) {
|
||||
Db::rollback();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
@@ -1951,8 +2088,8 @@ class AppraisalTasksController
|
||||
'report_no' => (string)$report['report_no'],
|
||||
'report_title' => (string)$report['report_title'],
|
||||
'publish_time' => $effectivePublishTime,
|
||||
'verify_url' => $usesPlatformVerify ? (string)($verify['verify_url'] ?? '') : '',
|
||||
'report_page_url' => $usesPlatformVerify ? (string)($verify['report_page_url'] ?? '') : '',
|
||||
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
||||
'report_page_url' => (string)($verify['report_page_url'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1960,8 +2097,8 @@ class AppraisalTasksController
|
||||
'id' => (int)$report['id'],
|
||||
'report_status' => 'published',
|
||||
'publish_time' => $effectivePublishTime,
|
||||
'verify_url' => $usesPlatformVerify ? (string)($verify['verify_url'] ?? '') : '',
|
||||
'report_page_url' => $usesPlatformVerify ? (string)($verify['report_page_url'] ?? '') : '',
|
||||
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
||||
'report_page_url' => (string)($verify['report_page_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\support\AppraisalEvidenceService;
|
||||
use app\support\MessageDispatcher;
|
||||
use app\support\EnterpriseWebhookService;
|
||||
use app\support\WarehouseService;
|
||||
@@ -10,6 +11,8 @@ use support\think\Db;
|
||||
|
||||
class OrdersController
|
||||
{
|
||||
private const MANUAL_ENTRY_SOURCE = 'manual_entry';
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$keyword = trim((string)$request->input('keyword', ''));
|
||||
@@ -56,6 +59,7 @@ class OrdersController
|
||||
|
||||
$warehouseStatusFilters = [
|
||||
'warehouse_active',
|
||||
'warehouse_pending_inbound',
|
||||
'warehouse_in_transit',
|
||||
'warehouse_received',
|
||||
'warehouse_pending_return',
|
||||
@@ -77,6 +81,9 @@ class OrdersController
|
||||
];
|
||||
if ($status === 'warehouse_in_transit') {
|
||||
$query->where('o.order_status', 'pending_shipping');
|
||||
} elseif ($status === 'warehouse_pending_inbound') {
|
||||
$query->where('o.order_status', 'pending_shipping')
|
||||
->where('o.source_channel', self::MANUAL_ENTRY_SOURCE);
|
||||
} elseif ($status === 'warehouse_received') {
|
||||
$query->whereIn('o.order_status', array_values(array_diff($warehouseActiveStatuses, ['pending_shipping', 'report_published'])));
|
||||
} elseif ($status === 'warehouse_pending_return') {
|
||||
@@ -99,8 +106,9 @@ class OrdersController
|
||||
$orderIds = array_map('intval', array_column($rows, 'id'));
|
||||
$sendTrackingMap = $this->latestLogisticsMap($orderIds, 'send_to_center');
|
||||
$returnTrackingMap = $this->latestLogisticsMap($orderIds, 'return_to_user');
|
||||
$transferFlowMap = $this->latestTransferFlowMap($orderIds);
|
||||
|
||||
$list = array_map(function (array $item) use ($sendTrackingMap, $returnTrackingMap) {
|
||||
$list = array_map(function (array $item) use ($sendTrackingMap, $returnTrackingMap, $transferFlowMap) {
|
||||
$orderId = (int)$item['id'];
|
||||
$sendTrackingNo = $sendTrackingMap[$orderId]['tracking_no'] ?? '';
|
||||
$sendTrackingStatus = $sendTrackingMap[$orderId]['tracking_status'] ?? '';
|
||||
@@ -108,7 +116,8 @@ class OrdersController
|
||||
(string)$item['order_status'],
|
||||
$sendTrackingNo,
|
||||
$sendTrackingStatus,
|
||||
(string)($item['display_status'] ?? '')
|
||||
(string)($item['display_status'] ?? ''),
|
||||
(string)($item['source_channel'] ?? '')
|
||||
);
|
||||
|
||||
return [
|
||||
@@ -130,6 +139,7 @@ class OrdersController
|
||||
$returnTrackingMap[$orderId]['tracking_no'] ?? '',
|
||||
$returnTrackingMap[$orderId]['tracking_status'] ?? '',
|
||||
),
|
||||
'internal_tag_no' => $transferFlowMap[$orderId]['internal_tag_no'] ?? '',
|
||||
'warehouse_bucket' => $warehouseBucket,
|
||||
'warehouse_bucket_text' => $this->warehouseOrderBucketText($warehouseBucket),
|
||||
'estimated_finish_time' => $item['estimated_finish_time'],
|
||||
@@ -154,6 +164,7 @@ class OrdersController
|
||||
$list = array_values(array_filter($list, function (array $item) use ($status) {
|
||||
if ($status === 'warehouse_active') {
|
||||
return in_array($item['warehouse_bucket'], [
|
||||
'warehouse_pending_inbound',
|
||||
'warehouse_in_transit',
|
||||
'warehouse_received',
|
||||
'warehouse_pending_return',
|
||||
@@ -206,6 +217,10 @@ class OrdersController
|
||||
->where('logistics_type', 'return_to_user')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
$transferFlow = Db::name('order_transfer_flows')
|
||||
->where('order_id', $id)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
$timeline = Db::name('order_timelines')
|
||||
->where('order_id', $id)
|
||||
->order('occurred_at', 'asc')
|
||||
@@ -268,6 +283,7 @@ class OrdersController
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
$inboundAttachments = $this->inboundAttachments($id, $request);
|
||||
$returnLogisticsNodes = [];
|
||||
if ($returnLogistics) {
|
||||
$returnLogisticsNodes = Db::name('order_logistics_nodes')
|
||||
@@ -352,6 +368,9 @@ class OrdersController
|
||||
)),
|
||||
] : null,
|
||||
'timeline' => $timeline,
|
||||
'transfer_flow' => $transferFlow ? [
|
||||
'internal_tag_no' => (string)($transferFlow['internal_tag_no'] ?? ''),
|
||||
] : null,
|
||||
'logistics_info' => $sendLogistics ? [
|
||||
'express_company' => $sendLogistics['express_company'],
|
||||
'tracking_no' => $sendLogistics['tracking_no'],
|
||||
@@ -377,6 +396,7 @@ class OrdersController
|
||||
'node_location' => $item['node_location'],
|
||||
], $logisticsNodes),
|
||||
] : null,
|
||||
'inbound_attachments' => $inboundAttachments,
|
||||
'return_logistics' => $returnLogistics ? [
|
||||
'express_company' => $returnLogistics['express_company'],
|
||||
'tracking_no' => $returnLogistics['tracking_no'],
|
||||
@@ -907,6 +927,440 @@ class OrdersController
|
||||
return api_success(['id' => $id], '已标记用户签收');
|
||||
}
|
||||
|
||||
public function createManualOrder(Request $request)
|
||||
{
|
||||
$serviceProvider = $this->normalizeServiceProvider((string)$request->input('service_provider', 'anxinyan'));
|
||||
$productInput = $this->requestArray($request, 'product_info');
|
||||
$extraInput = $this->requestArray($request, 'extra_info');
|
||||
$returnAddressInput = $this->requestArray($request, 'return_address');
|
||||
$materialsInput = $request->input('materials', []);
|
||||
$materials = is_array($materialsInput) ? $materialsInput : [];
|
||||
|
||||
$categoryId = (int)($productInput['category_id'] ?? 0);
|
||||
$brandId = (int)($productInput['brand_id'] ?? 0);
|
||||
$productName = trim((string)($productInput['product_name'] ?? ''));
|
||||
$consignee = trim((string)($returnAddressInput['consignee'] ?? ''));
|
||||
$mobile = trim((string)($returnAddressInput['mobile'] ?? ''));
|
||||
$province = trim((string)($returnAddressInput['province'] ?? ''));
|
||||
$city = trim((string)($returnAddressInput['city'] ?? ''));
|
||||
$district = trim((string)($returnAddressInput['district'] ?? ''));
|
||||
$detailAddress = trim((string)($returnAddressInput['detail_address'] ?? ''));
|
||||
|
||||
if ($serviceProvider === '') {
|
||||
return api_error('服务类型不正确', 422);
|
||||
}
|
||||
if ($categoryId <= 0 || $brandId <= 0 || $productName === '') {
|
||||
return api_error('请完整填写品类、品牌和商品名称', 422);
|
||||
}
|
||||
if ($consignee === '' || $mobile === '' || $province === '' || $city === '' || $district === '' || $detailAddress === '') {
|
||||
return api_error('请完整填写寄回收件信息', 422);
|
||||
}
|
||||
|
||||
$category = Db::name('catalog_categories')->where('id', $categoryId)->find();
|
||||
if (!$category) {
|
||||
return api_error('品类不存在', 422);
|
||||
}
|
||||
$brand = Db::name('catalog_brands')->where('id', $brandId)->find();
|
||||
if (!$brand) {
|
||||
return api_error('品牌不存在', 422);
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$serviceConfig = $this->serviceConfig($serviceProvider);
|
||||
$orderNo = $this->generateOrderNo();
|
||||
$appraisalNo = $this->generateAppraisalNo();
|
||||
$estimated = date('Y-m-d H:i:s', strtotime(sprintf('+%d hours', (int)$serviceConfig['sla_hours'])));
|
||||
$operatorId = (int)$request->header('x-admin-id', 0) ?: null;
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$user = $this->resolveManualOrderUser($consignee, $mobile, $now);
|
||||
$addressId = $this->ensureUserAddress((int)$user['id'], [
|
||||
'consignee' => $consignee,
|
||||
'mobile' => $mobile,
|
||||
'province' => $province,
|
||||
'city' => $city,
|
||||
'district' => $district,
|
||||
'detail_address' => $detailAddress,
|
||||
], $now);
|
||||
|
||||
$orderId = (int)Db::name('orders')->insertGetId([
|
||||
'order_no' => $orderNo,
|
||||
'appraisal_no' => $appraisalNo,
|
||||
'user_id' => (int)$user['id'],
|
||||
'service_mode' => 'physical',
|
||||
'service_provider' => $serviceProvider,
|
||||
'payment_status' => 'paid',
|
||||
'order_status' => 'pending_shipping',
|
||||
'display_status' => '待入库',
|
||||
'estimated_finish_time' => $estimated,
|
||||
'source_channel' => self::MANUAL_ENTRY_SOURCE,
|
||||
'source_customer_id' => '',
|
||||
'pay_amount' => (float)$serviceConfig['price'],
|
||||
'paid_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Db::name('order_products')->insert([
|
||||
'order_id' => $orderId,
|
||||
'category_id' => $categoryId,
|
||||
'category_name' => (string)$category['name'],
|
||||
'brand_id' => $brandId,
|
||||
'brand_name' => (string)$brand['name'],
|
||||
'color' => trim((string)($productInput['color'] ?? '')),
|
||||
'size_spec' => trim((string)($productInput['size_spec'] ?? '')),
|
||||
'serial_no' => trim((string)($productInput['serial_no'] ?? '')),
|
||||
'product_name' => $productName,
|
||||
'product_cover' => '',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Db::name('order_extras')->insert([
|
||||
'order_id' => $orderId,
|
||||
'purchase_channel' => trim((string)($extraInput['purchase_channel'] ?? '')),
|
||||
'purchase_price' => (float)($extraInput['purchase_price'] ?? 0),
|
||||
'purchase_date' => null,
|
||||
'usage_status' => trim((string)($extraInput['usage_status'] ?? '')),
|
||||
'condition_desc' => trim((string)($extraInput['condition_desc'] ?? '')),
|
||||
'has_accessories' => 0,
|
||||
'accessories_json' => json_encode([], JSON_UNESCAPED_UNICODE),
|
||||
'remark' => trim((string)($extraInput['remark'] ?? '')),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Db::name('order_return_addresses')->insert([
|
||||
'order_id' => $orderId,
|
||||
'user_address_id' => $addressId,
|
||||
'consignee' => $consignee,
|
||||
'mobile' => $mobile,
|
||||
'province' => $province,
|
||||
'city' => $city,
|
||||
'district' => $district,
|
||||
'detail_address' => $detailAddress,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$shippingTarget = (new WarehouseService())->bindOrderTarget($orderId, $serviceProvider, $categoryId, [
|
||||
'province' => $province,
|
||||
'city' => $city,
|
||||
'district' => $district,
|
||||
'detail_address' => $detailAddress,
|
||||
]);
|
||||
|
||||
$this->insertManualOrderMaterials($orderId, $materials, $now);
|
||||
|
||||
Db::name('appraisal_tasks')->insert([
|
||||
'order_id' => $orderId,
|
||||
'task_stage' => 'first_review',
|
||||
'service_provider' => $serviceProvider,
|
||||
'status' => 'pending',
|
||||
'assignee_id' => null,
|
||||
'assignee_name' => '未分配',
|
||||
'started_at' => null,
|
||||
'submitted_at' => null,
|
||||
'sla_deadline' => $estimated,
|
||||
'is_overtime' => 0,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Db::name('order_timelines')->insertAll([
|
||||
[
|
||||
'order_id' => $orderId,
|
||||
'node_code' => 'manual_created',
|
||||
'node_text' => '补录订单已创建',
|
||||
'node_desc' => '后台已补录订单资料,等待仓管入库。',
|
||||
'operator_type' => 'admin',
|
||||
'operator_id' => $operatorId,
|
||||
'occurred_at' => $now,
|
||||
'created_at' => $now,
|
||||
],
|
||||
[
|
||||
'order_id' => $orderId,
|
||||
'node_code' => 'pending_inbound',
|
||||
'node_text' => '待入库',
|
||||
'node_desc' => sprintf('可使用订单号或鉴定单号匹配入库,目标仓库:%s。', $shippingTarget['warehouse_name'] ?: '鉴定中心'),
|
||||
'operator_type' => 'system',
|
||||
'operator_id' => null,
|
||||
'occurred_at' => $now,
|
||||
'created_at' => $now,
|
||||
],
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return api_error('补录订单创建失败', 500, ['detail' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return api_success([
|
||||
'order_id' => $orderId,
|
||||
'order_no' => $orderNo,
|
||||
'appraisal_no' => $appraisalNo,
|
||||
'user_id' => (int)$user['id'],
|
||||
'next_status' => 'pending_shipping',
|
||||
], '补录订单已创建');
|
||||
}
|
||||
|
||||
public function manualOrderMeta(Request $request)
|
||||
{
|
||||
$categories = Db::name('catalog_categories')
|
||||
->field(['id', 'name', 'code', 'is_enabled', 'supported_service_types'])
|
||||
->where('is_enabled', 1)
|
||||
->order('sort_order', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$brands = Db::name('catalog_brands')
|
||||
->alias('b')
|
||||
->leftJoin('catalog_brand_categories cbc', 'cbc.brand_id = b.id')
|
||||
->field([
|
||||
'b.id',
|
||||
'b.name',
|
||||
'b.en_name',
|
||||
'b.code',
|
||||
'b.is_enabled',
|
||||
'b.supported_service_types',
|
||||
'GROUP_CONCAT(DISTINCT cbc.category_id) AS category_ids',
|
||||
])
|
||||
->where('b.is_enabled', 1)
|
||||
->group('b.id')
|
||||
->order('b.sort_order', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return api_success([
|
||||
'categories' => array_map(fn (array $item) => [
|
||||
'id' => (int)$item['id'],
|
||||
'name' => (string)$item['name'],
|
||||
'code' => (string)$item['code'],
|
||||
'supported_service_types' => $this->decodeJsonArray($item['supported_service_types'] ?? null),
|
||||
], $categories),
|
||||
'brands' => array_map(fn (array $item) => [
|
||||
'id' => (int)$item['id'],
|
||||
'name' => (string)$item['name'],
|
||||
'en_name' => (string)($item['en_name'] ?? ''),
|
||||
'code' => (string)($item['code'] ?? ''),
|
||||
'category_ids' => $this->decodeIntList($item['category_ids'] ?? ''),
|
||||
'supported_service_types' => $this->decodeJsonArray($item['supported_service_types'] ?? null),
|
||||
], $brands),
|
||||
]);
|
||||
}
|
||||
|
||||
public function uploadManualOrderFile(Request $request)
|
||||
{
|
||||
try {
|
||||
return api_success((new AppraisalEvidenceService())->upload($request));
|
||||
} catch (\Throwable $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
}
|
||||
}
|
||||
|
||||
private function inboundAttachments(int $orderId, Request $request): array
|
||||
{
|
||||
$logs = Db::name('order_transfer_flow_logs')
|
||||
->where('order_id', $orderId)
|
||||
->where('action_code', 'inbound_received')
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$attachments = [];
|
||||
foreach ($logs as $log) {
|
||||
$payload = $this->decodeJsonObject($log['payload_json'] ?? null);
|
||||
foreach ($this->decodeJsonArray($payload['inbound_attachments'] ?? []) as $item) {
|
||||
if (is_array($item)) {
|
||||
$attachments[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$normalized = (new AppraisalEvidenceService())->normalize($attachments, $request);
|
||||
|
||||
return array_values(array_filter($normalized, function (array $item) {
|
||||
return in_array((string)($item['file_type'] ?? ''), ['image', 'video'], true);
|
||||
}));
|
||||
}
|
||||
|
||||
private function decodeJsonArray(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return array_values($value);
|
||||
}
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? array_values($decoded) : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function decodeJsonObject(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function decodeIntList(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return array_values(array_filter(array_map('intval', $value), fn (int $item) => $item > 0));
|
||||
}
|
||||
if (!is_string($value) || trim($value) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map('intval', explode(',', $value)), fn (int $item) => $item > 0));
|
||||
}
|
||||
|
||||
private function requestArray(Request $request, string $key): array
|
||||
{
|
||||
$value = $request->input($key, []);
|
||||
return is_array($value) ? $value : [];
|
||||
}
|
||||
|
||||
private function normalizeServiceProvider(string $serviceProvider): string
|
||||
{
|
||||
$serviceProvider = trim($serviceProvider);
|
||||
return in_array($serviceProvider, ['anxinyan', 'zhongjian'], true) ? $serviceProvider : '';
|
||||
}
|
||||
|
||||
private function serviceConfig(string $serviceProvider): array
|
||||
{
|
||||
$configs = [
|
||||
'anxinyan' => ['price' => 99.00, 'sla_hours' => 48],
|
||||
'zhongjian' => ['price' => 199.00, 'sla_hours' => 72],
|
||||
];
|
||||
|
||||
return $configs[$serviceProvider] ?? $configs['anxinyan'];
|
||||
}
|
||||
|
||||
private function generateOrderNo(): string
|
||||
{
|
||||
do {
|
||||
$orderNo = 'AXY' . date('YmdHis') . mt_rand(100, 999);
|
||||
} while (Db::name('orders')->where('order_no', $orderNo)->find());
|
||||
|
||||
return $orderNo;
|
||||
}
|
||||
|
||||
private function generateAppraisalNo(): string
|
||||
{
|
||||
do {
|
||||
$appraisalNo = 'AXY-APP-' . date('Ymd') . '-' . mt_rand(1000, 9999);
|
||||
} while (Db::name('orders')->where('appraisal_no', $appraisalNo)->find());
|
||||
|
||||
return $appraisalNo;
|
||||
}
|
||||
|
||||
private function resolveManualOrderUser(string $consignee, string $mobile, string $now): array
|
||||
{
|
||||
$user = Db::name('users')
|
||||
->where('mobile', $mobile)
|
||||
->whereNull('deleted_at')
|
||||
->find();
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$userId = (int)Db::name('users')->insertGetId([
|
||||
'nickname' => $consignee,
|
||||
'avatar' => '',
|
||||
'mobile' => $mobile,
|
||||
'password' => '',
|
||||
'status' => 'enabled',
|
||||
'last_login_at' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
return Db::name('users')->where('id', $userId)->find();
|
||||
}
|
||||
|
||||
private function ensureUserAddress(int $userId, array $address, string $now): int
|
||||
{
|
||||
$existing = Db::name('user_addresses')
|
||||
->where('user_id', $userId)
|
||||
->where('consignee', (string)$address['consignee'])
|
||||
->where('mobile', (string)$address['mobile'])
|
||||
->where('province', (string)$address['province'])
|
||||
->where('city', (string)$address['city'])
|
||||
->where('district', (string)$address['district'])
|
||||
->where('detail_address', (string)$address['detail_address'])
|
||||
->find();
|
||||
if ($existing) {
|
||||
return (int)$existing['id'];
|
||||
}
|
||||
|
||||
$hasDefault = Db::name('user_addresses')
|
||||
->where('user_id', $userId)
|
||||
->where('is_default', 1)
|
||||
->find();
|
||||
|
||||
return (int)Db::name('user_addresses')->insertGetId([
|
||||
'user_id' => $userId,
|
||||
'consignee' => (string)$address['consignee'],
|
||||
'mobile' => (string)$address['mobile'],
|
||||
'province' => (string)$address['province'],
|
||||
'city' => (string)$address['city'],
|
||||
'district' => (string)$address['district'],
|
||||
'detail_address' => (string)$address['detail_address'],
|
||||
'is_default' => $hasDefault ? 0 : 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
private function insertManualOrderMaterials(int $orderId, array $materials, string $now): void
|
||||
{
|
||||
$evidenceService = new AppraisalEvidenceService();
|
||||
foreach ($materials as $index => $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$files = $evidenceService->normalize($item['files'] ?? [], null, true);
|
||||
if (!$files) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$orderUploadId = (int)Db::name('order_upload_items')->insertGetId([
|
||||
'order_id' => $orderId,
|
||||
'template_id' => null,
|
||||
'item_code' => trim((string)($item['item_code'] ?? 'manual_material_' . ($index + 1))),
|
||||
'item_name' => trim((string)($item['item_name'] ?? '补录资料')),
|
||||
'is_required' => !empty($item['is_required']) ? 1 : 0,
|
||||
'source_type' => 'initial',
|
||||
'status' => 'uploaded',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
foreach ($files as $file) {
|
||||
Db::name('order_upload_files')->insert([
|
||||
'order_upload_item_id' => $orderUploadId,
|
||||
'file_id' => (string)($file['file_id'] ?? ''),
|
||||
'file_url' => ltrim((string)($file['file_url'] ?? ''), '/'),
|
||||
'thumbnail_url' => ltrim((string)($file['thumbnail_url'] ?? ''), '/'),
|
||||
'quality_status' => 'uploaded',
|
||||
'quality_message' => '',
|
||||
'uploaded_by_user_id' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function trackingStatusText(string $status, string $logisticsType = 'send_to_center'): string
|
||||
{
|
||||
if ($logisticsType === 'return_to_user') {
|
||||
@@ -984,14 +1438,45 @@ class OrdersController
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function latestTransferFlowMap(array $orderIds): array
|
||||
{
|
||||
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
|
||||
if (!$orderIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Db::name('order_transfer_flows')
|
||||
->whereIn('order_id', $orderIds)
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$orderId = (int)($row['order_id'] ?? 0);
|
||||
if ($orderId > 0 && !isset($map[$orderId])) {
|
||||
$map[$orderId] = [
|
||||
'internal_tag_no' => (string)($row['internal_tag_no'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function warehouseOrderBucket(
|
||||
string $orderStatus,
|
||||
string $sendTrackingNo = '',
|
||||
string $sendTrackingStatus = '',
|
||||
string $displayStatus = ''
|
||||
string $displayStatus = '',
|
||||
string $sourceChannel = ''
|
||||
): string
|
||||
{
|
||||
if ($orderStatus === 'pending_shipping') {
|
||||
if ($sourceChannel === self::MANUAL_ENTRY_SOURCE && $sendTrackingNo === '') {
|
||||
return 'warehouse_pending_inbound';
|
||||
}
|
||||
|
||||
$hasSubmittedTracking = $sendTrackingNo !== '' && $sendTrackingStatus !== 'received';
|
||||
$hasSubmittedDisplayStatus = in_array($displayStatus, ['已提交运单', '用户已提交运单'], true)
|
||||
&& $sendTrackingStatus !== 'received';
|
||||
@@ -1020,6 +1505,7 @@ class OrdersController
|
||||
private function warehouseOrderBucketText(string $bucket): string
|
||||
{
|
||||
return match ($bucket) {
|
||||
'warehouse_pending_inbound' => '待入库',
|
||||
'warehouse_in_transit' => '在途',
|
||||
'warehouse_received' => '已入仓',
|
||||
'warehouse_pending_return' => '待寄回',
|
||||
@@ -1041,10 +1527,13 @@ class OrdersController
|
||||
'enterprise_order' => 'enterprise_push',
|
||||
'customer_push' => 'enterprise_push',
|
||||
'large_customer_push' => 'enterprise_push',
|
||||
'manual' => self::MANUAL_ENTRY_SOURCE,
|
||||
'manual_order' => self::MANUAL_ENTRY_SOURCE,
|
||||
'manual_entry' => self::MANUAL_ENTRY_SOURCE,
|
||||
];
|
||||
$sourceChannel = $aliases[$sourceChannel] ?? $sourceChannel;
|
||||
|
||||
return in_array($sourceChannel, ['mini_program', 'h5', 'enterprise_push'], true) ? $sourceChannel : '';
|
||||
return in_array($sourceChannel, ['mini_program', 'h5', 'enterprise_push', self::MANUAL_ENTRY_SOURCE], true) ? $sourceChannel : '';
|
||||
}
|
||||
|
||||
private function sourceChannelText(string $sourceChannel): string
|
||||
@@ -1053,6 +1542,7 @@ class OrdersController
|
||||
'mini_program' => '小程序',
|
||||
'h5' => 'H5',
|
||||
'enterprise_push' => '大客户推送订单',
|
||||
self::MANUAL_ENTRY_SOURCE => '后台补录订单',
|
||||
default => '未知渠道',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace app\controller\admin;
|
||||
use app\support\AppraisalEvidenceService;
|
||||
use app\support\ContentService;
|
||||
use app\support\EnterpriseWebhookService;
|
||||
use app\support\FulfillmentFlowService;
|
||||
use app\support\MaterialTagService;
|
||||
use app\support\MessageDispatcher;
|
||||
use support\Request;
|
||||
use support\think\Db;
|
||||
@@ -24,6 +26,7 @@ class ReportsController
|
||||
->alias('r')
|
||||
->leftJoin('orders o', 'o.id = r.order_id')
|
||||
->leftJoin('order_products p', 'p.order_id = r.order_id')
|
||||
->leftJoin('material_tag_codes mt', 'mt.report_id = r.id')
|
||||
->field([
|
||||
'r.id',
|
||||
'r.report_no',
|
||||
@@ -42,6 +45,9 @@ class ReportsController
|
||||
'p.product_name',
|
||||
'p.category_name',
|
||||
'p.brand_name',
|
||||
'mt.id as material_tag_id',
|
||||
'mt.verify_code as material_tag_verify_code',
|
||||
'mt.bind_status as material_tag_bind_status',
|
||||
])
|
||||
->order('r.id', 'desc');
|
||||
|
||||
@@ -80,6 +86,9 @@ class ReportsController
|
||||
'product_name' => $item['product_name'] ?: (string)($productSnapshot['product_name'] ?? ''),
|
||||
'category_name' => $item['category_name'] ?: (string)($productSnapshot['category_name'] ?? ''),
|
||||
'brand_name' => $item['brand_name'] ?: (string)($productSnapshot['brand_name'] ?? ''),
|
||||
'material_tag_bound' => (int)($item['material_tag_id'] ?? 0) > 0,
|
||||
'material_tag_verify_code' => (string)($item['material_tag_verify_code'] ?? ''),
|
||||
'material_tag_bind_status' => (string)($item['material_tag_bind_status'] ?? ''),
|
||||
];
|
||||
|
||||
if ($keyword !== '' && !$this->matchKeyword($mapped, $keyword)) {
|
||||
@@ -125,21 +134,21 @@ class ReportsController
|
||||
$zhongjianReportFiles = $this->evidenceService()->normalize($content['zhongjian_report_files_json'] ?? null, $request);
|
||||
$appraisalSnapshot = $this->enrichAppraisalSnapshot($report, $appraisalSnapshot);
|
||||
$evidenceAttachments = $this->evidenceService()->normalize($content['evidence_attachments_json'] ?? null, $request);
|
||||
$materialTag = (new MaterialTagService())->findBoundTagForReport($id);
|
||||
|
||||
$usesPlatformVerify = (string)($report['service_provider'] ?? '') !== 'zhongjian';
|
||||
$verify = $usesPlatformVerify ? (Db::name('report_verifies')->where('report_id', $id)->find() ?: []) : [];
|
||||
if ($usesPlatformVerify && ($report['report_status'] ?? '') === 'published') {
|
||||
$verify = Db::name('report_verifies')->where('report_id', $id)->find() ?: [];
|
||||
if (($report['report_status'] ?? '') === 'published') {
|
||||
$verify = $this->createOrUpdateVerifyRecord($report, date('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
$reportPageUrl = $usesPlatformVerify ? $this->buildPublicPageUrl('/pages/report/detail', ['report_no' => $report['report_no']]) : '';
|
||||
$verifyUrl = $usesPlatformVerify ? $this->buildPublicPageUrl('/pages/verify/result', ['report_no' => $report['report_no']]) : '';
|
||||
$reportPageUrl = $this->buildPublicPageUrl('/pages/report/detail', ['report_no' => $report['report_no']]);
|
||||
$verifyUrl = $this->buildPublicPageUrl('/pages/verify/result', ['report_no' => $report['report_no']]);
|
||||
if (!$verify) {
|
||||
$verify = [];
|
||||
}
|
||||
$verify['report_page_url'] = $usesPlatformVerify ? ($verify['report_page_url'] ?? $reportPageUrl) : '';
|
||||
$verify['verify_qrcode_url'] = $usesPlatformVerify ? ($verify['verify_qrcode_url'] ?? $reportPageUrl) : '';
|
||||
$verify['verify_url'] = $usesPlatformVerify ? ($verify['verify_url'] ?? $verifyUrl) : '';
|
||||
$verify['report_page_url'] = $verify['report_page_url'] ?? $reportPageUrl;
|
||||
$verify['verify_qrcode_url'] = $verify['verify_qrcode_url'] ?? $reportPageUrl;
|
||||
$verify['verify_url'] = $verify['verify_url'] ?? $verifyUrl;
|
||||
$defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($report['report_type'] ?? 'appraisal'));
|
||||
|
||||
return api_success([
|
||||
@@ -167,6 +176,7 @@ class ReportsController
|
||||
'valuation_info' => $valuationSnapshot,
|
||||
'evidence_attachments' => $evidenceAttachments,
|
||||
'zhongjian_report_files' => $zhongjianReportFiles,
|
||||
'material_tag' => $materialTag,
|
||||
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : $defaultRiskNotice,
|
||||
'verify_info' => [
|
||||
'verify_status' => $verify['verify_status'] ?? (($report['report_status'] ?? '') === 'published' ? 'valid' : 'pending'),
|
||||
@@ -333,11 +343,9 @@ class ReportsController
|
||||
'verify_url' => '',
|
||||
'report_page_url' => '',
|
||||
];
|
||||
$usesPlatformVerify = $serviceProvider !== 'zhongjian';
|
||||
|
||||
if ($reportStatus === 'published' && $reportRecord && $usesPlatformVerify) {
|
||||
if ($reportStatus === 'published' && $reportRecord) {
|
||||
$verifyInfo = $this->createOrUpdateVerifyRecord($reportRecord, $now);
|
||||
} else {
|
||||
} elseif ($reportStatus !== 'published') {
|
||||
Db::name('report_verifies')->where('report_id', $reportId)->delete();
|
||||
}
|
||||
|
||||
@@ -361,6 +369,7 @@ class ReportsController
|
||||
public function publish(Request $request)
|
||||
{
|
||||
$id = (int)$request->input('id', 0);
|
||||
$qrInput = trim((string)$request->input('qr_input', ''));
|
||||
if (!$id) {
|
||||
return api_error('报告 ID 不能为空', 422);
|
||||
}
|
||||
@@ -381,7 +390,29 @@ class ReportsController
|
||||
}
|
||||
|
||||
$effectivePublishTime = $report['publish_time'] ?: $now;
|
||||
$usesPlatformVerify = (string)($report['service_provider'] ?? '') !== 'zhongjian';
|
||||
$isOrderAppraisalReport = ($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0;
|
||||
$materialTag = null;
|
||||
if ($isOrderAppraisalReport) {
|
||||
$materialTag = (new MaterialTagService())->findBoundTagForReport($id);
|
||||
if (!$materialTag) {
|
||||
if ($qrInput === '') {
|
||||
Db::rollback();
|
||||
return api_error('请扫描验真吊牌二维码后再发布报告', 422);
|
||||
}
|
||||
|
||||
$task = Db::name('appraisal_tasks')
|
||||
->where('order_id', (int)$report['order_id'])
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
if (!$task) {
|
||||
Db::rollback();
|
||||
return api_error('报告未关联鉴定任务,不能绑定吊牌发布', 422);
|
||||
}
|
||||
|
||||
$materialTag = (new MaterialTagService())->bindTagToReportByTask((int)$task['id'], $qrInput, $request);
|
||||
}
|
||||
}
|
||||
|
||||
if ($report['report_status'] !== 'published') {
|
||||
Db::name('reports')->where('id', $id)->update([
|
||||
'report_status' => 'published',
|
||||
@@ -392,18 +423,13 @@ class ReportsController
|
||||
$report['publish_time'] = $effectivePublishTime;
|
||||
}
|
||||
|
||||
if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) {
|
||||
if ($isOrderAppraisalReport) {
|
||||
$this->refreshAppraisalSnapshot((int)$report['id'], (int)$report['order_id'], $report['service_provider'], $now);
|
||||
}
|
||||
|
||||
$verify = [];
|
||||
if ($usesPlatformVerify) {
|
||||
$verify = $this->createOrUpdateVerifyRecord($report, $now);
|
||||
} else {
|
||||
Db::name('report_verifies')->where('report_id', $id)->delete();
|
||||
}
|
||||
$verify = $this->createOrUpdateVerifyRecord($report, $now);
|
||||
|
||||
if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) {
|
||||
if ($isOrderAppraisalReport) {
|
||||
Db::name('orders')->where('id', $report['order_id'])->update([
|
||||
'order_status' => 'report_published',
|
||||
'display_status' => '报告已出具',
|
||||
@@ -424,7 +450,7 @@ class ReportsController
|
||||
'order_id' => $report['order_id'],
|
||||
'node_code' => 'report_published',
|
||||
'node_text' => '报告已出具',
|
||||
'node_desc' => $usesPlatformVerify ? '正式报告已发布,用户可查看报告并进行验真。' : '中检报告已发布,用户可查看报告。',
|
||||
'node_desc' => '正式报告已发布,用户可查看报告并进行验真。',
|
||||
'operator_type' => 'admin',
|
||||
'operator_id' => (int)$request->header('x-admin-id', 0) ?: null,
|
||||
'occurred_at' => $now,
|
||||
@@ -440,22 +466,24 @@ class ReportsController
|
||||
'report_title' => $report['report_title'],
|
||||
'product_name' => $product['product_name'] ?? '',
|
||||
'publish_time' => $report['publish_time'] ?: $now,
|
||||
'verify_url' => $usesPlatformVerify ? (string)($verify['verify_url'] ?? '') : '',
|
||||
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
||||
'fallback_title' => '报告已出具',
|
||||
'fallback_content' => $usesPlatformVerify ? '您的正式报告已生成,可前往报告中心查看并完成验真。' : '您的中检报告已生成,可前往报告中心查看。',
|
||||
'fallback_content' => '您的正式报告已生成,可前往报告中心查看并完成验真。',
|
||||
]);
|
||||
|
||||
(new FulfillmentFlowService())->markReportPublished((int)$report['order_id'], $request);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
|
||||
if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) {
|
||||
if ($isOrderAppraisalReport) {
|
||||
(new EnterpriseWebhookService())->recordOrderEvent((int)$report['order_id'], 'report_published', [
|
||||
'report_id' => $id,
|
||||
'report_no' => (string)$report['report_no'],
|
||||
'report_title' => (string)$report['report_title'],
|
||||
'publish_time' => $effectivePublishTime,
|
||||
'verify_url' => $usesPlatformVerify ? (string)($verify['verify_url'] ?? '') : '',
|
||||
'report_page_url' => $usesPlatformVerify ? (string)($verify['report_page_url'] ?? '') : '',
|
||||
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
||||
'report_page_url' => (string)($verify['report_page_url'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -463,8 +491,9 @@ class ReportsController
|
||||
'id' => $id,
|
||||
'report_status' => 'published',
|
||||
'publish_time' => $effectivePublishTime,
|
||||
'verify_url' => $usesPlatformVerify ? (string)($verify['verify_url'] ?? '') : '',
|
||||
'report_page_url' => $usesPlatformVerify ? (string)($verify['report_page_url'] ?? '') : '',
|
||||
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
||||
'report_page_url' => (string)($verify['report_page_url'] ?? ''),
|
||||
'material_tag' => $materialTag,
|
||||
], '报告已发布');
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\support\AppraisalEvidenceService;
|
||||
use app\support\FulfillmentFlowService;
|
||||
use support\Request;
|
||||
|
||||
@@ -10,7 +11,7 @@ class WarehouseWorkbenchController
|
||||
public function inboundLookup(Request $request)
|
||||
{
|
||||
try {
|
||||
return api_success($this->service()->lookupInboundByTrackingNo((string)$request->input('tracking_no', '')));
|
||||
return api_success($this->service()->lookupInboundByInboundNo($this->inboundNo($request)));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
} catch (\RuntimeException $e) {
|
||||
@@ -24,9 +25,10 @@ class WarehouseWorkbenchController
|
||||
{
|
||||
try {
|
||||
return api_success($this->service()->receiveInbound(
|
||||
(string)$request->input('tracking_no', ''),
|
||||
$this->inboundNo($request),
|
||||
(string)$request->input('internal_tag_no', ''),
|
||||
$request
|
||||
$request,
|
||||
$request->input('inbound_attachments', [])
|
||||
), '入库完成');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
@@ -37,6 +39,38 @@ class WarehouseWorkbenchController
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadInboundEvidenceFile(Request $request)
|
||||
{
|
||||
$evidenceService = new AppraisalEvidenceService();
|
||||
try {
|
||||
$asset = $evidenceService->upload($request);
|
||||
if (!in_array((string)($asset['file_type'] ?? ''), ['image', 'video'], true)) {
|
||||
$evidenceService->delete((string)($asset['file_url'] ?? ''));
|
||||
return api_error('拆包附件仅支持上传图片或视频', 422);
|
||||
}
|
||||
|
||||
return api_success($asset);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadReturnPackingFile(Request $request)
|
||||
{
|
||||
$evidenceService = new AppraisalEvidenceService();
|
||||
try {
|
||||
$asset = $evidenceService->upload($request);
|
||||
if (!in_array((string)($asset['file_type'] ?? ''), ['image', 'video'], true)) {
|
||||
$evidenceService->delete((string)($asset['file_url'] ?? ''));
|
||||
return api_error('打包装箱附件仅支持上传图片或视频', 422);
|
||||
}
|
||||
|
||||
return api_success($asset);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
}
|
||||
}
|
||||
|
||||
public function zhongjianLookup(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -96,7 +130,7 @@ class WarehouseWorkbenchController
|
||||
(string)$request->input('internal_tag_no', ''),
|
||||
(string)$request->input('qr_input', ''),
|
||||
$request
|
||||
), '验真吊牌已确认');
|
||||
), '验真吊牌匹配通过,请核对报告');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
} catch (\RuntimeException $e) {
|
||||
@@ -119,6 +153,23 @@ class WarehouseWorkbenchController
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmReturnReport(Request $request)
|
||||
{
|
||||
try {
|
||||
return api_success($this->service()->confirmReturnReport(
|
||||
(string)$request->input('internal_tag_no', ''),
|
||||
(int)$request->input('report_id', 0),
|
||||
$request
|
||||
), '报告已确认,可填写回寄运单');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
} catch (\RuntimeException $e) {
|
||||
return api_error($e->getMessage(), $e->getCode() ?: 404);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error('报告确认失败', 500, ['detail' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function shipReturn(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -126,7 +177,8 @@ class WarehouseWorkbenchController
|
||||
(string)$request->input('internal_tag_no', ''),
|
||||
(string)$request->input('express_company', ''),
|
||||
(string)$request->input('tracking_no', ''),
|
||||
$request
|
||||
$request,
|
||||
$request->input('packing_attachments', [])
|
||||
), '回寄运单已登记');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
@@ -141,4 +193,26 @@ class WarehouseWorkbenchController
|
||||
{
|
||||
return new FulfillmentFlowService();
|
||||
}
|
||||
|
||||
private function inboundNo(Request $request): string
|
||||
{
|
||||
$inboundNo = $this->requestString($request, 'inbound_no');
|
||||
if ($inboundNo !== '') {
|
||||
return $inboundNo;
|
||||
}
|
||||
|
||||
return $this->requestString($request, 'tracking_no');
|
||||
}
|
||||
|
||||
private function requestString(Request $request, string $key): string
|
||||
{
|
||||
foreach ([$request->get($key, null), $request->post($key, null), $request->input($key, null)] as $value) {
|
||||
$text = trim((string)($value ?? ''));
|
||||
if ($text !== '') {
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,10 +466,12 @@ class OrdersController
|
||||
'enterprise_order' => 'enterprise_push',
|
||||
'customer_push' => 'enterprise_push',
|
||||
'large_customer_push' => 'enterprise_push',
|
||||
'manual' => 'manual_entry',
|
||||
'manual_order' => 'manual_entry',
|
||||
];
|
||||
$sourceChannel = $aliases[$sourceChannel] ?? $sourceChannel;
|
||||
|
||||
return in_array($sourceChannel, ['mini_program', 'h5', 'enterprise_push'], true) ? $sourceChannel : '';
|
||||
return in_array($sourceChannel, ['mini_program', 'h5', 'enterprise_push', 'manual_entry'], true) ? $sourceChannel : '';
|
||||
}
|
||||
|
||||
private function sourceChannelText(string $sourceChannel): string
|
||||
@@ -478,6 +480,7 @@ class OrdersController
|
||||
'mini_program' => '小程序',
|
||||
'h5' => 'H5',
|
||||
'enterprise_push' => '大客户推送订单',
|
||||
'manual_entry' => '后台补录订单',
|
||||
default => '未知渠道',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,9 +92,8 @@ class ReportsController
|
||||
|
||||
$reportData = is_array($report) ? $report : $report->toArray();
|
||||
$content = Db::name('report_contents')->where('report_id', $reportData['id'])->find();
|
||||
$isZhongjian = (string)($reportData['service_provider'] ?? '') === 'zhongjian';
|
||||
$verify = $isZhongjian ? [] : (Db::name('report_verifies')->where('report_id', $reportData['id'])->find() ?: []);
|
||||
$verify = $isZhongjian ? [] : $this->normalizeVerifyInfo($reportData, $verify);
|
||||
$verify = Db::name('report_verifies')->where('report_id', $reportData['id'])->find() ?: [];
|
||||
$verify = $this->normalizeVerifyInfo($reportData, $verify);
|
||||
$pdfUrl = $this->ensurePdfFile($request, $reportData, $content ?: [], $verify ?: []);
|
||||
$evidenceAttachments = $this->evidenceService()->normalize($content['evidence_attachments_json'] ?? null, $request);
|
||||
$zhongjianReportFiles = $this->evidenceService()->normalize($content['zhongjian_report_files_json'] ?? null, $request);
|
||||
@@ -130,9 +129,9 @@ class ReportsController
|
||||
'risk_notice_text' => $payload['risk_notice_text'],
|
||||
'verify_info' => [
|
||||
'report_no' => $reportData['report_no'],
|
||||
'verify_status' => $isZhongjian ? '' : ($verify['verify_status'] ?? 'valid'),
|
||||
'verify_url' => $isZhongjian ? '' : ($verify['verify_url'] ?? ''),
|
||||
'verify_qrcode_url' => $isZhongjian ? '' : ($verify['verify_qrcode_url'] ?? ''),
|
||||
'verify_status' => $verify['verify_status'] ?? 'valid',
|
||||
'verify_url' => $verify['verify_url'] ?? '',
|
||||
'verify_qrcode_url' => $verify['verify_qrcode_url'] ?? '',
|
||||
],
|
||||
'file_info' => [
|
||||
'pdf_url' => $pdfUrl,
|
||||
@@ -218,9 +217,7 @@ class ReportsController
|
||||
'verify_info' => sprintf(
|
||||
'%s / %s',
|
||||
$verify['report_no'] ?? ($report['report_no'] ?? '-'),
|
||||
($report['service_provider'] ?? '') === 'zhongjian'
|
||||
? '中检报告'
|
||||
: (($verify['verify_status'] ?? 'valid') === 'valid' ? '有效' : ($verify['verify_status'] ?? '-'))
|
||||
(($verify['verify_status'] ?? 'valid') === 'valid' ? '有效' : ($verify['verify_status'] ?? '-'))
|
||||
),
|
||||
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : ($defaultRiskNotice !== '' ? $defaultRiskNotice : '-'),
|
||||
]);
|
||||
|
||||
@@ -52,6 +52,7 @@ class AdminAuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
return match (true) {
|
||||
str_starts_with($path, '/api/admin/dashboard') => ['dashboard.view'],
|
||||
str_starts_with($path, '/api/admin/manual-order/') => ['orders.manage', 'warehouse_workbench.manage'],
|
||||
str_starts_with($path, '/api/admin/orders') && strtoupper($method) === 'GET' => ['orders.manage', 'warehouse_workbench.manage'],
|
||||
str_starts_with($path, '/api/admin/order/') && strtoupper($method) === 'GET' => ['orders.manage', 'warehouse_workbench.manage'],
|
||||
str_starts_with($path, '/api/admin/orders'),
|
||||
|
||||
@@ -9,31 +9,21 @@ 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']);
|
||||
return $this->lookupInboundByInboundNo($trackingNo);
|
||||
}
|
||||
|
||||
public function receiveInbound(string $trackingNo, string $tagNo, Request $request): array
|
||||
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);
|
||||
$context = $this->lookupInboundByTrackingNo($trackingNo);
|
||||
$match = $this->resolveInboundOrder($inboundNo);
|
||||
$context = $this->formatOrderContext((int)$match['order_id']);
|
||||
$order = $context['order_info'];
|
||||
$orderId = (int)$order['id'];
|
||||
|
||||
@@ -47,6 +37,12 @@ class FulfillmentFlowService
|
||||
}
|
||||
|
||||
$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 {
|
||||
@@ -114,8 +110,8 @@ class FulfillmentFlowService
|
||||
$logistics = Db::name('order_logistics')
|
||||
->where('order_id', $orderId)
|
||||
->where('logistics_type', 'send_to_center')
|
||||
->where('tracking_no', trim($trackingNo))
|
||||
->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([
|
||||
@@ -140,7 +136,11 @@ class FulfillmentFlowService
|
||||
]);
|
||||
|
||||
$this->insertTimeline($orderId, 'inbound_received', '已入仓待检', '仓管扫描寄入运单并完成物品入库。', $operator, $now);
|
||||
$this->insertFlowLog($flow, 'inbound_received', '入库完成', '', '', 'warehouse_received', 'warehouse_pending_inspection', $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();
|
||||
@@ -149,7 +149,7 @@ class FulfillmentFlowService
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $this->formatOrderContext($orderId);
|
||||
return $this->formatOrderContext($orderId, $request);
|
||||
}
|
||||
|
||||
public function scanTransferForAppraisal(string $tagNo, Request $request): array
|
||||
@@ -283,15 +283,10 @@ class FulfillmentFlowService
|
||||
|
||||
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('订单报告未发布,不能确认寄回');
|
||||
@@ -302,7 +297,13 @@ class FulfillmentFlowService
|
||||
throw new \InvalidArgumentException('验真吊牌与当前订单报告不匹配');
|
||||
}
|
||||
|
||||
return $this->markReturnConfirmed($flow, $operator, 'return_tag_verified', '验真吊牌确认', '仓管已扫描验真吊牌并确认报告信息。');
|
||||
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
|
||||
@@ -326,7 +327,44 @@ class FulfillmentFlowService
|
||||
return $this->markReturnConfirmed($flow, $operator, 'return_confirmed', '中检报告已确认', '仓管已查看中检报告编号和报告文件。');
|
||||
}
|
||||
|
||||
public function shipReturn(string $tagNo, string $expressCompany, string $trackingNo, Request $request): array
|
||||
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('订单报告未发布,不能确认寄回');
|
||||
}
|
||||
if ((int)$report['id'] !== $reportId) {
|
||||
throw new \InvalidArgumentException('确认的报告与当前订单报告不匹配');
|
||||
}
|
||||
if ((string)($flow['current_stage'] ?? '') === 'return_confirmed') {
|
||||
return $this->formatOrderContext((int)$flow['order_id'], $request);
|
||||
}
|
||||
|
||||
if (($flow['service_provider'] ?? '') === 'zhongjian') {
|
||||
$content = Db::name('report_contents')->where('report_id', (int)$report['id'])->find();
|
||||
$files = $this->decodeJsonArray($content['zhongjian_report_files_json'] ?? null);
|
||||
if (trim((string)($report['zhongjian_report_no'] ?? '')) === '' || !$files) {
|
||||
throw new \InvalidArgumentException('中检报告未完整录入,不能确认寄回');
|
||||
}
|
||||
|
||||
return $this->markReturnConfirmed($flow, $operator, 'return_confirmed', '中检报告已确认', '仓管已查看中检报告编号和报告文件。');
|
||||
}
|
||||
|
||||
$boundTag = (new MaterialTagService())->findBoundTagForReport((int)$report['id']);
|
||||
if (!$boundTag) {
|
||||
throw new \InvalidArgumentException('当前报告未绑定验真吊牌,不能确认寄回');
|
||||
}
|
||||
|
||||
return $this->markReturnConfirmed($flow, $operator, 'return_tag_verified', '验真吊牌确认', '仓管已核对验真吊牌与报告信息。');
|
||||
}
|
||||
|
||||
public function shipReturn(string $tagNo, string $expressCompany, string $trackingNo, Request $request, array $packingAttachments = []): array
|
||||
{
|
||||
$operator = $this->operator($request);
|
||||
$flow = $this->findActiveFlowByTagNo($tagNo);
|
||||
@@ -342,6 +380,12 @@ class FulfillmentFlowService
|
||||
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();
|
||||
@@ -432,7 +476,9 @@ class FulfillmentFlowService
|
||||
]);
|
||||
|
||||
$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);
|
||||
$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),
|
||||
@@ -547,18 +593,7 @@ class FulfillmentFlowService
|
||||
'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),
|
||||
'flow_logs' => array_map(fn (array $log) => $this->formatFlowLog($log, $request), $flowLogs),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -649,7 +684,7 @@ class FulfillmentFlowService
|
||||
]);
|
||||
}
|
||||
|
||||
private function insertFlowLog(array $flow, string $code, string $text, string $beforeStage, string $beforeLocation, string $afterStage, string $afterLocation, array $operator, string $remark, string $now): void
|
||||
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'],
|
||||
@@ -665,11 +700,33 @@ class FulfillmentFlowService
|
||||
'operator_id' => $operator['id'],
|
||||
'operator_name' => $operator['name'],
|
||||
'remark' => mb_substr($remark, 0, 500),
|
||||
'payload_json' => null,
|
||||
'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();
|
||||
@@ -695,6 +752,89 @@ class FulfillmentFlowService
|
||||
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);
|
||||
@@ -849,6 +989,7 @@ class FulfillmentFlowService
|
||||
'mini_program' => '小程序',
|
||||
'h5' => 'H5',
|
||||
'enterprise_push' => '大客户推送订单',
|
||||
'manual_entry' => '后台补录订单',
|
||||
default => $sourceChannel ?: '未知渠道',
|
||||
};
|
||||
}
|
||||
@@ -865,6 +1006,19 @@ class FulfillmentFlowService
|
||||
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) {
|
||||
|
||||
@@ -352,9 +352,6 @@ class MaterialTagService
|
||||
if (!$task) {
|
||||
throw new \RuntimeException('任务不存在', 404);
|
||||
}
|
||||
if (($task['service_provider'] ?? '') === 'zhongjian') {
|
||||
throw new \InvalidArgumentException('中检订单不使用平台验真吊牌');
|
||||
}
|
||||
$report = Db::name('reports')
|
||||
->where('order_id', (int)$task['order_id'])
|
||||
->where('report_type', 'appraisal')
|
||||
|
||||
@@ -195,6 +195,9 @@ Route::post('/api/admin/auth/logout', [AdminAuthController::class, 'logout']);
|
||||
Route::get('/api/admin/dashboard', [AdminDashboardController::class, 'index']);
|
||||
Route::get('/api/admin/orders', [AdminOrdersController::class, 'index']);
|
||||
Route::get('/api/admin/order/detail', [AdminOrdersController::class, 'detail']);
|
||||
Route::get('/api/admin/manual-order/meta', [AdminOrdersController::class, 'manualOrderMeta']);
|
||||
Route::post('/api/admin/manual-order/create', [AdminOrdersController::class, 'createManualOrder']);
|
||||
Route::post('/api/admin/manual-order/file/upload', [AdminOrdersController::class, 'uploadManualOrderFile']);
|
||||
Route::get('/api/admin/order/warehouse/options', [AdminOrdersController::class, 'warehouseOptions']);
|
||||
Route::post('/api/admin/order/warehouse/reassign', [AdminOrdersController::class, 'reassignWarehouse']);
|
||||
Route::post('/api/admin/order/logistics/receive', [AdminOrdersController::class, 'receiveLogistics']);
|
||||
@@ -258,12 +261,15 @@ Route::get('/api/admin/warehouses', [AdminWarehousesController::class, 'index'])
|
||||
Route::post('/api/admin/warehouse/save', [AdminWarehousesController::class, 'save']);
|
||||
Route::get('/api/admin/warehouse-workbench/inbound/lookup', [AdminWarehouseWorkbenchController::class, 'inboundLookup']);
|
||||
Route::post('/api/admin/warehouse-workbench/inbound/receive', [AdminWarehouseWorkbenchController::class, 'inboundReceive']);
|
||||
Route::post('/api/admin/warehouse-workbench/inbound/evidence/upload', [AdminWarehouseWorkbenchController::class, 'uploadInboundEvidenceFile']);
|
||||
Route::post('/api/admin/warehouse-workbench/return/packing/upload', [AdminWarehouseWorkbenchController::class, 'uploadReturnPackingFile']);
|
||||
Route::get('/api/admin/warehouse-workbench/zhongjian/lookup', [AdminWarehouseWorkbenchController::class, 'zhongjianLookup']);
|
||||
Route::post('/api/admin/warehouse-workbench/zhongjian/outbound', [AdminWarehouseWorkbenchController::class, 'zhongjianOutbound']);
|
||||
Route::post('/api/admin/warehouse-workbench/zhongjian/inbound', [AdminWarehouseWorkbenchController::class, 'zhongjianInbound']);
|
||||
Route::get('/api/admin/warehouse-workbench/return/lookup', [AdminWarehouseWorkbenchController::class, 'returnLookup']);
|
||||
Route::post('/api/admin/warehouse-workbench/return/material-tag/verify', [AdminWarehouseWorkbenchController::class, 'verifyReturnMaterialTag']);
|
||||
Route::post('/api/admin/warehouse-workbench/return/zhongjian/confirm', [AdminWarehouseWorkbenchController::class, 'confirmZhongjianReturn']);
|
||||
Route::post('/api/admin/warehouse-workbench/return/report/confirm', [AdminWarehouseWorkbenchController::class, 'confirmReturnReport']);
|
||||
Route::post('/api/admin/warehouse-workbench/return/ship', [AdminWarehouseWorkbenchController::class, 'shipReturn']);
|
||||
Route::get('/api/admin/material/batches', [AdminMaterialsController::class, 'batches']);
|
||||
Route::get('/api/admin/material/batch/detail', [AdminMaterialsController::class, 'detail']);
|
||||
|
||||
Reference in New Issue
Block a user