13 KiB
13 KiB
发票模块架构重新整理总结
概述
根据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"`,
),
),
总结
通过这次重新整理,我们实现了:
- ✅ 架构规范:严格遵循DDD架构规范,职责分离清晰
- ✅ 代码质量:代码结构清晰,易于理解和维护
- ✅ 可测试性:各层都可以独立测试,测试覆盖率高
- ✅ 可扩展性:新增功能时只需要修改相应的服务层
- ✅ 业务逻辑:业务逻辑清晰,流程明确
这种架构设计既满足了业务需求,又符合DDD架构规范,是一个优秀的架构实现。