feat(api): 接入医签通业务接口与类型

This commit is contained in:
yelan
2026-09-01 10:11:49 +08:00
parent c34ef6f79a
commit 2e2d2250dd
12 changed files with 1677 additions and 56 deletions
@@ -0,0 +1,152 @@
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
import { request } from '@/utils/request'
import type {
ApiResponseOf,
PadBindingRequest,
PadSessionUploadInput,
SendSmsDeliveryRequest,
SignDeliveryResponseDto,
SignatureUploadResponse,
SigningTokenUploadInput,
TokenConsumeRequest,
TokenConsumeResponse,
} from './types'
function createIdempotencyKey() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return `clinical-web-${crypto.randomUUID()}`
}
return `clinical-web-${Date.now()}-${Math.random().toString(36).slice(2)}`
}
function idempotencyHeaders(value?: string) {
return { 'Idempotency-Key': value || createIdempotencyKey() }
}
export function sendSigningSms(
taskId: string,
payload: SendSmsDeliveryRequest,
idempotencyKey?: string,
): Promise<SignDeliveryResponseDto | null> {
return request
.post<ApiResponseOf<SignDeliveryResponseDto | null>>(
`/v1/sign-deliveries/${encodeURIComponent(taskId)}/sms/send`,
payload,
{ headers: idempotencyHeaders(idempotencyKey) },
)
.then(unwrapNullableApiResponse)
}
export function resendSigningSms(
taskId: string,
expectedRowVersion?: number,
idempotencyKey?: string,
): Promise<SignDeliveryResponseDto | null> {
const data = expectedRowVersion === undefined ? undefined : { expectedRowVersion }
return request
.post<ApiResponseOf<SignDeliveryResponseDto | null>>(
`/v1/sign-deliveries/${encodeURIComponent(taskId)}/sms/resend`,
data,
{ headers: idempotencyHeaders(idempotencyKey) },
)
.then(unwrapNullableApiResponse)
}
export function createPadSigningSession(
taskId: string,
payload: PadBindingRequest = {},
idempotencyKey?: string,
): Promise<SignDeliveryResponseDto> {
return request
.post<ApiResponseOf<SignDeliveryResponseDto>>(
`/v1/sign-deliveries/${encodeURIComponent(taskId)}/pad/sessions`,
payload,
{ headers: idempotencyHeaders(idempotencyKey) },
)
.then(unwrapApiResponse)
}
export function consumeSigningToken(payload: TokenConsumeRequest): Promise<TokenConsumeResponse> {
return request
.post<ApiResponseOf<TokenConsumeResponse>>('/v1/sign-deliveries/token/consume', payload)
.then(unwrapApiResponse)
}
function createSignatureFormData(file: File | Blob, metadata: string) {
const formData = new FormData()
const fileName =
typeof File !== 'undefined' && file instanceof File && file.name ? file.name : 'signature.png'
formData.append('file', file, fileName)
formData.append('metadata', metadata)
return formData
}
export function uploadSigningSignature(
payload: SigningTokenUploadInput,
): Promise<SignatureUploadResponse> {
const formData = createSignatureFormData(payload.file, payload.metadata)
return request
.post<ApiResponseOf<SignatureUploadResponse>>(
`/v1/sign-deliveries/${encodeURIComponent(payload.taskId)}/signature`,
formData,
{
params: {
deliveryId: payload.deliveryId,
uploadToken: payload.uploadToken,
},
headers: idempotencyHeaders(payload.idempotencyKey),
},
)
.then(unwrapApiResponse)
}
/**
* PAD 专用兼容路径。服务端要求同时提交 PAD 元数据和真实 PNG,旧的 JSON-only 提交会返回 409。
* 具体的 challenge、文件摘要和 storageKey 由设备适配层生成,页面不应伪造这些值。
*/
export function uploadPadSigningSignature(
payload: PadSessionUploadInput,
): Promise<SignatureUploadResponse> {
if (!payload.pad) {
return Promise.reject(new Error('PAD 上传缺少会话元数据'))
}
const formData = createSignatureFormData(payload.file, payload.metadata)
formData.append('challenge', payload.pad.challenge)
formData.append('contentType', payload.pad.contentType)
formData.append('sizeBytes', String(payload.pad.sizeBytes ?? payload.file.size))
formData.append('sha256', payload.pad.sha256)
formData.append('storageKey', payload.pad.storageKey)
return request
.post<ApiResponseOf<SignatureUploadResponse>>(
`/v1/sign-deliveries/${encodeURIComponent(payload.taskId)}/pad/sessions/${encodeURIComponent(payload.deliveryId)}/upload`,
formData,
{
params: { uploadToken: payload.uploadToken },
headers: idempotencyHeaders(payload.idempotencyKey),
},
)
.then(unwrapApiResponse)
}
export function uploadSigningSignatureByProof(
payload: SigningTokenUploadInput,
): Promise<SignatureUploadResponse> {
const formData = createSignatureFormData(payload.file, payload.metadata)
return request
.post<ApiResponseOf<SignatureUploadResponse>>('/v1/sign-deliveries/token/signature', formData, {
params: {
taskId: payload.taskId,
deliveryId: payload.deliveryId,
uploadToken: payload.uploadToken,
},
headers: idempotencyHeaders(payload.idempotencyKey),
})
.then(unwrapApiResponse)
}