Skip to content
bitzorcas
中EN

Guide

iManage 生命周期与流式处理

讲解 iManage 上传下载、签出签入、历史、租户客户端、流内存、并发幂等、错误与审计边界。

Last updated

IIManageArchivePort 把厂商 SDK 隔离为五个文档操作。它是翻译层,不是文档管理用例:当前没有 Application 服务为 TenantId、权限、扫描、幂等、审计和对账背书。

1. 文档生命周期

UploadCheckoutCheckin new versionCheckin Stream.NullDownload copyQuery history

Uploaded

CheckedOut

Downloaded

History

图中只表达 Port 操作,不代表源码维护本地状态机。真实状态由 iManage 决定,当前平台不持久化 shadow state。

2. 租户客户端

每个操作调用 IManageClientFactory.Create(tenantId) 并释放客户端。这为多实例路由提供基础,但 TenantId 仍是普通请求字段。

可信租户封装
// ① TenantId 只从认证上下文取,不接受 command.TenantId。
string tenantId = currentUser.User.TenantId;
// ② owner 验证目标 folder 与当前业务记录的关系。
await policy.AuthorizeUploadAsync(tenantId, command.CaseId, command.FolderId, ct);
// ③ 已扫描文件才进入连接器。
await using Stream stream = await files.OpenCleanReadAsync(command.FileAssetId, ct);
Result<IManageDocumentDto> result = await archive.UploadDocumentAsync(
new IManageUploadRequest(tenantId, command.FolderId, command.Name,
command.Extension, command.Description, command.CustomFields),
stream, ct);

具体 DTO 构造参数应以源码版本为准;关键点是 TenantId 和 stream 的可信来源,而不是让 API 直接映射 Port DTO。

3. 上传边界

Adapter 验证租户、文件夹、名称和扩展名的基本非空,但没有验证:

  • stream 非 null、可读或剩余长度;
  • 文件大小上限;
  • magic bytes、MIME 与扩展名一致;
  • 恶意内容扫描状态;
  • 扩展名 allowlist;
  • 重复请求/idempotency key;
  • 供应商超时后的未知结果。

非 seekable stream 的大小被报告为 0;null stream 会在读取 CanSeek 时抛错。调用方必须预验证并提供明确的内容长度。

4. 下载的内存模型

SDK 返回流后,Adapter 把全部内容复制到 MemoryStream 再返回。这简化了客户端生命周期,却把文件大小直接变成进程连续内存压力。

iManage stream

unbounded MemoryStream

Result Stream

HTTP / owner consumer

当前没有最大下载大小、流式 backpressure 或临时对象存储。CopyToAsync 的 IO 异常也不在只捕获 IManageException 的 catch 内。

GA 可选择:受限小文件缓冲;直接流式且延长客户端生命周期;或先写入受控临时对象存储。无论选择哪种,都要限定字节、超时、并发和释放所有权。

5. 下载用例示例

有界下载响应
// ① 文档权限和租户范围先由 owner 判断。
await policy.AuthorizeDownloadAsync(tenantId, documentId, version, ct);
Result<Stream> downloaded =
await archive.DownloadDocumentAsync(tenantId, documentId, version, ct);
if (downloaded.IsFailure)
return Problem(downloaded.Error);
// ② 当前返回 MemoryStream,调用方拥有并必须释放。
await using Stream content = downloaded.Value!;
// ③ 生产实现仍要执行下载上限、审计和安全响应头策略。
return await responseWriter.WriteAttachmentAsync(content, safeFileName, ct);

6. 签出与并发

Checkout 是远端状态变更。当前请求没有 idempotency key、expected version 或本地操作记录;网络超时后无法判断供应商是否已锁定。

重复 Checkout 的冲突语义完全由 SDK 错误决定,没有规范化为 AlreadyCheckedOut/OwnedByOther/UnknownOutcome。重试前应查询远端状态,而不是盲目再次执行。

7. 签入语义

Port 注释称 null stream 仅释放锁,Adapter 实际把 null 转成 Stream.Null 并调用 SDK CheckinAsync。仓库内没有合同测试证明厂商把空流解释为“只释放锁”。

在验证前,文档不能把该行为当保证。更安全的契约应拆分 UndoCheckout/ReleaseLock 与 CheckinNewVersion,避免以空流编码不同命令。

签入也没有 expected checked-out user/version、内容哈希或幂等键。owner 应保存 operation ID、远端文档 ID、开始时间和最终结果用于对账。

8. 自定义字段

Adapter 只识别 Custom1 到 Custom30,大小写不敏感;未知 key 被静默忽略。静默丢弃会让调用者误以为元数据已归档。

Application 应使用强类型映射或租户配置 schema,拒绝未知/重复字段,限制长度和敏感数据,并记录映射版本。

9. 时间与 DTO 映射

文档时间用 new DateTimeOffset(dateTime, TimeSpan.Zero) 映射,隐含供应商 DateTime 已是 UTC。若 Kind 为 Local,该构造可能抛异常;若 Unspecified,则只是把未证实时间标为 UTC。

需要用 SDK 契约和合同测试确定时区,显式 normalize。历史排序、审计和 SLA 都依赖这一点。

10. 错误分类

Adapter 只捕获 IManageException,错误 Code 拼成 ToolConnectors.iManage.ProviderFailed:{vendorCode} 并返回原始消息。

问题包括动态错误码破坏客户端稳定性、供应商信息泄漏、IO/取消/映射异常逃逸。建议稳定分类:Unavailable、UnauthorizedToProvider、NotFound、Conflict、RateLimited、Timeout、UnknownOutcome、ProviderFailed;厂商码只进受限诊断字段。

11. 审计、幂等与对账

Adapter 有意不写审计,但当前不存在拥有这些用例的 Application 层。因此系统暂时没有“谁为哪个案件上传/签出/签入”的统一审计。

状态变更应在调用前预留 operation record,使用业务 idempotency key;成功后保存远端 ID/版本;超时标 Unknown;后台通过文档详情/历史对账;审计引用同一 operation ID。

12. 测试与运维

合同测试覆盖租户路由、空/不可读/非 seekable/巨大 stream、恶意扩展、Custom 字段、时区 Kind、SDK 错误分类、下载释放、取消、Checkout 冲突、Checkin null 语义和未知结果对账。

监控调用数、延迟、字节、冲突、限流、未知结果、对账积压、凭据错误与租户客户端创建失败;日志不记录文件内容、名称、Custom 值或 token。

上线评审还应逐项回答:

  • 单文件和单租户的字节上限是多少;
  • 下载中断时谁释放供应商客户端与本地流;
  • Checkout 超时由哪个任务对账;
  • 重复 operation ID 返回原结果还是冲突;
  • 供应商历史时间按哪个时区解释;
  • 凭据轮换是否影响在途客户端;
  • 文档删除和保留由哪个系统拥有;
  • sandbox 与生产 SDK 版本是否一致。
Terminal window
rg -n "UploadDocumentAsync|DownloadDocumentAsync|CheckoutAsync|CheckinAsync|Custom[0-9]" \
src/Platform/ToolConnectors -g '*.cs'

返回 Tool Connectors 总览

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%