Files
junhong_cmp_fiber/docs/7月迭代/DDD规范.md
2026-07-13 12:01:18 +09:00

11 KiB
Raw Blame History

项目 DDD 设计规范

务实型 DDD——不是学院派不强制 Event Sourcing不追求完美追求可维护、可扩展、AI 辅助友好。
基准文档:docs/改造方向.md(绞杀者模式)


一、为什么用 DDD / 什么不是 DDD

现状问题

  • order/service.go 3031 行:订单创建、支付、佣金计算、代购、库存扣减全混一起
  • Model 只是 GORM 映射,没有业务行为(贫血模型)
  • 新增业务联动必须修改原有 Service牵一发动全身

目标

不是重写,是渐进替换

  1. 新功能按新结构写,不往旧 Service 加代码
  2. 旧功能迁移:每次迁移一个用例,跑通后再下一个
  3. AI 辅助结构清晰AI 生成代码时自动落入正确位置

二、目录结构

internal/
├── domain/                     ← 领域层(纯业务逻辑,不依赖 Fiber/GORM
│   ├── wallet/
│   │   ├── wallet.go           ← 聚合根(含业务方法)
│   │   ├── events.go           ← 领域事件定义
│   │   ├── repository.go       ← 仓储接口interface
│   │   └── vo.go               ← 值对象Money, CreditLimit
│   ├── approval/               ← 新:审批流领域
│   ├── notification/           ← 新:通知领域
│   ├── distribution/           ← 新:分销领域
│   └── package/                ← 套餐领域(迁移中)
│
├── application/                ← 应用层(用例,只做编排,不含业务判断)
│   ├── wallet/
│   │   ├── debit_wallet.go     ← 一个文件 = 一个用例
│   │   ├── credit_wallet.go
│   │   └── grant_credit.go     ← 新:授信
│   ├── approval/
│   │   ├── submit_approval.go
│   │   ├── advance_step.go
│   │   └── reject_approval.go
│   └── notification/
│       └── send_notification.go
│
├── infrastructure/             ← 基础设施层(实现 domain 里的 interface
│   └── persistence/            ← 原来的 store/postgres/(保持不动)
│
├── handler/                    ← 原有 handler保持不动
├── service/                    ← 原有 service保持不动新模块不在这里
└── store/                      ← 原有 store保持不动

过渡期规则

  • 旧模块:internal/service/xxx + internal/store/postgres/xxx
  • 新模块:internal/domain/xxx + internal/application/xxx
  • Handler 层统一调 Application 层(新)或 Service 层(旧)

三、领域模块划分

领域 聚合根 核心业务规则 状态
Asset资产 IotCard, Device 停复机条件、实名策略 迁移中
Order订单 Order 支付、退款、佣金触发 迁移中
Package套餐 Package, PackageUsage 生效条件、临期计算 迁移中
Wallet钱包 AgentWallet 余额扣减、信用额度、负余额 新功能用 DDD
Shop代理 Shop 层级关系、信用策略 部分迁移
Approval审批 ApprovalFlow 多级审批、推进、驳回 全新 DDD
Notification通知 Notification 分发、已读状态 全新 DDD
Distribution分销 DistributionRelation 发展人体系、二维码注册 全新 DDD

四、聚合根设计原则(富模型)

4.1 核心原则

业务规则住在聚合根里,外部只通过方法操作,不能直接改字段。

// ❌ 贫血模型(禁止)——所有判断在 Service 里
func (s *WalletService) Debit(ctx context.Context, walletID uint, amount int64) error {
    wallet, _ := s.store.Get(ctx, walletID)
    if wallet.Balance-wallet.FrozenBalance < amount {
        return errors.New(errors.CodeInsufficientBalance)
    }
    wallet.Balance -= amount
    s.store.Update(ctx, wallet)
}

// ✅ 富模型(推荐)——判断逻辑在聚合根里
func (w *AgentWallet) Debit(amount Money) error {
    available := w.Balance - w.FrozenBalance + w.CreditLimit  // 含信用额度
    if available < amount.Cents() {
        return ErrInsufficientBalance
    }
    w.Balance -= amount.Cents()
    w.recordEvent(WalletDebitedEvent{Amount: amount, BalanceAfter: w.Balance})
    return nil
}

// Application 层只做编排
func (uc *DebitWalletUseCase) Execute(ctx context.Context, cmd DebitCommand) error {
    wallet, err := uc.repo.GetByID(ctx, cmd.WalletID)
    if err != nil { return err }
    if err := wallet.Debit(cmd.Amount); err != nil { return err }
    return uc.repo.Save(ctx, wallet)
}

4.2 聚合根必须包含

type AggregateRoot struct {
    // 内嵌 GORM model过渡期
    gorm.Model
    
    // 业务字段...
    
    // 未发布领域事件内存中Save 后由框架分发)
    domainEvents []DomainEvent `gorm:"-" json:"-"`
}

// 获取并清空领域事件(由 Repository.Save 调用)
func (a *AggregateRoot) PopEvents() []DomainEvent {
    events := a.domainEvents
    a.domainEvents = nil
    return events
}

func (a *AggregateRoot) recordEvent(e DomainEvent) {
    a.domainEvents = append(a.domainEvents, e)
}

4.3 与现有 GORM 的共存

过渡期,聚合根 就是 现有的 GORM Model加业务方法不需要单独建领域对象

// internal/domain/wallet/wallet.go
// AgentWallet 钱包聚合根
// 直接复用并扩展现有 model.AgentWallet
type AgentWallet struct {
    model.AgentWallet                    // 内嵌原有 GORM 模型
    domainEvents  []DomainEvent `gorm:"-"`
}

// Debit 从钱包扣款(含信用额度)
func (w *AgentWallet) Debit(amount Money) error {
    available := w.Balance - w.FrozenBalance + w.CreditLimit
    if available < amount.Cents() {
        return ErrInsufficientBalance
    }
    w.Balance -= amount.Cents()
    w.recordEvent(WalletDebitedEvent{...})
    return nil
}

五、值对象设计

值对象:没有 ID靠值来判断相等不可变。

// internal/domain/wallet/vo.go

// Money 金额值对象(分为单位,防止浮点数精度问题)
type Money struct {
    cents int64
}

func NewMoney(cents int64) (Money, error) {
    if cents < 0 {
        return Money{}, ErrNegativeMoney
    }
    return Money{cents: cents}, nil
}

func (m Money) Cents() int64    { return m.cents }
func (m Money) Yuan() float64   { return float64(m.cents) / 100 }
func (m Money) Add(o Money) Money { return Money{cents: m.cents + o.cents} }

// CreditLimit 信用额度值对象
type CreditLimit struct {
    maxCents     int64  // 最大授信额度(分)
    allowNegative bool  // 是否允许负余额
}

六、领域事件(用 Asynq 实现)

领域事件不用消息队列,直接复用现有 Asynq 体系。

// internal/domain/wallet/events.go

// DomainEvent 领域事件接口
type DomainEvent interface {
    EventType() string
    OccurredAt() time.Time
}

// WalletDebitedEvent 钱包扣款事件
type WalletDebitedEvent struct {
    WalletID     uint
    ShopID       uint
    Amount       Money
    BalanceBefore int64
    BalanceAfter  int64
    RefType      string
    RefID        uint
    occurredAt   time.Time
}

func (e WalletDebitedEvent) EventType() string    { return "wallet.debited" }
func (e WalletDebitedEvent) OccurredAt() time.Time { return e.occurredAt }

Repository.Save 发布事件

// internal/infrastructure/persistence/wallet_repo.go
func (r *WalletRepository) Save(ctx context.Context, wallet *domainWallet.AgentWallet) error {
    err := r.db.WithContext(ctx).Save(&wallet.AgentWallet).Error
    if err != nil {
        return err
    }
    // 发布领域事件
    for _, evt := range wallet.PopEvents() {
        if err := r.publishEvent(ctx, evt); err != nil {
            r.logger.Error("领域事件发布失败", zap.Error(err), zap.String("type", evt.EventType()))
            // 事件发布失败不回滚业务(异步,可补偿)
        }
    }
    return nil
}

七、仓储接口

// internal/domain/wallet/repository.go

// WalletRepository 钱包仓储接口
type WalletRepository interface {
    GetByShopID(ctx context.Context, shopID uint, walletType string) (*AgentWallet, error)
    GetByID(ctx context.Context, id uint) (*AgentWallet, error)
    Save(ctx context.Context, wallet *AgentWallet) error
    SaveInTx(ctx context.Context, tx *gorm.DB, wallet *AgentWallet) error
}

实现在 internal/infrastructure/persistence/wallet_repo.go,复用现有 Store 逻辑。


八、应用服务(用例)

一个文件 = 一个用例。用例只做编排,不含业务判断。

// internal/application/wallet/debit_wallet.go

// DebitCommand 扣款命令
type DebitCommand struct {
    WalletID  uint
    Amount    domainWallet.Money
    RefType   string
    RefID     uint
    OperatorID uint
}

// DebitWalletUseCase 扣款用例
type DebitWalletUseCase struct {
    walletRepo domainWallet.WalletRepository
    txManager  TxManager
}

// Execute 执行扣款
func (uc *DebitWalletUseCase) Execute(ctx context.Context, cmd DebitCommand) error {
    return uc.txManager.RunInTx(ctx, func(tx *gorm.DB) error {
        wallet, err := uc.walletRepo.GetByID(ctx, cmd.WalletID)
        if err != nil {
            return err
        }
        if err := wallet.Debit(cmd.Amount); err != nil {
            return err
        }
        return uc.walletRepo.SaveInTx(ctx, tx, wallet)
    })
}

九、Handler 层调用方式

新模块Handler → Application UseCase不经过 Service 层)

// internal/handler/admin/wallet_handler.go
func (h *WalletHandler) GrantCredit(c *fiber.Ctx) error {
    var req dto.GrantCreditRequest
    // ... 参数解析

    cmd := walletApp.GrantCreditCommand{
        ShopID:    req.ShopID,
        MaxCredit: domainWallet.NewCreditLimit(req.MaxCreditCents),
        OperatorID: middleware.GetUserID(c),
    }
    if err := h.grantCreditUseCase.Execute(c.UserContext(), cmd); err != nil {
        return err
    }
    return response.OK(c, nil)
}

十、禁止事项

禁止 原因
在 Application 层做业务判断 业务规则必须在领域对象里
跨聚合直接访问另一聚合的字段 只能通过 ID 引用,运行时通过 Repository 加载
在领域层 import Fiber/GORM/Redis 领域层不依赖基础设施
在一个用例文件里实现多个业务流程 一文件一用例
领域事件发布失败时回滚业务 事件是异步补偿,不是事务一部分
在聚合根里调用 Repository 聚合根不依赖仓储

十一、本次迭代 DDD 落地计划

新模块 领域路径 用例路径
信用额度需求17 internal/domain/wallet/ internal/application/wallet/
审批流需求18/20/21 internal/domain/approval/ internal/application/approval/
站内消息需求18/22 internal/domain/notification/ internal/application/notification/
分销码需求16 internal/domain/distribution/ internal/application/distribution/
批量订购需求19 复用 Order 领域 internal/application/order/bulk_purchase.go