From 2915e6d599b38e8ab88d5722ae2f05839c56ea98 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Mon, 10 Aug 2026 12:05:20 +0800
Subject: [PATCH] feat: complete phase three admin loading workflow
---
.../pages/UnifiedOrdersView.test.ts | 4 +-
.../pages/UnifiedOrdersView.vue | 4 +-
.../unified-orders/workflows/README.md | 7 +-
.../unified-orders/workflows/api.test.ts | 28 ++--
src/modules/unified-orders/workflows/api.ts | 92 +++++++++---
.../components/UnifiedOrderWorkflowPanel.vue | 134 ++++++++++++------
.../pages/UnifiedOrderWorkflowView.vue | 2 +-
7 files changed, 191 insertions(+), 80 deletions(-)
diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts
index a4e55d36..ca836a86 100644
--- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts
+++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts
@@ -148,8 +148,8 @@ describe('UnifiedOrdersView contract', () => {
it('mounts downstream progress inside the existing order detail without opening unsafe writes', () => {
expect(source).toContain('UnifiedOrderWorkflowPanel')
expect(source).toContain("{ key: 'workflow', label: '后半程作业'")
- expect(source).toContain(':read-only="true"')
- expect(source).toContain(':show-delivery="false"')
+ expect(source).toContain(':order-id="String(detail.summary.orderId)"')
+ expect(source).not.toContain(':show-delivery=')
expect(source).toContain("detail.value?.summary.inboundId")
expect(routeSource).not.toContain('unified-order-workflow')
})
diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue
index 20dbac01..b1364dc7 100644
--- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue
+++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue
@@ -931,12 +931,10 @@ onMounted(() => {
diff --git a/src/modules/unified-orders/workflows/README.md b/src/modules/unified-orders/workflows/README.md
index a588c46e..dc058054 100644
--- a/src/modules/unified-orders/workflows/README.md
+++ b/src/modules/unified-orders/workflows/README.md
@@ -7,10 +7,11 @@
- `POST /split/smart`:智能分柜/保存预装清单
- `GET /preload`、`POST /preload/confirming`、`POST /preload/confirm`:预装清单查询与确认
- `GET/POST /loading/picking`、`/loading/picking/start`、`/loading/picking/complete`:拣货
-- `GET/POST /loading/evidence`:四步照片与封签证据元数据
+- `POST /platform/v1/files/loading-evidence/{tenantId}/{inboundId}`:上传真实装柜证据文件
+- `GET/POST /loading/evidence`:查询并登记五类装柜证据及锁码
+- `POST /loading/evidence/{evidenceId}/void`:按原因作废错误照片并保留审计
- `POST /loading/complete`:完成装柜
-- `POST /delivery/complete`:确认派送完成
-所有写请求带 `Idempotency-Key` 和服务端要求的 `expectedVersion`。`loading/complete` 与 `delivery/complete` 使用入库单版本;拣货使用拣货任务版本。当前没有 File 服务上传接口,证据组件不会把本地路径伪装成已上传文件。
+所有 Firstleg 写请求带 `Idempotency-Key` 和服务端要求的 `expectedVersion`。`loading/complete` 使用入库单版本;拣货使用拣货任务版本。国内端不提供在线冲正、回退或推进 `DELIVERED`。
权限码、状态流转和请求字段以 Firstleg Controller/DTO 为准;本模块不新增“进入分柜”操作,也不定义通用 `/actions/{ACTION}` 接口。
diff --git a/src/modules/unified-orders/workflows/api.test.ts b/src/modules/unified-orders/workflows/api.test.ts
index cb45a30e..da151999 100644
--- a/src/modules/unified-orders/workflows/api.test.ts
+++ b/src/modules/unified-orders/workflows/api.test.ts
@@ -1,12 +1,13 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import {
- completeDelivery,
completeLoading,
confirmingPreload,
getPicking,
getPreload,
smartSplit,
+ uploadEvidenceFile,
+ voidEvidence,
} from './api'
vi.mock('@/shared/api/http', () => ({
@@ -45,15 +46,6 @@ describe('unified order workflow API contract', () => {
expect(vi.mocked(http.post).mock.calls.some(([url]) => String(url).includes('/actions/'))).toBe(false)
})
- it('uses the inbound version for delivery completion', async () => {
- vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'DELIVERED', evidences: null } })
- await completeDelivery('2', '8', { expectedVersion: 9, remark: '已确认派送' })
- expect(http.post).toHaveBeenCalledWith(
- '/platform/v1/inbounds/8/delivery/complete', { expectedVersion: 9, remark: '已确认派送' },
- expect.objectContaining({ params: { tenantId: '2' }, headers: { 'Idempotency-Key': expect.any(String) } }),
- )
- })
-
it('does not swallow non-success responses', async () => {
vi.mocked(http.get).mockRejectedValue(new Error('无权限'))
await expect(getPreload('2', '8')).rejects.toThrow('无权限')
@@ -67,4 +59,20 @@ describe('unified order workflow API contract', () => {
expect.objectContaining({ params: { tenantId: '2' } }),
)
})
+
+ it('uploads a real evidence file and voids evidence through audited endpoints', async () => {
+ vi.mocked(http.post)
+ .mockResolvedValueOnce({ data: { fileId: '88', fileName: 'empty.jpg' } })
+ .mockResolvedValueOnce({ data: { mainStatus: 'LOADING', evidences: { inboundId: '8', evidences: [] } } })
+ await uploadEvidenceFile('2', '8', 'PHOTO_STEP_1', new File(['photo'], 'empty.jpg', { type: 'image/jpeg' }))
+ await voidEvidence('2', '8', '33', { expectedVersion: 2, reason: '照片错误' })
+ expect(http.post).toHaveBeenNthCalledWith(1,
+ '/platform/v1/files/loading-evidence/2/8', expect.any(FormData),
+ { headers: { 'Content-Type': 'multipart/form-data' } },
+ )
+ expect(http.post).toHaveBeenNthCalledWith(2,
+ '/platform/v1/inbounds/8/loading/evidence/33/void', { expectedVersion: 2, reason: '照片错误' },
+ expect.objectContaining({ params: { tenantId: '2' }, headers: { 'Idempotency-Key': expect.any(String) } }),
+ )
+ })
})
diff --git a/src/modules/unified-orders/workflows/api.ts b/src/modules/unified-orders/workflows/api.ts
index fbc4cf14..978c4fbf 100644
--- a/src/modules/unified-orders/workflows/api.ts
+++ b/src/modules/unified-orders/workflows/api.ts
@@ -2,7 +2,25 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
const inboundBasePath = '/platform/v1/inbounds'
+export interface OutboundGateItem { code: string; name: string; passed: boolean; actual?: string | null; requirement?: string | null }
+export interface OutboundDispatchAggregate {
+ orderId: number | string | null
+ orderNo: string | null
+ inboundId: number | string | null
+ inboundCode: string | null
+ containerBookingId: number | string | null
+ containerNo: string | null
+ oceanBookingId: number | string | null
+ oceanBookingStatus: string | null
+ truckAppointmentId: number | string | null
+ truckAppointmentStatus: string | null
+ customsStatus?: string | null
+ sailingStatus?: string | null
+ gate: { passed: boolean; items: OutboundGateItem[] } | null
+}
+
export type PreloadStatus = 'PENDING' | 'CONFIRMING' | 'CONFIRMED' | string
+/** 五类装柜取证。旧接口仍使用 PHOTO_STEP_* / SEAL 编码,页面统一映射为业务名称。 */
export type EvidenceType = 'PHOTO_STEP_1' | 'PHOTO_STEP_2' | 'PHOTO_STEP_3' | 'PHOTO_STEP_4' | 'SEAL'
export interface PreloadItem {
@@ -50,8 +68,13 @@ export interface Evidence {
sealNo: string | null
remark: string | null
version: number
+ status: 'ACTIVE' | 'VOIDED' | string
createdAt: string | null
updatedAt: string | null
+ voidReason?: string | null
+ voidedBy?: number | string | null
+ voidedByName?: string | null
+ voidedAt?: string | null
}
export interface EvidenceList {
@@ -90,15 +113,23 @@ export interface UpsertEvidenceRequest {
remark?: string
}
+export interface UploadEvidenceResult {
+ fileId: number | string
+ fileName: string | null
+ contentType?: string | null
+ sizeBytes?: number | null
+}
+
+export interface VoidEvidenceRequest {
+ expectedVersion: number
+ reason: string
+}
+
export interface CompleteLoadingRequest {
expectedVersion: number
remark?: string
}
-export interface CompleteDeliveryRequest {
- expectedVersion: number
- remark?: string
-}
function query(tenantId: string | number) {
return { tenantId: String(tenantId) }
@@ -119,6 +150,15 @@ export async function getPreload(tenantId: string | number, inboundId: string |
return unwrapApiResponse(response)
}
+export async function getOutboundDispatch(tenantId: string | number, orderId?: string | number, inboundId?: string | number) {
+ const response = await http.get, ApiEnvelope>(
+ '/platform/v1/first-leg/outbound-dispatches',
+ { params: { tenantId: String(tenantId), ...(orderId ? { orderId: String(orderId) } : {}), ...(inboundId ? { inboundId: String(inboundId) } : {}) } },
+ )
+ const result = unwrapApiResponse(response)
+ return result?.[0] || null
+}
+
export async function smartSplit(
tenantId: string | number,
inboundId: string | number,
@@ -204,6 +244,38 @@ export async function upsertEvidence(
return unwrapApiResponse(response)
}
+/** 真实 File 服务上传;返回的 fileId 才能提交证据,不接受本地文件名冒充。 */
+export async function uploadEvidenceFile(
+ tenantId: string | number,
+ inboundId: string | number,
+ evidenceType: EvidenceType,
+ file: File,
+) {
+ const body = new FormData()
+ body.append('evidenceType', evidenceType)
+ body.append('file', file)
+ const response = await http.post, ApiEnvelope>(
+ `/platform/v1/files/loading-evidence/${tenantId}/${inboundId}`,
+ body,
+ { headers: { 'Content-Type': 'multipart/form-data' } },
+ )
+ return unwrapApiResponse(response)
+}
+
+export async function voidEvidence(
+ tenantId: string | number,
+ inboundId: string | number,
+ evidenceId: string | number,
+ request: VoidEvidenceRequest,
+) {
+ const response = await http.post, ApiEnvelope>(
+ `${inboundBasePath}/${inboundId}/loading/evidence/${evidenceId}/void`,
+ request,
+ writeOptions('evidence-void', inboundId, tenantId),
+ )
+ return unwrapApiResponse(response)
+}
+
export async function completeLoading(
tenantId: string | number,
inboundId: string | number,
@@ -215,15 +287,3 @@ export async function completeLoading(
)
return unwrapApiResponse(response)
}
-
-export async function completeDelivery(
- tenantId: string | number,
- inboundId: string | number,
- request: CompleteDeliveryRequest,
-) {
- const response = await http.post, ApiEnvelope>(
- `${inboundBasePath}/${inboundId}/delivery/complete`, request,
- writeOptions('delivery-complete', inboundId, tenantId),
- )
- return unwrapApiResponse(response)
-}
diff --git a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue
index 0a9d7b5a..d7454a88 100644
--- a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue
+++ b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue
@@ -1,43 +1,43 @@