DDD架构下工单文档管理系统的文件操作职责划分咨询
DDD文档管理系统:仓储职责划分优化与领域事件应用建议
首先,你的制造业工单文档交付场景非常适合用DDD梳理,目前遇到的职责混乱是DDD落地中常见的痛点,我们一步步拆解优化:
背景回顾
你正在搭建遵循DDD原则的文档管理系统,核心围绕工单(Work Order)的文档上传、分配与交付,核心限界上下文包含DocumentPackage、Document、Requirements等实体。当前的主要困惑点:
DocumentRepository违反单一职责原则,同时处理数据库持久化和文件存储操作- 文件上传/下载/归档逻辑分散在多层,职责边界模糊
- 希望了解领域事件在该场景下的具体落地方式
一、仓储职责拆分:解决SRP问题
你的核心问题在于DocumentRepository同时承担了领域实体持久化和外部文件存储两个完全独立的职责,这不仅违反SRP,还导致领域实体依赖基础设施服务(比如DocuwareRepository),破坏了领域层的独立性。
1. 拆分独立的文件存储服务
把文件相关操作(上传、下载、归档)抽成专门的基础设施服务,定义IDocumentStorageService接口:
// 基础设施层:专注文件存储/处理的服务 public interface IDocumentStorageService { // 上传文件到外部存储(如Docuware)并返回存储元数据 Task<DocumentStorageResult> UploadAsync(Guid documentId, FileInfo fileInfo, CancellationToken cancellationToken); // 根据文档ID下载文件 Task<FileStream> DownloadAsync(Guid documentId, CancellationToken cancellationToken); // 根据DocumentPackage生成Zip归档 Task<Stream> CreatePackageArchiveAsync(DocumentPackage package, CancellationToken cancellationToken); // 补偿操作:删除已上传的文件 Task DeleteAsync(Guid documentId, CancellationToken cancellationToken); } // 存储结果DTO,传递外部存储元数据 public record DocumentStorageResult(Guid DocuwareDocumentId, Guid CabinetId);
2. 简化DocumentRepository,专注领域实体持久化
修改后的DocumentRepository只负责数据库操作,不再依赖外部存储服务:
// 领域仓储层:仅处理Document和关联存储信息的DB操作 public class DocumentRepository : IDocumentRepository { private readonly YourDbContext _context; public DocumentRepository(YourDbContext context) { _context = context; } public async Task<Document> Get(Guid id, CancellationToken cancellationToken) { return await _context.Documents.FirstOrDefaultAsync(x => x.Id == id, cancellationToken); } public async Task AddAsync(Document document, DocumentStorageResult storageResult, CancellationToken cancellationToken) { var storageInfo = new DocumentStorageInfo { DocuwareId = storageResult.DocuwareDocumentId, DocuwareFileCabinetId = storageResult.CabinetId, Document = document }; _context.Documents.Add(document); _context.DocumentStorageInfos.Add(storageInfo); await _context.SaveChangesAsync(cancellationToken); } // 其他DB相关方法... }
3. 调整UploadCommandHandler,协调各服务职责
现在Handler的角色是流程协调者,负责调用不同服务完成上传流程,并保证事务一致性:
public class UploadDocumentCommandHandler : IRequestHandler<UploadDocumentCommand, AppResponse> { private readonly IDocumentRepository _documentRepo; private readonly IDocumentPackageRepository _packageRepo; private readonly IDocumentStorageService _storageService; private readonly IUnitOfWork _unitOfWork; // 引入工作单元管理事务 private readonly IEventBus _eventBus; // 事件总线,用于发布领域事件 public UploadDocumentCommandHandler( IDocumentRepository documentRepo, IDocumentPackageRepository packageRepo, IDocumentStorageService storageService, IUnitOfWork unitOfWork, IEventBus eventBus) { _documentRepo = documentRepo; _packageRepo = packageRepo; _storageService = storageService; _unitOfWork = unitOfWork; _eventBus = eventBus; } public async Task<AppResponse> Handle(UploadDocumentCommand request, CancellationToken cancellationToken) { using var transaction = await _unitOfWork.BeginTransactionAsync(cancellationToken); try { // 1. 获取目标工单文档包(领域层操作) var package = await _packageRepo.Get(request.WorkOrderNumber, cancellationToken); // 2. 创建Document领域实体(工厂方法保证实体完整性) var document = DocumentFactory.CreateFromFile(request.DocumentId, request.FileInfo); // 3. 先上传文件到外部存储(基础设施层操作) var storageResult = await _storageService.UploadAsync(document.Id, request.FileInfo, cancellationToken); // 4. 持久化Document和存储元数据(仓储层操作) await _documentRepo.AddAsync(document, storageResult, cancellationToken); // 5. 更新DocumentPackage关联关系(领域层操作,封装业务规则) if (request.RequirementId.HasValue) { // 这里可以在Assign方法内部添加规则验证,比如检查文件是否符合需求格式 package.AssignDocumentToRequirement(document, request.RequirementId.Value); } else { package.AddUnassignedDocument(document); } await _packageRepo.SaveAsync(package, cancellationToken); // 6. 提交事务 await transaction.CommitAsync(cancellationToken); // 7. 发布领域事件,触发后续异步操作 await _eventBus.PublishAsync(new DocumentUploadedEvent( document.Id, package.Id, request.RequirementId, DateTime.UtcNow), cancellationToken); return AppResponse.Ok(); } catch (AppException ex) { await transaction.RollbackAsync(cancellationToken); // 补偿操作:如果文件已上传但DB操作失败,删除外部存储中的文件 if (request.DocumentId != Guid.Empty) { await _storageService.DeleteAsync(request.DocumentId, cancellationToken); } return AppResponse.Exception(ex); } } }
二、文档包下载归档功能的职责定位
对于"按Requirements层级打包Zip并长期存储"的需求,建议这样划分职责:
- 应用层:接收下载请求,调用
DocumentPackageRepository获取完整的DocumentPackage实体(包含关联的Document和Requirements) - 基础设施层:在
IDocumentStorageService的CreatePackageArchiveAsync方法中实现Zip打包逻辑,同时负责将归档文件存入长期存储(比如Docuware的归档专用柜) - 领域层:
DocumentPackage实体提供GetValidDocumentsForArchive()方法,封装"哪些文档需要被归档"的业务规则(比如排除已失效的文档)
完成归档后,发布DocumentPackageArchivedEvent,触发后续操作(比如通知客户归档已生成、更新工单交付状态)。
三、领域事件的具体应用建议
在你的场景中,领域事件可以很好地解决异步操作、跨上下文通信和一致性问题,推荐以下核心事件:
1. DocumentUploadedEvent - 文档上传完成事件
当文档成功上传并关联到工单包后发布,可用于:
- 通知生产团队/客户:"某工单的某文档已上传"
- 触发工单状态更新(比如从"待上传"变为"部分完成")
- 同步文档信息到ERP/MES等外部系统
public record DocumentUploadedEvent( Guid DocumentId, Guid DocumentPackageId, Guid? RequirementId, DateTime UploadedAt) : IDomainEvent;
2. DocumentPackageArchivedEvent - 文档包归档完成事件
当Zip归档生成并持久化后发布,可用于:
- 触发客户交付流程(比如发送归档下载链接)
- 记录审计日志,归档操作的时间、操作人员等
- 更新工单的交付状态为"已归档"
public record DocumentPackageArchivedEvent( Guid DocumentPackageId, string ArchiveStoragePath, DateTime ArchivedAt, string OperatorId) : IDomainEvent;
3. DocumentAssignedToRequirementEvent - 文档分配到需求事件
当文档被分配到特定Requirements时发布,可用于:
- 验证需求是否已满足(比如某需求要求的所有文档都已上传)
- 通知负责该需求的人员进行审核
事件发布与处理注意事项
- 事件必须在事务提交之后发布,避免事务回滚导致的无效事件
- 可以用MediatR的
INotification实现轻量级事件总线,或者自定义事件总线 - 事件处理器放在应用层或基础设施层,不要在领域层处理外部依赖操作
额外的DDD实践小贴士
- 确保
DocumentPackage的业务规则都封装在实体内部:比如AssignDocumentToRequirement方法应该检查文件格式、大小是否符合需求,不符合则抛出领域异常 - 引入**工作单元(Unit of Work)**模式,统一管理多个仓储的事务,避免手动处理多个DbContext的事务
- 领域实体中禁止依赖基础设施服务:所有外部依赖都通过应用层或仓储层传递,保持领域层的纯净性
内容的提问来源于stack exchange,提问作者ErpaDerp
相关产品推荐
相关产品推荐

