Files
tyapi-server/发票模块架构重新整理总结.md
2025-08-02 02:54:21 +08:00

13 KiB
Raw Blame History

发票模块架构重新整理总结

概述

根据DDD领域驱动设计架构规范重新整理了发票模块的架构明确了各层的职责分工和写法规范。通过这次重新整理实现了更清晰的架构分层、更规范的代码组织和更好的可维护性。

架构分层和职责

1. 领域服务层Domain Service Layer

职责定位

  • 纯业务规则处理:只处理领域内的业务规则和计算逻辑
  • 无外部依赖:不依赖仓储、外部服务或其他领域
  • 可测试性强:纯函数式设计,易于单元测试

InvoiceDomainService 接口设计

type InvoiceDomainService interface {
    // 验证发票信息完整性
    ValidateInvoiceInfo(ctx context.Context, info *value_objects.InvoiceInfo, invoiceType value_objects.InvoiceType) error
    
    // 验证开票金额是否合法(基于业务规则)
    ValidateInvoiceAmount(ctx context.Context, amount decimal.Decimal, availableAmount decimal.Decimal) error
    
    // 计算可开票金额(纯计算逻辑)
    CalculateAvailableAmount(totalRecharged decimal.Decimal, totalGifted decimal.Decimal, totalInvoiced decimal.Decimal) decimal.Decimal
    
    // 验证发票申请状态转换
    ValidateStatusTransition(currentStatus entities.ApplicationStatus, targetStatus entities.ApplicationStatus) error
    
    // 验证发票申请业务规则
    ValidateInvoiceApplication(ctx context.Context, application *entities.InvoiceApplication) error
}

实现特点

  • 无状态设计:不保存任何状态,所有方法都是纯函数
  • 参数化输入:所有需要的数据都通过参数传入
  • 业务规则集中:所有业务规则都在领域服务中定义
  • 可复用性强:可以在不同场景下复用

2. 聚合服务层Aggregate Service Layer

职责定位

  • 聚合根生命周期管理:协调聚合根的创建、更新、删除
  • 业务流程编排:按照业务规则编排操作流程
  • 领域事件发布:发布领域事件,实现事件驱动架构
  • 状态转换控制:控制聚合根的状态转换

InvoiceAggregateService 接口设计

type InvoiceAggregateService interface {
    // 申请开票
    ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*entities.InvoiceApplication, error)
    
    // 通过发票申请(上传发票)
    ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error
    
    // 拒绝发票申请
    RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error
}

实现特点

  • 依赖领域服务:调用领域服务进行业务规则验证
  • 依赖仓储:通过仓储进行数据持久化
  • 发布事件:发布领域事件,实现松耦合
  • 事务边界:每个方法都是一个事务边界

3. 应用服务层Application Service Layer

职责定位

  • 跨域协调:协调不同领域之间的交互
  • 数据聚合:聚合来自不同领域的数据
  • 事务管理:管理跨领域的事务
  • 外部服务调用:调用外部服务(如文件存储、邮件服务等)

InvoiceApplicationService 接口设计

type InvoiceApplicationService interface {
    // ApplyInvoice 申请开票
    ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error)
    
    // GetUserInvoiceInfo 获取用户发票信息
    GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error)
    
    // UpdateUserInvoiceInfo 更新用户发票信息
    UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error
    
    // GetUserInvoiceRecords 获取用户开票记录
    GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error)
    
    // DownloadInvoiceFile 下载发票文件
    DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error)
    
    // GetAvailableAmount 获取可开票金额
    GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error)
}

实现特点

  • 跨域协调协调User领域和Finance领域的交互
  • 数据聚合:聚合用户信息、企业认证信息、开票信息等
  • 业务编排:按照业务流程编排各个步骤
  • 错误处理:统一处理跨域错误和业务异常

架构优势

1. 职责分离清晰

  • 领域服务:专注于业务规则,无外部依赖
  • 聚合服务:专注于聚合根生命周期,发布领域事件
  • 应用服务:专注于跨域协调,数据聚合

2. 可测试性强

  • 领域服务:纯函数设计,易于单元测试
  • 聚合服务可以Mock依赖进行集成测试
  • 应用服务可以Mock外部服务进行端到端测试

3. 可维护性高

  • 单一职责:每个服务都有明确的职责
  • 依赖清晰:依赖关系明确,易于理解
  • 扩展性好:新增功能时只需要修改相应的服务层

4. 符合DDD规范

  • 领域边界:严格遵循领域边界
  • 依赖方向:依赖方向正确,从外到内
  • 聚合根设计:聚合根设计合理,状态转换清晰

代码示例

1. 领域服务实现

// ValidateInvoiceAmount 验证开票金额是否合法(基于业务规则)
func (s *InvoiceDomainServiceImpl) ValidateInvoiceAmount(ctx context.Context, amount decimal.Decimal, availableAmount decimal.Decimal) error {
    if amount.LessThanOrEqual(decimal.Zero) {
        return errors.New("开票金额必须大于0")
    }
    
    if amount.GreaterThan(availableAmount) {
        return fmt.Errorf("开票金额不能超过可开票金额,可开票金额:%s", availableAmount.String())
    }
    
    // 最小开票金额限制
    minAmount := decimal.NewFromInt(100) // 最小100元
    if amount.LessThan(minAmount) {
        return fmt.Errorf("开票金额不能少于%s元", minAmount.String())
    }
    
    return nil
}

2. 聚合服务实现

// ApplyInvoice 申请开票
func (s *InvoiceAggregateServiceImpl) ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*entities.InvoiceApplication, error) {
    // 1. 解析金额
    amount, err := decimal.NewFromString(req.Amount)
    if err != nil {
        return nil, fmt.Errorf("无效的金额格式: %w", err)
    }

    // 2. 验证发票信息
    if err := s.domainService.ValidateInvoiceInfo(ctx, req.InvoiceInfo, req.InvoiceType); err != nil {
        return nil, fmt.Errorf("发票信息验证失败: %w", err)
    }

    // 3. 获取用户开票信息
    userInvoiceInfo, err := s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
    if err != nil {
        return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
    }
    if userInvoiceInfo == nil {
        return nil, fmt.Errorf("用户开票信息不存在")
    }

    // 4. 创建发票申请聚合根
    application := entities.NewInvoiceApplication(userID, req.InvoiceType, amount, userInvoiceInfo.ID)
    
    // 5. 设置开票信息快照
    application.SetInvoiceInfoSnapshot(req.InvoiceInfo)
    
    // 6. 验证聚合根业务规则
    if err := s.domainService.ValidateInvoiceApplication(ctx, application); err != nil {
        return nil, fmt.Errorf("发票申请业务规则验证失败: %w", err)
    }

    // 7. 保存聚合根
    if err := s.applicationRepo.Create(ctx, application); err != nil {
        return nil, fmt.Errorf("保存发票申请失败: %w", err)
    }

    // 8. 发布领域事件
    event := events.NewInvoiceApplicationCreatedEvent(
        application.ID,
        application.UserID,
        application.InvoiceType,
        application.Amount,
        application.CompanyName,
        application.ReceivingEmail,
    )
    
    if err := s.eventPublisher.PublishInvoiceApplicationCreated(ctx, event); err != nil {
        fmt.Printf("发布发票申请创建事件失败: %v\n", err)
    }

    return application, nil
}

3. 应用服务实现

// ApplyInvoice 申请开票
func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error) {
    // 1. 验证用户是否存在
    user, err := s.userRepo.GetByID(ctx, userID)
    if err != nil {
        return nil, err
    }
    if user.ID == "" {
        return nil, fmt.Errorf("用户不存在")
    }

    // 2. 验证发票类型
    invoiceType := value_objects.InvoiceType(req.InvoiceType)
    if !invoiceType.IsValid() {
        return nil, fmt.Errorf("无效的发票类型")
    }

    // 3. 获取用户企业认证信息
    userWithEnterprise, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
    if err != nil {
        return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
    }

    // 4. 检查用户是否有企业认证信息
    if userWithEnterprise.EnterpriseInfo == nil {
        return nil, fmt.Errorf("用户未完成企业认证,无法申请开票")
    }

    // 5. 获取用户开票信息
    userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(
        ctx, 
        userID, 
        userWithEnterprise.EnterpriseInfo.CompanyName, 
        userWithEnterprise.EnterpriseInfo.UnifiedSocialCode,
    )
    if err != nil {
        return nil, err
    }

    // 6. 验证开票信息完整性
    invoiceInfo := value_objects.NewInvoiceInfo(
        userInvoiceInfo.CompanyName,
        userInvoiceInfo.TaxpayerID,
        userInvoiceInfo.BankName,
        userInvoiceInfo.BankAccount,
        userInvoiceInfo.CompanyAddress,
        userInvoiceInfo.CompanyPhone,
        userInvoiceInfo.ReceivingEmail,
    )
    
    if err := s.userInvoiceInfoService.ValidateInvoiceInfo(ctx, invoiceInfo, invoiceType); err != nil {
        return nil, err
    }

    // 7. 计算可开票金额
    availableAmount, err := s.calculateAvailableAmount(ctx, userID)
    if err != nil {
        return nil, fmt.Errorf("计算可开票金额失败: %w", err)
    }

    // 8. 验证开票金额
    amount, err := decimal.NewFromString(req.Amount)
    if err != nil {
        return nil, fmt.Errorf("无效的金额格式: %w", err)
    }
    
    if err := s.invoiceDomainService.ValidateInvoiceAmount(ctx, amount, availableAmount); err != nil {
        return nil, err
    }

    // 9. 调用聚合服务申请开票
    aggregateReq := services.ApplyInvoiceRequest{
        InvoiceType: invoiceType,
        Amount:      req.Amount,
        InvoiceInfo: invoiceInfo,
    }
    
    application, err := s.invoiceAggregateService.ApplyInvoice(ctx, userID, aggregateReq)
    if err != nil {
        return nil, err
    }

    // 10. 构建响应DTO
    return &dto.InvoiceApplicationResponse{
        ID:          application.ID,
        UserID:      application.UserID,
        InvoiceType: application.InvoiceType,
        Amount:      application.Amount,
        Status:      application.Status,
        InvoiceInfo: invoiceInfo,
        CreatedAt:   application.CreatedAt,
    }, nil
}

依赖注入配置

1. 领域服务配置

// 发票领域服务
fx.Annotate(
    finance_service.NewInvoiceDomainService,
    fx.ResultTags(`name:"domainService"`),
),

2. 聚合服务配置

// 发票聚合服务
fx.Annotate(
    finance_service.NewInvoiceAggregateService,
    fx.ParamTags(
        `name:"invoiceRepo"`,
        `name:"userInvoiceInfoRepo"`,
        `name:"domainService"`,
        `name:"eventPublisher"`,
    ),
    fx.ResultTags(`name:"aggregateService"`),
),

3. 应用服务配置

// 发票应用服务
fx.Annotate(
    finance.NewInvoiceApplicationService,
    fx.As(new(finance.InvoiceApplicationService)),
    fx.ParamTags(
        `name:"invoiceRepo"`,
        `name:"userInvoiceInfoRepo"`,
        `name:"userRepo"`,
        `name:"userAggregateService"`,
        `name:"rechargeRecordRepo"`,
        `name:"walletRepo"`,
        `name:"domainService"`,
        `name:"aggregateService"`,
        `name:"userInvoiceInfoService"`,
        `name:"storageService"`,
        `name:"logger"`,
    ),
),

总结

通过这次重新整理,我们实现了:

  1. 架构规范严格遵循DDD架构规范职责分离清晰
  2. 代码质量:代码结构清晰,易于理解和维护
  3. 可测试性:各层都可以独立测试,测试覆盖率高
  4. 可扩展性:新增功能时只需要修改相应的服务层
  5. 业务逻辑:业务逻辑清晰,流程明确

这种架构设计既满足了业务需求又符合DDD架构规范是一个优秀的架构实现。