This commit is contained in:
2026-08-04 21:01:39 +08:00
parent 88bd939170
commit 5cb1ca307d
77 changed files with 9914 additions and 131 deletions
@@ -0,0 +1,100 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import {
confirmWeightResult,
correctWeightResult,
createWeightResult,
downloadWeightImage,
getWeightWorkspace,
retryWeightRecognition,
uploadWeightImage,
} from './weightOcr'
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn(), post: vi.fn(), put: vi.fn() },
ApiResponseError: class ApiResponseError extends Error {},
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
describe('inbound weight OCR API', () => {
beforeEach(() => {
vi.mocked(http.get).mockReset()
vi.mocked(http.post).mockReset()
vi.mocked(http.put).mockReset()
})
it('loads the measurement workspace using the real firstleg path', async () => {
const detail = { inboundId: '9007199254740993', weightResults: [] }
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: detail })
await expect(getWeightWorkspace('10001', '9007199254740993')).resolves.toEqual(detail)
expect(http.get).toHaveBeenCalledWith(
'/platform/v1/first-leg/inbounds/9007199254740993/measurement',
{ params: { tenantId: '10001' } },
)
})
it('uploads and retries against the file service without converting long IDs', async () => {
const result = { fileId: '9007199254740995', recognitionStatus: 'SUCCESS' }
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: result })
const file = new File(['image'], 'weight.png', { type: 'image/png' })
await expect(uploadWeightImage('10001', '9007199254740993', file)).resolves.toEqual(result)
const [uploadPath, body] = vi.mocked(http.post).mock.calls[0]!
expect(uploadPath).toBe('/platform/v1/files/measurements/10001/9007199254740993')
expect(body).toBeInstanceOf(FormData)
expect((body as FormData).get('file')).toBe(file)
await retryWeightRecognition('10001', '9007199254740993', '9007199254740995')
expect(http.post).toHaveBeenLastCalledWith(
'/platform/v1/files/measurements/10001/9007199254740993/9007199254740995/recognize',
)
})
it('registers, corrects, and confirms a weight result using the implemented contracts', async () => {
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1' } })
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', version: 1 } })
const createPayload = {
fileId: '9007199254740995',
ocrProvider: 'FILE_WEIGHT_OCR',
ocrRawText: null,
ocrWeightValue: 12.5,
ocrWeightUnit: 'KG',
confidence: 0.98,
}
await createWeightResult('10001', '20001', createPayload)
expect(http.post).toHaveBeenNthCalledWith(
1,
'/platform/v1/first-leg/inbounds/20001/weight-results',
createPayload,
{ params: { tenantId: '10001' } },
)
const correction = { version: 0, adjustedWeightValue: 12.8, adjustedWeightUnit: 'KG' as const, adjustmentReason: '读数模糊' }
await correctWeightResult('10001', '20001', '30001', correction)
expect(http.put).toHaveBeenCalledWith(
'/platform/v1/first-leg/inbounds/20001/weight-results/30001',
correction,
{ params: { tenantId: '10001' } },
)
await confirmWeightResult('10001', '20001', '30001', { version: 1 })
expect(http.post).toHaveBeenLastCalledWith(
'/platform/v1/first-leg/inbounds/20001/weight-results/30001/confirm',
{ version: 1 },
{ params: { tenantId: '10001' } },
)
})
it('downloads an authenticated image as a blob', async () => {
const image = new Blob(['png'], { type: 'image/png' })
vi.mocked(http.get).mockResolvedValue(image)
await expect(downloadWeightImage('10001', '20001', '40001')).resolves.toBe(image)
expect(http.get).toHaveBeenCalledWith(
'/platform/v1/files/measurements/10001/20001/40001',
{ responseType: 'blob' },
)
})
})
@@ -0,0 +1,123 @@
import axios from 'axios'
import { ApiResponseError, http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type {
ConfirmWeightResultRequest,
CorrectWeightResultRequest,
CreateWeightResultRequest,
WeightImageUploadResult,
WeightResultResource,
WeightWorkspaceResource,
} from '../types'
const measurementBasePath = '/platform/v1/first-leg/inbounds'
const imageBasePath = '/platform/v1/files/measurements'
const maxErrorEnvelopeBytes = 1024 * 1024
function segment(value: string) {
return encodeURIComponent(value)
}
async function parseBlobError(blob: Blob, status?: number) {
if (!blob.type.toLowerCase().includes('json')) return null
if (blob.size > maxErrorEnvelopeBytes) {
return new ApiResponseError('图片接口返回了超出限制的 JSON 响应', 'INVALID_IMAGE_RESPONSE', status)
}
try {
const payload = JSON.parse(await blob.text()) as Partial<ApiEnvelope<unknown>>
return new ApiResponseError(
payload.code === '200' ? '图片接口未返回图片内容' : payload.message || '称重图片读取失败',
payload.code === '200' ? 'INVALID_IMAGE_RESPONSE' : payload.code || (status ? `HTTP_${status}` : 'IMAGE_FAILED'),
status,
payload.data,
payload.requestId,
payload.traceId,
)
} catch {
return new ApiResponseError('图片接口返回了无法识别的响应', 'INVALID_IMAGE_RESPONSE', status)
}
}
export async function getWeightWorkspace(tenantId: string, inboundId: string) {
const response = await http.get<ApiEnvelope<WeightWorkspaceResource>, ApiEnvelope<WeightWorkspaceResource>>(
`${measurementBasePath}/${segment(inboundId)}/measurement`,
{ params: { tenantId } },
)
return unwrapApiResponse(response)
}
export async function uploadWeightImage(tenantId: string, inboundId: string, file: File) {
const body = new FormData()
body.append('file', file)
const response = await http.post<ApiEnvelope<WeightImageUploadResult>, ApiEnvelope<WeightImageUploadResult>>(
`${imageBasePath}/${segment(tenantId)}/${segment(inboundId)}`,
body,
)
return unwrapApiResponse(response)
}
export async function retryWeightRecognition(tenantId: string, inboundId: string, fileId: string) {
const response = await http.post<ApiEnvelope<WeightImageUploadResult>, ApiEnvelope<WeightImageUploadResult>>(
`${imageBasePath}/${segment(tenantId)}/${segment(inboundId)}/${segment(fileId)}/recognize`,
)
return unwrapApiResponse(response)
}
export async function downloadWeightImage(tenantId: string, inboundId: string, fileId: string) {
try {
const blob = await http.get<Blob, Blob>(
`${imageBasePath}/${segment(tenantId)}/${segment(inboundId)}/${segment(fileId)}`,
{ responseType: 'blob' },
)
const responseError = await parseBlobError(blob, 200)
if (responseError) throw responseError
return blob
} catch (error) {
if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
const responseError = await parseBlobError(error.response.data, error.response.status)
if (responseError) throw responseError
}
throw error
}
}
export async function createWeightResult(
tenantId: string,
inboundId: string,
payload: CreateWeightResultRequest,
) {
const response = await http.post<ApiEnvelope<WeightResultResource>, ApiEnvelope<WeightResultResource>>(
`${measurementBasePath}/${segment(inboundId)}/weight-results`,
payload,
{ params: { tenantId } },
)
return unwrapApiResponse(response)
}
export async function correctWeightResult(
tenantId: string,
inboundId: string,
resultId: string,
payload: CorrectWeightResultRequest,
) {
const response = await http.put<ApiEnvelope<WeightResultResource>, ApiEnvelope<WeightResultResource>>(
`${measurementBasePath}/${segment(inboundId)}/weight-results/${segment(resultId)}`,
payload,
{ params: { tenantId } },
)
return unwrapApiResponse(response)
}
export async function confirmWeightResult(
tenantId: string,
inboundId: string,
resultId: string,
payload: ConfirmWeightResultRequest,
) {
const response = await http.post<ApiEnvelope<WeightResultResource>, ApiEnvelope<WeightResultResource>>(
`${measurementBasePath}/${segment(inboundId)}/weight-results/${segment(resultId)}/confirm`,
payload,
{ params: { tenantId } },
)
return unwrapApiResponse(response)
}