资讯动态

Documenso 部分签署 PDF 下载:为 PENDING 封套实时生成“已烧录字段“的预览 PDF

发布时间:2026/9/14 14:45:37 来源:尧图企业网站定制
Documenso 部分签署 PDF 下载为 PENDING 封套实时生成已烧录字段的预览 PDF【免费下载链接】documensoThe Open Source DocuSign Alternative.项目地址: https://gitcode.com/GitHub_Trending/do/documenso本文基于 DocumensoThe Open Source DocuSign Alternative仓库中的功能设计文档 Partial Signed PDF Download深入讲解该功能的完整设计如何让团队/所有者在封套envelope仍处于PENDING状态时就能下载到一份把所有当前已插入字段烧录burn in进原始 PDF 的部分签署版文档。文章覆盖响应行为矩阵、4xx 错误码设计、按需生成流程、内容寻址 ETag、UI 下载对话框的门控逻辑并逐一对应仓库中的实际源码实现与 E2E 测试用例帮助读者完整掌握草稿态合同预览这一场景的端到端技术方案。问题背景PENDING 封套此前只能拿到无字段的原始 PDF在 Documenso 的签署流程中封套经历DRAFT → PENDING → COMPLETED/REJECTED的状态流转。封套分发后进入PENDING各签署人陆续在应用内填写、签署自己的字段只有当最后一名签署人完成、后台seal-document任务执行后才会产出最终的封存 PDF含全部签名、PKI 签名、证书页与审计日志附录。在此之前的设计里一个PENDING封套可供下载的字节只有一种initialData对应的原始 PDF不含任何字段。对于需要跟踪签署进度、或想在签署过程中向客户展示目前签到哪里了的团队侧用户来说这是一个明显的空白。本功能的目标就是补上这个空白让团队成员在封套仍为PENDING时取回一份所有当前已插入字段均已烧录进 PDF 的文件。功能暴露在两个入口v2 APIGET /api/v2/envelope/item/{envelopeItemId}/download?versionpendingAPI Token 鉴权UI在既有EnvelopeDownloadDialog中与Original按钮并列新增一个Partial按钮当封套为PENDING时取代原有的Signed槽位。底层复用会话鉴权的文件路由GET /api/files/envelope/{envelopeId}/envelopeItem/{id}/download/pending。功能范围与关键设计决策设计文档对范围Scope的界定非常明确每一条都对应一个可溯源的取舍约束说明源码印证仅 v2 API不提供 v1 对等能力download.ts 只挂载在 v2 路由仅internalVersion 2封套遗留 v1 封套返回400 ENVELOPE_LEGACYfiles.helpers.ts 中internalVersion ! 2分支仅限团队侧/所有者收件人 token 下载路径明确不支持pending——收件人已有应用内叠加层查看器用于核验而一份可下载的半签署 PDF 是已部分履行合同的泄漏向量服务端 schema 拒绝 UI 对话框隐藏按钮双重强制无 PKI 签名、无证书页、无审计日志附录响应明确不是最终签署文档generate-partial-signed-pdf.ts 的注释与实现无水印或横幅文字文件名后缀_pending.pdf、Cache-Control: no-store, private响应头、以及没有 PKI 签名本身足以表达草稿状态见下文响应头设计API 行为矩阵与 4xx 错误设计/api/v2/envelope/item/{id}/download?versionpending与会话侧/api/files/envelope/{envelopeId}/envelopeItem/{id}/download/pending共享同一套行为矩阵封套状态响应PENDINGv2 封套200返回烧录了当前已插入字段的 PDFPENDINGv1 遗留封套400ENVELOPE_LEGACYDRAFT400ENVELOPE_DRAFTCOMPLETED400ENVELOPE_COMPLETEDREJECTED400ENVELOPE_REJECTED值得特别强调的是**v1 的 PENDING 返回 400 而非 501**这一决策5xx 保留给真实的服务端故障而这个封套无法满足当前请求形态属于调用方可自行处理的客户端侧条件。这个语义在 app-error.ts 中落地——新增的ENVELOPE_DRAFT、ENVELOPE_COMPLETED、ENVELOPE_REJECTED、ENVELOPE_LEGACY四个错误码全部映射到 400见toRestAPIError与genericErrorCodeToTrpcErrorCodeMap使调用方在日志与指标里能干净地把这类业务拒绝和真实的 5xx 服务器故障区分开。错误响应体的形状也经过兼容设计保留既有的{ error: message }字段以向后兼容并新增code: APP_ERROR_CODE字段供需要按错误码分支的调用方使用。文档下载路由/document/{documentId}/download本身保持不动。其他响应约定文件名{title}_pending.pdfETag对sha256(envelope.status 已插入字段的 (field.id, field.customText, field.signature?.id, field.signature?.created))做内容寻址If-None-Match命中时返回 304无持久缓存ETag 未命中时按请求即时生成。服务端实现单一入口 判别联合类型路由与校验器拆分顺带修复既有 bugv2 下载路由定义在 download.ts由 router.ts 挂载到/api/v2与/api/v2-beta前缀下app.route(/api/v2, downloadRoute)并统一经过 CORS 与 API 限流中间件。校验器定义在 download.types.ts这里有两个关键点version枚举扩展为original | signed | pending默认signedZoddescribe中直接写明了pending的语义仅 PENDING 状态有效、不是最终签署文档校验器拆分为paramenvelopeItemIdqueryversion两部分。这是顺带修复的一个既有 bug原本version被错误地接在路径参数校验器上而version实际以查询字符串到达导致?versionoriginal这类请求会静默返回 signed PDF。拆分后路由同时挂载sValidator(param, ...)与sValidator(query, ...)两个参数各归其位。version pending的分支在取数时会额外查询 envelope 的 recipientsrole signingStatus并把完整 envelope 传给处理助手其余版本只传statusif (version pending) { return await handleEnvelopeItemFileRequest({ ...baseOptions, version, envelopeItemId: envelopeItem.id, envelope: envelopeItem.envelope, }); } return await handleEnvelopeItemFileRequest({ ...baseOptions, version, status: envelopeItem.envelope.status, });统一分发助手handleEnvelopeItemFileRequestfiles.helpers.ts 中的handleEnvelopeItemFileRequest是所有封套条目文件请求预览/下载的单一入口其选项类型是一个判别联合discriminated union从类型层面约束不同版本需要不同入参type HandleEnvelopeItemFileRequestOptions { title: string; documentData: DocumentDataInput; isDownload: boolean; context: ContextHonoEnv; } ( | { version: signed | original; status: DocumentStatus } | { version: pending; envelopeItemId: string; envelope: EnvelopeForPendingDownload } );signed/original走handleStaticFileRequest直接取存储字节signed用documentData.dataoriginal用documentData.initialDataETag 为字节内容哈希pending走handlePendingFileRequest即时生成。handlePendingFileRequest的执行顺序是状态校验 → 版本校验 → 查询已插入字段 → 计算 ETag/304 → 取原始 PDF → 生成部分签署 PDF → 设置响应头if (envelope.status ! DocumentStatus.PENDING) { const errorCode match(envelope.status) .with(DocumentStatus.DRAFT, () AppErrorCode.ENVELOPE_DRAFT) .with(DocumentStatus.COMPLETED, () AppErrorCode.ENVELOPE_COMPLETED) .with(DocumentStatus.REJECTED, () AppErrorCode.ENVELOPE_REJECTED) .otherwise(() AppErrorCode.INVALID_REQUEST); throw new AppError(errorCode, { message: Envelope ${envelope.id} must be pending to download a partially signed PDF, statusCode: 400, }); } if (envelope.internalVersion ! 2) { throw new AppError(AppErrorCode.ENVELOPE_LEGACY, { ... statusCode: 400 }); }字段查询只取inserted: true的字段即已被签署人实际填写/签完的按id asc排序并关联signature签名 id 与创建时间进入 ETag 指纹。ETag 与响应头pending 的 ETag 是内容寻址的——对{ envelopeStatus, fields: [{ id, customText, signatureId, signatureCreated }] }的 JSON 做sha256const etag Buffer.from( sha256(JSON.stringify({ envelopeStatus: envelope.status, fields: fields.map((field) ({ id: field.id, customText: field.customText, signatureId: field.signature?.id ?? null, signatureCreated: field.signature?.created ?? null, })), })), ).toString(hex); if (c.req.header(If-None-Match) etag) { return c.body(null, 304); }字段排序由数据库查询的orderBy: { id: asc }保证确定性因此指纹在没有任何新字段被插入的前提下跨请求稳定。响应统一带Cache-Control: no-store, private文件名通过contentDisposition(${baseTitle}_pending.pdf)下发。整个 pending 流程不落任何持久缓存ETag 命中即 304未命中则当场生成。按需生成generatePartialSignedPdf真正的 PDF 合成在 generate-partial-signed-pdf.ts新增文件它是一个小巧的编排器复用既有的 V2 字段叠加管线PDF.load(pdfData)加载原始PDFflattenAll()后upgradeVersion(1.7)用groupBy(fields, (field) field.page)按页分组已插入字段对每一页调用既有的insertFieldInPDFV2与正式签署封套用的是同一个叠加助手生成该页的透明覆盖层 PDFpdfDoc.embedPage(overlayPdf, 0)将覆盖层嵌入原页并根据页面rotation90/180/270计算平移量后drawPage绘制保证旋转页面的字段位置正确再次flattenAll()save({ useXRefStream: true })输出字节。文件头注释明确定位了产物性质No PKI signature, no certificate page, no audit log appendix - this is a preview of the in-progress envelope, not a final executed document.无 PKI 签名、无证书页、无审计日志附录——这是进行中封套的预览不是最终签署文档。页码缺失会直接抛错Page ${n} does not exist由路由层统一转为 500 兜底。团队侧文件路由与收件人 token 路由的分野会话侧下载路由在 files.ts 的GET /envelope/:envelopeId/envelopeItem/:envelopeItemId/download/:version?会话鉴权 →checkEnvelopeFileAccess校验团队或同组织访问权 →pending分支把查到的 envelope含 recipients整体交给同一个handleEnvelopeItemFileRequest助手AppError 统一包装成{ error, code }返回。而收件人 token 不接受 pending是通过schema 层实现的files.types.ts 中团队侧下载参数 schema 为z.enum([signed, original, pending])收件人 token 下载参数 schema 保持z.enum([signed, original])不变——/api/files/token/.../download/pending会在 schema 校验阶段被直接拒绝不进入业务逻辑。配合客户端对话框在存在 token 时不渲染 Partial 按钮泄漏向量在服务端和 UI 两端都被封死。UI 实现Partial 按钮的三重门控envelope-download-dialog.tsx 是 UI 侧的核心改动。对话框展示Original加以下之一状态为COMPLETED→Signed既有行为状态为PENDING、无收件人 token、且非遗留封套!isLegacy→Partial其他情况DRAFT、REJECTED、携带收件人 token 的 PENDING、遗留封套的 PENDING→ 不显示。const secondaryDownload useMemo{ version: signed | pending; label: string } | null(() { if (envelopeStatus DocumentStatus.COMPLETED) { return { version: signed, label: t({ message: Signed, context: Signed document (adjective) }) }; } if (envelopeStatus DocumentStatus.PENDING !token !isLegacy) { return { version: pending, label: t({ message: Partial, context: Partially signed document (adjective) }) }; } return null; }, [envelopeStatus, isLegacy, token, t]);为了门控 Partial 按钮对话框新增了可选propisLegacy?: boolean组件 JSDoc 中写清了它的语义遗留封套走另一条字段渲染管线部分签署 PDF 助手未实现所以对其隐藏按钮。这个 prop 只影响 Partial 分支因此状态永远不可能为 PENDING 的调用方硬编码 DRAFT/COMPLETED/REJECTED 的、或恒设 token 的以及恒定带收件人 token 的调用方都可以不传。全仓库共 11 处调用其中三处传值isLegacy{envelope.internalVersion 1}或row.internalVersion 1documents-table-action-dropdown.tsx文档表格行操作envelope-editor.tsx封套编辑器;document-page-view-dropdown.tsx文档页面视图下拉。其余八处调用点保持不动。设计文档如实记录了这一权衡的代价未来某处团队侧用法中状态可能为 PENDING、但开发者忘了传isLegacy时Partial 按钮会静默不出现——但状态门控保证了不会出现点了必坏的情况按钮缺失在测试中可被发现而必填 prop的替代方案被否决因为 11 处调用中有 8 处会携带一个毫无意义的值。客户端URL 构造与文件名后缀envelope-download.ts 中的EnvelopeItemPdfUrlOptions允许version: original | signed | pending。URL 构造逻辑区分两侧无 token 时生成会话侧地址/api/files/envelope/{envelopeId}/envelopeItem/{id}/download/{version}有 token 时生成/api/files/token/{token}/envelopeItem/{id}/download/{version}。注释点明了防御层次token 侧构造出的pendingURL 会被服务端 schema 拒绝而是否允许 pending的最终裁决在调用点对话框的无 token 门控完成。download-pdf.ts 中DocumentVersion同步扩展为三值文件名后缀逻辑收敛为一个小 switchconst versionToFilenameSuffix (version: DocumentVersion): string { switch (version) { case signed: return _signed.pdf; case pending: return _pending.pdf; case original: return .pdf; } };E2E 测试验证该功能由 Playwright E2E 规格 partial-signed-pdf-download.spec.ts 完整覆盖测试用例与设计文档Verification一一对应主流程returns a PDF with inserted fields, supports ETag, and rejects after completion种子化一个含两名签署人的 PENDING 封套每人一个签名域签署人 1 通过envelope.field.signrecipient.completeDocumentWithToken完成签署轮询确认封套仍为PENDING、签署人 1 为SIGNEDAPI Token 调用?versionpending断言 200、Content-Type含application/pdf、Cache-Control恰为no-store, private、Content-Disposition含_pending.pdf、存在 ETag用libpdf/core加载返回字节断言页数与原始 PDF 相同证明没有证书页/审计页附加携带If-None-Match: etag再次请求断言 304签署人 2 完成签署、封套翻转为COMPLETED后同一?versionpending请求断言 400 且code ENVELOPE_COMPLETED随后?versionsigned请求成功返回 200。拒绝分支rejects draft and legacy pending envelopesDRAFT 封套返回 400ENVELOPE_DRAFT将 PENDING 封套的internalVersion改写为 1 后返回 400ENVELOPE_LEGACY。此外功能合入的静态验证标准为npx tsc --noEmit -p apps/remix/tsconfig.json与npm run lint手动验证路径为在 PENDINGv2封套的文档表格或封套编辑器中打开下载对话框确认Partial与Original并列且产出带当前字段的_pending.pdfCOMPLETED 封套显示Signedv1 PENDING 封套两者都不显示状态门控本会显示 Partial被isLegacy过滤掉。局限与后续方向设计文档的 Out of Scope / Follow-ups 部分同样值得纳入评估它界定了该功能当前的明确边界收件人 token 下载路径API 与 UI——已明确决定不做除非出现具体需求并给出限制泄漏向量的方案v1 对等支持——不做。为 v1 实现部分签署需要把legacy_insertFieldInPDF/insertFieldInPDFV1移植进一个partial 专用流程而在 v1 逐步退役的背景下这段代码没有长期归宿对应实现即仓库中的 legacy-insert-field-in-pdf.ts 与 insert-field-in-pdf-v1.ts文档下载路由/document/{documentId}/download保持不变其错误形状与校验器接线与之前一致如确有调用方需要按code分支可在后续统一为同样的{ error, code }形状持久缓存层 / 任务队列生成——仅当大 PDF 的 p95 延迟成为问题再重新评估ENVELOPE_LEGACY的专属 toast——目前由兜底的 Something went wrong 提示处理在 UI 已有isLegacy门控的前提下该错误从对话框本身不可达API 直连调用方仍可能收到它。小结Documenso 的部分签署 PDF 下载功能用一个按需生成 内容寻址 ETag 严格状态门控的组合在不动既有封存管线的前提下补上了 PENDING 窗口的下载空白。其工程上值得借鉴的点包括用判别联合类型把不同版本请求需要不同入参约束在编译期用 schema 枚举差异在入口拒绝不合法的收件人侧请求用 4xx 结构化code把业务拒绝与服务器故障在调用方侧清晰区分以及用可选 prop 调用点门控在 11 处调用中只改动 3 处。完整的实现证据链分布在 download.types.ts、download.ts、files.ts、files.helpers.ts、generate-partial-signed-pdf.ts、app-error.ts、envelope-download-dialog.tsx 与 partial-signed-pdf-download.spec.ts 中可逐行对照本文内容深入阅读。【免费下载链接】documensoThe Open Source DocuSign Alternative.项目地址: https://gitcode.com/GitHub_Trending/do/documenso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价