chore: prepare release build

This commit is contained in:
wushumin
2026-05-16 16:32:56 +08:00
parent dd56e0861b
commit deecb5d33e
28 changed files with 4396 additions and 361 deletions

View File

@@ -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) {