feat(sign): 补齐医护端与患者端签署流程
This commit is contained in:
+20
-27
@@ -5,16 +5,20 @@ import type {
|
||||
ApiResponse,
|
||||
SignatureUploadInput,
|
||||
SignatureUploadResponse,
|
||||
SigningProof,
|
||||
SigningContextResponse,
|
||||
TokenConsumeRequest,
|
||||
TokenConsumeResponse,
|
||||
} from './types'
|
||||
|
||||
export function consumeSigningToken(payload: TokenConsumeRequest): Promise<TokenConsumeResponse> {
|
||||
export function consumeSigningToken(
|
||||
payload: TokenConsumeRequest,
|
||||
idempotencyKey: string,
|
||||
): Promise<TokenConsumeResponse> {
|
||||
return request
|
||||
.post<ApiResponse<TokenConsumeResponse>>('/v1/sign-deliveries/token/consume', payload, {
|
||||
headers: {
|
||||
'Idempotency-Key': createIdempotencyKey(),
|
||||
'Idempotency-Key': idempotencyKey,
|
||||
},
|
||||
})
|
||||
.then(unwrapApiResponse)
|
||||
@@ -23,48 +27,37 @@ export function consumeSigningToken(payload: TokenConsumeRequest): Promise<Token
|
||||
/**
|
||||
* 读取本次签署的完整上下文:患者信息 / 签署人 / 文书正文 / 医生已填结果。
|
||||
*
|
||||
* 契约见 `.workbuddy-ai/患者端H5真实接入方案-2026-09-28.md` 第三节。
|
||||
* **后端就绪前这个调用会失败**,所以调用方必须能优雅降级 ——
|
||||
* 拿不到正文就不能放行签署(见 `useSignFlow` 的 `documentError`)。
|
||||
* Context 使用已消费凭证,不再把原始一次性 Token 放进请求 URL、日志或后续请求。
|
||||
*/
|
||||
export function fetchSigningContext(token: string): Promise<SigningContextResponse> {
|
||||
export function fetchSigningContext(proof: SigningProof): Promise<SigningContextResponse> {
|
||||
return request
|
||||
.get<ApiResponse<SigningContextResponse>>('/v1/sign-deliveries/token/context', {
|
||||
params: { token },
|
||||
})
|
||||
.post<ApiResponse<SigningContextResponse>>('/v1/sign-deliveries/token/context', proof)
|
||||
.then(unwrapApiResponse)
|
||||
}
|
||||
|
||||
function createIdempotencyKey(value?: string) {
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return `patient-h5-${crypto.randomUUID()}`
|
||||
}
|
||||
|
||||
return `patient-h5-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
}
|
||||
|
||||
export function uploadSigningSignature(
|
||||
payload: SignatureUploadInput,
|
||||
): Promise<SignatureUploadResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', payload.file, 'signature.png')
|
||||
formData.append('metadata', payload.metadata)
|
||||
formData.append('deliveryId', payload.deliveryId)
|
||||
formData.append('uploadToken', payload.uploadToken)
|
||||
formData.append(
|
||||
'metadata',
|
||||
new Blob([JSON.stringify(payload.metadata)], { type: 'application/json' }),
|
||||
)
|
||||
formData.append(
|
||||
'signer',
|
||||
new Blob([JSON.stringify(payload.signer)], { type: 'application/json' }),
|
||||
)
|
||||
|
||||
return request
|
||||
.post<ApiResponse<SignatureUploadResponse>>(
|
||||
`/v1/sign-deliveries/${encodeURIComponent(payload.taskId)}/signature`,
|
||||
formData,
|
||||
{
|
||||
params: {
|
||||
deliveryId: payload.deliveryId,
|
||||
uploadToken: payload.uploadToken,
|
||||
},
|
||||
headers: {
|
||||
'Idempotency-Key': createIdempotencyKey(payload.idempotencyKey),
|
||||
'Idempotency-Key': payload.idempotencyKey,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -24,6 +24,38 @@ export interface TokenConsumeRequest {
|
||||
taskId?: string
|
||||
}
|
||||
|
||||
export interface SignatureFieldPosition {
|
||||
id: string
|
||||
page: number
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
coordinateSystem: 'TOP_LEFT' | 'PDF_BOTTOM_LEFT'
|
||||
}
|
||||
|
||||
/** Facts read from the actual PNG plus an optional assertion against a configured PDF field. */
|
||||
export interface SignaturePngMetadata {
|
||||
imageWidth: number
|
||||
imageHeight: number
|
||||
sizeBytes: number
|
||||
sha256: string
|
||||
contentType: 'image/png'
|
||||
fieldId: string
|
||||
page: number
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
coordinateSystem: SignatureFieldPosition['coordinateSystem']
|
||||
}
|
||||
|
||||
export interface SigningProof {
|
||||
taskId: string
|
||||
deliveryId: string
|
||||
uploadToken: string
|
||||
}
|
||||
|
||||
export interface TokenConsumeResponse {
|
||||
deliveryId: string
|
||||
taskId: string
|
||||
@@ -35,12 +67,7 @@ export interface TokenConsumeResponse {
|
||||
|
||||
/** 患者端看到的任务状态。不是 `WAITING_SIGN` 时不该放行签署 */
|
||||
export type SigningTaskSignStatus =
|
||||
| 'WAITING_SIGN'
|
||||
| 'SIGNED'
|
||||
| 'EXPIRED'
|
||||
| 'VOIDED'
|
||||
| 'FAILED'
|
||||
| string
|
||||
'WAITING_SIGN' | 'WAITING_CONFIRM' | 'SIGNED' | 'EXPIRED' | 'VOIDED' | 'FAILED' | string
|
||||
|
||||
export interface SigningContextPatient {
|
||||
name?: string | null
|
||||
@@ -107,10 +134,24 @@ export interface SigningContextResponse {
|
||||
staffAnswers?: Record<string, string> | null
|
||||
/** 患者端可交互的字段。按约定只有主签名位(`id === 'signature'`) */
|
||||
fields?: SigningContextField[] | null
|
||||
/** Configured PDF signature locations, separate from body form-field definitions. */
|
||||
signatureFields?: SignatureFieldPosition[] | null
|
||||
/** Recoverable summary of the real server pipeline and artifacts after a successful upload. */
|
||||
uploadResult?: SignatureUploadResponse | null
|
||||
/** Server-owned patient signature completion timestamp (separate from department confirmation). */
|
||||
signedAt?: string | null
|
||||
}
|
||||
|
||||
export type SignatureUploadStatus =
|
||||
'CREATED' | 'WAITING_SIGN' | 'SIGNED' | 'EXPIRED' | 'VOIDED' | 'GENERATING' | 'FAILED' | string
|
||||
| 'CREATED'
|
||||
| 'WAITING_SIGN'
|
||||
| 'SIGNED'
|
||||
| 'EXPIRED'
|
||||
| 'VOIDED'
|
||||
| 'GENERATING'
|
||||
| 'WAITING_CONFIRM'
|
||||
| 'FAILED'
|
||||
| string
|
||||
|
||||
export interface SignatureUploadResponse {
|
||||
taskId: string
|
||||
@@ -132,6 +173,13 @@ export interface SignatureUploadInput {
|
||||
deliveryId: string
|
||||
uploadToken: string
|
||||
file: Blob
|
||||
metadata: string
|
||||
idempotencyKey?: string
|
||||
metadata: SignaturePngMetadata
|
||||
signer: {
|
||||
type: string
|
||||
name: string
|
||||
relation: string
|
||||
consentAccepted: true
|
||||
observedSignedAt: string
|
||||
}
|
||||
idempotencyKey: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user