366 lines
13 KiB
Markdown
366 lines
13 KiB
Markdown
|
|
# 发票模块架构重新整理总结
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
根据DDD(领域驱动设计)架构规范,重新整理了发票模块的架构,明确了各层的职责分工和写法规范。通过这次重新整理,实现了更清晰的架构分层、更规范的代码组织和更好的可维护性。
|
|||
|
|
|
|||
|
|
## 架构分层和职责
|
|||
|
|
|
|||
|
|
### 1. 领域服务层(Domain Service Layer)
|
|||
|
|
|
|||
|
|
#### 职责定位
|
|||
|
|
- **纯业务规则处理**:只处理领域内的业务规则和计算逻辑
|
|||
|
|
- **无外部依赖**:不依赖仓储、外部服务或其他领域
|
|||
|
|
- **可测试性强**:纯函数式设计,易于单元测试
|
|||
|
|
|
|||
|
|
#### `InvoiceDomainService` 接口设计
|
|||
|
|
```go
|
|||
|
|
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` 接口设计
|
|||
|
|
```go
|
|||
|
|
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` 接口设计
|
|||
|
|
```go
|
|||
|
|
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. 领域服务实现
|
|||
|
|
```go
|
|||
|
|
// 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. 聚合服务实现
|
|||
|
|
```go
|
|||
|
|
// 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. 应用服务实现
|
|||
|
|
```go
|
|||
|
|
// 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. 领域服务配置
|
|||
|
|
```go
|
|||
|
|
// 发票领域服务
|
|||
|
|
fx.Annotate(
|
|||
|
|
finance_service.NewInvoiceDomainService,
|
|||
|
|
fx.ResultTags(`name:"domainService"`),
|
|||
|
|
),
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 聚合服务配置
|
|||
|
|
```go
|
|||
|
|
// 发票聚合服务
|
|||
|
|
fx.Annotate(
|
|||
|
|
finance_service.NewInvoiceAggregateService,
|
|||
|
|
fx.ParamTags(
|
|||
|
|
`name:"invoiceRepo"`,
|
|||
|
|
`name:"userInvoiceInfoRepo"`,
|
|||
|
|
`name:"domainService"`,
|
|||
|
|
`name:"eventPublisher"`,
|
|||
|
|
),
|
|||
|
|
fx.ResultTags(`name:"aggregateService"`),
|
|||
|
|
),
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 应用服务配置
|
|||
|
|
```go
|
|||
|
|
// 发票应用服务
|
|||
|
|
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架构规范,是一个优秀的架构实现。
|