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