You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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并长期存储"的需求,建议这样划分职责:

  1. 应用层:接收下载请求,调用DocumentPackageRepository获取完整的DocumentPackage实体(包含关联的Document和Requirements)
  2. 基础设施层:在IDocumentStorageService的CreatePackageArchiveAsync方法中实现Zip打包逻辑,同时负责将归档文件存入长期存储(比如Docuware的归档专用柜)
  3. 领域层: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 09:19:24