fshop

command
v1.9.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 24, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

FShop - DDD 架构教学示例

项目简介

FShop 是一个完整的电商系统示例,展示如何使用 Freedom Framework 构建基于 DDD(领域驱动设计)架构的企业级应用。本项目通过电商场景(商品、购物车、订单、发货)演示 DDD 的核心概念和最佳实践。

学习目标

通过本示例,你将学习到:

  • DDD 分层架构的实际应用
  • 聚合、实体、工厂的设计与实现
  • CQS(命令查询分离)模式
  • 领域事件驱动架构
  • 资源库模式与依赖倒置
  • 事务处理与数据一致性

项目结构

fshop/
├── adapter/              # 适配器层(外层)
│   ├── controller/      # HTTP 控制器
│   ├── repository/      # 资源库实现
│   └── consumer/       # 事件消费者
├── domain/              # 领域层(核心层)
│   ├── aggregate/       # 聚合(CQS 模式)
│   ├── entity/          # 实体
│   ├── po/              # 持久化对象
│   ├── vo/              # 值对象
│   ├── event/           # 领域事件
│   └── dependency/      # 依赖接口
└── infra/               # 基础设施层(内层)
    └── domainevent/     # 领域事件基础设施
架构分层说明
层级 职责 说明
Adapter 适配外部 处理 HTTP 请求、数据库访问、消息队列
Domain 核心业务 封装业务逻辑,不依赖外部实现
Infra 基础设施 提供通用能力(事件、缓存、事务)

步骤一:数据库与 PO 生成

1.1 创建数据库表
-- 商品表
CREATE TABLE goods (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    price INT,
    stock INT,
    tag VARCHAR(20),
    created_at DATETIME,
    updated_at DATETIME
);

-- 购物车表
CREATE TABLE cart (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    goods_id INT,
    num INT,
    created_at DATETIME
);

-- 订单表
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(50),
    user_id INT,
    total_price INT,
    status VARCHAR(20),
    created_at DATETIME
);
1.2 生成 PO 代码

使用 Freedom 提供的 new-po 工具自动生成持久化对象:

freedom new-po --dsn "root:password@tcp(127.0.0.1:3306)/fshop?charset=utf8"

生成的文件位于 domain/po/ 目录:

// domain/po/goods.go
type Goods struct {
    ID        int       `gorm:"primaryKey;column:id"`
    Name      string    `gorm:"column:name"`
    Price     int       `gorm:"column:price"`
    Stock     int       `gorm:"column:stock"`
    Tag       string    `gorm:"column:tag"`
    CreatedAt time.Time `gorm:"column:created_at"`
    UpdatedAt time.Time `gorm:"column:updated_at"`
    changes   map[string]interface{}
}

// 自动生成的字段更新方法
func (obj *Goods) SetName(name string) {
    obj.Name = name
    obj.Update("name", name)
}

func (obj *Goods) AddStock(stock int) {
    obj.Stock += stock
    obj.Update("stock", gorm.Expr("stock + ?", stock))
}

详细说明:参见 PO 指南


步骤二:创建实体

实体继承 freedom.Entity 和 PO 结构,封装业务行为。

// domain/entity/goods.go
package entity

import (
    "strconv"
    "github.com/8treenet/freedom"
    "github.com/8treenet/freedom/example/fshop/domain/po"
)

// Goods 商品实体
type Goods struct {
    freedom.Entity    // 必须继承
    po.Goods         // 继承 PO 结构
}

// Identity 返回唯一标识
func (g *Goods) Identity() string {
    return strconv.Itoa(g.ID)
}

// MarkedTag 为商品打标签
func (g *Goods) MarkedTag(tag string) error {
    if tag != "HOT" && tag != "NEW" && tag != "NONE" {
        return errors.New("Tag doesn't exist")
    }
    g.SetTag(tag)
    return nil
}
// domain/entity/cart.go
package entity

type Cart struct {
    freedom.Entity
    po.Cart
}

// Identity 返回唯一标识
func (c *Cart) Identity() string {
    return strconv.Itoa(c.ID)
}

// AddNum 增加商品数量
func (c *Cart) AddNum(num int) {
    c.Num += num
}

详细说明:参见 DDD 指南 - 实体


步骤三:创建聚合(CQS 模式)

聚合基于 CQS(命令查询分离)原则,将写操作(Command)和读操作(Query)分离。

3.1 定义依赖接口
// domain/dependency/dependency.go
package dependency

type CartRepo interface {
    New(userID, goodsID, num int) (*entity.Cart, error)
    FindByGoodsID(userID, goodsID int) (*entity.Cart, error)
    Save(cart *entity.Cart) error
    FindAll(userID int) ([]*entity.Cart, error)
    DeleteAll(userID int) error
}

type GoodsRepo interface {
    Get(id int) (*entity.Goods, error)
    Save(goods *entity.Goods) error
}
3.2 创建工厂

工厂负责创建聚合根(Command/Query),框架自动注入依赖。

// domain/aggregate/cart_factory.go
func init() {
    freedom.Prepare(func(initiator freedom.Initiator) {
        initiator.BindFactory(func() *CartFactory {
            return &CartFactory{}
        })
    })
}

// CartFactory 购物车聚合工厂
type CartFactory struct {
    UserRepo  dependency.UserRepo
    CartRepo  dependency.CartRepo
    GoodsRepo dependency.GoodsRepo
}

// NewCartAddCmd 创建添加购物车命令
func (f *CartFactory) NewCartAddCmd(goodsID, userID int) (*CartAddCmd, error) {
    // 验证用户
    user, e := f.UserRepo.Get(userID)
    if e != nil {
        return nil, e
    }

    // 验证商品
    goods, e := f.GoodsRepo.Get(goodsID)
    if e != nil {
        return nil, e
    }

    cmd := &CartAddCmd{
        cartRepo: f.CartRepo,
    }
    cmd.User = *user
    cmd.goods = *goods
    return cmd, nil
}
3.3 创建命令(Command)

命令负责修改系统状态。

// domain/aggregate/cart_add_cmd.go
// CartAddCmd 添加购物车聚合根
type CartAddCmd struct {
    entity.User          // 继承 User 实体
    goods    entity.Goods // 包含 Goods 实体
    cartRepo dependency.CartRepo
}

// Run 执行命令
func (cmd *CartAddCmd) Run(goodsNum int) error {
    // 库存验证
    if goodsNum > cmd.goods.Stock {
        return errors.New("库存不足")
    }

    // 查找购物车中是否已有该商品
    if cartEntity, err := cmd.cartRepo.FindByGoodsID(cmd.User.ID, cmd.goods.ID); err == nil {
        // 已存在,增加数量
        cartEntity.AddNum(goodsNum)
        return cmd.cartRepo.Save(cartEntity)
    }

    // 创建新购物车项
    _, e := cmd.cartRepo.New(cmd.User.ID, cmd.goods.ID, goodsNum)
    return e
}
3.4 创建查询(Query)

查询负责读取数据,不修改状态。

// domain/aggregate/cart_item_query.go
// CartItemQuery 购物车查询聚合根
type CartItemQuery struct {
    entity.User
    allCart  []*entity.Cart
    goodsMap map[int]*entity.Goods
}

// VisitAllItem 遍历所有商品
func (query *CartItemQuery) VisitAllItem(f func(id, goodsId int, goodsName string, goodsNum, totalPrice int)) {
    for i := 0; i < len(query.allCart); i++ {
        goodsEntity := query.goodsMap[query.allCart[i].GoodsID]
        f(query.allCart[i].ID, goodsEntity.ID, goodsEntity.Name,
          query.allCart[i].Num, query.allCart[i].Num*goodsEntity.Price)
    }
}

// AllItemTotalPrice 计算总价
func (query *CartItemQuery) AllItemTotalPrice() (totalPrice int) {
    for i := 0; i < len(query.allCart); i++ {
        goodsEntity := query.goodsMap[query.allCart[i].GoodsID]
        totalPrice += query.allCart[i].Num * goodsEntity.Price
    }
    return
}

// MarshalJSON JSON 序列化
func (query *CartItemQuery) MarshalJSON() ([]byte, error) {
    var obj struct {
        TotalPrice int `json:"totalPrice"`
        Items      []struct {
            ID         int    `json:"id"`
            GoodsID    int    `json:"goodsId"`
            GoodsName  string `json:"goodsName"`
            GoodsNum   int    `json:"goodsNum"`
            TotalPrice int    `json:"totalPrice"`
        }
    }
    obj.TotalPrice = query.AllItemTotalPrice()
    for i := 0; i < len(query.allCart); i++ {
        goodsEntity := query.goodsMap[query.allCart[i].GoodsID]
        obj.Items = append(obj.Items, struct {
            ID         int
            GoodsID    int
            GoodsName  string
            GoodsNum   int
            TotalPrice int
        }{
            query.allCart[i].ID,
            goodsEntity.ID,
            goodsEntity.Name,
            query.allCart[i].Num,
            query.allCart[i].Num * goodsEntity.Price,
        })
    }
    return json.Marshal(obj)
}

详细说明:参见 DDD 指南 - 聚合


步骤四:创建服务层

服务层协调领域对象,实现业务流程编排。

// domain/cart.go
func init() {
    freedom.Prepare(func(initiator freedom.Initiator) {
        initiator.BindService(func() *CartService {
            return &CartService{}
        })
        initiator.InjectController(func(ctx freedom.Context) (service *CartService) {
            initiator.FetchService(ctx, &service)
            return
        })
    })
}

// CartService 领域服务
type CartService struct {
    Worker      freedom.Worker
    CartRepo    dependency.CartRepo
    CartFactory *aggregate.CartFactory
}

// Add 添加商品到购物车
func (c *CartService) Add(userID, goodsID, goodsNum int) error {
    // 使用工厂创建命令
    cmd, e := c.CartFactory.NewCartAddCmd(goodsID, userID)
    if e != nil {
        return e
    }
    return cmd.Run(goodsNum)
}

// Items 查询购物车商品
func (c *CartService) Items(userID int) (json.Marshaler, error) {
    // 使用工厂创建查询
    query, e := c.CartFactory.NewCartItemQuery(userID)
    if e != nil {
        return nil, e
    }
    return query, nil
}

详细说明:参见 Service 指南


步骤五:创建控制器

控制器处理 HTTP 请求,调用服务层。

// adapter/controller/cart.go
func init() {
    freedom.Prepare(func(initiator freedom.Initiator) {
        initiator.BindController("/cart", &CartController{})
    })
}

// CartController 购物车控制器
type CartController struct {
    Worker    freedom.Worker
    CartSev   *domain.CartService
    Request   *infra.Request
}

// Post 添加商品到购物车
func (c *CartController) Post() freedom.Result {
    var req vo.CartAddReq
    e := c.Request.ReadJSON(&req, true)
    if e != nil {
        return &infra.JSONResponse{Error: e}
    }

    e = c.CartSev.Add(req.UserID, req.GoodsID, req.GoodsNum)
    return &infra.JSONResponse{Error: e}
}

// GetItems 获取购物车商品列表
func (c *CartController) GetItems() freedom.Result {
    userID, err := c.Worker.IrisContext().URLParamInt("userId")
    if err != nil {
        return &infra.JSONResponse{Error: err}
    }

    vo, err := c.CartSev.Items(userID)
    if err != nil {
        return &infra.JSONResponse{Error: err}
    }
    return &infra.JSONResponse{Object: vo}
}

详细说明:参见 路由指南


步骤六:领域事件

6.1 定义事件
// domain/event/shop_goods.go
type ShopGoods struct {
    UserID    int
    OrderNO   string
    GoodsID   int
    GoodsNum  int
    GoodsName string
}
6.2 实体发布事件
// domain/entity/user.go
func (u *User) ChangePassword(newPassword, oldPassword string) error {
    if u.Password != oldPassword {
        return errors.New("Password error")
    }
    u.SetPassword(newPassword)

    // 发布领域事件
    u.AddPubEvent(&event.ChangePassword{
        UserID:      u.ID,
        NewPassword: u.Password,
        OldPassword: oldPassword,
    })
    return nil
}
6.3 事件基础设施
// infra/domainevent/event_transaction.go
type EventTransaction struct {
    txManager *transaction.Transaction
    eventBus  *eventManager
}

// Execute 执行事务并处理事件
func (e *EventTransaction) Execute(fn func() error) error {
    // 1. 执行事务
    err := e.txManager.Execute(fn)
    if err != nil {
        return err
    }

    // 2. 事务成功后推送事件
    return e.eventBus.push()
}

步骤七:事务处理

事务组件保证多个数据库操作的原子性。

// domain/aggregate/cart_shop_cmd.go
type CartShopCmd struct {
    orderRepo   dependency.OrderRepo
    goodsRepo   dependency.GoodsRepo
    cartRepo    dependency.CartRepo
    tx          *domainevent.EventTransaction
}

// Shop 购物车结算
func (cmd *CartShopCmd) Shop() error {
    return cmd.tx.Execute(func() error {
        // 1. 修改商品库存
        for _, cart := range cmd.allCartEntity {
            goods := cmd.goodsEntityMap[cart.GoodsID]
            goods.AddStock(-cart.Num)
            if err := cmd.goodsRepo.Save(goods); err != nil {
                return err
            }
        }

        // 2. 创建订单
        order, err := cmd.orderRepo.New()
        if err != nil {
            return err
        }

        // 3. 清空购物车
        if err := cmd.cartRepo.DeleteAll(cmd.userEntity.ID); err != nil {
            return err
        }

        return nil
    })
}

步骤八:资源库实现

资源库实现依赖接口,负责数据访问。

// adapter/repository/cart.go
type CartRepository struct {
    freedom.Repository
}

func (repo *CartRepository) New(userID, goodsID, num int) (*entity.Cart, error) {
    cart := &entity.Cart{}
    cart.UserID = userID
    cart.GoodsID = goodsID
    cart.Num = num
    _, err := createCart(repo, cart.PO())
    return cart, err
}

func (repo *CartRepository) FindByGoodsID(userID, goodsID int) (*entity.Cart, error) {
    cart, err := findCartByMap(repo, map[string]interface{}{
        "user_id":  userID,
        "goods_id": goodsID,
    })
    if err != nil {
        return nil, err
    }
    return &entity.Cart{Cart: cart}, nil
}

学习路径建议

  1. 基础理解:阅读 PO 指南,了解数据持久化
  2. 领域建模:阅读 DDD 指南,理解实体、聚合、工厂
  3. 服务层:阅读 Service 指南,学习业务编排
  4. 路由开发:阅读 路由指南,掌握 HTTP 接口
  5. 实践项目:参考本项目代码,动手实现一个简单的 DDD 应用

核心概念总结

概念 说明 示例
实体(Entity) 有唯一标识的对象,封装业务行为 User、Goods、Cart
聚合(Aggregate) 一组相关实体,由聚合根管理 CartAddCmd、CartItemQuery
工厂(Factory) 创建聚合根,封装创建逻辑 CartFactory、ShopFactory
服务(Service) 协调领域对象,实现业务流程 CartService、OrderService
资源库(Repository) 数据访问抽象,依赖倒置 CartRepo、GoodsRepo
领域事件(Event) 重要状态变化的通知 ShopGoods、OrderPay

Documentation

Overview

Code generated by 'freedom new-project fshop'

Directories

Path Synopsis
adapter
po
Package po generated by 'freedom new-po'
Package po generated by 'freedom new-po'
vo
Package infra code generated by 'freedom new-project base'
Package infra code generated by 'freedom new-project base'

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL