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

358 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 项目 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` |