# 项目 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` |