# 项目 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 核心原则

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

```go
// ❌ 贫血模型（禁止）——所有判断在 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 聚合根必须包含

```go
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（加业务方法），不需要单独建领域对象：

```go
// 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，靠值来判断相等，不可变。

```go
// 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 体系。

```go
// 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 发布事件**：

```go
// 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
}
```

---

## 七、仓储接口

```go
// 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 逻辑。

---

## 八、应用服务（用例）

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

```go
// 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 层）

```go
// 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` |
